Aller au contenu

Composition et chargement dynamique de modules avec MEF

·698 mots·4 mins
Sommaire

Managed Extensibility Framework (MEF) est un framework permettant d’étendre facilement des applications. Il embarque tout un ensemble de fonctions et 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 un peu partout (dans une assembly, dans un répertoire, …) et de les ajouter dans une structure de données.

Cette étape de récupération est appelée “composition”.

Utilisation et mise en place
#

Tout d’abord, il faut installer MEF (via Nuget) et pouvoir inclure les espaces de noms suivants, à partir de 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:

  • Je souhaite exécuter un ensemble de traitements et de processus dans un ordre défini;
  • Chacun de ces traitement est défini dans une classe (voire, dans un projet séparé), et expose des propriétés au travers d’une interface, intitulé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;
}

Chacun des processus (plugin) doit hériter de cette interface, et implémenter une méthode Start(), ainsi que la propriété Priority qui sera utilisée par la suite. L’attribut InheritedExport, issu de l’espace de nom System.ComponentModel.Composition indique que les classes héritant de cette interface devront être reprises et interpretées 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, qui est un ensemble d’informations mises à disposition en fonction de la source interrogée,
  • d’un container, qui s’occupe d’assembler toutes ces informations
  • et d’une liste, qui sera peuplée par le container ci-dessus. C’est cette liste qui reprendra les instances héritées de l’interface définie IDataLoader que nous avons définie ci-dessus.

Le catalogue, c’est simplement une classe qui hérite de ComposablePartCatalog (dans le namespace System.ComponentModel.Composition.Primitives), et qui permettra d’accéder à certaines instances de classes spécifiques.

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 (par exemple, var catalog = AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly()); pour récupérer les classes présentes dans l’assembly en cours d’exécution)

Mise en place
#

Dans la classe “hôte” (le fichier main.cs par exemple), nous pouvons définir ceci:

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

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

Dans l’exemple ci-dessus, j’utilise un new DirectoryCatalog("."), qui va parcourir le répertoire courant pour y récupérer les classes intéressantes. Pour simplifier, si l’exécutable trouve une DLL contenant une ou plusieurs classes héritant de l’interface IDataLoader, il l’ajoutera à notre liste LoadedModules.

On instancie ensuite notre container, qui va inventorier les modules ou extensions disponibles, puis on va lui appliquer la méthode ComposeParts, pour que l’objet courant (this) soit peuplé avec toutes les classes trouvées dans le catalogue. Nous pourrions tout à fait peupler un autre objet, en lui passant simplement l’instance en paramètre.

Comme l’interface s’appelle IDataLoader, il doit exister une List<IDataLoader> dans l’objet passé en paramètre; cette liste sera remplie avec toutes les instances de 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 cette étape de composition, les instances sont disponibles au travers de la liste déclarée ci-dessus. Il suffit alors de parcourir la liste (que l’on peut trier comme n’importe quelle autre collection; ici, sur base de la proprité Priority), pour appeler la méthode Start() sur chacune d’entre elles:

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