Partager via


Managed Extensibility Framework

Développement d'applications modulables dans .NET 4 avec la bibliothèque Managed Extensibility Framework

Glenn Block

Avec la parution imminente de Microsoft .NET Framework 4, vous allez découvrir une nouvelle technologie capable de simplifier grandement le développement d'applications. Si vous rencontrez des difficultés à concevoir des applications qui soient plus faciles à entretenir et à étendre, poursuivez la lecture.

La bibliothèque Managed Extensibility Framework (MEF) est une nouvelle bibliothèque fournie avec .NET Framework 4 et Silverlight 4 qui simplifie la conception de systèmes modulables pouvant être étendus par des tiers après leur déploiement. MEF ouvre vos applications, permettant l'introduction incrémentielle de nouvelles fonctionnalités par les développeurs, les créateurs de structures et les extendeurs tiers.

Pourquoi le proposons-nous ?

Il y a plusieurs années, chez Microsoft, plusieurs équipes s'évertuaient à trouver des solutions au problème suivant, comment construire des applications à partir de composants réutilisables pouvant être découverts, réutilisés et agencés de manière dynamique :

  • Visual Studio 2010 développait un nouvel éditeur de code extensible. Les capacités centrales de l'éditeur, ainsi que les capacités de tiers, étaient toutes à déployer en tant que binaires devant être découverts au moment de l'exécution. Une des spécifications principales était de prendre en charge tardivement le chargement d'extensions afin de réduire le temps de démarrage et la consommation de mémoire.
  • « Oslo » intégrait la technologie « Intellipad », nouvel éditeur de texte extensible pour utiliser MEF (Managed Extensibility Framework). Dans Intellipad, les plug-ins devaient être créés dans IronPython.
  • Acropolis apportait une structure pour la construction d'applications composites. Le runtime Acropolis découvrait les « pièces » composant l'application au moment de l'exécution et apportait ces pièces en les couplant avec des services. Acropolis faisait grand usage de XAML pour la création des composants.

Ce problème n'était pas spécifique à Microsoft. Les clients implémentent leurs propres solutions d'extensibilité personnalisées depuis longtemps. Il y avait une occasion à saisir pour la plateforme de s'illustrer et une solution plus polyvalente à apporter à la fois à Microsoft et à ses clients.

Avions-nous besoin de quelque chose de neuf ?

MEF n'est en aucune manière la première solution à ce problème. De nombreuses solutions ont été proposées, une longue liste de produits qui dépassent les limites de la plateforme et incluent des technologies telles que EJB, CORBA, l'implémentation d'OSGI d'Eclipse et Spring du côté Java. Sur la plateforme de Microsoft, il y a le modèle de composant et System.Addin au sein même du .NET Framework. Plusieurs solutions « open source » existent également, telles que l'architecture SODA de SharpDevelop et des conteneurs Inversion of Control comme Castle Windsor, Structure Map et les modèles & pratiques de Unity.

Avec toutes ces approches existantes, pourquoi proposer une solution supplémentaire ? Nous avons réalisé qu'aucune de nos solutions d'alors ne répondait à la question de l'extensibilité générale par des tiers de manière satisfaisante. Elles étaient soit trop lourdes pour un usage général ou nécessitaient trop d'effort de la part de l'hôte ou du développeur de l'extension. MEF représente le point culminant de l'apprentissage de chacune de ces solutions, et une tentative de traiter les poins faibles mentionnés.

Intéressons-nous aux principaux concepts de MEF, illustrés en figure 1.

image : Principaux concepts de Managed Extensibility Framework

Figure 1 Principaux concepts de Managed Extensibility Framework

Concepts

Au cœur de MEF résident quelques concepts essentiels :

Pièce modulable (ou, simplement, pièce)—Une pièce fournit des services aux autres pièces et consomme les services fournis par les autres pièces. Dans MEF, les pièces peuvent provenir de n'importe où, de l'intérieur ou de l'extérieur de l'application ; du point de vue de MEF, cela n'a aucune importance.

Exportation—Une exportation est un service que fournit une pièce. Lorsqu'une pièce fournit une exportation, on dit qu'elle l'exporte. Par exemple, une pièce peut exporter un enregistreur ou, dans le cas de Visual Studio, une extension d'éditeur. Les pièces peuvent fournir de nombreuses exportations, mais la plupart d'entre elles n'en fournissent qu'une seule.

Importation—Une importation est un service que consomme une pièce. Lorsqu'une pièce consomme une importation, on dit qu'elle l'importe. Les pièces peuvent importer des services uniques, tels que l'enregistreur, ou importer plusieurs services, tels que l'extension d'éditeur.

Contrats—Un contrat est un identificateur d'exportation ou d'importation. L'exportateur spécifie un contrat de chaîne qu'il fournit ; l'importateur spécifie le contrat dont il a besoin. MEF dérive les noms de contrats des types qui sont exportés et importés, de sorte que vous n'avez pas à vous en soucier dans la plupart des cas.

Composition—Les pièces sont composées par MEF, lequel les instancie puis met en correspondance les exportateurs et les importateurs.

Modèles de programmation—le ou les visages de MEF

Les développeurs consomment MEF via un modèle de programmation. Un modèle de programmation procure un moyen de déclarer les composants comme des pièces MEF. Prêt à l'emploi, MEF propose un modèle de programmation attribué, dont il sera principalement question dans cet article. Ce modèle est juste l'un des nombreux modèles de programmation autorisés par MEF. L'API principale de MEF est totalement indifférente aux attributs.

Exploration du modèle de programmation attribué

Dans le modèle de programmation attribué, les pièces (dénommées pièces attribuées) sont définies à l'aide d'un jeu d'attributs .NET, qui vit dans l'espace de noms System.ComponentModel.Composition. Dans les sections qui suivent, nous parlerons de la construction d'une application extensible de gestion de commandes WPF (Windows Presentation Foundation) à l'aide de ce modèle. Cette application permet aux clients d'ajouter de nouvelles vues personnalisées dans leurs environnements par simple déploiement d'un binaire dans le dossier bin. J'indiquerai comment implémenter cela par MEF. J'améliorerai progressivement la conception et en dirai plus sur les capacités de MEF et sur ce que le modèle de programmation attribué apporte à mesure du déroulement du processus.

Exportation d'une classe

L'application de gestion des commandes permet le branchement de nouvelles vues. Pour exporter quelque chose à destination de MEF, vous utilisez l'attribut Export tel que décrit ici :

[Export]
public partial class SalesOrderView : UserControl
{
public SalesOrderView()
  {
InitializeComponent();
  }
}

La pièce ci-dessous exporte le contrat SalesOrderView. Par défaut, l'attribut Export utilise le type concret du membre (dans ce cas, la classe) comme contrat. Vous pouvez également spécifier explicitement le contrat en passant un paramètre au constructeur de l'attribut.

Importation via propriétés et champs

Les pièces attribuées peuvent exprimer les choses dont elles ont besoin en utilisant l'attribut d'importation sur un propriété ou un champ. L'application exporte une pièce ViewFactory, que les autres pièces peuvent utiliser pour accéder aux vues. Cette ViewFactory importe SalesOrderView à l'aide d'une importation de propriété. Importer une propriété signifie simplement décorer une propriété à l'aide d'un attribut Import:

[Export]
public class ViewFactory
{
  [Import]
  public SalesOrderView OrderView { get; set; }
}

Importation via constructeurs

Les pièces peuvent également importer par l'intermédiaire de constructeurs (suivant une technique qualifiée d'injection du constructeur) à l'aide de l'attribut ImportingConstructor, comme illustré ci-dessous. Lorsque vous utilisez un constructeur d'importation, MEF part du principe que tous les paramètres sont des importations, rendant l'attribut d'importation inutile :

[Export]
public class ViewFactory
{
  [ImportingConstructor]
  public ViewFactory(SalesOrderView salesOrderView)
{
}
}

En règle générale, l'importation via constructeurs plutôt que via propriétés est une question de préférence, bien qu'il soit parfois approprié d'utiliser des importations de propriétés, notamment lorsque des pièces ne sont pas instanciées par MEF, comme dans l'exemple WPF App. La recomposition n'est pas non plus prise en charge sur des paramètres de constructeurs.

Composition

Avec SalesOrderView et ViewFactory en place, vous pouvez commencer maintenant la composition. Les pièces MEF ne sont pas découvertes ou créées automatiquement. Vous devez au contraire écrire du code de démarrage qui mettra en œuvre la composition. Vous pouvez notamment choisir de le faire dans le point d'entrée de votre application qui, dans le cas présent, est la classe App.

Démarrer MEF nécessite quelques étapes :

  • Ajoutez les importations des contrats qui doivent être créés par le conteneur.
  • Créez un catalogue que MEF utilise pour découvrir les pièces.
  • Créez un conteneur qui compose des instances des pièces.
  • Composez en appelant la méthode Composeparts sur le conteneur et en transmettant l'instance qui a les importations.

Comme vous le voyez ici, j'ai ajouté une importation ViewFactory sur la classe App. J'ai créé ensuite un DirectoryCatalog pointant vers le dossier bin, puis un conteneur qui utilise le catalogue. Enfin, j'ai appelé Composeparts, qui a causé la composition d'une instance App et l'importation de ViewFactory d'être satisfaite :

public partial class App : Application
{
  [Import]
public ViewFactory ViewFactory { get; set; }

public App()
  {
this.Startup += new StartupEventHandler(App_Startup);
  }

void App_Startup(object sender, StartupEventArgs e)
  {
var catalog = new DirectoryCatalog(@".\");
var container = new CompositionContainer(catalog);
container.Composeparts(this);
  }
}

Au cours de la composition, le conteneur créera ViewFactory et satisfera son importation SalesOrderView. Tout ceci entraîne la création de SalesOrderView. Enfin, la classe Application aura son importation ViewFactory satisfaite. De cette manière, MEF a assemblé le graphique d'objet tout entier sur la base d'informations déclaratives, plutôt que de requérir manuellement un code impératif pour réaliser l'assembly.

L'exportation d'éléments non-MEF vers MEF via des propriétés

En intégrant MEF dans une application existante, ou avec d'autres infrastructures, vous trouverez souvent des instances de classe non liées à MEF (ce qui signifie qu'il ne s'agit pas de pièces) que vous trouverez judicieux de mettre à la disposition des importateurs. Il peut s'agir de types d'infrastructures scellées tels que System.String, de singletons au niveau de l'application tels que Application.Current, ou d'instances récupérées d'une fabrique, telles qu'une instance d'enregistreur récupérée de Log4Net.

Pour permettre cela, MEF autorise les exportations de propriétés. Pour utiliser les exportations de propriétés, vous créez une pièce intermédiaire avec une propriété décorée d'une exportation. Cette propriété est essentiellement une fabrique et exécute toute logique personnalisée qui est nécessaire à la récupération de la valeur non MEF. Dans l'exemple de code suivant, vous voyez que Loggerpart exporte un enregistreur Log4Net, permettant à d'autres pièces telles que l'App de l'importer plutôt que de dépendre de l'accès à la méthode d'accesseur statique :

public class Loggerpart
{
  [Export]
public ILog Logger
  {
get { return LogManager.GetLogger("Logger"); }
  }
}

Les exportations de propriété sont comme des couteaux suisses dans leur fonctionnalité, qui permettent à MEF de bien jouer avec d'autres. Vous les trouverez extrêmement utiles pour intégrer MEF dans vos apps existantes et pour communiquer avec les systèmes hérités.

Découplage d'une implémentation avec une interface

En reprenant l'exemple de SalesOrderView, une relation étroite a été formée entre ViewFactory et SalesOrderView. La fabrique attend un SalesOrderView concret qui limite les options d'extensibilité ainsi que la testabilité de l'usine elle-même. MEF permet aux importations de se découpler de l'implémentation de l'exportateur en utilisant une interface comme contrat :

public interface ISalesOrderView{}

[Export(typeof(ISalesOrderView))]
public partial class SalesOrderView : UserControl, ISalesOrderView
{
   ...
}

[Export]
public class ViewFactory
{
  [Import]
ISalesOrderView OrderView{ get; set; }
}

Dans le code précédent, j'ai changé SalesOrderView pour implémenter ISalesOrderView et l'exporter explicitement. J'ai également changé la fabrique du côté de l'importateur pour importer ISalesOrderView. Notez que l'importateur n'a pas à spécifier explicitement le type, puisque MEF peut le dériver du type de propriété, qui est ISalesOrderView.

Cela soulève la question de savoir si ViewFactory doit également implémenter une interface telle que IViewFactory. Il ne s'agit pas d'une obligation, bien que cela se justifie dans un but de simulation. À ce sujet, je ne m'attends pas à ce que qui que ce soit remplace ViewFactory, et celui-ci est conçu pour être testé, cela ne pose donc pas de problème. Vous pouvez avoir plusieurs exportations sur une pièce pour que celle-ci puisse être importée sous plusieurs contrats. SalesOrderView, par exemple, peut exporter à la fois UserControl et ISalesOrderView en utilisant un attribut d'exportation supplémentaire :

[Export (typeof(ISalesOrderView))]
[Export (typeof(UserControl))]
public partial class SalesOrderView : UserControl, ISalesOrderView
{
   ...
}

Assemblies de contrats

Lorsque vous commencerez à créer des contrats, il vous faudra un moyen de déployer ces contrats à destination de tiers. Un moyen commun de le faire consiste à disposer d'un assembly de contrats qui contienne des interfaces pour les contrats qui seront implémentés par des extendeurs. L'assembly de contrats devient une sorte de SDK que les pièces référenceront. Un modèle classique consiste à nommer l'assembly de contrats comme suit : nom de l'application + .Contracts, comme dans SalesOrderManager.Contracts.

Importation de nombreuses exportations du même contrat

ViewFactory importe actuellement une seule vue. Le codage à la main d'un membre (paramètre de propriété) pour chaque vue fonctionne pour un très petit nombre de types prédéfinis de vues qui changent peu. Toutefois, avec une approche de ce type, l'ajout de nouvelles vues nécessite que la fabrique soit recompilée.

Si vous attendez un grand nombre de types de vues, MEF offre une meilleure approche. Au lieu d'utiliser une interface de vue spécifique, vous pouvez créer une interface IView générique que toutes les vues exportent. La fabrique importe ensuite une collection de toutes les Iviews disponibles. Pour importer une collection dans le modèle attribué, utilisez l'attribut ImportMany :

[Export]
public class ViewFactory
{
  [ImportMany]
IEnumerable<IView> Views { get; set; }
}

[Export(typeof(IView))]
public partial class SalesOrderView : UserControl, IView
{
}
//in a contract assembly
public interface IView{}

Vous voyez ici que ViewFactory importe maintenant une collection d'instances IView plutôt qu'une vue spécifique. SalesOrder implémente IView et l'exporte plutôt que ISalesOrderView. Avec cette refactorisation, ViewFactory peut maintenant prendre en charge un jeu de vues ouvert.

MEF prend également en charge l'importation à l'aide de collections concrètes telles que ObservableCollection<T> ou List<T>, ou de collections personnalisées qui fournissent un constructeur par défaut.

Stratégie de contrôle de création des pièces

Par défaut, toutes les instances de pièces présentes dans le conteneur sont des singletons, et sont donc partagées par toutes les pièces qui les importent dans le conteneur. Pour cette raison, tous les importateurs de SalesOrderView et ViewFactory obtiendront la même instance. C'est souhaitable dans de nombreux cas, puisque cela évite d'avoir des membres statiques dont dépendent les autres composants. Toutefois, il est parfois nécessaire que chaque importateur dispose de sa propre instance, par exemple, pour permettre l'affichage simultané de plusieurs instances SalesOrderView à l'écran.

La stratégie de création de pièce dans MEF peut avoir une des trois valeurs suivantes : CreationPolicy.Shared, CreationPolicy.NonShared ou CreationPolicy.Any. Pour spécifier une stratégie de création sur une pièce, vous la décorez avec l'attribut partCreationPolicy, comme suit :

[partCreationPolicy(CreationPolicy.NonShared)]
[Export(typeof(ISalesOrderView))]
public partial class SalesOrderView : UserControl, ISalesOrdderView
{
public SalesOrderView()
  {
  }
}

L'attribut partCreationPolicy peut également être spécifié du côté de l'importateur en définissant la propriété RequiredCreationPolicy sur l'importation.

Distinction des exportations à l'aide de métadonnées

ViewFactory fonctionne maintenant avec un jeu ouvert de vues, mais je n'ai aucun moyen de distinguer une vue d'une autre. Je pourrais ajouter un membre à IView appelé ViewType, qui serait fourni par la vue, puis filtrer sur cette propriété. Une autre solution consiste à utiliser les fonctions de métadonnées d'exportation de MEF pour annoter la vue avec son ViewType. L'utilisation des métadonnées offre l'avantage supplémentaire d'autoriser la mise en attente de l'instanciation de la vue jusqu'à ce qu'elle soit nécessaire, ce qui permet de conserver les ressources et d'améliorer les performances.

Définition des métadonnées d'exportation

Pour définir les métadonnées sur une exportation, vous utilisez l'attribut ExportMetadata. Ci-dessous, SalesOrderView a été modifié de sorte d'exporter une interface de marqueur IView comme son contrat. Il ajoute ensuite des métadonnées supplémentaires de « ViewType » pour lui permettre d'être repéré parmi d'autres vues partageant le même contrat :

[ExportMetadata("ViewType", "SalesOrder")]
[Export(typeof(IView)]
public partial class SalesOrderView : UserControl, IView
{
}

ExportMetadata possède deux paramètres, une clé qui est une chaîne et une valeur d'objet de type. L'utilisation de chaînes magiques comme dans l'exemple précédent peut être problématique car elles ne sont pas compatibles à la compilation. Au lieu d'une chaîne magique, nous pouvons fournir une constante pour la clé et un enum pour la valeur :

[ExportMetadata(ViewMetadata.ViewType, ViewTypes.SalesOrder)]
[Export(typeof(IView)]
public partial class SalesOrderView : UserControl, IView
{
  ...
}
//in a contract assembly
public enum ViewTypes {SalesOrderView}

public class ViewMetadata
{
public const string ViewType = "ViewType";
}

L'utilisation de l'attribut ExportMetadata offre une grande souplesse, mais il faut noter quelques réserves :

  • Les clés de métadonnées ne sont pas découvrables dans l'IDE. L'auteur de la pièce doit savoir quelles clés et quels types de métadonnées sont valides pour l'exportation.
  • Le compilateur ne validera pas les métadonnées pour garantir leur exactitude.
  • ExportMetadata ajoute du bruit au code, masquant son intention.

MEF propose une solution pour traiter les questions ci-dessus : des exportations personnalisées.

Attributs d'exportation personnalisés

MEF autorise la création d'exportations personnalisées qui incluent leurs propres métadonnées. Créer une exportation personnalisée implique de créer un ExportAttribute dérivé qui spécifie également des métadonnées. Nous pouvons utiliser des exportations personnalisées pour créer un attribut ExportView qui incluse des données pour ViewType :

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class ExportViewAttribute : ExportAttribute {
public ExportViewAttribute()
:base(typeof(IView))
  {}

public ViewTypes ViewType { get; set; }
}

ExportViewAttribute spécifie qu'il exporte IView en appelant le constructeur de base d'Export. Il est décoré avec un MetadataAttribute, qui spécifie que l'attribut fournit des métadonnées. Cet attribut indique à MEF de regarder toutes les propriétés publiques et de créer des métadonnées associées sur l'exportation en se servant du nom de la propriété comme de la clé. Dans notre exemple, la seule métadonnée est ViewType.

La dernière chose importante à noter à propos de l'attribut ExportView est qu'il est décoré à l'aide d'un attribut AttributeUsage. Cela spécifie que l'attribut est valide uniquement sur les classes et qu'un seul attribut ExportView peut être présent.

En règle générale, AllowMultiple doit avoir « false » pour valeur ; avec la valeur « true », l'importateur recevra un tableau de valeurs plutôt qu'une seule. AllowMultiple doit conserver la valeur « true » lorsqu'on trouve plusieurs exportations avec différentes métadonnées du même contrat sur le même membre.

L'application de nouvel ExportViewAttribute sur le SalesOrderView produit désormais ce qui suit :

[ExportView(ViewType = ViewTypes.SalesOrder)]  
public partial class SalesOrderView : UserControl, IView
{
}

Comme vous le constatez, les exportations personnalisées veillent à ce que les métadonnées correctes soient fournies à une exportation donnée. Elles réduisent également le bruit dans le code, et sont plus découvrables via IntelliSense et expriment mieux l'intention en étant spécifiques à des domaines.

À présent que les métadonnées ont été définies sur la vue, ViewFactory peut l'importer.

Importation d'exportations tardives et accès aux métadonnées

Pour autoriser l'accès aux métadonnées, MEF bénéficie d'une nouvelle API de.NET Framework 4, System.Lazy<T>. Il autorise la mise en attente de l'instanciation d'une instance jusqu'à ce que la propriété de valeur de Lazy fasse l'objet d'un accès. MEF étend plus encore Lazy<T> avec Lazy<T,TMetadata> pour autoriser l'accès aux métadonnées d'exportation sans instanciation de l'exportation sous-jacente.

TMetadata est un type de vue de métadonnées. Une vue de métadonnées est une interface qui définit les propriétés en lecture seule qui correspondent aux clés dans les métadonnées exportées. Lorsque la propriété de métadonnées fait l'objet d'un accès, MEF implémente dynamiquement TMetadata et définit les valeurs en fonction des métadonnées fournies dans l'exportation.

C'est à cela que ViewFactory ressemble lorsque la propriété View est changée en importation avec Lazy<T,TMetadata>:

[Export]
public class ViewFactory
{
  [ImportMany]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }
}

public interface IViewMetadata
{
ViewTypes ViewType {get;}
}

Une fois qu'une collection d'exportations tardives dotée de métadonnées a été importée, vous pouvez utiliser LINQ pour filtrer sur le jeu. Dans l'extrait de code suivant, j'ai implémenté une méthode GetViews sur ViewFactory pour récupérer toutes les vues du type spécifié. Notez qu'elle accède à la propriété Value pour fabriquer des vraies instances de vues uniquement pour les vues qui correspondent au filtre :

[Export]
public class ViewFactory
{
  [ImportMany]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }

public IEnumerable<View> GetViews(ViewTypesviewType) {
return Views.Where(v=>v.Metadata.ViewType.Equals(viewType)).Select(v=>v.Value);
  }
}

Avec ces changements, ViewFactory découvre maintenant toutes les vues qui sont disponibles au moment où la fabrique est composée par MEF. Si de nouvelles implémentations apparaissent dans le conteneur ou dans les catalogues après cette composition initiale, elles ne seront pas vues par la ViewFactory, puisque celle-ci était déjà composée. En plus de cela, MEF empêchera en réalité les vues d'être ajoutées au catalogue en déclenchant une CompositionException—c'est-à-dire, jusqu'à ce que la recomposition soit activée.

Recomposition

La recomposition est une fonction de MEF qui autorise la mise à jour automatique des importations des pièces à mesure que de nouvelles exportations correspondantes apparaissent dans le système. La recomposition est utile pour télécharger des pièces depuis un serveur distant, par exemple. SalesOrderManager peut être modifié de sorte que lorsqu'il démarre, il initie le téléchargement de plusieurs vues facultatives. À mesure que les vues font leur apparition, elles apparaissent dans la fabrique de vues. Pour rendre la ViewFactory recomposable, nous définissons la propriété AllowRecomposition sur l'attribut ImportMany de la propriété Views comme étant « true », comme indiqué ici :

[Export]
public class ViewFactory
{
[ImportMany(AllowRecomposition=true)]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }

public IEnumerable<View>GetViews(ViewTypesviewType) {
return Views.Where(v=>v.Metadata.ViewType.Equals(viewType)).Select(v=>v.Value);
  }
}

Lorsque la recomposition aura lieu, la collection Views sera instantanément remplacée par une nouvelle collection qui contiendra le jeu de vues mis à jour.

Avec la recomposition activée, l'application peut télécharger des assemblys supplémentaires depuis le serveur et les ajouter au conteneur. Vous pouvez le faire via les catalogues MEF. MEF offre plusieurs catalogues, deux d'entre eux étant recomposables. DirectoryCatalog, que vous avez déjà vu, est un catalogue qui est recomposé en appelant sa méthode Refresh. Un autre catalogue recomposable est AggregateCatalog, qui est un catalogue de catalogues. Vous y ajoutez des catalogues à l'aide de la propriété de collection Catalogs, qui démarre la recomposition. Le dernier catalogue que je vais utiliser est un AssemblyCatalog, qui accepte un assembly sur lequel il construit ensuite un catalogue. La figure 2 illustre comment utiliser ces catalogues ensemble à des fins de téléchargement dynamique.

Figure 2 Utilisation de catalogues MEF pour un téléchargement dynamique

void App_Startup(object sender, StartupEventArgs e)
{
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(newDirectoryCatalog((@"\.")));
var container = new CompositionContainer(catalog);
container.Composeparts(this);
base.MainWindow = MainWindow;
this.DownloadAssemblies(catalog);
}

private void DownloadAssemblies(AggregateCatalog catalog)
{
//asynchronously downloads assemblies and calls AddAssemblies
}

private void AddAssemblies(Assembly[] assemblies, AggregateCatalog catalog)
{
var assemblyCatalogs = new AggregateCatalog();
foreach(Assembly assembly in assemblies)
assemblyCatalogs.Catalogs.Add(new AssemblyCatalog(assembly));
catalog.Catalogs.Add(assemblyCatalogs);
}

Le conteneur en figure 2 est créé avec AggregateCatalog. Un DirectoryCatalog lui est ensuite ajouté pour saisir les pièces locales dans le dossier bin. Le catalogue agrégé est transmis à la méthode DownloadAssemblies, laquelle télécharge de manière asynchrone les assemblys, puis appelle AddAssemblies. Cette méthode crée un nouveau AggregateCatalog, auquel il ajoute des AssemblyCatalogs pour chaque assembly téléchargé. AddAssemblies ajoute ensuite le AggregateCatalog contenant les assemblys pour l'agrégat principal. La raison qu'il ajoute de cette manière est que la recomposition ait lieu en une seule fois, plutôt que de manière répétée, ce qui se produirait si nous ajoutions directement les catalogues d'assemblys.

Lorsque la recomposition se produit, la collection est immédiatement mise à jour. Le résultat varie en fonction du type de propriété de la collection. Si la propriété est de type IEnumerable<T>, elle est remplacée par une nouvelle instance. S'il s'agit d'une collection concrète qui hérite de List<T> ou ICollection, MEF appellera Clear puis Add pour chaque élément. Dans les deux cas, cela signifie que vous devrez tenir compte de la sécurité des threads en utilisant la Recomposition. La Recomposition n'est pas seulement associé aux ajouts, elle l'est également aux suppressions. Si les catalogues sont supprimés du conteneur, ces pièces seront également supprimées.

Composition, réjection et diagnostics stables

Il arrive qu'une pièce spécifie une importation manquante, si elle ne figure pas dans le catalogue. Lorsque cela se produit, MEF empêche que la pièce dépourvue de sa dépendance (ou tout autre chose qui en dépend) soit découverte. MEF procède de cette manière dans le but de stabiliser le système et d'éviter les échecs d'exécution qui ne manqueraient pas de se produire si la pièce était créée.

Ici, SalesOrderView a été modifié pour importer un Ilogger, même en l'absence de toute instance d'enregistreur :

[ExportView(ViewType = ViewTypes.SalesOrder)]  
public partial class SalesOrderView : UserControl, IView
{
[Import]
public ILogger Logger { get; set; }
}

Étant donné qu'aucune exportation d'ILogger n'est disponible, l'exportation de SalesOrderView ne sera pas visible par le conteneur. Cela ne déclenchera pas d' exception. SalesOrderView sera simplement ignoré. Si vous consultez la collection Views de ViewFactory, celle-ci sera vide.

Le rejet aura également lieu dans les cas où l'on trouve plusieurs exportations pour une seule importation. Dans ces cas, la pièce qui importe la seule exportation est rejetée :

[ExportView(ViewType = ViewTypes.SalesOrder)]  
public partial class SalesOrderView : UserControl, IView
{
[Import]
public ILogger Logger { get; set; }
}
 [Export(typeof(ILogger))]  
public partial class Logger1 : ILogger
{
}
 [Export(typeof(ILogger))]  
public partial class Logger2 : ILogger
{
}

Dans l'exemple précédent, SalesOrderView sera rejeté car il existe plusieurs implémentations d'ILogger, mais qu'une seule implémentation est importée. MEF offre des fonctions pour autoriser une exportation par défaut en présence de plusieurs. Pour plus d'informations à ce sujet, consultez codebetter.com/blogs/glenn.block/archive/2009/05/14/customizing-container-behavior-part-2-of-n-defaults.aspx.

Vous pouvez vous demander pourquoi MEF ne crée pas SalesOrderView et ne déclenche pas une exception. Dans un système extensible ouvert, si MEF déclenche une exception, il sera très difficile à l'application de la traiter, ou que le contexte sache comment réagir, car la pièce peut être manquante ou l'importation être imbriquée très profondément au sein de la composition. Sans traitement approprié, l'application se trouverait dans un état de non-validité et serait inutilisable. MEF rejette la pièce, veillant ainsi au maintien de la stabilité de l'application. Pour plus d'informations sur la stabilité des compositions, consultez : blogs.msdn.com/gblock/archive/2009/08/02/stable-composition-in-mef-preview-6.aspx.

Diagnostic du rejet

Le rejet est une fonction très puissante qui est parfois difficile à diagnostiquer, notamment lorsque le graphique de dépendance tout entier est rejeté. Dans le premier exemple du début, ViewFactory importe directement un SalesOrderView. Supposons que MainWindow a importé ViewFactory et que SalesOrderView est rejeté. ViewFactory et MainWindow sont alors également rejetés. Vous pouvez vraiment vous demander ce qui se passe si cela se produit puisque vous savez que MainWindow et ViewFactory sont effectivement présents. La raison du rejet est une dépendance manquante.

MEF vous permet de comprendre la situation. Pour aider au diagnostic de ce problème, il dispose d'informations de suivi. Dans l'IDE, tous les messages de rejet sont suivis dans la fenêtre de sortie, bien qu'ils puissent également être suivis à l'aide de n'importe quel écouteur de trace valide. Par exemple, lorsque l'application essaie d'importer MainWindow, les messages de suivi présentés en figure 3 seront générés.

Figure 3 Messages de suivi de MEF

System.ComponentModel.Composition Warning: 1 : The ComposablepartDefinition 'Mef_MSDN_Article.SalesOrderView' has been rejected. The composition remains unchanged. The changes were rejected because of the following error(s): The composition produced a single composition error. The root cause is provided below. Review the CompositionException.Errors property for more detailed information.

1) No valid exports were found that match the constraint '((exportDefinition.ContractName == "Mef_MSDN_Article.ILogger") AndAlso (exportDefini-tion.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "Mef_MSDN_Article.ILogger".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity"))))', invalid exports may have been rejected.

Resulting in: Cannot set import 'Mef_MSDN_Article.SalesOrderView.Logger (ContractName="Mef_MSDN_Article.ILogger")' on part 'Mef_MSDN_Article.SalesOrderView'.
Element: Mef_MSDN_Article.SalesOrderView.logger (ContractName="Mef_MSDN_Article.ILogger") -->Mef_MSDN_Article.SalesOrderView -->TypeCatalog (Types='Mef_MSDN_Article.MainWindow, Mef_MSDN_Article.SalesOrderView, ...').

La sortie du suivi identifie la cause du problème : SalesOrderView nécessite un ILogger, or aucun n'est repérable. Nous voyons alors que son rejet a entraîné le rejet de la fabrique, puis de la MainWindow.

Inspection des pièces dans le débogueur

Vous pouvez aussi inspecter véritablement les pièces disponibles du catalogue. Il en sera question dans la section consacrée à l'hébergement. La figure 4 montre les pièces disponibles dans la fenêtre espion (dans les cercles verts) ainsi que l'importation ILogger requise (dans le cercle bleu).

image : Pièces disponibles et ILogger requis dans une fenêtre espion

Figure 4 Pièces disponibles et ILogger requis dans une fenêtre espion

Diagnostiquer le rejet au niveau de la ligne de commande

L'un des objectifs de MEF était de prendre en charge l'analysabilité statique, et d'autoriser l'analyse de la composition en dehors de l'environnement d'exécution. Nous ne disposons pas encore de cette prise en charge dans Visual Studio, mais Nicholas Blumhardt a créé MEFX.exe (mef.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=33536), un outil en ligne de commande qui procure cette fonctionnalité. MEFX analyse les assemblys et détermine les pièces qui sont rejetées ainsi que la raison de ce rejet.

Si vous exécutez MEFX.exe au niveau de la ligne de commande, vous découvrez une gamme d'options ; vous pouvez lister des importations ou des exportations spécifiques, ou encore toutes les pièces disponibles. Par exemple, vous voyez ici l'utilisation de MEFX pour afficher la liste des pièces :

C:\mefx>mefx.exe /dir:C:\SalesOrderManagement\bin\debug /parts 
SalesOrderManagement.SalesOrderView
SalesOrderManagement.ViewFactory
SalesOrderManagement.MainWindow

C'est utile pour obtenir un inventaire des pièces, mais MEFX peut également repérer les rejets, ce qui nous intéresse ici, comme illustré en figure 5.

Figure 5 Suivi des rejets avec MEFX.exe

C:\mefx>mefx.exe /dir:C:\SalesOrderManagement\bin\debug /rejected /verbose 

[part] SalesOrderManagement.SalesOrderView from: DirectoryCatalog (Path="C:\SalesOrderManagement\bin\debug")
  [Primary Rejection]
  [Export] SalesOrderManagement.SalesOrderView (ContractName="SalesOrderManagement.IView")
  [Export] SalesOrderManagement.SalesOrderView (ContractName="SalesOrderManagement.IView")
  [Import] SalesOrderManagement.SalesOrderView.logger (ContractName="SalesOrderManagement.ILogger")
    [Exception] System.ComponentModel.Composition.ImportCardinalityMismatchException: No valid exports were found that match the constraint '((exportDefinition.ContractName == "SalesOrderManagement.ILogger") AndAlso (exportDefinition.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "SalesOrderManagement.ILogger".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity"))))', invalid exports may have been rejected.
at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition, AtomicCompositionatomicComposition)
at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition)
at Microsoft.ComponentModel.Composition.Diagnostics.CompositionInfo.AnalyzeImportDefinition(ExportProvider host, IEnumerable`1 availableparts, ImportDefinition id)

La dissection de la sortie en figure 6 révèle la cause du problème : ILogger est introuvable. Comme vous le constatez, dans les systèmes de grande envergure dotés de nombreuses pièces, MEFX est un outil extrêmement utile. Pour plus d'informations sur MEFX, consultez blogs.msdn.com/nblumhardt/archive/2009/08/28/analyze-mef-assemblies-from-the-command-line.aspx.

image : Exemple de pièce dans IronRuby

Figure 6 Exemple de pièce dans IronRuby

En résumé, le modèle attribué présente plusieurs avantages :

  • Il offre aux pièces un moyen universel pour déclarer leurs exportations et importations.
  • Il permet aux systèmes de découvrir dynamiquement les pièces disponibles plutôt que d'exiger une préinscription.
  • Il est statiquement analysable, autorisant des outils tels que MEFX à déterminer les échecs en avance.

Je vais maintenant vous présenter brièvement l'architecture et ce qu'elle autorise. Vue de haut, l'architecture de MEF se compose de couches : modèles de programmation, hébergement et primitives.

Révision des modèles de programmation

Le modèle attribué est une simple implémentation de ces primitives qui utilisent les attributs comme moyen de découverte. Les primitives peuvent représenter des pièces non attribuées, voire des pièces qui ne sont pas statiquement typées, comme dans le service d'exécution de langage dynamique (DLR, Dynamic Language Runtime). La figure 6 illustre une pièce IronRuby qui exporte une IOperation. Notez qu'elle utilise la syntaxe native d'IronRuby pour déclarer une pièce plutôt que le modèle attribué, puisque les attributs ne sont pas pris en charge dans le DLR.

MEF n'est pas fourni avec un modèle de programmation IronRuby, bien qu'il soit probable que nous ajoutions à l'avenir la prise en charge du langage dynamique.

Pour en savoir plus sur les expériences de construction d'un modèle de programmation Ruby dans le blog suivant : blogs.msdn.com/nblumhardt/archive/tags/Ruby/default.aspx.

Hébergement : Là où se produit la composition

Les modèles de programmation définissent les pièces, les importations et les exportations. Pour créer des instances et des graphiques d'objet, MEF fournit l'hébergement d'API qui sont principalement situées dans l'espace de noms System.ComponentModel.Composition.Hosting. La couche d'hébergement offre beaucoup de souplesse, de configurabilité et d'extensibilité. C'est l'endroit où se produit l'essentiel du travail dans MEF et où commence la découverte. La plupart de ceux qui se contentent de créer des pièces ne toucheront jamais cet espace de noms. Toutefois, si vous êtes hébergeur, vous les utiliserez comme je l'ai fait précédemment pour démarrer la composition.

Les catalogues fournissent des définitions de pièces (ComposablepartDefinition), qui décrivent les exportations et importations disponibles. Ils constituent l'unité principale de découverte dans MEF. MEF offre plusieurs catalogues dans l'espace de noms System.ComponentModel.Composition. Vous en avez déjà vus certains, tels que DirectoryCatalog, qui analyse un répertoire, AssemblyCatalog, qui analyse un assembly et TypeCatalog, qui analyse un jeu de types spécifique. Chacun de ces catalogues est spécifique au modèle de programmation attribué. Toutefois, AggregateCatalog est indépendant des modèles de programmation. Les catalogues héritent de ComposablepartCatalog et constituent des points d'extensibilité dans MEF. Les catalogues personnalisés ont plusieurs usages, qui vont de fournir un modèle de programmation entièrement nouveau à envelopper ou filtrer les catalogues existants.

La figure 7 montre un exemple de catalogue filtré, qui accepte un prédicat à utiliser pour filtrer le catalogue intérieur d'où seront renvoyées les pièces.

Figure 7 Un catalogue filtré

public class FilteredCatalog : ComposablepartCatalog, 
{
private readonly composablepartcatalog _inner;
private readonly IQueryable<ComposablepartDefinition> _partsQuery;

public FilteredCatalog(ComposablepartCatalog inner,
Expression<Func<ComposablepartDefinition, bool>> expression)
  {
      _inner = inner;
    _partsQuery = inner.parts.Where(expression);
  }

public override IQueryable<ComposablepartDefinition> parts
  {
get
      {
return _partsQuery;
      }
  }
}

Le CompositionContainer compose, ce qui signifie qu'il crée des pièces et satisfait les importations de ces pièces. En satisfaisant les importations, il puisera dans le pool des exportations disponibles. Si ces exportations ont également des importations, le conteneur les satisfera d'abord. De cette manière, le conteneur assemble des graphiques d'objet entiers à la demande. Pour le pool d'exportations, la source principale est un catalogue, mais des instances de pièces existantes peuvent avoir été ajoutées au conteneur et composées. Il est courant d'ajouter manuellement la classe du point d'entrée au conteneur, combinée à des pièces extraites du catalogue, bien que dans la plupart des cas les pièces proviennent du catalogue.

Les conteneurs peuvent être également imbriqués au sein d'une hiérarchie pour prendre en charge les scénarios de portée. Par défaut, les conteneurs enfants interrogent leur parent, mais ils peuvent également fournir leurs propres catalogues de pièces enfants, qui seront créées au sein du conteneur enfant :

var catalog = new DirectoryCatalog(@".\");
var childCatalog = new DirectoryCatalog(@".\Child\";
var rootContainer = new CompositionContainer(rootCatalog));
var childContainer = new CompositionContainer(childCatalog, 
rootContainer);

Dans le code précédent, childContainer apparaît comme un enfant de rootContainer. rootContainer et childContainer fournissent tous deux leurs propres catalogues. Pour plus d'informations sur l'utilisation du conteneur pour héberger MEF au sein de vos applications, voir codebetter.com/blogs/glenn.block/archive/2010/01/15/hosting-mef-within-your-applications.aspx.

Les primitives : Où naissent les pièces et les modèles de programmation

Les primitives situées dans System.ComponentModel.Composition.Primitives sont au niveau le plus bas de MEF. Elles représentent l'univers quantique de MEF, en quelque sorte, et son point d'extensibilité ultime. Jusqu'à présent, j'ai couvert le modèle de programmation attribué. Toutefois, le conteneur de MEF n'est pas du tout lié aux attributs, mais aux primitives. Les primitives définissent une représentation abstraite des pièces, qui comprend des définitions telles que ComposablepartDefinition, ImportDefinition et ExportDefinition, ainsi que Composablepart et Export, qui représentent de véritables instances.

L'exploration des primitives est un voyage en soi, un voyage que j'aimerais relater dans un futur article. Pour en savoir plus à ce sujet, cliquez sur blogs.msdn.com/dsplaisted/archive/2009/06/08/a-crash-course-on-the-mef-primitives.aspx.

MEF dans Silverlight 4, et au-delà

MEF est également fourni avec Silverlight 4. Tout ce dont il a été question ici s'applique au développement d'applications RIA (Rich Internet Applications) extensibles. Dans Silverlight, nous sommes allés encore plus loin et avons introduit des API supplémentaires pour faciliter la construction d'applications sur MEF. Ces améliorations seront, au final, intégrées au .NET Framework.

Pour en savoir plus sur MEF dans Silverlight 4, consultez ce post : codebetter.com/blogs/glenn.block/archive/2009/11/29/mef-has-landed-in-silverlight-4-we-come-in-the-name-of-extensibility.aspx.

Je n'a fait que gratter la surface de ce qu'il est possible de faire avec MEF. Il s'agit d'un outil puissant, robuste et flexible que vous pouvez ajouter à votre arsenal pour ouvrir vos applications à un monde de possibilités nouvelles. J'ai hâte de voir ce que vous allez en faire !        

Glenn Blockest responsable de projet de la nouvelle bibliothèque MEF (Managed Extensibility Framework) dans.NET Framework 4. Avant, il était planificateur de produit responsable de Prism et d'autres systèmes de conseil aux clients, au département Modèles & pratiques. Block est un fou d'informatique qui passe le plus clair de son temps à partager sa passion par le biais de conférences et de groupes tels que ALT.NET. Consultez son blog à l'adresse codebetter.com/blogs/glenn.block

Je remercie les experts techniques suivants d'avoir relu cet article : Ward Bell, Nicholas Blumhardt, Krzysztof Cwalina, Andreas Håkansson, Krzysztof Kozmic, Phil Langeberg, Amanda Launcher, Jesse Liberty, Roger Pence, Clemens Szypierski, Mike Taulty, Micrea Trofin et Hamilton Verissimo