IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Guide de programmation MEF


précédentsommairesuivant

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 :

 
Sélectionnez
  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] ;
 
Sélectionnez
  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 ;
 
Sélectionnez
  [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.

 
Sélectionnez
  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.

 
Sélectionnez
  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:

 
Sélectionnez
  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 :

 
Sélectionnez
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.

 
Sélectionnez
  [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.

 
Sélectionnez
[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 » :

 
Sélectionnez
  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é.

 
Sélectionnez
  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 :

 
Sélectionnez
  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.

 
Sélectionnez
[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.

 
Sélectionnez
  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 :

 
Sélectionnez
  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.

 
Sélectionnez
  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.

 
Sélectionnez
[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.

 
Sélectionnez
 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.

 
Sélectionnez
 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 :

 
Sélectionnez
[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>.

 
Sélectionnez
[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] :

 
Sélectionnez
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.

 
Sélectionnez
[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 :

 
Sélectionnez
[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 :

 
Sélectionnez
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.

 
Sélectionnez
[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.

 
Sélectionnez
[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.

 
Sélectionnez
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é.

 
Sélectionnez
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é.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
[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.

 
Sélectionnez
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.

 
Sélectionnez
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] :

 
Sélectionnez
[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 :

 
Sélectionnez
[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 :

 
Sélectionnez
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 :

image

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.

image

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 :

 
Sélectionnez
[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.

 
Sélectionnez
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].

 
Sélectionnez
[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 :

 
Sélectionnez
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 :

 
Sélectionnez
[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.

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

image

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 :

 
Sélectionnez
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 :

image

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 :

 
Sélectionnez
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 :

image

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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

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

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

 
Sélectionnez
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).


précédentsommairesuivant

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