Vue d'ensemble de Managed Extensibility Framework

Cette rubrique fournit une vue d'ensemble de Managed Extensibility Framework présenté dans .NET Framework 4.

Cette rubrique comprend les sections suivantes.

  • Description du langage MEF
  • Problème d'extensibilité
  • Possibilités offertes par MEF
  • Disponibilité du langage MEF
  • MEF et MAF
  • SimpleCalculator : exemple d'application
  • Catalogues et conteneur de composition
  • Importations et exportations avec des attributs
  • Importations supplémentaires et ImportMany
  • Logique de la calculatrice
  • Extension de SimpleCalculator à l'aide d'une nouvelle classe
  • Extension de SimpleCalculator à l'aide d'un nouvel assembly
  • Conclusion
  • Informations complémentaires

Description du langage MEF

Managed Extensibility Framework (MEF) est une bibliothèque pour la création d'applications simples et extensibles. Elle permet aux développeurs d'applications de découvrir et d'utiliser des extensions sans aucune configuration. Elle permet également aux développeurs d'extensions d'encapsuler du code facilement tout en évitant des dépendances dures et fragiles. De plus, MEF permet de réutiliser des extensions dans et entre les applications.

Problème d'extensibilité

Imaginez que vous soyez l'architecte d'une application volumineuse devant prendre en charge l'extensibilité. Votre application doit inclure un nombre potentiellement important de petits composants qu'elle est censée créer et exécuter.

Face à un tel problème, l'approche la plus simple consiste à inclure les composants sous forme de code source dans votre application et à les appeler directement à partir de votre code. Cette méthode a plusieurs inconvénients évidents. Le principal problème est que vous ne pouvez pas ajouter de nouveaux composants sans modifier le code source. Cette restriction peut être acceptable dans une application Web par exemple, mais elle est rédhibitoire dans le cadre d'une application cliente. L'autre problème est que vous ne pouvez pas accéder au code source des composants car ils sont développés par des tiers, et pour la même raison, vous ne pouvez pas leur permettre d'accéder au vôtre.

Une approche un peu plus élaborée consisterait à fournir un point d'extension ou une interface pour découpler l'application de ses composants. Grâce à ce modèle, vous seriez en mesure de fournir une interface qu'un composant pourrait implémenter, ainsi qu'une API pour lui permettre d'interagir avec votre application. Si cette solution résout le problème de l'accès requis au code source, elle pose tout de même d'autres difficultés.

Étant donné que l'application est incapable de découvrir des composants elle-même, vous devez encore lui indiquer explicitement quels composants sont disponibles et doivent être chargés. Pour ce faire, il suffit généralement d'enregistrer explicitement les composants disponibles dans un fichier de configuration. La vérification de l'exactitude des composants devient ainsi un problème de maintenance, en particulier si la mise à jour doit être effectuée par l'utilisateur final et non le développeur.

De plus, les composants sont incapables de communiquer entre eux, hormis via les canaux rigides de l'application elle-même. Si l'architecte de l'application n'a pas anticipé la nécessité d'établir une communication particulière, c'est généralement impossible.

Enfin, les développeurs de composants doivent accepter une dépendance dure sur l'assembly contenant l'interface qu'ils implémentent. Cette dépendance rend compliquée l'utilisation d'un composant dans plusieurs applications, et peut également poser des problèmes lorsque vous créez une infrastructure de test pour les composants.

Possibilités offertes par MEF

Au lieu de cette inscription explicite des composants disponibles, MEF permet de les découvrir implicitement grâce à la composition. Un composant MEF (appelé partie) spécifie de manière déclarative ses dépendances (appelées importations) et les fonctions (appelées exportations) qu'il rend disponibles. Lorsqu'une partie est créée, le moteur de composition MEF fournit à ses importations les éléments disponibles des autres parties.

Cette approche résout les problèmes abordés dans la section précédente. Étant donné que les parties MEF spécifient leurs fonctions de façon déclarative, elles sont détectables au moment de l'exécution, ce qui signifie qu'une application peut utiliser des parties sans avoir recours à des références codées en dur ni à des fichiers de configuration fragiles. MEF permet aux applications de découvrir et d'examiner des parties grâce à leurs métadonnées, sans les instancier ni même charger leurs assemblys. Par conséquent, il est inutile de spécifier avec précision quand et comment les extensions doivent être chargées.

En plus de fournir ses exportations, une partie peut spécifier ses importations, qui seront remplies par d'autres parties. Cela permet et facilite la communication entre les parties tout en garantissant une bonne factorisation du code. Par exemple, les services communs à de nombreux composants peuvent être factorisés dans une partie distincte et être facilement modifiés ou remplacés.

Étant donné que le modèle MEF n'a besoin d'aucune dépendance dure sur un assembly d'application particulier, il permet aux extensions d'être réutilisées entre les applications. Cela permet aussi de développer facilement un atelier de test, indépendant de l'application, pour tester des composants d'extension.

Une application extensible écrite à l'aide de MEF déclare une importation qui peut être remplie par des composants d'extension, et peut également déclarer des exportations pour exposer des services d'application aux extensions. Chaque composant d'extension déclare une exportation et éventuellement des importations. De cette façon, les composants d'extension eux-mêmes sont automatiquement extensibles.

Disponibilité du langage MEF

MEF fait partie intégrante du .NET Framework 4 et est disponible partout où le .NET Framework est utilisé. Vous pouvez utiliser MEF dans vos applications clientes, si elles utilisent des Windows Forms, WPF ou une autre technologie, ou dans les applications serveur qui s'appuient sur ASP.NET.

MEF et MAF

Les précédentes versions du .NET Framework ont introduit Managed Add-in Framework (MAF), une infrastructure conçue pour permettre aux applications d'isoler et de gérer des extensions. MAF offre un niveau de focus légèrement supérieur à MEF : son focus se concentre sur l'isolement des extensions et le chargement/déchargement des assemblys, tandis que le celui de MEF porte sur la détectabilité, l'extensibilité et la portabilité. Les deux infrastructures interagissent de façon transparente et peuvent être utilisées par une même application.

SimpleCalculator : exemple d'application

Le meilleur moyen de découvrir les possibilités de MEF consiste à créer une application MEF simple. Dans cet exemple, vous allez générer une calculatrice très simple nommée SimpleCalculator. L'objectif de SimpleCalculator est de créer une application console qui accepte des commandes arithmétiques de base (de type « 5+3 » ou « 6-2 ») et retourne des résultats corrects. Grâce à MEF, vous pourrez ajouter de nouveaux opérateurs sans modifier le code de l'application.

Pour télécharger l'intégralité du code pour cet exemple, consultez l'exemple SimpleCalculator (page éventuellement en anglais).

RemarqueRemarque

L'objectif de SimpleCalculator est avant tout de montrer les concepts et la syntaxe de MEF, et non de fournir un scénario d'utilisation réaliste.La plupart des applications qui tirent le meilleur parti de MEF sont plus complexes que SimpleCalculator.Pour obtenir des exemples plus détaillés, consultez Managed Extensibility Framework (page éventuellement en anglais) sur Codeplex.

Pour commencer, dans Visual Studio 2010, créez un nouveau projet d'application console nommé SimpleCalculator. Ajoutez une référence à l'assembly System.ComponentModel.Composition, dans lequel réside MEF. Ouvrez Module1.vb ou Program.cs et ajoutez des instructions Imports ou using pour System.ComponentModel.Composition et System.ComponentModel.Composition.Hosting. Ces deux espaces de noms contiennent des types MEF dont vous aurez besoin pour développer une application extensible. En Visual Basic, ajoutez le mot clé Public à la ligne qui déclare le module Module1.

Catalogues et conteneur de composition

Le cœur du modèle de composition MEF est le conteneur de composition, qui contient toutes les parties disponibles et exécute la composition. (Autrement dit, la correspondance entre les importations et les exportations.) Le type de conteneur de composition le plus commun est CompositionContainer, et c'est celui que vous allez utiliser pour SimpleCalculator.

En Visual Basic, dans Module1.vb, ajoutez une classe publique nommée Program. Ajoutez ensuite la ligne suivante à la classe Program dans Module1.vb ou Program.cs :

Dim _container As CompositionContainer
private CompositionContainer _container;

Pour découvrir les parties disponibles, les conteneurs de composition utilisent un catalogue. Un catalogue est un objet qui rend disponibles les parties découvertes dans une source. MEF propose des catalogues pour découvrir des éléments dans un type, un assembly ou un dossier fourni. Les développeurs d'applications peuvent facilement créer des catalogues pour détecter des éléments dans d'autres sources, tels qu'un service Web.

Ajoutez le constructeur suivant à la classe Program :

Public Sub New()
    'An aggregate catalog that combines multiple catalogs
     Dim catalog = New AggregateCatalog()

    'Adds all the parts found in the same assembly as the Program class
    catalog.Catalogs.Add(New AssemblyCatalog(GetType(Program).Assembly))

    'Create the CompositionContainer with the parts in the catalog
    _container = New CompositionContainer(catalog)

    'Fill the imports of this object
    Try
        _container.ComposeParts(Me)
    Catch ex As Exception
        Console.WriteLine(ex.ToString)
    End Try
End Sub
private Program()
{
    //An aggregate catalog that combines multiple catalogs
    var catalog = new AggregateCatalog();
    //Adds all the parts found in the same assembly as the Program class
    catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly));

    //Create the CompositionContainer with the parts in the catalog
    _container = new CompositionContainer(catalog);

    //Fill the imports of this object
    try
    {
        this._container.ComposeParts(this);
    }
    catch (CompositionException compositionException)
    {
        Console.WriteLine(compositionException.ToString());
   }
}

L'appel à ComposeParts indique au conteneur de composition qu'il doit composer un ensemble spécifique de parties (dans ce cas, l'instance actuelle de Program). Toutefois, rien se produira à ce stade, puisque Program n'a pas d'importations à remplir.

Importations et exportations avec des attributs

Tout d'abord, Program doit importer une calculatrice. Cela permet de séparer les problèmes d'interface utilisateur, comme l'entrée et la sortie console qui iront dans Program, à partir de la logique de la calculatrice.

Ajoutez le code suivant à la classe Program :

<Import(GetType(ICalculator))>
Public Property calculator As ICalculator
[Import(typeof(ICalculator))]
public ICalculator calculator;

Remarquez que la déclaration de l'objet calculator n'est pas inhabituelle, mais qu'elle est décorée avec l'attribut ImportAttribute. Cet attribut déclare qu'un élément est une importation ; autrement dit, il sera rempli par le moteur de composition lorsque l'objet sera composé.

Chaque importation a un contrat qui détermine à quelles exportations elle doit être associée. Le contrat peut être une chaîne explicitement spécifiée ou il peut être généré automatiquement par MEF à partir d'un type donné (dans ce cas l'interface ICalculator). Toute exportation déclarée avec un contrat correspondant effectuera cette importation. Notez que, bien que le type de l'objet calculator est en fait ICalculator, ce n'est pas une obligation. Le contrat ne dépend pas du type de l'objet d'importation. (Dans ce cas, vous pouvez laisser le typeof(ICalculator) de côté. MEF déduira automatiquement que le contrat est basé sur le type de l'importation, sauf si vous le spécifiez explicitement.)

Ajoutez cette interface très simple au module ou à l'espace de noms SimpleCalculator :

Public Interface ICalculator
    Function Calculate(ByVal input As String) As String
End Interface
public interface ICalculator
{
    String Calculate(String input);
}

Maintenant que vous avez défini ICalculator, vous avez besoin d'une classe qui l'implémente. Ajoutez la classe suivante au module ou à l'espace de noms SimpleCalculator :

<Export(GetType(ICalculator))>
Public Class MySimpleCalculator
   Implements ICalculator

End Class
[Export(typeof(ICalculator))]
class MySimpleCalculator : ICalculator
{

}

Voici l'exportation qui correspondra à l'importation dans Program. Pour que l'exportation corresponde à l'importation, elles doivent avoir le même contrat. Une exportation avec un contrat basé sur typeof(MySimpleCalculator) engendrerait une incompatibilité, et l'importation ne serait pas remplie ; le contrat doit correspondre exactement.

Étant donné que le conteneur de composition sera rempli avec toutes les parties disponibles dans cet assembly, la partie MySimpleCalculator sera disponible. Lorsque le constructeur pour Program exécutera la composition sur l'objet Program, son importation sera remplie avec un objet MySimpleCalculator, qui sera créé à cette fin.

La couche d'interface utilisateur (Program) n'a besoin d'aucune autre information. Vous pouvez donc remplir le reste de la logique d'interface utilisateur dans la méthode Main.

Ajoutez le code suivant à la méthode Main :

Sub Main()
    Dim p As New Program()
    Dim s As String
    Console.WriteLine("Enter Command:")
    While (True)
        s = Console.ReadLine()
        Console.WriteLine(p.calculator.Calculate(s))
    End While
End Sub
static void Main(string[] args)
{
    Program p = new Program(); //Composition is performed in the constructor
    String s;
    Console.WriteLine("Enter Command:");
    while (true)
    {
        s = Console.ReadLine();
        Console.WriteLine(p.calculator.Calculate(s));
    }
}

Ce code lit simplement une ligne d'entrée et appelle la fonction Calculate d'ICalculator dans le résultat, qu'il réécrit dans la console. C'est le seul code dont vous avez besoin dans Program. Tout le reste du processus sera effectué dans les parties.

Importations supplémentaires et ImportMany

Pour que SimpleCalculator soit extensible, il doit importer une liste d'opérations. Un attribut ImportAttribute ordinaire est rempli par un seul ExportAttribute. Si plusieurs sont disponibles, le moteur de composition génère une erreur. Pour créer une importation qui peut être remplie par un nombre indéfini d'exportations, vous pouvez utiliser l'attribut ImportManyAttribute.

Ajoutez la propriété suivante à la classe MySimpleCalculator :

<ImportMany()>
Public Property operations As IEnumerable(Of Lazy(Of IOperation, IOperationData))
[ImportMany]
IEnumerable<Lazy<IOperation, IOperationData>> operations;

Lazy<T, TMetadata> est un type fourni par MEF pour contenir des références indirectes à des exportations. Ici, en plus de l'objet exporté lui-même, vous obtenez des métadonnées d'exportation ou des informations qui décrivent l'objet exporté. Chaque Lazy<T, TMetadata> contient un objet IOperation représentant une opération réelle et un objet IOperationData représentant ses métadonnées.

Ajoutez les interfaces simples suivantes au module ou à l'espace de noms SimpleCalculator :

Public Interface IOperation
    Function Operate(ByVal left As Integer, ByVal right As Integer) As Integer
End Interface

Public Interface IOperationData
    ReadOnly Property Symbol As Char
End Interface
public interface IOperation
{
     int Operate(int left, int right);
}

public interface IOperationData
{
    Char Symbol { get; }
}

Dans ce cas, les métadonnées de chaque opération sont le symbole qui représente cette opération, tel que +, -, *, etc. Pour rendre l'addition disponible, ajoutez la classe suivante au module ou à l'espace de noms SimpleCalculator :

<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "+"c)>
Public Class Add
    Implements IOperation

    Public Function Operate(ByVal left As Integer, ByVal right As Integer) As Integer Implements IOperation.Operate
        Return left + right
    End Function
End Class
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '+')]
class Add: IOperation
{
    public int Operate(int left, int right)
    {
        return left + right;
    }
}

L'attribut ExportAttribute fonctionne comme auparavant. L'attribut ExportMetadataAttribute joint des métadonnées à cette exportation, sous la forme d'une paire nom-valeur. Bien que la classe Add implémente IOperation, une classe qui implémente IOperationData n'est pas explicitement définie. Au lieu de cela, une classe est implicitement créée par MEF avec des propriétés basées sur les noms des métadonnées fournies. (C'est l'une des différentes méthodes permettant d'accéder aux métadonnées dans MEF.)

La composition dans MEF est récursive. Vous avez composé explicitement l'objet Program, qui a importé un ICalculator de type MySimpleCalculator. Ensuite, MySimpleCalculator importe une collection d'objets IOperation, et cette importation sera remplie lorsque MySimpleCalculator sera créé, en même temps que les importations de Program. Si la classe Add a déclaré une importation supplémentaire, cette dernière sera également remplie et ainsi de suite. Toute importation non remplie provoque une erreur de composition. (Toutefois, il est possible de déclarer que des importations sont facultatives ou de leur assigner des valeurs par défaut.)

Logique de la calculatrice

Maintenant que ces parties sont en place, il ne reste plus que la logique de la calculatrice elle-même. Ajoutez le code suivant dans la classe MySimpleCalculator pour implémenter la méthode Calculate :

Public Function Calculate(ByVal input As String) As String Implements ICalculator.Calculate
    Dim left, right As Integer
    Dim operation As Char
    Dim fn = FindFirstNonDigit(input) 'Finds the operator
    If fn < 0 Then
        Return "Could not parse command."
    End If
    operation = input(fn)
    Try
        left = Integer.Parse(input.Substring(0, fn))
        right = Integer.Parse(input.Substring(fn + 1))
    Catch ex As Exception
        Return "Could not parse command."
    End Try
    For Each i As Lazy(Of IOperation, IOperationData) In operations
        If i.Metadata.symbol = operation Then
            Return i.Value.Operate(left, right).ToString()
        End If
    Next
    Return "Operation not found!"
End Function
public String Calculate(String input)
{
    int left;
    int right;
    Char operation;
    int fn = FindFirstNonDigit(input); //finds the operator
    if (fn < 0) return "Could not parse command.";

    try
    {
        //separate out the operands
        left = int.Parse(input.Substring(0, fn));
        right = int.Parse(input.Substring(fn + 1));
    }
    catch 
    {
        return "Could not parse command.";
    }

    operation = input[fn];

    foreach (Lazy<IOperation, IOperationData> i in operations)
    {
        if (i.Metadata.Symbol.Equals(operation)) return i.Value.Operate(left, right).ToString();
    }
    return "Operation Not Found!";
}

Les étapes initiales analysent la chaîne d'entrée dans les opérandes de gauche et de droite et un caractère d'opérateur. Dans la boucle foreach, chaque membre de la collection operations est examiné. Ces objets sont de type Lazy<T, TMetadata>, et leurs valeurs de métadonnées et d'objet exporté sont accessibles respectivement via la propriété Metadata et la propriété Value. Dans ce cas, si la propriété Symbol de l'objet IOperationData est détectée comme une correspondance, la calculatrice appelle la méthode Operate de l'objet IOperation et retourne le résultat.

Pour terminer la calculatrice, vous avez également besoin d'une méthode d'assistance qui retourne la position du premier caractère non-numérique dans une chaîne. Ajoutez la méthode d'assistance suivante à la classe MySimpleCalculator :

Private Function FindFirstNonDigit(ByVal s As String) As Integer
    For i = 0 To s.Length
        If (Not (Char.IsDigit(s(i)))) Then Return i
    Next
    Return -1
End Function
private int FindFirstNonDigit(String s)
{
    for (int i = 0; i < s.Length; i++)
    {
        if (!(Char.IsDigit(s[i]))) return i;
    }
    return -1;
}

Vous devez maintenant être en mesure de compiler et d'exécuter le projet. En Visual Basic, assurez-vous d'avoir ajouté le mot clé Public au module Module1. Dans la fenêtre de la console, tapez une addition, par exemple « 5+3 », et la calculatrice retournera les résultats. Tout autre opérateur entraînera l'affichage du message « Operation Not Found! ».

Extension de SimpleCalculator à l'aide d'une nouvelle classe

Maintenant que la calculatrice fonctionne, il est facile d'ajouter une nouvelle opération. Ajoutez la classe suivante au module ou à l'espace de noms SimpleCalculator :

<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "-"c)>
Public Class Subtract
    Implements IOperation

    Public Function Operate(ByVal left As Integer, ByVal right As Integer) As Integer Implements IOperation.Operate
        Return left - right
    End Function
End Class
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '-')]
class Subtract : IOperation
{
    public int Operate(int left, int right)
    {
        return left - right;
    }
}

Compilez et exécutez le projet. Tapez une soustraction, par exemple « 5-3 ». La calculatrice prend désormais en charge la soustraction et l'addition.

Extension de SimpleCalculator à l'aide d'un nouvel assembly

L'ajout de classes au code source est une opération relativement simple, mais MEF permet de rechercher des éléments en dehors de la source d'une application. Pour illustrer cette possibilité, vous devrez modifier SimpleCalculator pour qu'il recherche des parties dans un répertoire et dans son propre assembly, en ajoutant un DirectoryCatalog.

Ajoutez un nouveau répertoire nommé Extensions au projet SimpleCalculator. Assurez-vous de l'ajouter au niveau du projet, et non de la solution. Ajoutez ensuite un nouveau projet de bibliothèque de classes à la solution, appelé ExtendedOperations. Le nouveau projet se compilera dans un assembly séparé.

Ouvrez le concepteur des propriétés de projet pour le projet ExtendedOperations, puis cliquez sur l'onglet Compiler ou Générer. Modifiez le Chemin de sortie de la génération ou le Chemin de sortie pour pointer vers le répertoire Extensions du répertoire de projet SimpleCalculator (.. \SimpleCalculator\Extensions\).

Dans Module1.vb ou Program.cs, ajoutez la ligne suivante au constructeur Program :

catalog.Catalogs.Add(New DirectoryCatalog("C:\SimpleCalculator\SimpleCalculator\Extensions"))
catalog.Catalogs.Add(new DirectoryCatalog("C:\\SimpleCalculator\\SimpleCalculator\\Extensions"));

Remplacez le chemin d'accès de l'exemple par le chemin d'accès à votre répertoire Extensions. (Ce chemin d'accès absolu sert uniquement à des fins de débogage. Dans une application de production, vous utiliseriez un chemin d'accès relatif.) Le DirectoryCatalog ajoutera maintenant au conteneur de composition toutes les parties situées dans les assemblys du répertoire Extensions.

Dans le projet ExtendedOperations, ajoutez des références à SimpleCalculator et à System.ComponentModel.Composition. Dans le fichier de classe ExtendedOperations, ajoutez une instruction Imports ou using pour System.ComponentModel.Composition. En Visual Basic, ajoutez également une instruction Imports pour SimpleCalculator. Ajoutez ensuite la classe suivante au fichier de classe ExtendedOperations :

<Export(GetType(SimpleCalculator.IOperation))>
<ExportMetadata("Symbol", "%"c)>
Public Class Modulo
    Implements IOperation

    Public Function Operate(ByVal left As Integer, ByVal right As Integer) As Integer Implements IOperation.Operate
        Return left Mod right
    End Function
End Class
[Export(typeof(SimpleCalculator.IOperation))]
[ExportMetadata("Symbol", '%')]
public class Mod : SimpleCalculator.IOperation
{
    public int Operate(int left, int right)
    {
        return left % right;
    }
}

Notez que l'attribut ExportAttribute doit avoir le même type que l'ImportAttribute pour que le contrat corresponde.

Compilez et exécutez le projet. Testez le nouvel opérateur Mod (%).

Conclusion

Cette rubrique a couvert les concepts de base de MEF.

  • Parties, catalogues et conteneur de composition

    Les parties et le conteneur de composition sont les blocs de construction élémentaires d'une application MEF. Une partie est un objet qui importe ou exporte une valeur, y compris lui-même. Un catalogue fournit une collection de parties à partir d'une source particulière. Le conteneur de composition utilise les parties fournies par un catalogue pour exécuter la composition, la liaison d'importations à des exportations.

  • Importations et exportations

    Les importations et les exportations sont un moyen de communication utilisé par les composants. Lors d'une importation, le composant spécifie qu'il a besoin d'une valeur ou d'un objet en particulier, et lors d'une exportation, il indique la disponibilité d'une valeur. Chaque importation est associée à une liste d'exportations par l'intermédiaire de son contrat.

Informations complémentaires

Pour télécharger l'intégralité du code pour cet exemple, consultez l'exemple SimpleCalculator (page éventuellement en anglais).

Pour plus d'informations et des exemples de code, consultez Managed Extensibility Framework (page éventuellement en anglais). Pour obtenir une liste des types MEF, consultez l'espace de noms System.ComponentModel.Composition.

Historique des modifications

Date

Historique

Motif

Juillet 2010

Mise à jour des étapes. Ajout des étapes manquantes pour VB. Ajout d'un lien vers l'exemple à télécharger.

Commentaires client.