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;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:
interface
uses
System.ComponentModel.Composition;
type
[InheritedExport]
IMessage = interface
['{7B32CB2C-F93F-4C59-8A19-89D6F86F36F1}']
function ToString: string;
end;
implementation
end.
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]and the Run method:
property Msgs: TArray<IMessage> read FMsgs write FMsgs;
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.