Omówienie modelu programowania przypisywanego (MEF)

W programie Managed Extensibility Framework (MEF) model programowania jest konkretną metodą definiowania zestawu obiektów koncepcyjnych, na których działa meF. Te obiekty koncepcyjne obejmują części, importy i eksporty. MeF używa tych obiektów, ale nie określa sposobu ich reprezentowania. W związku z tym możliwe jest wiele różnych modeli programowania, w tym dostosowanych modeli programowania.

Domyślny model programowania używany w mef jest modelem programowania przypisywanego. W przypisanych częściach modelu programowania import, eksporty i inne obiekty są definiowane z atrybutami, które dekorują zwykłe klasy programu .NET Framework. W tym temacie wyjaśniono, jak używać atrybutów udostępnianych przez model programowania przypisanego do tworzenia aplikacji MEF.

Importowanie i eksportowanie podstaw

Eksport jest wartością, którą część udostępnia innym częściom w kontenerze, a import jest wymaganiem, aby część wyrażała kontener, do wypełnienia z dostępnych eksportów. W modelu programowania przypisywanego importy i eksporty są deklarowane przez dekorowanie klas lub elementów członkowskich z atrybutami Import i Export . Atrybut Export może udekorować klasę, pole, właściwość lub metodę, podczas gdy Import atrybut może ozdobić pole, właściwość lub parametr konstruktora.

Aby import był zgodny z eksportem, import i eksport muszą mieć ten sam kontrakt. Kontrakt składa się z ciągu, nazywanego nazwą kontraktu, a typem wyeksportowanego lub zaimportowanego obiektu, nazywanym typem kontraktu. Tylko wtedy, gdy zarówno nazwa kontraktu, jak i typ kontraktu są zgodne z eksportem rozważanym do realizacji określonego importu.

Oba parametry kontraktu mogą być niejawne lub jawne. Poniższy kod przedstawia klasę, która deklaruje podstawowy import.

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

W tym importze Import atrybut nie ma ani typu kontraktu, ani parametru nazwy kontraktu. W związku z tym oba zostaną wywnioskowane z obiektu ozdobionego. W takim przypadku typ kontraktu to IMyAddin, a nazwa kontraktu będzie unikatowym ciągiem utworzonym na podstawie typu kontraktu. (Innymi słowy, nazwa kontraktu będzie zgodna tylko z eksportami, których nazwy są również wnioskowane z typu IMyAddin).

Poniżej przedstawiono eksport zgodny z poprzednim importem.

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

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

W tym eksporcie typ kontraktu jest IMyAddin określony jako parametr atrybutu Export . Wyeksportowany typ musi być taki sam jak typ kontraktu, pochodzący z typu kontraktu lub zaimplementować typ kontraktu, jeśli jest interfejsem. W tym eksporcie rzeczywisty typ MyLogger implementuje interfejs IMyAddin. Nazwa kontraktu jest wywnioskowana z typu kontraktu, co oznacza, że ten eksport będzie zgodny z poprzednim importem.

Uwaga

Eksporty i importy powinny być zwykle deklarowane w klasach publicznych lub elementach członkowskich. Inne deklaracje są obsługiwane, ale eksportowanie lub importowanie prywatnego, chronionego lub wewnętrznego elementu członkowskiego przerywa model izolacji dla tej części i dlatego nie jest zalecane.

Typ kontraktu musi być dokładnie zgodny z eksportem i importem, aby był traktowany jako zgodny. Rozważ następujący eksport.

<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 { }

W tym eksporcie typ kontraktu to MyLogger zamiast IMyAddin. Mimo że MyLogger implementuje element i dlatego może być rzutowane IMyAddinna obiekt, ten eksport nie będzie zgodny z poprzednim importem IMyAddin , ponieważ typy kontraktów nie są takie same.

Ogólnie rzecz biorąc, nie jest konieczne określenie nazwy kontraktu, a większość kontraktów powinna być zdefiniowana pod względem typu kontraktu i metadanych. Jednak w pewnych okolicznościach ważne jest, aby bezpośrednio określić nazwę kontraktu. Najczęstszym przypadkiem jest to, że klasa eksportuje kilka wartości, które mają wspólny typ, taki jak typy pierwotne. Nazwę kontraktu można określić jako pierwszy parametr atrybutu Import lub Export . Poniższy kod przedstawia import i eksport z określoną nazwą kontraktu 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;
}

Jeśli typ kontraktu nie zostanie określony, będzie on nadal wnioskowany z typu importu lub eksportu. Jednak nawet jeśli nazwa kontraktu jest określona jawnie, typ kontraktu musi być dokładnie zgodny z importem i eksportem, aby był traktowany jako zgodny. Jeśli na przykład MajorRevision pole było ciągiem, wywnioskowane typy kontraktów nie będą zgodne, a eksport nie będzie zgodny z importem, mimo że ma taką samą nazwę kontraktu.

Importowanie i eksportowanie metody

Atrybut Export może również udekorować metodę w taki sam sposób jak klasa, właściwość lub funkcja. Eksporty metod muszą określać typ kontraktu lub nazwę kontraktu, ponieważ nie można wywnioskować typu. Określony typ może być pełnomocnikiem niestandardowym lub typem ogólnym, takim jak Func. Poniższa klasa eksportuje metodę o nazwie 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);
}

W tej klasie DoSomething metoda przyjmuje jeden int parametr i zwraca wartość string. Aby dopasować ten eksport, część importu musi zadeklarować odpowiedni element członkowski. Poniższa klasa importuje metodę DoSomething .

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; }
}

Aby uzyskać więcej informacji na temat używania Func<T, T> obiektu, zobacz Func<T,TResult>.

Typy importów

MeF obsługuje kilka typów importu, w tym dynamicznych, leniwych, wstępnych i opcjonalnych.

Importy dynamiczne

W niektórych przypadkach klasa importu może chcieć dopasować eksporty dowolnego typu, które mają określoną nazwę kontraktu. W tym scenariuszu klasa może zadeklarować import dynamiczny. Poniższy import pasuje do każdego eksportu o nazwie kontraktu "TheString".

Public Class MyClass1

    <Import("TheString")>
    Public Property MyAddin

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

Gdy typ kontraktu zostanie wywnioskowany ze słowa kluczowego, będzie pasować do dowolnego typu kontraktu dynamic . W takim przypadku import powinien zawsze określać nazwę kontraktu, która ma być zgodna. (Jeśli nie określono nazwy kontraktu, import zostanie uznany za zgodny z brakiem eksportów). Oba następujące eksporty będą zgodne z poprzednim importem.

<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 { }

Oczywiście klasa importowania musi być przygotowana do obsługi obiektu dowolnego typu.

Importy z opóźnieniem

W niektórych przypadkach klasa importowania może wymagać pośredniego odwołania do zaimportowanego obiektu, aby obiekt nie był natychmiast tworzone. W tym scenariuszu klasa może zadeklarować leniwy import przy użyciu typu kontraktu Lazy<T>. Następująca właściwość importowania deklaruje leniwy import.

Public Class MyClass1

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

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

Z punktu widzenia aparatu składowego typ Lazy<T> kontraktu jest uznawany za identyczny z typem kontraktu T. W związku z tym poprzedni import będzie zgodny z następującym eksportem.

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

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

Nazwę kontraktu i typ kontraktu można określić w atrybucie Import dla leniwego importu, zgodnie z opisem we wcześniejszej sekcji "Podstawowe importy i eksporty".

Importy wymagań wstępnych

Wyeksportowane części MEF są zwykle tworzone przez aparat kompozycji w odpowiedzi na bezpośrednie żądanie lub potrzebę wypełnienia dopasowanego importu. Domyślnie podczas tworzenia części aparat kompozycji używa konstruktora bez parametrów. Aby aparat używał innego konstruktora, możesz oznaczyć go atrybutem ImportingConstructor .

Każda część może mieć tylko jeden konstruktor do użycia przez aparat kompozycji. Podanie konstruktora bez parametrów ani ImportingConstructor atrybutu ani podanie więcej niż jednego ImportingConstructor atrybutu spowoduje wystąpienie błędu.

Aby wypełnić parametry konstruktora oznaczonego atrybutem ImportingConstructor , wszystkie te parametry są automatycznie deklarowane jako importy. Jest to wygodny sposób deklarowania importów używanych podczas inicjowania części. Poniższa klasa używa ImportingConstructor metody do deklarowania importu.

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;
    }
}

Domyślnie ImportingConstructor atrybut używa wywnioskowanych typów kontraktów i nazw kontraktów dla wszystkich importów parametrów. Można to zastąpić, dekorując parametry za pomocą Import atrybutów, które następnie mogą jawnie zdefiniować typ kontraktu i nazwę kontraktu. Poniższy kod demonstruje konstruktor, który używa tej składni do importowania klasy pochodnej zamiast klasy nadrzędnej.

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

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

W szczególności należy zachować ostrożność przy użyciu parametrów kolekcji. Jeśli na przykład określisz ImportingConstructor konstruktora z parametrem typu IEnumerable<int>, import będzie pasować do pojedynczego eksportu typu IEnumerable<int>, zamiast zestawu eksportów typu int. Aby dopasować zestaw eksportów typu int, należy udekorować parametr za pomocą atrybutu ImportMany .

Parametry zadeklarowane jako import przez ImportingConstructor atrybut są również oznaczone jako import wymagań wstępnych. MeF zwykle umożliwia eksportom i importom tworzenie cyklu. Na przykład cykl polega na tym, że obiekt A importuje obiekt B, który z kolei importuje obiekt A. W zwykłych okolicznościach cykl nie jest problemem, a kontener kompozycji zwykle konstruuje oba obiekty.

Gdy zaimportowana wartość jest wymagana przez konstruktor części, ten obiekt nie może uczestniczyć w cyklu. Jeśli obiekt A wymaga skonstruowania obiektu B, zanim będzie można go skonstruować, a obiekt B importuje obiekt A, wówczas cykl nie będzie mógł rozwiązać problemu i wystąpi błąd kompozycji. W związku z tym importy zadeklarowane na parametrach konstruktora są wstępnie wymaganymi importami, które muszą zostać wypełnione przed każdym eksportem z obiektu, który wymaga ich użycia.

Opcjonalne importy

Atrybut Import określa wymaganie, aby część działała. Jeśli nie można zrealizować importu, skład tej części zakończy się niepowodzeniem, a część nie będzie dostępna.

Możesz określić, że import jest opcjonalny przy użyciu AllowDefault właściwości . W takim przypadku kompozycja zakończy się pomyślnie, nawet jeśli import nie jest zgodny z żadnymi dostępnymi eksportami, a właściwość importowania zostanie ustawiona na wartość domyślną dla jej typu właściwości (null dla właściwości obiektu, false wartości logicznej lub zera dla właściwości liczbowych). Poniższa klasa używa opcjonalnego importu.

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

Importowanie wielu obiektów

Atrybut Import zostanie pomyślnie skomponowany tylko wtedy, gdy pasuje do jednego i tylko jednego eksportu. Inne przypadki spowodują błąd kompozycji. Aby zaimportować więcej niż jeden eksport zgodny z tym samym kontraktem, użyj atrybutu ImportMany . Importy oznaczone tym atrybutem są zawsze opcjonalne. Na przykład kompozycja nie zakończy się niepowodzeniem, jeśli nie ma pasujących eksportów. Poniższa klasa importuje dowolną liczbę eksportów typu IMyAddin.

Public Class MyClass1

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

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

Dostęp do zaimportowanej tablicy można uzyskać przy użyciu zwykłej IEnumerable<T> składni i metod. Zamiast tego można użyć zwykłej tablicy (IMyAddin[]).

Ten wzorzec może być bardzo ważny, gdy używasz go w połączeniu ze składnią Lazy<T> . Na przykład przy użyciu poleceń ImportMany, IEnumerable<T>i Lazy<T>można zaimportować odwołania pośrednie do dowolnej liczby obiektów i utworzyć wystąpienie tylko tych, które stają się niezbędne. Poniższa klasa przedstawia ten wzorzec.

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; }
}

Unikanie odnajdywania

W niektórych przypadkach możesz uniemożliwić odnajdywanie części w ramach wykazu. Na przykład część może być klasą bazową przeznaczoną do dziedziczenia, ale nie używaną. Istnieją dwa sposoby, aby to osiągnąć. Najpierw możesz użyć słowa kluczowego abstract w klasie części. Klasy abstrakcyjne nigdy nie dostarczają eksportów, chociaż mogą zapewnić dziedziczone eksporty do klas, które pochodzą z nich.

Jeśli klasa nie może być abstrakcyjna, można ją ozdobić atrybutem PartNotDiscoverable . Część ozdobiona tym atrybutem nie zostanie uwzględniona w żadnym wykazie. W poniższym przykładzie pokazano te wzorce. DataOne zostanie odnaleziony przez wykaz. Ponieważ DataTwo jest abstrakcyjna, nie zostanie odnaleziona. Ponieważ DataThree użyto atrybutu PartNotDiscoverable , nie zostanie odnaleziony.

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

Widoki metadanych i metadanych

Eksporty mogą udostępniać dodatkowe informacje o sobie znane jako metadane. Metadane mogą służyć do przekazywania właściwości wyeksportowanego obiektu do części importowania. Część importu może użyć tych danych, aby zdecydować, które eksporty mają być używane, lub zbierać informacje o eksporcie bez konieczności ich konstruowania. Z tego powodu importowanie musi być leniwe, aby można było używać metadanych.

Aby użyć metadanych, zazwyczaj deklarujesz interfejs znany jako widok metadanych, który deklaruje, jakie metadane będą dostępne. Interfejs widoku metadanych musi mieć tylko właściwości, a te właściwości muszą mieć get metody dostępu. Poniższy interfejs to przykładowy widok metadanych.

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; }
}

Można również użyć kolekcji ogólnej, IDictionary<string, object>, jako widoku metadanych, ale powoduje to przeniknąć korzyści z sprawdzania typów i należy unikać.

Zazwyczaj wszystkie właściwości o nazwie w widoku metadanych są wymagane, a wszystkie eksporty, które nie dostarczają ich, nie zostaną uznane za zgodne. Atrybut DefaultValue określa, że właściwość jest opcjonalna. Jeśli właściwość nie jest dołączona, zostanie przypisana wartość domyślna określona jako parametr .DefaultValue Poniżej przedstawiono dwie różne klasy ozdobione metadanymi. Obie te klasy pasują do poprzedniego widoku metadanych.

<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
{
}

Metadane są wyrażane po atrybucie Export za pomocą atrybutu ExportMetadata . Każdy element metadanych składa się z pary nazwa/wartość. Część nazwy metadanych musi być zgodna z nazwą odpowiedniej właściwości w widoku metadanych, a wartość zostanie przypisana do tej właściwości.

Jest to importer, który określa, jaki widok metadanych, jeśli istnieje, będzie używany. Importowanie z metadanymi jest deklarowane jako leniwy import z interfejsem metadanych jako drugi parametr typu do Lazy<T,T>. Poniższa klasa importuje poprzednią część z metadanymi.

Public Class Addin

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

W wielu przypadkach należy połączyć metadane z atrybutem ImportMany , aby przeanalizować dostępne importy i wybrać i utworzyć wystąpienie tylko jednej kolekcji lub filtrować w celu dopasowania do określonego warunku. Poniższa klasa tworzy wystąpienie tylko IPlugin obiektów, które mają Name wartość "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;
    }
}

Importowanie i eksportowanie dziedziczenia

Jeśli klasa dziedziczy z części, ta klasa może również stać się częścią. Importy są zawsze dziedziczone przez podklasy. W związku z tym podklasa części będzie zawsze częścią, z tymi samymi importami co jej klasa nadrzędna.

Eksporty zadeklarowane przy użyciu atrybutu Export nie są dziedziczone przez podklasy. Jednak część może eksportować się za pomocą atrybutu InheritedExport . Podklasy części będą dziedziczyć i zapewniać ten sam eksport, w tym nazwę kontraktu i typ kontraktu. W przeciwieństwie do atrybutu Export można InheritedExport stosować tylko na poziomie klasy, a nie na poziomie elementu członkowskiego. W związku z tym eksporty na poziomie elementu członkowskiego nigdy nie mogą być dziedziczone.

W poniższych czterech klasach przedstawiono zasady importowania i eksportowania dziedziczenia. NumTwo dziedziczy z NumOneelementu , więc NumTwo zaimportuje IMyDatawartość . Zwykłe eksporty nie są dziedziczone, więc NumTwo nie będą eksportować niczego. NumFour dziedziczy z NumThree. Ponieważ NumThree używany , NumFourInheritedExportma jeden eksport z typem NumThreekontraktu . Eksporty na poziomie elementu członkowskiego nigdy nie są dziedziczone, więc IMyData nie są eksportowane.

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

Jeśli istnieją metadane skojarzone z atrybutem InheritedExport , te metadane również będą dziedziczone. (Aby uzyskać więcej informacji, zobacz sekcję "Metadane i widoki metadanych". Dziedziczone metadane nie mogą być modyfikowane przez podklasę. Jednak przez ponowne zadeklarowanie atrybutu InheritedExport o tej samej nazwie kontraktu i typie kontraktu, ale z nowymi metadanymi podklasa może zastąpić dziedziczone metadane nowymi metadanymi. Poniższa klasa demonstruje tę zasadę. Część MegaLogger dziedziczy i Logger zawiera InheritedExport atrybut . Ponieważ MegaLogger ponownie deklaruje nowe metadane o nazwie Status, nie dziedziczy metadanych nazwy i wersji z klasy 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.
}

Podczas ponownego deklarowania atrybutu InheritedExport w celu zastąpienia metadanych upewnij się, że typy kontraktów są takie same. (W poprzednim przykładzie IPlugin jest typem kontraktu). Jeśli będą się one różnić, zamiast zastąpić, drugi atrybut utworzy drugi, niezależny eksport ze strony. Ogólnie rzecz biorąc, oznacza to, że podczas zastępowania atrybutu należy jawnie określić typ kontraktu InheritedExport , jak pokazano w poprzednim przykładzie.

Ponieważ interfejsy nie mogą być tworzone bezpośrednio, zazwyczaj nie można ich dekorować ani ExportImport atrybutów. Interfejs można jednak ozdobić atrybutem InheritedExport na poziomie interfejsu, a eksport wraz z wszystkimi skojarzonymi metadanymi będzie dziedziczony przez wszystkie klasy implementujące. Sam interfejs nie będzie jednak dostępny jako część.

Atrybuty eksportu niestandardowego

Podstawowe atrybuty Export eksportu i InheritedExport, można rozszerzyć w celu uwzględnienia metadanych jako właściwości atrybutu. Ta technika jest przydatna do stosowania podobnych metadanych do wielu części lub tworzenia drzewa dziedziczenia atrybutów metadanych.

Atrybut niestandardowy może określać typ kontraktu, nazwę kontraktu lub inne metadane. Aby zdefiniować atrybut niestandardowy, klasa dziedzicząca z ExportAttribute (lub InheritedExportAttribute) musi być ozdobiona atrybutem MetadataAttribute . Poniższa klasa definiuje atrybut niestandardowy.

<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; }
}

Ta klasa definiuje atrybut niestandardowy o nazwie z MyAttribute typem IMyAddin kontraktu i niektórymi metadanymi o nazwie MyMetadata. Wszystkie właściwości w klasie oznaczonej atrybutem MetadataAttribute są uważane za metadane zdefiniowane w atrybucie niestandardowym. Następujące dwie deklaracje są równoważne.

<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; }

W pierwszej deklaracji typ kontraktu i metadane są jawnie zdefiniowane. W drugiej deklaracji typ kontraktu i metadane są niejawne w dostosowanym atrybucie. Szczególnie w przypadkach, gdy wiele identycznych metadanych należy zastosować do wielu części (na przykład informacji o autorach lub prawach autorskich), użycie atrybutu niestandardowego może zaoszczędzić dużo czasu i duplikować. Ponadto można utworzyć drzewa dziedziczenia atrybutów niestandardowych, aby umożliwić korzystanie z odmian.

Aby utworzyć opcjonalne metadane w atrybucie niestandardowym, możesz użyć atrybutu DefaultValue . Gdy ten atrybut jest stosowany do właściwości w klasie atrybutów niestandardowych, określa, że właściwość ozdobiona jest opcjonalna i nie musi być dostarczana przez eksportera. Jeśli wartość właściwości nie zostanie podana, zostanie przypisana wartość domyślna dla typu właściwości (zazwyczaj null, falselub 0).

Zasady tworzenia

Gdy część określa import i kompozycję jest wykonywana, kontener kompozycji próbuje znaleźć pasujący eksport. Jeśli importowanie z eksportem zostanie pomyślnie dopasowane, importujący element członkowski zostanie ustawiony na wystąpienie wyeksportowanego obiektu. Miejsce, z którego pochodzi to wystąpienie, jest kontrolowane przez zasady tworzenia części eksportu.

Dwie możliwe zasady tworzenia są współużytkowane i nieudostępne. Część z zasadami tworzenia współużytkowanych będzie współdzielona między każdym importem w kontenerze dla części z tym kontraktem. Gdy aparat kompozycji znajdzie dopasowanie i musi ustawić właściwość importowania, utworzy wystąpienie nowej kopii części tylko wtedy, gdy jeszcze nie istnieje; w przeciwnym razie dostarczy istniejącą kopię. Oznacza to, że wiele obiektów może mieć odwołania do tej samej części. Takie części nie powinny polegać na stanie wewnętrznym, który może zostać zmieniony z wielu miejsc. Te zasady są odpowiednie dla części statycznych, części, które zapewniają usługi i części, które zużywają dużo pamięci lub innych zasobów.

Część z zasadami tworzenia nieudostępnianych zostanie utworzona za każdym razem, gdy zostanie znaleziony pasujący import dla jednego z jego eksportów. W związku z tym dla każdego importu w kontenerze, który pasuje do jednego z wyeksportowanych kontraktów części, zostanie utworzone wystąpienie nowej kopii. Stan wewnętrzny tych kopii nie zostanie udostępniony. Te zasady są odpowiednie dla części, w których każdy import wymaga własnego stanu wewnętrznego.

Zarówno import, jak i eksport mogą określać zasady tworzenia części z wartości Shared, NonSharedlub Any. Wartością domyślną jest Any zarówno import, jak i eksport. Eksport, który określa Shared lub NonShared będzie pasować tylko do importu, który określa to samo lub który określa Any. Podobnie import, który określa Shared lub NonShared będzie pasować tylko do eksportu, który określa to samo lub który określa Any. Importy i eksporty z niezgodnymi zasadami tworzenia nie są traktowane jako zgodne, w taki sam sposób jak import i eksport, którego nazwa kontraktu lub typ kontraktu nie są zgodne. Jeśli zarówno import, jak i eksport określ Anywartość , lub nie określi zasad tworzenia i wartości domyślnej na wartość , zasady tworzenia będą domyślnie Anyudostępniane.

W poniższym przykładzie przedstawiono zarówno import, jak i eksporty określające zasady tworzenia. PartOne nie określa zasad tworzenia, więc wartość domyślna to Any. PartTwo nie określa zasad tworzenia, więc wartość domyślna to Any. Ponieważ zarówno import, jak i eksport domyślny do Any, PartOne zostaną udostępnione. PartThree określa Shared zasady tworzenia, więc PartTwo i PartThree będzie współużytkuje tę samą kopię PartOne. PartFour określa NonShared zasady tworzenia, więc PartFour nie będą współużytkowane w programie PartFive. PartSix określa NonShared zasady tworzenia. PartFive każda PartSix z nich będzie otrzymywać oddzielne kopie programu PartFour. PartSeven określa Shared zasady tworzenia. Ponieważ nie ma wyeksportowanych PartFour zasad Sharedtworzenia , PartSeven import nie pasuje do niczego i nie zostanie wypełniony.

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

Cykl życia i dysponowanie

Ponieważ części są hostowane w kontenerze kompozycji, ich cykl życia może być bardziej złożony niż zwykłe obiekty. Części mogą implementować dwa ważne interfejsy związane z cyklem życia: IDisposable i IPartImportsSatisfiedNotification.

Części, które wymagają wykonania pracy przy zamykaniu lub które muszą wydać zasoby, powinny implementować IDisposable, jak zwykle w przypadku obiektów programu .NET Framework. Jednak ponieważ kontener tworzy i utrzymuje odwołania do części, tylko kontener, który jest właścicielem części, powinien wywołać metodę Dispose . Sam kontener implementuje IDisposableelement , a w ramach jego oczyszczania Dispose wywoła Dispose wszystkie posiadane przez niego części. Z tego powodu należy zawsze pozbyć się pojemnika kompozycji, gdy jest on i wszystkie jego części nie są już potrzebne.

W przypadku długotrwałych kontenerów kompozycji użycie pamięci według części z zasadami tworzenia nieużytkowanych może stać się problemem. Te nieudostępne części można tworzyć wiele razy i nie zostaną usunięte do momentu usunięcia samego kontenera. Aby rozwiązać ten problem, kontener udostępnia metodę ReleaseExport . Wywołanie tej metody w eksporcie innym niż współużytkowane usuwa ten eksport z kontenera kompozycji i usuwa go. Części, które są używane tylko przez usunięty eksport, i tak dalej w drzewie, są również usuwane i usuwane. W ten sposób zasoby mogą być odzyskiwane bez samego kontenera kompozycji.

IPartImportsSatisfiedNotification zawiera jedną metodę o nazwie OnImportsSatisfied. Ta metoda jest wywoływana przez kontener kompozycji na wszystkich częściach, które implementują interfejs po zakończeniu kompozycji, a import części jest gotowy do użycia. Części są tworzone przez silnik kompozycji, aby wypełnić import innych części. Przed ustawieniem importu części nie można wykonać żadnej inicjalizacji, która opiera się na importowanych wartościach lub manipuluje importowanymi wartościami w konstruktorze części, chyba że te wartości zostały określone jako wymagania wstępne przy użyciu atrybutu ImportingConstructor . Jest to zwykle preferowana metoda, ale w niektórych przypadkach iniekcja konstruktora może być niedostępna. W takich przypadkach inicjowanie można wykonać w OnImportsSatisfiedpliku , a część powinna implementować element IPartImportsSatisfiedNotification.