MEF Teil 4 – Vererbung mit Composable Parts

Klassen, welche die Attribute Import und Export enthalten, können von anderen Klassen geerbt werden. Das Verhalten vom Managed Extensibility Framework (MEF) hat hierbei einige Besonderheiten, auch in Bezug auf mögliche Metadaten. Hilfestellung bietet das Attribut InheriedExport.

Enthält eine Klasse das Attribut Export, so wird dieses nicht an Unterklassen vererbt. Die Unterklasse kann nicht von MEF gefunden und mit anderen Parts verbunden werden. Importe verhalten sich genau entgegengesetzt. Das Attribut Import wird immer vererbt. Somit ist eine Klasse, die ein Import geerbt hat, ebenfalls ein Composable Part.

[Export]
public class ClassOne
{
    [Import]
    public object ImportParts { get; set; }
}
public class ClassTwo : ClassOne
{
    // ...
}

Die Eigenschaft ImportParts erbt die Klasse ClassTwo von ClassOne. Da das Attribut Import ebenfalls vererbt wird, kann die Eigenschaft ImportParts der Klasse ClassTwo mit kompatiblen Parts gebunden werden. Gewöhnliche Exports werden nicht geerbt, sodass die Klasse ClassTwo keine Daten exportiert.

Das Attribut InheritedExport

Das Verhalten ändert sich, wenn statt des Attributs Export, das Attribut InheritedExport benutzt wird. Unterklassen, die von solchen Klassen erben, erben auch den gleichen Export, einschließlich des Vertragstyps und des Vertragsnamens. Einzige Einschränkung: Das Attribut InheritedExport kann nur auf Klassen und Schnittstellen angewendet werden, nicht auf Memberebene.

[InheritedExport]
public class ClassOne
{
    [Export]
    public object ExportParts { get; set; }

    [Import]
    public object ImportParts { get; set; }
}
public class ClassTwo : ClassOne
{
    // ...
}

Die Klasse ClassTwo erbt von der Klasse ClassOne. Da ClassOne das Attribut InheritedExport verwendet, erbt ClassTwo den Export von ClassOne, inklusive dem Vertragstyp. Die Eigenschaft ExportParts erbt die Klasse ClassTwo zwar ebenfalls, allerdings ohne das Attribut Export. Exports auf Memberebene werden niemals vererbt. Das Attribut InheritedExport hat keinen Einfluss auf Importe. Die Eigenschaft ImportParts wird inklusive dem Attribut Import an ClassTwo vererbt.

Vererbung von Metadaten

Metadaten werden durch das Attribut InheritedExport ebenfalls vererbt. Diese geerbten Metadaten können von Unterklassen nicht verändert oder durch zusätzliche Metadaten ergänzt werden. Allerdings können durch erneutes Dekorieren der Klasse mit dem Attribut InheritedExport neue Metadaten deklariert werden. Vertragstyp und Vertragsname müssen hierbei die gleichen sein, wie bei der Basisklasse. Ansonsten wird ein weiterer, unabhängiger Export erstellt. Daraus ergibt sich die Tatsache, dass bei einem InheritedExport der Vertragsname und Vertragstyp immer angegeben werden müssen.

Bei dem folgenden Beispiel erben zwei Klassen (die Klassen Mercedes und BMW) von einer gemeinsamen Basisklasse (CarBase).

using System;
using System.ComponentModel.Composition;

namespace CarContract
{
    [InheritedExport(typeof(ICarContract))]
    [ExportMetadata("Name", "no name")]
    [ExportMetadata("Color", "no color")]
    public class CarBase : ICarContract
    {
        public virtual string StartEngine(string name)
        {
            return String.Format("{0} starts the BaseCar.", name);
        }
    }
}

Bei Mercedes werden keine Metadaten oder das Attribut Export angegeben. Beides wird von CarBase geerbt.

using System;
using CarContract;

namespace CarMercedes
{
    public class Mercedes : CarBase
    {
        public override string StartEngine(string name)
        {
            return String.Format("{0} starts the Mercedes.", name);
        }
    }
}

Die Klasse BMW wird mit den Attributen InheritedExport und ExportMetadata erneut dekoriert. Somit erhält BMW komplett neue Metadaten.

using System;
using System.ComponentModel.Composition;
using CarContract;

namespace CarBMW
{
    [InheritedExport(typeof(ICarContract))]
    [ExportMetadata("Name", "BMW")]
    [ExportMetadata("Price", (uint)40000)]
    public class BMW : CarBase
    {
        public override string StartEngine(string name)
        {
            return String.Format("{0} starts the BMW.", name);
        }
    }
}

Nach der Ausführung des Programms ist gut zu erkennen, dass die Klasse Mercedes die gleichen Metadaten hat wie die Basisklasse. Die Klasse BMW enthält dagegen komplett neue Metadaten.

Unbenannt

Allerdings ist es störend, dass die Basisklasse CarBase ebenfalls ein Composable Part ist. Dieses ist nicht immer erwünscht. Abhilfe schafft hier die Verwendung von Schnittstellen.

Beispiel 1 (Visual Studio 2010) auf GitHub

Schnittstellen

Die Attribute Import und Export können nicht an Schnittstellen angegeben werden, da Schnittstellen nicht direkt instanziiert werden können. Eine Schnittstelle kann allerdings auf Schnittstellenebene mit dem Attribut InheritedExport dekoriert werden. Dieser Export (mit den Metadaten) wird von jeder beliebigen Klasse geerbt. Das obige Beispiel braucht nur so angepasst werden, dass aus der Basisklasse CarBase eine Schnittstelle wird: ICarBase. Da ICarContract schon die Methode StartEngine definiert und ICarBase von ICarContract abgeleitet ist, kann die Methode in ICarBase entfallen.

using System;
namespace CarContract
{
    public interface ICarContract
    {
        string StartEngine(string name);
    }
}

using System;
using System.ComponentModel.Composition;
namespace CarContract
{
    [InheritedExport(typeof(ICarContract))]
    [ExportMetadata("Name", "no name")]
    [ExportMetadata("Color","no color")]
    public interface ICarBase : ICarContract {  }
}

Bei der Ausführung des Beispiels ist zu erkennen, dass nur noch zwei Composable Parts vorhanden sind.

Unbenannt2

Beispiel 2 (Visual Studio 2010) auf GitHub

Benutzerdefinierte Exportattribute

Die Attribute Export und InheritedExport können so erweitert werden, dass in den Attributeigenschaften die Metadaten enthalten sind. Diese Art der benutzerdefinierten Attribute beinhaltet Vertragsname, Vertragstyp und die Metadaten. Das Deklarieren der Composable Parts wird hierdurch erheblich vereinfacht, insbesondere wenn die Metadaten umfangreich und bei vielen Parts benötigt werden.

Im 2. Teil zum MEF ‘Metadaten und Erstellungsrichtlinien’ wurden schon vier Arten der benutzerdefinierten Attribute vorgestellt. Das folgende Bespiel stellt eine weitere Variante da.

Bei diesem Beispiel werden die Metadaten durch die Klasse CarAttribute dargestellt. Diese Klasse muss mit dem Attribut MetadataAttribute dekoriert werden. Zusätzlich wird die Klasse von ExportAttribute oder InheritedExportAttribute abgeleitet. Somit stellt diese auch das Attribut Export bereit. Der Konstruktor leitet den Vertragstyp ICarContract an die Klasse ExportAttribute weiter.

using System;
using System.ComponentModel.Composition;

namespace CarContract
{
    [MetadataAttribute]
    [AttributeUsage(AttributeTargets.Class)]
    public class CarAttribute : ExportAttribute
    {
        public CarAttribute()
            : base(typeof(ICarContract))
        {
            // set default values
            this.Name = "noName";
            this.Color = CarColor.Unkown;
            this.Price = 0;
        }

        public string Name { get; set; }
        public CarColor Color { get; set; }
        public uint Price { get; set; }
    }
}

Die Klasse CarAttribute definiert ein benutzerdefiniertes Attribut mit den Namen Car und den Vertragstyp ICarContract. Das Dekorieren einer Klasse mit dem Attribut Car sieht wie folgt aus:

[Car(Name="Mercedes", Color=CarColor.Blue, Price=48000)]
public class Mercedes : ICarContract
{
    public string StartEngine(string name)
    {
        return String.Format("{0} starts the Mercedes.", name);
    }
}

Ohne die Klasse CarAttribute müssten die Metadaten und der Vertragstyp separat angegeben werden, was deutlich aufwendiger wäre.

[Export(typeof(ICarContract))]
[ExportMetadata("Name", "Mercedes")]
[ExportMetadata("Color", CarColor.Blue)]
[ExportMetadata("Price", (uint)48000)]
public class Mercedes : ICarContract
{
    public string StartEngine(string name)
    {
        return String.Format("{0} starts the Mercedes.", name);
    }
}

Beispiel 3 (Visual Studio 2010) auf GitHub

Im nächsten Teil geht es dann um die Kataloge und Container.

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.

4 thoughts on “MEF Teil 4 – Vererbung mit Composable Parts”

  1. Hallo Stefan,

    Vielen Dank für das tolle Tutorial. Da kann ich viel lernen.
    Auch wenn es schon eine Weile her ist, hoffe ich dass Du hier noch mitliest.
    Ich habe nämlich zwei Probleme:

    1. Kann ich die Beispiele nicht mehr downloaden.
    2. Scheine ich hier einen Fehler zu machen. Ich bekomme folgende Exception:

    An unhandled exception of type ‘System.ComponentModel.Composition.CompositionContractMismatchException’ occurred in System.ComponentModel.Composition.dll

    Additional information: Es kann keine Instanz der Metadatenansicht “CarContract.CarAttribute, CarContract, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null” erstellt werden, da kein Konstruktor ausgewählt werden konnte. Stellen Sie sicher, dass von dem Typ ein Konstruktor implementiert wird, der ein Argument vom Typ “IDictionary” erhält.

    Und hier meine Programmklasse:

    class Program {

    [ImportMany(typeof(ICarContract))]
    private IEnumerable<Lazy> 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 car in CarParts) {

    Console.WriteLine(car.Metadata.Name);
    Console.WriteLine(car.Metadata.Color);
    Console.WriteLine(car.Metadata.Price);
    Console.WriteLine(“”);
    }

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

    Console.ReadLine();
    }
    }

    Vielleicht siehr ja jemand meinen Fehler.

    Gruß

    Lothar

    1. Hallo Stefan,

      nachdem Du mir die Beispiele geschickt hast, hab ich es gleich rausgefunden.
      Siehe unten.
      Was mir aber noch nicht ganz klar ist, ist der Bezug zwischen
      ICarMetadata und CarAttribute.

      CarAttribute ist nicht von ICarMetadata abgeleitet.
      ICarMetadata hat auch kein Base.
      Und trotzdem können die Informationen aus CarAttribute in
      ICarMetaData abgebildet werden.
      Irgendwie fehlt mir an der Stelle noch ein wenig das Verständnis.

      class Program {

      [ImportMany(typeof(ICarContract))]
      private IEnumerable<Lazy> 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 car in CarParts) {

      Console.WriteLine(car.Metadata.Name);
      Console.WriteLine(car.Metadata.Color);
      Console.WriteLine(car.Metadata.Price);
      Console.WriteLine(“”);
      }

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

      Console.ReadLine();
      }
      }

      Gruß Lothar

      1. Hallo Lothar,

        Die Beispiele habe ich auf OneDrive abgelegt. Aus irgendeinen Grund fehlten alle Dateifreigaben. Somit war ein Zugriff auf die Beispiele nicht möglich. Ich habe damit begonnen die Links wieder anzupassen. Bis ich alle Posts allerdings durch habe, kann es noch eine Weile dauern. Wenn noch andere Beispiele benötigt werden, auf die derzeit kein Zugriff möglich ist, so kann ich dir diese zumailen.

        Zu deiner eigentlichen Frage:
        Das Interface ICarMetadata dient dazu die Datentypen der einzelnen Metadaten festzulegen. Ohne Interface sind die Metadaten einfach Name/Value Paare, wobei Value immer vom Datentyp Object ist.
        Ein Import sieht ohne Interface wie folgt aus:
        [ImportMany(typeof(ICarContract))]
        private IEnumerable<Lazy<ICarContract, Dictionary>> CarParts { get; set; }

        Mit Interface:
        [ImportMany(typeof(ICarContract))]
        private IEnumerable<Lazy> CarParts { get; set; }

        Die Klasse CarAttribute definiert ein ‘normales’ Attribut. Dadurch wird nicht nur der Datentyp der Metadaten definiert, sondern Visual Studio weis auch, welche Metadaten alles erlaubt sind.
        statt:
        [ExportMetadata(“Name”, “BMW”)]
        [ExportMetadata(“Color”, CarColor.Black)]
        [ExportMetadata(“Price”, (uint)55000)]
        [Export(typeof(ICarContract))]
        public class BMW : ICarContract

        kann geschrieben werden:
        [CarMetadata(Name = “BMW”, Color = CarColor.Black, Price = 55000)]
        [Export(typeof(ICarContract))]
        public class BMW : ICarContract

        Die Verbindung zwischen dem Interface ICarAttribute und der Klasse CarAttribute wird durch das Attribute ‘MetadaAttribute’ hergestellt.

        Das Interface ICarAttribute vereinfacht die Handhabung der Imports (im Host), da dadurch die Metadaten typsicher werden.
        Die Klasse CarAttribute vereinfacht die Handhabung an den Exports (CarBMW, …), da dadurch die einzelnen möglichen Metadaten schon beim Editieren in Visual Studio bekannt werden.

        Im 2. Teil der Artikelserie gehen ich speziell auf die Metadaten ein. Vielleicht helfen dir dort die Beispiele weiter. Die dortigen Beispiele müssten wieder verfügbar sein 😉

        Stefan

Leave a comment