共用方式為


管理擴展性架構 (MEF)

本文提供 .NET Framework 4 中引進的Managed Extensibility Framework 概觀。

什麼是MEF?

Managed Extensibility Framework (MEF) 是一個連結庫,用於建立輕量型且可延伸的應用程式。 它可讓應用程式開發人員探索並使用擴充功能,而不需要任何設定。 它也可讓延伸模塊開發人員輕鬆地封裝程式代碼,並避免脆弱的硬式相依性。 MEF 不僅允許在應用程式內重複使用擴充功能,也可跨應用程式重複使用。

擴充性的問題

假設您是必須支援擴充性的大型應用程式架構設計人員。 您的應用程式需要包含可能大量的較小元件,並負責創建和運行這些元件。

問題最簡單的方法是在應用程式中包含元件作為原始程式碼,並直接從您的程式代碼呼叫它們。 這有一些明顯的缺點。 最重要的是,您無法在沒有修改原始程式碼的情況下加入新的元件,例如 Web 應用程式中可接受的限制,但在用戶端應用程式中無法運作。 同樣有問題,您可能無法存取元件的原始程式碼,因為它們可能由第三方開發,因此您無法允許它們存取您的原始程式碼。

稍微複雜的方法是提供延伸點或介面,以允許應用程式與其元件之間的分離。 在此模型中,您可能會提供元件可以實作的介面,以及讓其能夠與應用程式互動的 API。 這解決了需要原始程式碼存取的問題,但仍有自己的困難。

因為應用程式本身沒有探索元件的任何容量,所以它仍然必須明確告知哪些元件可供使用,而且應該載入。 這通常是藉由在組態檔中明確註冊可用的元件來完成。 這表示確保元件正確無誤會成為維護問題,特別是當預期進行更新的是使用者而不是開發人員時。

此外,元件無法彼此通訊,除非透過應用程式本身嚴格定義的通道。 如果應用程式架構設計人員未預期需要特定通訊,通常是不可能的。

最後,元件開發人員必須接受在組件中包含實作介面的硬性相依性。 這會使元件難以用於多個應用程式,而且當您為元件建立測試架構時也會建立問題。

MEF 提供的內容

MEF 提供了一種透過 組合隱含探索可用元件的方式,而不是明確註冊它們。 稱為 部分的 MEF 元件會以宣告方式指定其相依性(稱為 匯入),以及它提供的功能(稱為 匯出)。 建立元件時,MEF 組合引擎會利用其他元件可供使用的資源來提供其需要的依賴項。

此方法可解決上一節所討論的問題。 因為 MEF 元件會以宣告方式指定其功能,所以可以在運行時間探索它們,這表示應用程式可以使用元件,而不需要硬式編碼參考或脆弱的元件檔。 MEF 允許應用程式透過中繼資料來探索和檢視部件,而不需要實例化它們,甚至不需載入其程序集。 因此,不需要仔細指定何時及如何載入延伸模組。

除了提供的匯出功能之外,元件還可以指定其匯入需求,並由其他元件來填充這些匯入。 這不僅可讓元件之間的通訊變得可行,而且很容易,而且能夠很好地分解程序代碼。 例如,許多元件通用的服務可以分解成個別的元件,並輕鬆地修改或取代。

因為 MEF 模型不需要對特定應用程式元件的緊密相依性,所以它可讓擴充模組在不同應用程式之間重複使用。 這也可讓您輕鬆地開發與應用程式無關的測試控管,以測試擴充元件。

使用MEF撰寫的可延伸應用程式會宣告可由延伸模組元件填入的匯入,也可以宣告匯出,以便將應用程式服務公開至延伸模組。 每個擴充元件都會宣告匯出,也可以宣告匯入。 如此一來,擴充元件本身就會自動擴充。

MEF 可用處

MEF 是 .NET Framework 4 的重要組成部分,並且在任何使用 .NET Framework 的地方都可以使用。 您可以在用戶端應用程式中使用MEF,無論是使用Windows Forms、WPF或任何其他技術,或是使用 ASP.NET 的伺服器應用程式中。

MEF 和MAF

舊版 .NET Framework 引入了受管理的附加元件框架(MAF),其設計目的是讓應用程式隔離和管理擴充功能。 MAF 的重點略高於MEF,著重於延伸模組隔離和元件載入和卸除,而MEF的重點在於可探索性、擴充性和可移植性。 這兩個架構可順暢地互通,而單一應用程式可以利用這兩者。

SimpleCalculator:範例應用程式

查看MEF可以執行的最簡單的方式,就是建置簡單的MEF應用程式。 在此範例中,您會建置一個非常簡單的計算機,名為 SimpleCalculator。 SimpleCalculator 的目標是建立可接受基本算術命令的控制台應用程式,格式為 “5+3” 或 “6-2”,並傳回正確的答案。 使用MEF,您將能夠在不變更應用程式程式代碼的情況下新增運算符。

若要下載此範例的完整程式碼,請參閱 SimpleCalculator 範例 (Visual Basic)

備註

SimpleCalculator 的目的是要示範 MEF 的概念和語法,而不一定提供實際案例供其使用。 許多受益於MEF功能的應用程式比SimpleCalculator更複雜。 如需更廣泛的範例,請參閱 GitHub 上的 受控擴充性架構

  • 若要開始,請在 Visual Studio 中建立新的控制台應用程式專案,並將它命名為 SimpleCalculator

  • 請將參考新增至 MEF 所在的組件 System.ComponentModel.Composition

  • 開啟 Module1.vbProgram.cs,並新增 Importsusing 的指示詞來設置 System.ComponentModel.CompositionSystem.ComponentModel.Composition.Hosting。 這兩個命名空間包含您需要開發可延伸應用程式的MEF類型。

  • 如果您使用 Visual Basic,請將 Public 關鍵詞新增至宣告模組的 Module1 行。

組合容器和目錄

MEF 組合模型的核心是 組合容器,其中包含所有可用元件並執行組合。 組合是匯入與導出的比對。 最常見的組合容器類型是 CompositionContainer,您將針對 SimpleCalculator 使用此類型。

如果您使用 Visual Basic,請在 Program中新增名為 的公用類別。

將下列這一行新增至 ProgramModule1.vbProgram.cs 中的 類別:

Dim _container As CompositionContainer
private CompositionContainer _container;

為了探索其可用的元件,組合容器會使用 目錄。 目錄是一個對象,用於提供從某些來源探索到的元件。 MEF 提供目錄,以探索由提供的類型、組件或目錄中的零件。 應用程式開發人員可以輕鬆地建立新的目錄,以探索來自其他來源的元件,例如 Web 服務。

將下列建構函式新增至 Program 類別:

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

ComposeParts 的呼叫告知組合容器去組合一組特定的元件,在此案例中是目前的 Program 實例。 不過,此時不會發生任何情況,因為 Program 沒有要填滿的匯入。

包含屬性的匯入和匯出

首先,您需要Program 導入計算機。 這可讓使用者介面的問題區隔開,例如控制台輸入和輸出會進入Program,與計算機的邏輯分開處理。

將下列程式碼新增至 Program 類別:

<Import(GetType(ICalculator))>
Public Property calculator As ICalculator
[Import(typeof(ICalculator))]
public ICalculator calculator;

請注意,calculator 物件的宣告並不罕見,但它是由 ImportAttribute 屬性裝飾的。 這個屬性宣告某物為匯入項目;也就是說,當物件被組合時,會由組合引擎填補。

每個輸入都有一個合約,決定與之匹配的輸出。 合約可以是明確指定的字串,或可由MEF從指定型別自動產生,在此案例中為介面 ICalculator。 與合約相符的任何宣告的出口都將符合此匯入的要求。 請注意,雖然 對象的類型 calculator 實際上是 ICalculator,但這並非必要。 合約與匯入物件的類型無關。 (在此情況下,您可以省略 typeof(ICalculator)。除非您明確指定合約,否則MEF會自動假設合約是以匯入的類型為基礎。

將這個非常簡單的介面新增至模組或 SimpleCalculator 命名空間:

Public Interface ICalculator
    Function Calculate(input As String) As String
End Interface
public interface ICalculator
{
    string Calculate(string input);
}

現在您已定義 ICalculator,您需要實作它的類別。 將下列類別新增至模組或 SimpleCalculator 命名空間:

<Export(GetType(ICalculator))>
Public Class MySimpleCalculator
   Implements ICalculator

End Class
[Export(typeof(ICalculator))]
class MySimpleCalculator : ICalculator
{

}

以下是符合 中 Program匯入的匯出。 為了讓出口符合進口,出口必須有相同的合約。 根據 typeof(MySimpleCalculator) 合約匯出會產生不相符的情況,匯入將無法完成。合約必須完全相符。

由於組合容器會填入此元件中所有可用的元件,因此 MySimpleCalculator 元件將可供使用。 當 Program 的建構函式在 Program 對象上執行組合時,其匯入會被填入一個 MySimpleCalculator 物件,而這個物件將特別為此被建立。

使用者介面層 (Program) 不需要知道任何其他事情。 因此,您可以在方法中 Main 填入其餘的使用者介面邏輯。

將下列程式碼加入 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));
    }
}

此程式代碼只會讀取一行輸入,並在結果上呼叫 CalculateICalculator函式,然後將結果輸出至主控台。 這就是你在Program中所需的所有代碼。 其餘所有工作都會在元件中發生。

Imports 和 ImportMany 屬性

為了讓 SimpleCalculator 能夠擴充,它必須匯入作業清單。 一般 ImportAttribute 屬性只有唯一的一個 ExportAttribute 會被填入。 如果有多個可用的,組合引擎會產生錯誤。 為了建立可以由任意匯出數量填充的匯入,您可以使用 ImportManyAttribute 屬性。

將下列 operations 屬性新增至 MySimpleCalculator 類別:

<ImportMany()>
Public Property operations As IEnumerable(Of Lazy(Of IOperation, IOperationData))
[ImportMany]
IEnumerable<Lazy<IOperation, IOperationData>> operations;

Lazy<T,TMetadata> 是 MEF 提供的型別,用來保存導出的間接引用。 在這裡,除了導出的物件本身之外,您也會取得 匯出元數據或描述導出物件的資訊。 每個 Lazy<T,TMetadata> 物件都包含 IOperation 物件,代表實際作業,以及 IOperationData 代表其元數據的物件。

將下列簡單介面新增至模組或 SimpleCalculator 命名空間:

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

在此情況下,每個作業的元數據都是代表該作業的符號,例如 +、-、*等等。 若要讓新增作業可供使用,請將下列類別新增至模組或 SimpleCalculator 命名空間:

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

屬性 ExportAttribute 的運作方式與之前一樣。 ExportMetadataAttribute 屬性會以名稱-值對的形式將元數據附加至該匯出。 Add 類別雖然實作了 IOperation,但卻沒有明確定義實作 IOperationData 的類別。 相反地,MEF 會根據提供的元數據名稱,以屬性隱含建立類別。 (這是在MEF中存取元數據的幾種方式之一。

MEF 中的組合是 遞歸的。 您已明確撰寫 Program 物件,該物件匯入的 ICalculator 是型別 MySimpleCalculatorMySimpleCalculator 接著匯入 IOperation 物件的集合,當建立 MySimpleCalculator 時會填入這些匯入,並與 Program 的匯入同時進行。 如果類別 Add 宣告了進一步的匯入,那麼也必須填入,以此類推。 任何未填入的匯入都會導致組合錯誤。 不過,可以宣告匯入為選擇性或指派預設值。

計算機邏輯

有了這些部分,剩下的就是計算機邏輯本身。 在 MySimpleCalculator 類別中新增下列程式代碼以實作 Calculate 方法:

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

初始步驟會將輸入字串剖析為左右作數和運算符字元。 在foreach迴圈中,會檢查operations集合的每個成員。 這些物件的類型為 Lazy<T,TMetadata>,而且其元數據值和導出的物件可以分別使用 Metadata 屬性和 Value 屬性來存取。 在這種情況下,如果發現物件 Symbol 的屬性 IOperationData 相符,計算程式會呼叫 Operate 物件的方法 IOperation,並傳回結果。

若要完成計算機,您也需要協助程式方法,這個方法會傳回字串中第一個非數位字元的位置。 將下列協助程式方法新增至 MySimpleCalculator 類別:

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

您現在應該能夠編譯並執行專案。 在 Visual Basic 中,確定您已將 關鍵字新增 PublicModule1。 在控制台視窗中,輸入加法作業,例如「5+3」,然後計算機會顯示結果。 任何其他運算子都會導致「操作未找到」訊息。

使用新類別擴充 SimpleCalculator

現在計算機運作正常,新增作業很簡單。 將下列類別新增至模組或 SimpleCalculator 命名空間:

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

編譯並執行專案。 輸入減法運算,例如 「5-3」。。 計算機現在支持減法和加法。

以新的組件來擴充 SimpleCalculator

將類別新增至原始程式碼相當簡單,但MEF提供了尋找應用程式自身來源以外组件的功能。 若要示範這一點,您必須新增 DirectoryCatalog 來修改 SimpleCalculator,使其能搜尋資料夾及自身的元件。

將名為 Extensions 的新目錄新增至 SimpleCalculator 專案。 請務必將它新增至專案層級,而不是在方案層級。 然後將新的類別庫專案新增至名為 ExtendedOperations的方案。 新的專案會編譯成一個單獨的組件。

開啟 ExtendedOperations 專案的 [專案屬性設計工具],然後按兩下 [ 編譯 ] 或 [ 置] 索引標籤。變更 [建置輸出路徑 ] 或 [ 輸出路徑 ] 以指向 SimpleCalculator 項目目錄中的 Extensions 目錄 。。\SimpleCalculator\Extensions\)。

Module1.vbProgram.cs中,將下列這一行新增至建 Program 構函式:

catalog.Catalogs.Add(
    New DirectoryCatalog(
        "C:\SimpleCalculator\SimpleCalculator\Extensions"))
catalog.Catalogs.Add(
    new DirectoryCatalog(
        "C:\\SimpleCalculator\\SimpleCalculator\\Extensions"));

將範例路徑取代為延伸模組目錄的路徑。 (此絕對路徑僅供偵錯之用。在生產應用程式中,您會使用相對路徑。 DirectoryCatalog 現在會將在 Extensions 目錄中任何元件中找到的任何元件新增至組合容器。

ExtendedOperations專案中,新增對SimpleCalculatorSystem.ComponentModel.Composition的參考。 在ExtendedOperations類別檔案中,為Imports新增usingSystem.ComponentModel.Composition指令。 在 Visual Basic 中,也新增 ImportsSimpleCalculator語句。 然後將下列類別新增至 ExtendedOperations 類別檔案:

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

請注意,為了讓合約相符, ExportAttribute 屬性的類型必須與 相同 ImportAttribute

編譯並執行專案。 測試新的 Mod(%)運算子。

結論

本主題涵蓋MEF的基本概念。

  • 元件、目錄和組合容器

    元件和組合容器是MEF應用程式的基本建置元件。 元件是匯入或匯出值的任何物件,最多可包含其本身。 目錄提供特定來源的元件集合。 組合容器會使用目錄所提供的元件來執行組合、匯入至導出的系結。

  • 匯入和匯出

    匯入和匯出是元件通訊的方式。 使用匯入時,元件會指定特定值或物件的需求,並使用導出來指定值的可用性。 每個匯入會根據其合約與匯出清單進行匹配。

後續步驟

若要下載此範例的完整程式碼,請參閱 SimpleCalculator 範例 (Visual Basic)

如需詳細資訊和程式代碼範例,請參閱 Managed Extensibility Framework。 如需MEF類型清單,請參閱 System.ComponentModel.Composition 命名空間。