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).