MEF jest kolejnym frameworkiem umożliwiającym tworzenie rozszerzalnych aplikacji. Pomijając marketing serwowany na różnych stronach, jest to po prostu biblioteka umożliwiająca dependency injection. W wersji .NET 4.0 została zintegrowana w .NET Framework i nie musi być instalowana osobno. Na oficjalnej stronie można sporo poczytać o budowie MEF’a jednak w poście chciałbym ograniczyć to do minimum ponieważ dla mnie osobiście, zbyt dokładne intro do tematu po prostu zniechęca mnie do dalszego czytania.
Zacznijmy więc od przykładu! Chcemy napisać aplikację, która zawiera usługę umożliwiającą wyświetlanie wiadomości. Ponadto chcemy to zrobić w sposób maksymalnie elastyczny tak aby implementacja mogła być wstrzykiwana dynamicznie – umożliwi nam to późniejsze testy jednostkowe. Najpierw dołączamy referencje do MEF:
Następnie definiujemy interfejs (kontrakt):
public interface IMessageService { void Show(string message,string caption); }
Oraz dwie implementacje (prawdziwa dla wpf oraz stub dla unit test):
public class WpfMessageBoxService:IMessageBoxService { public void Show(string message,string caption) { MessageBox.show(caption,message); } } public class StubMessageBoxService:IMessageBoxService { public void Show(string message,string caption) { // do nothing } }
Następnie gdzieś w kodzie, np. w ViewModel mamy właściwość reprezentującą usługę:
public class SampleViewModel { //... public void Show() { // jakas logika MessageBoxService.Show("...","..."); } public IMessageBoxService MessageBoxService{get;set;} }
Przedstawiona architektura umożliwia wstrzyknięcie dowolnej implementacji do MessageBoxService . W klasycznych IoC dostęp do konkretnej implementacji uzyskujemy np. poprzez:
this.MessageBoxService = container.Resolve<IMessageBoxService>();
Za pomocą MEF jest to jak najbardziej możliwe. Jednak przeważnie wykorzystuje się atrybuty: Export aby wyeksportować konkretną implementację do wspólnego kontenera, oraz Import aby ją wstrzyknąć. Zatem implementacje powinny wyglądać następująco:
[Export(typeof(IMessageBoxService))] public class WpfMessageBoxService:IMessageBoxService { public void Show(string message,string caption) { MessageBox.show(caption,message); } } [Export(typeof(IMessageBoxService))] public class StubMessageBoxService:IMessageBoxService { public void Show(string message,string caption) { // do nothing } }
Z kolei polecenie wstrzyknięcia dokonujemy za pomocą Import:
public class SampleViewModel { //... public void Show() { // jakas logika MessageBoxService.Show("...","..."); } [Import] public IMessageBoxService MessageBoxService{get;set;} }
Ostatnia zagadka, jak import jest powiązany eksportem? Możemy w końcu atrybutem export oznaczyć wiele różnych implementacji. Skąd MEF ma wiedzieć czy WpfMessageBoxService powinien zostać użyty czy StubMessageBoxService?
Do tego służą katalogi oraz metoda ComposeParts. Najpierw definiujemy katalog zawierający wszelkie klasy opatrzone atrybutem Export, a potem wywołujemy ComposeParts na klasie, która zawiera atrybuty Import. Jeśli istnieje kilka implementacji w katalogu, wtedy Import wyrzuci wyjątek (ponieważ Import w przeciwieństwie do ImportMany stanowi relację jeden do jednego). MEF wspiera kilka typów katalogów:
-
AssemblyCatalog – ładuje bibliotekę i w katalogu pluginów umieszczane są wszystkie klasy opatrzone Export.
-
DirectoryCatalog – ładuję wszystkie biblioteki zawarte w danym katalogu Windows i szuka w nich klas opatrzonych Export.
-
Type Catalog – jawne wczytanie danej klasy do kontenera.
-
Aggregate Catalog – kolekcja zawierająca dowolną ilość katalogów (np. TypeCatalog oraz AssemblyCatalog).
Ostatnim więc krokiem jest stworzenie katalogu i skonstruowanie SampleViewModel (wstrzyknięcie implementacji):
TypeCatalog typeCatalog=new TypeCatalog(typeof(WpfMessageBoxService)); var container = new CompositionContainer(typeCatalog); container.ComposeParts(sampleViewModel);
Po wywołaniu ComposeParts, MessageBoxService będzie zawierał WpfMessageBoxService. Oczywiście dużo ciekawszym jest załadowanie pluginów z bibliotek wewnętrznych np.:
DirectoryCatalog directoryCatalog=new DirectoryCatalog("bin\\plugins"); AssemblyCatalog assemblyCatalog = new AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly()); AggregateCatalog aggregateCatalog=new AggregateCatalog(); aggregateCatalog.Catalogs.Add(directoryCatalog); aggregateCatalog.Catalogs.Add(assemblyCatalog); var container = new CompositionContainer(aggregateCatalog); container.ComposeParts(sampleViewModel);
W kolejnych postach przedstawię bardziej szczegółowo zagadnienia związane z eksportem i importem typów.
Pytanie na ile można ten mechanizm stosować do “wstrzykiwania” nowych własności w systemach biznesowych, nowe dokumenty biznesowe i ich “ścieżki”?