Laden...

[Artikel] Custom Settings Provider

Erstellt von egrath vor 17 Jahren Letzter Beitrag vor 17 Jahren 33.886 Views
egrath Themenstarter:in
871 Beiträge seit 2005
vor 17 Jahren
[Artikel] Custom Settings Provider

Custom Settings Provider

Dieser Artikel beschäftig sich mit der Entwicklung und Benutzung von eigenen Settings-Providern unter .NET 2.0

Übersicht

Seit Version 2.0 des .NET Frameworks gibt es ein neues Konfigurationsmodell, welches auf sg. Settings Providern basiert. Standardmässig wird ein auf XML basierender Provider mitgeliefert welcher folgende Funktionalitäten und im weiteren folgende Einschränkungen enthält:
*Konfigurationsdaten werden in XML Dateien gespeichert *Scoping für User- und Applikationssettings *User- settings von Applikation schreibbar, Applikationssettings nicht *Bei Versionierung von Assemblys für jede Version eigene Settings

Besonders der letzte Punkt kann teilweise lästig sein, wenn man einen relativ schnellen Release-Zyklus bei einer Applikation hat. Da dies der Fall ist, habe ich mich dafür entschieden einen eigenen Provider zu entwickeln, der in der Lage ist Unix-Konforme Konfigurationsdateien zu schreiben und zu lesen.

Die Implementierung

Ein neuer Settings Provider muss grundsätzlich von der Klasse "SettingsProvider" erben und zumindest die folgenden Methoden überschreiben:
*protected override string ApplicationName
o Eigenschaft, welche den Namen der aktuell ausgeführten Applikation zurückliefert

*public override SettingsPropertyValueCollection GetPropertyValues( SettingsContext context, SettingsPropertyCollection collection )
o Liefert eine Sammlung von Eigenschaften für die aktuelle Applikation unter berücksichtigung des übergebenen Kontextes aus. Dieser könnte beispielsweise ein User- oder Applikations-Scope sein. Im Fall dieses Artikels gibt es kein Scoping, alle Settings werden in ein gemeinsames File geschrieben.

*public override void SetPropertyValues( SettingsContext context, SettingsPropertyValueCollection collection )
o Wie oben, nur dass keine Eigenschaften geliefert werden, sondern wie der Name schon impliziert geschrieben werden.

*public override void Initialize( string name, System.Collections.Specialized.NameValueCollection config )
o Übernimmt die Initialisierung des Providers

Unser Provider basiert darauf, dass beim erstellen der Settings-Collection die entsprechenden Werte aus einer INI-Datei gelesen werden, welche sich im globalen Applikations-Einstellungs-Ordner aller User Befindet ("%ALLUSERSPROFILE%\Application Data"). Der übersichtlichkeit halber wird der Pfad zu unserer INI-Datei im Konstruktor der Klasse erstellt:


public SpecialSettingsProvider()
{
    m_ConfigFile = String.Format( "{0}\\{1}",
                   System.Environment.GetFolderPath( Environment.SpecialFolder.CommonApplicationData ),
                   "config.ini" );
}

Sehen wir uns als nächstes die einzelnen überschreibungen der oben gelisteten Methoden an:


public override string ApplicationName
{
    get { return Assembly.GetExecutingAssembly().GetModules()[0].Name.ToString(); }
    set {}
}

Der Inhalt dieser Property ist relativ simpel, es wird lediglich der Name der aktuell ausgeführten Assembly ermittelt und als String zurückgeliefert.


public override SettingsPropertyValueCollection GetPropertyValues( SettingsContext context, SettingsPropertyCollection collection )
{
    // Lesen der Settings aus der Konfigurationsdatei
    m_IniFileDictionary = new Dictionary<string, string>();

    StreamReader reader = new StreamReader( m_ConfigFile );
    string iniFileLine = String.Empty;
    while( iniFileLine != null )
    {
        iniFileLine = reader.ReadLine();
        if( iniFileLine == null ) break;
        int sepIndex = iniFileLine.IndexOf( "=" );
        string key = iniFileLine.Substring( 0, sepIndex );
        string value = iniFileLine.Substring( sepIndex + 1, iniFileLine.Length - sepIndex - 1 );
        m_IniFileDictionary.Add( key, value );
    }
    reader.Close();

    // Erstellen der Collection und rückgabe derselben
    SettingsPropertyValueCollection coll = new SettingsPropertyValueCollection();

    foreach( SettingsProperty setting in collection )
    {
        SettingsPropertyValue val = new SettingsPropertyValue( setting );

        val.IsDirty = false;
        try
        {
            val.SerializedValue = m_IniFileDictionary[setting.Name];
        }
        catch( Exception e )
        {
            throw ( new ConfigurationErrorsException( "Key not found!", e ) );
        }

        coll.Add( val );
    }

    return coll;
}

In dieser Methode wird nun die eigentliche Aufgabe des erstellens der Collection für die Settings übernommen. Dazu wird das INI-File gelesen und die darin enthaltenen Werte in ein Dictionary geschrieben. Die Werte dieses werden dann in eine für den SettingsProvider eigene Collection geschrieben mit welcher dieser nun seine eigentliche Arbeit durchführt. Interessant sind in dieser Methode die beiden übergebenen Parameter:
*SettingsContext context
o In diesem wird der Kontext übergeben für den die Collection zurückgeliefert werden soll. Dieser wird aber von uns ignoriert da wir keine Kontextabhängigen Settings unterstützen.

*SettingsPropertyCollection collection
o In dieser Collection werden im groben gesehen die Namen jener Eigenschaften übergeben, für welche wir die entsprechenden Daten besorgen sollen. Den Inhalt dieser Collection erstellen die Basismethoden des Providers selbst anhand der weiter unten vorgestellten Klasse welche sich um die Verwaltung der Settings kümmert.

Die einzelnen Elemente der Collection enthalten im wesentlichen zwei Properties. Dies ist zum einen "IsDirty" mit welchem festgelegt wird, ob dieses Setting geändert wurde (Wir von uns auf false gesetzt, da wir ja der Originator der Daten sind). Zum zweiten "SerializedValue", welche dann die eigentlichen Daten des Settings enthält.


public override void SetPropertyValues( SettingsContext context, SettingsPropertyValueCollection collection )
{
    foreach( SettingsPropertyValue value in collection )
    {
        try
        {
            m_IniFileDictionary[value.Name] = ( string ) value.SerializedValue;
        }
        catch( Exception e )
        {
            throw( new ConfigurationErrorsException( "Key not found!", e ));
        }
    }

    StreamWriter writer = new StreamWriter( "config.ini" );
    foreach( KeyValuePair<string,string> kv in m_IniFileDictionary )
    {
        writer.WriteLine( "{0}={1}", kv.Key, kv.Value );
    }
    writer.Close();
}

Diese Methode wird aufgerufen, wenn die Applikation geänderte Settings wieder zurückschreiben möchte. Übergeben wird eine Collection mit Werten (welche mittlerweile geändert worden sein können). Diese Werte schreiben wir in unser Dictionary, welches im Anschluss wiederrum in das originale INI-File geschrieben wird. Auch hier wird der Parameter "context" aus dem oben genannten Grund ignoriert.


public override void Initialize( string name, System.Collections.Specialized.NameValueCollection config )
{
    base.Initialize( ApplicationName, config );
}

Die Initialisierung unseres Providers überlassen wir der Basismethode - wir haben nichts besonderes zu tun hier ....

Die Benutzung innerhalb der Applikation

Was nützt uns der beste Provider wenn wir Ihn nicht benutzen. Wir erstellen uns daher in unserer Applikation folgende Klasse:


[SettingsProvider( typeof( SpecialSettingsProvider ))]
class AppSettings : ApplicationSettingsBase
{
    [ApplicationScopedSetting()]
    [DefaultSettingValue( "UNKNOWN" )]
    public string Dsn
    {
        get { return ( string ) this["Dsn"]; }
        set { this["Dsn"] = value; }
    }
}

Über Attribute wird hier festgelegt dass es sich um eine Settings-Klasse handelt. Es müssen für alle Werte welche wir in unserem INI-File vorhalten die entsprechenden Property erstellt werden. Zusätzlich können auch noch Default-Werte eingetragen werden. Somit kann beispielsweise nach einer überprüfung ob das INI-File nicht vorhanden ist, die Default-Werte in dieses geschrieben werden. Wie man erkennen kann wurde das Attribut "ApplicationScopedSetting" gesetzt - obwohl wir ja gesagt haben dass wir Kontextunabhängig sind. Das Framework kennt leider nur Applications- und Userspezifische Settings und eines der beiden muss gesetzt werden da wir zwangsläufig in einem der beiden Kontexte arbeiten müssen. Da wir in den überschriebenen Methoden aber keine unterscheidung machen ist dies nur eine kosmetische Geschichte.

Um den Provider nun auch tatsächlich anzusprechen, machen wir folgendes:


public static void Main( string[] args )
{
    AppSettings settings = new AppSettings();

    Console.Out.WriteLine( "Dsn = {0}", settings.Dsn );
    string newDsn = Console.In.ReadLine();
    settings.Dsn = newDsn;
    settings.Save();
}

Ist glaub ich relativ selbsterklärend.

Da es sich um ein relativ komplexes Thema handelt, welches hier auch nur ziemlich kurz angerissen wurde empfehle ich euch die entsprechenden Klassen in der MSDN nachzuschlagen und den hier vorgestellten Code ein wenig zu verinnerlichen (oder Ihr benutzt Ihn einfach Out-of-the-Box ohne nachzudenken 😉

Den gesamten Quellcode der Testapplikation findet Ihr untenstehend:


//
// How to implement a custom SettingsProvider to read and write (simple) INI Files
// See [URL]http://msdn2.microsoft.com/en-us/library/ms181001.aspx[/URL] for a Registry based example
//
// Egon A. Rath
//

using System;
using System.Configuration;
using System.Collections.Generic;
using System.Reflection;
using System.IO;

public class SettingsTest
{
    public static void Main( string[] args )
    {
        AppSettings settings = new AppSettings();

        Console.Out.WriteLine( "Dsn = {0}", settings.Dsn );
        string newDsn = Console.In.ReadLine();
        settings.Dsn = newDsn;
        settings.Save();
    }
}

class SpecialSettingsProvider : SettingsProvider
{
    private Dictionary<string, string> m_IniFileDictionary;
    private string m_ConfigFile;

    public SpecialSettingsProvider()
    {
        m_ConfigFile = String.Format( "{0}\\{1}", System.Environment.GetFolderPath( Environment.SpecialFolder.CommonApplicationData ), "config.ini" );
    }

    public override string ApplicationName
    {
        get { return Assembly.GetExecutingAssembly().GetModules()[0].Name.ToString(); }
        set { }
    }

    public override SettingsPropertyValueCollection GetPropertyValues( SettingsContext context, SettingsPropertyCollection collection )
    {
        // Lesen der Settings aus der Konfigurationsdatei
        m_IniFileDictionary = new Dictionary<string, string>();

        StreamReader reader = new StreamReader( m_ConfigFile );
        string iniFileLine = String.Empty;
        while( iniFileLine != null )
        {
            iniFileLine = reader.ReadLine();
            if( iniFileLine == null ) break;
            int sepIndex = iniFileLine.IndexOf( "=" );
            string key = iniFileLine.Substring( 0, sepIndex );
            string value = iniFileLine.Substring( sepIndex + 1, iniFileLine.Length - sepIndex - 1 );
            m_IniFileDictionary.Add( key, value );
        }
        reader.Close();

        // Erstellen der Collection und rückgabe derselben
        SettingsPropertyValueCollection coll = new SettingsPropertyValueCollection();

        foreach( SettingsProperty setting in collection )
        {
            SettingsPropertyValue val = new SettingsPropertyValue( setting );

            val.IsDirty = false;
            try
            {
                val.SerializedValue = m_IniFileDictionary[setting.Name];
            }
            catch( Exception e )
            {
                throw ( new ConfigurationErrorsException( "Key not found!", e ) );
            }

            coll.Add( val );
        }

        return coll;
    }

    public override void SetPropertyValues( SettingsContext context, SettingsPropertyValueCollection collection )
    {
        foreach( SettingsPropertyValue value in collection )
        {
            try
            {
                m_IniFileDictionary[value.Name] = ( string ) value.SerializedValue;
            }
            catch( Exception e )
            {
                throw ( new ConfigurationErrorsException( "Key not found!", e ) );
            }
        }

        StreamWriter writer = new StreamWriter( m_ConfigFile );
        foreach( KeyValuePair<string, string> kv in m_IniFileDictionary )
        {
            writer.WriteLine( "{0}={1}", kv.Key, kv.Value );
        }
        writer.Close();
    }

    public override void Initialize( string name, System.Collections.Specialized.NameValueCollection config )
    {
        base.Initialize( ApplicationName, config );
    }
}

[SettingsProvider( typeof( SpecialSettingsProvider ) )]
class AppSettings : ApplicationSettingsBase
{
    [ApplicationScopedSetting()]
    [DefaultSettingValue( "UNKNOWN" )]
    public string Dsn
    {
        get { return ( string ) this["Dsn"]; }
        set { this["Dsn"] = value; }
    }
}