MEF ed i Metadata

Alcune volte può capitare di dover associare delle informazioni agli export, per esempio specificare cosa ci offre una determinata implementazione di un contratto. Questo permette agli imports di sapere cosa offre un determinato componente/plugin e di utilizzare solo quelli che risolvono le nostre esigenze. MEF supporta due tipi di metadata: typed e non typed. Il post tratterà solo i metadata typed; per vedere un esempio di utilizzo dei non typed potete dare un’occhiata alla MEF Programming Guide.

Ok ora possiamo partire a vedere qualcosa di concreto!

Supponiamo di dover creare un plugin manager che ci permette di accedere ai plugins installati. Elenchiamo un paio di specifiche:

  1. I plugin implementano l’interfaccia IPlugin
  2. Un plugin può offrire funzionalità diverse, a seconda della funzionalità implementa una specifica interfaccia
  3. Il plugin manager deve implementare l’interfaccia IPluginManager
  4. Tramite il plugin manager possiamo ottenere uno o più plugins che offrono una determinata funzionalità


Iniziamo col definire l’interfaccia IPluginManager

public interface IPluginManager
{
    event EventHandler PluginsChanged;
 
    IEnumerable<Lazy<IPlugin, IPluginMetadata>> Plugins { get; set; }
 
    T GetPlugin<T>() where T : class;
    IEnumerable<T> GetPlugins<T>() where T : class;
}

Supponiamo di avere due interfacce che specificano due diverse funzionalità:

  1. IFilter: filtra una collezione di dati (chiamiamolo segnale)
  2. IFeature: da un segnale estrae una particolare caratteristica
public interface IFilter
{
    IList<double> Filter(IList<double> values);
}
public interface IFeature
{
    double Extract(IList<double> values);
}
Ora che abbiamo tutto il necessario per costruire il nostro sistema passiamo a giocare con i metadata. Come ho accennato all’inizio i metadata sono delle informazioni aggiuntive che associamo agli export. L’unica informazione di cui noi abbiamo bisogno è il tipo di funzionalità che espone il nostro plugin. Queste informazioni vengono “aggiunte” tramite l’attributo MetadataAttribute, ma visto che stiamo usando i metadata typed le nostre informazioni verranno associate ai vari export tramite l’attributo ExportPluginAttribute
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ExportPluginAttribute : ExportAttribute, IPluginMetadata
{
    public ExportPluginAttribute()
        : base(typeof(IPlugin))
    {
    }
 
    public Type PluginType { get; set; }
}
Dal codice possiamo notare tre cose fondamentali dell’attributo ExportPluginAttribute
  • E’ decorato con l’attributo MetadataAttribute che permette di segnalare al CompositionContainer (a dire il vero la classe che si occupa di recuperare le informazioni aggiuntive è un’altra) che contiene delle informazioni aggiuntive
  • Deriva da ExportAttribute così nelle implementazioni di IPlugin evitiamo di specificare ogni volta che il contratto è IPlugin (lo specifichiamo richiamando il costruttore di ExportAttribute)
  • Implementa l’interfaccia IPluginMetadata che contiene le informazioni aggiuntive (in questo caso solo il tipo del plugin, cioè il tipo di funzionalità: IFilter o IFeature)
public interface IPluginMetadata
{
    Type PluginType { get; }
}

Finalmente possiamo passare alla parte più importante della nostra piccola applicazione, il Plugin Manager!

[Export(typeof(IPluginManager))]
public class PluginManager : IPluginManager
{
    private IEnumerable<Lazy<IPlugin, IPluginMetadata>> plugins;
 
    #region IPluginManager Members
 
    public event EventHandler PluginsChanged;
 
    [ImportMany(AllowRecomposition = true)]
    public IEnumerable<Lazy<IPlugin, IPluginMetadata>> Plugins
    {
        get
        {
            return plugins;
        }
 
        set
        {
            if (plugins == value)
                return;
 
            plugins = value;
            OnPluginsChanged();
        }
    }
 
    public T GetPlugin<T>() where T : class
    {
        return GetPlugins<T>().FirstOrDefault();
    }
 
    public IEnumerable<T> GetPlugins<T>() where T : class
    {
        return (from p in this.Plugins
                where p.Metadata.PluginType == typeof(T)
                select p.Value).Cast<T>();
    }
 
    #endregion
 
    protected virtual void OnPluginsChanged()
    {
        EventHandler pc = PluginsChanged;
 
        if (pc != null)
            PluginsChanged(this, EventArgs.Empty);
    }
}

Le parti fondamentali di questo codice sono la proprietà Plugins ed il metodo GetPlugins<T>

La proprietà Plugins è già stata vista nel post precedente e l’unica cosa da notare è l’utilizzo della classe Lazy<T, TMetadata> in maniera tale da poter accedere alle informazioni aggiuntive tramite la proprietà Metadata.

Il compito del metodo GetPlugins<T> è molto semplice: recupera tutti i plugins che hanno la proprietà PluginType uguale al tipo di plugin che vogliamo :)

Continuiamo con il mettere insieme i cocci…
Il PluginManager verrà utilizzato dal ViewModel della MainWindow.xaml

[Export(typeof(MainViewModel))]
public class MainViewModel
{
    private IPluginManager pluginManager;
 
    [ImportingConstructor]
    public MainViewModel(IPluginManager pluginManager)
    {
        this.pluginManager = pluginManager;
 
        this.FilterPlugins = new ThreadSafeObservableCollection<IFilter>();
        this.FeaturePlugins = new ThreadSafeObservableCollection<IFeature>();
 
        this.pluginManager.PluginsChanged += new EventHandler(pluginManager_PluginsChanged);
 
        this.LoadPlugins();
    }
 
    public ObservableCollection<IFilter> FilterPlugins { get; set; }
 
    public ObservableCollection<IFeature> FeaturePlugins { get; set; }
 
    private void pluginManager_PluginsChanged(object sender, EventArgs e)
    {
        LoadPlugins();
    }
 
    private void LoadPlugins()
    {
        this.FilterPlugins.Clear();
        this.FeaturePlugins.Clear();
 
        foreach (IFilter filter in pluginManager.GetPlugins<IFilter>())
            this.FilterPlugins.Add(filter);
 
        foreach (IFeature feature in pluginManager.GetPlugins<IFeature>())
            this.FeaturePlugins.Add(feature);
    }
}

Il PluginManager verrà passato al costruttore del MainViewModel grazie all’attributo ImportingConstructor e per caricare i plugins utilizziamo il metodo GetPlugins spiegato precedentemente.

Cosa da notare è l’utilizzo della classe ThreadSafeObservableCollection<T>, caduta proprio a “faggiuolo” (termine tecnico) se guardate la data dell’articolo, al posto della classica ObservableCollection<T> in quanto se utilizziamo quest’ultima otteniamo un exception dove il Message è

The composition produced a single composition error. The root cause is provided below. Review the CompositionException.Errors property for more detailed information.

1) This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.

Resulting in: An exception occurred while trying to set the value of property 'PluginManager.PluginManager.Plugins'.

Resulting in: Cannot activate part 'PluginManager.PluginManager'.
Element: PluginManager.PluginManager -->  PluginManager.PluginManager -->  AssemblyCatalog (Assembly="PluginManager, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")

Questo perchè il metodo base.Refresh() del DirectoryWatcherCatalog viene chiamato su un thread diverso da quello della UI.

Ora è restata solo la MainWindow, ed è proprio lei che si occupa di creare il CompositionContainer ed i Catalogs

public MainWindow()
{
    CompositionContainer compositionContainer;
    DirectoryWatcherCatalog directoryCatalog;
    AggregateCatalog aggregateCatalog;
 
    InitializeComponent();
 
    if (string.IsNullOrEmpty(Settings.Default.PluginsDirectory))
    {
        Settings.Default.PluginsDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Plugins");
 
        if (!Directory.Exists(Settings.Default.PluginsDirectory))
            Directory.CreateDirectory(Settings.Default.PluginsDirectory);
 
        Settings.Default.Save();
    }
 
    directoryCatalog = new DirectoryWatcherCatalog(Settings.Default.PluginsDirectory);
    aggregateCatalog = new AggregateCatalog(directoryCatalog, new AssemblyCatalog(Assembly.GetExecutingAssembly()));
    compositionContainer = new CompositionContainer(aggregateCatalog);
 
    compositionContainer.ComposeParts(this);
}
 
[Import(typeof(MainViewModel))]
public MainViewModel ViewModel
{
    get
    {
        return (MainViewModel)this.DataContext;
    }
 
    set
    {
        this.DataContext = value;
    }
}

All’interno della solution trovate un progetto chiamato MyPlugins (contiene 3 plugins che non fanno nulla :) ), una volta avviato il progetto PluginManager potete copiare MyPlugins.dll all’interno di ..\PluginManager\bin\Debug\Plugins e dovreste vedere due plugin Feature ed un plugin Filter.

[Download Code]

Buon MEF a tutti :D