Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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.
Podstawy importu i eksportu
Eksport jest wartością, którą część udostępnia innym częściom w kontenerze, a import jest wymaganiem, które część wyraża wobec kontenera, aby zostało spełnione przez dostępne eksporty. W modelu programowania z atrybutami, importy i eksporty są deklarowane przez dekorowanie klas lub członków przy użyciu atrybutów 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 jeśli zarówno nazwa kontraktu, jak i typ kontraktu są zgodne, eksport jest uznawany za spełniający określony import.
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, pochodzić z typu kontraktu lub implementować typ kontraktu, jeśli jest on 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 / Notatka
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 członka przerywa model izolacji 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 IMyAddin
i dlatego może być rzutowane na obiekt IMyAddin
, ten eksport nie będzie zgodny z poprzednim importem, 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ć niestandardowym delegatem 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, moduł importujący musi zadeklarować odpowiedni członek. 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 importów, w tym dynamiczny, leniwy, wymagany i opcjonalny.
Importy dynamiczne
W niektórych przypadkach klasa importująca 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 leniwe
W niektórych przypadkach klasa importująca może wymagać pośredniego odwołania do zaimportowanego obiektu, aby obiekt nie był natychmiast tworzony. 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 elementów wymaganych
Wyeksportowane części MEF są zazwyczaj tworzone przez silnik kompozycji w odpowiedzi na bezpośrednie żądanie lub potrzebę spełnienia dopasowanego zapotrzebowania na import. 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. Brak konstruktora bez parametrów oraz brak atrybutu ImportingConstructor
, lub podanie więcej niż jednego atrybutu ImportingConstructor
, 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 w trakcie inicjowania części kodu. 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 parametrach kolekcji. Jeśli na przykład określisz ImportingConstructor
dla konstruktora z parametrem typu IEnumerable<int>
, import dopasuje się do pojedynczego eksportu typu IEnumerable<int>
, zamiast do 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, kompozycja tej części nie powiedzie się, a część nie będzie możliwa do użycia.
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 w 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, używając elementów 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 pokazuje 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ą zapewniać dziedziczone eksporty do klas, które się z nich wywodzą.
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 odkryty przez katalog. 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.
}
Metadane i widoki 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ć opóźnione, 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 rezygnuje się wtedy z korzyści płynących ze sprawdzania typów, dlatego należy tego 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, gdzie interfejs metadanych jest drugim parametrem typu dla 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 będziesz chciał połączyć metadane z atrybutem ImportMany
, aby przeanalizować dostępne importy i wybrać oraz utworzyć instancję tylko jednego elementu, lub filtrować kolekcję, aby spełniała określony warunek. Poniższa klasa instancjuje tylko te IPlugin
obiekty, 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 członka nigdy nie mogą być dziedziczone.
W poniższych czterech klasach przedstawiono zasady dziedziczenia dotyczące importu i eksportu.
NumTwo
dziedziczy po NumOne
, więc NumTwo
zaimportuje IMyData
. Zwykłe eksporty nie są dziedziczone, więc NumTwo
nie będą eksportować niczego.
NumFour
dziedziczy z NumThree
. Ponieważ NumThree
używa InheritedExport
, NumFour
ma jeden eksport z typem kontraktu NumThree
. Eksporty na poziomie członkowskim nigdy nie są dziedziczone, więc IMyData
nie jest eksportowany.
<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 po Logger
i zawiera atrybut InheritedExport
. 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 nadpisywania, drugi atrybut utworzy drugi, niezależny eksport z części. 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ć atrybutami Export
ani Import
. 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 eksportu, Export
i InheritedExport
, można rozszerzyć, uwzględniając metadane 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 niestandardowy atrybut o nazwie MyAttribute
, typ kontraktu IMyAddin
i metadane 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 uniknąć powielania. 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
, false
lub 0).
Zasady tworzenia
Gdy część określa import, a kompozycja jest wykonywana, kontener kompozycyjny próbuje znaleźć odpowiadają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 ten obiekt, jest kontrolowane przez politykę tworzenia części eksportującej.
Dwie możliwe zasady tworzenia to współużytkowane i niewspółużytkowane. Część z polityką tworzenia jako współdzielona będzie dzielona między wszystkie importy w kontenerze dla części zgodnie z tym kontraktem. Gdy silnik kompozycji znajdzie dopasowanie i musi ustawić właściwość importowania, utworzy nową kopię elementu tylko wtedy, gdy ona jeszcze nie istnieje; w przeciwnym razie użyje istniejącej kopii. 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 mógłby być zmieniany w wielu miejscach. 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.
Za każdym razem, gdy znajdzie się pasujący import dla jednego z jego eksportów, zostanie utworzona część zgodnie z polityką tworzenia jako nieudostępniana. Każdy import w kontenerze, który odpowiada jednemu z wyeksportowanych kontraktów części, spowoduje utworzenie nowej kopii. Stan wewnętrzny tych kopii nie zostanie udostępniony. Polityka ta jest właściwa dla sytuacji, 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
, NonShared
lub 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ślają Any
, lub nie określają polityki tworzenia i domyślna będzie Any
, polityka tworzenia będzie domyślnie udostępniana.
W poniższym przykładzie przedstawiono zarówno importy, jak i eksporty, które określają 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ślnie używają Any
, PartOne
będą wspólne.
PartThree
określa Shared
politykę tworzenia, więc PartTwo
i PartThree
będą współużytkować 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
i PartSix
otrzymają oddzielne kopie PartFour
.
PartSeven
określa Shared
zasady tworzenia. Ponieważ nie ma wyeksportowanej PartFour
zgodnej z zasadą Shared
tworzenia, import PartSeven
nie pasuje do żadnego elementu 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 utylizacja
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ą pracy podczas zamykania lub które muszą zwolnić 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 IDisposable
, a podczas czyszczenia w Dispose
wywoła Dispose
dla wszystkich części, które posiada. Z tego powodu należy zawsze wyrzucić pojemnik kompozycji, gdy on i jego części są już zbędne.
W przypadku długowiecznych kontenerów kompozycji, użycie pamięci przez części z polityką tworzenia nieudostępnianą może stać się problemem. Te niewspółdzielone części można tworzyć wiele razy i nie zostaną usunięte, dopóki kontener nie zostanie usunięty. Aby rozwiązać ten problem, kontener udostępnia metodę ReleaseExport
. Wywołanie tej metody na nie współdzielonym eksporcie usuwa ten eksport z kompozycyjnego kontenera i usuwa go z pamięci. Części, które są używane tylko przez usunięty eksport, a także te znajdujące się głębiej w hierarchii, są również usuwane i utylizowane. W ten sposób zasoby mogą być odzyskiwane bez usuwania samego kontenera kompozycji.
IPartImportsSatisfiedNotification
zawiera jedną metodę o nazwie OnImportsSatisfied
. Metoda ta jest wywoływana przez kontener kompozycji na wszystkich częściach, które implementują interfejs po zakończeniu kompozycji, gdy importy części są gotowe do użycia. Części są tworzone przez silnik kompozycji, aby uzupełnić importy 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 OnImportsSatisfied
, a część powinna implementować IPartImportsSatisfiedNotification
.