Guide de programmation MEF

Jérémie Bertrand (Traduction)

Cet article est une traduction autorisée du guide de programmation MEF disponible en anglais sur CodePlex et enrichi de quelques compléments. Il se découpera en trois parties : une introduction sur le Managed Extensibility Framework, une autre centrée sur la découverte et l'utilisation de MEF, tandis que la dernière se penchera sur les fonctionnalités spécifiques à Silverlight.

Commentez

- Titre : Guide de programmation MEF

- Auteur : Jérémie Bertrand (Traduction)

- Parution : 30 mai 2010

- Mise à jour : 30 mai 2010

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2010 Jérémie Bertrand. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Qu'est-ce que MEF

- I-B. Quels sont les problèmes que MEF peut résoudre

- I-C. Comment fonctionne MEF

- I-D. Vue d'ensemble de l'architecture

- II. Guide de programmation MEF

- II-A. Héberger MEF dans une application

- II-A-1. Ajout d'une Part directement dans le container

- II-A-2. Ajout en utilisant un AssemblyCatalog

- II-B. Définition des Composable Parts et des contrats

- II-B-1. Composable Part

- II-B-2. Contrats

- II-B-2-1. Contrats d'interface / abstraits

- II-B-3. Assemblage de contrats

- II-C. Déclaration des exports

- II-C-1. Export de Composable Part

- II-C-2. Export de propriétés

- II-C-3. Export de méthodes

- II-C-4. Héritage d'export

- II-C-5. Découverte de Composable Parts privées

- II-D. Déclaration des imports

- II-D-1. Import de propriétés

- II-D-2. Paramètre de constructeur

- II-D-2-1. Import de paramètres

- II-D-3. Import de champs

- II-D-4. Imports optionnels

- II-D-5. Import de collections

- II-D-6. IPartImportsSatisfiedNotification

- II-E. Imports différés

- II-F. Exports et métadonnées

- II-F-1. Ajouter des métadonnées à un export

- II-F-1-1. Utiliser l'attribut ExportMetadata

- II-F-1-2. Utiliser un attribut d'export personnalisé

- II-F-2. Importer des métadonnées

- II-F-2-1. Utiliser des métadonnées fortement typées

- II-F-2-2. Utiliser des métadonnées faiblement typées

- II-F-2-3. Le filtrage des métadonnées et l'attribut DefaultValueAttribute

- II-G. Utilisation des catalogues

- II-G-1. AssemblyCatalog

- II-G-2. DirectoryCatalog

- II-G-3. AggregateCatalog

- II-G-4. TypeCatalog

- II-G-5. DeploymentCatalog - uniquement dans Silverlight

- II-G-6. Utilisation d'un catalogue dans un Container

- II-H. Catalogues filtrés

- II-I. Cycle de vie des Parts

- II-I-1. Shared, Non Shared et propriété

- II-I-2. Disposer le container

- II-I-3. Container et références des Parts

- II-I-4. Scoped operations et récupération rapide des ressources

- II-I-5. Hiérarchie des containers

- II-I-6. Ordre de disposition

- II-I-7. AddPart / RemovePart

- II-J. Recomposition

- II-J-1. Mises en garde

- II-J-2. La recomposition et Silverlight

- II-K. Requêter le CompositionContainer

- II-K-1. GetExportedValue

- II-K-2. GetExport

- II-K-3. GetExportedValueOrDefault

- II-L. Composition Batch

- II-M. Débogage et diagnostics

- II-M-1. Diagnostiquer les problèmes de composition

- II-M-1-1. Problèmes liés au rejet

- II-M-1-2. Dump Composition State

- II-M-1-3. Trouver les root causes probables

- II-M-1-4. Débogage des proxies

- II-M-2. Mefx : outil d'analyse en ligne de commande

- II-M-3. Tracer les informations de composition

- II-N. FAQ

- II-N-1. Comment avoir des exports utilisant un objet Type

- II-N-2. Comment utiliser des containers imbriqués

- III. Fonctionnalités spécifiques à Silverlight

- III-A. Héberger MEF dans Silverlight en utilisant le CompositionInitializer

- III-A-1. Utilisation de CompositionInitializer depuis des éléments XAML

- III-A-2. Mises en garde dans l'utilisation de CompositionInitializer.SatisfyImports

- III-B. Redéfinir la configuration de l'hôte

- III-B-1. Redéfinition grâce aux catalogues

- III-B-1-1. Configurer la découverte des Parts dans le XAP courant

- III-B-2. Redéfinition grâce à un container

- III-C. Instanciation dynamique et ExportFactory<T>

- III-C-1. ExportFactory<T>

- III-C-2. ExportLifetimeContext<T>

- III-D. Partitionnement d'application avec le DeploymentCatalog

- III-D-1. DeploymentCatalog

- III-D-2. Télécharger des Parts après le démarrage de l'application

- III-D-3. Composer dynamiquement les Parts téléchargées et utiliser la recomposition

- III-D-4. Téléchargement des Parts à la demande après démarrage de l'application et basé sur les actions de l'utilisateur

- III-D-5. Gestion des erreurs durant le téléchargement

- III-D-6. Mise à jour de la progression

- III-D-7. Annulation du téléchargement

- III-D-8. Utilisation du DeploymentCatalog quand l'application est hors-ligne ou Out of Browser

- III-D-9. Mises en garde contre l'utilisation du DeploymentCatalog

- IV. Codes sources et exemples

- V. Liens

- VI. Remerciements

- VII. Contact

I - Introduction

I-A - Qu'est-ce que MEF

Le Managed Extensibility Framework (MEF) simplifie la création d'applications extensibles. MEF offre des capacités de découverte ainsi que de composition que vous pourrez exploiter afin de charger des modules pour vos applications.

I-B - Quels sont les problèmes que MEF peut résoudre

MEF propose une solution simple au problème d'extensibilité lors de l'exécution. Jusqu'à présent, les applications voulant implémenter un modèle de plugins devaient créer leur propre infrastructure. Ces plugins étaient souvent spécifiques à l'application et ne pouvaient donc être réutilisés dans d'autres applications.

- MEF fournit une approche standard, dans laquelle l'application hôte s'expose elle-même et consomme des modules externes. De par leur nature, les modules ou extensions peuvent être réutilisés dans différentes applications. Toutefois, une extension pourra être utilisée de façon spécifique par chaque application. Les extensions peuvent également dépendre les unes des autres et MEF s'assurera qu'elles soient connectées entre elles dans le bon ordre ;

- MEF offre plusieurs approches de découverte afin que votre application localise et charge les extensions disponibles ;

- MEF permet de d'ajouter des informations supplémentaires (métadonnées) aux extensions, facilitant ainsi le filtrage et la recherche.

I-C - Comment fonctionne MEF

Grosso modo, le coeur de MEF est composé d'un catalogue et d'un container. Le catalogue est responsable de la découverte des extensions tandis que le container va coordonner leur création et satisfaire les dépendances.

- la première chose à connaître est la Composable Part (aussi appelée Part par la suite), qui représente une extension. Une Composable Part offre un ou plusieurs exports, et peut aussi dépendre d'un ou plusieurs services externes dit imports. Une Composable Part gère également une instance, qui peut être une instance d'un type donné (l'implémentation MEF par défaut). Néanmoins, MEF est extensible et des implémentations supplémentaires de Composable Part peuvent être créées tant qu'elles respectent les contrats d'Export/Import ;

- les exports et les imports ont chacun un contrat. Les contrats sont les ponts entre les exports et les imports. Un contrat d'export peut consister à fournir des métadonnées qui pourront alors être utilisées pour le filtrage ou la découverte. Par exemple, cela peut indiquer une capacité spécifique offerte par l'export ;

- les containers MEF interagissent avec les catalogues pour avoir accès aux Parts. Le container lui-même résout les dépendances des Parts et expose les Exports au monde extérieur. Vous êtes libre d'ajouter directement des Parts au container si vous en avez envie ;

- une Part retournée par un catalogue sera probablement une extension de votre application. Elle peut avoir des dépendances (Import) sur des composants proposés par l'application hôte et peut également fournir des fonctionnalités (Export) ;

- l'implémentation de MEF utilise les attributs pour déclarer les exports et les imports. Cela permet à MEF de déterminer quels Composable Parts, imports et exports sont disponibles.

Schéma explicatif :

I-D - Vue d'ensemble de l'architecture

Le design de MEF peut être divisé en trois couches distinctes. La couche Container, qui contient la plupart des API publiques disponibles pour l'utilisateur ; la couche Primitives, qui fournit une couche d'abstraction, permettant ainsi à MEF de ne pas être fortement lié à une seule approche de découverte ; et enfin la couche d'implémentation par défaut de la couche Primitives, appelée Attributed Programming Model, qui repose sur les types et les attributs pour la découverte et la définition des imports/exports.

La couche Container n'a pas de dépendances sur l'Attributed Programming Model, au contraire elle fonctionne uniquement avec les abstractions fournies par la couche Primitives. Cela rend ainsi possible le développement d'un modèle de programmation complètement différent, et qui peut par exemple fonctionner avec le modèle de programmation par défaut.

Le container représente le coordonnateur ainsi que le constructeur de la topologie ExportProvider. Le set d'instances d'ExportProvider utilisé par l'instance d'un container est chaîné, donc les instances peuvent se requêter entre elles lors de la phase de satisfaction de dépendances. Vous pouvez aussi implémenter un ExportProvider personnalisé afin d'exposer des exports de n'importe quelle source, comme WCF, un Container IoC, Remoting.

Un livre blanc (« Hosting the .NET composition primitives », disponible dans la partie « Liens » de cet article) décrit comment les Primitives peuvent être utilisées indépendamment de MEF.

II - Guide de programmation MEF

II-A - Héberger MEF dans une application

Héberger MEF dans une application implique la création d'une instance de CompositionContainer. Il faut y ajouter des Composable Parts (modules), ainsi que l'application hôte elle-même. Et enfin composer le tout.

Pour plus de détails, voici les différentes étapes de l'hébergement :

- créez une classe hôte. Dans l'exemple ci-dessous nous utilisons une application console, dans laquelle l'hôte est la classe Program ;

- ajoutez une référence à l'assembly System.ComponentModel.Composition ;

- ajoutez un using vers System.ComponentModel.Composition ;

- ajoutez une méthode Compose() qui crée une instance du container et compose l'hôte ;

- ajoutez une méthode Run() qui appelle Compose() ;

- instanciez la classe hôte dans la méthode Main() ;

Pour une application ASP.NET ou WPF, la classe hôte est instanciée par le runtime, rendant inutile cette dernière étape.

Voilà à quoi devrait ressembler le code correspondant aux étapes ci-dessus :

using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Reflection;
using System;

public class Program
{
public static void Main(string[] args)
{
Program p = new Program();
p.Run();
}

public void Run()
{
Compose();
}

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

- définissez un ou plusieurs exports, que l'hôte importera par la suite. Nous allons définir l'interface IMessageSender. La Composable Part EmailSender sera également définie et elle s'exportera en tant que IMessageSender ce qui sera déclaré via l'attribut [System.ComponentModel.Composition.Export] ;

public interface IMessageSender
{
void Send(string message);
}

[Export(typeof(IMessageSender))]
public class EmailSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine(message);
}
}

- ajoutez une propriété dans la classe hôte pour chaque import, décoré de l'attribut [System.ComponentModel.Composition.Import]. Par exemple, dans le code ci-dessous un import pour IMessageSender a été ajouté dans la classe Program ;

[Import]
public IMessageSender MessageSender { get; set; }

- ajoutez les Parts au CompositionContainer. Il y a plusieurs façons de faire cela : la première est d'ajouter directement une instance d'une ComposablePart existante, tandis que la seconde, qui est l'approche la plus commune, utilise les catalogues que nous verrons plus loin.

II-A-1 - Ajout d'une Part directement dans le container

Ajoutez manuellement chaque Composable Part dans la méthode Compose(), grâce à la méthode d'extension ComposeParts(). Dans l'exemple ci-dessous, une instance de EmailSender est ajoutée au CompositionContainer dans l'instance courante de la classe Program qui l'importe.

private void Compose()
{
var container = new CompositionContainer();
container.ComposeParts(this, new EmailSender());
}

II-A-2 - Ajout en utilisant un AssemblyCatalog

En utilisant le catalogue, le container prend en charge la création automatique des Parts plutôt que d'avoir à les rajouter manuellement. Pour cela, il faut créer un catalogue dans la méthode Compose(). Passez-le ensuite au constructeur du container.

Dans l'exemple ci-dessous, un AssemblyCatalog est créé avec comme paramètre du constructeur l'assemblage exécuté. Nous n'ajoutons pas directement une instance de EmailSender, elle sera découverte dans le catalogue.

private void Compose()
{
var catalog = new AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly());
var container = new CompositionContainer(catalog);
container.ComposeParts(this);
}

Après avoir suivi ces différentes étapes, le code devrait maintenant ressembler à ceci:

using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Reflection;
using System;

public class Program
{
[Import]
public IMessageSender MessageSender { get; set; }

public static void Main(string[] args)
{
Program p = new Program();
p.Run();
}

public void Run()
{
Compose();
MessageSender.Send("Message Sent");
}

private void Compose()
{
AssemblyCatalog catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
var container = new CompositionContainer(catalog);
container.ComposeParts(this);
}
}

public interface IMessageSender
{
void Send(string message);
}

[Export(typeof(IMessageSender))]
public class EmailSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine(message);
}
}

Lorsque ce code est compilé puis exécuté, l'application compose avec son import IMessageSender. Quand la méthode Send()est appelée, la console affiche donc « Message Sent ».

Pour des scénarios d'hébergement plus avancés, vous pouvez vous reporter à ce post : http://codebetter.com/blogs/glenn.block/archive/2010/01/15/hosting-mef-within-your-applications.aspx

II-B - Définition des Composable Parts et des contrats

II-B-1 - Composable Part

Une ComposablePart est une unité composée au sein de MEF. Les Parts exportent des services qui sont essentiels à d'autres Composable Parts, et importent des services d'autres Composable Parts.

Dans le système de programmation de MEF, les attributs System.ComponentModel.Composition.Import et System.ComponentModel.Composition.Export sont utilisés afin de déclarer leurs imports et exports. Une Composable Part doit contenir au moins un export. Elle doit également être ajoutée explicitement au container ou créée via l'utilisation de catalogues. Les catalogues fournis par MEF identifient une Composable Part grâce à la présence de l'attribut d'export.

II-B-2 - Contrats

Les Composable Parts ne dépendent pas directement les unes des autres, elles passent des contrats qui sont des chaînes de caractères servant d'identifiants. Tous les exports ont des contrats, et tous les imports déclarent les contrats dont ils ont besoin. Le container utilise les informations du contrat pour lier les imports et les exports. Si le contrat n'est pas défini, MEF va implicitement utiliser le nom complet du type comme contrat. Si un type est utilisé comme contrat, c'est également son nom complet qui va servir.

De préférence un type devrait être utilisé pour un contrat, et pas une chaîne de caractères. Bien que les contrats puissent être une chaîne, cela peut mener à des ambiguïtés. Par exemple « Sender » peut interférer avec d'autres implémentations de « Sender » dans une librairie différente. Pour cette raison, si vous devez spécifier un contrat avec une chaîne de caractères, il est recommandé que le nom du contrat soit composé avec l'espace de noms incluant le nom de la société, par exemple « Contoso.Exports.Sender ».

Dans le code ci-dessous, tous les contrats sont équivalents :

namespace MEFSample
{
[Export]
public class Exporter {...}

[Export(typeof(Exporter))]
public class Exporter1 {...}

[Export("MEFSample.Exporter")]
public class Exporter2 {...}
}

II-B-2-1 - Contrats d'interface / abstraits

Un pattern commun est de faire en sorte que la Composable Part utilise une interface ou un type abstrait comme contrat d'export plutôt qu'un type concret. Cela permet à l'importateur d'être totalement indépendant d'une implémentation spécifique de l'export. Par exemple, dans le code ci-dessous vous pouvez voir qu'il y a deux implémentations de Sender, les deux s'exportant en tant que IMessageSender. La classe Notifier importe une collection de IMessageSender et invoque leur méthode Send(). De nouvelles implémentations de Sender peuvent ainsi être facilement ajoutées au système.

[Export(typeof(IMessageSender))]
public class EmailSender : IMessageSender {
...
}

[Export(typeof(IMessageSender))]
public class TCPSender : IMessageSender {
...
}

public class Notifier {
[ImportMany]
public IEnumerable<IMessageSender> Senders {get; set;}
public void Notify(string message) {
foreach(IMessageSender sender in Senders)
sender.Send(message);
}
}

II-B-3 - Assemblage de contrats

Un pattern commun lorsque l'on développe une application extensible avec MEF est de déployer un assemblage contenant les contrats utilisées par l'application hôte et ses extensions. Ces contrats seront donc les interfaces ou les classe abstraites faisant le lien entre les imports et les exports. Des assemblages de contrats supplémentaires pourront contenir des interfaces permettant de voir les métadonnées utilisées par les importateurs, ainsi que des attributs personnalisés d'exports.

Vous devez préciser le type d'interface (IMessageSender) qui sera exporté, sinon c'est le type lui-même (EmailSender) qui le sera.

II-C - Déclaration des exports

Les Composable Parts déclarent leurs exports grâce à l'attribut [System.ComponentModel.Composition.ExportAttribute]. Avec MEF il est possible d'exporter la Part, mais aussi seulement les propriétés ou les méthodes de la Part.

II-C-1 - Export de Composable Part

Un export au niveau de la Composable Part est utilisé quand une Composable Part a besoin de s'exporter. Pour cela, il suffit de décorer la Composable Part avec l'attribut [System.ComponentModel.Composition.ExportAttribute], comme le montre le code ci-dessous.

[Export]
public class SomeComposablePart {
...
}

II-C-2 - Export de propriétés

Les Parts peuvent également exporter leurs propriétés. L'export de propriétés est avantageux pour diverses raisons :

- cela permet d'exporter des types scellés tels que les types de bases de la CLR, ou d'autres types provenant de tiers ;

- cela permet de découpler l'export de sa création. Par exemple, vous pouvez exporter le HttpContext existant qui a été exécuté pour vous ;

- cela permet d'avoir une famille d'exportations liées dans la même Composable Part, comme la Composable Part DefaultSendersRegistry qui exporte un ensemble par défaut de senders comme propriétés.

Par exemple, vous pouvez avoir une classe Configuration qui exporte un entier avec un contrat « Timeout » :

public class Configuration
{
[Export("Timeout")]
public int Timeout
{
get { return int.Parse(ConfigurationManager.AppSettings["Timeout"]); }
}
}
[Export]
public class UsesTimeout
{
[Import("Timeout")]
public int Timeout { get; set; }
}

II-C-3 - Export de méthodes

Un export de méthode se produit lorsqu'une Part exporte l'une de ses méthodes. Les méthodes sont exportées comme des délégués qui sont spécifiés dans le contrat d'export. L'export de méthode possède plusieurs avantages :

- cela permet un contrôle plus précis de ce qui est exporté. Par exemple un moteur de règles est susceptible d'importer un set de méthodes extensibles ;

- cela protège l'appelant de toute connaissance du type ;

- ils peuvent être générés via du code, ce qui n'est pas permis pour les autres formes d'export.

L'export de méthodes ne peut avoir plus de quatre arguments, à cause d'une limitation du framework.

Dans l'exemple ci-dessous, la classe IMessageSender exporte sa méthode Send comme un délégué Action<String>. La classe Processor importe le même délégué.

public class MessageSender
{
[Export(typeof(Action<string>))]
public void Send(string message)
{
Console.WriteLine(message);
}
}

[Export]
public class Processor
{
[Import(typeof(Action<string>))]
public Action<string> MessageSender { get; set; }

public void Send()
{
MessageSender("Processed");
}
}

Vous pouvez également exporter et importer des méthodes en utilisant un simple contrat à chaîne de caractères :

public class MessageSender
{
[Export("MessageSender")]
public void Send(string message)
{
Console.WriteLine(message);
}
}

[Export]
public class Processor
{
[Import("MessageSender")]
public Action<string> MessageSender { get; set; }

public void Send()
{
MessageSender("Processed");
}
}

Quand vous faites un export de méthodes, il faut obligatoirement spécifier un type ou une chaîne de caractères de nom de contrat, et ne pas laisser le champ vide.

II-C-4 - Héritage d'export

MEF supporte la capacité pour une classe mère/interface de définir des exports qui seront automatiquement hérités. C'est idéal pour l'intégration des frameworks souhaitant utiliser les fonctionnalités de découverte de MEF mais ne voulant pas que cela requiert de modifier du code existant. Pour mettre en place un export d'héritage, il suffit d'utiliser l'attribut System.ComponentModel.Composition.InheritedExportAttribute. Dans l'exemple ci-dessous, Logger implémente ILogger et est donc automatiquement exporté en tant qu'ILogger.

[InheritedExport]
public interface ILogger
{
void Log(string message);
}

public class Logger : ILogger
{
public void Log(string message);
}

II-C-5 - Découverte de Composable Parts privées

MEF supporte la découverte de Composable Parts publiques et privées. Vous n'avez rien à faire pour activer ce comportement. Mais il est à noter que dans les environnements de confiance partielle/moyenne (incluant Silverlight), la composition de types privés n'est pas supportée.

II-D - Déclaration des imports

Les Composable Parts déclarent leurs imports grâce à l'attribut [System.ComponentModel.Composition.ImportAttribute]. Comme pour l'export, il y a plusieurs façons de le faire, à savoir via des champs, des propriétés ou des paramètres de constructeur.

II-D-1 - Import de propriétés

Pour importer la valeur d'une propriété, il faut décorer la propriété avec l'attribut [System.ComponentModel.Composition.ImportAttribute]. Par exemple le code ci-dessous importe un IMessageSender.

class Program
{
[Import]
public IMessageSender MessageSender { get; set; }
}

II-D-2 - Paramètre de constructeur

Vous pouvez aussi importer via des paramètres de constructeur. Cela signifie qu'au lieu d'ajouter des propriétés pour chaque import, il vous suffit d'ajouter des paramètres dans le constructeur. Pour cela, suivez ces étapes :

- Ajoutez un attribut [System.ComponentModel.Composition.ImportingConstructorAttribute] au constructeur

- Ajoutez un paramètre au constructeur pour chaque import

Le code ci-dessous importe un IMessageSender dans le constructeur de la classe Program :

class Program
{
[ImportingConstructor]
public Program(IMessageSender messageSender)
{
...
}
}

II-D-2-1 - Import de paramètres

Il y a deux façons de définir les imports sur le constructeur :

- Import implicite : par défaut le container va utiliser le type du paramètre pour identifier le contrat. Par exemple dans le code défini dans le dernier paragraphe, c'est le type IMessageSender qui est utilisé.

- Import explicite : si vous souhaitez spécifier un contrat il faut ajouter un attribut [System.ComponentModel.Composition.ImportAttribute] au paramètre.

II-D-3 - Import de champs

MEF supporte également l'import direct sur les champs.

class Program
{
[Import]
private IMessageSender _messageSender;
}

L'import/export des membres privés (champs, propriétés, méthodes) est supporté en confiance totale, mais risque d'être problématique en confiance partielle/moyenne.

II-D-4 - Imports optionnels

MEF vous permet de spécifier qu'un import est optionnel. Quand vous utilisez cette capacité, le container va fournir un export s'il y en a de disponible, sinon il va définir l'import avec Default(T). Pour rendre un import optionnel, définissez la valeur de la propriété AllowDefault de l'import à true.

[Export]
public class OrderController
{
private ILogger _logger;

[ImportingConstructor]
public OrderController([Import(AllowDefault=true)] ILogger logger)
{
if(logger == null)
logger = new DefaultLogger();
_logger = logger;
}
}

La classe OrderController importe optionnellement logger en tant que paramètre du constructeur. Si logger n'est pas défini par l'import, le champ _logger sera défini avec une instance de DefaultLogger. Dans le cas contraire, c'est l'import de logger qui est utilisé.

II-D-5 - Import de collections

En plus des imports simples, vous pouvez importer des collections en utilisant l'attribut ImportMany. Cela signifie que des instances du contrat spécifié seront importées depuis le container.

Les Parts MEF supportent aussi la recomposition. Cela signifie que si de nouveaux exports sont disponibles dans le container, la collection sera automatiquement mise à jour.

Dans l'exemple ci-dessous, la classe Notifier importe une collection de IMessageSender. Cela signifie que si trois exports de IMessageSender sont disponibles dans le container, ils seront tous les trois ajoutés à la propriété Senders durant la composition.

public class Notifier
{
[ImportMany(AllowRecomposition=true)]
public IEnumerable<IMessageSender> Senders {get; set;}

public void Notify(string message)
{
foreach(IMessageSender sender in Senders)
{
sender.Send(message);
}
}
}

II-D-6 - IPartImportsSatisfiedNotification

Dans différentes situations il peut être important pour votre classe d'être notifiée quand MEF a fini avec le processus d'import de l'instance de votre classe. C'est ce cas qu'implémente l'interface [System.ComponentModel.Composition.IPartImportsSatisfiedNotification]. Elle ne possède qu'une seule méthode : OnImportsSatisfied, qui est appelée quand tous les imports pouvant être satisfait le sont.

public class Program : IPartImportsSatisfiedNotification
{
[ImportMany]
public IEnumerable<IMessageSender> Senders {get; set;}

public void OnImportsSatisfied()
{
// when this is called, all imports that could be satisfied have been satisfied.
}
}

II-E - Imports différés

Durant la composition d'une Part, un import va déclencher l'instanciation d'une ou plusieurs Parts qui expose les exports requis par la Part à l'origine de la composition. Pour certaines applications, retarder cette instanciation - et prévenir ainsi la composition récursive le long du graphe - est un facteur important à considérer quand elle requièrt la création d'un graphe d'objets long et complexe pouvant s'avérer coûteux et inutile.

C'est pourquoi MEF supporte les imports différés, ou Lazy Import. Pour utiliser cette capacité, il vous suffit d'importer le type générique System.Lazy<T> à la place de T.

Etudions le code ci-dessous :

[Export]
public class HttpServerHealthMonitor
{
[Import]
public IMessageSender Sender { get; set; }
...
}

La classe HttpServerHealthMonitor importe une propriété qui dépend de l'implémentation d'un contrat (IMessageSender). Lorsque MEF subvient à cette dépendance, il lui faut créer une instance de type IMessageSender et subvenir récursivement à ses dépendances.

Pour retarder cet import, il suffit d'utiliser le type Lazy<IMessageSender>.

[Export]
public class HttpServerHealthMonitor
{
[Import]
public Lazy<IMessageSender> Sender { get; set; }
...
}

Dans ce cas, vous optez pour retarder cette instanciation jusqu'à ce que vous en ayez réellement besoin. Pour utiliser cette instance, il vous faut utiliser la propriété [Lazy<T>.Value]. Ici ce sera donc Sender.Value, qui retournera un objet de type IMessageSender.

II-F - Exports et métadonnées

Dans la déclaration des exports, nous avons vu les bases de l'export des services et valeurs d'une Part. Mais dans certains cas il est nécessaire d'associer des informations à l'export. Généralement ces informations sont utilisées afin de préciser les capacités de Parts ayant un contrat commun. Cela permet aux imports de définir ce dont ils ont besoin spécifiquement, ou de récupérer toutes les implémentations disponibles lors de la composition puis de vérifier leur capacité à l'exécution, avant de les utiliser.

II-F-1 - Ajouter des métadonnées à un export

Considérons l'interface IMessageSender introduite plus haut. Supposons que nous en avons plusieurs implémentations et qu'elles ont des différences pouvant être relevées par la classe important ces implémentations.

Dans notre exemple, le type de transport des messages ainsi que le fait qu'il soit sécurisé sont des informations importantes que l'importateur veut connaître.

II-F-1-1 - Utiliser l'attribut ExportMetadata

Tout ce que nous avons à faire pour ajouter ces informations à l'export est d'utiliser l'attribut [System.ComponentModel.Composition.ExportMetadataAttribute] :

public interface IMessageSender
{
void Send(string message);
}

[Export(typeof(IMessageSender))]
[ExportMetadata("transport", "smtp")]
public class EmailSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine(message);
}
}

[Export(typeof(IMessageSender))]
[ExportMetadata("transport", "smtp")]
[ExportMetadata("secure", null)]
public class SecureEmailSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine(message);
}
}

[Export(typeof(IMessageSender))]
[ExportMetadata("transport", "phone_network")]
public class SMSSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine(message);
}
}

II-F-1-2 - Utiliser un attribut d'export personnalisé

Afin de pouvoir utiliser un type fort comme metadata, vous devez créer votre propre attribut et le décorer avec l'attribut [System.ComponentModel.Composition.MetadataAttribute].

Dans l'exemple suivant, la classe dérive de l'attribut ExportAttribute et signale donc un export, mais notre attribut personnalisé spécifie également des métadonnées.

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class MessageSenderAttribute : ExportAttribute
{
public MessageSenderAttribute() : base(typeof(IMessageSender)) { }
public MessageTransport Transport { get; set; }
public bool IsSecure { get; set; }
}

public enum MessageTransport
{
Undefined,
Smtp,
PhoneNetwork,
Other
}

Ci-dessus, l'attribut MetadataAttribute est appliqué à notre attribut personnalisé. La prochaine étape est d'utiliser notre attribut sur des implémentations de IMessageSender :

[MessageSender(Transport=MessageTransport.Smtp)]
public class EmailSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine(message);
}
}

[MessageSender(Transport=MessageTransport.Smtp, IsSecure=true)]
public class SecureEmailSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine(message);
}
}

[MessageSender(Transport=MessageTransport.PhoneNetwork)]
public class SMSSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine(message);
}
}

Voilà tout ce qui est requis du côté de l'export. Sous le capot, MEF va toujours remplir un dictionnaire, mais il le fait maintenant de manière invisible.

Vous pouvez également créer des attributs de métadonnées qui ne sont pas eux-mêmes des exports. Dans ce cas les métadonnées sont ajoutées aux exports du membre qui est décoré avec l'attribut personnalisé.

II-F-2 - Importer des métadonnées

Les importateurs ont accès aux métadonnées attachées aux exports.

II-F-2-1 - Utiliser des métadonnées fortement typées

Afin d'accéder à des métadonnées fortement typées, il faut créer une vue de ces métadonnées en définissant une interface correspondant aux propriétés en lecture seule de la métadonnée (noms et types). Pour l'exemple définit plus haut, nous avons donc l'interface suivante :

public interface IMessageSenderCapabilities
{
MessageTransport Transport { get; }
bool IsSecure { get; }
}

Vous pouvez alors commencer les imports en utilisant le type System.Lazy<T, TMetadata> où T est le type du contrat et TMetadata est l'interface que vous avez créée.

[Export]
public class HttpServerHealthMonitor
{
[ImportMany]
public Lazy<IMessageSender, IMessageSenderCapabilities>[] Senders { get; set; }

public void SendNotification()
{
foreach(var sender in Senders)
{
if (sender.Metadata.Transport == MessageTransport.Smtp &&
sender.Metadata.IsSecure)
{
var messageSender = sender.Value;
messageSender.Send("Server is fine");

break;
}
}
}
}

II-F-2-2 - Utiliser des métadonnées faiblement typées

Pour utiliser des métadonnées faiblement typées, vous devez utiliser le type System.Lazy<T, TMetadata> pour vos imports en lui passant un IDictionary<String, Object> pour les métadonnées. Vous pouvez alors accéder aux métadonnées via la propriété Metadata qui est un dictionnaire.

Il est préférable d'utiliser des métadonnées fortement typées, cependant certains systèmes nécessitent un accès dynamique aux métadonnées, ce que permettent les métadonnées faiblement typées.

[Export]
public class HttpServerHealthMonitor
{
[ImportMany]
public Lazy<IMessageSender, IDictionary<string,object>>[] Senders { get; set; }

public void SendNotification()
{
foreach(var sender in Senders)
{
if (sender.Metadata.ContainsKey("Transport") && sender.Metadata["Transport"] == MessageTransport.Smtp &&
sender.Metadata.ContainsKey("Issecure") && Metadata["IsSecure"] == true)
{
var messageSender = sender.Value;
messageSender.Send("Server is fine");

break;
}
}
}
}

II-F-2-3 - Le filtrage des métadonnées et l'attribut DefaultValueAttribute

Quand vous spécifiez une vue de métadonnées, cela va mettre en place un filtrage implicite sur les exports contenant les propriétés de métadonnées définies dans la vue. Vous pouvez spécifier dans la vue de métadonnées qu'une des propriétés n'est pas obligatoire en utilisant l'attribut [System.ComponentModel.DefaultValueAttribute].

Ci-dessous vous pouvez voir que l'on a défini la valeur par défaut à false sur la propriété IsSecure. Cela a pour effet que si une Part s'exporte en tant que IMessageSender mais n'a pas de métadonnées pour IsSecure, elle sera quand même prise en compte.

public interface IMessageSenderCapabilities
{
MessageTransport Transport { get; }
[DefaultValue(false)];
bool IsSecure { get; }
}

II-G - Utilisation des catalogues

Une des forces du modèle de programmation de MEF est la découverte dynamique de Parts grâce aux catalogues. Ils permettent aux applications de consommer facilement des exports qui ont été enregistrés via l'attribut Export. Voici la listes des catalogues fournis par MEF.

II-G-1 - AssemblyCatalog

La classe [System.ComponentModel.Composition.Hosting.AssemblyCatalog] permet de découvrir les exports présents dans un assemblage donné.

var catalog = new AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly());

II-G-2 - DirectoryCatalog

La classe [System.ComponentModel.Composition.Hosting.DirectoryCatalog] permet de découvrir les exports des assemblages présents dans un répertoire donné.

var catalog = new DirectoryCatalog("Extensions");

Si un chemin relatif est utilisé, il sera complété à partir du répertoire racine du domaine d'application courant.

Le catalogue de répertoire ne scanne qu'une seule fois le répertoire, il ne rafraîchit pas ses données dès qu'un changement se produit dans le répertoire. Mais il est possible d'implémenter votre propre système de détection de modification, par exemple avec un FileSystemWatcher, et d'appeler la méthode Refresh du catalogue afin qu'il scan à nouveau le répertoire et qu'il recompose les imports/exports.

var catalog = new DirectoryCatalog("Extensions");
//logique de détection de modification du répertoire
catalog.Refresh();

Ce catalogue n'est pas supporté par Silverlight.

II-G-3 - AggregateCatalog

Quand un AssemblyCatalog ou un DirectoryCatalog seuls ne suffisent pas, vous pouvez les combiner grâce à la classe [System.ComponentModel.Composition.Hosting.AggregateCatalog]. Elle va combiner plusieurs catalogues en un seul . Un usage typique est d'y ajouter l'assemblage courant ainsi qu'un DirectoryCatalog vers un dossier d'extensions. Vous pouvez passer une collection de catalogues au constructeur de AggregateCatalog ou les ajouter un à un via sa propriété Catalogs.

var catalog = new AggregateCatalog(
new AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly()),
new DirectoryCatalog("Extensions"));

II-G-4 - TypeCatalog

La classe [System.ComponentModel.Composition.Hosting.TypeCatalog] permet de découvrir des exports de types donnés.

var catalog = new TypeCatalog(typeof(type1), typeof(type2), ...);

II-G-5 - DeploymentCatalog - uniquement dans Silverlight

La version Silverlight de MEF inclut le DeploymentCatalog pour télécharger dynamiquement des XAPs distants. Ce catalogue sera décrit en détail dans la partie de MEF pour Silverlight.

II-G-6 - Utilisation d'un catalogue dans un Container

Pour utiliser un catalogue dans un container, il suffit de le passer en tant que paramètre du constructeur du container.

var container = new CompositionContainer(catalog);

II-H - Catalogues filtrés

Il peut parfois être utile de filtrer les catalogues sur des critères spécifiques. Pour cela, il est courant d'utiliser les règles de création des Parts. Le code suivant montre comment mettre cela en place.

var catalog = new AssemblyCatalog(typeof(Program).Assembly);
var parent = new CompositionContainer(catalog);

var filteredCat = new FilteredCatalog(catalog,
def => def.Metadata.ContainsKey(CompositionConstants.PartCreationPolicyMetadataName) &&
((CreationPolicy)def.Metadata[CompositionConstants.PartCreationPolicyMetadataName]) == CreationPolicy.NonShared);
var child = new CompositionContainer(filteredCat, parent);

var root = child.GetExportedValue<Root>();
child.Dispose();

Si les règles de création ne suffisent pas pour sélectionner les Parts voulues, vous pouvez utiliser l'attribut [System.ComponentModel.Composition.PartMetadataAttribute] à la place. Cet attribut vous permet d'attacher des métadonnées à une Part afin de pouvoir les utiliser par la suite pour construire une expression de filtrage.

[PartMetadata("scope", "webrequest"), Export]
public class HomeController : Controller
{
}

Le code ci-dessus va permettre la création d'un container enfant avec des Parts ayant un scope avec pour valeur webrequest.

var catalog = new AssemblyCatalog(typeof(Program).Assembly);
var parent = new CompositionContainer(catalog);

var filteredCat = new FilteredCatalog(catalog,
def => def.Metadata.ContainsKey("scope") &&
def.Metadata["scope"].ToString() == "webrequest");
var perRequest = new CompositionContainer(filteredCat, parent);

var controller = perRequest.GetExportedValue<HomeController>();
perRequest.Dispose();

La classe FilteredCatalog n'est pas fournie par MEF. Mais son implémentation n'est pas compliquée, en tout cas tant que l'on reste dans une utilisation basique.

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

public class FilteredCatalog : ComposablePartCatalog, INotifyComposablePartCatalogChanged
{
private readonly ComposablePartCatalog _inner;
private readonly INotifyComposablePartCatalogChanged _innerNotifyChange;
private readonly IQueryable<ComposablePartDefinition> _partsQuery;

public FilteredCatalog(ComposablePartCatalog inner,
Expression<Func<ComposablePartDefinition, bool>> expression)
{
_inner = inner;
_innerNotifyChange = inner as INotifyComposablePartCatalogChanged;
_partsQuery = inner.Parts.Where(expression);
}

public override IQueryable<ComposablePartDefinition> Parts
{
get
{
return _partsQuery;
}
}

public event EventHandler<ComposablePartCatalogChangeEventArgs> Changed
{
add
{
if (_innerNotifyChange != null)
_innerNotifyChange.Changed += value;
}
remove
{
if (_innerNotifyChange != null)
_innerNotifyChange.Changed -= value;
}
}

public event EventHandler<ComposablePartCatalogChangeEventArgs> Changing
{
add
{
if (_innerNotifyChange != null)
_innerNotifyChange.Changing += value;
}
remove
{
if (_innerNotifyChange != null)
_innerNotifyChange.Changing -= value;
}
}
}

II-I - Cycle de vie des Parts

Il est essentiel de comprendre le cycle de vie des Parts dans un container MEF, ainsi que ses implications. Etant donné que MEF se concentre sur les applications ouvertes, il est particulièrement important de savoir que les créateurs d'applications n'auront pas le contrôle sur l'ensemble des parties une fois que l'application hôte aura ajouté les extensions.

Le cycle de vie peut être expliqué comme étant le désir de « shareability » d'une Part (ses exports), qui se traduit par la politique de contrôle de création, d'arrêt et de suppression de cette Part.

II-I-1 - Shared, Non Shared et propriété

La « shareability » d'une Part est définie via un ensemble de politiques de création au niveau de la classe en utilisant l'attribut PartCreationPolicy. Il accepte les valeurs suivantes :

- Shared : l'auteur de la Part signifie à MEF qu'au plus une instance de la Part existe par container ;

- NonShared : l'auteur de la Part signifie à MEF que chaque requête d'export concernant la Part recevra une nouvelle instance de la Part ;

- Any ou aucune valeur : l'auteur de la Part permet à la Part d'être utilisée aussi bien en tant que « Shared » que « NonShared ».

La politique de création peut être définie sur une Part en utilisant l'attribut [System.ComponentModel.Composition.PartCreationPolicyAttribute] :

[PartCreationPolicy(CreationPolicy.NonShared)]
[Export(typeof(IMessageSender))]
public class SmtpSender : IMessageSender
{
}

Le container sera toujours le propriétaire des Parts qu'il crée. En d'autres termes, la propriété n'est jamais transférée à l'acteur à l'origine de la requête via un import ou un export.

La police de création de Parts peut aussi servir pour définir un critère d'import. Il suffit de spécifier une valeur de l'énumération CreationPolicy pour la valeur RequiredCreationPolicy :

[Export]
public class Importer
{
[Import(RequiredCreationPolicy=CreationPolicy.NonShared)]
public Dependency Dep { get; set; }
}

Cette fonctionnalité peut s'avérer utile pour des scénarios où la shareability d'une Part est importante pour la classe à l'origine de l'import. Par défaut RequiredCreationPolicy a la valeur Any, donc des Parts ayant une politique de création à Shared ou NonShared peuvent être importées.

Le tableau suivant résume le comportement d'import en fonction de la politique de création :

-

Part.Any

Part.Shared

Part.NonShared

Import.Any

Partagée

Partagée

Non partagée

Import.Shared

Partagée

Partagée

Pas de correspondance

Import.NonShared

Non partagée

Pas de correspondance

Non partagée

Quand les deux parties définissent Any, le résultat est une Part partagée.

II-I-2 - Disposer le container

L'instance d'un container est généralement celle qui va diriger le cycle de vie de ses Parts. Les instances des Parts créées par le container ont leur cycle de vie conditionné par celui du container. La seule façon de mettre fin au cycle de vie d'un container est de le disposer. Les implications de cette action sont les suivantes :

- La méthode Dispose des Parts implémentant IDisposable sera appelée

- Les références des Parts contenues dans le container seront supprimées

- Les Parts partagées seront disposées et supprimées

- Les exports différés ne fonctionneront plus après que le container ait été disposé

- L'utilisation des Parts pourra lever l'exception System.ObjectDisposedException

II-I-3 - Container et références des Parts

Nous croyons que le Garbage Collector .NET est la meilleure chose qui soit pour nettoyer proprement. Néanmoins, nous devons aussi fournir un container avec un comportement déterministe. Ainsi, le container ne gardera pas les anciennes références des Parts qu'il a créées à moins d'être dans un des cas suivants :

- la Part est Shared ;

- la Part implémente IDisposable ;

- un ou plusieurs imports sont configurés pour la recomposition.

Dans ces cas-là, une référence à la Part est gardée. Combiné au fait que vous pouvez avoir des Parts non partagées et continuer à requêter le container, la demande de mémoire peut devenir un problème. Afin de réguler ce problème, vous pouvez vous appuyer sur les stratégies exposées dans les deux paragraphes suivants.

II-I-4 - Scoped operations et récupération rapide des ressources

Certains types d'applications, comme les web apps ou les web services varient beaucoup des applications de bureau. Elles sont plus susceptibles de reposer sur des opérations en lots et à courte durée de vie. Par exemple un service windows peut regarder directement et une seule fois si un nombre prédéterminé de fichiers sont présents et lancer une opération batch afin de modifier leur format. Les opérations web peuvent être limitées à une opération par requête.

Pour ce genre de scénarios, vous devez utiliser des containers enfants (ceci est expliqué dans le paragraphe suivant) ou libérer rapidement le graphe de l'objet. Celui-ci permet au container de disposer et de supprimer les références des Parts non partagées dans le graphe de l'objet - jusqu'à ce qu'il atteigne une Part partagée.

Afin de libérer le plus rapidement possible le graphe de l'objet vous devez appeler la méthode ReleaseExport de la classe CompositionContainer :

var batchProcessorExport = container.GetExport<IBatchProcessor>();

var batchProcessor = batchProcessorExport.Value;
batchProcessor.Process();

container.ReleaseExport(batchProcessorExport);

Le schéma ci-dessous représente un graphe d'objets et montre quelles Parts seraient libérées (références supprimées, disposées) et celles qui resteraient intactes :

Comme la Part racine n'est pas partagée, aucune référence ne sera conservée par le container. Continuons en vérifiant les exports servis à la Part racine. Dep 1 est à la fois non partagée et disposable, donc la Part est disposée et sa référence sera supprimée du container. Dep 2 aura le même comportement. Cependant, l'export de Dep 2 est laissé intact car il est partagé. Ainsi d'autres Parts peuvent l'utiliser.

Vous n'avez pas besoin de faire plusieurs passages pour libérer les Parts pouvant l'être, l'implémentation traverse le graphe de façon profonde dès la première fois.

II-I-5 - Hiérarchie des containers

Une autre façon de résoudre le problème est d'utiliser la hiérarchie des containers. Vous pouvez créer des containers et les connecter à un container parent, faisant d'eux des containers enfants.

A moins de fournir un catalogue différent au container enfant il ne sera pas d'une grande aide car l'instanciation est basée sur celle du parent.

Ce que vous devez donc faire c'est filtrer le catalogue parent sur la base d'un critère qui va séparer le groupe de Parts créé par le parent de celui créé par l'enfant. Ou alors vous devez spécifier un nouveau catalogue qui va exposer un groupe de Parts qui sera créé par le container enfant. Comme le container enfant est censé avoir une durée de vie courte, ses Parts seront libérées et disposées avant celles de son parent.

Une approche commune est d'avoir les Parts partagées créées par le container parent et les Parts non partagées par le container enfant. Comme les Parts partagées peuvent dépendre des exports fournis par les Parts non partagées. Le catalogue principal devra contenir toutes les Parts. Le container enfant devra donc mettre en place un filtre sur le catalogue principal pour n'avoir que les Parts non partagées.

Pour plus d'information à ce sujet, veuillez vous reporter au paragraphe sur les catalogues filtrés.

II-I-6 - Ordre de disposition

L'ordre de disposition n'est nullement garanti. Cela signifie que vous ne devez pas tenter d'utiliser un import dans votre méthode Dispose(). Par exemple :

[Export]
public class SomeService : IDisposable
{
[Import]
public ILogger Logger { get; set; }

public void Dispose()
{
Logger.Info("Disposing"); // might throw exception!
}
}

L'utilisation de l'instance de l'import logger dans l'implémentation de la méthode Dispose peut engendrer un problème étant donné que l'implémentation du contrat ILogger peut aussi implémenter l'interface IDisposable et donc qu'elle est peut-être déjà disposée.

II-I-7 - AddPart / RemovePart

Toutes les Parts ne sont pas créées par un container. Vous pouvez également lui ajouter et lui enlever des Parts. Ce process déclenche la composition et peut démarrer la création des Parts répondant aux dépendances des Parts ajoutées récursivement. Quand une Part ajoutée est supprimée, MEF est assez intelligent pour libérer les ressources associées et disposer les Parts non partagées utilisées par la Part ajoutée.

MEF ne va jamais prendre la propriété d'une instance créée par vous, mais il peut avoir la propriété d'une Part qu'il a créé afin de satisfaire les imports de votre instance.

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

class Program
{
static void Main(string[] args)
{
var catalog = new AssemblyCatalog(typeof(Program).Assembly);
var container = new CompositionContainer(catalog);
var root = new Root();

// add external part
container.ComposeParts(root);

// ... use the composed root instance

// removes external part
batch = new CompositionBatch();
batch.RemovePart(root);
container.Compose(batch);
}
}

public class Root
{
[Import(RequiredCreationPolicy = CreationPolicy.NonShared)]
public NonSharedDependency Dep { get; set; }
}

[Export, PartCreationPolicy(CreationPolicy.NonShared)]
public class NonSharedDependency : IDisposable
{
public NonSharedDependency()
{
}

public void Dispose()
{
Console.WriteLine("Disposed");
}
}

II-J - Recomposition

Certaines applications sont faites pour changer dynamiquement durant leur exécution. Par exemple, une nouvelle extension peut être téléchargée, ou d'autres peuvent devenir disponibles pour n'importe quelle raison. MEF peut gérer ce genre de scénarios grâce à ce qu'on appelle la recomposition, qui est le changement de valeur des imports après leur composition initiale.

Un import peut informer MEF qu'il supporte la recomposition via la propriété AllowRecomposition de l'attribut [System.ComponentModel.Composition.ImportAttribute].

[Export]
public class HttpServerHealthMonitor
{
[ImportMany(AllowRecomposition=true)]
public IMessageSender[] Senders { get; set; }

Cela indique à MEF que votre classe est prête à gérer la recomposition, et si la disponibilité des implémentations de IMessenger est modifiée (une nouvelle est disponible ou une autre ne l'est plus), la collection sera modifiée afin de refléter ce changement. Une fois qu'une Part a optée pour la recomposition, elle sera notifiée à chaque fois qu'il y aura une modification des implémentations disponibles dans le catalogue, ou si les instances sont manuellement ajoutées / supprimées du container.

II-J-1 - Mises en garde

- quand une recomposition a lieu, les instances de la collection / tableau sont remplacées par de nouvelles instances, ce n'est pas une mise à jour. Dans l'exemple du dessus, si un nouveau IMessageSender apparaît, Senders sera complètement remplacé par un nouveau tableau. Cela a pour but de faciliter la Thread-safety ;

- la recomposition est valide pour quasiment tous les types d'imports supportés : champs, propriétés et collections, mais pas pour les paramètres de constructeur ;

- si votre type implémente l'interface [System.ComponentModel.Composition.IPartImportsSatisfiedNotification], prenez en compte le fait que la méthode ImportCompleted sera également appelée à chaque recomposition.

II-J-2 - La recomposition et Silverlight

Dans Silverlight, la recomposition utilise une règle spéciale à cause du partitionnement de l'application. Pour plus de détails, reportez vous au chapitre sur le DeploymentCatalog dans Silverlight.

II-K - Requêter le CompositionContainer

La classe CompositionContainer expose plusieurs surcharges permettant de récupérer les exports ainsi que les objets exportés et des collections des deux.

Vous devez connaître le comportement suivant, partagé par les surcharges de ces méthodes, à moins que ce ne soit explicitement indiqué :

- lors de la demande d'une instance seule, si aucune n'est trouvée une exception sera levée ;

- lors de la demande d'une instance seule, si plusieurs sont trouvées une exception sera levée.

II-K-1 - GetExportedValue

Le code suivant permet de récupérer une instance du contrat Root :

var container = new CompositionContainer(new AssemblyCatalog(typeof(Program).Assembly));
Root partInstance = container.GetExportedValue<Root>();

Si vous avez un export ayant un nom de contrat différent du nom de la classe, vous devez utiliser la surcharge suivante :

[Export("my_contract_name")]
public class Root
{
}

var container = new CompositionContainer(new AssemblyCatalog(typeof(Program).Assembly));
Root partInstance = container.GetExportedValue<Root>("my_contract_name");

II-K-2 - GetExport

GetExport retrouve une référence instanciée avec retard d'un export. L'utilisation de la propriété Value de l'export va forcer la création de l'instance. L'instanciation successive de la propriété Value de l'export retournera la même instance, que la Part ait un cycle de vie Shared ou Non-Shared.

Lazy<Root> export = container.GetExport<Root>();
var root = export.Value; //create the instance.

II-K-3 - GetExportedValueOrDefault

Cette méthode fonctionne exactement comme GetExportedValue mais ne lèvera pas d'exception dans le cas où aucune correspondance ne serait trouvée.

var root = container.GetExportedValueOrDefault<Root>(); // may return null

II-L - Composition Batch

Une instance de container MEF n'est pas immuable. Des changements peuvent survenir si le catalogue des imports est modifié (par exemple si celui-ci surveille un dossier) ou si votre code ajoute ou supprime des Parts durant l'exécution. Le composition batch évite de devoir invoquer la méthode Compose du container après chaque changement.

Le batch contient une liste des Parts à ajouter / supprimer. Après avoir effectué les modifications, le container va automatiquement déclencher une composition qui va mettre à jour les imports recomposés après les modifications.

Comme scénario, considérez une fenêtre de paramètres et un utilisateur qui sélectionne et désélectionne des options. Celles-ci seraient mappées à des Parts présentes ou non dans le catalogue. Afin d'appliquer le batch vous devez appeler la méthode Compose comme indiqué ci-dessous :

var batch = new CompositionBatch();
batch.AddPart(partInstance1);
batch.AddPart(partInstance2);
batch.RemovePart(part3);

container.Compose(batch);

Pour les types utilisant le modèle de programmation par attributs, il y a des méthodes d'extensions sur AttributedModelServices pour le CompositionContainer qui permettent de masquer le CompositionBatch dans les cas où il n'est pas requis :

container.ComposeParts(partInstance1, partInstance2,... ); // creates a CompositionBatch and calls AddPart on all the passed parts followed by Compose
container.ComposeExportedValue<IFoo>(instanceOfIFoo); // creates a CompositionBatch and calls AddExportedValue<T> followed by Compose.

II-M - Débogage et diagnostics

II-M-1 - Diagnostiquer les problèmes de composition

II-M-1-1 - Problèmes liés au rejet

Une des implications de la composition stable est que les Parts rejetées ne vont tout simplement pas apparaître.

Parce que les Parts sont interdépendantes, une Part rejetée peut entraîner une chaîne de rejet des Parts dépendantes.

Trouver la « Root Cause » d'une cascade de rejet comme celle-ci peut être compliqué.

Parmi les samples de la preview 9 de MEF (téléchargeables dans la partie « Liens »), le sample CompositionDiagnostics est un prototype de librairie de diagnostic qui peut être utilisé afin de traquer les problèmes de composition.

Les deux fonctions basiques qui sont implémentées permettent à l'état de rejet d'une Part d'être inspecté et de sortir le contenu du catalogue avec les informations d'analyse du statut et de la root cause.

II-M-1-2 - Dump Composition State

Pour un diagnostic complet, la composition entière doit être sortie sous forme de texte :

var cat = new AssemblyCatalog(typeof(Program).Assembly);
using (var container = new CompositionContainer(cat))
{
var ci = new CompositionInfo(cat, container);
CompositionInfoTextFormatter.Write(ci, Console.Out);
}

La sortie peut s'avérer intéressante, incluant les « primary rejection » devinées et l'analyse des problèmes courant comme la non-correspondance de l'identité du type, la non-correspondance de la politique de création et les métadonnées requises manquantes :

Il y a ici assez d'information pour diagnostiquer correctement les problèmes les plus courants.

II-M-1-3 - Trouver les root causes probables

La technique du dump ci-dessus est complète mais verbeuse, et si vous avez accès au process d'exécution dans un débugger, la technique suivante est plus pratique :

var fooInfo = ci.GetPartDefinitionInfo(typeof(Foo));

La valeur de retour de CompositionInfo.GetPartDefinitionInfo() est un objet donnant un accès rapide aux mêmes informations analytiques que le dump, mais relatives à la Part passée en paramètre (ici Foo). L'API expose :

- l'état de rejet de la Part (IsRejected) ;

- si elle est la cause principale de rejet, ou si elle est rejetée à cause de rejets en cascade (IsPrimaryRejection) ;

- quelles Parts peuvent être les root causes du rejet de la Part (PossibleRootCauses) ;

- l'état de tous les imports (ImportDefinitions) ;

- pour chaque import, quels exports répondraient aux imports (ImportDefinitions..Actuals) ;

- pour chaque import, quels autres exports avec le même nom de contrat ne correspondent pas, et la raison de cette non-correspondance (ImportDefinitions..Unsuitable ExportDefinitions).

II-M-1-4 - Débogage des proxies

Les types MEF comme ComposablePartCatalog peuvent être inspectés grâce au débuggeur :

II-M-2 - Mefx : outil d'analyse en ligne de commande

MEFX (téléchargeable via les previews de MEF sur codeplex ou dans la partie « Liens » de cet article) est un utilitaire permettant la réalisation de routines de diagnostic affichant les informations sur les Parts directement dans la console.

C:\Users\...\CompositionDiagnostics> mefx /?

/?

Print usage.

/causes

List root causes - parts with errors not related to the rejection of other parts.

/dir:C:\MyApp\Parts

Specify directories to search for parts.

/exporters:MyContract

List exporters of the given contract.

/exports

Find exported contracts.

/file:MyParts.dll

Specify assemblies to search for parts.

/importers:MyContract

List importers of the given contract.

/imports

Find imported contracts.

/parts

List all parts found in the source assemblies.

/rejected

List all rejected parts.

/type:MyNamespace.MyType

Print details of the given part type.

/verbose

Print verbose information on each part.

Le paramètre /parts liste toutes les Parts de la composition :

C:\Users\...\CompositionDiagnostics> mefx /dir:..\MefStudio /parts

Designers.CSharp.Commands

Designers.BasicComponentFactory

Designers.CSharpFormFactory

...

Les paramètres /rejected et /causes vont respectivement afficher les informations sur les Parts rejetées et sur les root causes probables.

Le paramètre /verbose permet d'afficher les informations détaillées des Parts pouvant être récupérées :

C:\Users\...\CompositionDiagnostics> mefx /dir:..\MefStudio /type:Designers.BasicComponentFactory /verbose

[Part] Designers.BasicComponentFactory from: DirectoryCatalog (Path="..\MefStudio")

[Export] Designers.BasicComponentFactory (ContractName="Contracts.HostSurfaceFactory")

[Import] Contracts.HostSurfaceFactory.propertyGrid (ContractName="Contracts.IPropertyGrid")

[Actual] ToolWindows.PropertyGridWindow (ContractName="Contracts.IPropertyGrid") from: ToolWindows.PropertyGridWindow from: DirectoryCatalog (Path="..\MefStudio")

[Import] Contracts.HostSurfaceFactory.Commands (ContractName="System.Void(Contracts.HostSurface)")

[Actual] Designers.CSharp.Commands.Cut (ContractName="System.Void(Contracts.HostSurface)") from: Designers.CSharp.Commands from: DirectoryCatalog (Path="..\MefStudio")

[Actual] Designers.CSharp.Commands.Copy (ContractName="System.Void(Contracts.HostSurface)") from: Designers.CSharp.Commands from: DirectoryCatalog (Path="..\MefStudio")

[Actual] Designers.CSharp.Commands.Paste (ContractName="System.Void(Contracts.HostSurface)") from: Designers.CSharp.Commands from: DirectoryCatalog (Path="..\MefStudio")

Il est important de savoir que cet utilitaire ne peut analyser que des assemblages MEF générés avec une version compatible de MEF. Par exemple, mefx.exe présent sur CodePlex et donc généré avec la version de MEF présente sur CodePlex n'est pas compatible avec la version de MEF présente dans le framework .NET, et vice-versa.

II-M-3 - Tracer les informations de composition

Les informations produites durant la composition peuvent être vues dans la fenêtre de sortie du débuggeur, ou en s'attachant à la source de trace « System.CompositionModel.Composition".

II-N - FAQ

II-N-1 - Comment avoir des exports utilisant un objet Type

La classe CompositionContainer ne fournit pas de surcharges non-génériques parmi les méthodes GetExport*, comme GetExportedValue par exemple :

// not supported
object value = container.GetExportedValue(type);

A la place, utilisez la surcharge de GetExports(ImportDefinition) :

IEnumerable<Export> matching = container.GetExports(
new ContractBasedImportDefinition(
AttributedModelServices.GetContractName(type),
AttributedModelServices.GetTypeIdentity(type),
Enumerable.Empty<string>(),
ImportCardinality.ExactlyOne,
false,
false,
CreationPolicy.Any));
object value = matching.Single().Value;

II-N-2 - Comment utiliser des containers imbriqués

Les containers imbriqués sont parfois utilisés afin de manager le cycle de vie et les portées partagées.

Pour plus de détails sur les catalogues filtrés, veuillez vous reporter au chapitre adéquat.

Lorsque des containers enfants sont créés, ils prennent généralement deux paramètres de constructeur - leur propre catalogue et leur parent (un CompositionContainer passé en tant que ExportProvider).

En utilisant cette configuration, toutes les références sont requêtées via le plus imbriqué des containers enfants.

Si une dépendance peut être satisfaite depuis le catalogue du container, elle le sera. Le cycle de vie de Parts résultants de cette dépendance sera lié au cycle de vie du container enfant, et toutes les dépendances de ces Parts seront satisfaites depuis le même container (récursivement, en utilisant cet algorithme).

Quand un container enfant ne peut satisfaire la dépendance via son propre catalogue, il appelle celui de son parent. Si la dépendance est satisfaite grâce à son parent, le cycle de vie de la Part résultante sera lié à celui du parent. Les dépendances de cette Part seront satisfaites par le parent, plus aucun appel ne sera fait à l'enfant.

La clé pour réussir à mettre en place toute la configuration est le catalogue fourni à chaque container.

Comme règle générale, toutes les Parts NonShared devraient pouvoir être créées à n'importe quel niveau et devraient donc apparaître dans les catalogues de tous les containers.

Dans la hiérarchie des containers, les Parts partagées sont un peu plus compliquées. Le partage est relatif au container qui a créé la Part, donc une Part partagée dans le catalogue d'un container enfant sera partagée au sein de ce container spécifique. Les autres containers qui peuvent voir la même Part dans leur catalogue créeront et partageront leurs propres instances.

Cela signifie qu'à l'intérieur d'un seul graphe d'objets, il est possible d'avoir plus d'une instance d'une Part partagée (une dans un enfant, une autre accessible via une dépendance résolue par le parent).

III - Fonctionnalités spécifiques à Silverlight

III-A - Héberger MEF dans Silverlight en utilisant le CompositionInitializer

Dans une application de bureau, vous devez configurer manuellement le CompositionContainer ainsi que les catalogues afin que l'application puisse découvrir les Parts. Ce container a souvent besoin d'être exposé (de préférence à travers un wrapper) et d'être utilisé par les Parts qui en ont besoin afin de composer dynamiquement de nouvelles Parts.

Dans Silverlight, une nouvelle API est introduite. Appelée System.ComponentModel.Composition.CompositionInitializer, elle est présente dans l'assemblage System.ComponentModel.CompositionInitialization. La classe CompositionInitializer permet aux Parts d'être composées par MEF sans avoir à la faire à la main car elle va automatiquement configurer MEF à la demande. Avec CompositionInitializer, n'importe quelle classe nouvellement instanciée peut avoir des imports, MEF les satisfera. Cela signifie que vous pouvez l'utiliser n'importe où dans votre application Silverlight. Néanmoins, une bonne pratique est de l'utiliser dans la classe App afin d'importer le MainView, ou le MainViewModel si vous utilisez le pattern MVVM comme dans l'exemple ci-dessous :

using System.ComponentModel.Composition;

public class App : Application {
public App() {
this.Startup += this.Application_Startup;
this.Exit += this.Application_Exit;
this.UnhandledException += this.Application_UnhandledException;

InitializeComponent();
}

private void Application_Startup(object sender, StartupEventArgs e)
{
CompositionInitializer.SatisfyImports(this)
var mainPage = new MainPage();
mainPage.DataContext = ViewModel;
RootVisual = mainPage;
}

[Import]
public MainViewModel ViewModel {get;set;}
}

[Export]
public class MainViewModel : INotifyPropertyChanged {
[Import]
public ILogger Loger {get;set;}
}

[Export(typeof(ILogger))]
public class Logger : ILogger {
}

App importe un MainViewModel. Puis au démarrage de l'application, elle appelle CompositionInitializer en se passant elle-même en paramètre afin de satisfaire ses imports. MainViewModel est donc découvert par MEF qui va lui injecter une instance de Logger. L'application instancie ensuite une MainView et lui affecte comme DataContext le ViewModel importé.

Que se passe-t-il réellement ?

La première fois que la méthode SatisfyImports est appelée, le CompositionInitializer crée un nouveau container partagé qui sera utilisé pour les futurs appels à SatisfyImports. Le catalogue de ce container contient toutes les Parts découvertes dans les assemblages du XAP principal de l'application. Cela signifie qu'en plus de l'assemblage exécuté, tous les assemblages référencés (et leurs références) ont leurs Parts découvertes.

La classe Application n'a pas d'attribut Export. La méthode SatisfyImports est faite pour utiliser seulement les Parts ne pouvant être découvertes via le catalogue, c'est à dire les Parts n'ayant pas d'exports. Si vous tentez de passer en paramètre de SatisfyImports une instance ayant des exports, cela lèvera une exception.

III-A-1 - Utilisation de CompositionInitializer depuis des éléments XAML

La classe CompositionInitializer peut être appelée plusieurs fois avec la configuration créée lors de son premier appel. Cela la rend idéale pour l'utiliser non seulement dans la classe Application, mais aussi dans les éléments créés en XAML. Par exemple, regardez le code ci-dessous :

<UserControl
x:Class="OrderManagement.OrderView"
xmlns:c="clr-namespace:MyApp.Controls">
<StackPanel>
<OrderHeader/>
<OrderDetails/>
</StackPanel>
<UserControl>

OrderHeader et OrderDetails sont des contrôles imbriqués dans OrderView et sont créés en XAML. Cependant les deux ont leur ViewModel respectif qu'ils importent. Le code ci-dessous représente OrderHeader. Notez qu'il ne possède pas d'attribut d'export.

public class OrderHeader : UserControl {
[Import]
public OrderHeaderViewModel ViewModel {get;set;}

public OrderHeader() {
CompositionInitializer.SatisfyImports(this);
this.DataContext = ViewModel;
}
}

Dans cet exemple, OrderHeader importe directement son ViewModel plutôt que d'être lié par un élément externe comme dans le cas du MainView étudié plus haut. L'import direct permet à un élément d'être utilisé en XAML sans avoir besoin de connaître son lien avec le contrôle conteneur. Ce type de pattern prend tout son sens pour les contrôles tiers utilisant MEF pour simplifier leur utilisation. Après, c'est une question de préférence.

III-A-2 - Mises en garde dans l'utilisation de CompositionInitializer.SatisfyImports

- Par défaut seuls les assemblages dans le XAP courant sont découverts. Vous pouvez cependant redéfinir ce comportement en utilisant la configuration de l'hôte (vu dans le chapitre suivant) ;

- toutes les Parts créées avec le CompositionInitializer sont maintenues par MEF jusqu'à la fin de l'exécution de l'application. Ce n'est donc pas l'idéal pour la composition de Parts multi-instances. Dans ce cas, on peut utiliser l'ExportFactory à la place du CompositionInitializer (vu dans les chapitres suivants) ;

- comme mentionné plus haut, les exports ne sont pas supportés.

III-B - Redéfinir la configuration de l'hôte

MEF crée une configuration par défaut de l'hôte pour le CompositionInitializer la première fois que SatisfyImports est appelée. C'est idéal pour débuter avec MEF ou pour une application simple qui va seulement découvrir les Parts de son XAP. Pour des scénarios plus complexes, comme le partitionnement d'application (vu dans les chapitres suivants), la classe System.ComponentModel.Composition.Hosting.CompositionHost (dans l'assemblage System.ComponentModel.Composition.Initialization.dll) permet de redéfinir la configuration. Pour utiliser CompositionHost, il faut appeler sa méthode d'initialisation au démarrage de l'application et lui spécifier la configuration que MEF utilisera.

CompositionHost.Initialize ne peut être appelée qu'une seule fois. Tous les appels suivants lèveront une exception. Il est aussi recommandé que seul l'hôte appelle cette méthode, plutôt qu'un composant tiers.

III-B-1 - Redéfinition grâce aux catalogues

La façon la plus simple de redéfinir la configuration est d'appeler la surcharge de Initialize qui prend en paramètre des catalogues. MEF va créer un container qui utilisera ces catalogues.

Dans le code ci-dessous, la configuration est redéfinie via un AggregateCatalog permettant d'ajouter d'autres catalogues dans le futur.

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

public class App : Application {
public App() {
this.Startup += this.Application_Startup;
this.Exit += this.Application_Exit;
this.UnhandledException += this.Application_UnhandledException;

InitializeComponent();
}

private void Application_Startup(object sender, StartupEventArgs e)
{
var aggregateCatalog = new AggregateCatalog();
var assemblyCatalog = new AssemblyCatalog(typeof(App).Assembly);
CompositionHost.Initialize(assemblyCatalog, aggregateCatalog);
CompositionInitializer.SatisfyImports(this);  //imports are satisfied
...
}

[Import]
public MainViewModel ViewModel {get;set;}

L'application appelle Initialize en lui passant un AggregateCatalog et un AssemblyCatalog pour l'assemblage courant. Satisfyimports est ensuite appelée et va donc importer ViewModel.

III-B-1-1 - Configurer la découverte des Parts dans le XAP courant

Quand vous redéfinissez la configuration vous prenez le contrôle total, MEF ne va plus assumer la configuration. Cela signifie que les Parts dans le XAP principal ne sont pas découvertes. Dans l'exemple du dessus, nous lui passons un catalogue d'assemblage avec l'assemblage courant. Si vous souhaitez que le XAP principal soit découvert, vous pouvez utiliser un DeploymentCatalog créé avec son constructeur par défaut. Cela va dire à MEF « trouve toutes les Parts dans le XAP courant ». Voilà un exemple d'utilisation de DeploymentCatalog :

private void Application_Startup(object sender, StartupEventArgs e)
{
var aggregateCatalog = new AggregateCatalog();
CompositionHost.Initialize(new DeploymentCatalog(),aggregateCatalog); //add the current XAP.
CompositionInitializer.SatisfyImports(this);  //imports are satisfied
...
}

III-B-2 - Redéfinition grâce à un container

La redéfinition grâce aux catalogues suffit dans la plupart des cas. Mais pour des scénarions plus avancés, vous pouvez vouloir redéfinir le container lui-même. Pour faire ceci, il faut appeler la surcharge d'Initialize qui accepte un container. Voici un code d'exemple :

private void Application_Startup(object sender, StartupEventArgs e)
{
var aggregateCatalog = new AggregateCatalog();
var container = new CompositionContainer(new DeploymentCatalog(), aggregateCatalog);
CompositionHost.Initialize(container);
CompositionInitializer.SatisfyImports(this);  //imports are satisfied
...
}

III-C - Instanciation dynamique et ExportFactory<T>

Un import différé fournit un importateur avec une seule instance du contrat d'export. La valeur sous-jacente est créée lors de la première utilisation, les requêtes suivantes retournant la même instance.

[Import]
Lazy<IFoo> foo;

// elsewhere in the same class

Assert.AreSame(foo.Value, foo.Value);

III-C-1 - ExportFactory<T>

Parfois une Part a besoin de créer à la volée plusieurs instances non-partagées d'un export. Par exemple, une application de management de commandes à besoin de créer à la volée de nouveaux OrderViewModel quand une nouvelle commande est créée. Dans ces situations, ExportFactory<T> est plus appropriée que Lazy<T> :

[Export]
public class OrderController {

[Import]
public ExportFactory<OrderViewModel> OrderVMFactory {get;set;}

public OrderViewModel CreateOrder() {
return OrderVMFactory.CreateExport().Value;
}
}

La classe OrderController importe une factory chargée de créer des OrderViewModels. Chaque fois que l'application va appeler la méthode CreateOrder, le controller va utiliser la factory afin de créer une nouvelle instance de OrderViewModel. Cette instance est créée par le container sous-jacent qui fournit les imports.

L'Import d'un ExportFactory<T> est très similaire à l'import d'un Lazy<T>. Les mêmes règles de détermination du nom de contrat et du type s'appliquent, et le type ExportFactory<T, TMetadata> présente une métadonnée fortement typée dans le même genre que Lazy<T, TMetadata>.

III-C-2 - ExportLifetimeContext<T>

La valeur de retour de la méthode ExportFactory<T>.CreateExport() est de type ExportLifetimeContext<T>. Ce type expose la valeur de l'export de la Part créée via la propriété Value. Il fournit également une implémentation de IDisposable, donc quand la valeur exportée n'est plus requise, la Part sous-jacente et toutes ses dépendances peuvent être nettoyées.

Plus de détails dans ce post de Nicolas Blumhardt.

III-D - Partitionnement d'application avec le DeploymentCatalog

Le modèle de programmation par défaut de Silverlight requiert que toutes les Parts MEF soient dans le XAP courant. Ce n'est pas un problème pour de simples applications composables, mais cela le devient pour des applications plus grosses :

- le XAP par défaut va gonfler continuellement ce qui va entraîner le téléchargement de l'app initiale et gâcher l'expérience utilisateur lors du démarrage ;

- ça ne permet pas une extensibilité comme celle offerte par des applications de bureau. Dans bien des scénarios il serait idéal de permettre à l'application de s'étendre sur le serveur ;

- il est difficile d'ajouter des fonctionnalités à une grosse application quand plusieurs équipes travaillent dessus.

III-D-1 - DeploymentCatalog

Le DeploymentCatalog télécharge les Parts depuis les XAPs présents sur le serveur web. Vous pouvez donc diviser votre application en autant de XAPs que vous souhaitez et utiliser un DeploymentCatalog afin de les remettre ensemble par la suite. Le DeploymentCatalog télécharge les catalogues de façon asynchrone et implémente le pattern d'événement async afin de vous permettre de suivre le téléchargement, à savoir traquer la fin du téléchargement, les échecs, etc. Il n'est pas requis de le faire, c'est néanmoins très recommandé.

Pour utiliser un DeploymentCatalog avec un CompositionInitializer, vous devez redéfinir la configuration en utilisant CompositionHost, sinon le CompositionInitializer ne verra pas les Parts téléchargées.

III-D-2 - Télécharger des Parts après le démarrage de l'application

Le scénario le plus commun et le plus simple est de réduire l'empreinte de démarrage de votre application et de démarrer immédiatement le téléchargement des autres parties en arrière-plan. Par exemple ci-dessous (le codage des modules est à titre illustratif, il n'est pas obligatoire) l'application de management des commandes démarre avec seulement les modules Home et Order. Le téléchargement des modules Admin, Reporting et Forecasting qui ne sont pas requis initialement est alors immédiatement initié. Voici un diagramme explicitant le design de l'application :

Et le code montrant comment cela fonctionne :

public class App : Application {
public App() {
this.Startup += this.Application_Startup;
this.Exit += this.Application_Exit;
this.UnhandledException += this.Application_UnhandledException;

InitializeComponent();
}

private void Application_Startup(object sender, StartupEventArgs e)
{
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(CreateCatalog("Modules/Admin"));
catalog.Catalogs.Add(CreateCatalog("Modules/Reporting"));
catalog.Catalogs.Add(CreateCatalog("Modules/Forecasting"));

CompositionHost.Initialize(new DeploymentCatalog(), catalog);
CompositionInitializer.SatisfyImports(this);

RootVisual = new MainPage();
}

private DeploymentCatalog CreateCatalog(string uri) {
var catalog = new DeploymentCatalog(uri);
catalog.DownloadCompleted += (s,e) => DownloadCompleted(s, e);
catalog.DownloadAsync();
return catalog;
}

private void DownloadCompleted(object sender, AsyncCompletedEventArgs e) {
if (e.Error != null) {
MessageBox.Show(e.Error.Message);
}
}

private Lazy<IModule, IModuleMetadata>[] _modules;

[ImportMany(AllowRecomposition=true)]
public Lazy<IModule, IModuleMetadata>[] Modules {
get{return _modules;}
set{
_modules = value;
ShowModules();
}
}

private void ShowModules() {
//logic to show the modules
}

}

De nombreuses choses ont été réalisées afin de permettre le fonctionnement de cette application. Premièrement, un AggregateCatalog est créé, auquel on ajoute trois DeploymentCatalogs pour les modules en utilisant la méthode d'aide CreateCatalog. Cette dernière fonctionne comme ceci :

- crée un nouveau DeploymentCatalog basé sur l'URI passée en paramètre ;

- s'abonne à l'événement DownloadCompleted. Ceci sert principalement à traquer les erreurs de téléchargement ;

- démarre le téléchargement asynchrone ;

- retourne le catalogue.

Par la suite, la méthode CompositionHost.Initialize est appelée avec pour paramètres un DeploymentCatalog vide (afin de récupérer les Parts du XAP courant) et l'AggregateCatalog contenant les DeploymentCatalogs qui seront téléchargés. Et enfin l'application compose. Elle n'a qu'un import pour tous les modules disponibles, ce qui au démarrage correspondra aux modules inclus dans le XAP. Une fois les modules importés, la méthode ShowModules est appelée afin de les afficher. Notez que l'application n'attend pas que tous les modules soient téléchargés, elle compose immédiatement, quelles que soient les Parts présentes. Notez également que dans l'événement DownloadCompleted il n'y a pas de besoin de mettre de code gérant l'ajout des nouvelles Parts au container.

III-D-3 - Composer dynamiquement les Parts téléchargées et utiliser la recomposition

Dans le code ci-dessus, on n'attend pas d'avoir les nouvelles Parts avant d'afficher ou de composer l'application. Cela lève la question suivante, comment les nouveaux modules sont ajoutés. Cela repose entièrement sur la recomposition. En effet, l'import des modules a sa propriété AllowRecomposition à true. Cela indique à MEF que de nouvelles Parts peuvent arriver durant l'exécution, après la composition initiale. Le DeploymentCatalog est recomposable, ce qui signifie qu'une fois que le téléchargement des XAPs est terminé et que les Parts sont découvertes, il indique automatiquement à MEF que les nouvelles Parts sont arrivées. Quand cela arrive, l'import des modules va automatiquement être remplacé par le nouveau set de modules. Il ne remplacera pas les instances de module existantes. Ainsi le DeploymentCatalog et la recomposition sont complémentaires pour le scénario de partitionnement de l'application.

La recomposition requiert que les imports existants de ces contrats soient recomposables, autrement MEF empêche la recomposition ce qui aura pour résultat de lever une exception lors de l'ajout d'un DeploymentCatalog. Par exemple, si la propriété AllowRecomposition=true est enlevée de l'attribut d'import de la propriété modules, MEF va lever une exception quand de nouveaux modules arriveront.

III-D-4 - Téléchargement des Parts à la demande après démarrage de l'application et basé sur les actions de l'utilisateur

Le paragraphe précédent étudiait le téléchargement des Parts lors du démarrage de l'application. Un scénario alternatif est une application qui va télécharger les Parts à la demande après qu'elle ait démarrée. Par exemple, pour garder le même exemple que tout à l'heure, les modules ne seront téléchargés que si l'on en a besoin.

Afin de mettre en place ce genre de configuration, un peu plus de travail lors du démarrage de l'application est nécessaire. Les APIs utilisées seront les mêmes, avec juste une addition : au lieu de passer un AggregateCatalog au moment du démarrage, il sera exposé à travers un service qui sera exporté aux autres Parts, leur autorisant ainsi à télécharger les nouveaux XAPs.

Voilà le code illustrant cet exemple à l'aide d'un DeploymentCatalogService :

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

public class App : Application {
public App() {
this.Startup += this.Application_Startup;
this.Exit += this.Application_Exit;
this.UnhandledException += this.Application_UnhandledException;

InitializeComponent();
}

private void Application_Startup(object sender, StartupEventArgs e)
{
DeploymentCatalogService.Initialize();
CompositionInitializer.SatisfyImports(this);

var mainPage = new MainPage();
mainPage.DataContext = ViewModel;
RootVisual = mainPage;
}

[Export(typeof(IDeploymentCatalogService))]
public class DeploymentCatalogService : IDeploymentCatalogService
{
private static AggregateCatalog _aggregateCatalog;

Dictionary<string, DeploymentCatalog> _catalogs;

public DeploymentCatalogService()
{
_catalogs = new Dictionary<string, DeploymentCatalog>();
}

public static void Initialize()
{
_aggregateCatalog = new AggregateCatalog();
_aggregateCatalog.Catalogs.Add(new DeploymentCatalog());
CompositionHost.Initialize(_aggregateCatalog);
}

public void AddXap(string relativeUri, Action<AsyncCompletedEventArgs> completedAction)
{
DeploymentCatalog catalog;
if (!_catalogs.TryGetValue(relativeUri, out catalog))
{
catalog = new DeploymentCatalog(relativeUri);

if (completedAction != null)
catalog.DownloadCompleted += (s, e) => completedAction(e);
else
catalog.DownloadCompleted += (s,e) => DownloadCompleted(s, e);

catalog.DownloadAsync();
_catalogs[relativeUri] = catalog;
_aggregateCatalog.Catalogs.Add(catalog);
}
}

void DownloadCompleted(object sender, AsyncCompletedEventArgs e)
{
if (e.Error != null)
{
throw new InvalidOperationException(e.Error.Message, e.Error);
}
}
}

}

public interface IDeploymentService {
void AddXap(string relativeUri, Action<AsyncCompletedEventArgs> completedAction);
}

La classe DeploymentCatalogService expose une méthode statique Initialize qui va initialiser le CompositionHost avec un AggregateCatalog et garder une référence statique vers ce dernier. Le membre statique permet à l'instance exportée du DeploymentCatalogService d'avoir accès à l'AggregateCatalog afin de pouvoir y ajouter de nouveaux catalogues. La méthode AddXap prend l'URI qui sera utilisée afin de créer le DeploymentCatalog correspondant, si celui-ci n'a pas déjà été créé. Elle s'abonne aussi à l'événement DownloadCompleted et démarre le téléchargement asynchrone. Le catalogue est alors immédiatement ajouté à l'AggregateCatalog.

III-D-5 - Gestion des erreurs durant le téléchargement

Comme le DeploymentCatalogService implémente le pattern d'événement async, aucune exception ne sera levée durant le téléchargement. Si une exception se produisait, cela lèverait l'événement DownloadCompleted et affecterait l'exception à la propriété Error de la classe d'arguments d'événements. L'exemple ci-dessus le montre. Si une erreur se produit, il est préférable de supprimer l'instance de DeploymentCatalog afin d'en créer une nouvelle. Vous ne pouvez pas la remettre à zéro. Cela ne posera pas de problème de laisser l'instance dans l'AggregateCatalog, cependant comme le catalogue n'a pas été téléchargé, il va simplement retourner une collection de Parts vide.

III-D-6 - Mise à jour de la progression

Des notifications sont continuellement envoyées par le DeploymentCatalog durant le téléchargement, permettant ainsi d'indiquer la progression via une ProgressBar par exemple. Pour recevoir ces notifications, il suffit de s'abonner à l'événement DownloadProgressChanged.

L'exemple ci-dessous montre un exemple de prise en charge de ces notifications :

public class DownloadManagerViewModel, INotifyPropertyChanged {

public event PropertyChangedEventHandler PropertyChanged;

public long BytesReceived {get;private set;}

public int Percentage {get; private set;}

public void DownloadProcessChanged(object sender, DownloadProgressChangedEventArgs e) {
BytesReceived = e.BytesReceived;
Percentage = e.ProgressPercentage;
RaisePropertyChanged("BytesReceived");
RaisePropertyChanged("Percentage");
}

public void RaisePropertyChanged(string property) {
var handler = PropertyChanged;
if(handler!=null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}

La classe DownloadManagerViewModel expose une méthode DownloadProgressChanged qui peut être appelée par l'application quand l'événement survient. Dans cette méthode, les propriétés BytesReceived et Percentage sont définies grâce à l'eventArgs. La méthode RaisePropertyChanged est ensuite appelée afin de notifier l'interface utilisateur des changements survenus.

III-D-7 - Annulation du téléchargement

Dans plusieurs scénarios il peut s'avérer utile de permettre à l'utilisateur d'annuler le téléchargement. Par exemple si l'application peut être utilisée en hors-ligne. Pour ces cas-là, le DeploymentCatalog fournit la méthode CancelAsync. Une fois cette méthode appelée, vous ne pouvez plus utiliser le catalogue. Pour reprendre le téléchargement vous devrez créer un nouveau catalogue.

III-D-8 - Utilisation du DeploymentCatalog quand l'application est hors-ligne ou Out of Browser

La classe DeploymentCatalog utilise la classe Silverlight WebClient. Quand une application est hors-ligne, cette classe va automatiquement switcher afin d'utiliser le cache du navigateur. Cela signifie que les XAPs déjà téléchargés sont encore accessibles, du moins aussi longtemps qu'ils sont dans le cache. Quand le cache est effacé, les XAPs doivent être re-téléchargés.

III-D-9 - Mises en garde contre l'utilisation du DeploymentCatalog

- L'hôte doit être configuré pour utiliser un DeploymentCatalog, comme vu plus haut ;

- les assemblages Silverlight mis en cache ne sont pas supportés, même le XAP principal (cela signifie que MEF ne va pas les découvrir ou initier leur télécharger). Il y a deux moyens de contourner ceci :

- vous pouvez faire en sorte que l'application référence des assemblages partagés. Cela permettra aux XAPs de référencer ces assemblages avec la propriété CopyLocal à false dans les références ;

- vous pouvez avoir un XAP partagé qui va contenir les assemblages partagés. Tant que le XAP est téléchargé avant que les Parts qui en ont besoin le soient, les références de ce XAP fonctionneront (CopyLocal=false). Cela vous permettra d'introduire dynamiquement de nouveaux contrats qui ne sont pas référencés statiquement.

- la localisation n'est pas supportée. Seuls les assemblages définis dans le manifeste seront utilisés ;

- il est impossible d'accéder aux ressources/fichiers en vrac en dehors de l'assemblage au moyen de ressources embarquées ;

- les catalogues téléchargés ne sont pas copiés dans le système de fichiers quand l'application fonctionne en mode Out of Browser ;

Pour plus de détails sur le DeploymentCatalog, lisez ce post de Glenn Block : Building Hello MEF - Part IV - DeploymentCatalog

IV - Codes sources et exemples

Code source du guide de programmation : FTP | HTTP

Samples de MEF Preview 9 : FTP | HTTP

MEFX  : FTP | HTTP

V - Liens

- MEF sur CodePlex

- MEF Programming Guide

- MEF Contrib

- MEF Visualization Tool

- Hosting the .NET Composition Primitives par Nicholas Blumhardt

- Dynamic Part Instantiation in MEF par Nicholas Blumhardt

- La série de posts HelloMEF de Glenn Block

- Hosting MEF within your applications par Glenn Block

- Export Providers and Custom factories with MEF par Glenn Block

- How do I expose configuration information through MEF par Glenn Block

- Première application MEF par Julien Corioland

- Live meeting de Julien Corioland, Simon Ferquel et Thomas Lebrun (Slides et sources de Julien Corioland, sources de Simon Ferquel)

VI - Remerciements

Je remercie Frédéric Ringenbach, eusebe19 et jacques_jean pour leur relecture et remarques.

VII - Contact

Vous pouvez me contacter par message privé ou via mon site laedit.net pour toute remarque ou suggestion.