Solution Explorer

MVVM Tutorial – Part 7 (MEF)

Solution Explorer

Solution Explorer

In diesem letzten Teil unseres Tutorials möchte ich mich mit MEF in Zusammenhang mit dem MVVM Pattern beschäftigen. MEF steht für Manged Extensibility Framework und bietet uns eine Möglichkeit, unsere Komponent mit einer sehr losen Kopplung zu verknüpfen. Hier werden wir den Pfad Model – ViewModel – StartApplication mit MEF entkoppeln. Man kann damit auch das View entkoppeln, verliert dann aber die Möglichkeit, das View einfach per XAML einzubinden.

Als erstes löschen wir das ModelOutput Projekt, da es sowieso nicht mehr benötigt wird und nur zusätzliche Arbeit bedeuten würde.

Nun müssen wir im Model die PersonList so umbauen, das sie nicht mehr statisch ist, sondern instanziierbar. Das instanziieren übernimmt dann später MEF für uns. Dies ist nötig, da wir im nächsten Schritt Interfaces extrahieren wollen und es keine Interfaces für statische Felder gibt. Außerdem spendieren wir der PersonList eine Methode AddPerson, welche die hinzugeügte Person zurückgibt, da unser ViewModel ja keine konkrete Implementation der Person Klasse mehr kennen soll und somit auch keine mehr selbst erzeugen kann.

    public class PersonList
    {
        public IList<IPerson> Persons { get; private set; }

        private PersonList()
        {
            Persons = new List<IPerson>()
            {
                new Person(){Name = "John Doe", 
                    Street = "Onestreet", 
                    PostalCode = 12345, 
                    City = "Onecity"},
                new Person(){Name = "Jane Doe", 
                    Street = "Onestreet", 
                    PostalCode = 12345, 
                    City = "Onecity"},
                new Person(){Name = "Foo Bar", 
                    Street = "Twostreet", 
                    PostalCode = 54321, 
                    City = "Twocity"}
            };
        }

        public IPerson AddPerson()
        {
            var person = new Person();
            Persons.Add(person);
            return person;
        }

    }

Nachdem wir die PersonList Klasse nun so umgebaut haben, extrahieren wir die Interfaces IPerson und IPersonList aus den beiden Klassen. Man beachte hierbei, dass die PersonList immer mit dem IPerson Interface arbeiten sollte und nie die konkrete Person Klasse nach außen geben sollte. Die Peiden Interfaces machen wir nun public und verschieben sie in ein neues Projekt Namens PersonContracts. Hierbei dürfen wir die Referenz von Model auf ModelContracts nicht vergessen, damit unsere Klassen auch ihre Interfaces kennen.

Da die PersonList nun nicht mehr statisch ist, könenn wir in dem PersonListViewModel auch nicht mehr statisch darauf zugreifen. Um das zu beheben, ändern wir unser PersonListViewModel so ab, dass es vom generischen Typ ViewModel erbt. Dementsprechend muss natürlich auch der Konstruktor umgeändert werden.Unsere PersonListViewModel Klasse wird nun Fehler werfen, da es keine Objekte vom Typ Person mehr erzeugen kann, da es ja die konkrete Klasse nicht mehr kennt. Da wir aber in weiser Voraussicht der PersonList die Methode AddPerson() spendiert haben. Wir ändern also die ExecuteAddPersonCommand Methode zu

        private void ExecuteAddPersonCommand()
        {
            Persons.Add(new PersonViewModel(Model.AddPerson()));
        }

Um nun keine Endlosschleifen zu erzeugen, müssen wir auch die Persons_CollectionChanged Methode ändern, um dies abzufangen:

        void Persons_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                foreach (PersonViewModel vm in e.NewItems)
                {
                    if (!Model.Persons.Contains(vm.Model))
                    {
                        Model.Persons.Add(vm.Model);
                    }
                }
            }
            else if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach (PersonViewModel vm in e.OldItems)
                {
                    if (!Model.Persons.Contains(vm.Model))
                    {
                        Model.Persons.Remove(vm.Model);
                    }
                }
            }
            else if (e.Action == NotifyCollectionChangedAction.Reset)
            {
                Model.Persons.Clear();
            }
        }

Jetzt machen wir das gleiche, wie vorher mit den Models auch mit den ViewModels. Interfaces extrahieren, die KLassen so umschreiben, dass sie mit den Interfaces arbeiten und die Interfaces in ein neues Projekt ViewModelContracts verschieben. In den Referenzen vom ViewModel Projekt löschen wir nun die Referenz auf Model und fügen Referenzen auf ModelContracts und ViewModelContracts hinzu.

Beim StartApplication Projekt entfernen wir nun auch die Referenzen zu ViewModel und ViewModelBase und fpügen eine Referenz zu ViewModelContracts hinzu.

Jetzt haben wir unsere Komponenten soweit umgeschrieben, dass wir sie mit MEF komponieren (tolles Wort, oder?) können. MEF finden wir in der Assembly System.ComponentModel.Composition, wesshalb alle Projekte (Model, ViewModel, StartApplication), welche damit arbeiten sollen, eine Referenz darauf benötigen.
Fangen wir wieder mit dem Model an: Wir schreiebn folgende Annotation an unsere PersonList Klasse:

    [Export(typeof(IPersonList))]
    public class PersonList : IPersonList
    {
    // ...

Damit sagen wir MEF, das wir diese Klasse als eine Instanz vom Typ IPersonList exportieren wollen.

In dem PersonListViewModel Importieren wir nun dieses Objekt in den Konstructor.

        [ImportingConstructor]
        public PersonListViewModel(IPersonList model)
            : base(model)
        {
        // ...

Den Typ müssen wir hier nicht explizit spezifizieren, da MEF sich diesen implizit aus dem Parameter holt. Man könnte aber auch schreiben:

        [ImportingConstructor(typeof(IPersonList))]
        public PersonListViewModel(IPersonList model)
            : base(model)
        {
        // ...

Beides hat in diesem Fall den gleichen Effekt. Bei dem Export vom Model haben wir jedoch explizit das Interface angegeben, da MEF sonst ein Objekt vom Typ PersonList und nicht vom Typ IPersonList exportieren würde.

Unser PersonListViewModel exportieren wir nun wieder:

    [Export(typeof(IPersonListViewModel))]
    public class PersonListViewModel : ViewModel<IPersonList>, IPersonListViewModel
    {
    // ...

Um es in der MainWindow.xaml.cs vom StartApplication Projekt wieder in eine private Property zu importieren.

        [Import]
        private IPersonListViewModel viewModel;

Die SetupBindings Methode wird nun so umgeschrieben, dass sie diese Property nutzt und nicht ein neues Objekt erstellt.

Jetzt haben wir MEF mitgeteilt, was wir exportieren und was wir importieren wollen. Nun müssen wir MEF noch einmal sagen, dass diese ganzen Komponenten bitte auch zusammengefügt werden sollen. Dazu erstelenw ir eine neue Methode im MainWindow, welche im Konstruktor vor SetupBindings aufgerufen werden muss:

        private void MefInitialization()
        {
            var catalog = new AggregateCatalog();
            catalog.Catalogs.Add(new DirectoryCatalog("."));

            var container = new CompositionContainer(catalog);
            container.SatisfyImportsOnce(this);
        }

Es wird zu erst ein AggregateCatalog erstellt. Dieser kann mehrere Kataloge unterschiedlichen Typs aufnehmen. In diesem Fall wollen wir aber nur einen DirectoryCatalog, welcher das aktuelle Verzeichniss durchsuchen soll. Anschließend wird ein container erstellt, welcher unsere Komponenten zusammenfügt.

An diesem Punkt solltet ihr das Projekt starten können, dann aber zur Laufzeit einen Fehlre von MEF bekommen. Dies liegt daran, dass die .dlls unserer einzelnen Projekte nicht mehr automatisch in das \bin\Debug Verzeichniss von StartApplication kopiert werden, da wir ja keine Referenzen mehr auf die Projekte haben. Man könnte nun den OutputPath der einzelnen Projekte alle auf das \bin\Debug Verzeichniss des StartApplication Projekts zeigen lassen. Die, wie ich finde, schönere Methode ist aber, ein Post Build Event (Unter den Projekteigenschaften – Build Events zu finden)

copy "$(TargetPath)" "$(SolutionDir)StartApplication\bin\Debug"

Dieses fügen wir nun in allen benötigten Projekten ein. Wenn man die Solution nun neu kompilliert, sollten im \bin\Debug Verzeichniss des StartApplication Projekts die benötigten .dlls auftauchen. Wenn man das Projekt jetzt ausführt, sollte auch alles laufen und wir haben die gewünschte lose Kopplung erreicht.

Am Ende wieder die Solution: MVVMTutorial Solution (Model, ViewModelBase, ViewModel, View, StartApplication und View Nr. 2 mit MEF entkoppelt)

Damit bin ich auch schon am Ende meines MVVM Tutorials angekommen. Ich hoffe, ich konnte einigen damit helfen, MVVM zu lernen und zu verstehen. Über eine Rückmeldung würde ich mich natürlich freuen.

4 Gedanken zu „MVVM Tutorial – Part 7 (MEF)

  1. Valyr

    Hi,
    sehr schönes Tutorial. Hab etwas Anlauf gebraucht um MVVM zu verstehen und du knüpfst auch noch MEF dran. Habe meine eigene Anwendung nun mit beidem ausgestattet und es funktioniert wunderbar.
    Einen kleinen Wunsch hätte ich noch, ich habe eine Ribbonbar in der ich gerne all die Buttons usw. die bisher in den einzelnen Views sind als zusätzliche RibbonGroup einbinden möchte. Das will bei mir nicht so recht funktionieren.
    Hast du da eine Beispiel oder könntest du da ein kleines Tutorial machen wie man eine Ribbonbar mittels MVVM&MEF füttert?

    Hoffe das liest noch jemand, der Beitrag ist ja nun über ein Jahr alt.

    MfG
    Valyr

  2. Bastian

    Hallo,
    eine sehr schöne Anleitung, die auch denen, welche noch nie mit MVVM gearbeitet haben, das Thema etwas näher bringt!
    Allerdings muss ich sagen, dass du durch das Thema MEF sehr durchgerast bist. So kommt man zum Beispiel bei dem Thema Interface und die Umstellung auf die Interfaces kaum hinterher (Achtung, das ist eine persönliche Meinung).
    Aber zum Glück stellt du jedes mal das Projekt bereit. Dafür meinen Dank und für diese sehr Einsteiger freundliche Anleitung!

Schreibe einen Kommentar

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

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.