Dela via


Hanterad utökningsbarhetsramverk (MEF)

Den här artikeln innehåller en översikt över Managed Extensibility Framework som introducerades i .NET Framework 4.

Vad är MEF?

Managed Extensibility Framework (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 bräckliga hårda beroenden. MEF tillåter inte bara att tillägg återanvänds i program, utan även mellan program.

Problemet med utökningsbarhet

Anta att du är arkitekt för ett stort program som måste ge stöd för utökningsbarhet. Programmet 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 tillåta dem att komma åt din.

En något mer sofistikerad metod skulle vara att tillhandahålla en tilläggspunkt eller ett gränssnitt för att tillåta avkoppling 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 för att göra det möjligt för den att 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 anges vilka komponenter som är tillgängliga och bör läsas in. Detta görs vanligtvis genom att uttryckligen 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 göra uppdateringen.

Dessutom kan komponenter inte kommunicera med varandra, förutom genom själva programmets strikt definierade kanaler. Om programarkitekten inte har förutsett behovet av en viss kommunikation är det vanligtvis omöjligt.

Slutligen måste komponentutvecklarna 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 mer ä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 komposition. En MEF-komponent, som kallas en del, anger deklarativt både dess beroenden (kallas importer) och vilka funktioner (kallas exporter) som den gör tillgängliga. När en del skapas uppfyller MEF-kompositionsmotorn importkraven med hjälp av 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 bräckliga konfigurationsfiler. MED MEF kan program identifiera och undersöka delar med sina 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 de tillhandahållna exporterna kan en del ange sina importer, som kommer att fyllas av andra delar. Detta gör kommunikationen mellan delar inte bara möjlig, utan enkel och möjliggör bra kodfaktorisering. Till exempel kan tjänster som är gemensamma för många komponenter 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 en testsel, 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 med 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 oavsett var .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 koncentrerar sig på isolation av tillägg och inläsning och lossning av sammanställningar, medan MEF:s fokus ligger på upptäckbarhet, 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 hjälp av 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).

Anmärkning

Syftet med SimpleCalculator är att demonstrera mef-begreppen och syntaxen 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 ha störst 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 SimpleCalculatornamnet .

  • 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 direktiv för System.ComponentModel.Composition och System.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 modulen Module1 .

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 av importen till exporten. 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 angivet 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 kompositionscontainern 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ågra importer att fylla.

Importer och exporter med attribut

Först ska du Program importera en kalkylator. Detta gör det möjligt att separera användargränssnittsproblem, till exempel konsolens indata och utdata som ska 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; det vill säga att det kommer att fyllas av kompositionsmotorn när objektet komponeras.

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. All export som deklareras med ett matchande kontrakt kommer att uppfylla den här importen. Observera att även om objektets calculator typ 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 om du inte uttryckligen anger det.)

Lägg till det här mycket 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 ICalculatorbehö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. Att exportera enligt ett kontrakt baserat på typeof(MySimpleCalculator) skulle ge ett matchningsfel och importen skulle inte fyllas i. Kontraktet måste matcha exakt.

Eftersom kompositionscontainern fylls i med alla tillgängliga delar i den här sammansättningen blir MySimpleCalculator delen tillgänglig. När konstruktorn för Program utför sammansättning på Program objektet, fylls dess import med ett MySimpleCalculator objekt som skapas för det ändamålet.

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 metoden Main:

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 en rad med indata och anropar Calculate funktionen av ICalculator på resultatet, som den skriver tillbaka till konsolen. Det är all kod du behöver i Program. Allt resten av arbetet 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 med ett och bara ett ExportAttribute. Om fler än en är 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. Varje Lazy<T,TMetadata> innehåller ett IOperation objekt som representerar en faktisk operation 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 å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 den exporten. Add-klassen implementerar IOperation, men en klass som implementerar IOperationData definieras 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 ett ICalculator objekt 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. Add Om klassen deklarerade ytterligare en import måste även den fyllas i och så vidare. All 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 varje medlem i operations samlingen. Dessa objekt är av typen Lazy<T,TMetadata>, och deras metadatavärden och exporterade objekt kan nås med Metadata egenskapen respektive Value 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 PublicModule1. I konsolfönstret skriver du en additionsåtgärd, till exempel "5+3", och kalkylatorn returnerar resultatet. Andra operatorer resulterar i meddelandet "Åtgärden hittades inte!".

Utöka SimpleCalculator med hjälp av 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 hjälp av 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 mapp, samt i dess egen programsamling, efter 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 projektegenskaperdesignern för ExtendedOperations-projektet och klicka på fliken Kompilera eller Skapa . Ändra utdatasökvägen eller utdatasökvägen så att den pekar på katalogen Extensions i projektkatalogen SimpleCalculator (.. \SimpleCalculator\Extensions\).

Lägg till följande rad i konstruktorn i Module1.vb eller Program:

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 din tilläggskatalog. (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 Extensions i kompositionscontainern.

ExtendedOperations I projektet lägger du till referenser till SimpleCalculator och System.ComponentModel.Composition. ExtendedOperations I klassfilen lägger du till ett Imports eller ett using direktiv för 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 Operatorn Mod (%).

Slutsats

Det här avsnittet beskriver de grundläggande begreppen i MEF.

  • Delar, kataloger och kompositionscontainern

    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 inklusive 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 export.

  • 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 via dess 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.