本文提供 .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.vb 或 Program.cs,並新增
Imports
或using
的指示詞來設置System.ComponentModel.Composition
和System.ComponentModel.Composition.Hosting
。 這兩個命名空間包含您需要開發可延伸應用程式的MEF類型。如果您使用 Visual Basic,請將
Public
關鍵詞新增至宣告模組的Module1
行。
組合容器和目錄
MEF 組合模型的核心是 組合容器,其中包含所有可用元件並執行組合。 組合是匯入與導出的比對。 最常見的組合容器類型是 CompositionContainer,您將針對 SimpleCalculator 使用此類型。
如果您使用 Visual Basic,請在 Program
中新增名為 的公用類別。
將下列這一行新增至 Program
Module1.vb 或 Program.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));
}
}
此程式代碼只會讀取一行輸入,並在結果上呼叫 Calculate
的ICalculator
函式,然後將結果輸出至主控台。 這就是你在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
是型別 MySimpleCalculator
。
MySimpleCalculator
接著匯入 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 中,確定您已將 關鍵字新增 Public
至 Module1
。 在控制台視窗中,輸入加法作業,例如「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.vb 或 Program.cs中,將下列這一行新增至建 Program
構函式:
catalog.Catalogs.Add(
New DirectoryCatalog(
"C:\SimpleCalculator\SimpleCalculator\Extensions"))
catalog.Catalogs.Add(
new DirectoryCatalog(
"C:\\SimpleCalculator\\SimpleCalculator\\Extensions"));
將範例路徑取代為延伸模組目錄的路徑。 (此絕對路徑僅供偵錯之用。在生產應用程式中,您會使用相對路徑。 DirectoryCatalog 現在會將在 Extensions 目錄中任何元件中找到的任何元件新增至組合容器。
在ExtendedOperations
專案中,新增對SimpleCalculator
和System.ComponentModel.Composition
的參考。 在ExtendedOperations
類別檔案中,為Imports
新增using
或System.ComponentModel.Composition
指令。 在 Visual Basic 中,也新增 Imports
的 SimpleCalculator
語句。 然後將下列類別新增至 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 命名空間。