Bisherige Artikel der Serie:
Im ersten Artikel dieser Serie ist beschrieben, was das Managed Extensibility Framework ist und welche Möglichkeiten es bietet. In diesem Artikel werde ich eine erste Einführung in die Verwendung des MEF zeigen. Anhand einer einfachen Demo-Anwendung soll der Einsatz demonstriert werden. Programme, die das MEF einsetzen, müssen die Assembly System.ComponentModel.Composition referenzieren. Sie kann hier heruntergeladen werden.
In der Demo-App, einem einfachen WPF-Programm, soll ein TabControl mit bestimmten Seiten gefüllt werden. So sieht das Ergebnis aus:

In einem ersten Schritt zeige ich die Umsetzung ohne den Einsatz von MEF. Die Anwendung besteht aus einem Hauptfenster, welches ein TabControl enthält und drei UserControls, die als Seiten in das TabControl geladen werden.
XAML-Code des Hauptfensters:
<Window x:Class="MefDemoApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="500" Width="600">
<Grid>
<TabControl ItemsSource="{Binding}" DisplayMemberPath="Title">
<TabControl.ContentTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Page}"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
CodeBehind des Hauptfensters:
public partial class MainWindow : Window
{
public UserControl Page1 = new Page1();
public UserControl Page2 = new Page2();
public UserControl Page3 = new Page3();
public MainWindow()
{
InitializeComponent();
DataContext = new List<object>
{
new {Title = "Page 1", Page = Page1},
new {Title = "Page 2", Page = Page2},
new {Title = "Page 3", Page = Page3}
};
}
}
Der DataContext des Hauptfensters wird mit einer Liste von anonymen Objekten geladen, die jeweils einen Titel und die entsprechende Seite enthalten. Die Titel-Eigenschaft wird für die Beschriftung des Tabs verwendet. Wie im XAML-Code zu sehen ist, wird die ItemsSource des TabControls an den DataContext gebunden und der Content der Tab-Seiten an die Page-Eigenschaft gebunden.
Die Klassen Page1, Page2 und Page3 sind UserControls mit beliebigem Inhalt.
Ein erstes Problem, das im Quelltext zu sehen ist besteht in der direkten Abhängigkeit zu den UserControls. Die Erzeugung der Seiten mittels new Page1() reduziert die Wartbarkeit und die Erweiterbarkeit. Das Hauptfenster sollte nicht dafür verantwortlich sein, welche Seiten angezeigt werden. Erst recht sollte es die Seiten nicht selbst erzeugen.
Die wichtigsten Bestandteile des MEF sind der Katalog und der CompositionContainer. Der Katalog ist verantwortlich für Bereitstellung von Erweiterungen und der CompositionContainer sorgt für die Erzeugung dieser Erweiterungen und der Erfüllung der Abhängigkeiten.
Eine Erweiterung wird im MEF ComposablePart genannt. Ein ComposablePart enthält ein oder mehrere Komponenten (Exports), die anderen ComposableParts zur Verfügung gestellt werden. Es kann Abhängigkeiten zu anderen ComposableParts definieren. Diese Abhängigkeiten werden Imports genannt. Der CompositionContainer erzeugt ein ComposablePart und füllt die als Import deklarierten Komponenten mit den Komponenten, die als Export deklariert sind. Die Verbindung eines Imports mit einem Export ist möglich, da ein Contract zwischen Import und Export definiert wird. Der CompositionContainer kann über diesen Contract feststellen, welche exportierte Komponente zu welcher importierenden Komponente gemappt werden muss.
Die Deklaration der UserControls sieht bei der Verwendung des MEF nun so aus:
[Import(
"Page1")]
public UserControl Page1;
[Import("Page2")]
public UserControl Page2;
[Import("Page3")]
public UserControl Page3;
Wie hier zu sehen ist, werden die Instanzen nicht mehr explizit mittels new erzeugt. Stattdessen sind sie mit einem Import-Attribut versehen. Jedes Import-Attribut erthält eine Zeichenkette, die den Contract zum jeweiligen Export darstellt. Der CompositionContainer wird versuchen die Felder, die mit einem Import gekennzeichnet sind, mit einer passenden Komponente zu initialisieren.
Dafür müssen die UserControls natürlich als Export deklariert werden. Der CodeBehind eines UserControls sieht dann so aus:
[Export("Page1")]
public partial class Page1 : UserControl
{
public Page1()
{
InitializeComponent();
}
}
Die Klasse wird mit dem Export-Attribut versehen. Das Attribut erhält hier die Zeichenkette "Page1". Das ist der Contract, über den Imports mit derselben Bezeichnung verknüpft werden können. Die Contract-Bezeichnung kann im Export-Attribut weggelassen werden. Ist das der Fall, wird der Klassenname als Contract-Bezeichnung verwendet.
Jetzt fehlt eigentlich nur noch die Herstellung der Verbindung zwischen exportierten und importierenden Parts. Diese Aufgabe übernimmt der CompositionContainer. Im folgenden Quelltext ist zu sehen, wie der Konstructor des Hauptfensters erweitert wurde:
public MainWindow()
{
InitializeComponent();
var catalog = new AttributedAssemblyPartCatalog(Assembly.GetExecutingAssembly());
var container = new CompositionContainer(catalog.CreateResolver());
container.AddPart(this);
container.Compose();
DataContext = new List<object>
{
new {Title = "Page 1", Page = Page1},
new {Title = "Page 2", Page = Page2},
new {Title = "Page 3", Page = Page3}
};
}
Ein spezieller Katalog wird erzeugt, damit Exports und Imports gefunden werden können, die sich in der aktuellen Assembly befinden. Neben dem AttributedAssemblyPartCatalog gibt es noch diese Katalog-Varianten:
- AttributedTypesPartCatalog - lädt Erweiterungen, die als Typen angegeben wurden
- AggregatingComposablePartCatalog - fässt mehrere Cataloge zusammeny
-
DirectoryPartCatalog - lädt Erweiterungen aus einem angegebenen Verzeichnis
Der CompositionContainer wird erzeugt und erhält dabei eine ValueResolver-Instanz des Kataloges. Der Container benötigt diesen Resolver um die einzelnen Instanzen der angeforderten Erweiterungen erzeugen zu können. Nun muss nur noch mittels container.AddPart(this) die aktuelle Instanz des Hauptfensters hinzugefügt werden. Jetzt hat der Container alle Informationen um beim Aufruf der Compose()-Methode alle notwendigen Erweiterungen zusammenzusammeln und die Abhängigkeiten zu mit korrekten Instanzen zu füllen.
In dem vorliegenden Beispiel erkennt das MEF, dass die MainWindow-Instanz Parts importiert, die die Contracts "Page1", "Page2" und "Page3" haben. Dem CompositionContainer wurde ein Katalog zur Verfügung gestellt wurde, der ein exportierenden Part für jeden dieser Contracts enthält. Daher kann im Compose()-Aufruf auch das Setzen der einzelnen Seiten stattfinden.
Was haben wir eigentlich erreicht? Ich gebe zu, der Quelltext ist nicht gerade hübscher geworden. Für dieselbe Funktionalität ist er zudem auch noch um einige Zeilen gewachsen. Allerdings haben wir eine direkte Abhängigkeit zu den UserControls etwas "aufgeweicht". Dort wo vorher die Typen und deren Konstruktor-Aufrufe standen, haben wir nun eine Abstrahierung in Form von Kontrakten.
Noch besser ist, dass an dieser Stelle nicht Schluss ist. Das hier gezeigte Beispiel war nur ein kleiner Auschnitt. In folgenden Artikeln werde ich auf die fortgeschritteren Möglichkeiten des MEF eingehen.
Der Quelltext der Beispiel-Anwendung kann
hier heruntergeladen werden.