grimboite/articles/dev/2014-03-30-mef-composition.md

4.5 KiB

Title Date Slug Tags
Composition avec MEF 2014-03-30 composition-with-mef framework, composition, code, dev

MEF est un framework permettant d'étendre facilement des applications. Il embarque tout un ensemble de méthodes pour permettre de charger dynamiquement des contextes et composants plus petits, utiles pour un utilisateur, en créant par exemple un mécanisme de plugins. L'idée est donc de définir un chargeur (loader) et de définir une interface qui sera respectée par tous les plugins. Au chargement de l'application, le loader s'occupera de récupérer tous les petits morceaux éparpillés (dans une assembly, dans un répertoire, ...) et de les lancer. L'étape de récupération est appelée "composition".

Tout d'abord, il faut bien entendu installer MEF (via Nuget) et pouvoir inclure les namespaces suivants (disponibles dans la DLL System.ComponentModel.Composition):

using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;

L'utilisation que j'en fais est assez simple: j'ai un ensemble de traitement à exécuter dans un ordre défini; chaque traitement est défini dans une classe (voire dans un projet séparé), et expose des propriétés au travers d'une interface, que j'ai appelée IDataLoader. Cette interface ressemble à ceci:

using System.ComponentModel.Composition;

[InheritedExport]
public interface IDataLoader
{
    void Start();
    int Priority { get; }
    string Type { get; }
    event EventHandler OnMessageSent;
    event EventHandler OnPercentageChange;
}

Chaque traitement (plugin) va hériter de cette interface, et va implémenter une méthode Start(), ainsi que la propriété Priority qui sera utilisée par la suite. L'attribut InheritedExport indique que les classes héritant de cette interface devront également être reprises lors de la composition.

Création des modules

Pour créer un nouveau module, il suffit de définir une nouvelle classe qui hérite de l'interface IDataLoader définie ci-dessus:

public class Module1 : IDataLoader
{
    public void Start()
    {
        // do something
    }

    public string Type
    {
        get { return this.GetType().ToString(); }
    }

    public int Priority
    {
        get { return 0; }
    }
}

Composition

La composition en elle-même se fait au travers d'un catalogue (le bidule qui répertorie toutes les classes à instancier) et d'une liste de modules. C'est une liste qui contient des instances de l'interface définie ci-dessus. Dans la classe "hôte" (le fichier main.cs par exemple):

[ImportMany]
public List<IDataLoader> LoadedModules { get; set; }

private void Compose()
{
    catalog = new DirectoryCatalog(".");
    var container = new CompositionContainer(catalog);
    container.ComposeParts(this);
}

Le catalogue, c'est simplement une classe qui hérite de ComposablePartCatalog (dans le namespace System.ComponentModel.Composition.Primitives). Parmi les types existants, on a par exemple:

  • DirectoryCatalog, qui va pêcher les classes parmi les assemblies posées dans un répertoire particulier
  • AssemblyCatalog pour ne prendre que les classes situées dans une assembly spécifique (genre var catalog = AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly());)
  • ...

Ci-dessus, j'utilise un new DirectoryCatalog("."), qui va parcourir le répertoire courant pour y récupérer les classes intéressantes. On instancie ensuite un container, qui va inventorier toutes ces classes, puis on appelle la méthode ComposeParts sur le container pour que l'objet courant (this) soit initialisé avec les bidules trouvés dans le catalogue. Mon interface s'appelle IDataLoader, il existe donc une List<IDataLoader> dans l'objet courant, qui sera remplie avec toutes les classes héritant de cette interface (vous vous rappelez de l'attribut InheritedExport sur l'interface?). L'attribut ImportMany indique au container de charger autant de classes qu'il trouvera.

Après la composition, les instances sont dispos au travers de la liste déclarée ci-dessus. Dans l'exemple, après que les plugins aient été chargés, on les parcourt dans une liste triée par priorité, et on appelle la méthode Start():

foreach (var module in LoadedModules.OrderBy(p => p.Priority))
{
    module.Start();
}