Vom Plugin-Interface zu MEF (Managed Extensibility Framework)

„Never touch a running system!“. Warum schaffe ich es einfach nicht mich daran zu halten? Etwas Positives hat das Ganze aber – ich habe einen Grund einen kleinen Artikel über MEF zu schreiben. Am .NET Day Franken habe ich einen Vortrag über MEF besucht. Es wurde zwar nicht viel gezeigt, was ich nicht selbst schon in einem kleinen Sample gemacht habe, aber das Thema finde ich einfach interessant. Daher habe ich mich dazu entschieden meine ‚UserCustomization‘ im CD-Manager auf MEF umzustellen …

Eine kurze Agenda:
1. Erste Schritte mit dem MEF
2. Aktueller Aufbau im CD-Manager
3. Warum funktioniert das Ganze nicht?!?
4. Fazit


1. Erste Schritte mit dem MEF

Das MEF lässt sich sehr gut für PlugIns nutzen. Eine einfache Umsetzung möchte ich euch hier zeigen.

Zuerst legen wir uns eine Gemeinsam genutzte Assembly an, in der wir das Interface für die PlugIns definieren.
Für die gesamte Applikation wird das .NET Framework 4.0 benötigt.

1
2
3
4
5
6
[InheritedExport]
public interface IPlugIn
{
    string Name { get; set; }
    void Execute();
}

Das PlugIn Interface enthält in diesem Fall nicht viel. Nur einen Namen und eine Funktion zum Ausführen des PlugIns.
Das Attribut [InheritedExport] wird benötigt, damit alle Klassen, die von diesem Interface ableiten, exportiert werden.
Damit das Attribut verwendet werden kann ist ein Verweis auf System.ComponentModel.Composition nötig.

Im zweiten Schritt basteln wir uns gleich mal ein PlugIn. Wir legen eine neue Assembly für das PlugIn an und fügen gleich einen Verweis auf unsere ‚Interface-Assembly‘ hinzu. Anschließend legen wir uns eine Klasse an, die unser Interface implementiert.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyFirstMEFPlugIn : IPlugIn
{
    public string Name { get; set; }
 
    private MyFirstMEFPlugIn()
    {
        Name = "My First PlugIn";
    }
 
    public void Execute()
    {
        MessageBox.Show(Name);
    }
}

Bei der Ausführung lasse ich hier den Namen des PlugIns in einer MessageBox ausgeben. Damit dies Funktioniert muss natürlich ein Verweis zu System.Windows.Forms hinzugefügt werden.

Was uns jetzt noch fehlt ist eine Anwendung, die all unsere PlugIns lädt. Am besten man legt sich eine neue Windows Forms Applikation mit einem ‚PlugIns laden …‘ Button an. Beim klick auf den Button sollen alle PlugIns im Anwendungsverzeichnis geladen und anschließend ausgeführt werden. Ich weiß nicht sehr intelligent, aber zum Testen ok :).
Zudem brauchen wir noch eine Klasse die unsere PlugIns ‚verwaltet‘. Fangen wir am besten mit dieser an. Wir fügen zu unserer Windows Forms Applikation eine neue Klasse hinzu und nennen diese ‚PlugInManagement‘. Damit uns unser Interface zur Verfügung steht brauchen wir auch wieder einen Verweis auf unsere gemeinsam genutzte Assembly.

1
2
3
4
5
6
7
8
9
10
11
12
public class PlugInManagement
{
    [ImportMany(typeof(IPlugIn))]
    public List<IPlugIn> LoadedPlugIns { get; set; }
 
    public void LoadPlugins()
    {
        var catalog = new DirectoryCatalog(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
        var compositionContainer = new CompositionContainer(catalog);
        compositionContainer.ComposeParts(this);
    }
}

So. Was haben wir hier gemacht? Zuerst haben wir uns eine Eigenschaft angelegt, die die gefundenen PlugIns aufnehmen wird. Diese haben wir mit dem Attribut [ImportMany(typeof(IPlugIn))] versehen. Durch ImportMany stellen wir sicher, dass wir alle gefunden PlugIns laden und nicht nur eine einzige Erweiterung.
Die Funktion ‚LoadPlugins‘ übernimmt dann den eigentlichen Ladevorgang. In der Zeile 8 legen wir uns einen neuen DirectoryCatalog an. Damit laden wir alle gefundenen PlugIns im angegebenen Verzeichnis. Als Verzeichnis habe ich hier unser Anwendungsverzeichnis gewählt. Dem CompositionContainer übergeben wir dann unseren Katalog und rufen anschließend ComposeParts auf.
Ein paar Verweise fehlen uns noch, die möchte ich natürlich nicht verschweigen:
System.IO, System.Reflection, System.ComponentModel.Composition und System.ComponentModel.Composition.Hosting

Jetzt noch schnell zu unsere Win-Forms Anwendung.

1
2
3
4
5
6
7
8
9
10
private void btnLoadPlugIns_Click(object sender, EventArgs e)
{
    PlugInManagement management = new PlugInManagement();
    management.LoadPlugins();
 
    for (int i = 0; i < management.LoadedPlugIns.Count; i++)
    {
        management.LoadedPlugIns[i].Execute();
    }
}

So. Alle Projekte kompilieren. Das PlugIn ins Verzeichnis der Windows Forms Applikation kopieren und anschließend die Applikation starten. Fertig!


2. Aktueller Aufbau im CD-Manager

Im CD-Manager habe ich mir mein eigenes PlugIn System gebastelt. Im Grunde eigentlich schon eine gute Basis für MEF, aber es gab leider ein Problem …
Auch im CD-Manager gibt es ein PlugIn Interface. Es gab im CD-Manager jedoch einen entscheidenden Unterschied: Da ich das Ganze nicht direkt zur Erweiterung des CD-Managers verwendet habe, sondern um Benutzerspezifische Funktionen auszulagern, habe ich immer nach einer bestimmten Assembly gesucht und geprüft, ob diese das PlugIn Interface implementiert. Per Reflection habe ich dann eine Instanz meiner Klasse erzeugt und die Funktionen aufgerufen. Da das Ganze System ja jetzt obsolete ist möchte ich da drauf jetzt auch gar nicht weiter eingehen …


3. Warum funktioniert das Ganze nicht?!?

Da ich wie gesagt bereits ein Interface für die Erweiterungen hatte, musste ich dies nur um das [InheritedExport] Attribut erweitern. Somit war eine Grundvoraussetzung für MEF schon geschaffen. Nachdem ich dann den Code im CD-Manager und die neue Funktionalität erweitert hatte war ich gespannt wie das Ganze in der Praxis funktioniert. Da ich bereits drei Verschiedene Anpassungen für den CD-Manager habe, konnte ich auch gleich zum Testen anfangen:

Test 1: Ein PlugIn (Im weiteren Verlauf PlugIn_A genannt):
Plugin in das dafür vorgesehene Verzeichnis kopiert; Anwendung gestartet; Alles wunderbar.

Test 2: Zweites PlugIn hinzugefügt (Im weiteren Verlauf PlugIn_B genannt):
Plugin in das dafür vorgesehene Verzeichnis kopiert; Anwendung gestartet; 😯 Was ist jetzt passiert? Im Debugger sehe ich, dass zwei PlugIns geladen wurden, jedoch sagt er mir, dass er zwei Mal PlugIn_A geladen hat.

Test 3: Drittes PlugIn hinzugefügt (Im weiteren Verlauf PlugIn_C genannt):
Plugin in das dafür vorgesehene Verzeichnis kopiert; Anwendung gestartet; 😡 Jetzt sagt der Debugger, dass drei PlugIns geladen wurden – jedoch drei Mal PlugIn_A.

💡 Einen kurzen Augenblick später hatte ich einen Verdacht und tatsächlich hat sich dieser Verdacht bestätigt:
Bei den PlugIn Assemblies war als Ausgabename bei allen PlugIns ‚CDManager.UserCustomization‘ angegeben. Da man schlecht drei Dateien mit dem gleichen Namen im selben Verzeichnis haben kann, habe ich diese einfach umbenannt. Das Umbenennen selbst stellt dabei kein Problem dar, aber der original Ausgabename. Ich habe noch nicht recherchiert, woran das genau liegt, jedoch spielt wohl der original Ausgabename bei MEF eine Rolle, wenn es darum geht PlugIns zu laden. Da es intern drei Mal derselbe Ausgabename war, wurde daher fälschlicherweise drei Mal PlugIn_A geladen.
Ich habe Anschließend den Ausgabenamen bei PlugIn_A angepasst und die Anwendung mit den drei PlugIns neu gestartet. Wie erwartet wurde PlugIn_A dann korrekt geladen und zusätzlich zwei Mal PlugIn_B. (Da PlugIn_B und PlugIn_C immer noch den gleichen Ausgabenamen hatten)


4. Fazit

Ein PlugIn-System mit MEF lässt sich schnell und einfach umsetzten. Jedoch gibt es auch hier den einen oder anderen Fallstrick. Programmieren wäre ja sonst langweilig, wenn alles auf Anhieb funktionieren würde 😉

Links:
MEF bei Codeplex

3 Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert