MEF Teil 3 – Lifecycle beeinflussen und überwachen

Das Binden der Composable Parts wurde im 1. Teil ausführlich vorgestellt. Bei einer Anwendung kann es aber notwendig sein, solche Verbindungen gezielt wieder aufzulösen ohne gleich den ganzen Container zu löschen. Des weiteren werden Schnittstellen vorgestellt, die die Parts darüber informieren, ob deren Verbindung hergestellt, oder der Part komplett gelöscht wurde.

Das Interface IPartImportsSatisfiedNotification

Für die Parts kann es hilfreich sein zu erfahren, wann das Binden abgeschlossen ist. Hierzu wird die Schnittstelle IPartImportsSatisfiedNotification implementiert. Die Schnittstelle kann sowohl im Import, als auch im Export implementiert werden.

[Export(typeof(ICarContract))]
public class BMW : ICarContract, IPartImportsSatisfiedNotification
{
    // ...
    public void OnImportsSatisfied()
    {
        Console.WriteLine("BMW import is satisfied.");
    }
}
class Program : IPartImportsSatisfiedNotification
{
    [ImportMany(typeof(ICarContract))]
    private IEnumerable<Lazy<ICarContract>> CarParts { get; set; }

    static void Main(string[] args)
    {
        new Program().Run();
    }
    void Run()
    {
        var catalog = new DirectoryCatalog(".");
        var container = new CompositionContainer(catalog);
        container.ComposeParts(this);
        foreach (Lazy<ICarContract> car in CarParts)
            Console.WriteLine(car.Value.StartEngine("Sebastian"));
        container.Dispose();
    }
    public void OnImportsSatisfied()
    {
        Console.WriteLine("CarHost imports are satisfied.");
    }
}

Beispiel 1 (Visual Studio 2010) auf GitHub

Bei der Ausführung des Programms wird nach container.ComposeParts() (Zeile 14) die Methode OnImportsSatisfied() vom Host aufgerufen. Wird zum ersten Mal auf ein Export zugegriffen, wird von diesem erst der Konstruktor, dann die Methoden OnImportsSatisfied() und StartEngine() ausgeführt.

Die Reihenfolge der Methodenaufrufe ändert sich deutlich, wenn nicht mit der Klasse Lazy<T> gearbeitet wird. In diesem Fall wird nach der Methode container.ComposeParts() erst der Konstruktor und dann die Methode OnImportsSatisfied() von allen Exports ausgeführt. Danach erst wird OnImportsSatisfied() vom Host aufgerufen und zum Schluss die Methode StartEngine() aller Exports.

Die Verwendung von IDisposable

Wie in .NET üblich, sollten auch die Exports die Schnittstelle IDisposable implementieren. Da das Managed Extensibility Framework die Parts verwaltet, sollte auch nur der Container, der die Parts beinhaltet, Dispose() aufrufen. Wird vom Container Dispose() aufgerufen, so ruft er von allen Parts ebenfalls Dispose() auf. Deshalb ist es wichtig, die Methode Dispose() vom Container aufzurufen, wenn dieser nicht mehr benötigt wird.

Freigabe von Exports

Wurde die Erstellungsrichtinie (Creation Policy) auf NonShared gesetzt, so werden gleiche Exports mehrfach erstellt. Diese werden erst dann wieder freigegeben, wenn der gesamte Container mit der Methode Dispose() zerstört wird. Gerade bei lang lebigen Anwendungen kann dieses zu Problemen führen. Deshalb besitzt die Klasse CompositionContainer die Methoden ReleaseExports() und ReleaseExport(). ReleaseExports() zerstört alle Parts, während ReleaseExport() nur einzelne Parts freigibt. Bei der Freigabe wird von jedem Export Dispose() aufgerufen, wenn von diesem die Schnittstelle IDisposable implementiert wurde. Somit lassen sich gezielt Exports wieder aus dem Container entfernen, ohne den ganzen Container zerstören zu müssen. Die Methoden ReleaseExports() und ReleaseExport() können nur auf Exports angewendet werden, deren Erstellungsrichtlinie auf NonShared steht.

Bei dem folgenden Beispiel wurde in jedem Export die Schnittstelle IDisposable implementiert.

using System;
using System.ComponentModel.Composition;
using CarContract;
namespace CarBMW
{
    [Export(typeof(ICarContract))]
    public class BMW : ICarContract, IDisposable
    {
        private BMW()
        {
            Console.WriteLine("BMW constructor.");
        }
        public string StartEngine(string name)
        {
            return String.Format("{0} starts the BMW.", name);
        }
        public void Dispose()
        {
            Console.WriteLine("Disposing BMW.");
        }
    }
}

Der Host bindet zuerst alle Exports auf den Import. Nach dem Aufruf der Methode StartEngine() werden alle Exports durch die Methode ReleaseExports() wieder freigegeben. Nach erneutem Binden der Exports auf den Import werden dieses Mal die Exports einzeln entfernt. Zum Schluss wird durch die Methode Dispose() der Container zerstört.

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using CarContract;
namespace CarHost
{
    class Program
    {
        [ImportMany(typeof(ICarContract), RequiredCreationPolicy = CreationPolicy.NonShared)]
        private IEnumerable<Lazy<ICarContract>> CarParts { get; set; }

        static void Main(string[] args)
        {
            new Program().Run();
        }
        void Run()
        {
            var catalog = new DirectoryCatalog(".");
            var container = new CompositionContainer(catalog);

            container.ComposeParts(this);
            foreach (Lazy<ICarContract> car in CarParts)
                Console.WriteLine(car.Value.StartEngine("Sebastian"));

            Console.WriteLine("");
            Console.WriteLine("ReleaseExports.");
            container.ReleaseExports<ICarContract>(CarParts);
            Console.WriteLine("");

            container.ComposeParts(this);
            foreach (Lazy<ICarContract> car in CarParts)
                Console.WriteLine(car.Value.StartEngine("Sebastian"));

            Console.WriteLine("");
            Console.WriteLine("ReleaseExports.");
            foreach (Lazy<ICarContract> car in CarParts)
                container.ReleaseExport<ICarContract>(car);

            Console.WriteLine("");
            Console.WriteLine("Dispose Container.");
            container.Dispose();
        }
    }
}

Die Ausgabe des Programms sieht dementsprechend wie folgt aus:

CommandWindowSample02

Beispiel 2 (Visual Studio 2010) auf GitHub

Ausblick

Im 4. Teil geht es um Vererbung bei den Composable Parts. Wie verhält sich MEF, wenn Klassen das Attribut Import oder Export enthalten und diese Klassen innerhalb einer Vererbungshierarchie enthalten sind.

Author: Stefan Henneken

I’m Stefan Henneken, a software developer based in Germany. This blog is just a collection of various articles I want to share, mostly related to Software Development.

7 thoughts on “MEF Teil 3 – Lifecycle beeinflussen und überwachen”

  1. Prima Reihe zum Thema MEF.
    Frage zu diesem Teil: OnImportsSatisfied -> was wäre denn ein klassischer Anwendungsfall, wenn die Methode aufgerufen wird, was behandelt man im Rahmen dieses Events? Das ist mir nicht klar geworden.

    Implementierung von IDisposable: den Container erstelle ich mit einer using-Anweisung, soweit so gut, wozu dann noch IDisposable auf die Exportklasse? Der Catalog wird in den Beispielen immer innerhalb der Methode deklariert…

    1. OnImportsSatisfied ist z.B. bei den Exports sinnvoll. Ein Export kann über den Konstruktor weitere Imports erhalten. Der Zugriff auf diese Elemente ist aber erst dann möglich, wenn das Komposen komplett abgeschlossen ist. Und genau dieses wird durch die Methode OnImportsSatisfied bekannt gegeben.

      Wird der Container durch die Methode Dispose zerstört (genau das passiert ja bei der using-Anweisung), so wird auch bei jedem Export Dispose aufgerufen. Dazu muss das zugehörige Export natürlich das Interface IDisposable implementiert haben. Somit hat jeder Export die Gelegenheit „aufzuräumen“.

  2. Hallo,

    ein ausgezeichnetes Tutorial.

    Ich habe in Sample02 im Host ab Zeile 39 noch einmal gebunden:

    Console.WriteLine(“”);

    container.ComposeParts(this);
    foreach (Lazy car in CarParts)
    Console.WriteLine(car.Value.StartEngine(“Sebastian”));

    um zu demonstrieren, dass ein Aufruf von Dispose auf dem Container ein Aufruf von Dispose auf jedem Export nachsichzieht, wie in Deiner Antwort vom 29.03.2013, Absatz 2, ausgeführt.

    Gruß
    Felix

  3. Hallo, durch ReleaseExports wird zwar auf jedem Export Dispose() aufgerufen, man kann die Exports im Host aber einfach weiterverwenden, ohne sie durch container.ComposeParts wieder aufbauen und binden zu müssen. Also sind die Export-Instanzen im Host doch noch völlig intakt? Was meinst du?

    Console.WriteLine(“ReleaseExports.”);
    container.ReleaseExports(CarParts);
    Console.WriteLine(“”);

    foreach (Lazy car in CarParts)
    Console.WriteLine(car.Value.StartEngine(“Sebastian”));

    danke fürs gute Tutorial,
    Thomas

    1. Zur Behauptung von Thomas (30.10.2012):
      Diese Fragestellung hat mit MEF meines Erachtens nichts am Hut.
      Korrigiert mich bitte, wenn ich falsch liege:
      Nur weil Du eine Methode namens “Dispose” aufrufst, ist das Objekt doch nicht gleich zerstört?
      Führe “CarParts.GetEnumerator().Dispose();” aus – Du kannst weiterhin durch CarParts iterieren.
      “Zerstören” kannst Du die Export-Instanzen, indem Du CarParts = null setzt und damit einfach keinen Zugriff mehr auf jene hast.
      Selbst dann fliegen sie noch irgendwo herum, bis der GC sie zermalmt.

      Dem ungehindert sollte man Dispose trotzdem verwenden und sinnvoll einbinden (siehe http://msdn.microsoft.com/de-de/library/66x5fx1b.aspx).

      Das Tutorial geht gut rein, vielen Dank dafür!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: