Artikel

Oktober 2002 | Artikel

Eine Sache der Einstellung

(Link zum Artikel: http://www.it-republik.de/dotnet/artikel/0250)

Anwendungen konfigurieren mit C#

Text: von Christoph Müller
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Es kommt häufig vor, dass eine Anwendung konfigurierbar sein muss. Früher verwendete man ini-Dateien zum Abspeichern der Einstellungen. Mit Windows 95 kam die zentrale Registry als Speicherort dazu. Und heute, mit dem .NET Framework, sind es XML-Dateien. Nie war es einfacher eine Anwendung zu konfigurieren als mit dem .NET Framework. Wie genau das gemacht wird, soll dieser Artikel zeigen.

.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:
  1. <configuration>
  2. <appSettings>
  3. <add key="ConnectionString" value="..." />
  4. </appSettings>
  5. </configuration>
Um diese Einstellungen programmatisch zu lesen, gibt es im Namespace System.Configuration die Klasse ConfigurationSettings. Über die statische Eigenschaft AppSettings kann man direkt auf die Einstellungen der appSettings-Sektion zugreifen. Die Eigenschaft gibt eine NameValueCollection zurück. Folgender C#-Code zeigt das Auslesen des ConnectionString-Werts aus der appSettings-Sektion:
  1. 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:
  1. <configuration>
  2. <configSections>
  3. <section name="beispiel"
  4. type="System.Configuration.NameValueSectionHandler,System, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
  5. </configSections>
  6. <beispiel>
  7. <add key="ConnectionString" value="..." />
  8. </beispiel>
  9. </configuration>
Das name-Attribut gibt den Namen der neuen Sektion an, das type-Attribut die Configuration Section Handler-Klasse sowie die Assembly, in der sich diese Klasse befindet. In diesem Fall wird die NameValueSectionHandler-Klasse verwendet, die sich in der Assembly System befindet. Da sich diese im Global Assembly Cache (GAC) befindet, muss sie über ihren Full Assembly Reference Name angegeben werden. Aber dies nur am Rande.
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:
  1. NameValueCollection beispielSektion = (NameValueCollection)ConfigurationSettings.GetConfig("beispiel");
Bei der Pfadangabe ist zu beachten, dass man Sektionsgruppen und die Sektion mit einem Slash voneinander trennt, also: Sektionsgruppe/Sektion. In diesem Fall liegt die Konfigurationssektion direkt unter dem Dokument-Root und kann deshalb einfach über ihren Namen angesprochen werden.
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:
  1. <pizzas>
  2. <pizza name="Mozzarella" durchmesser="28" preis="2,5" />
  3. </pizzas>
Ein Configuration Section Handler, der für jede definierte Pizza diese drei Attribute ausliest und ein Array von Pizza Objekten zurückgibt, könnte so aussehen:
  1. public object Create(object parent, object configContext, XmlNode section)
  2. {
  3. ArrayList pizzas = new ArrayList();
  4. foreach(XmlNode pizza in section.SelectNodes("pizza"))
  5. {
  6. pizzas.Add(new Pizza(pizza.Attributes["name"].Value,
  7. Convert.ToSingle(pizza.Attributes["durchmesser"].Value),
  8. Convert.ToSingle(pizza.Attributes["preis"].Value)));
  9. }
  10. return pizzas.ToArray(typeof(Pizza));
  11. }
Wenn die ConfigurationSettings.GetConfig-Methode für eine pizzas-Sektion das erste Mal aufgerufen wird, wird die entsprechende Configuration Section Handler-Klasse instanziert. Ihre Create-Methode wird dann für jede pizzas-Konfigurationssektion in der Konfigurationsdatei-Hierarchie, beginnend mit der machine.config, aufgerufen. Das Konfigurationssystem cacht den Rückgabewert der Create-Methode und gibt bei mehrmaligem Aufrufen von GetConfig für die selbe Sektion das Objekt aus dem Cache zurück. Die drei Parameter der Methode Create sind in Tabelle 1 aufgelistet.

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
  1. public object Create(object parent, object configContext, XmlNode section)
  2. {
  3. ArrayList pizzas = new ArrayList();
  4. foreach(XmlNode pizza in section.SelectNodes("pizza"))
  5. {
  6. pizzas.Add(new Pizza(pizza.Attributes["name"].Value,
  7. Convert.ToSingle(pizza.Attributes["durchmesser"].Value),
  8. Convert.ToSingle(pizza.Attributes["preis"].Value)));
  9. }
  10. if(parent != null)
  11. {
  12. foreach(Pizza pizza in (Pizza[])parent)
  13. {
  14. if(!ContainsPizza(pizzas, pizza))
  15. pizzas.Add(pizza);
  16. }
  17. }
  18. return pizzas.ToArray(typeof(Pizza));
  19. }
Im Code-Beispiel in Listing 1 wurde die Create-Methode so erweitert, dass übergeordnete Einstellungen mit den aktuellen vereint werden. Zunächst wird geprüft, ob parent null ist. Das ist dann der Fall, wenn es keine übergeordneten Einstellungen gibt. Dann wird für jedes Pizza-Objekt geprüft, ob es bereits in der aktuellen ArrayList pizzas vorhanden ist. Dies macht hier die Methode ContainsPizza. Wenn die Pizza nicht vorhanden ist, wird sie hinzugefügt. Hierbei wird davon ausgegangen, dass eine untergeordnete Einstellung eine übergeordnete überschreibt. Deshalb werden der übergeordneten Einstellung nur Pizza-Objekte hinzugefügt, die nicht in der aktuellen Konfigurationssektion vorhanden sind.
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
  1. IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(
  2. IsolatedStorageScope.Assembly |
  3. IsolatedStorageScope.User |
  4. IsolatedStorageScope.Roaming,
  5. null, null);
  6. IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream(
  7. "test.xml",
  8. FileMode.Create,
  9. isoStore);
  10. XmlDocument doc = new XmlDocument();
  11. doc.AppendChild(doc.CreateElement("test"));
  12. doc.Save(isoStream);
  13. isoStream.Close();
Im Beispiel in Listing 2 wird eine Datei test.xml im Isolated Storage gespeichert. Hier wird zunächst ein IsolatedStorageFile-Objekt über die Methode IsolatedStorageFile.GetStore erstellt. Danach wird über ein IsolatedStorageFileStream-Objekt die Datei test.xml im Isolated Storage erzeugt. Dann wird ein XML-Dokument erstellt und über die Save-Methode in den Stream geschrieben. Zum Schluss wird der Stream geschlossen. Auf diese Weise kann man zum Beispiel auch Benutzereinstellungsdateien im Isolated Storage speichern.
Fazit
Dieser Artikel hat mehrere Möglichkeiten der Konfiguration von .NET-Anwendungen gezeigt. Von einfachen Einstellungen mit Name-Value-Paaren bis hin zu sehr komplexen Konfigurationen mit eigenen Configuration Section Handlern. Auch eigene Konfigurationsdateien und Benutzereinstellungsdateien und deren Speicherung, zum Beispiel im Isolated Storage, wurden behandelt. Abschließend lässt sich sagen, dass das .NET-Konfigurationssystem sehr mächtig ist und das Konfigurieren von Anwendungen stark vereinfachen kann.


Anzeige

Kommentare

zurück zum Seitenanfang