Introduzione
Ai primi di giugno 2008 e' uscito, un po in sordina a dir la verita', la prima CTP del Managed Extensibility Framework (MEF) e sebbene ancora in fase embrionale visto che farà parte della prossima "Wave" credo valga la pena tenerlo d'occhio non fosse altro perchè nel team oltre a nomi noti quali Krzysztof Cwalina e Brad Abrams si è da poco aggiunto un certo Hamilton Verissimo ovvero il papà di Castle Project.
Gli esempi allegati al MEF sono molto WPF oriented ma non per questo MEF=WPF anche se l'accoppiata MEF + Model-View-ViewModel offre l'opportunità per scenari di estensibilità decisamente interessanti.
Al momento MEF altro non è che una dll ComponentModel.dll,in versione guarda caso 4.0.0.0, che estende System.ComponentModel con un nuovo namespace Composition.
Supponiamo di voler accoppiare, oppure se preferite, iniettare, nella proprietà Role di un istanza della classe Person un determinato ruolo:
public class Person
{
[Import("Role")]
public string Role { get; set; }
}
public class RoleProvider
{
[Export("Role")]
public string AvailableRole
{
get { return "Developer" ; }
}
}
Il codice per realizzare quanto richiesto è:
Person p = new Person();
RoleProvider rp = new RoleProvider();
CompositionContainer container = new CompositionContainer();
container.AddComponent<Person>(p);
container.AddComponent<RoleProvider>(rp);
container.Bind();
Console.WriteLine(p.Role); //Developer
Console.ReadLine();
Analizzando la definizione delle classi avrete notato che le proprieta' Role di RoleProvider e Person sono decorate rispettivamente con degli attributi [Export] e [Import] i quali indicano i punti di estensibilita' messi a disposizione da Person e RoleProvider, la stringa passata nel costruttore permette di associare a questi punti degli identificativi univoci (Contract Names)
In sintesi:
- RoleProvider mette a disposizione di MEF un 'componente' il cui nome è “Role” mentre Person ha bisogno di un ‘componente’ con lo stesso nome.
- A mettere in comunicazione i due componenti ci pensa l'oggetto CompositionContainer il quale alimentato con i vari tipi che espongono o richiedono componenti al momento del Bind 'connette' i vari componenti in base al relativo identificativo.
- Se un Import rimane senza corrispondente Export, il Bind fallisce, cosi come fallisce nel caso in cui in CompositionContainer vengano inseriti più Export con lo stesso identificativo.
- Nel caso si abbiano piu' Imports che richiedono lo stesso Export, by default, tutti gli Imports condividono la stessa istanza, comportamento comunque modificabile attraverso l’ attributo [CompositionOptions]
Tutto questo e' sicuramente molto interessante, peccato che di estensibile ci sia poco o nulla, infatti tutti gli attori sono fortemente legati tra loro.
Rendiamo la soluzione estensibile
Creiamo una nuovo progetto Class Library e spostiamo al suo interno la classe RoleProvider creata in precedenza, compiliamo e copiamo la dll di output in una cartella Extensions posta sotto la directory contenente l'eseguibile principale, separando, di fatto, consumer e provider.
A questo punto dobbiamo modificare il codice che accoppia le entità con quello che segue:
Person p = new Person();
DirectoryWatchingComponentCatalog catalog = new DirectoryWatchingComponentCatalog()
catalog.AddDirectory(@".\Extensions");
var container = new CompositionContainer(catalog.Resolver);
container.AddComponent<Person>(p);
container.Bind();
Console.WriteLine(p.Role);
Console.ReadLine();
Come potete notare un nuovo elemento è entrato in gioco, si trarra del DirectoryWatchingComponentCatalog il quale tiene sotto controllo una o più directories e mette a disposizione gli Imports/Exports in esse contenuti.
E’ anche possibile notificare ai componenti quando l’asset delle directory cambia implementando l’interfaccia INotifyChanged.
Che succede al nostro esempio se copio in Extensions due assembly che espongono ognuna un componente “Role” ? ottengo un eccezione per i motivi citati in precedenza, vediamo quindi come gestire il caso più frequente: Più Export resi disponibili a un unico Import.
One Import vs More Exports
Creiamo un altro progetto Class Library e copiamo al suo interno la classe RoleProvider vista in precedenza modificando il valore ritornato, giusto per avere due risultati diversi ,aggiungiamo ad entrambe le classi RoleProvider dei metadati che ci consentano successivamente di discriminare un componente dall’altro e copiamo entrambe le dll nella directory Extensions utilizzata in precedenza.
namespace RoleLibrary1
{
public class RoleProvider
{
[Export("Role")]
[ExportProperty("IsActive",false)]
public string AvailableRole
{
get { return "Developer"; }
}
}
}
namespace RoleLibrary2
{
public class AnotherRoleProvider
{
[Export("Role")]
[ExportProperty("IsActive",true)]
public string AvailableRole
{
get { return "Architect"; }
}
}
}
e cambiamo la parte di Binding con questo codice:
Person p = new Person();
DirectoryWatchingComponentCatalog catalog = new DirectoryWatchingComponentCatalog();
catalog.AddDirectory(@".\Extensions");
var container = new CompositionContainer(catalog.Resolver);
var ret=container.TryGetImportInfos("Role");
if (ret.Succeeded)
{
foreach (IImportInfo importInfo in ret.Value)
{
if ((bool)importInfo.Metadata["IsActive"])
{
string role = (string)importInfo.GetBoundValue();
p.Role = role;
}
}
}
Console.WriteLine(p.Role);
Console.ReadLine();
Alle estensioni abbiamo aggiunto attraverso l’attributo [ExportProperty] dei metadati aggiuntivi e anzichè delegare al metodo Bind il compito di iniettare le estensioni (cosa che fallirebbe in questo caso vista la presenza di due assembly che esportano lo stesso contract name) usiamo il metodo TryGetImportInfos per ottenere una collezione di IImportInfo e, in base al valore del metadato “IsActive” decidere quale istanza utilizzare invocando il metodo GetBoundValue();
Delle varie estensioni, solo quella sul quale viene invocato il metodo GetBoundValue viene effettivamente caricata in memoria.
Lookup Dinamico
Fin’ora abbiamo utilizzato una stringa come tipo da iniettare, in un caso reale è molto probabile che si abbia a che fare con un tipo che condivide la stessa interfaccia con più estensioni come in questo caso:
Entities project
namespace Entities
{
public interface IAddress
{
int CAP { get; set; }
string Address { get; set; }
string City { get; set; }
}
}
Il progetto Entities è referenziato sia dall’assembly principale che dalle varie estensioni, quindi ci aspettiamo che un estensione scritta in questo modo funzioni correttamente
[Export("Address")]
public class Address:Entities.IAddress
{
public int CAP
{
get{return 20100;}
set{throw new NotImplementedException(); }
}
string Entities.IAddress.Address
{
get{return "Via Torino, 51";}
set{throw new NotImplementedException();}
}
public string City
{
get{return "MILANO";}
set{throw new NotImplementedException();}
}
}
ed effettivamente è cosi, ma che succede se creo un estensione come quella qui sotto che nulla sa dell’interfaccia IAddress?
[Export("Address")]
public class LocationInfos
{
public int CAP
{
get { return 10100; }
set { throw new NotImplementedException(); }
}
public string Address
{
get { return "Via Milano, 77"; }
set { throw new NotImplementedException(); }
}
public string City
{
get { return "TORINO"; }
set { throw new NotImplementedException(); }
}
}
Nulla!, tutto continua a funzionare in quanto le regole di binding, a patto che il contract name (“Role”) coincida, verificano che la firma di LocationInfos soddisfa quella dichiarata dalla controparte Import.
Conclusione
E’ ancora molto presto per esprimere pareri su MEF e, non si sa quanto di quanto visto fin’ora rimarrà nella versione finale, ma viste le persone coinvolte sicuramente MEF sarà un altro dei vari acronimi di cui parleremo spesso nei mesi a venire.