War diese Seite hilfreich?
Ihr Feedback ist uns wichtig. Teilen Sie uns Ihre Meinung mit.
Weiteres Feedback?
1500 verbleibende Zeichen
Exportieren (0) Drucken
Alle erweitern
Erweitern Minimieren

Schnelle Entwicklung mit benutzerdefinierten Anwendungsblöcken für Enterprise Library

Veröffentlicht: 24. Jul 2006
Von Mark Seemann

Enterprise Library für Microsoft .NET Framework 2.0 ist eine Bibliothek mit Anwendungsblöcken. Dies sind modulare Komponenten, die Entwickler bei häufig auftretenden Aufgaben während der Entwicklung unterstützen sollen. Sie enthält ein erweiterbares Framework zur Erstellung robuster, skalierbarer Anwendungen. Zum einen bietet Enterprise Library mehrere nützliche Anwendungsblöcke, zum anderen können auch eigene wiederverwendbare Komponenten integriert werden. In diesem Artikel werde ich anhand eines Beispiels demonstrieren, wie ein in Enterprise Library integrierter Anwendungsblock erstellt wird.

Der Code kann hier heruntergeladen werden: AppBlocks2006_07.exe (315 KB)

Das Patterns & Practices-Team von Microsoft, das die Enterprise Library entwickelt hat, haben den Begriff "Anwendungsblock" zur Bezeichnung einer Bibliothek eingeführt, die mehr als nur eine DLL-Datei ist. Obwohl es sich bei einem Anwendungsblock tatsächlich um eine DLL handelt, verfügt dieser über weitere spezielle Charakteristiken.

Anwendungsblöcke sind konfigurationsgebunden. Dies bedeutet, ihr Verhalten wird durch die Konfiguration definiert. Diese Konfiguration kann in einer normalen .NET-Konfigurationsdatei festgelegt werden oder während der Ausführung erstellt und in den Anwendungsblock eingefügt werden. Ein Teil der Konfiguration besteht aus typischen Konfigurationswerten wie Zahlen, die Bedingungen für ein bestimmtes Verhalten definieren. Im Fall von Anwendungsblöcken werden durch die Konfiguration jedoch auch Erweiterungen des Basisblocks definiert.

Anwendungsblöcke sind außerdem modular und erweiterbar. Die Erweiterbarkeit wird üblicherweise mithilfe eines Providermodells erreicht. Der Block delegiert dabei einen Teil seiner Arbeit an einen Provider, der eine von dem Block verwendete Basisklasse oder Schnittstelle implementiert. Obwohl ein Block normalerweise einige voreingestellte Provider (beispielsweise einen Provider, mit dem Einträge ins Windows®-Ereignisprotokoll vorgenommen werden können, oder einen, mit dem Daten in SQL Server™ gelesen und geschrieben werden können) enthält, können weitere Provider entwickelt und ersetzt werden, indem der jeweilige Providertyp in der Konfiguration des Blocks definiert wird.

Wann sollte ein Anwendungsblock anstelle einer normalen Bibliothek erstellt werden? Wenn für eine Konfiguration der Bibliothek kein Bedarf besteht und diese von anderen nicht erweitert werden soll, müssen Sie keinen Anwendungsblock erstellen. Wenn Ihnen diese Fähigkeiten jedoch nützlich erscheinen, sollten Sie das Erstellen eines Anwendungsblocks in Betracht ziehen. Da Enterprise Library bereits über eine umfangreiche Infrastruktur zur Konfigurationsverwaltung verfügt, die Sie verwenden können, kann das Erstellen eines Anwendungsblocks vorteilhafter sein als eine eigenständige, konfigurierbare Bibliothek.

Durch einen Anwendungsblock wird die Bibliothek komplexer. Außerdem entstehen Abhängigkeiten von anderen Blöcken in Enterprise Library. Im Gegenzug verfügen Sie jedoch über ein allgemeines Konfigurationsverwaltungssystem, und Sie können auf andere Teile von Enterprise Library zugreifen. So muss beispielsweise sehr häufig auf Daten zugegriffen werden. Wenn Sie Ihren Anwendungsblock in Enterprise Library integrieren, können Sie für Ihren Block einen optionalen Provider schreiben, der den Data Access Application Block (Anwendungsblock für den Datenzugriff) verwendet. Bei ordnungsgemäßer Durchführung wird dadurch eine optionale Abhängigkeit vom Data Access Application Block hergestellt. Wenn Sie keinen Datenzugriff benötigen, können Sie diese Abhängigkeit vermeiden.

Auf dieser Seite

 Erstellen eines Anwendungsblocks
 Factorys für Objekte
 Erstellen von Factorys
 Definieren der Plug-In-Konfiguration
 Implementieren von "PlugInProvider"
 Entwurfszeitverhalten
 Providerknoten
 XML-Serialisierung und -Deserialisierung
 Schlussbemerkung
 Der Autor

Erstellen eines Anwendungsblocks

In diesem Artikel werde ich zeigen, wie ein Anwendungsblock erstellt wird, der zur Verwaltung von Plug-Ins für eine Anwendung verwendet werden kann. Dabei werde ich auf viele Aspekte der Entwicklung von Anwendungsblöcken eingehen. Einige Teile der Implementierung, die sich nicht auf Enterprise Library beziehen, werde ich jedoch kürzen. Die Lösung, die in diesem Artikel erstellt wird, ist stark vereinfacht und keinesfalls sicher. Eine umfassende Abhandlung zum sicheren und stabilen Laden von Plug-Ins finden Sie in dem MSDN®Magazine-Artikel "Do You Trust It? Discover Techniques for Safely Hosting Untrusted Add-Ins with the .NET Framework 2.0" (in englischer Sprache) von Shawn Farkas. Da der von mir erstellte Anwendungsblock für das Plug-In-Ladeprogramm erweiterbar ist, kann ein neuer Provider entwickelt werden, der die im Artikel von Shawn Farkas beschriebenen Methoden nutzt und diesem Anwendungsblock hinzugefügt werden kann.

Da das Wichtigste vorab nun geklärt wäre, fangen wir an. Zuerst definiere ich das Ziel des Anwendungsblocks. Da er zum Laden von Plug-Ins dienen soll, ist es meiner Meinung nach ein angemessenes Ziel, Entwickler in die Lage zu versetzen, Code wie den folgenden zu schreiben:

PlugInManager<MyPlugIn> mgr = 
    PlugInFactory.GetManager<MyPlugIn>();
IList<MyPlugIn> plugIns = mgr.GetPlugIns();
// Do something with the plug-ins...

Von der statischen Klasse PlugInFactory wird ein neues PlugInManager<T>-Objekt erstellt, das zum Abrufen der Plug-Ins verwendet werden kann. In diesem Artikel ist GetPlugIns der einzige Member von PlugInManager<T>. In einer umfangreicheren Implementierung würden wahrscheinlich andere Funktionalitäten hinzugefügt, wie z. B. die Möglichkeit, einzelne Plug-Ins zur Laufzeit zu aktivieren oder zu deaktivieren. PlugInManager<T> delegiert die eigentliche Arbeit an einen Provider, der in der Konfiguration des Anwendungsblocks festgelegt wurde. Von diesem Provider werden, wie in seiner Implementierung definiert, die Plug-Ins dann geladen und zurückgegeben.

Ein Anwendungsblock besteht aus der Laufzeitkomponente und einer optionalen Entwurfszeitkomponente. Ich werde Ihnen die Entwicklung beider Komponenten zeigen. In der Laufzeitkomponente ist die Logik der Bibliothek implementiert. Dies ist die Datei, auf die Sie in Ihrem Projekt verweisen müssen, wenn Sie den Anwendungsblock verwenden möchten. Die Entwicklung der Laufzeitkomponente ist in mehrere eigenständige Schritte gegliedert.

Bei der Entwurfszeitkomponente handelt es sich um eine weitere Assembly der Bibliothek, die Sie erstellen und einbinden können. So wie die Laufzeitkomponente in das Laufzeitframework von Enterprise Library integriert ist, ist die Entwurfszeitkomponente in die Entwurfszeittools von Enterprise Library integriert. Diese Entwurfszeitfunktionalität ist vorrangig dazu gedacht, die Arbeit des Entwicklers bei der Verwendung des Anwendungsblocks zu vereinfachen. Sie kann jedoch auch von Systemadministratoren oder anderen Benutzern als grafisches Tool zum Bearbeiten der Konfiguration einer Anwendung verwendet werden.

Ein Anwendungsblock ist eine komplexe Codesammlung mit zahlreichen interagierenden Klassen. Beim Lesen dieser schrittweisen Anleitung fragen Sie sich möglicherweise, warum so viele bewegliche Elemente erforderlich sind. Dies liegt zum Teil daran, dass Enterprise Library viel Flexibilität bei der Art der Implementierung Ihres Anwendungsblocks bietet. Bei vielen Schritten verwendet Enterprise Library Abstraktionen, wie Schnittstellen und abstrakte Klassen, um bestimmte Aufgaben durchzuführen. Bei den meisten derartiger Szenarios ist eine erweiterbare Standardimplementierung enthalten. Diese muss jedoch nur selten verwendet werden. Sie können Ihre eigene Implementierung erstellen, wenn Sie dies bevorzugen.

Bei jeder Durchführung einer Aufgabe in Enterprise Library – wie das Lesen von Konfigurationsdaten aus einem persistenten Speicher oder das Instanziieren eines konkreten Typs, wenn ein abstrakter Typ angefordert wird – kann das Standardverhalten erweitert oder geändert werden. An jedem Punkt der Erweiterbarkeit in Enterprise Library sind typischerweise ein paar interagierende Klassen beteiligt. Da Enterprise Library über viele Punkte zur Erweiterbarkeit verfügt, gibt es viele interagierende Klassen – eine Tatsache, die Sie bald selbst herausfinden werden.

Factorys für Objekte

In Enterprise Library werden Objekte mithilfe von Factorys erstellt. In einem eigenen, auf Attributen beruhenden Ansatz erhalten Klassen den Typ der zugehörigen Factory als Attribut. Die Factory wird anschließend durch das Ableiten von in Enterprise Library bereitgestellten Basisklassen implementiert. Abbildung 1 zeigt eine Übersicht des typischen Vorgangs zur Objekterstellung. Wie Sie sehen, ist dies ein komplexer Prozess. Beachten Sie jedoch, dass dadurch mehr als nur die Erstellung einer beliebigen Instanz eines Typs erreicht wird. Sie fordern einen abstrakten Typ an und erhalten, basierend auf Konfigurationsdaten, die richtige Implementierung dieses Typs, ordnungsgemäß konfiguriert und ausgestattet.

Dieses Framework zur Objekterstellung ist sehr leicht erweiterbar, und Sie sollten es als Objekterstellungsmechanismus für Ihren eigenen Anwendungsblock verwenden. Sie werden jedoch unversehens feststellen, dass Sie selbst eine Vielzahl von Factoryklassen erstellen. Abbildung 2 zeigt eine Übersicht der Interaktion zwischen diesen Klassen. Bei Verwendung des Anwendungsblocks für das Plug-In-Ladeprogramm dient die statische Klasse PlugInFactory typischerweise als Einstiegspunkt. Diese Klasse existiert nur aus Bequemlichkeit. Mithilfe von PlugInManagerFactory<T> erstellt sie Instanzen von PlugInManager<T>. Von den meisten Anwendungsblöcken in Enterprise Library wird eine solche statische Factoryklasse als Einstiegspunkt in den Anwendungsblock bereitgestellt. Bei der Erstellung eines Anwendungsblocks in Enterprise Library ist die Implementierung einer statischen Factoryklasse optional. Ich habe mich jedoch für das Einbinden einer solchen Klasse für das Plug-In-Ladeprogramm entschieden, da es hier sinnvoll ist.

Interaktion von Objektfactoryklassen
Abbildung 2:   Interaktion von Objektfactoryklassen

Bei dem Ansatz von Enterprise Library werden konfigurierbare Objekte mithilfe von Factorys erstellt, und obwohl dieses Framework sehr flexibel ist, werden keine Generika unterstützt, da Typen zum Zeitpunkt der Kompilierung unbekannt sind. Aus diesem Grund delegieren die generischen Klassen des Plug-In-Ladeprogramms die meiste Arbeit an nicht generische Klassen, die von Enterprise Library erstellt und verwaltet werden können. PlugInManager<T> dient als Wrapper der abstrakten Klasse PlugInProvider, während PlugInManagerFactory<T> als Wrapper von PlugInProviderFactory dient. Wenn Ihr Anwendungsblock keine Unterstützung für Generika benötigt, müssen Klassen wie PlugInManager<T> und PlugInManagerFactory<T> nicht implementiert werden.

Die Klasse PlugInProviderFactory, die für die Erstellung von PlugInProvider-Instanzen zuständig ist, wird von einer in Enterprise Library bereitgestellten Klasse abgeleitet. Mithilfe des Attributs CustomFactory, das in Schritt 1 von Abbildung 1 beschrieben wurde, identifiziert PlugInProvider die Klasse PlugInProviderCustomFactory, die von Enterprise Library zusammen mit Konfigurationsklassen verwendet wird, um Instanzen von PlugInProvider zu erstellen.

Dies mag komplex erscheinen, Enterprise Library übernimmt jedoch den schwersten Teil der Arbeit. Sie müssen zwar ein paar Klassen implementieren, diese sind jedoch alle ziemlich einfach. Sie fragen sich wahrscheinlich, wozu diese ganze Komplexität notwendig ist. Wie bereits erwähnt, wird mehr als nur die Erneuerung einer Instanz von PlugInProvider durchgeführt. Für Anfänger: Sie können keine PlugInProvider-Klasse implementieren, da es sich um eine abstrakte Klasse handelt. Wenn Sie eine Instanz von PlugInProvider anfordern, muss vom Anwendungsblock, basierend auf Konfigurationsdaten, tatsächlich eine Implementierung dieser abstrakten Klasse bereitgestellt werden. In Enterprise Library wird dieser Mechanismus verwendet, um zu ermitteln, was erstellt und wie es erstellt werden soll.

Erstellen von Factorys

Bei näherer Betrachtung des beabsichtigten Zielcodes weiter oben wird deutlich, dass die ersten Klassen, die implementiert werden müssen, PlugInManager<T> und PlugInFactory sind. Beide Klassen delegieren die meiste Arbeit an andere Klassen. PlugInManager<T> verwendet PlugInProvider, um die Hauptteil der Arbeit zu verrichten, wie in Abbildung 3 verdeutlicht wird. Da der Objekterstellungsmechanismus von Enterprise Library keine Generika unterstützt, ist PlugInProvider keine generische Klasse. Um also IList<T> zurückzugeben, muss die von PlugInProvider zurückgegebene IList-Schnittstelle in die entsprechende typsichere Auflistung umgewandelt werden.

PlugInProvider selbst ist eine einfache Klasse, die in Abbildung 4 dargestellt wird. Da sie abstrakt ist, bietet sie den Punkt zur Erweiterbarkeit für das Plug-In-Ladeprogramm. Wenn Sie das Plug-In-Ladeprogramm erweitern möchten, können Sie eine neue Klasse von PlugInProvider ableiten und Ihre benutzerdefinierte Logik dort implementieren. Das bemerkenswerteste Feature von PlugInProvider ist das Attribut CustomFactory, durch das Enterprise Library angewiesen wird, wie eine neue Instanz einer von PlugInProvider abgeleiteten Klasse erstellt wird. Beachten Sie auch die abstrakte Methode GetPlugIns, die von erbenden Klassen implementiert werden muss.

PlugInProviderCustomFactory wird von AssemblerBased-CustomFactory<TObject, TConfiguration> abgeleitet, eine abstrakte Klasse, die von Enterprise Library bereitgestellt wird. Beim Ableiten von dieser Klasse müssen Sie lediglich die Methode GetConfiguration implementieren. Zwei neue Klassen, PlugInLoaderSettings und PlugInProviderData, treten hier zum ersten Mal auf, wie Sie im folgenden Code sehen können:

public class PlugInProviderCustomFactory :
    AssemblerBasedCustomFactory<PlugInProvider, PlugInProviderData>
{
    protected override PlugInProviderData GetConfiguration(
        string name, IConfigurationSource configurationSource)
    {
        PlugInLoaderSettings settings =
            (PlugInLoaderSettings)configurationSource.
                GetSection(PlugInLoaderSettings.SectionName);
        return settings.PlugInProviders.Get(name);
    }
}

Bei diesen Klassen handelt es sich um Konfigurationsklassen, die ich im nächsten Abschnitt ausführlicher erläutern werde.

Im Moment ist am meisten darauf zu achten, dass die Methode GetConfiguration die geeigneten Konfigurationsdaten zurückgibt, sodass Enterprise Library ein neues PlugInProvider-Objekt erzeugen kann, wie in Abbildung 1 gezeigt wird. Nach dieser benutzerdefinierten Factory kann ich eine Factoryklasse erstellen, mit der ich später PlugInProvider-Instanzen erzeugen kann, wie hier gezeigt wird:

public class PlugInProviderFactory : 
    NameTypeFactoryBase<PlugInProvider>
{
    public PlugInProviderFactory(IConfigurationSource configSource) :
        base(configSource) { }
}

Obwohl es sich hierbei um eine weitere Factoryklasse handelt, muss ich lediglich von der Klasse NameTypeFactoryBase<T> ableiten und einen öffentlichen Konstruktor bereitstellen. PlugInManagerFactory<T> dient einfach als Wrapper für PlugInProviderFactory. PlugInFactory erstellt und verwaltet ein Dictionary dieser Factorys und delegiert die Arbeit an die entsprechende Factory, wie im Code in Abbildung 5 dargestellt.

Zu beachten ist hier die für das Plug-In-Ladeprogramm spezifische Namenskonvention. Bei polymorphen Enterprise Library-Auflistungen dienen die Namen ihrer Elemente als Schlüsselwerte, daher müssen alle Namen innerhalb der Auflistung eindeutig sein. Für das Plug-In-Ladeprogramm wäre der Typ des Plug-Ins der intuitivste Schlüssel gewesen. Dadurch hätte ich jedoch meine eigene nach Typ geschlüsselte Auflistungsklasse erstellen müssen, und ich hätte die von Enterprise Library bereitgestellten Klassen nicht wiederverwenden können.

Da die Entwurfszeitkomponente in jedem Fall einen eindeutigen Namen benötigt, besteht die Konvention für das Plug-In-Ladeprogramm darin, dass jede PlugInProvider-Klasse nach dem assemblyqualifizierten Namen des Plug-In-Typs benannt wird. In der Praxis können Sie dies in Abbildung 5 sehen. Dies ist zwar etwas trickreich, der Benutzer wird dies jedoch nicht bemerken, da ich diese Konvention auch in der Entwurfszeitkomponente berücksichtige. Wenn Sie andererseits den reinen XML-Code bearbeiten möchten, denken Sie daran, dass dort ohnehin nur Zeichenfolgen vorkommen.

Somit sind wir am Ende des ersten Schritts zur Erstellung eines Anwendungsblocks. Wenn Sie sich nochmals Abbildung 1 anschauen, fragen Sie sich vielleicht, warum ich keine Assemblerklassen definiert habe. Der Grund dafür ist, dass Assembler an Implementierungen von PlugInProvider gebunden sind und nicht an die abstrakte Klasse PlugInProvider selbst. Als Nächstes beschreibe ich, wie Konfigurationsklassen definiert werden, und anschließend, wie eine PlugInProvider-Klasse implementiert wird. Dabei erläutere ich auch das Erstellen einer entsprechenden Assembler-Klasse.

Definieren der Plug-In-Konfiguration

Das Konfigurationsframework von Enterprise Library baut auf System.Configuration auf und funktioniert in ähnlicher Weise. Zum Definieren des Konfigurationsabschnitts für das Plug-In-Ladeprogramm erstelle ich die Klasse PlugInLoaderSettings, die in Abbildung 6 gezeigt wird. Anstatt direkt von System.Configuration.ConfigurationSection abzuleiten, sollte der Konfigurationsabschnitt eines Anwendungsblocks von Microsoft.Practices.EnterpriseLibrary.Common.Configuration.SerializableConfigurationSection abgeleitet werden. Dadurch erhält die Klasse Enterprise Library-Funktionalität. Infolgedessen können Sie neben anderen Dingen den Konfigurationsabschnitt an einem anderen Ort als die Konfigurationsdatei der Anwendung speichern.

Die Klasse PlugInLoaderSettings enthält ausschließlich eine Auflistung von PlugInProviderData-Klassen. Die Klasse PlugInProviderData enthält Daten, die zum Konfigurieren einer Instanz von PlugInProvider verwendet werden, wie nachfolgend dargestellt:

public class PlugInProviderData : NameTypeConfigurationElement
{
    public PlugInProviderData() : base() { }
    public PlugInProviderData(string name, Type type) :
        base(name, type) { }
}

Diese Klasse repräsentiert ein Konfigurationselement und ist indirekt von System.Configuration.ConfigurationElement abgeleitet. Wenn ich ein einfaches Konfigurationselement hätte erstellen wollen, hätte ich PlugInProviderData direkt von ConfigurationElement ableiten können. Enterprise Library bietet jedoch zwei weitere Optionen. Eine davon ist die Klasse NamedConfigurationElement, die andere NameTypeConfigurationElement. Erstere fügt dem Konfigurationselement einen Namen hinzu. Dies ist beim Implementieren der Entwurfszeitfunktionen des Anwendungsblocks hilfreich. Dieser Name dient auch als eindeutiger Schlüssel in den von Enterprise Library bereitgestellten generischen Konfigurationsauflistungsklassen.

Von NameTypeConfigurationElement wird eine zusätzliche Type-Eigenschaft zum Konfigurationselement hinzugefügt. Diese wird zur Unterstützung polymorpher Auflistungen verwendet. Genau das möchte ich in diesem Fall erreichen – verschiedene Plug-In-Provider mit individuellen Konfigurationseinstellungen für verschiedene Plug-In-Typen angeben. Während der Name des Konfigurationselements als Schlüssel zu diesem Element dient, gibt die Eigenschaft Type den Typ an, der von dem Element konfiguriert wird. Im Fall des Plug-In-Ladeprogramms wird von der Eigenschaft Type ein Typ festgelegt, der PlugInProvider implementiert. Denken Sie daran, dass im Plug-In-Ladeprogramm die Eigenschaft Name der Konvention nach dazu verwendet wird, den assemblyqualifizierten Namen des Plug-In-Typs zu speichern. Diese zwei Typen können leicht miteinander verwechselt werden. Der Name gibt jedoch an, welcher Plug-In-Typ vom Provider bedient werden sollte, während die Eigenschaft Type den Typ des Providers angibt. Da der Name der Schlüssel ist, kann ein Plug-In-Typ nur einmal definiert werden. Dagegen können viele verschiedene Plug-In-Typen vom gleichen Providertyp bedient werden. Tatsächlich werden sie meistens sogar alle vom selben Provider bedient.

Aufgrund der Art, wie in Enterprise Library diese Konfigurationselementklassen erzeugt werden, ist es nicht möglich, PlugInProviderData als abstrakte Klasse zu definieren. Sie sollten sie sich jedoch als abstrakt vorstellen. Beachten Sie, dass diese Klasse eigentlich keine Aufgabe hat. In dieser speziellen Implementierung hätte ich sie also weglassen können und meine Konfigurationselemente für die verschiedenen Plug-In-Provider durch direktes Ableiten von NameTypeConfigurationElement erstellen können. Die abstrakte Klasse PlugInProvider enthält dagegen einige Implementierungen, und es ist leichter, die Codestruktur des Anwendungsblocks zu verstehen, wenn eine 1:1-Beziehung zwischen Providern und den zugehörigen Konfigurationselementen besteht.

Implementieren von "PlugInProvider"

An diesem Punkt ist das abstrakte Framework der Laufzeitkomponente vollständig, es gibt jedoch noch keine Funktionalität. Es ist an der Zeit, PlugInProvider zu implementieren. Dies ist eine vereinfachte Implementierung, die nicht sicher ist und keine Plug-Ins unterstützt, die aus dem Arbeitsspeicher entfernt werden können. Aus diesem Grund nenne ich sie NaivePlugInProvider.

Wie in Abbildung 7 dargestellt, wird die Klasse NaivePlugInProvider von PlugInProvider abgeleitet. Ihre Hauptfunktion ist in der Methode GetPlugIns implementiert. Von ihr werden einfach alle Typen in allen Assemblys geladen und wiedergegeben, die sich im konfigurierten Ordner befinden. Wird der gewünschte Plug-In-Typ von einem Typ implementiert, wird eine neue Instanz dieses Typs erstellt und zur Liste der zurückzugebenden Plug-Ins hinzugefügt. Beachten Sie, dass bei dieser Implementierung alle Plug-Ins über einen Standardkonstruktor verfügen müssen. Bei einer robusteren Implementierung wäre der Ansatz wahrscheinlich ausgefeilter.

NaivePlugInProvider verfügt über zwei weitere Merkmale, die zwar weniger offensichtlich sind, im Zusammenhang mit der Erstellung eines Enterprise Library-Anwendungsblocks jedoch interessant sind: die Verwendung des Attributs ConfigurationElementType und das Fehlen eines Standardkonstruktors.

Beim Konfigurieren des Anwendungsblocks für das Plug-In-Ladeprogramm sollten Sie sich lediglich Gedanken darüber machen müssen, welche PlugInProvider-Klasse Sie verwenden möchten, und nicht darüber, welche Klasse Konfigurationsdaten für diesen Provider bereitstellt. Das Attribut ConfigurationElementType enthält diese Informationen. Dies bedeutet, dass die Konfigurationsdaten nur Informationen darüber enthalten, welche PlugInProvider-Klasse erstellt werden soll. Dagegen wird von der Enterprise Library-Infrastruktur ermittelt, welche Klasse Konfigurationsdaten für diesen Provider enthält. In diesem Fall ist dies die Klasse NaivePlugInProviderData, dargestellt in Abbildung 8. Diese Klasse wird von PlugInProviderData abgeleitet und verfügt über eine zusätzliche Konfigurationseigenschaft, mit der Sie den Ordner angeben können, der die Plug-In-Assemblys enthält.

Der andere interessante Aspekt von NaivePlugInProvider ist das Fehlen eines Standardkonstruktors. Wie erstellt Enterprise Library eine neue Instanz von NaivePlugInProvider, wenn kein Standardkonstruktor vorhanden ist? NaivePlugInProviderData ist mit einem Assembler-Attribut ausgestattet. Dieses Attribut gibt einen Typ an, der eine NaivePlugInProvider-Instanz aus einer NaivePlugInProviderData-Instanz erstellen kann.

Die Klasse NaivePlugInProviderAssembler wird auch in Abbildung 8 dargestellt. Eine Assemblerklasse muss IAssembler<TObject, TConfiguration> implementieren, in der als einzige Methode Assemble enthalten ist. Diese verwendet die bereitgestellten Konfigurationsdaten, um die relevanten Informationen zu entnehmen und eine neue NaivePlugInProvider-Instanz zu erstellen.

An diesem Punkt enthält das Plug-In-Ladeprogramm eine voll funktionsfähige, wenn auch einfache Implementierung und ist einsatzbereit. Sie können nun weitere Provider entwickeln, um verschiedene Verhaltensweisen der Plug-In-Erkennung für den Anwendungsblock zu erstellen. Eine nahe liegende Erweiterung ist ein Provider mit sicheren Verfahrensweisen zum Erkennen und Laden von Plug-Ins. Eine andere denkbare Erweiterung könnte Plug-Ins aus BLOBs in einer SQL Server-Tabelle abrufen und möglicherweise eine optionale Abhängigkeit vom Data Access Application Block erzeugen.

Wenn es Ihnen nichts ausmacht, die gesamte Konfiguration per Hand in XML zu schreiben, können Sie hier abbrechen und Ihren Anwendungsblock ausliefern. Andernfalls können Sie eine Entwurfszeitkomponente erstellen, die für Sie die Arbeit übernimmt.

Entwurfszeitverhalten

Die Entwurfszeitkomponente wird in die Konfigurationsanwendung von Enterprise Library integriert. Es handelt sich dabei um eine erweiterbare Windows Forms-Anwendung, mit der Anwendungskonfigurationsdateien über eine komfortable Benutzeroberfläche bearbeitet werden können. Das Schreiben von reinem XML kann somit vermieden werden. Die Komponente hat drei Aufgaben: Sie muss das Verhalten für die Benutzeroberfläche der Anwendung selbst bereitstellen, sie muss ermöglichen, dass die Benutzereinstellungen von der Anwendung serialisiert werden können, und sie muss in der Lage sein, Konfigurationsdaten zu deserialisieren, wenn der Benutzer eine bestehende Anwendungskonfigurationsdatei öffnet.

Da die Anwendungskonfigurationsdateien auf XML basieren, sind sie hierarchisch gegliedert. In der Konfigurationsanwendung wird dies als eine aus Knoten bestehende Baumstruktur dargestellt. Jedes Konfigurationselement in der Laufzeitkomponente muss durch eine Entwurfszeitknotenklasse dargestellt werden, die zusätzliches Entwurfszeitverhalten für die Konfigurationsklasse bereitstellt. In Abbildung 9 wird diese Beziehung für den Anwendungsblock des Plug-In-Ladeprogramms veranschaulicht.

Zuordnung von Laufzeit- und Entwurfszeitklassen
Abbildung 9:   Zuordnung von Laufzeit- und Entwurfszeitklassen

Die Entwurfszeitkomponente muss zur Integration in die Konfigurationsanwendung von Enterprise Library darin registriert werden. Die Assembly und die zugehörigen Abhängigkeiten müssen sich im selben Verzeichnis befinden wie die Konfigurationsanwendung selbst, und sie muss mit dem Attribut ConfigurationDesignManager gekennzeichnet sein, wie nachfolgend dargestellt:

[assembly: ConfigurationDesignManager(
    typeof(PlugInLoaderConfigurationDesignManager))]

Durch dieses Attribut auf Assemblyebene wird PlugInLoaderConfigurationDesignManager bei der Konfigurationsanwendung registriert. Diese Klasse wird von der abstrakten Klasse ConfigurationDesignManager abgeleitet.

Beim Definieren des Entwurfszeitverhaltens wird angegeben, welche Aktionen in welchem Kontext möglich sind. Dies wird erreicht durch das Überschreiben der in ConfigurationDesignManager enthaltenen Methode Register:

public override void Register(IServiceProvider serviceProvider)
{
    PlugInLoaderCommandRegistrar cmdRegistrar =
        new PlugInLoaderCommandRegistrar(serviceProvider);
    cmdRegistrar.Register();

    // Node map code goes here...
}

Die Klasse PlugInLoaderCommandRegistrar wird von der abstrakten Klasse CommandRegistrar abgeleitet. Ihre Aufgabe ist die Registrierung von Entwurfszeitaktionen in der Konfigurationsanwendung. Die erste von mir zu implementierende Aktion ist der Befehl zum Hinzufügen des Anwendungsblocks zu einer Anwendungskonfigurationsdatei. Wenn der Anwendungsblock für das Plug-In-Ladeprogramm zu einer Anwendungskonfiguration hinzugefügt wird, muss eine PlugInLoaderSettingsNode-Klasse zusammen mit ihrer untergeordneten Klasse PlugInProviderCollectionNode zur Hierarchie hinzugefügt werden.

Zuerst müssen diese Knotenklassen wie folgt definiert werden:

public class PlugInLoaderSettingsNode : ConfigurationNode
{
    public PlugInLoaderSettingsNode() :
        base("Plug-In Loader Application Block") {}

    [ReadOnly(true)]
    public override string Name
    {
        get { return base.Name; } set { base.Name = value; }
    }
}

PlugInProviderCollectionNode ist fast identisch, da PlugInLoaderSettingsNode keine anderen Eigenschaften als die Auflistung von PlugInProviders enthält. Auch wenn Sie vielleicht denken, ich hätte eine allgemeine Klasse für beide Knoten verwenden können, ist dies nicht der Fall. Beide Knoten beanspruchen verschiedene Plätze in der Hierarchie, und ich möchte jedem unterschiedliche Aktionen zuweisen. Wenn Sie sich fragen, warum ich die Eigenschaft Name überschrieben habe, so war der einzige Grund dafür, sie dadurch mit dem Schreibschutzattribut kennzeichnen zu können. Die Knoten sind dadurch in der Konfigurationsanwendung schreibgeschützt.

Wenn der Benutzer den Befehl aufruft, den Anwendungsblock für das Plug-In-Ladeprogramm zu einer Anwendungskonfigurationsdatei hinzuzufügen, müssen diese beiden Knoten zur Hierarchie hinzugefügt werden. Um dies zu erreichen, erstelle ich die Klasse AddPlugInLoaderSettingsNodeCommand, wie in Abbildung 10 dargestellt. Sie wird von AddChildNodeCommand abgeleitet und überschreibt die Methode ExecuteCore, um die gewünschte Logik zu implementieren. Die Befehlsklasse muss mit einer Knotenklasse verknüpft sein, sodass die Basisklasse weiß, dass eine Instanz von PlugInLoaderSettingsNode erstellt werden und diese zur Hierarchie hinzugefügt werden soll. Dies ist bereits nach dem Aufruf an die Basisimplementierung von ExecuteCore geschehen. Ich muss somit lediglich eine neue PlugInProviderCollectionNode-Klasse erzeugen und diese zum Einstellungsknoten hinzufügen.

In der Klasse AddPlugInLoaderSettingsNodeCommand wird definiert, was beim Aufrufen des Befehls durch den Benutzer geschieht. Ich muss jedoch noch festlegen, wann und wo dieser Befehl verfügbar ist. Er sollte nur verfügbar sein, wenn der Benutzer den Stammknoten der Anwendungskonfiguration ausgewählt hat, und der Befehl sollte nur einmal aufgerufen werden können. Dies erreiche ich durch die Klasse PlugInLoaderCommandRegistrar, indem ich die abstrakte Methode Register überschreibe:

public override void Register()
{
    this.AddPlugInLoaderCommand();
    // Add other commands here...
}

Die Methode AddPlugInLoaderCommand enthält nur die drei folgenden Anweisungen:

private void AddPlugInLoaderCommand()
{
    ConfigurationUICommand cmd =
        ConfigurationUICommand.CreateSingleUICommand(
        this.ServiceProvider, "Plug-In Loader Application Block",
        "Add the Plug-In Loader Application Block",
        new AddPlugInLoaderSettingsNodeCommand(this.ServiceProvider),
        typeof(PlugInLoaderSettingsNode));
    this.AddUICommand(cmd, typeof(ConfigurationApplicationNode));
    this.AddDefaultCommands(typeof(PlugInLoaderSettingsNode));
}

Durch Aufrufen von CreateSingleUICommand lege ich fest, dass dieser Befehl nur einmal aufgerufen werden kann. In diesem Methodenaufruf stelle ich außerdem Anzeigetexte sowie eine Instanz von AddPlugInLoaderSettingsNodeCommand bereit, die aufgerufen wird, wenn der Benutzer sich für die Durchführung dieser Aktion entscheidet. Mit dem Aufruf von AddUICommand verknüpfe ich diesen Befehl mit dem Typ ConfigurationApplicatonNode. Dabei handelt es sich um den Typ des Stammknotens der Anwendungskonfiguration. Durch die Methode AddDefaultCommands werden zur neu erstellten Klasse PlugInLoaderSettingsNode Standardbefehle hinzugefügt, wie z. B. zum Hinzufügen oder Löschen.

Providerknoten

PlugInProviderData muss durch PlugInProviderNode erweitert werden, und NaivePlugInProviderData durch NaivePlugInProviderNode, wie in Abbildung 9 dargestellt wird. Durch die abstrakte Klasse PlugInProviderNode in Abbildung 11 wird Entwurfszeitfunktionalität für PlugInProviderData bereitgestellt. Mehrere Attribute aus dem System.ComponentModel-Namespace sind hier beteiligt: Category, Editor und ReadOnly. Diese bieten die gleiche Funktionalität wie im Eigenschaftenraster von Visual Studio®.

Zuordnung von Laufzeit- und Entwurfszeitklassen
Abbildung 9:   Zuordnung von Laufzeit- und Entwurfszeitklassen

PlugInProviderNode dient als Wrapper einer PlugInProviderData-Instanz, von der alle Konfigurationsdaten, abgesehen vom Plug-In-Typ, bereitgestellt und gespeichert werden. Das Plug-In-Ladeprogramm verwendet eine spezielle Namenskonvention, bei der der konfigurierte Name von PlugInProvider dem assemblyqualifizierten Namen des Plug-In-Typs entspricht. Da es keine Garantie dafür gibt, dass die Eigenschaft Name zu einem Typ aufgelöst werden kann, wird der Plug-In-Typ von PlugInProviderNode separat in einer Membervariablen gespeichert.

Die Eigenschaft PlugInType enthält außerdem das Attribut BaseType, das von der im Attribut Editor festgelegten Klasse TypeSelectorEditor verwendet wird, um die verfügbaren Typen zu filtern. Von diesem Editor werden nur abstrakte Typen, Basistypen oder Schnittstellen als verfügbare Typen aufgelistet. Bei der Auswahl eines Plug-In-Typs sind nur diese Typen für die Auflistung wichtig, da ein Plug-In nicht auf einem versiegelten Typ basieren kann.

Ein weiteres beachtenswertes Feature von PlugInProviderNode ist die schreibgeschützte Eigenschaft Provider. Ich finde es immer vorteilhaft, überprüfen zu können, welcher Provider konfiguriert ist, und dies ist der direkteste Weg, um den Benutzer zu informieren. Andernfalls kann es bei Verwendung der Konfigurationsanwendung schwierig sein, dies herauszufinden.

Der letzte zu erwähnende Aspekt von PlugInProviderNode ist, dass ich OnRenamed verwende, damit der Name des Knotens und der Name der zugrunde liegenden Daten synchronisiert bleiben.

PlugInProviderNode wird von einer NaivePlugInProviderNode-Klasse durch Bereitstellung der Eigenschaft PlugInFolder erweitert. Das Hinzufügen eines NaivePlugInProvider-Befehls zur Konfigurationsanwendung ist ein wenig einfacher als das Hinzufügen des Anwendungsblocks selbst, da von diesem Befehl keine zusätzlichen Unterknoten einer NaivePlugInProviderNode-Klasse erstellt werden müssen. Somit muss ich keine separate Befehlsklasse für diese Aktion erstellen. Stattdessen lasse ich einfach PlugInLoaderCommandRegistrar die gesamte Befehlsregistrierung übernehmen:

this.AddMultipleChildNodeCommand(
    "Naive Plug-In Provider", "Add a new naive Plug-In Provider",
    typeof(NaivePlugInProviderNode),
    typeof(PlugInProviderCollectionNode));
this.AddDefaultCommands(typeof(NaivePlugInProviderNode));

Durch den Aufruf von AddMultipleChildNodeCommand gebe ich an, dass dieser Befehl beliebig oft aufgerufen werden kann, um neue NaivePlugInProviderNode-Klassen als untergeordnete Knoten einer PlugInProviderCollectionNode-Klasse zu erstellen. Weitere PlugInProvider-Knotentypen können mithilfe eines ähnlichen Mechanismus hinzugefügt werden.

XML-Serialisierung und -Deserialisierung

Beim Arbeiten mit der Konfigurationsanwendung bleiben die Einstellungen nur so lange im Arbeitsspeicher, bis die Änderungen gespeichert werden. Beim Speichern der Änderungen müssen die in den einzelnen Knoten konfigurierten Daten in XML serialisiert werden. Glücklicherweise übernehmen Enterprise Library und System.Configuration das Serialisieren von Konfigurationselementen in und aus XML. Sie müssen dabei lediglich angeben, wie die Knoten Konfigurationsklassen zugeordnet werden.

Dies wird durch das Überschreiben der in PlugInLoaderConfigurationDesignManager enthaltenen Methode GetConfigurationSectionInfo erreicht: Bei dieser Implementierung wird PlugInLoaderSettingsNode aus der Hierarchie abgerufen. Die tatsächliche Arbeit wird jedoch an die Klasse PlugInLoaderSettingsBuilder delegiert, die in Abbildung 12 gezeigt wird. Diese interne Klasse erzeugt eine neue, leere PlugInLoaderSettings-Instanz und verwendet anschließend die Knotenhierarchie, um dieser Instanz Konfigurationsdaten hinzuzufügen.

Da die Konfigurationshierarchie des Plug-In-Ladeprogramms so flach ist, werden lediglich alle PlugInProvider-Klassen durchlaufen und die Konfigurationsdaten aus den zugehörigen Knoten hinzugefügt. Wäre die Hierarchie weiter untergliedert, müsste bei diesem Vorgang die Knotenhierarchie durchlaufen und eine äquivalente Hierarchie von Konfigurationsklassen erzeugt werden.

Neben der XML-Serialisierung durch Enterprise Library und System.Configuration wird von dem Framework auch das Deserialisieren aus XML nach Konfigurationsklasseninstanzen durchgeführt. Sie müssen jedoch den Code bereitstellen, der die Konfigurationsklassen einer Hierarchie von Knoten zuordnet. Der erste Schritt dafür ist das Überschreiben der in PlugInLoaderConfigurationDesignManager enthaltenen Methode OpenCore:

protected override void OpenCore(IServiceProvider serviceProvider,
    ConfigurationApplicationNode rootNode, ConfigurationSection section)
{
    if (section != null)
    {
        PlugInLoaderSettingsNodeBuilder builder =
            new PlugInLoaderSettingsNodeBuilder(serviceProvider,
                (PlugInLoaderSettings)section);
        rootNode.AddNode(builder.Build());
     }
}

Wie bei der XML-Serialisierung delegiere ich hier die tatsächliche Arbeit an eine andere Klasse: PlugInLoaderSettingsNodeBuilder. Diese Klasse wird von NodeBuilder abgeleitet, die ein paar Dienstprogrammmethoden für die Zuordnung von Konfigurationsklassen und Knoten bereitstellt. Die Idee dabei ist, dass der Stammknoten und alle Auflistungsknoten über verknüpfte Knotengeneratorklassen verfügen. Daher enthält eine Knotengeneratorklasse nur Code zur Erstellung des eigenen Knotentyps und delegiert die Arbeit zur Erstellung der übrigen Knotenhierarchie an andere Knotengeneratorklassen. Dies ist auch bei der Klasse PlugInLoaderSettingsNodeBuilder der Fall, die eine neue PlugInLoaderSettingsNode-Klasse erzeugt und den Auftrag zur Erstellung der PlugInProvider-Auflistung an eine andere Klasse delegiert. Im herunterladbaren Code zu diesem Artikel wird die Vorgehensweise verdeutlicht.

In diesem Code wird eine NodeCreationService-Klasse verwendet, um eine neue PlugInProviderNode-Klasse aus den Konfigurationsdaten zu erstellen. Dieser Knoten wird dann zum Auflistungsknoten hinzugefügt. Damit NodeCreationService die Zuordnung durchführen kann, muss eine Knotenzuordnung von PlugInLoaderConfigurationDesignManager in der Methode Register registriert werden.

Zum Erstellen und Registrieren der Knotenzuordnung erstelle ich die Klasse PlugInLoaderNodeMapRegistrar (abgeleitet von NodeMapRegistrar) und überschreibe die zugehörige abstrakte Methode Register. Hier erstelle ich einfach die Zuordnung zwischen der Konfigurationsdatenklasse und der entsprechenden Knotenklasse durch einen Aufruf der geerbten Methode AddMultipleNodeMap:

this.AddMultipleNodeMap("Naive Plug-In Provider",
    typeof(NaivePlugInProviderNode), typeof(NaivePlugInProviderData));

Mithilfe des Mechanismus von Knotengeneratorklassen kann ich eine Knotenhierarchie mit fast unbegrenzter Tiefe und Komplexität generieren, während die Implementierung der einzelnen Knotengeneratoren relativ einfach gehalten wird. Dieser Ansatz mag für das hier erläuterte einfache Beispiel zu kompliziert erscheinen. Er lässt sich jedoch leicht für komplexere Konfigurationsschemen skalieren, in denen er noch weitaus nützlicher wird.

Schlussbemerkung

Das Erstellen eines Enterprise Library-Anwendungsblocks ist nicht trivial, für das entsprechende Projekt kann sich der Aufwand jedoch lohnen. Der Kern der Erstellung eines Enterprise Library-Anwendungsblocks oder der Umwandlung einer normalen konfigurationsgebundenen Bibliothek in einen Anwendungsblock besteht aus der Entwicklung der Laufzeitkomponente. Die optionalen Extras sind jedoch ebenso bedeutend. Ich habe die Entwurfszeitkomponente beschrieben. Dies ist jedoch nur der erste Schritt von vielen.

Das Verpacken des Anwendungsblocks in einem Microsoft® Installer-Paket (MSI) ist nicht allzu schwierig. Der Block kann dadurch wesentlich einfacher heruntergeladen und installiert werden. Sie sollten außerdem das Erstellen einer umfassenden Dokumentation in Betracht ziehen – nicht nur eine aus XML-Kommentaren generierte API-Dokumentation, sondern auch eine kurze Einführung, eine Übersicht über die Architektur, eine Anleitung für erste Schritte usw.

Die meisten Entwickler lernen anhand von Beispielen. Durch ein paar gute Schnellstart-Beispielanwendungen wird die Einführung des Anwendungsblocks einfacher. Auch wenn Ihre Zielgruppe sich auf Ihre Organisation beschränkt, lohnt sich der Einsatz von Beispielen.

Der Autor

Mark Seemann arbeitet für Microsoft Consulting Services in Kopenhagen, Dänemark, wo er Kunden und Partnern von Microsoft beim Planen, Entwerfen und Entwickeln von Anwendungen auf Unternehmensebene hilft. Er dankt Tom Hollander für seine wertvolle Hilfe bei diesem Artikel. Mark ist erreichbar über seinen Blog unter blogs.msdn.com/ploeh (in englischer Sprache).

Aus der Ausgabe Juli 2006 des MSDN Magazine.


Anzeigen:
© 2015 Microsoft