Managed Extensibility Framework (MEF)
Det här avsnittet innehåller en översikt över Managed Extensibility Framework som introducerades i .NET Framework 4.
Vad är MEF?
Managed Extensibility Framework eller MEF är ett bibliotek för att skapa enkla och utökningsbara program. Det gör att programutvecklare kan identifiera och använda tillägg utan att någon konfiguration krävs. Det gör det också enkelt för tilläggsutvecklare att kapsla in kod och undvika ömtåliga hårda beroenden. MEF tillåter inte bara att tillägg återanvänds i program, utan även mellan program.
Problemet med utökningsbarhet
Imagine att du är arkitekt för ett stort program som måste ge stöd för utökningsbarhet. Ditt program måste innehålla ett potentiellt stort antal mindre komponenter och ansvarar för att skapa och köra dem.
Den enklaste metoden för problemet är att inkludera komponenterna som källkod i ditt program och anropa dem direkt från koden. Detta har ett antal uppenbara nackdelar. Viktigast av allt är att du inte kan lägga till nya komponenter utan att ändra källkoden, en begränsning som kan vara acceptabel i till exempel ett webbprogram, men som inte kan användas i ett klientprogram. Lika problematiskt är att du kanske inte har åtkomst till källkoden för komponenterna, eftersom de kan utvecklas av tredje part och av samma anledning kan du inte ge dem åtkomst till din.
En något mer sofistikerad metod skulle vara att tillhandahålla en tilläggspunkt eller ett gränssnitt för att tillåta frånkoppling mellan programmet och dess komponenter. Under den här modellen kan du tillhandahålla ett gränssnitt som en komponent kan implementera och ett API som gör att den kan interagera med ditt program. Detta löser problemet med att kräva källkodsåtkomst, men det har fortfarande sina egna svårigheter.
Eftersom programmet saknar kapacitet för att identifiera komponenter på egen hand måste det fortfarande uttryckligen meddelas vilka komponenter som är tillgängliga och bör läsas in. Detta åstadkoms vanligtvis genom att explicit registrera de tillgängliga komponenterna i en konfigurationsfil. Det innebär att säkerställa att komponenterna är korrekta blir ett underhållsproblem, särskilt om det är slutanvändaren och inte utvecklaren som förväntas utföra uppdateringen.
Dessutom är komponenterna oförmögna att kommunicera med varandra, förutom genom de strikt definierade kanalerna i själva programmet. Om programarkitekten inte har förutsett behovet av en viss kommunikation är det vanligtvis omöjligt.
Slutligen måste komponentutvecklare acceptera ett hårt beroende av vilken sammansättning som innehåller det gränssnitt som de implementerar. Detta gör det svårt för en komponent att användas i fler än ett program och kan också skapa problem när du skapar ett testramverk för komponenter.
Vad MEF tillhandahåller
I stället för den här explicita registreringen av tillgängliga komponenter ger MEF ett sätt att identifiera dem implicit via sammansättning. En MEF-komponent, som kallas en del, anger deklarativt både dess beroenden (kallas importer) och vilka funktioner (så kallade exporter) som den gör tillgängliga. När en del skapas uppfyller mef-kompositionsmotorn sin import med det som är tillgängligt från andra delar.
Den här metoden löser de problem som beskrivs i föregående avsnitt. Eftersom MEF-delar deklarativt anger sina funktioner kan de identifieras vid körning, vilket innebär att ett program kan använda delar utan antingen hårdkodade referenser eller ömtåliga konfigurationsfiler. MED MEF kan program identifiera och undersöka delar med hjälp av deras metadata, utan att instansiera dem eller ens läsa in deras sammansättningar. Därför behöver du inte noggrant ange när och hur tillägg ska läsas in.
Förutom den tillhandahållna exporten kan en del ange dess import, som kommer att fyllas av andra delar. Detta gör kommunikationen mellan delarna inte bara möjlig, utan enkel och möjliggör god kodfaktorisering. Tjänster som är gemensamma för många komponenter kan till exempel räknas in i en separat del och enkelt ändras eller ersättas.
Eftersom MEF-modellen inte kräver något hårt beroende av en viss programsammansättning kan tillägg återanvändas från program till program. Detta gör det också enkelt att utveckla ett testsele, oberoende av programmet, för att testa tilläggskomponenter.
Ett utökningsbart program som skrivits med hjälp av MEF deklarerar en import som kan fyllas i av tilläggskomponenter och kan även deklarera exporter för att göra programtjänster tillgängliga för tillägg. Varje tilläggskomponent deklarerar en export och kan även deklarera importer. På så sätt är själva tilläggskomponenterna automatiskt utökningsbara.
Där MEF är tillgängligt
MEF är en integrerad del av .NET Framework 4 och är tillgänglig överallt där .NET Framework används. Du kan använda MEF i dina klientprogram, oavsett om de använder Windows Forms, WPF eller någon annan teknik eller i serverprogram som använder ASP.NET.
MEF och MAF
Tidigare versioner av .NET Framework introducerade MAF (Managed Add-in Framework), som utformats för att tillåta program att isolera och hantera tillägg. MAF:s fokus är något högre nivå än MEF och fokuserar på tilläggsisolering och monteringsinläsning och avlastning, medan MEF:s fokus ligger på identifiering, utökningsbarhet och portabilitet. De två ramverken samverkar smidigt och ett enda program kan dra nytta av båda.
SimpleCalculator: Ett exempelprogram
Det enklaste sättet att se vad MEF kan göra är att skapa ett enkelt MEF-program. I det här exemplet skapar du en mycket enkel kalkylator med namnet SimpleCalculator. Målet med SimpleCalculator är att skapa ett konsolprogram som accepterar grundläggande aritmetiska kommandon i formatet "5+3" eller "6-2" och returnerar rätt svar. Med mef kan du lägga till nya operatorer utan att ändra programkoden.
Information om hur du laddar ned den fullständiga koden för det här exemplet finns i SimpleCalculator-exemplet (Visual Basic).
Anteckning
Syftet med SimpleCalculator är att demonstrera begrepp och syntax för MEF, i stället för att nödvändigtvis ge ett realistiskt scenario för dess användning. Många av de program som skulle dra mest nytta av kraften i MEF är mer komplexa än SimpleCalculator. Mer omfattande exempel finns i Managed Extensibility Framework på GitHub.
Börja med att skapa ett nytt konsolprogramprojekt i Visual Studio och ge det
SimpleCalculator
namnet .Lägg till en referens till
System.ComponentModel.Composition
sammansättningen, där MEF finns.Öppna Module1.vb eller Program.cs och lägg till
Imports
- eller -using
instruktioner förSystem.ComponentModel.Composition
ochSystem.ComponentModel.Composition.Hosting
. Dessa två namnområden innehåller MEF-typer som du behöver för att utveckla ett utökningsbart program.Om du använder Visual Basic lägger du till nyckelordet
Public
på raden som deklarerar modulenModule1
.
Sammansättningscontainer och kataloger
Kärnan i MEF-kompositionsmodellen är kompositionscontainern, som innehåller alla tillgängliga delar och utför komposition. Sammansättning är matchningen mellan import och export. Den vanligaste typen av kompositionscontainer är CompositionContainer, och du använder den för SimpleCalculator.
Om du använder Visual Basic lägger du till en offentlig klass med namnet Program
i Module1.vb.
Lägg till följande rad i Program
klassen i Module1.vb eller Program.cs:
Dim _container As CompositionContainer
private CompositionContainer _container;
För att identifiera de delar som är tillgängliga för den använder kompositionscontainrarna en katalog. En katalog är ett objekt som gör tillgängliga delar identifierade från någon källa. MEF tillhandahåller kataloger för att identifiera delar från en angivna typ, en sammansättning eller en katalog. Programutvecklare kan enkelt skapa nya kataloger för att identifiera delar från andra källor, till exempel en webbtjänst.
Lägg till följande konstruktor i Program
klassen :
Public Sub New()
' An aggregate catalog that combines multiple catalogs.
Dim catalog = New AggregateCatalog()
' Adds all the parts found in the same assembly as the Program class.
catalog.Catalogs.Add(New AssemblyCatalog(GetType(Program).Assembly))
' Create the CompositionContainer with the parts in the catalog.
_container = New CompositionContainer(catalog)
' Fill the imports of this object.
Try
_container.ComposeParts(Me)
Catch ex As CompositionException
Console.WriteLine(ex.ToString)
End Try
End Sub
private Program()
{
try
{
// An aggregate catalog that combines multiple catalogs.
var catalog = new AggregateCatalog();
// Adds all the parts found in the same assembly as the Program class.
catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly));
// Create the CompositionContainer with the parts in the catalog.
_container = new CompositionContainer(catalog);
_container.ComposeParts(this);
}
catch (CompositionException compositionException)
{
Console.WriteLine(compositionException.ToString());
}
}
Anropet till ComposeParts instruerar sammansättningscontainern att skapa en specifik uppsättning delar, i det här fallet den aktuella instansen av Program
. I det här läget kommer dock ingenting att hända, eftersom Program
det inte finns någon import att fylla.
Importerar och exporterar med attribut
Först importerar Program
du en kalkylator. Detta gör det möjligt att separera användargränssnittsproblem, till exempel konsolens indata och utdata som kommer att gå till Program
, från logiken i kalkylatorn.
Lägg till följande kod i Program
klassen :
<Import(GetType(ICalculator))>
Public Property calculator As ICalculator
[Import(typeof(ICalculator))]
public ICalculator calculator;
Observera att deklarationen calculator
av objektet inte är ovanlig, men att den är dekorerad med ImportAttribute attributet . Det här attributet deklarerar något som en import. Den fylls alltså av kompositionsmotorn när objektet består.
Varje import har ett kontrakt som avgör vilka exporter den ska matchas med. Kontraktet kan vara en uttryckligen angiven sträng eller genereras automatiskt av MEF från en viss typ, i det här fallet gränssnittet ICalculator
. Alla exporter som deklarerats med ett matchande kontrakt uppfyller den här importen. Observera att även om typen av calculator
objekt i själva verket ICalculator
är , är detta inte obligatoriskt. Kontraktet är oberoende av typen av importobjekt. (I det här fallet kan du utelämna typeof(ICalculator)
. MEF förutsätter automatiskt att kontraktet baseras på typen av import såvida du inte uttryckligen anger det.)
Lägg till det här enkla gränssnittet i modulen eller SimpleCalculator
namnområdet:
Public Interface ICalculator
Function Calculate(input As String) As String
End Interface
public interface ICalculator
{
string Calculate(string input);
}
Nu när du har definierat ICalculator
behöver du en klass som implementerar den. Lägg till följande klass i modulen eller SimpleCalculator
namnområdet:
<Export(GetType(ICalculator))>
Public Class MySimpleCalculator
Implements ICalculator
End Class
[Export(typeof(ICalculator))]
class MySimpleCalculator : ICalculator
{
}
Här är exporten som matchar importen i Program
. För att exporten ska matcha importen måste exporten ha samma kontrakt. Om du exporterar enligt ett kontrakt baserat på typeof(MySimpleCalculator)
skulle det uppstå ett matchningsfel och importen skulle inte fyllas i. Kontraktet måste matcha exakt.
Eftersom kompositionscontainern fylls med alla delar som är tillgängliga i den här sammansättningen blir MySimpleCalculator
delen tillgänglig. När konstruktorn för Program
utför sammansättning på objektet fylls dess import med ett MySimpleCalculator
-objekt som skapas för det ändamåletProgram
.
Användargränssnittsskiktet (Program
) behöver inte veta något annat. Du kan därför fylla i resten av användargränssnittslogik i Main
-metoden.
Lägg till följande kod i Main
-metoden:
Sub Main()
' Composition is performed in the constructor.
Dim p As New Program()
Dim s As String
Console.WriteLine("Enter Command:")
While (True)
s = Console.ReadLine()
Console.WriteLine(p.calculator.Calculate(s))
End While
End Sub
static void Main(string[] args)
{
// Composition is performed in the constructor.
var p = new Program();
Console.WriteLine("Enter Command:");
while (true)
{
string s = Console.ReadLine();
Console.WriteLine(p.calculator.Calculate(s));
}
}
Den här koden läser helt enkelt indataraden Calculate
och anropar funktionen ICalculator
för på resultatet, som den skriver tillbaka till konsolen. Det är all kod du behöver i Program
. Allt annat arbete kommer att ske i delarna.
Importer och ImportMany-attribut
För att SimpleCalculator ska vara utökningsbar måste den importera en lista över åtgärder. Ett vanligt ImportAttribute attribut fylls i med ett och bara ett ExportAttribute. Om det finns fler än en tillgänglig genererar kompositionsmotorn ett fel. Om du vill skapa en import som kan fyllas i med valfritt antal exporter kan du använda attributet ImportManyAttribute .
Lägg till följande åtgärdsegenskap i MySimpleCalculator
klassen:
<ImportMany()>
Public Property operations As IEnumerable(Of Lazy(Of IOperation, IOperationData))
[ImportMany]
IEnumerable<Lazy<IOperation, IOperationData>> operations;
Lazy<T,TMetadata> är en typ som tillhandahålls av MEF för att innehålla indirekta referenser till exporter. Förutom själva det exporterade objektet får du även exportmetadata eller information som beskriver det exporterade objektet. Var Lazy<T,TMetadata> och en innehåller ett IOperation
objekt som representerar en faktisk åtgärd och ett IOperationData
objekt som representerar dess metadata.
Lägg till följande enkla gränssnitt i modulen eller SimpleCalculator
namnområdet:
Public Interface IOperation
Function Operate(left As Integer, right As Integer) As Integer
End Interface
Public Interface IOperationData
ReadOnly Property Symbol As Char
End Interface
public interface IOperation
{
int Operate(int left, int right);
}
public interface IOperationData
{
char Symbol { get; }
}
I det här fallet är metadata för varje åtgärd den symbol som representerar den åtgärden, till exempel +, -, *och så vidare. Lägg till följande klass i modulen eller SimpleCalculator
namnområdet för att göra additionsåtgärden tillgänglig:
<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "+"c)>
Public Class Add
Implements IOperation
Public Function Operate(left As Integer, right As Integer) As Integer Implements IOperation.Operate
Return left + right
End Function
End Class
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '+')]
class Add: IOperation
{
public int Operate(int left, int right)
{
return left + right;
}
}
Attributet ExportAttribute fungerar som det gjorde tidigare. Attributet ExportMetadataAttribute bifogar metadata, i form av ett namn/värde-par, till exporten. Add
Klassen implementerar IOperation
, men en klass som implementerar definieras IOperationData
inte uttryckligen. I stället skapas en klass implicit av MEF med egenskaper baserat på namnen på de angivna metadata. (Det här är ett av flera sätt att komma åt metadata i MEF.)
Sammansättning i MEF är rekursiv. Du har uttryckligen skapat objektet Program
, som importerade en ICalculator
som visade sig vara av typen MySimpleCalculator
. MySimpleCalculator
importerar i sin tur en samling IOperation
objekt och den importen fylls i när MySimpleCalculator
skapas, samtidigt som importen av Program
. Om klassen deklarerade ytterligare en import måste även den Add
fyllas i och så vidare. En import som lämnas ofylld resulterar i ett kompositionsfel. (Det är dock möjligt att deklarera importer som valfria eller tilldela dem standardvärden.)
Kalkylatorlogik
Med dessa delar på plats är allt som återstår själva kalkylatorlogik. Lägg till följande kod i MySimpleCalculator
klassen för att implementera Calculate
metoden:
Public Function Calculate(input As String) As String Implements ICalculator.Calculate
Dim left, right As Integer
Dim operation As Char
' Finds the operator.
Dim fn = FindFirstNonDigit(input)
If fn < 0 Then
Return "Could not parse command."
End If
operation = input(fn)
Try
' Separate out the operands.
left = Integer.Parse(input.Substring(0, fn))
right = Integer.Parse(input.Substring(fn + 1))
Catch ex As Exception
Return "Could not parse command."
End Try
For Each i As Lazy(Of IOperation, IOperationData) In operations
If i.Metadata.symbol = operation Then
Return i.Value.Operate(left, right).ToString()
End If
Next
Return "Operation not found!"
End Function
public String Calculate(string input)
{
int left;
int right;
char operation;
// Finds the operator.
int fn = FindFirstNonDigit(input);
if (fn < 0) return "Could not parse command.";
try
{
// Separate out the operands.
left = int.Parse(input.Substring(0, fn));
right = int.Parse(input.Substring(fn + 1));
}
catch
{
return "Could not parse command.";
}
operation = input[fn];
foreach (Lazy<IOperation, IOperationData> i in operations)
{
if (i.Metadata.Symbol.Equals(operation))
{
return i.Value.Operate(left, right).ToString();
}
}
return "Operation Not Found!";
}
De första stegen parsar indatasträngen i vänster och höger operander och ett operatortecken. I loopen foreach
undersöks alla medlemmar i operations
samlingen. Dessa objekt är av typen Lazy<T,TMetadata>och deras metadatavärden och exporterade objekt kan nås med Metadata egenskapen Value respektive egenskapen. I det här fallet, om Symbol
egenskapen för IOperationData
objektet identifieras som en matchning, anropar Operate
kalkylatorn -metoden IOperation
för objektet och returnerar resultatet.
För att slutföra kalkylatorn behöver du också en hjälpmetod som returnerar positionen för det första icke-siffriga tecknet i en sträng. Lägg till följande hjälpmetod i MySimpleCalculator
klassen:
Private Function FindFirstNonDigit(s As String) As Integer
For i = 0 To s.Length - 1
If Not Char.IsDigit(s(i)) Then Return i
Next
Return -1
End Function
private int FindFirstNonDigit(string s)
{
for (int i = 0; i < s.Length; i++)
{
if (!char.IsDigit(s[i])) return i;
}
return -1;
}
Nu bör du kunna kompilera och köra projektet. I Visual Basic kontrollerar du att du har lagt till nyckelordet i Public
Module1
. I konsolfönstret skriver du en tilläggsåtgärd, till exempel "5+3", och kalkylatorn returnerar resultatet. Andra operatorer resulterar i meddelandet "Åtgärden hittades inte!".
Utöka SimpleCalculator med en ny klass
Nu när kalkylatorn fungerar är det enkelt att lägga till en ny åtgärd. Lägg till följande klass i modulen eller SimpleCalculator
namnområdet:
<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "-"c)>
Public Class Subtract
Implements IOperation
Public Function Operate(left As Integer, right As Integer) As Integer Implements IOperation.Operate
Return left - right
End Function
End Class
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '-')]
class Subtract : IOperation
{
public int Operate(int left, int right)
{
return left - right;
}
}
Kompilera och kör projektet. Skriv en subtraktionsåtgärd, till exempel "5–3". Kalkylatorn stöder nu både subtraktion och addition.
Utöka SimpleCalculator med en ny sammansättning
Det är enkelt att lägga till klasser i källkoden, men MEF ger möjlighet att titta utanför ett programs egen källa för delar. För att demonstrera detta måste du ändra SimpleCalculator för att söka i en katalog, samt dess egen sammansättning, för delar, genom att lägga till en DirectoryCatalog.
Lägg till en ny katalog med namnet Extensions
i SimpleCalculator-projektet. Se till att lägga till den på projektnivå och inte på lösningsnivå. Lägg sedan till ett nytt klassbiblioteksprojekt i lösningen med namnet ExtendedOperations
. Det nya projektet kompileras till en separat sammansättning.
Öppna Project Properties Designer för Projektet ExtendedOperations och klicka på fliken Kompilera eller Skapa. Ändra utdatasökvägen för Build eller Utdata så att den pekar på katalogen Tillägg i projektkatalogen SimpleCalculator (.. \SimpleCalculator\Extensions\).
I Modul1.vb eller Program.cs lägger du till följande rad i Program
konstruktorn:
catalog.Catalogs.Add(
New DirectoryCatalog(
"C:\SimpleCalculator\SimpleCalculator\Extensions"))
catalog.Catalogs.Add(
new DirectoryCatalog(
"C:\\SimpleCalculator\\SimpleCalculator\\Extensions"));
Ersätt exempelsökvägen med sökvägen till katalogen Tillägg. (Den här absoluta sökvägen är endast avsedd för felsökning. I ett produktionsprogram använder du en relativ sökväg.) DirectoryCatalog Lägger nu till alla delar som finns i alla sammansättningar i katalogen Tillägg i kompositionscontainern.
I projektet ExtendedOperations lägger du till referenser till SimpleCalculator och System.ComponentModel.Composition. I klassen ExtendedOperations lägger du till en Imports
-instruktion för using
System.ComponentModel.Composition. I Visual Basic lägger du också till en Imports
instruktion för SimpleCalculator. Lägg sedan till följande klass i extendedoperations-klassfilen:
<Export(GetType(SimpleCalculator.IOperation))>
<ExportMetadata("Symbol", "%"c)>
Public Class Modulo
Implements IOperation
Public Function Operate(left As Integer, right As Integer) As Integer Implements IOperation.Operate
Return left Mod right
End Function
End Class
[Export(typeof(SimpleCalculator.IOperation))]
[ExportMetadata("Symbol", '%')]
public class Mod : SimpleCalculator.IOperation
{
public int Operate(int left, int right)
{
return left % right;
}
}
Observera att för att kontraktet ska matcha ExportAttribute måste attributet ha samma typ som ImportAttribute.
Kompilera och kör projektet. Testa den nya Mod-operatorn (%).
Slutsats
Det här avsnittet beskriver de grundläggande begreppen i MEF.
Delar, kataloger och sammansättningscontainern
Delar och kompositionscontainern är de grundläggande byggstenarna i ett MEF-program. En del är ett objekt som importerar eller exporterar ett värde, upp till och med sig självt. En katalog innehåller en samling delar från en viss källa. Kompositionscontainern använder de delar som tillhandahålls av en katalog för att utföra sammansättning, bindningen av importer till exporter.
Import och export
Import och export är det sätt på vilket komponenter kommunicerar. Med en import anger komponenten ett behov av ett visst värde eller objekt, och med en export anger den tillgängligheten för ett värde. Varje import matchas med en lista över exporter genom sitt kontrakt.
Nästa steg
Information om hur du laddar ned den fullständiga koden för det här exemplet finns i SimpleCalculator-exemplet (Visual Basic).
Mer information och kodexempel finns i Managed Extensibility Framework. En lista över MEF-typerna finns i System.ComponentModel.Composition namnområdet.