程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 托管可擴展性框架

托管可擴展性框架

編輯:關於.NET

在.NET 4中使用托管可擴展性框架構建可組合的應用程序

托管可擴展性框架 (MEF) 是 .NET Framework 4 和 Silverlight 4 中新增的一個庫,用於 簡化在部署後可由第三方進行擴展的可組合系統的設計。MEF 可使您的應用程序具有開放性, 從而允許應用程序開發人員、框架編寫者以及第三方擴展程序不斷引入新功能。

構建托管可擴展性框架的原因

幾年前,在 Microsoft 內部,一些小組致力於為一個問題找到解決方案,即如何基於可重 用的組件構建可動態發現、重用和組合的應用程序:

Visual Studio 2010 當時在構建新的可擴展代碼編輯器。該編輯器的核心功能以及第三方 功能都作為在運行時發現的二進制文件進行部署。核心要求之一是支持延遲加載擴展,以減少 啟動時間和內存消耗。

“Oslo”引入了“Intellipad”,這是一個可使用 MEF 的新增可擴展文本編輯器。在 Intellipad 中,插件要使用 IronPython 進行編寫。

Acropolis 提供用於構建復合應用程序的框架。Acropolis 運行時可在運行時發現應用程序 組件“部件”,並以松散耦合方式向這些部件提供服務。Acropolis 大量使用 XAML 來編寫組 件。

此問題並不是 Microsoft 所特有的。多年來,客戶一直實現其自己的自定義可擴展性解決 方案。顯然,這是很好的機會,平台可以步入這一領域,提供更通用的解決方案,有助於 Microsoft 和客戶實現雙贏。

我們是否需要新事物?

無論如何,MEF 不是此問題的第一種解決方案。人們提出過許多解決方案 — 跨越平台邊界 的嘗試數不勝數,涉及的工作包括 EJB、CORBA、Eclipse 的 OSGI 實現以及 Java 端的 Spring 等等。在 Microsoft 的平台上,.NET Framework 自身內部包含組件模型和 System.Addin。同時存在若干種開源解決方案,包括 SharpDevelop 的 SODA 體系結構和“控 制反轉”容器(如 Castle Windsor、Structure Map 以及模式和實踐的 Unity)。

既然目前已有這些方法,為何還要引入新事物?這是因為我們意識到,我們當前的所有解決 方案對於常規第三方可擴展性都不理想。這些解決方案要麼規模過大,不適合常規用途,要麼 需要主機或擴展開發人員一方完成過多工作。MEF 在最大程度上秉承了所有這些解決方案的優 點,嘗試解決剛才所提及的令人頭痛的問題。

讓我們來看看 MEF 的核心概念,如圖 1 所示。

圖 1 托管可擴展性框架中的核心概念

概念

MEF 有幾個基本核心概念:

可組合的部件(或簡稱“部件”)— 一個部件向其他部件提供服務,並使用其他部件提供 的服務。MEF 中的部件可來自任何位置(應用程序內部或外部);從 MEF 的角度來看,這並無 區別。

導出 — 導出是部件提供的服務。某個部件提供一個導出時,稱為該部件導出 該服務。例 如,部件可以導出記錄程序(對於 Visual Studio 而言則是導出編輯器擴展)。雖然大多數部 件只提供一個導出,但也有部件可提供多個導出。

導入 — 導入是部件使用的服務。某個部件使用一個導入時,稱為該部件導入 該服務。部 件可導入一個服務(如記錄程序),也可導入多個服務(如編輯器擴展)。

約定 — 約定是導出或導入的標識符。導出程序指定其提供的字符串約定,導入程序指定其 需要的約定。MEF 從要導出和導入的類型派生約定名稱,因此在大多數情況下,您不必考慮這 一點。

組合 — 部件由 MEF 組合,MEF 將部件實例化,然後使導出程序與導入程序相匹配。

編程模型 — MEF 的外觀

開發人員可通過編程模型使用 MEF。通過編程模型,可將組件聲明為 MEF 部件。MEF 提供 了一個現成可用的特性化編程模型,這將是本文的重點內容。該模型只是 MEF 支持的眾多可能 的編程模型之一。MEF 的核心 API 完全與特性無關。

深入探討特性化編程模型

在特性化編程模型中,部件(稱為“特性化部件”)使用 System.ComponentModel.Composition 命名空間中的一組 .NET 特性進行定義。在下面幾節中 ,我將使用此模型嘗試構建一個可擴展的 Windows Presentation Foundation (WPF) 銷售訂單 管理應用程序。使用此應用程序,客戶只需在 bin 文件夾中部署一個二進制文件,便可在其環 境中添加新的自定義視圖。我們將了解一下如何通過 MEF 實現此功能。我將在嘗試過程中逐步 改善設計,並對 MEF 的功能以及特性化編程模型在該過程中所起到的作用進行更多說明。

導出類

該訂單管理應用程序允許插入新視圖。若要向 MEF 中導出某些內容,可使用 Export 特性 進行導出,如下所示:

[Export]
public partial class SalesOrderView : UserControl
{
public SalesOrderView()
  {
InitializeComponent();
  }
}

上面的部件導出 SalesOrderView 約定。默認情況下,Export 特性將成員(在此例中為類 )的具體類型用作約定。您還可以通過向特性構造函數傳遞參數來顯式指定約定。

通過屬性和字段導入

特性化部件可通過對屬性或字段使用 import 特性來表示其需求。應用程序可導出 ViewFactory 部件,其他部件可使用該部件來訪問視圖。該 ViewFactory 使用屬性導入來導入 SalesOrderView。導入屬性僅表示使用 Import 特性修飾屬性:

[Export]
public class ViewFactory
{
  [Import]
  public SalesOrderView OrderView { get; set; }
}

通過構造函數導入

部件也可使用 ImportingConstructor 特性,通過構造函數進行導入(通常稱為構造函數注 入),如下所示。使用導入構造函數時,MEF 會假設所有參數都是導入,從而不必使用 import 特性:

[Export]
public class ViewFactory
{
  [ImportingConstructor]
  public ViewFactory(SalesOrderView salesOrderView)
{
}
}

一般來說,通過構造函數而不是屬性進行導入屬於個人喜好問題,盡管有時適合使用屬性導 入,尤其是當存在並非由 MEF 實例化的部件(如 WPF 應用程序示例中)時。構造函數參數也 不支持重新組合。

組合

隨著 SalesOrderView 和 ViewFactory 准備就緒,現在便可以啟動組合。不會自動發現或 創建 MEF 部件。而是需要編寫一段進行組合的啟動代碼。實現此功能的常見位置為應用程序入 口點處,在本例中為 App 類。

啟動 MEF 涉及以下幾個步驟:

添加需要容器創建的約定的導入。

創建 MEF 用於發現部件的目錄。

創建組合部件實例的容器。

通過對容器調用 Composeparts 方法並傳入具有導入的實例,來進行組合。

正如您在此處所見,我已對 App 類添加了 ViewFactory 導入。然後我創建了指向 bin 文 件夾的 DirectoryCatalog,並創建了使用該目錄的容器。最後,我調用了 Composeparts,該 方法組合 App 實例並滿足 ViewFactory 導入:

public partial class App : Application
{
  [Import]
public ViewFactory ViewFactory { get; set; }

public App()
  {
this.Startup += new StartupEventHandler(App_Startup);
  }

void App_Startup(object sender, StartupEventArgs e)
  {
var catalog = new DirectoryCatalog(@".\");
var container = new CompositionContainer(catalog);
container.Composeparts(this);
  }
}

在組合期間,容器會創建 ViewFactory 並滿足其 SalesOrderView 導入。這會創建 SalesOrderView。最後,Application 類會滿足其 ViewFactory 導入。這樣,MEF 便基於聲明 性信息組成整個對象圖,而不需要手動執行命令性代碼來組成該圖。

通過屬性將非 MEF 項導出到 MEF

將 MEF 集成到現有應用程序中或與其他框架集成時,通常會發現需要提供給導入程序使用 的非 MEF 相關類實例(這表示這些類不是部件)。這些實例可能是密封框架類型(如 System.String)、應用程序范圍的單例(如 Application.Current)或從工廠檢索的實例(如 從 Log4Net 檢索的記錄程序實例)。

為對此提供支持,MEF 允許進行屬性導出。若要使用屬性導出,請使用以導出修飾的屬性創 建中間部件。該屬性實質上是一個工廠,執行檢索非 MEF 值所需的任何自定義邏輯。在下面的 代碼示例中,可以看到 Loggerpart 導出 Log4Net 記錄程序,從而使其他部件(如 App)可導 入該記錄程序,而不是依賴於訪問靜態訪問器方法:

public class Loggerpart
{
  [Export]
public ILog Logger
  {
get { return LogManager.GetLogger("Logger"); }
  }
}

屬性導出在其功能方面如同瑞士軍刀,使 MEF 可與其他對象配合良好。您會發現,在將 MEF 集成到現有應用程序中時以及與舊系統交互時,屬性導出十分用處。

將實現與接口分離

返回到 SalesOrderView 示例,在 ViewFactory 和 SalesOrderView 之間已形成了緊密耦 合關系。工廠需要一個具體的 SalesOrderView,用於限制可擴展性選項以及工廠本身的可測試 性。MEF 允許將接口用作約定,從而將導入與導出程序實現分離:

public interface ISalesOrderView{}

[Export(typeof(ISalesOrderView))]
public partial class SalesOrderView : UserControl, ISalesOrderView
{
   ...
}

[Export]
public class ViewFactory
{
  [Import]
ISalesOrderView OrderView{ get; set; }
}

在上面的代碼中,我更改了 SalesOrderView,以實現 ISalesOrderView 並將其顯式導出。 我還更改了導入程序端的工廠以導入 ISalesOrderView。請注意,導入程序不必顯式指定類型 ,因為 MEF 可從屬性類型 ISalesOrderView 派生該類型。

這就帶來一個問題:ViewFactory 是否還應實現 IViewFactory 這樣的接口?雖然這可能會 對模擬有些作用,但並不要求這樣做。在本例中,我不希望任何人更換 ViewFactory,並且它 是采用可測試的方式而設計的,因此工作正常。在一個部件上可以具有多個導出,以使該部件 能通過多個約定進行導入。例如,SalesOrderView 可通過擁有另一個 export 特性,來導出 UserControl 和 ISalesOrderView:

[Export (typeof(ISalesOrderView))]
[Export (typeof(UserControl))]
public partial class SalesOrderView : UserControl, ISalesOrderView
{
   ...
}

約定程序集

開始創建約定時,需要能夠將這些約定部署到第三方。實現此目的的常用方法是使某個約定 程序集包含擴展程序將實現的約定的接口。該約定程序集成為部件將引用的 SDK 形式。常見模 式是采用“應用程序名稱 + .Contracts”的形式命名約定程序集,如 SalesOrderManager.Contracts。

導入同一約定的多個導出

ViewFactory 當前只導入一個視圖。對每個視圖的一個成員(屬性參數)進行硬編碼適用於 不會頻繁更改且預定義類型數量很少的視圖。但是使用這種方法時,添加新視圖需要重新編譯 工廠。

如果需要很多類型的視圖,則 MEF 提供了更好的方法。您可創建所有視圖都導出的通用 IView 接口,而不是使用特定的視圖接口。工廠隨後導入所有可用 IView 的集合。若要在特性 化模型中導入集合,請使用 ImportMany 特性:

[Export]
public class ViewFactory
{
  [ImportMany]
IEnumerable<IView> Views { get; set; }
}

[Export(typeof(IView))]
public partial class SalesOrderView : UserControl, IView
{
}
//in a contract assembly
public interface IView{}

在此處,您可看到 ViewFactory 現在導入了 IView 實例的集合,而不是特定視圖。 SalesOrder 實現 IView,並將其導出(而不是 ISalesOrderView)。通過此重構, ViewFactory 現在可支持開放視圖集。

MEF 還支持使用具體集合(如 ObservableCollection<T> 或 List<T>)以及 提供默認構造函數的自定義集合來進行導入。

控制部件創建策略

默認情況下,容器中的所有部件實例都是單例,因而由在容器中導入它們的所有部件共享。 因此,SalesOrderView 和 ViewFactory 的所有導入程序都將獲得同一實例。在很多情況下需 要這樣,因為這樣便無需擁有其他組件所依賴的靜態成員。但是,有時每個導入程序都需要獲 取自己的實例,例如用於同時在屏幕上查看多個 SalesOrderView 實例。

MEF 中的部件創建策略可以是以下三個值之一:CreationPolicy.Shared、 CreationPolicy.NonShared 或 CreationPolicy.Any。若要指定部件的創建策略,請使用 partCreationPolicy 特性修飾部件,如下所示:

[partCreationPolicy(CreationPolicy.NonShared)]
[Export(typeof(ISalesOrderView))]
public partial class SalesOrderView : UserControl, ISalesOrdderView
{
public SalesOrderView()
  {
  }
}

通過對導入設置 RequiredCreationPolicy 屬性,也可在導入程序端指定 PartCreationPolicy。

使用元數據區分導出

ViewFactory 現在使用一個開放視圖集,但是我無法區分各個視圖。我可以向 IView 添加 名為 ViewType 的成員(視圖會提供該成員),然後根據該屬性進行篩選。另一種方法是使用 MEF 的導出元數據工具,以通過 ViewType 對視圖進行注釋。使用元數據具有另一個好處,即 視圖實例化可延遲到需要時進行,這可節約資源並提高性能。

定義導出元數據

若要對導出定義元數據,請使用 ExportMetadata 特性。下面的 SalesOrderView 已更改為 導出 IView 標記接口作為它的約定。它隨後會添加“ViewType”的其他元數據,以便可以放在 共享同一約定的其他視圖間:

[ExportMetadata("ViewType", "SalesOrder")]
[Export(typeof(IView)]
public partial class SalesOrderView : UserControl, IView
{
}

ExportMetadata 有兩個參數,即一個字符串形式的鍵和一個類型對象值。如前面的示例中 那樣使用魔幻字符串可能會存在問題,因為這在編譯時並不安全。我們可以為鍵提供常量並為 值提供枚舉(而不使用魔幻字符串):

[ExportMetadata(ViewMetadata.ViewType, ViewTypes.SalesOrder)]
[Export(typeof(IView)]
public partial class SalesOrderView : UserControl, IView
{
  ...
}
//in a contract assembly
public enum ViewTypes {SalesOrderView}

public class ViewMetadata
{
public const string ViewType = "ViewType";
}

使用 ExportMetadata 特性可提供很大的靈活性,但是使用該特性時需要注意一些事項:

在 IDE 中無法發現元數據鍵。部件編寫者必須知道對導出有效的元數據鍵和類型。

編譯器不會驗證元數據以確保其正確。

ExportMetadata 會向代碼添加更多干擾信息,從而隱藏真正意圖。

MEF 提供了解決方案來解決以上的問題:自定義導出。

自定義導出特性

MEF 允許創建包括其自己的元數據的自定義導出。創建自定義導出包括創建還指定元數據的 派生 ExportAttribute。我們可使用自定義導出來創建包含 ViewType 元數據的 ExportView 特性:

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class ExportViewAttribute : ExportAttribute {
public ExportViewAttribute()
:base(typeof(IView))
  {}

public ViewTypes ViewType { get; set; }
}

ExportViewAttribute 指定它通過調用 Export 的基本構造函數來導出 IView。它使用 MetadataAttribute 進行修飾,這指定該特性提供元數據。此特性告知 MEF 查看所有公共屬性 ,並通過將屬性名稱用作鍵,對導出創建相關聯的元數據。在這種情況下,唯一的元數據為 ViewType。

最後需要記住的重要一點是,ExportView 特性使用 AttributeUsage 特性進行修飾。這指 定該屬性僅對類有效,且只能存在一個 ExportView 特性。

一般來說,AllowMultiple 應設置為 false;如果為 true,則導入程序將傳遞一組值而不 是單個值。當多個導出具有同一成員的同一約定的不同元數據時,AllowMultiple 應保留為 True。

將新的 ExportViewAttribute 應用於 SalesOrderView 現在會產生如下結果:

[ExportView(ViewType = ViewTypes.SalesOrder)]
public partial class SalesOrderView : UserControl, IView
{
}

如您所見,自定義導出可確保為特定導出提供正確的元數據。這些導出還可減少代碼中的干 擾信息,更加容易通過 IntelliSense 進行發現,並且可通過特定於域來更好地表達意圖。

既然已對視圖定義了元數據,ViewFactory 便可將其導入。

導入延遲導出和訪問元數據

為了允許訪問元數據,MEF 使用 .NET Framework 4 的一個新 API,即 System.Lazy<T>。使用該 API 可延遲實例的實例化,直至訪問 Lazy 的 Value 屬性。 MEF 使用 Lazy<T,TMetadata> 進一步擴展 Lazy<T>,以允許在不實例化基礎導出 的情況下訪問導出元數據。

TMetadata 是元數據視圖類型。元數據視圖是接口,用於定義對應於所導出元數據中的鍵的 只讀屬性。訪問元數據屬性時,MEF 將動態實現 TMetadata,且將基於導出提供的元數據來設 置值。

這是 View 屬性使用 Lazy<T,TMetadata> 更改為導入時,ViewFactory 展示的內容 :

[Export]
public class ViewFactory
{
  [ImportMany]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }
}

public interface IViewMetadata
{
ViewTypes ViewType {get;}
}

導入了包含元數據的延遲導出集合後,可使用 LINQ 對該集合進行篩選。在下面的代碼段中 ,我對 ViewFactory 實現了 GetViews 方法,以檢索指定類型的所有視圖。請注意,它會訪問 Value 屬性,以便僅為與篩選器匹配的視圖生成實際視圖實例:

[Export]
public class ViewFactory
{
  [ImportMany]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }

public IEnumerable<View> GetViews(ViewTypesviewType) {
return Views.Where(v=>v.Metadata.ViewType.Equals(viewType)).Select (v=>v.Value);
  }
}

通過這些更改,ViewFactory 現在可發現在 MEF 組合工廠時 可用的所有視圖。如果在該初 始組合後容器或目錄中出現了新的實現,則 ViewFactory 無法發現這些新實現,因為它已經組 合。不僅如此,MEF 實際上會通過引發 CompositionException 來阻止將視圖添加到目錄,也 就是說,除非啟用重新組合,否則無法添加視圖。

重新組合

重新組合是 MEF 的一項功能,此功能允許部件在系統中出現新的匹配導出時自動更新其導 入。重新組合在某些方案中十分有用,例如從遠程服務器下載部件時。 SalesOrderManager 可 以進行更改,以便在其啟動時,可啟動多個可選視圖的下載。這些視圖顯示時,會出現在視圖 工廠中。為了使 ViewFactory 可重新組合,我們在 Views 屬性的 ImportMany 特性上將 AllowRecomposition 屬性設置為 true,如下所示:

[Export]
public class ViewFactory
{
[ImportMany(AllowRecomposition=true)]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }

public IEnumerable<View>GetViews(ViewTypesviewType) {
return Views.Where(v=>v.Metadata.ViewType.Equals(viewType)).Select (v=>v.Value);
  }
}

進行重新組合時,Views 集合將立刻替換為包含一組更新過的視圖的新集合。

啟用重新組合後,應用程序可從服務器下載其他程序集並將這些程序集添加到容器。可通過 MEF 的目錄執行此操作。MEF 提供了多個目錄,其中有兩個目錄可重新組合。 DirectoryCatalog(您已看到過)是可通過調用其 Refresh 方法來重新組合的目錄。另一個可 重新組合的目錄是 AggregateCatalog,這是目錄的目錄。您可使用 Catalogs 集合屬性向該目 錄添加目錄,這會啟動重新組合。我將使用的最後一個目錄是 AssemblyCatalog,該目錄接受 一個它隨後將在其之上構建目錄的程序集。圖 2 演示一個示例,說明如何結合使用這些目錄進 行動態下載。

圖 2 使用 MEF 目錄進行動態下載

void App_Startup(object sender, StartupEventArgs e)
{
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(newDirectoryCatalog((@"\.")));
var container = new CompositionContainer(catalog);
container.Composeparts(this);
base.MainWindow = MainWindow;
this.DownloadAssemblies(catalog);
}

private void DownloadAssemblies(AggregateCatalog catalog)
{
//asynchronously downloads assemblies and calls AddAssemblies
}

private void AddAssemblies(Assembly[] assemblies, AggregateCatalog catalog)
{
var assemblyCatalogs = new AggregateCatalog();
foreach(Assembly assembly in assemblies)
assemblyCatalogs.Catalogs.Add(new AssemblyCatalog(assembly));
catalog.Catalogs.Add(assemblyCatalogs);
}

圖 2 中的容器是使用 AggregateCatalog 創建的。該容器隨後將 DirectoryCatalog 添加 到其中,以在 bin 文件夾中獲取本地部件。聚合目錄會傳遞到 DownloadAssemblies 方法,該 方法異步下載程序集,然後調用 AddAssemblies。該方法會創建新的 AggregateCatalog,向該 目錄為每個下載程序集添加 AssemblyCatalogs。然後,AddAssemblies 添加包含主要聚合的程 序集的 AggregateCatalog。它之所以采用這種方式進行添加,是為了一次性完成重新組合,而 不是反復進行(在直接添加程序集目錄時會出現這種情況)。

進行重新組合時,集合會立即更新。結果因集合屬性類型而異。如果屬性類型是 IEnumerable<T>,則它將替換為新實例。如果它是繼承自 List<T> 或 ICollection 的具體集合,則 MEF 將對每一項依次調用 Clear 和 Add。無論是哪一種情況, 都意味著在使用重新組合時必須考慮線程安全。重新組合不僅與添加有關,也與刪除有關。如 果從容器中移除目錄,則也會移除這些部件。

穩定的組合、拒絕和診斷

有時,一個部件可能指定一個缺少的導入,因為該導入在目錄中不存在。發生這種情況時, MEF 會阻止發現缺少依賴關系的部件(或依賴於其的任何對象)。MEF 這樣做是為了穩定系統 ,並防止在創建了部件時一定會發生的運行時故障。

此處的 SalesOrderView 已更改,以便在即使不存在記錄程序實例時也導入 ILogger:

[ExportView(ViewType = ViewTypes.SalesOrder)]
public partial class SalesOrderView : UserControl, IView
{
[Import]
public ILogger Logger { get; set; }
}

因為沒有可用 ILogger 導出,所以不會向容器顯示 SalesOrderView 的導出。這不會引發 異常,而僅僅忽略 SalesOrderView。如果您檢查 ViewFactory 的 Views 集合,則會發現該集 合是空的。

如果有多個導出可用於單個導入,則還會發生拒絕。在這些情況下,會拒絕導入單個導出的 部件:

[ExportView(ViewType = ViewTypes.SalesOrder)]
public partial class SalesOrderView : UserControl, IView
{
[Import]
public ILogger Logger { get; set; }
}
  [Export(typeof(ILogger))]
public partial class Logger1 : ILogger
{
}
  [Export(typeof(ILogger))]
public partial class Logger2 : ILogger
{
}

在上面的示例中,會拒絕 SalesOrderView,因為存在多個 ILogger 實現,但只導入單個實 現。MEF 提供了允許默認導出在多處存在的工具。有關此方面的詳細信息,請參閱 codebetter.com/blogs/glenn.block/archive/2009/05/14/customizing-container-behavior -part-2-of-n-defaults.aspx。

您可能會問,MEF 為何不創建 SalesOrderView 並引發異常。在開放的可擴展系統中,如果 MEF 引發異常,則應用程序會非常難以處理異常或使上下文知道要進行何種操作,因為可能缺 少部件,或導入可能深深嵌套在組合中。如果不正確處理,則應用程序就會處於無效狀態並且 不可用。MEF 會拒絕部件,從而確保保持應用程序穩定性。有關穩定組合的詳細信息,請參閱 :blogs.msdn.com/gblock/archive/2009/08/02/stable-composition-in-mef-preview-6.aspx 。

診斷拒絕

拒絕是一項非常強大的功能,但有時可能難以進行診斷,尤其是在拒絕整個依賴關系圖時。 在先前的第一個示例中,ViewFactory 直接導入 SalesOrderView。假設 MainWindow 導入了 ViewFactory,而 SalesOrderView 被拒絕。隨後 ViewFactory 和 MainWindow 也會被拒絕。 如果您看到這種情況發生,可能會百思不得其解,因為您知道 MainWindow 和 ViewFactory 是 實際存在的;拒絕的原因是缺少依賴關系。

MEF 不會讓您束手無措。為了幫助診斷此問題,它提供了跟蹤。在 IDE 中,可從輸出窗口 跟蹤所有拒絕消息,不過也可以從任何有效的跟蹤偵聽器跟蹤這些消息。例如,當應用程序嘗 試導入 MainWindow 時,會輸出圖 3 中的跟蹤消息。

圖 3 MEF 跟蹤消息

System.ComponentModel.Composition Warning: 1 : The  ComposablepartDefinition 'Mef_MSDN_Article.SalesOrderView' has been rejected.  The composition remains unchanged. The changes were rejected because of  the following error(s): The composition produced a single composition  error. The root cause is provided below. Review the  CompositionException.Errors property for more detailed information.

1) No valid exports were found that match the constraint  '((exportDefinition.ContractName == "Mef_MSDN_Article.ILogger") AndAlso  (exportDefini-tion.Metadata.ContainsKey("ExportTypeIdentity") AndAlso  "Mef_MSDN_Article.ILogger".Equals(exportDefinition.Metadata.get_Item ("ExportTypeIdentity"))))', invalid exports may have been rejected.

Resulting in: Cannot set import 'Mef_MSDN_Article.SalesOrderView.Logger  (ContractName="Mef_MSDN_Article.ILogger")' on part  'Mef_MSDN_Article.SalesOrderView'.
Element: Mef_MSDN_Article.SalesOrderView.logger  (ContractName="Mef_MSDN_Article.ILogger") -->Mef_MSDN_Article.SalesOrderView  -->TypeCatalog (Types='Mef_MSDN_Article.MainWindow,  Mef_MSDN_Article.SalesOrderView, ...').

跟蹤輸出會顯示問題的根本原因:SalesOrderView 需要 ILogger,但無法找到。然後,我 們可以看到拒絕 SalesOrderView 會導致拒絕工廠,最終拒絕 MainWindow。

在調試器中檢查部件

您可以更進一步,實際檢查目錄中的可用部件,我將在有關托管的小節中對此進行討論。在 圖 4 中,您可以在監視窗口中查看可用部件(在綠色圈中)以及所需的 ILogger 導入(在藍 色圈中)。

圖 4 監視窗口中顯示的可用部件和所需 ILogger

在命令行處診斷拒絕

MEF 的一個目標是支持靜態可分析性,從而允許在運行時環境之外分析組合。我們在 Visual Studio 中還未提供這種工具支持,但是 Nicholas Blumhardt 編寫了 MEFX.exe (mef.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=33536),這是實現此功能的一 個命令行工具。MEFX 可分析程序集並確定要拒絕的部件以及拒絕的原因。

如果在命令行處運行 MEFX.exe,則您將看到很多選項;您可列出特定導入、導出或可用的 所有部件。例如,在此處可以看到如何使用 MEFX 顯示部件列表:

C:\mefx>mefx.exe /dir:C:\SalesOrderManagement\bin\debug /parts
SalesOrderManagement.SalesOrderView
SalesOrderManagement.ViewFactory
SalesOrderManagement.MainWindow

這對於獲取部件清單十分有用,但是 MEFX 也可向下跟蹤拒絕,這是我們現在所關注的問題 ,如圖 5 所示。

圖 5 使用 MEFX.exe 向下跟蹤拒絕

C:\mefx>mefx.exe /dir:C:\SalesOrderManagement\bin\debug /rejected  /verbose 

[part] SalesOrderManagement.SalesOrderView from: DirectoryCatalog  (Path="C:\SalesOrderManagement\bin\debug")
  [Primary Rejection]
  [Export] SalesOrderManagement.SalesOrderView  (ContractName="SalesOrderManagement.IView")
  [Export] SalesOrderManagement.SalesOrderView  (ContractName="SalesOrderManagement.IView")
  [Import] SalesOrderManagement.SalesOrderView.logger  (ContractName="SalesOrderManagement.ILogger")
   [Exception]  System.ComponentModel.Composition.ImportCardinalityMismatchException: No valid  exports were found that match the constraint  '((exportDefinition.ContractName == "SalesOrderManagement.ILogger") AndAlso  (exportDefinition.Metadata.ContainsKey("ExportTypeIdentity") AndAlso  "SalesOrderManagement.ILogger".Equals(exportDefinition.Metadata.get_Item ("ExportTypeIdentity"))))', invalid exports may have been rejected.
at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports (ImportDefinition definition, AtomicCompositionatomicComposition)
at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports (ImportDefinition definition)
at  Microsoft.ComponentModel.Composition.Diagnostics.CompositionInfo.AnalyzeImportDefin ition(ExportProvider host, IEnumerable`1 availableparts, ImportDefinition id)

仔細分析圖 6 中的輸出可了解問題的根本原因:找不到 ILogger。如您所見,在具有許多 部件的大型系統中,MEFX 是非常有用的工具。有關 MEFX 的詳細信息,請參閱 blogs.msdn.com/nblumhardt/archive/2009/08/28/analyze-mef-assemblies-from-the- command-line.aspx。

圖 6 IronRuby 中的部件示例

概括來說,特性化模型具有以下幾個優勢:

它為部件提供了聲明其導出和導入的通用方法。

它使系統可以動態發現可用部件,而無需預先注冊。

它可進行靜態分析,從而允許諸如 MEFX 這樣的工具可提前確定故障。

我現在將帶您快速了解一下體系結構,並看看它能提供什麼功能。在較高級別,MEF 體系結 構分為以下幾層:編程模型層、托管層和基元層。

再次討論編程模型

特性化模型只是將特性用作發現方法的基元的一種實現。基元可表示非特性化部件,甚至可 表示未靜態類型化的部件,如在動態語言運行時 (DLR) 中一樣。在圖 6 中,您可以看到導出 IOperation 的 IronRuby 部件。請注意,它使用 IronRuby 的本機語法來聲明部件而不是特性 化模型,因為 DLR 中不支持特性。

IronRuby 編程模型不附帶 MEF,盡管我們以後可能會增加動態語言支持。

您可以在以下博客系列中閱讀有關構建 Ruby 編程模型的實驗的詳細信息: blogs.msdn.com/nblumhardt/archive/tags/Ruby/default.aspx。

托管層:進行組合的位置

編程模型定義部件、導入和導出。為了實際創建實例和對象圖,MEF 提供了托管 API,這些 API 主要位於 System.ComponentModel.Composition.Hosting 命名空間中。托管層提供很大的 靈活性、可配置性和可擴展性。MEF 中的很多“工作”都在這一層進行,MEF 中的發現也是從 這一層開始。僅僅編寫部件的大多數人永不會接觸此命名空間。但是,如果您是托管方,則您 會如同我之前所做那樣使用它們,以啟動組合。

目錄提供描述可用導出和導入的部件定義 (ComposablepartDefinition)。它們是 MEF 中用 於發現的主要單元。MEF 在 System.ComponentModel.Composition 命名空間中提供了多個目錄 (您已看到過其中一些目錄),包括掃描目錄的 DirectoryCatalog、掃描程序集的 AssemblyCatalog 以及掃描特定類型集的 TypeCatalog。這些目錄中的每個目錄都特定於特性 化編程模型。但是,AggregateCatalog 與編程模型無關。目錄從 ComposablepartCatalog 繼 承,並且是 MEF 中的擴展點。自定義目錄有許多用途:從提供全新的編程模型到封裝和篩選現 有目錄。

圖 7 顯示一個已篩選目錄的示例,該目錄接受謂詞,以對將返回部件的內部目錄進行篩選 。

圖 7 已篩選的目錄

public class FilteredCatalog : ComposablepartCatalog,
{
private readonly composablepartcatalog _inner;
private readonly IQueryable<ComposablepartDefinition>  _partsQuery;

public FilteredCatalog(ComposablepartCatalog inner,
Expression<Func<ComposablepartDefinition, bool>> expression)
  {
    _inner = inner;
   _partsQuery = inner.parts.Where(expression);
  }

public override IQueryable<ComposablepartDefinition> parts
  {
get
    {
return _partsQuery;
    }
  }
}

CompositionContainer 進行組合,這表示它會創建部件並滿足其他部件的導入。在滿足導 入時,它將從可用導出的池中進行獲取。如果這些導出也有導入,則容器將首先滿足它們。這 樣,容器將按需組成整個對象圖。導出池的主要來源是目錄,但是容器也可以直接將現有部件 實例添加到其中並進行組合。雖然在大多數情況下部件來自於目錄,但手動將入口點類以及從 目錄提取的部件一起添加到容器,這也十分常見。

容器也可嵌套在層次結構中,以支持確定范圍方案。默認情況下,子容器會查詢父級,但是 也可以提供其自己的子部件目錄,這些子部件會在子容器中創建:

var catalog = new DirectoryCatalog(@".\");
var childCatalog = new DirectoryCatalog(@".\Child\";
var rootContainer = new CompositionContainer(rootCatalog));
var childContainer = new CompositionContainer(childCatalog,
rootContainer);

在上面的代碼中,childContainer 安排為 rootContainer 的子級。rootContainer 和 childContainer 都提供其自己的目錄。有關在應用程序中使用容器托管 MEF 的詳細信息,請 參閱 codebetter.com/blogs/glenn.block/archive/2010/01/15/hosting-mef-within-your- applications.aspx。

基元層:生成部件和編程模型的位置

位於 System.ComponentModel.Composition.Primitives 處的基元是 MEF 中的最低級別。 可以說,它們是 MEF 及其上層擴展點的量子世界。到現在為止,我已討論完特性化編程模型。 但是,MEF 的容器根本不會綁定到特性;而是綁定到基元。基元定義部件的抽象表示形式,這 包括如 ComposablepartDefinition、ImportDefinition 和 ExportDefinition 這樣的定義以 及表示實際實例的 Composablepart 和 Export。

討究基元本身是另一個主題,我可能會在以後的文章中進行討論。目前,您可在 blogs.msdn.com/dsplaisted/archive/2009/06/08/a-crash-course-on-the-mef- primitives.aspx 處找到有關基元的詳細信息。

Silverlight 4 中的 MEF 及其他

MEF 也作為 Silverlight 4 的一部分提供。我在此處討論的所有內容都與開發可擴展的富 Internet 應用程序有關。在 Silverlight 中,我們甚至更進一步,引入了其他 API,以簡化 在 MEF 上構建應用程序的過程。這些增強功能最後會加入到 .NET Framework 中。

您可在下面的帖子中找到有關 Silverlight 4 中的 MEF 的詳細信息: codebetter.com/blogs/glenn.block/archive/2009/11/29/mef-has-landed-in-silverlight- 4-we-come-in-the-name-of-extensibility.aspx。

我只是簡要介紹了使用 MEF 可以實現的功能。這是一個強大、穩健而靈活的工具,您可將 其添加到工具集中,以幫助您將應用程序向一個充滿各種可能性的全新世界開放。我期待看到 您使用該工具所完成的工作!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved