As you may know I really like many concepts that we can find in .Net and here is another one: the Managed Extensibility Framework. Its core parts are basically a DI container and one catalog (or more) that contains information about exports and imports. So you can create classes that are not referenced directly by any other part of your application and define an export on them. But that alone would be useless because we actually want to use that class, don't we? So on the other side you can specify an import. Be it for the constructor of another class or its properties.
In the following I will show you how to use this concept in your Delphi application. Because I am very bad in finding examples I took a look around and found this nice example for MEF and I will use very similar examples.
Hello world
So, let's get started. We will create a console application that looks like this:
program MEFSample; {$APPTYPE CONSOLE} uses MEFSample.Main, MEFSample.Message, System.ComponentModel.Composition.Catalog, System.ComponentModel.Composition.Container, SysUtils; var main: TMain; catalog: TRttiCatalog; container: TCompositionContainer; begin ReportMemoryLeaksOnShutdown := True; main := TMain.Create; catalog := TRttiCatalog.Create(); container := TCompositionContainer.Create(catalog); try try container.SatisfyImportsOnce(main); main.Run(); except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; finally container.Free(); catalog.Free(); main.Free; end; Readln; end.
We create the catalog which pulls all the exports and imports from the RTTI and pass it to the composition container which is responsible for resolving those informations when creating new objects or using the SatisfyImportsOnce method on an already existing object like in our example.
Our MEFSample.Main unit looks as follows:
unit MEFSample.Main; interface uses System.ComponentModel.Composition; type TMain = class private FMsg: TObject; public procedure Run; [Import('Message')] property Msg: TObject read FMsg write FMsg; end; implementation procedure TMain.Run; begin Writeln(Msg.ToString); end; end.
The unit System.ComponentModel.Composition contains all the attributes. If you do not include it, you get the "W1025 Unsupported language feature: 'custom attribute'" compiler warning which means those attributes are not applied.
We specify the Msg property as named import. When calling the SatifyImportOnce Method (which is also used internally when creating objects with the CompositionContainer) it looks for all those imports and tries to find matching exports.
That leads us to the last unit for this first example:
unit MEFSample.Message; interface uses System.ComponentModel.Composition; type [Export('Message')] TSimpleHello = class public function ToString: string; override; end; implementation function TSimpleHello.ToString: string; begin Result := 'Hello world!'; end; initialization TSimpleHello.ClassName; end.
Again, we need to use System.ComponentModel.Composition to make the Export attribute work. We have some simple class that is exported with a name.
One important point: Since this class is referenced nowhere else in our application the compiler will just ignore it. That is why we need to call some method of it in the initialization part of the unit which is called when the application starts. So this makes the compiler include our class.
When we start the application we see the amazing "Hello world".
Using contracts
This worked but actually that is not how we should do this. So let's define a contract called IMessage.
unit MEFSample.Contracts; interface uses System.ComponentModel.Composition; type [InheritedExport] IMessage = interface ['{7B32CB2C-F93F-4C59-8A19-89D6F86F36F1}'] function ToString: string; end; implementation end.We use another attribute here which tells the catalog to export all classes, that implement this interface. We also need to specify a guid for that interface. We change our TSimpleHello class:
TSimpleHello = class(TInterfacedObject, IMessage)and in our main class we change the Msg property:
[Import] property Msg: IMessage read FMsg write FMsg;
The more the better!
What keeps us from creating another class that implements IMessage? Nothing, so let's do this:
type TSimpleHola = class(TInterfacedObject, IMessage) public function ToString: string; override; end; implementation function TSimpleHola.ToString: string; begin Result := 'Hola mundo'; end; initialization TSimpleHola.ClassName;
When we run this we get an ECompositionException with message 'There are multiple exports but a single import was requested.' which totally makes sense. So we need to change something:
[ImportMany] property Msgs: TArray<IMessage> read FMsgs write FMsgs;and the Run method:
procedure TMain.Run; var m: IMessage; begin for m in FMsgs do Writeln(m.ToString); end;
We start the application and get both messages.
Breaking it down
What if we only want to export and import smaller parts than a whole class? Well then define the export on those parts!
type TSimpleHello = class(TInterfacedObject, IMessage) private FText: string; public function ToString: string; override; [Import('Text')] property Text: string read FText write FText; end; TTextProvider = class private function GetText: string; public [Export('Text')] property Text: string read GetText; end; implementation function TSimpleHello.ToString: string; begin Result := FText; end; function TTextProvider.GetText: string; begin Result := 'Bonjour tout le monde'; end; initialization TSimpleHello.ClassName; TTextProvider.ClassName;
So what is this all about? Different parts of the applications can be created and put together in a declarative way using attributes. With MEF you can create code that is free of unnecessary dependencies - clean code.
The sample and the required units can be downloaded here or directly from the svn. The source is based on some implementation I originally found here.
P.S. I just made the sample also work in Delphi 2010 - this is available in svn.