Partager via


Aperçu du modèle de programmation à attributs (MEF)

Dans le Framework d’extensibilité managé (MEF), un modèle de programmation est une méthode particulière de définition de l’ensemble d’objets conceptuels sur lesquels MEF fonctionne. Ces objets conceptuels incluent des parties, des importations et des exportations. MEF utilise ces objets, mais ne spécifie pas comment ils doivent être représentés. Par conséquent, un large éventail de modèles de programmation sont possibles, y compris les modèles de programmation personnalisés.

Le modèle de programmation par défaut utilisé dans MEF est le modèle de programmation attribué. Dans les parties de modèle de programmation attribuées, les importations, les exportations et d’autres objets sont définis avec des attributs qui décorent des classes .NET Framework ordinaires. Cette rubrique explique comment utiliser les attributs fournis par le modèle de programmation attribué pour créer une application MEF.

Informations de base sur l’importation et l’exportation

Une exportation est une valeur qu’une partie fournit à d’autres parties du conteneur, et une importation est une exigence qu’une partie exprime au conteneur, à remplir à partir des exportations disponibles. Dans le modèle de programmation attribué, les import et export sont déclarés en décorant des classes ou des membres avec les attributs Import et Export. L’attribut Export peut décorer une classe, un champ, une propriété ou une méthode, tandis que l’attribut Import peut décorer un champ, une propriété ou un paramètre de constructeur.

Pour qu’une importation soit mise en correspondance avec une exportation, l’importation et l’exportation doivent avoir le même contrat. Le contrat se compose d’une chaîne, appelée nom du contrat, et du type de l’objet exporté ou importé, appelé type de contrat. Ce n'est que si le nom et le type de contrat correspondent tous deux qu'une exportation est considérée comme remplissant une importation particulière.

Les deux paramètres de contrat peuvent être implicites ou explicites. Le code suivant montre une classe qui déclare une importation de base.

Public Class MyClass1
    <Import()>
    Public Property MyAddin As IMyAddin
End Class
public class MyClass
{
    [Import]
    public IMyAddin MyAddin { get; set; }
}

Dans cette importation, l’attribut Import n’a ni un type de contrat ni un paramètre de nom de contrat attaché. Par conséquent, ces deux paramètres sont déduits de la propriété décorée. Dans ce cas, le type de contrat sera IMyAddin, et le nom du contrat sera une chaîne unique créée à partir du type de contrat. (En d’autres termes, le nom du contrat correspond uniquement aux exportations dont les noms sont également déduits à partir du type IMyAddin.)

L’exemple suivant montre une exportation qui correspond à l’importation précédente.

<Export(GetType(IMyAddin))>
Public Class MyLogger
    Implements IMyAddin

End Class
[Export(typeof(IMyAddin))]
public class MyLogger : IMyAddin { }

Dans cette exportation, le type de contrat est IMyAddin dû au fait qu’il est spécifié en tant que paramètre de l’attribut Export . Le type exporté doit être identique au type de contrat, dériver du type de contrat ou implémenter le type de contrat s’il s’agit d’une interface. Dans cette exportation, le type MyLogger réel implémente l’interface IMyAddin. Le nom du contrat est déduit du type de contrat, ce qui signifie que cette exportation correspond à l’importation précédente.

Remarque

Les exportations et importations doivent habituellement être déclarées sur des classes publiques ou des membres publics. D'autres déclarations sont prises en charge, mais l'exportation ou l'importation d'un membre privé, protégé ou interne interrompt le modèle d'isolation pour le composant et n'est donc pas recommandée.

Le type de contrat doit correspondre exactement pour que l'exportation et l'importation soient considérées concordantes. Considérez l’exportation suivante.

<Export()> 'WILL NOT match the previous import!
Public Class MyLogger
    Implements IMyAddin

End Class
[Export] //WILL NOT match the previous import!
public class MyLogger : IMyAddin { }

Dans cette exportation, le type de contrat est MyLogger au lieu de IMyAddin. Même si MyLogger implémente IMyAddin, et par conséquent peut être casté en un objet IMyAddin, cet export ne correspond pas à l'import précédent, car les types de contrat ne sont pas identiques.

En général, il n’est pas nécessaire de spécifier le nom du contrat, et la plupart des contrats doivent être définis en termes de type de contrat et de métadonnées. Toutefois, dans certaines circonstances, il est important de spécifier directement le nom du contrat. Le cas le plus courant est lorsqu’une classe exporte plusieurs valeurs qui partagent un type commun, comme les primitives. Le nom du contrat peut être spécifié comme premier paramètre de l'attribut Import ou Export. Le code suivant montre une importation et une exportation avec un nom de contrat spécifié de MajorRevision.

Public Class MyExportClass

    'This one will match
    <Export("MajorRevision")>
    Public ReadOnly Property MajorRevision As Integer
        Get
            Return 4
        End Get
    End Property

    <Export("MinorRevision")>
    Public ReadOnly Property MinorRevision As Integer
        Get
            Return 16
        End Get
    End Property
End Class
public class MyClass
{
    [Import("MajorRevision")]
    public int MajorRevision { get; set; }
}

public class MyExportClass
{
    [Export("MajorRevision")] //This one will match.
    public int MajorRevision = 4;

    [Export("MinorRevision")]
    public int MinorRevision = 16;
}

Si le type de contrat n’est pas spécifié, il est toujours déduit du type de l’importation ou de l’exportation. Toutefois, même si le nom du contrat est spécifié explicitement, le type de contrat doit également correspondre exactement pour que l’importation et l’exportation soient considérées comme une correspondance. Par exemple, si le MajorRevision champ était une chaîne, les types de contrat déduits ne correspondent pas et l’exportation ne correspondrait pas à l’importation, malgré le même nom de contrat.

Importation et exportation d’une méthode

L’attribut Export peut également décorer une méthode, de la même façon qu’une classe, une propriété ou une fonction. Les exportations de méthode doivent spécifier un type de contrat ou un nom de contrat, car le type ne peut pas être déduit. Le type spécifié peut être un délégué personnalisé ou un type générique, tel que Func. La classe suivante exporte une méthode nommée DoSomething.

Public Class MyAddin

    'Explicitly specifying a generic type
    <Export(GetType(Func(Of Integer, String)))>
    Public Function DoSomething(ByVal TheParam As Integer) As String
        Return Nothing 'Function body goes here
    End Function

End Class
public class MyAddin
{
    //Explicitly specifying a generic type.
    [Export(typeof(Func<int, string>))]
    public string DoSomething(int TheParam);
}

Dans cette classe, la DoSomething méthode prend un paramètre unique int et retourne un string. Pour correspondre à cette exportation, la partie d’importation doit déclarer un membre approprié. La classe suivante importe la DoSomething méthode.

Public Class MyClass1

    'Contract name must match!
    <Import()>
    Public Property MajorRevision As Func(Of Integer, String)
End Class
public class MyClass
{
    [Import] //Contract name must match!
    public Func<int, string> DoSomething { get; set; }
}

Pour plus d’informations sur l’utilisation de l’objet Func<T, T> , consultez Func<T,TResult>.

Types d’importations

MEF prend en charge plusieurs types d'importations, y compris des importations dynamiques, différées, requises et facultatives.

Importations dynamiques

Dans certains cas, la classe d’importation peut souhaiter correspondre aux exportations de n’importe quel type qui a un nom de contrat particulier. Dans ce scénario, la classe peut déclarer une importation dynamique. L’importation suivante correspond à n’importe quelle exportation portant le nom de contrat « TheString ».

Public Class MyClass1

    <Import("TheString")>
    Public Property MyAddin

End Class
public class MyClass
{
    [Import("TheString")]
    public dynamic MyAddin { get; set; }
}

Lorsque le type de contrat est déduit du dynamic mot clé, il correspond à n’importe quel type de contrat. Dans ce cas, une importation doit toujours spécifier un nom de contrat correspondant. (Si aucun nom de contrat n’est spécifié, l’importation est considérée comme correspondant à aucune exportation.) Les deux exportations suivantes correspondent à l’importation précédente.

<Export("TheString", GetType(IMyAddin))>
Public Class MyLogger
    Implements IMyAddin

End Class

<Export("TheString")>
Public Class MyToolbar

End Class
[Export("TheString", typeof(IMyAddin))]
public class MyLogger : IMyAddin { }

[Export("TheString")]
public class MyToolbar { }

Évidemment, la classe d’importation doit être prête à traiter un objet de type arbitraire.

Importations différées

Dans certains cas, la classe d’importation peut nécessiter une référence indirecte à l’objet importé, afin que l’objet ne soit pas instancié immédiatement. Dans ce scénario, la classe peut déclarer une importation différée à l’aide d’un type de contrat de Lazy<T>. La propriété d'importation suivante déclare une importation différée.

Public Class MyClass1

    <Import()>
    Public Property MyAddin As Lazy(Of IMyAddin)

End Class
public class MyClass
{
    [Import]
    public Lazy<IMyAddin> MyAddin { get; set; }
}

Du point de vue du moteur de composition, un type Lazy<T> de contrat est considéré comme identique au type de contrat de T. Par conséquent, l’importation précédente correspondrait à l’exportation suivante.

<Export(GetType(IMyAddin))>
Public Class MyLogger
    Implements IMyAddin

End Class
[Export(typeof(IMyAddin))]
public class MyLogger : IMyAddin { }

Le nom du contrat et le type de contrat peuvent être spécifiés dans l’attribut Import d’une importation différée, comme décrit précédemment dans la section « Importations et exportations de base ».

Importations requises

Les parties MEF exportées sont généralement créées par le moteur de composition, en réponse à une demande directe ou à la nécessité de remplir une importation correspondante. Par défaut, lors de la création d’un composant, le moteur de composition utilise le constructeur sans paramètre. Pour que le moteur utilise un constructeur différent, vous pouvez le marquer avec l’attribut ImportingConstructor .

Chaque partie ne peut avoir qu'un seul constructeur utilisé par le moteur de composition. Fournir aucun constructeur sans paramètre et aucun ImportingConstructor attribut, ni fournir plusieurs ImportingConstructor attributs, génère une erreur.

Pour remplir les paramètres d’un constructeur marqué avec l’attribut ImportingConstructor , tous ces paramètres sont automatiquement déclarés en tant qu’importations. Il s’agit d’un moyen pratique de déclarer des imports utilisés lors de l’initialisation d’une partie. La classe suivante utilise ImportingConstructor pour déclarer une importation.

Public Class MyClass1

    Private _theAddin As IMyAddin

    'Parameterless constructor will NOT be used
    'because the ImportingConstructor
    'attribute is present.
    Public Sub New()

    End Sub

    'This constructor will be used.
    'An import with contract type IMyAddin
    'is declared automatically.
    <ImportingConstructor()>
    Public Sub New(ByVal MyAddin As IMyAddin)
        _theAddin = MyAddin
    End Sub

End Class
public class MyClass
{
    private IMyAddin _theAddin;

    //Parameterless constructor will NOT be
    //used because the ImportingConstructor
    //attribute is present.
    public MyClass() { }

    //This constructor will be used.
    //An import with contract type IMyAddin is
    //declared automatically.
    [ImportingConstructor]
    public MyClass(IMyAddin MyAddin)
    {
        _theAddin = MyAddin;
    }
}

Par défaut, l’attribut ImportingConstructor utilise des types de contrat déduits et des noms de contrats pour toutes les importations de paramètres. Il est possible de supplanter cela en décorant les paramètres par des attributs Import, qui peuvent ensuite définir explicitement le type de contrat et le nom du contrat. Le code suivant illustre un constructeur qui utilise cette syntaxe pour importer une classe dérivée au lieu d’une classe parente.

<ImportingConstructor()>
Public Sub New(<Import(GetType(IMySubAddin))> ByVal MyAddin As IMyAddin)

End Sub
[ImportingConstructor]
public MyClass([Import(typeof(IMySubAddin))]IMyAddin MyAddin)
{
    _theAddin = MyAddin;
}

En particulier, vous devez être prudent avec les paramètres de collection. Par exemple, si vous spécifiez ImportingConstructor sur un constructeur avec un paramètre de type IEnumerable<int>, l’importation correspond à une seule exportation de type IEnumerable<int>, au lieu d’un ensemble d’exportations de type int. Pour correspondre à un ensemble d’exportations de type int, vous devez décorer le paramètre avec l’attribut ImportMany .

Les paramètres déclarés en tant qu’importations par l’attribut ImportingConstructor sont également marqués comme des importations préalables. MEF permet normalement aux exportations et aux importations de former un cycle. Par exemple, un cycle est l’endroit où l’objet A importe l’objet B, qui importe à son tour l’objet A. Dans des circonstances ordinaires, un cycle n’est pas un problème et le conteneur de composition construit normalement les deux objets.

Lorsqu’une valeur importée est requise par le constructeur d’une partie, cet objet ne peut pas participer à un cycle. Si l’objet A nécessite que l’objet B soit construit avant de pouvoir être construit lui-même et que l’objet B importe l’objet A, le cycle ne pourra pas résoudre et une erreur de composition se produira. Les importations déclarées sur les paramètres de constructeur sont donc des importations requises, qui doivent toutes être remplies avant l’une des exportations de l’objet qui les requiert peuvent être utilisées.

Importations facultatives

L’attribut Import spécifie une exigence pour que la partie fonctionne. Si une importation ne peut pas être réalisée, la composition de ce composant échouera et le composant ne sera pas disponible.

Vous pouvez spécifier qu’une importation est facultative à l’aide de la AllowDefault propriété. Dans ce cas, la composition réussit même si l’importation ne correspond à aucune exportation disponible, et la propriété d’importation est définie sur la valeur par défaut pour son type de propriété (null pour les propriétés d’objet, false pour les valeurs booléennes ou zéro pour les propriétés numériques.) La classe suivante utilise une importation facultative.

Public Class MyClass1

    <Import(AllowDefault:=True)>
    Public Property thePlugin As Plugin

    'If no matching export is available,
    'thePlugin will be set to null.
End Class
public class MyClass
{
    [Import(AllowDefault = true)]
    public Plugin thePlugin { get; set; }

    //If no matching export is available,
    //thePlugin will be set to null.
}

Importation de plusieurs objets

L’attribut Import ne sera composé que lorsqu’il correspond à une seule exportation. D’autres cas produisent une erreur de composition. Pour importer plusieurs exportations qui correspondent au même contrat, utilisez l’attribut ImportMany . Les importations marquées avec cet attribut sont toujours facultatives. Par exemple, la composition ne échoue pas si aucune exportation correspondante n’est présente. La classe suivante importe n’importe quel nombre d’exportations de type IMyAddin.

Public Class MyClass1

    <ImportMany()>
    Public Property MyAddin As IEnumerable(Of IMyAddin)

End Class
public class MyClass
{
    [ImportMany]
    public IEnumerable<IMyAddin> MyAddin { get; set; }
}

Le tableau importé est accessible à l’aide de la syntaxe et des méthodes ordinaires IEnumerable<T> . Il est également possible d’utiliser un tableau ordinaire (IMyAddin[]) à la place.

Ce modèle peut être très important lorsque vous l’utilisez en combinaison avec la Lazy<T> syntaxe. Par exemple, en utilisant ImportMany, IEnumerable<T>et , vous Lazy<T>pouvez importer des références indirectes vers n’importe quel nombre d’objets et instancier uniquement ceux qui deviennent nécessaires. La classe suivante montre ce modèle.

Public Class MyClass1

    <ImportMany()>
    Public Property MyAddin As IEnumerable(Of Lazy(Of IMyAddin))

End Class
public class MyClass
{
    [ImportMany]
    public IEnumerable<Lazy<IMyAddin>> MyAddin { get; set; }
}

Éviter la découverte

Dans certains cas, vous pouvez empêcher qu'une partie soit découverte comme faisant partie d’un catalogue. Par exemple, la partie peut être une classe de base destinée à être héritée, mais pas utilisée. Il existe deux façons d’y parvenir. Tout d'abord, vous pouvez utiliser le mot-clé abstract sur la classe de composant. Les classes abstraites ne fournissent jamais d’exportations, bien qu’elles puissent fournir des exportations héritées vers des classes qui dérivent de celles-ci.

Si la classe ne peut pas être abstraite, vous pouvez la décorer avec l’attribut PartNotDiscoverable . Une partie décorée avec cet attribut ne sera incluse dans aucun catalogue. L’exemple suivant illustre ces modèles. DataOne sera découvert par le catalogue. Dans la mesure où DataTwo elle est abstraite, elle ne sera pas découverte. Étant donné que DataThree l’attribut PartNotDiscoverable est utilisé, il ne sera pas découvert.

<Export()>
Public Class DataOne
    'This part will be discovered
    'as normal by the catalog.
End Class

<Export()>
Public MustInherit Class DataTwo
    'This part will not be discovered
    'by the catalog.
End Class

<PartNotDiscoverable()>
<Export()>
Public Class DataThree
    'This part will also not be discovered
    'by the catalog.
End Class
[Export]
public class DataOne
{
    //This part will be discovered
    //as normal by the catalog.
}

[Export]
public abstract class DataTwo
{
    //This part will not be discovered
    //by the catalog.
}

[PartNotDiscoverable]
[Export]
public class DataThree
{
    //This part will also not be discovered
    //by the catalog.
}

Métadonnées et vues de métadonnées

Les exportations peuvent fournir des informations supplémentaires sur elles-mêmes appelées métadonnées. Les métadonnées peuvent être utilisées pour transmettre les propriétés de l’objet exporté à la partie d’importation. Le composant d’importation peut utiliser ces données pour décider quelles exportations utiliser ou collecter des informations sur une exportation sans avoir à la construire. Pour cette raison, une importation doit être différée pour utiliser des métadonnées.

Pour utiliser des métadonnées, vous déclarez généralement une interface appelée vue de métadonnées, qui déclare les métadonnées qui seront disponibles. L’interface d’affichage des métadonnées doit avoir uniquement des propriétés, et ces propriétés doivent get avoir des accesseurs. L’interface suivante est un exemple de vue de métadonnées.

Public Interface IPluginMetadata

    ReadOnly Property Name As String

    <DefaultValue(1)>
    ReadOnly Property Version As Integer

End Interface
public interface IPluginMetadata
{
    string Name { get; }

    [DefaultValue(1)]
    int Version { get; }
}

Il est également possible d’utiliser une collection générique, IDictionary<string, object>comme vue de métadonnées, mais cela perd les avantages de la vérification de type et doit être évité.

En règle générale, toutes les propriétés nommées dans la vue de métadonnées sont requises, et toutes les exportations qui ne les fournissent pas ne seront pas considérées comme une correspondance. L’attribut DefaultValue spécifie qu’une propriété est facultative. Si la propriété n’est pas incluse, elle reçoit la valeur par défaut spécifiée en tant que paramètre de DefaultValue. Voici deux classes différentes décorées avec des métadonnées. Ces deux classes correspondent à la vue de métadonnées précédente.

<Export(GetType(IPlugin))>
<ExportMetadata("Name", "Logger")>
<ExportMetadata("Version", 4)>
Public Class MyLogger
    Implements IPlugin

End Class

'Version is not required because of the DefaultValue
<Export(GetType(IPlugin))>
<ExportMetadata("Name", "Disk Writer")>
Public Class DWriter
    Implements IPlugin

End Class
[Export(typeof(IPlugin)),
    ExportMetadata("Name", "Logger"),
    ExportMetadata("Version", 4)]
public class Logger : IPlugin
{
}

[Export(typeof(IPlugin)),
    ExportMetadata("Name", "Disk Writer")]
    //Version is not required because of the DefaultValue
public class DWriter : IPlugin
{
}

Les métadonnées sont exprimées après l’attribut Export à l’aide de l’attribut ExportMetadata . Chaque élément de métadonnées est composé d’une paire nom/valeur. La partie nom des métadonnées doit correspondre au nom de la propriété appropriée dans la vue de métadonnées, et la valeur sera affectée à cette propriété.

Il s’agit de l’importateur qui spécifie l’affichage des métadonnées, le cas échéant, qui sera utilisé. Une importation contenant des métadonnées est déclarée comme une importation paresseuse, avec l’interface de métadonnées comme deuxième paramètre de type de Lazy<T,T>. La classe suivante importe la partie précédente avec des métadonnées.

Public Class Addin

    <Import()>
    Public Property plugin As Lazy(Of IPlugin, IPluginMetadata)
End Class
public class Addin
{
    [Import]
    public Lazy<IPlugin, IPluginMetadata> plugin;
}

Dans de nombreux cas, vous souhaiterez combiner des métadonnées avec l’attribut ImportMany , afin d’analyser les importations disponibles et de choisir et instancier une seule collection pour qu’elle corresponde à une certaine condition. La classe suivante n'instancie que des objets IPlugin qui ont la valeur Name « Logger ».

Public Class User

    <ImportMany()>
    Public Property plugins As IEnumerable(Of Lazy(Of IPlugin, IPluginMetadata))

    Public Function InstantiateLogger() As IPlugin

        Dim logger As IPlugin
        logger = Nothing

        For Each Plugin As Lazy(Of IPlugin, IPluginMetadata) In plugins
            If Plugin.Metadata.Name = "Logger" Then
                logger = Plugin.Value
            End If
        Next
        Return logger
    End Function

End Class
public class User
{
    [ImportMany]
    public IEnumerable<Lazy<IPlugin, IPluginMetadata>> plugins;

    public IPlugin InstantiateLogger()
    {
        IPlugin logger = null;

        foreach (Lazy<IPlugin, IPluginMetadata> plugin in plugins)
        {
            if (plugin.Metadata.Name == "Logger")
                logger = plugin.Value;
        }
        return logger;
    }
}

Importer et exporter l’héritage

Si une classe hérite d’une partie, cette classe peut également devenir une partie. Les importations sont toujours héritées par des sous-classes. Par conséquent, une sous-classe d'un élément sera toujours un élément, avec les mêmes imports que sa classe parente.

Les exportations déclarées à l’aide de l’attribut Export ne sont pas héritées par des sous-classes. Toutefois, une partie peut s’exporter elle-même à l’aide de l’attribut InheritedExport . Les sous-classes du composant héritent et fournissent la même exportation, y compris le nom du contrat et le type de contrat. Contrairement à un Export attribut, InheritedExport ne peut être appliqué qu’au niveau de la classe, et non au niveau membre. Par conséquent, les exportations au niveau membre ne peuvent jamais être héritées.

Les quatre classes suivantes illustrent les principes de l’héritage d’importation et d’exportation. NumTwo hérite de NumOne, donc NumTwo importera IMyData. Les exportations ordinaires ne sont pas héritées et NumTwo n’exportent donc rien. NumFour hérite de NumThree. Comme NumThree utilise InheritedExport, NumFour a un export avec le type de contrat NumThree. Les exportations au niveau membre ne sont jamais héritées. IMyData Elles ne sont donc pas exportées.

<Export()>
Public Class NumOne
    <Import()>
    Public Property MyData As IMyData
End Class

Public Class NumTwo
    Inherits NumOne

    'Imports are always inherited, so NumTwo will
    'Import IMyData

    'Ordinary exports are not inherited, so
    'NumTwo will NOT export anything.  As a result it
    'will not be discovered by the catalog!

End Class

<InheritedExport()>
Public Class NumThree

    <Export()>
    Public Property MyData As IMyData

    'This part provides two exports, one of
    'contract type NumThree, and one of
    'contract type IMyData.

End Class

Public Class NumFour
    Inherits NumThree

    'Because NumThree used InheritedExport,
    'this part has one export with contract
    'type NumThree.

    'Member-level exports are never inherited,
    'so IMyData is not exported.

End Class
[Export]
public class NumOne
{
    [Import]
    public IMyData MyData { get; set; }
}

public class NumTwo : NumOne
{
    //Imports are always inherited, so NumTwo will
    //import IMyData.

    //Ordinary exports are not inherited, so
    //NumTwo will NOT export anything. As a result it
    //will not be discovered by the catalog!
}

[InheritedExport]
public class NumThree
{
    [Export]
    Public IMyData MyData { get; set; }

    //This part provides two exports, one of
    //contract type NumThree, and one of
    //contract type IMyData.
}

public class NumFour : NumThree
{
    //Because NumThree used InheritedExport,
    //this part has one export with contract
    //type NumThree.

    //Member-level exports are never inherited,
    //so IMyData is not exported.
}

S’il existe des métadonnées associées à un InheritedExport attribut, ces métadonnées seront également héritées. (Pour plus d’informations, consultez la section précédente « Métadonnées et vues de métadonnées ». Les métadonnées héritées ne peuvent pas être modifiées par la sous-classe. Toutefois, en déclarant à nouveau l’attribut InheritedExport avec le même nom de contrat et le même type de contrat, mais avec de nouvelles métadonnées, la sous-classe peut remplacer les métadonnées héritées par de nouvelles métadonnées. La classe suivante illustre ce principe. Le composant MegaLogger hérite de Logger et inclut l'attribut InheritedExport . Étant donné que MegaLogger redéclare des métadonnées nouvelles nommées Status, elle n’hérite pas des métadonnées Name et Version de Logger.

<InheritedExport(GetType(IPlugin))>
<ExportMetadata("Name", "Logger")>
<ExportMetadata("Version", 4)>
Public Class Logger
    Implements IPlugin

    'Exports with contract type IPlugin
    'and metadata "Name" and "Version".
End Class

Public Class SuperLogger
    Inherits Logger

    'Exports with contract type IPlugin and
    'metadata "Name" and "Version", exactly the same
    'as the Logger class.

End Class

<InheritedExport(GetType(IPlugin))>
<ExportMetadata("Status", "Green")>
Public Class MegaLogger
    Inherits Logger

    'Exports with contract type IPlugin and
    'metadata "Status" only. Re-declaring
    'the attribute replaces all metadata.

End Class
[InheritedExport(typeof(IPlugin)),
    ExportMetadata("Name", "Logger"),
    ExportMetadata("Version", 4)]
public class Logger : IPlugin
{
    //Exports with contract type IPlugin and
    //metadata "Name" and "Version".
}

public class SuperLogger : Logger
{
    //Exports with contract type IPlugin and
    //metadata "Name" and "Version", exactly the same
    //as the Logger class.
}

[InheritedExport(typeof(IPlugin)),
    ExportMetadata("Status", "Green")]
public class MegaLogger : Logger        {
    //Exports with contract type IPlugin and
    //metadata "Status" only. Re-declaring
    //the attribute replaces all metadata.
}

Lorsque vous déclarez à nouveau l’attribut InheritedExport pour remplacer les métadonnées, assurez-vous que les types de contrat sont identiques. (Dans l’exemple précédent, IPlugin est le type de contrat.) S’ils diffèrent, au lieu de remplacer, le deuxième attribut crée une deuxième exportation indépendante de la partie. En règle générale, cela signifie que vous devrez spécifier explicitement le type de contrat lorsque vous remplacez un InheritedExport attribut, comme indiqué dans l’exemple précédent.

Étant donné que les interfaces ne peuvent pas être instanciées directement, elles ne peuvent généralement pas être décorées avec Export ou Import attributs. Toutefois, une interface peut être décorée avec un InheritedExport attribut au niveau de l’interface, et cette exportation avec toutes les métadonnées associées est héritée par toutes les classes d’implémentation. Toutefois, l’interface elle-même ne sera pas disponible en tant que partie.

Attributs d’exportation personnalisés

Les attributs d'exportation de base, Export et InheritedExport, peuvent être étendus pour inclure des métadonnées en tant que propriétés d'attribut. Cette technique est utile pour appliquer des métadonnées similaires à de nombreuses parties ou créer une arborescence d’héritage d’attributs de métadonnées.

Un attribut personnalisé peut spécifier le type de contrat, le nom du contrat ou toute autre métadonnées. Pour définir un attribut personnalisé, une classe hérite de ExportAttribute (ou InheritedExportAttribute) doit être décorée avec l’attribut MetadataAttribute . La classe suivante définit un attribut personnalisé.

<MetadataAttribute()>
<AttributeUsage(AttributeTargets.Class, AllowMultiple:=false)>
Public Class MyAttribute
    Inherits ExportAttribute

    Public Property MyMetadata As String

    Public Sub New(ByVal myMetadata As String)
        MyBase.New(GetType(IMyAddin))

        myMetadata = myMetadata
    End Sub

End Class
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class MyAttribute : ExportAttribute
{
    public MyAttribute(string myMetadata)
        : base(typeof(IMyAddin))
    {
        MyMetadata = myMetadata;
    }

    public string MyMetadata { get; private set; }
}

Cette classe définit un attribut personnalisé nommé MyAttribute avec le type IMyAddin de contrat et certaines métadonnées nommées MyMetadata. Toutes les propriétés d’une classe marquée avec l’attribut MetadataAttribute sont considérées comme des métadonnées définies dans l’attribut personnalisé. Les deux déclarations suivantes sont équivalentes.

<Export(GetType(IMyAddin))>
<ExportMetadata("MyMetadata", "theData")>
Public Property myAddin As MyAddin
<MyAttribute("theData")>
Public Property myAddin As MyAddin
[Export(typeof(IMyAddin)),
    ExportMetadata("MyMetadata", "theData")]
public MyAddin myAddin { get; set; }
[MyAttribute("theData")]
public MyAddin myAddin { get; set; }

Dans la première déclaration, le type de contrat et les métadonnées sont explicitement définis. Dans la deuxième déclaration, le type de contrat et les métadonnées sont implicites dans l’attribut personnalisé. En particulier dans les cas où une grande quantité de métadonnées identiques doit être appliquée à de nombreuses parties (par exemple, l’auteur ou les informations de copyright), l’utilisation d’un attribut personnalisé peut économiser beaucoup de temps et de duplication. En outre, les arborescences d’héritage d’attributs personnalisés peuvent être créées pour permettre des variations.

Pour créer des métadonnées facultatives dans un attribut personnalisé, vous pouvez utiliser l’attribut DefaultValue . Lorsque cet attribut est appliqué à une propriété dans une classe d’attributs personnalisée, il spécifie que la propriété décorée est facultative et ne doit pas être fournie par un exportateur. Si une valeur de la propriété n’est pas fournie, elle reçoit la valeur par défaut pour son type de propriété (généralement null, falseou 0.)

Stratégies de création

Lorsqu’une partie spécifie une importation et une composition est effectuée, le conteneur de composition tente de trouver une exportation correspondante. Si l'importation correspond avec succès à une exportation, le membre importateur est défini comme une instance de l'objet exporté. Là où cette instance provient est contrôlée par la stratégie de création du composant d’exportation.

Les deux stratégies de création possibles sont partagées et non partagées. Un composant doté d'une stratégie de création partagée sera partagé entre chaque importation dans le conteneur pour un composant avec ce contrat. Lorsque le moteur de composition trouve une correspondance et qu’il doit définir une propriété d’importation, il instanciera une nouvelle instance de la partie uniquement si une telle copie n'existe pas déjà ; sinon, l'instance existante sera fournie. Cela signifie que de nombreux objets peuvent avoir des références à la même partie. De tels composants ne doivent pas s'appuyer sur l'état interne susceptible d'être modifié à partir de nombreux emplacements. Cette stratégie est appropriée pour les composants statiques, les parties qui fournissent des services et des parties qui consomment beaucoup de mémoire ou d’autres ressources.

Un composant doté de la stratégie de création non partagée est créé chaque fois qu'une importation correspondante est trouvée pour l'une de ses exportations. Une nouvelle copie est donc instanciée pour chaque importation dans le conteneur qui correspond à l'un des contrats exportés du composant. L’état interne de ces copies ne sera pas partagé. Cette stratégie est appropriée pour les composants pour lesquels chaque importation exige son propre état interne.

L’importation et l’exportation peuvent spécifier la stratégie de création d’un élément, parmi les valeurs Shared, NonShared ou Any. La valeur par défaut concerne Any les importations et les exportations. Exportation qui spécifie Shared ou NonShared ne correspondra qu’à une importation qui spécifie le même ou qui spécifie Any. De même, une importation qui spécifie Shared ou NonShared ne correspond qu’à une exportation qui spécifie le même ou qui spécifie Any. Les importations et les exportations avec des stratégies de création incompatibles ne sont pas considérées comme une correspondance, de la même façon qu’une importation et une exportation dont le nom du contrat ou le type de contrat ne correspondent pas. Si l'importation et l'exportation spécifient Any, ou ne spécifient pas de stratégie de création et sont dotées par défaut de la stratégie Any, la stratégie de création sera partagée par défaut.

L’exemple suivant montre à la fois les importations et les exportations spécifiant des stratégies de création. PartOne ne spécifie pas de stratégie de création. Par conséquent, la valeur par défaut est Any. PartTwo ne spécifie pas de stratégie de création. Par conséquent, la valeur par défaut est Any. Comme l'importation et l'exportation présentent la valeur par défaut Any, PartOne est partagé. PartThree spécifie une Shared stratégie de création. Par conséquent PartTwo , et PartThree partagera la même copie de PartOne. PartFour spécifie une politique de création NonShared, donc PartFour ne sera pas partagée dans PartFive. PartSix spécifie une NonShared stratégie de création. PartFive et PartSix recevra chacune des copies distinctes de PartFour. PartSeven spécifie une Shared stratégie de création. Étant donné qu’aucun PartFour n’est exporté avec une stratégie de création de Shared, l’importation PartSeven ne correspond à rien et ne sera pas remplie.

<Export()>
Public Class PartOne
    'The default creation policy for an export is Any.
End Class

Public Class PartTwo

    <Import()>
    Public Property partOne As PartOne

    'The default creation policy for an import is Any.
    'If both policies are Any, the part will be shared.

End Class

Public Class PartThree

    <Import(RequiredCreationPolicy:=CreationPolicy.Shared)>
    Public Property partOne As PartOne

    'The Shared creation policy is explicitly specified.
    'PartTwo and PartThree will receive references to the
    'SAME copy of PartOne.

End Class

<Export()>
<PartCreationPolicy(CreationPolicy.NonShared)>
Public Class PartFour
    'The NonShared creation policy is explicitly specified.
End Class

Public Class PartFive

    <Import()>
    Public Property partFour As PartFour

    'The default creation policy for an import is Any.
    'Since the export's creation policy was explicitly
    'defined, the creation policy for this property will
    'be non-shared.

End Class

Public Class PartSix

    <Import(RequiredCreationPolicy:=CreationPolicy.NonShared)>
    Public Property partFour As PartFour

    'Both import and export specify matching creation
    'policies.  PartFive and PartSix will each receive
    'SEPARATE copies of PartFour, each with its own
    'internal state.

End Class

Public Class PartSeven

    <Import(RequiredCreationPolicy:=CreationPolicy.Shared)>
    Public Property partFour As PartFour

    'A creation policy mismatch.  Because there is no
    'exported PartFour with a creation policy of Shared,
    'this import does not match anything and will not be
    'filled.

End Class
[Export]
public class PartOne
{
    //The default creation policy for an export is Any.
}

public class PartTwo
{
    [Import]
    public PartOne partOne { get; set; }

    //The default creation policy for an import is Any.
    //If both policies are Any, the part will be shared.
}

public class PartThree
{
    [Import(RequiredCreationPolicy = CreationPolicy.Shared)]
    public PartOne partOne { get; set; }

    //The Shared creation policy is explicitly specified.
    //PartTwo and PartThree will receive references to the
    //SAME copy of PartOne.
}

[Export]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class PartFour
{
    //The NonShared creation policy is explicitly specified.
}

public class PartFive
{
    [Import]
    public PartFour partFour { get; set; }

    //The default creation policy for an import is Any.
    //Since the export's creation policy was explicitly
    //defined, the creation policy for this property will
    //be non-shared.
}

public class PartSix
{
    [Import(RequiredCreationPolicy = CreationPolicy.NonShared)]
    public PartFour partFour { get; set; }

    //Both import and export specify matching creation
    //policies.  PartFive and PartSix will each receive
    //SEPARATE copies of PartFour, each with its own
    //internal state.
}

public class PartSeven
{
    [Import(RequiredCreationPolicy = CreationPolicy.Shared)]
    public PartFour partFour { get; set; }

    //A creation policy mismatch.  Because there is no
    //exported PartFour with a creation policy of Shared,
    //this import does not match anything and will not be
    //filled.
}

Cycle de vie et élimination

Étant donné que les parties sont hébergées dans le conteneur de composition, leur cycle de vie peut être plus complexe que les objets ordinaires. Les parties peuvent implémenter deux interfaces importantes liées au cycle de vie : IDisposable et IPartImportsSatisfiedNotification.

Les parties qui nécessitent une exécution de travail lors de l’arrêt ou qui doivent libérer des ressources doivent être implémentées IDisposable, comme d’habitude pour les objets .NET Framework. Toutefois, étant donné que le conteneur crée et gère des références à des parties, seul le conteneur propriétaire d’un composant doit appeler la Dispose méthode dessus. Le conteneur lui-même implémente IDisposableet, dans le cadre de son nettoyage dans Dispose , il appellera Dispose sur tous les composants qu'il détient. Pour cette raison, vous devez toujours supprimer le conteneur de composition quand lui et tous les composants qu'il détient ne sont plus requis.

Pour les conteneurs de composition durables, la consommation de mémoire par des composants ayant une politique de création non partagée peut devenir problématique. Ces parties non partagées peuvent être créées plusieurs fois et ne seront pas supprimées tant que le conteneur lui-même n’est pas supprimé. Pour résoudre ce problème, le conteneur fournit la ReleaseExport méthode. L'appel de cette méthode sur un export non partagé supprime cet export du conteneur de composition et le libère. Les composants utilisés seulement par l'exportation supprimée, et ainsi de suite jusqu'au bas de l'arborescence, sont également supprimés. De cette façon, les ressources peuvent être récupérées sans supprimer le conteneur de composition lui-même.

IPartImportsSatisfiedNotification contient une méthode nommée OnImportsSatisfied. Cette méthode est appelée par le conteneur de composition sur toutes les parties qui implémentent l’interface lorsque la composition a été terminée et les importations du composant sont prêtes à être utilisées. Les composants sont créés par le moteur de composition pour remplir les importations des autres composants. Avant que les importations d’un composant aient été définies, vous ne pouvez pas effectuer d’initialisation qui s’appuie sur ou manipule des valeurs importées dans le constructeur de composant, sauf si ces valeurs ont été spécifiées comme prérequis à l’aide de l’attribut ImportingConstructor . Ceci est normalement la méthode conseillée mais, dans certains cas, l'injection de constructeur peut ne pas être disponible. Dans ces cas, l’initialisation peut être effectuée dans OnImportsSatisfied, et la partie doit implémenter IPartImportsSatisfiedNotification.