Managed Extensibility Framework 概觀

更新:2010 年 7 月

本主題提供 .NET Framework 4 引進之 Managed Extensibility Framework 的概觀。

這個主題包含下列章節。

  • 何謂 MEF?
  • 擴充性問題
  • MEF 提供的內容
  • 在哪裡可以使用 MEF?
  • MEF 和 MAF
  • SimpleCalculator:應用程式範例
  • 組合容器和目錄
  • 具有屬性的匯入項目和匯出項目
  • 進一步匯入和 ImportMany
  • 計算機邏輯
  • 使用新類別擴充 SimpleCalculator
  • 使用新的組件擴充 SimpleCalculator
  • 結論
  • 取得和學習進階資訊

何謂 MEF?

Managed Extensibility Framework 或 MEF 是用於建立輕量型可擴充應用程式的程式庫。 它可讓應用程式開發人員無需任何設定,即可探索並使用擴充功能。 它也可讓擴充功能開發人員輕易地封裝程式碼並避免出現不牢固的硬式相依性。 透過 MEF,不僅可在應用程式內重複使用擴充功能,也可跨應用程式重複使用擴充功能。

擴充性問題

假設您是應用程式的架構設計人員,需要設計必須提供擴充性支援的大型應用程式。 您的應用程式必須納入數目可能很多的較小型元件,並負責建立和執行它們。

解決該問題的最簡單方法是將元件以原始程式碼納入應用程式,並直接從透過程式碼對其進行呼叫。 但此方法具有一些明顯的缺點。 其中最重要的一點是,不修改原始程式碼就無法加入新的元件,這一限制在 Web 應用程式中還可接受,但在用戶端應用程式中卻行不通。 同樣的問題還有您可能無法存取元件的原始程式碼,原因是這些程式碼可能是由協力廠商所開發,而基於同樣的原因,您也不能允許協力廠商來存取您的原始程式碼。

稍微複雜一點的方法是提供擴充點或介面,以允許解除應用程式與其元件之間的結合。 在此模型下,您可能要提供一個元件可以實作的介面和一個 API,讓其可以與您的應用程式互動。 如此便解決了需要原始程式碼存取權限的問題,但是該方法本身仍存在一些難點。

由於應用程式缺少自我探索元件的功能,因此必須明確地告知應用程式可以使用哪些元件和應該載入哪些元件。 要達成這一作業,通常需要在組態檔中明確地註冊可用的元件。 這意味著需要對元件進行維護以確保其正確性,尤其對使用者來說會是一個問題,畢竟使用者不比預期進行更新的開發人員。

此外,除了透過應用程式本身嚴格定義的通道之外,元件無法彼此進行通訊。 如果應用程式架構設計人員沒有特定的通訊需求,則通常無法實現。

最後,元件開發人員必須接受包含於組件中其所實作介面的硬式相依性。 如此會造成難以在多個應用程式中使用一個元件的問題,也會在您建立元件的測試架構時產生問題。

MEF 提供的內容

除了明確註冊可用元件之外,MEF 還會提供透過「組合」(Composition) 進行隱含探索的方式。 MEF 元件 (稱為「組件」(Part)) 會同時以宣告方式指定其相依性 (稱為「匯入」(Import)) 和可用的功能 (稱為「匯出」(Export))。 建立組件時,MEF 組合引擎會利用其他組件中的可用內容來滿足其匯入項目。

這一方法會解決上一節中所討論的問題。 由於 MEF 組件會以宣告方式指定其功能,因此可在執行階段對其進行探索,這意味著應用程式可在不使用硬式編碼參考或易損組態檔的情況下,使用組件。 MEF 可讓應用程式依中繼資料來探索和檢查組件 (Part),而不會將它們執行個體化,甚至不會載入其組件 (Assembly)。 因此,不必謹慎地指定應載入擴充功能的時間和方式。

除了提供的匯出項目以外,組件可以指定其匯入項目,這將由其他組件來填寫。 如此不但允許在組件之間進行通訊,而且會讓通訊變得更加容易,同時也允許更好地建構程式碼。 例如,可以將許多元件中常見的服務納入個別的組件,這樣可以更容易地進行修改或取代。

由於 MEF 模型不需要對特定應用程式組件具有硬式相依性,因此允許在應用程式之間重複使用擴充功能。 這樣也易於開發與應用程式無關的測試控管,來測試擴充功能元件。

使用 MEF 所組合的可擴充應用程式會宣告可由擴充功能元件填寫的匯入項目,同時也可能宣告匯出項目,以將應用程式服務公開給擴充功能。 每個擴充功能元件都會宣告匯出項目,同時可能也會宣告匯入項目。 如此一來,擴充功能元件本身就會自動擴充。

在哪裡可以使用 MEF?

MEF 是 .NET Framework 4 不可或缺的部分,可在使用 .NET Framework 的地方找到。 您可以在用戶端應用程式中使用 MEF,而無論其是使用 Windows Forms、WPF 還是任何其他技術,也可以在使用 ASP.NET 的伺服器應用程式中使用 MEF。

MEF 和 MAF

舊版 .NET Framework 引進了 Managed Add-in Framework (MAF),旨在允許應用程式隔離和管理擴充功能。 MAF 的側重點要略高於 MEF,前者將注意力集中在擴充功能隔離和組件載入與卸載上,而 MEF 的側重點在於探索性、擴充性和可移植性。 這兩種架構可順暢地進行相互操作,單一應用程式即可同時利用這二者。

SimpleCalculator:應用程式範例

查看 MEF 可進行何種操作的最簡單方式是建置簡單的 MEF 應用程式。 在這個範例中,您會建置非常簡單的計算機,名為 SimpleCalculator。 SimpleCalculator 的目標是建立一個主控台應用程式,其接受形式為 "5+3" 或 "6-2" 的基本算術運算命令,並傳回正確的答案。 使用 MEF,您將可以加入新的運算子,而無需變更應用程式程式碼。

若要下載這個範例的完整程式碼,請參閱 SimpleCalculator 範例 (英文)。

注意事項注意事項

SimpleCalculator 的目的在於示範 MEF 的概念和語法,而不一定要提供其用法的實際案例。許多得益於 MEF 功能的應用程式都要比 SimpleCalculator 複雜。如需大量範例的詳細資訊,請參閱 Codeplex 上的 Managed Extensibility Framework (英文)。

若要開始進行,請在 Visual Studio 2010 中建立名為 SimpleCalculator 的新主控台應用程是專案。 加入 MEF 所在之 System.ComponentModel.Composition 組件的參考。 開啟 Module1.vb 或 Program.cs,並針對 System.ComponentModel.Composition 和 System.ComponentModel.Composition.Hosting 加入 Imports 或 using 陳述式。 這兩個命名空間包含您開發可擴充應用程式所需的 MEF 類型。 在 Visual Basic 中,將 Public 關鍵字加入至宣告 Module1 的那一行。

組合容器和目錄

MEF 組合模型的核心是「組合容器」(Composition Container),其包含所有可用的組件並執行組合。 (也就是比對匯入項目與匯出項目)。最常見的組合容器型別為 CompositionContainer,且您會將此型別用於 SimpleCalculator。

在 Visual Basic 中,於 Module1.vb 中加入名為 Program 的公用類別。 然後將下列一行程式碼加入至 Module1.vb 或 Program.cs 中的 Program 類別:

Dim _container As CompositionContainer
private CompositionContainer _container;

為了探索可用的組件,組合容器會利用「目錄」(Catalog)。 透過目錄物件,可以從部分來源探索到可用組件。 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 Exception
        Console.WriteLine(ex.ToString)
    End Try
End Sub
private Program()
{
    //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);

    //Fill the imports of this object
    try
    {
        this._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 屬性進行裝飾則很不尋常。 此屬性宣告某項目為匯入項目,也就是說,組合物件時會由組合引擎來填入該項目。

每個匯入項目都有一個「合約」(Contract),其會決定要進行比對的匯出項目。 合約可以是明確指定的字串,或者可由 MEF 透過指定的型別自動產生,在此案例中為介面 ICalculator。 利用相符合約宣告的任何匯出項目都滿足此匯入項目。 請注意,calculator 物件的型別實際上是 ICalculator,但並非必要。 合約與匯入物件的型別無關 (在這種情形下,您可以省略 typeof(ICalculator)。 MEF 會自動讓合約採用基於匯入的型別,除非您另已明確指定型別)。

將此極簡易的介面加入至模組或 SimpleCalculator 命名空間:

Public Interface ICalculator
    Function Calculate(ByVal 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) 的合約下進行的匯出將會產生不相符的情況,並且不會填入匯入項目;合約必須完全相符。

由於組合容器中會填入此組件 (Assembly) 中所有可用的組件 (Part),因此可以使用 MySimpleCalculator 組件 (Part)。 當 Program 的建構函式在 Program 物件上執行組合時,其匯入項目會由針對匯入目的而建立的 MySimpleCalculator 物件來填入。

使用者介面層 (Program) 不需要了解其他內容。 因此,您可以在 Main 方法中填入剩餘的使用者介面邏輯。

將下列程式碼加入至 Main 方法中:

Sub Main()
    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)
{
    Program p = new Program(); //Composition is performed in the constructor
    String s;
    Console.WriteLine("Enter Command:");
    while (true)
    {
        s = Console.ReadLine();
        Console.WriteLine(p.calculator.Calculate(s));
    }
}

此程式碼只是讀取一行輸入,接著對它寫回主控台的結果,呼叫 ICalculator 的 Calculate 函式。 這就是您在 Program 中所需的所有程式碼。 剩餘工作將在組件中發生。

進一步匯入和 ImportMany

為了讓 SimpleCalculator 可擴充,需要匯入作業清單。 一個一般 ImportAttribute 屬性可由且僅可由一個 ExportAttribute 填入。 如果填入多個,則組合引擎會產生錯誤。 若要建立可由任意數目匯出項目來填入的匯入項目,您可以使用 ImportManyAttribute 屬性。

將下列運算屬性加入至 MySimpleCalculator 類別:

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

Lazy<T, TMetadata> 是 MEF 所提供用以存放間接匯出參考的型別。 這裡,除了已匯出物件本身,您也會取得「匯出中繼資料」(Export Metadata) 或描述已匯出物件的資訊。 Lazy<T, TMetadata> 包含 IOperation 物件 (代表實際運算) 和 IOperationData 物件 (代表其中繼資料)。

將下列簡易介面加入至模組或 SimpleCalculator 命名空間:

Public Interface IOperation
    Function Operate(ByVal left As Integer, ByVal 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(ByVal left As Integer, ByVal 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 中的組合是「遞迴」(Recursive)。 您已明確組合 Program 物件,該物件匯入了一個型別為 MySimpleCalculator 的 ICalculator。 接著,MySimpleCalculator 會匯入 IOperation 物件的集合,而在建立 MySimpleCalculator 時 (匯入 Program 的同時) 填入匯入。 如果 Add 類別宣告進一步匯入,則也必須填入該匯入,依次類推。 任何未填入的匯入都會導致組合錯誤。 (然而,可以將匯入宣告為選擇性匯入,或者對其指派預設值)。

計算機邏輯

安置好這些組件之後,剩餘的便是計算機邏輯本身。 請將下列程式碼加入至 MySimpleCalculator 類別,以實作 Calculate 方法:

Public Function Calculate(ByVal input As String) As String Implements ICalculator.Calculate
    Dim left, right As Integer
    Dim operation As Char
    Dim fn = FindFirstNonDigit(input) 'Finds the operator
    If fn < 0 Then
        Return "Could not parse command."
    End If
    operation = input(fn)
    Try
        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;
    int fn = FindFirstNonDigit(input); //finds the operator
    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 屬性受到存取。 在此情況下,如果探索到 IOperationData 物件的 Symbol 屬性為相符項目,則計算機會呼叫 IOperation 物件的 Operate 方法,並傳回結果。

若要完成計算機,您還需要 Helper 方法來傳回字串中第一個非數字字元的位置。 將下列 Helper 方法加入至 MySimpleCalculator 類別:

Private Function FindFirstNonDigit(ByVal s As String) As Integer
    For i = 0 To s.Length
        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 中,確定您已將 Public 關鍵字加入至 Module1。 在主控台視窗中輸入加法運算 (例如 "5+3"),計算機便會傳回結果。 任何其他運算子將會產生 "Operation Not Found!" (找不到運算!) 訊息。

使用新類別擴充 SimpleCalculator

現在計算機已可運作,輕易便可加入新的運算。 將下列類別加入至模組或 SimpleCalculator 命名空間:

<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "-"c)>
Public Class Subtract
    Implements IOperation

    Public Function Operate(ByVal left As Integer, ByVal 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 還能夠到應用程式本身原始程式以外的地方尋找組件。 若要示範此能力,您需要修改 SimpleCalculator,以透過加入 DirectoryCatalog,在目錄以及其自己的組件 (Assembly) 中搜尋組件 (Part)。

將名為 Extensions 的新目錄加入至 SimpleCalculator 專案。 確定在專案層級而非方案層級加入它。 接著,將新的類別庫專案加入至名為 ExtendedOperations 的方案。 新的專案便會編譯成個別的組件。

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

將下面這行加入至 Module1.vb 或 Program.cs 中的 Program 建構函式:

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

請以您的 Extensions 目錄路徑取代範例路徑。 這個絕對路徑僅供偵錯使用。 在實際執行應用程式中,您會使用相對路徑。DirectoryCatalog 現在會將於 Extensions 目錄之任何組件 (Assembly) 中找到的組件 (Part) 加入至組合容器。

在 ExtendedOperations 專案中,加入 SimpleCalculator 和 System.ComponentModel.Composition 的參考。 在 ExtendedOperations 類別檔中,加入 System.ComponentModel.Composition 的 Imports 或 using 陳述式。 在 Visual Basic 中,同樣加入 SimpleCalculator 的 Imports 陳述式。 然後,將下列類別加入至 ExtendedOperations 類別檔:

<Export(GetType(SimpleCalculator.IOperation))>
<ExportMetadata("Symbol", "%"c)>
Public Class Modulo
    Implements IOperation

    Public Function Operate(ByVal left As Integer, ByVal 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 範例 (英文)。

如需程式碼範例的詳細資訊,請參閱 Managed Extensibility Framework (英文)。 如需 MEF 型別的清單,請參閱 System.ComponentModel.Composition 命名空間。

變更記錄

日期

記錄

原因

2010 年 7 月

已更新步驟。 已加入遺漏的 VB 步驟。 已加入下載範例的連結。

客戶回函。