(0) exportieren Drucken
Alle erweitern
Erweitern Minimieren

.NET-Konfigurationsdaten und Handler

Veröffentlicht: 04. Okt 2002 | Aktualisiert: 22. Jun 2004
Von Robert Kuzelj

Dieser Artikel zeigt den Umgang mit Konfigurationsdaten der .NET-Plattform. Der Leser erfährt, wie er eigene Daten in der Syntax des Konfigurationsdateiformats definiert und es werden Klassen vorgestellt, die es ermöglichen diese Daten auszulesen und als Objekte darzustellen.

Auf dieser Seite

 Konfigurationsformat
 Quick & Dirty
 Sektionen
 Erweiterte Sektionen
 Fallstricke
 Deklaratives
 IConfigurationSectionHandler
 Am Anfang...
 Eigene Objekte nutzen
 Skin-Handler
 Fazit

Wenn Sie eine Applikation ausliefern wollen, möchten Sie Ihren Anwendern meistens auch die Möglichkeit geben, diese Anwendung in gewissem Umfang den persönlichen Bedürfnissen anzupassen. Die Anpassungen können dabei beispielsweise Standardverzeichnisse, Farb- und Formgebung der Oberfläche oder Einstellungen der zu nutzenden Datenbankverbindungen umfassen.

Microsoft hat mit der .NET-Plattform auch eine neue Art der Konfigurationsdatenbereitstellung definiert. Nach den Ini-Dateien in Windows 3.1 und der Registry ab Windows 95 gibt es jetzt so genannte Konfigurationsdateien.

Dabei handelt es sich um XML-Dateien mit dem Namen der Applikation und der Endung .config, die im Hauptverzeichnis Ihrer Applikation liegen müssen, um als solche erkannt zu werden. Genauere Informationen zu Konfigurationsdateien finden Sie unter [Richter].

Vor- und Nachteile
Aber warum sollte man seine Konfigurationsdaten überhaupt in einer .config-Datei ablegen? Immerhin gibt es INI-Dateien und die Registry.
Der Vorteil liegt darin, dass XML ein Meta-Datenformat ist, mit dem sich beliebig komplexe Strukturen abbilden lassen. Gleichzeitig ist XML aber auch ein internationaler Standard.INI-Dateien haben dagegen ein triviales, zu geschlossenes Format und die Registry ist eine aufwändige proprietäreTechnologie.

Mit dem .NET Framework verzichtet Microsoft selbst auf die Speicherung von Konfigurationsinformationen in INI-Dateien und Registry, sondern setzt in vielen Bereichen auf XML config-Dateien. So müssen z.B. Einstellungen des Remotings oder der Assembly Versionsumleitung unbedingt in Konfigurationsdateien abgelegt werden.

Da bietet es sich natürlich an, eigene Informationen auch in diesen Konfigurationsdateien abzulegen. Darüber hinaus hat Microsoft neben den allgemeinen Angaben über den Namen und Speicherort der Konfigurationsdatei auch noch ein Schema definiert, dass es Ihnen ermöglicht auch komplexe Konfigurationsinformationen abzulegen.

Dadurch verringert sich Ihr Einarbeitungsaufwand. Sie sind nicht mehr gezwungen ein eigenes Format zu erfinden. Und schließlich gibt es innerhalb der .NET Klassenbibliothek (FCL) verschiedene Klassen, die es Ihnen ermöglichen leicht (d.h. in einer objektorientierten Art) auf die abgespeicherten Informationen zuzugreifen. Zu guter letzt empfiehlt es sich einfach aufgrund des Wartungsaufwands alle Konfigurationsinformationen an einer zentralen Stelle abzulegen und nicht über verschiedene Datastores zu verteilen.

Bitte beachten Sie bei der Nutzung von Konfigurationsdateien jedoch, dass diese nicht für den Einsatz in Multiuser-Umgebungen gedacht sind. Deshalb erlauben die Config-Klassen der FCL auch konsequenterweise kein Schreiben von Daten - die dabei entstehenden Synchronisationsprobleme können sinnvoll nur durch Datenbanken übernommen werden. Wenn Sie trotzdem auf Konfigurationsdateien schreibend zugreifen möchten, können Sie sich allerdings des XML-Datasets bedienen oder das Windows-Management-API nutzen. Beide Themen sind jedoch nicht Bestandteil dieses Artikels.

Beispiel
Die Möglichkeiten der Konfigurationsdatenstrukturierung und des Lesens dieser Konfigurationsdaten werden anhand einer simplen Anwendung zur Eingabe und Ausgabe von Benutzerdaten näher erläutert. Die zu ändernden Konfigurationsdaten umfassen dabei die Möglichkeit den Skin der Applikation zu ändern. Hier allerdings nicht im Sinne von skinnable wie bei Windows XP - dies wäre für diesen Artikel zu komplex.

Bild01



















Es ist nur möglich, Farbeinstellungen der Buttons und der Eingabefelder zu verändern, sowie die Titelzeile des Dialogs anzupassen. Alle eigenen Konfigurationsdaten werden dabei immer in der Datei UserManager.exe.config abgelegt, die sich im Hauptverzeichnis der Applikation befindet.

Konfigurationsformat

Was ist denn nun eine Konfigurationsdatei wirklich? Um dies zu beantworten und zur Vorbereitung auf die kommenden Diskussionen, erst einmal eine Konfigurationsdatei in voller Pracht:

<configuration>
  <configSections>
    <section name="skin" 
             type="AdminConsole.SkinSectionHandler, 
                   UserConsole"/>
  </configSections>
  <startup>
    <requiredRuntime version="v1.0.3705" safemode="true"/>
  </startup>
  <skin>
    <form>
      <Title value="Administrator Console User.Library"/>
      <BackGround value="Wheat"/>
    </form>
  </skin>
</configuration>

Bei einer Konfigurationsdatei handelt es sich um eine XML-Datei, also eine ASCII-Datei (im Gegensatz zum Binärformat der Registry). Damit aber eine solche XML-Datei auch als Konfigurationsdatei durch .NET erkannt wird, muss sie folgenden Regeln entsprechen:

  • Sie muss die Endung .config haben (normale XML-Dateien enden häufig auf .xml).

  • Sie muss im gleichen Verzeichnis liegen wie die EXE-Datei, die sie konfigurieren soll.

  • Das Rootelement der Datei heißt <configuration>.

Innerhalb dieses Rootelements können nun beliebig viele von Microsoft vordefinierte und eigen definierte Elemente abgelegt werden.

Das obige Beispiel zeigt innerhalb des <configuration>-Elements die von Microsoft definierten Elemente <configSection> und <section> und die eigenen Elemente <skin> und <form>.

Quick & Dirty

Die einfachste und schnellste Art Konfigurationsdaten abzulegen ist das vordefinierte <appSettings>-Element zu benutzen. Es handelt sich dabei um ein von Microsoft definiertes XML-Tag, das erlaubt einfache Name/Value-Paare innerhalb einer Konfigurationsdatei abzulegen. Eigentlich ganz ähnlich wie unter Win 3.1. Sie erinnern sich? Hier ein Auszug aus der System.ini.

wave=mmdrv.dll
timer=timer.drv

Dieselben Einstellungen in einer config-Datei abgelegt, sehen dann so aus:

<configuration>
  <appSettings>
    <add key="wave" value="mmdrv.dll"/>
    <add key="timer" value = "timer.drv"/>
  </appSettings>
</configuration>

Wie bereits erwähnt, ermöglicht die Verwendung des <appSettings>-Elements die Ablage beliebiger Name/Value-Paare mittels des <add>-Element. Im <add>-Element werden dabei über das Attribut key der Name und über das Attribut value der Wert des Paares definiert. Die Anpassungen für den UserManager sehen mit dieser Notation wie folgt aus:

<configuration>
  <appSettings>
    <add key="Title" value="Administrator Console User.Library"/>
    <add key="BackGround" value = "Wheat"/>
    <add key="AddButtonColor" value="LimeGreen"/>
    <add key="DelButtonColor" value = "Red"/>
    <add key="UserNameTextFieldColor" value = "Gold"/>
    <add key="GroupTextFieldColor" value = "Khaki"/>
  </appSettings>
</configuration>

Die eigentliche Reaktion Ihres Programms auf diese Konfigurationsdaten müssen Sie selbst implementieren, denn im Gegensatz zu den Standardkonfigurationsdaten wie z.B. Versionskontrolle oder Remoting kann die Common Language Runtime (CLR) in diesem Fall keine Annahmen über den Sinn und Zweck dieser Daten treffen.

Konfigurationsdaten auslesen
Aber wie greift man nun auf die so abgelegten Daten zu? Um die Daten auszulesen, greifen Sie auf die statische Eigenschaft AppSettings der Klasse ConfigurationSettings zu. Sie können mit den Werten, die im key-Attribut definiert sind, die entsprechenden Werte des value-Attributs auslesen. Achten Sie dabei beim Auslesen der Schlüssel auf Gross-/Kleinschreibung. Das folgende Code-Snippet zeigt die Methode InitSkin, die während des Konstruktoraufrufs der Klasse UserManager ausgeführt wird.

//aus der Datei UserConsole.cs die den Dialog definiert.
private void InitSkin(){
  NameValueCollection cfg = ConfigurationSettings.AppSettings;
  this.Text = (string) cfg["Title"];
  this.BackColor = Color.FromName((string) cfg["BackGround"]);
  this.btnAdd.BackColor = Color.FromName((string) 
                          cfg["AddButtonColor"]);
  this.btnDel.BackColor = Color.FromName((string) 
                          cfg["DelButtonColor"]);
  this.txtUserName.BackColor = Color.FromName((string) 
                               cfg["UserNameTextFieldColor"]);
  this.txtGroup.BackColor = Color.FromName((string) 
                            cfg["GroupTextFieldColor"]);
}
Bild02



















Sie sehen, dass AppSettings eine NameValueCollection ist, auf deren Werte Sie über die definierten Schlüssel zugreifen können. Die Werte werden im konkreten Beispiel ausgelesen und die GUI-Elemente werden daraufhin angepasst (z.B. indem die Farbe der Buttons neu gesetzt wird).

Sektionen

Mit Name/Value-Paaren allein kann man aber keine komplexen Konfigurationsszenarien erstellen. Das ging ja schon in Windows 3.1 besser! Hier nochmal ein Ausschnitt aus system.ini mit mehreren Sektionen:

[drivers]
wave=mmdrv.dll
timer=timer.drv
[386enh]
woafont=app850.FON
EGA80WOA.FON=EGA80850.FON

Bieten also nicht vielleicht auch config-Dateien einen Weg, Name-Werte-Paare in Gruppen zusammenzufassen?
Selbstverständlich, denn die Problematik, die sich aus der Verwendung des <appSettings>-Elements ergibt, wird bei der Nutzung einer großer Anzahl eigener Konfigurationsdaten schnell offensichtlich.

<appSettings>-Elemente stellen allerdings nur einen flachen Namensraum zur Verfügung, der doppelte Namenseinträge nicht zulässt. Bei vielen Elementen wird dies sehr schnell unübersichtlich. Sie könnten z.B. neben Informationen zum Skin auch noch Informationen über Defaultwerte beim Neuanlegen von Usern definieren oder in der Konfigurationsdatei ablegen wollen, welche Userdatenbank und welcher Server angesprochen werden sollen etc.

Die Syntax der Konfigurationsdateien erlaubt glücklicherweise die Definition eigener Sektionen. Dazu muss innerhalb des <configSections>-Elements zuerst ein neues <section>-Element deklariert werden. Über das Attribut name wird dabei der Name des Sektionselements definiert und über das Attribut type der Handler für diese Sektion (zu Handlern später mehr).

Erst durch die Deklaration einer neuen Sektion kann man dieses Element auch innerhalb der Konfigurationsdatei nutzen. Innerhalb des neuen Elements können Sie nun Einträge als Attributname/Wert-Paare vornehmen. Ähnlich der zuvor beschriebenen Einträge - allerdings hat sich die Notation an dieser Stelle geändert- mussten die Key/Value-Paare zuvor unter Zurhilfenahme des <add>-Elements eingetragen werden, können Sie nun ad hoc eigene Attribute samt Werten innerhalb der zuvor definierten Sektion ablegen. Die folgende Konfigurationsdatei zeigt die Beispieldaten unter Nutzung einer selbstdefinierten Sektion skin.

<configuration>
  <configSections>
    <section name="skin" 
             type="System.Configuration.SingleTagSectionHandler" />
  </configSections>
  <skin
    Title="Administrator Console User.Library"
    AddButtonColor = "LimeGreen"
    DelButtonColor = "Red"
    UserNameTextFieldColor = "Gold"
    GroupTextFieldColor = "Khaki"
    BackGround = "Wheat"
  />
</configuration>

Wie im ersten Beispiel (und den folgenden) müssen Sie auch hier wieder den Code zur Interpretation der Konfigurationsdaten selbst schreiben. In diesem Fall greifen Sie auf die statische Methode GetConfig() der Klasse ConfigurationSettings zu. Ihr übergeben Sie den Namen der definierten und auszulesenden Sektion. Hier das angepasste Beispiel:

private void InitSkin(){
  IDictionary skin = (IDictionary) 
      ConfigurationSettings.GetConfig("skin");
  this.Text = (string) skin["Title"];
  this.BackColor = Color.FromName((string) skin["BackGround"]);
  this.btnAdd.BackColor = Color.FromName((string) 
      skin["AddButtonColor"]);
  this.btnDel.BackColor = Color.FromName((string) 
      skin["DelButtonColor"]);
  this.txtUserName.BackColor = Color.FromName((string)
      skin["UserNameTextFieldColor"]);
  this.txtGroup.BackColor = Color.FromName((string) 
       skin["GroupTextFieldColor"]);}

Prinzipiell hat sich natürlich nicht viel geändert. Über die statische Methode GetConfig der Klasse ConfigurationSettings erhalten Sie ein Dictionary, das die Schlüssel/Werte-Paare enthält, die Sie innerhalb der Sektion abgelegt haben. Der Vorteil gegenüber der ersten Lösung liegt klar auf der Hand: Sie können beliebig viele Sektionen definieren und haben jederzeit über den Namen der Sektion Zugriff auf die dort abgelegten Werte.

Erweiterte Sektionen

Alles schön und gut, aber wäre es nicht auch schön Konfigurationsdaten baumartig in einer beliebigen Tiefe zu schachteln? Immerhin kann das die Registry ab Windows NT 3.5 ja auch schon!

In der Tat reicht die Zweiteilung der Konfigurationsdaten in Sektionen und direkt zugeordnete Schlüssel/Wertepaare oft nicht aus - deshalb gibt es die Möglichkeit, Sektionen ineinander zu schachteln. Für das nächste Beispiel wurden die Konfigurationsdaten auf mehrere Sektionen aufgeteilt.

Die Sektion skin enthält dabei die Untersektionen form für die dialogspezifischen Einstellungen, die Untersektion button für die Farbe der Buttons und die Untersektion textfield für die Anpassung der Eingabefelder.

Wie bereits zuvor müssen Sie im Element <configSections> die zu nutzenden Elemente deklarieren. Um eine Baumstruktur auszubauen nutzt man dabei das Element <sectionGroup>. Dieses definiert über das Attribut name den Namen der Obersektion und enthält wiederum weitere <sectionGroup>- oder <section>-Elemente. Die eigentlichen Konfigurationsinformationen schreiben Sie dann wie bereits zuvor unter Nutzung der deklarierten Sektionen und Sektionsgruppen in das Element <configuration>. Die folgende Konfigurationsdatei wurde wie beschrieben aufgeteilt:

<configuration>
  <configSections>
    <sectionGroup name="skin">
      <section name="form"
        type="System.Configuration.NameValueSectionHandler,
              system, 
              Version=1.0.3300.0,
              Culture=neutral,
              PublicKeyToken=b77a5c561934e089"/>
      <section name="button"
        type="System.Configuration.NameValueSectionHandler,
              system, 
              Version=1.0.3300.0,
              Culture=neutral,
              PublicKeyToken=b77a5c561934e089"/>
      <section name="textfield"
        type="System.Configuration.NameValueSectionHandler,
              system, 
              Version=1.0.3300.0,
              Culture=neutral,
              PublicKeyToken=b77a5c561934e089"/>
    </sectionGroup>
  </configSections>
  <skin>
    <form
      Title ="Administrator Console User.Library"
      BackGround ="Wheat"/>
    <button
      AddButtonColor="LimeGreen"
      DelButtonColor = "Red"/>
    <textfield
      UserNameTextFieldColor = "Gold"
      GroupTextFieldColor = "Khaki"/>
  </skin>
</configuration>

Auf die Werte können Sie natürlich wie auch zuvor über die Klasse ConfigurationSettings zugreifen. Gibt man der Methode GetConfig als Parameter den Namen der gewünschten Sektion als Pfadausdruck mit (z.B. Sektion1/Untersektion2/Untersektion3), erhält man eine Dirctionary-Instanz, deren Schlüssel den Namen der Attribute innerhalb der Sektion entspricht.

private void InitSkin(){
  IDictionary form = (IDictionary) 
    ConfigurationSettings.GetConfig("skin/form");
  this.Text = (string) form["Title"];
  this.BackColor = Color.FromName((string) form["BackGround"]);
  IDictionary button = (IDictionary) 
    ConfigurationSettings.GetConfig("skin/button");
  this.btnAdd.BackColor = Color.FromName((string) 
    button["AddButtonColor"]);
  this.btnDel.BackColor = Color.FromName((string) 
    button["DelButtonColor"]);
  IDictionary textfield= (IDictionary) 
    ConfigurationSettings.GetConfig("skin/textfield");
  this.txtUserName.BackColor = Color.FromName((string) 
    textfield["UserNameTextFieldColor"]);
  this.txtGroup.BackColor = Color.FromName((string) 
    textfield["GroupTextFieldColor"]);}

Der Code ist annähernd derselbe wie zuvor, nur dass in diesem Falle gleich mehrere Dictionaries zur Verfügung stehen, die über einen Pfadausdruck angesprochen werden müssen.

Fallstricke

Auf einige Fallen sollten Sie beim Definieren von Sektionen allerdings achten:

  • <sectionGroup>-Elemente können nur andere <sectionGroup>- oder <section>-Elemente beinhalten.

  • Nur Attribute innerhalb von <section>-Elementen können ausgewertet werden. Sie können zwar auch Attribute auf <sectionGroup>-Elementen definieren, haben allerdings keine Möglichkeit des Zugriffs. Jeder Zugriff über die Klasse ConfigurationSettings liefert einen null-Wert.

    <configuration>
      <configSections>
        <sectionGroup name="skin">
          <section name="form" .../>
        </sectionGroup>
      </configSections>
      <skin 
        attribute="value">
        <form ... />
      </skin>
    </configuration>
    Object skin = ConfigurationSettings.GetConfig("skin");
    if (skin == null){
        Console.WriteLine("isNull!");}
    
  • Vermeiden Sie die mehrfache Instanzierung von <section>- oder <sectionGroup>-Elementen innerhalb des gleichen Eltern-Elements. Der Zugriff über ConfigurationSettings liefert immer nur das letzte gefundene Element.

    Dies gilt nicht, wenn Sie eigene Handler nutzen (dazu gleich mehr). Im folgenden Beispiel enthält die Dictionary-Instanz immer die Schlüssel/Werte-Paare AddButtonColor:LimeGreen und DelButtonColor:Red.

    <configuration>
      <skin>
        <button
          AddButtonColor="Yellow"
          DelButtonColor = "Black"/>
        <button
          AddButtonColor="LimeGreen"
          DelButtonColor = "Red"/>
      </skin>
    </configuration>
      IDictionary button = (IDictionary) 
        ConfigurationSettings.GetConfig("skin/button");
    

Deklaratives

Natürlich ist Ihnen während der vorangegangenen Beispiele nicht entgangen, dass bei der Deklaration von Sektionen nicht nur deren Namen angeben wurde, sondern jeweils auch ein Attribut mit dem Namen type mit einem Wert belegt wurde. Und sicherlich fragen Sie sich schon die ganze Zeit: wozu eigentlich?

Mit der Angabe des type wird die CLR angewiesen beim Einlesen der Konfigurationsdaten eine Instanz einer bestimmten Klasse zu nutzen. Die Angabe muss dabei immer den vollqualifizierten Namen der verwendeten Klasse enthalten. Darüber hinaus kann außerdem noch der strong Name der Assembly angegeben werden.

Sie haben bisher nur zwei dieser Klasse kennen gelernt:
System.Configuration.SingleTagSectionHandler und System.Configuration.NameValueSectionHandler. Bei der Deklaration einer Sektion wird eine Instanz des definierten Typs erzeugt. Diese wird später dazu genutzt die eigentlichen Sektionen auszulesen und die Daten als Objekte zur Verfügung zu stellen. Beide bisher verwendeten Handler erzeugen dabei jeweils eine Collection auf deren Elemente Sie per Schlüssel zugreifen können. Damit ist natürlich auch die Frage beantwortet, warum man Sektionen überhaupt deklarieren muss.

"Halt!" werden Sie jetzt denken. Am Anfang wurde eine Sektion mit dem Namen <appSettings> verwendet. Die wurde doch auch nicht deklariert. Doch, doch! Wurde sie. Nicht von Ihnen, aber von Microsoft. Ein kurzer Ausschnitt aus der Datei machine.config (befindet sich im CONFIG-Verzeichnis Ihrer .NET-Runtime-Installation) zeigt Ihnen die relevanten Informationen:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
 <configSections>
<!-- tell .NET Framework to ignore CLR sections -->
  <section name="runtime" 
    type="System.Configuration.IgnoreSectionHandler,
          System, 
          Version=1.0.3300.0, 
          Culture=neutral, 
          PublicKeyToken=b77a5c561934e089" 
          allowLocation="false"/>
....
  <section name="appSettings" 
    type="System.Configuration.NameValueFileSectionHandler, 
          System, 
          Version=1.0.3300.0, 
          Culture=neutral, 
          PublicKeyToken=b77a5c561934e089"/>
....
</configSections>

In der Datei machine.config werden generell alle Einstellungen definiert, die über Applikationsgrenzen hinweg auf diesem PC gelten sollen. Auch Sie können hier eigene Definitionen (z.B. von Sektionen) hinterlegen. Aber Vorsicht! Der Unsachgemäße Umgang mit dieser Datei kann ähnlich gravierende Folgen auf die Stabilität Ihres Systems haben wie der unbedachte Umgang mit der Registry in älteren Windows-Versionen.

IConfigurationSectionHandler

Bleibt eigentlich nur noch zu klären: Warum muss man den Handler überhaupt angeben, wenn doch sowieso immer ein dictionary-ähnliches Objekt erzeugt wird?

Dieser Eindruck mag entstanden sein durch die bisher ausschließliche Verwendung von Handlern, die eben dieses Verhalten zeigen. Prinzipiell jedoch hindert Sie jedoch niemand daran einen eigenen Handler zu implementieren, der Objekte einer ganz anderen (eigenen) Klasse erzeugt.

Da stellt sich natürlich die Frage; warum sollten Sie das denn tun?
Die Vorliebe mit typisierten Interfaces zu arbeiten anstatt mit String/Objekt-Paaren ist eine Antwort. Eine andere ist, dass Sie in der Lage sind eigene Konfigurationsschemata leichter und sicherer wiederzuverwenden.

Bevor Sie sich nun allerdings auf die Programmierung eines Handlers stürzen wäre es natürlich nett zu erfahren, was ein Handler eigentlich ist und wie er funktioniert.

Handler Einmaleins
Ein Handler ist eine Instanz einer Klasse, die das Interface IConfigurationSectionHandler und die dort definierte Methode Create implementiert.

public interface IConfigurationSectionHandler{
  public object Create(object _parent, 
                       object _ctx, 
                       XmlNode _section);}

Beim Einlesen einer Konfigurationsdatei erzeugt die CLR eine DOM-Repräsentation der eingelesenen Datei. Sie durchläuft diese Repräsentation und erzeugt für jede deklarierte Sektion eine passende Handlerinstanz.

Bitte denken Sie daran, das geschieht nicht nur für die von Ihnen definierten Sektionen, sondern für alle Sektionen, also auch jene, die z.B. in der Datei machine.config enthalten sind.

Trifft die CLR bei der weiteren Abarbeitung auf eine konkrete Sektion innerhalb des DOM-Baums, ruft Sie die Methode Create des assoziierten Handlers auf. Der Methode Create werden dabei drei Parameter übergeben:

  • die Elternsektion vom Typ object,

  • der HTTPConfigurationContext. Dieser ist nur für ASP.NET-Anwendungen relevant und wird ansonsten mit null belegt und schließlich

  • die aktuell eingelesene Sektion als XmlNode.

Die Handler-Instanz wertet die Informationen innerhalb dieses Nodes aus, erzeugt ein Objekt eines beliebigen Typs und gibt dieses als Ergebnis zurück.

Die folgenden Abschnitte zeigen Ihnen nun, wie eine konkrete Handlerklasse definiert und genutzt wird.

Am Anfang...

jedes Handlers steht natürlich wie immer eine Konfigurationsdatei. In diesem Falle werden Userdaten (Username und Maschinenkennung) abgelegt.

<configuration>
  <configSections>
    <section name="User" 
             type="AdminConsole.UserSectionHandler, 
                   UserConsole"/>
  </configSections>
  <User
    name = "Robert" 
    machine = "Frodo"/>
</configuration>

Wie bereits zuvor wird eine eigene Sektion deklariert und ein Handler zugeordnet (in diesem Fall ein eigener). Im weiteren Verlauf wird die Sektion genutzt und mit konkreten Daten befüllt.
Es soll ja ein eigenes, typisiertes Objekt erzeugt werden, also muss auch eine neue Klasse für die Userdaten definiert werden:

public class User{
  private String mName
  private String mMachine;
  public User( string _name, string _machine){ 
    mName = _name;
    mMachine = _machine;
  }
  public string Name{get{return mName;}}
  public string Machine{get{return mMachine;}}
}

Nun muss nur noch der Handler implementiert und das durch ihn erzeugte Objekt verwendet werden.

public class UserSectionHandler: IConfigurationSectionHandler{
  public object Create(object _parent, 
                       object _ctx, 
                       XmlNode _section){
    string name = _section.SelectSingleNode("@name").Value;
    string machine = _section.SelectSingleNode("@machine").Value;
    return new User(name, machine);}}

Wie bereits angekündigt, soll ein typisiertes Objekt der Klasse User erzeugt werden. Der Handler liest hierzu die Attribute des übergebenen XmlNode aus. Dies geschieht mittels der Methode SelectSingleNode. Es wird anschließend ein Userobjekt erzeugt und als Rückgabewert der Methode Create zurückgegeben.

Eigene Objekte nutzen

Bleibt noch den konkreten Code zur Nutzung des typisierten Objektes zu betrachten:

private void InitSkin(){
  User user = (User) ConfigurationSettings.GetConfig("User");
  this.txtName = user.Name;
  this.txtMachine = user.Machine;}

Wie zuvor greift man die Konfigurationsdaten durch die Methode GetConfig der ConfigurationSettings-Klasse ab. Nur wird das erhaltene Objekt nicht auf ein IDictionary oder eine NameValueCollection gecastet, sondern eben auf ein Userobjekt.
"Naja, so richtig hat's mich nicht überzeugt!", werden Sie jetzt wohl denken. "Jede Menge Aufwand für die Implementierung eines Handlers und der Clientcode hat sich kaum geändert".

private void InitSkin(){
  IDictionary user = (IDictionary) 
    ConfigurationSettings.GetConfig("User");
  this.txtName = user["name"];
  this.txtPwd = user["machine"];}

Zugegebenermaßen ist der Unterschied in der Codemenge zu vorher eher marginal. Aber zum einen ist der Code jetzt streng typisiert, d.h. der Compiler kann Zugriffsfehler auf Konfigurationdaten feststellen und der Editor kann Sie mit Intellisense unterstützen. . Zum anderen liegt der Vorteil eigener Handler darin, dass sie komplexe Verarbeitungsschritte für Konfigurationseinstellungen ausführen können, auch ohne dass Sie selbst auf diese Einstellungen zugreifen.

Ein Handler wird erzeugt, wenn Sie ihn mit GetConfig() anfordern. Wenn dann in dessen Create() beliebig viel Code steht, muss Ihr Programm doch von all dem nichts bemerken. Umfangreiche Konfigurationsinformationen können von Create() allein verarbeitet werden. Sie rufen bei Programmstart womöglich nur einmal GetConfig() auf. Das ist dann deklarative Programmierung: Statt Konfigurationsinformationen imperativ mit eigenem Code zu lesen und zu verarbeiten, beschreiben Sie die Einstellungen nur in der config-Datei und "wünschen" sich anschließend nur, dass sie korrekt interpretiert werden.

Das nächste Beispiel ist jedoch noch ausführlicher und zeigt, wie die Problematik der Skins typsicher und wiederverwendbar mit eigenen Handlern abgebildet werden kann.

Skin-Handler

Um eine Vorstellung von den zu erzeugenden Objekten zu haben, die für die Skins benötigt werden, wurde ein Klassenmodel erzeugt, dass aus der Klasse SkinConfig besteht (der Wurzel). Objekte dieser Klasse enthalten wiederum jeweils eine Instanz der Klasse TextFieldColorConfig, ButtonColorConfig und FormConfig.
Wie üblich, muss zuerst die Konfigurationsdatei angepasst werden.

Zuerst wie schon zuvor, wird innerhalb des <configSection>-Elements ein <section>-Element eingefügt, das die Namen der Sektion definiert und den Handler, der für diesen Sektionstyp zuständig ist. In diesem Fall ist dies die Klasse SkinSectionHandler (Bitte denken Sie daran: der Name der Klasse muss immer voll qualifiziert sein und die Assembly, in der sich der Handler befindet, muss exklusive der Endung "exe" bzw. "dll" angegeben werden).

Bild03















Der Inhalt der definierten Sektion muss im Gegensatz zu den vorherigen Beispielen nicht weiter definiert werden, d.h es müssen keine weitern Untersektionen und Handler definiert werden - einzig die angegebene Handler-Klasse SkinSectionHandler muss das Schema kennen, um es richtig interpretieren zu können, d.h. Sie muss innerhalb von <skin> ein <form>-Element erwarten usw.

<configuration>
  <configSections>
    <section name="skin" 
             type="AdminConsole.SkinSectionHandler, 
                   UserConsole"/>
  </configSections>
  <skin>
    <form>
      <Title value="Administrator Console User.Library"/>
      <BackGround value="Wheat"/>
    </form>
    <button>
      <Add value = "LimeGreen"/>
      <Del value = "Red" />
    </button>
    <textfield>
      <Name value = "Gold" />
      <Group value = "Khaki" />
    </textfield>
  </skin>
</configuration>

Anschließend der Code zur Implementierung der SkinConfig-Klasse und der von ihr verwendeten Klassen. Die Klassen FormConfig, ButtonColorConfig und TextColorConfig werden dabei als innere Klassen bereitgestellt, da sie außerhalb einer SkinConfig-Instanz nicht existieren können.

public class SkinConfig{
  private FormConfig mForm;
  private ButtonColorConfig mButton;
  private TextFieldColorConfig mTextField;
  public SkinConfig( string _title, string _background, 
                     string _add, string _del, 
                     string _name, string _group) {
    mForm = new FormConfig(_title, _background);
    mButton = new ButtonColorConfig(_add, _del);
    mTextField = new TextFieldColorConfig(_name, _group);}
  public FormConfig Form{get{return mForm;}}
  public ButtonColorConfig Button{get{return mButton;}}
  public TextFieldColorConfig TextField{get{return mTextField;}}
  public class FormConfig{
    private string mTitle;
    private string mBackground;
    public FormConfig(string _title, string _background){
      mTitle = _title;
      mBackground = _background;}
    public string Background{get{return mBackground;}}
    public string Title{get{return mTitle;>
  public class ButtonColorConfig{
    private string mAdd;
    private string mDel;
    public ButtonColorConfig(string _add, string _del){
      mAdd = _add;
      mDel = _del;}
    public Color Add{get{return Color.FromName(mAdd);}}
    public Color Del{get{return Color.FromName(mDel);>
  public class TextFieldColorConfig{
    private string mName;
    private string mGroup;
    public TextFieldColorConfig(string _name, string _group){
      mName = _name;
      mGroup = _group;}
    public Color Name{get{return Color.FromName(mName);}}
    public Color Group{get{return Color.FromName(mGroup);>}

Die Klasse SkinSectionHandler ist eine Implementierung eines IConfigurationSectionHandler. Die Methode Create erzeugt aus dem übergebenen XmlNode (in diesem Fall dem Skin-Element) ein SkinConfig-Objekt und gibt dieses zurück.

public class SkinSectionHandler: IConfigurationSectionHandler{
  public object Create(object _parent, object _ctx, 
                       XmlNode _section){
    XmlNode form = _section.SelectSingleNode("form");
    string background = 
      form.SelectSingleNode("BackGround/@value").Value;
    string title = form.SelectSingleNode("Title/@value").Value;
    XmlNode button = _section.SelectSingleNode("button");
    string add = button.SelectSingleNode("Add/@value").Value;
    string del = button.SelectSingleNode("Del/@value").Value;
    XmlNode textfield = _section.SelectSingleNode("textfield");
    string name = 
      textfield.SelectSingleNode("Name/@value").Value;
    string group = 
      textfield.SelectSingleNode("Group/@value").Value;
    SkinConfig skin = new SkinConfig(title, background, 
                                     add, del, 
                                     name, group);
    return skin;
  }
}

Der Zugriff auf das erzeugte SkinConfig-Objekt geschieht nun wieder unter Zurhilfenahme der Klasse ConfigurationSettings und der Methode GetConfig, an die der Name übergeben wird.

private void InitSkin(){
  SkinConfig skin = (SkinConfig) 
    ConfigurationSettings.GetConfig("skin");
  this.Text = skin.Form.Title;
  this.BackColor = skin.Form.Background;
  this.btnAdd.BackColor = skin.Button.Add;
  this.btnDel.BackColor = skin.Button.Del;
  this.txtUserName.BackColor = skin.TextField.Name;
  this.txtGroup.BackColor = skin.TextField.Group;}

Die Verwendung der Konfigurationsdaten ist nun - das werden Sie zugeben - wesentlich einfacher geworden. Als kleiner Vergleich noch mal der Zugriff auf die Konfigurationsdaten ohne eigenen Handler:

IConfigurationSectionHandeler

Die Vorteile liegen hoffentlich auf der Hand:

private void InitSkin(){
  IDictionary form = (IDictionary) 
    ConfigurationSettings.GetConfig("skin/form");
  this.Text = (string) form["Title"];
  this.BackColor = Color.FromName((string) form["BackGround"]);
  IDictionary button = (IDictionary) 
    ConfigurationSettings.GetConfig("skin/button");
  this.btnAdd.BackColor = Color.FromName((string) 
    button["AddButtonColor"]);
  this.btnDel.BackColor = Color.FromName((string) 
    button["DelButtonColor"]);
  IDictionary textfield= (IDictionary) 
    ConfigurationSettings.GetConfig("skin/textfield");
  this.txtUserName.BackColor = Color.FromName((string) 
    textfield["UserNameTextFieldColor"]);
  this.txtGroup.BackColor = Color.FromName((string) 
    textfield["GroupTextFieldColor"]);}
  • Der Code ist typisiert, daher auch kürzer und besser lesbar, da Typumwandlungen entfallen.

  • Wiederverwendung ist möglich.

  • Unter der Verwendung von VS.NET und mit Intellisense schreibt sich der Code nahezu selbst.

Fazit

Konfigurationsdateien unter .NET bieten eine flexible Lösung eigene Konfigurationsdaten in read-mostly Szenarien abzulegen. Besonders angenehm ist die Tatsache, dass die Möglichkeiten zur Strukturierung der Daten aus Entwicklersicht angenehm skalieren: Sind am Anfang eines Projektes nur wenige Konfigurationsdaten vorhanden, kann man diese sehr leicht unter Verwendung des <appSetting>-Elements ablegen.

Wird im Vorlauf des Projektes klarer, dass die Daten komplexer sind, ist es sehr leicht möglich eine baumartige Struktur aufzubauen. Erkennt man zudem, daß sich bestimmte Konfigurationselemente über mehrere Projekte hinweg wiederverwenden lassen, lässt sich leicht ein Konfigurationshandler implementieren, der diese Informationen in typisierter Form verschiedenen Applikationen zur Verfügung stellt.

Literatur
[Richter] Jeffrey Richter: Microsoft .NET Framework Programmierung; ISBN 3860636502, MSPress 2002


Microsoft führt eine Onlineumfrage durch, um Ihre Meinung zur MSDN-Website zu erfahren. Wenn Sie sich zur Teilnahme entscheiden, wird Ihnen die Onlineumfrage angezeigt, sobald Sie die MSDN-Website verlassen.

Möchten Sie an der Umfrage teilnehmen?
Anzeigen:
© 2014 Microsoft