Dela via


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 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 instruktioner 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 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 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. 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. MySimpleCalculatorimporterar 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 PublicModule1. 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.