.NET-Konfigurationsdateien sind normale XML-Dateien. Sie enthalten Einstellungen für das .NET Framework und die Runtime, aber auch eigene Einstellungen für die Anwendung. Diese Einstellungen werden grundsätzlich vererbt, dadurch ergibt sich eine Konfigurationsdatei-Hierarchie. Die oberste Konfigurationsdatei in der Hierarchie ist immer die Maschinen-Konfigurationsdatei, die machine.config. Sie enthält computerweite Einstellungen und gilt für alle .NET-Anwendungen. Als nächstes kommen die Anwendungs-Konfigurationsdateien in der Hierarchie. Bei einer Konsolen- oder Windows-Anwendung müssen sich diese im selben Verzeichnis wie die Anwendung befinden. Der Dateiname ist der Name der Anwendung + .config: Für die Anwendung TestApp.exe zum Beispiel muss die Konfigurationsdatei TestApp.exe.config heißen. Bei ASP.NET-Anwendungen heißen die Konfigurationsdateien immer web.config. In jedem Verzeichnis einer Web-Anwendung kann sich eine web.config-Datei befinden. Die Einstellungen werden hier anhand des angeforderten URL vererbt. Nehmen wir zum Beispiel den Pfad /SubDir1/SubDir2/Test.aspx. Hier benutzt die Datei Test.aspx die Einstellungen aus der machine.config, der web.config im WebRoot, in SubDir1 und in SubDir2.
Einfache Konfigurationsdateien
Die appSettings-Sektion ist ein vordefinierter Bereich, der für einfache Einstellungen benutzt werden kann. Die Einstellungen werden hier in Name-Value-Paaren gespeichert. Folgendes Beispiel zeigt, die appSettings-Sektion in einer Konfigurationsdatei:<configuration><appSettings><add key="ConnectionString" value="..." /></appSettings></configuration>
String connectionString = ConfigurationSettings.AppSettings["ConnectionString"];
Komplexe Konfigurationsdateien
Es gibt Situationen, gerade in größeren Anwendungen, in denen die appSettings-Sektion nicht ausreicht. Es wird bei sehr vielen Einstellungen einfach zu unübersichtlich. Deshalb wäre es nützlich, wenn man eigene Sektionen definieren könnte, um die Einstellungen in Gruppen aufzuteilen. Natürlich wurde auch das bei .NET mitbedacht. Hier zeigt sich gleich ein brauchbarer Vorteil gegenüber den ini-Dateien: Durch die Wahl von XML als Dateiformat, kann man unendlich viele Untergruppen definieren und somit die Einstellungen hierarchisch ordnen.Das Root-Element für die Definition eigener Konfigurationssektionen ist das configSections-Element. Dieses muss unter dem configuration-Element als erstes Element notiert werden. Zusätzliche Sektionen definiert man mit section-Elementen. Es besteht auch die Möglichkeit, mit sectionGroup-Elementen die section- und weitere sectionGroup-Elemente zu gruppieren, um damit eine Hierarchie zu definieren und Sektionsnamenskonflikte zu vermeiden. Dabei können die sectionGroup-Elemente beliebig tief verschachtelt werden. Im Folgenden die Definition einer Beispielsektion:
<configuration><configSections><section name="beispiel"type="System.Configuration.NameValueSectionHandler,System, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /></configSections><beispiel><add key="ConnectionString" value="..." /></beispiel></configuration>
Jetzt hat man eine neue Konfigurationssektion, die man ähnlich verwenden kann wie die appSettings-Sektion. Ein Unterschied ist das programmatische Auslesen der neuen Sektion. Hierzu verwendet man die statische Methode GetConfig der ConfigurationSettings-Klasse und übergibt den Pfad zu der Sektion, die man auslesen möchte:
NameValueCollection beispielSektion = (NameValueCollection)ConfigurationSettings.GetConfig("beispiel");
Es gibt im .NET Framework verschiedene Configuration Section Handler-Klassen, die für manche Standardsituationen ausreichen. Es gibt aber auch die Möglichkeit eigene Configuration Section Handler-Klassen zu entwickeln.
Eigener Configuration Section Handler
Ein Configuration Section Handler ist eine Klasse, die das Interface System.Configuration.IConfigurationSectionHandler implementiert. Dieses Interface definiert die Methode Create.Nehmen wir an, wir müssen in einer Konfigurationssektion Pizzas konfigurieren. Dazu müssen wir zu jeder Pizza vielleicht Name, Durchmesser und Preis konfigurieren können. Unsere Konfigurationssektion könnte so aussehen:
<pizzas><pizza name="Mozzarella" durchmesser="28" preis="2,5" /></pizzas>
public object Create(object parent, object configContext, XmlNode section){ArrayList pizzas = new ArrayList();foreach(XmlNode pizza in section.SelectNodes("pizza")){pizzas.Add(new Pizza(pizza.Attributes["name"].Value,Convert.ToSingle(pizza.Attributes["durchmesser"].Value),Convert.ToSingle(pizza.Attributes["preis"].Value)));}return pizzas.ToArray(typeof(Pizza));}
| object parent | Das Konfigurationsobjekt, das in einer übergeordneten Konfigurationssektion von der Create-Methode zurückgegeben wurde. Siehe auch Abschnitt Vererbung von Einstellungen. Wenn es keine übergeordnete Konfigurationssektion gibt, ist dieser Parameter eine null-Referenz. |
| object configContext | Ein Objekt, das Informationen zum Konfigurationskontext enthält. Im ASP.NET-Kontext wird z.B. ein HttpConfigurationContext-Objekt übergeben, über das man den virtuellen Pfad zur Konfigurationsdatei auslesen kann. Wenn es keine Konfigurationskontext-Informationen gibt, ist dieser Parameter eine null-Referenz. |
| XmlNode section | Der XmlNode der Konfigurationssektion. |
Vererbung von Einstellungen
Einstellungen werden standardmäßig vererbt. Das heißt, wenn in einer untergeordneten Konfigurationsdatei eine Konfigurationssektion nicht überschrieben wird, wird automatisch auf die übergeordnete zugegriffen. Wenn aber zum Beispiel in der Maschinen- und in einer Anwendungs-Konfigurationsdatei eine pizzas-Konfigurationssektion definiert ist, überschreibt die Konfigurationssektion in der Anwendungs-Konfigurationsdatei die der Maschinen-Konfigurationsdatei. Damit die Einstellungen in der Maschinen-Konfigurationsdatei nicht verloren gehen, muss im Configuration Section Handler eine Vererbungslogik vorhanden sein. Um die Einstellungen der übergeordneten Konfigurationssektion auszulesen, wird der zuvor erwähnte parent-Parameter verwendet. Über diesen Parameter erhält man Zugriff auf das Objekt, das die Create-Methode des übergeordneten Configuration Section Handlers zurückgegeben hat. Aufgabe der Vererbungslogik kann es nun sein, die übergeordnete und die aktuelle Konfigurationssektion zu vereinen.Listing 1
public object Create(object parent, object configContext, XmlNode section){ArrayList pizzas = new ArrayList();foreach(XmlNode pizza in section.SelectNodes("pizza")){pizzas.Add(new Pizza(pizza.Attributes["name"].Value,Convert.ToSingle(pizza.Attributes["durchmesser"].Value),Convert.ToSingle(pizza.Attributes["preis"].Value)));}if(parent != null){foreach(Pizza pizza in (Pizza[])parent){if(!ContainsPizza(pizzas, pizza))pizzas.Add(pizza);}}return pizzas.ToArray(typeof(Pizza));}
Man kann hier natürlich auch jede andere Vererbungslogik implementieren. Zum Beispiel, dass man durch add-, -remove- oder clear-Tags Einstellungen hinzufügen, einzelne oder alle vererbte Einstellungen entfernen kann. Der NameValueSectionHandler implementiert zum Beispiel diese Logik.
Einstellungen speichern
Die Standardlösung von .NET ist nur für das Lesen von Einstellungen vorgesehen. Man sollte deshalb nur Einstellungen in die Konfigurationsdateien speichern, die von der Anwendung nicht programmatisch verändert und gespeichert werden sollen. .NET-Konfigurationsdateien sind für das Speichern von Einstellungen gedacht, die man einmal vor dem ersten Ausführen der Anwendung macht. Ein gutes Beispiel ist vielleicht ein ConnectionString einer Datenbank-Anwendung. Dieser wird in der Regel einmal bei der Installation der Anwendung konfiguriert und nicht ständig programmatisch geändert.Durch die Vererbungsmöglichkeit der Einstellungen wüsste das Konfigurationssystem auch nicht, in welche Konfigurationsdatei eine Einstellung gespeichert werden soll. Wenn es zum Beispiel in der Maschinen- und in einer Anwendungs-Konfigurationsdatei eine pizzas-Konfigurationssektion gäbe, wüsste das Konfigurationssystem beim Speichern nicht, ob es eine Einstellung in die Maschinen- oder in die Anwendungs-Konfigurationsdatei speichern soll.
Deshalb geht man beim Speichern von Einstellungen den manuellen Weg mit eigenen Konfigurationsdateien. Man sollte die .NET-Konfigurationsdateien nicht während der Laufzeit verändern, denn sie enthalten neben den eigenen Einstellungen meist auch Einstellungen für die Common Language Runtime und das .NET Framework. Bei ASP.NET wird z.B. bei jedem Ändern einer web.config-Datei die AppDomain der Anwendung entladen und neu gestartet.
Eigene Konfigurationsdateien
Es gibt noch keine Vorgabe für das Format von eigenen Konfigurationsdateien. Es gibt auch noch keine Vorgabe, wie man eigene Konfigurationsdateien auslesen sollte. Es bleibt im Moment dem Entwickler selbst überlassen, wie er diese Aufgabe löst. Ich kann hier also nur Empfehlungen geben.Es ist natürlich auch hier XML als Dateiformat zu bevorzugen. Lesen kann man die Einstellungen dann zum Beispiel manuell über die Xml-Klassen, oder automatisch über die Xml-Serialisierungs-Klassen. Einen Ansatz dazu finden Sie in dem Artikel Settings mit Klasse: Einstellungen für VB.NET-Anwendungen von Helma Spona in Ausgabe 02.02 des dot,net magazins.
Es ist auf jeden Fall zu empfehlen, eigene Settings-Klassen zu erstellen, über die die Anwendung auf Einstellungen zugreift. Dadurch wird der XML-Teil vom restlichen Teil der Anwendung versteckt. Wenn es nicht allzu viele Einstellungen sind, kann man zum Beispiel für jede Einstellung eine eigene typisierte Property implementieren. Oder man implementiert bei sehr vielen Einstellungen einfache Zugriffsmethoden, die den Namen der Einstellung übergeben bekommen und deren Wert zurückgeben oder setzen. Dies ist gleichzeitig ein einfach zu erweiternder Ansatz.
Benutzereinstellungen speichern
In der Registry hatte man den HKEY_CURRENT_USER-Schlüssel, unter dem alle benutzerspezifischen Einstellungen gespeichert wurden. Wie macht man das aber bei .NET, wenn man auf die Registry verzichten will?Benutzereinstellungsdateien sind nichts anderes als die vorher erwähnten eigenen Konfigurationsdateien, nur dass sie für jeden User getrennt gespeichert werden. Es geht also nur noch darum, wo und wie benutzerspezifische Dateien abgelegt werden können. Es bieten sich hier mehrere Möglichkeiten an.
Zum einen kann man den Pfad zum ApplicationData- oder LocalApplicationData-Ordner im Windows-Userprofil auslesen und um die Ordnerstruktur \FirmenName\ProduktName\ProduktVersion erweitern. Dadurch werden Namenskonflikte mit anderen Anwendungen verhindert. Der Pfad zum ApplicationData- oder LocalApplicationData-Ordner wird über die GetFolderPath-Methode der System.Environment-Klasse ausgelesen. Windows Forms-Anwendungen können auch die statischen Eigenschaften der Application-Klasse UserAppDataPath und LocalUserAppDataPath benutzen. Der ApplicationData-Ordner ist der Ort im Userprofil für Roaming User-Daten und der LocalApplicationData-Ordner für nicht-Roaming User-Daten.
Eine andere Möglichkeit ist die Verwendung des Isolated Storages. Mit den Isolated Storage-Klassen besteht die Möglichkeit, Dateien unter anderem benutzerabhängig zu speichern. Dabei überlässt man den Isolated Storage-Klassen den Ort, an dem die Dateien gespeichert werden. Sie werden an einer ähnlichen Stelle gespeichert, wie wir es in der ersten Möglichkeit manuell gemacht haben, nur dass Namenskonflikte durch kryptische Ordnernamen verhindert werden.
Listing 2
IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.Assembly |IsolatedStorageScope.User |IsolatedStorageScope.Roaming,null, null);IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream("test.xml",FileMode.Create,isoStore);XmlDocument doc = new XmlDocument();doc.AppendChild(doc.CreateElement("test"));doc.Save(isoStream);isoStream.Close();


