天天看點

實戰MEF(2):導出&導入

上一文中,我們大緻明白了,利用MEF架構實作自動掃描并組裝擴充元件的思路。本文我們繼續前進,從最初的定義公共接口開始,一步步學會如何使用MEF。

在上一文中我們知道,對于每一個實作了公共規範的擴充元件,都需要進行導出,随後我們的主應用程式檔案中會自動進行組裝。這便産生了一個疑問:為什麼需要導出?

如果大家還記得,以前我們用VC++寫.dll檔案時,都會把需要提供給别人調用的函數标記為導出函數,這樣别人才能調用我們編寫的函數。就好比我們的家,我們一般會有客廳,既然叫客廳,當然是展現給客人看的。有客人來了,我們會在客廳接待,當然我們不願意讓客人進入我們的卧室,那是較為隐私的地方。

是以,對于我們編寫的擴充元件,我們要告訴MEF,哪些類應該被掃描,就像我們的網站一樣,我們會過濾哪些頁面允許搜尋引擎進行抓取,一樣的道理。

要把元件标記為可導出類型,需要在類型的定義代碼上附加System.ComponentModel.Composition.ExportAttribute特性。我們可以看看ExportAttribute類的定義。

[AttributeUsageAttribute(AttributeTargets.Class|AttributeTargets.Method|AttributeTargets.Property|AttributeTargets.Field, AllowMultiple = true,

Inherited = false)]

public class ExportAttribute : Attribute

從定義我們看到,ExportAttribute特性可以用于類以及類的成員,能常我們會附加到整個類,以表示整個類型進行導出。

判斷哪個導出類型符合組裝容器導入的條件,是根據ContractName和ContractType屬性。

ContractName我們可以在附加ExportAttribute時指定,也可以不指定。ContractType屬性指定要導出的類型,如果不指定,預設就是目前要導出的類型。比如:

// 公共接口

public interface IMember

{

string GetMemberType();

}

[Export]

public class VipMember : IMember

public string GetMemberType()

return "VIP會員";

上面的例子,公共接口是IMember,類VipMember實作了該接口并标記為導出類型,但不指定ContractName和ContractType屬性。在這種情況下,預設的協定類型為VipMember,特性附加到哪個類上,預設的導出類型就是該類的類型。

然後,我們再定義一個GenMember類。

public class GenMember : IMember

return "普通會員";

這時候,對于GenMember類,導出的類型就是GenMember。

也許大家已經發現,這樣定義導出類型缺點很明顯,即沒有一個通過的協定類型,這樣一來,在組裝擴充元件時就不能做到自動識别了,因為我們每擴充一個類就新一個協定類型(ContractType),這會導緻主應用程式的代碼需要反複修改,無法一勞永逸了。是以,通常來說,我們應當把ContractType設定為公共接口的類型,如上面例子中的IMember。故我們應該把代碼改為:

[Export(typeof(IMember))]

這樣一改,就滿足需求了,隻要實作了IMember接口并且附加了ExportAttribute的類型都會被組裝容器自動掃描,哪怕你擴充了99999999999999999999999999999個元件,它都能掃描并組裝。

如果你希望組裝容器在掃描類型時需要特定的類,可以在ExportAttribute中定義ContractName。這樣就使得掃描類型的比對條件變得更精準,縮小了查找範圍。當然這樣做也降低了智能性,因為在組裝代碼中,你還要去比對協定名,這也使得主應用程式的代碼會不斷進行修改。

把類型導出之後,就可以提供給組裝容器進行組裝了。就拿我們上面的例子說吧,接下來我們對VipMember和GenMember類進行組裝。

class Program

[Import(typeof(IMember))]

public List<IMember> AllMembers;

static void Main(string[] args)

// 發現類型的方式為目前程式集

AssemblyCatalog catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

// 建立組裝容器

CompositionContainer container = new CompositionContainer(catalog);

Program p = new Program();

// 開始組裝

try

container.ComposeParts(p);

Console.WriteLine("---------- 測試調用 ----------");

foreach (IMember m in p.AllMembers)

Console.WriteLine(m.GetMemberType());

catch (CompositionException cex)

Console.WriteLine(cex.Message);

while (Console.ReadKey().Key != ConsoleKey.Escape) ;

因為我們對IMember擴充了兩個類,為了能讓它們全部導入,在Program類中定義了一個List<IMember>的字段,我們希望把所有導入的類型都放進這個List中。附加ImportAttribute時要與ExportAttribute相對應,前面我們隻定義ContractType,是以這裡導入時,我們依然使用ContractType來比對。

這個應用程式看起來似乎沒啥問題,估計可以運作了,于是我們可以按下F5看看結果。

實戰MEF(2):導出&amp;導入

噢,God,居然發生"奇迹"了,從異常資訊中我們得知,ImportAttribute不能标記一次性導入多個類型的List的字段。那如何解決呢?莫急莫急,看看以下這個Attribute:

// 摘要:

// 指定屬性、字段或參數應通過 System.ComponentModel.Composition.Hosting.CompositionContainer

// 對象用所有比對的導出進行填充。

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]

public class ImportManyAttribute : Attribute, IAttributedImport

是的,這個Attribute專用于導入多個類型的,我們隻要把代碼改為這樣就行了:

[ImportMany(typeof(IMember))]

……

再次運作,我們可以得到預期的結果了。

實戰MEF(2):導出&amp;導入

當今時代,我們什麼東西都要綠色環保,我們的程式也不例外。上面的程式看起來是沒什麼大問題了。不過,如果我們要導入的類型可能會攜帶一些大型資料,我們要是能讓它們延遲初始化那就節約了一些資源開銷。雖然延遲初始化叫起來不太動聽,不過也不算難以了解的概念,就好像你買體彩中了大獎,但提供方不是直接把一張張鈔票拿給你,可能會先給你支票,然後,你憑支票去銀行提錢。

這個延遲初始化也類似,它在聲明時并不立即初始化,等到你使用的時候才初始化。System.Lazy<T>類将帶領我們走向綠色環保的現代生活,它會等到你通路其Value屬性時才初始化。于是,我們也把上面的代碼環保一下。

public List<Lazy<IMember>> AllMembers;

foreach (Lazy<IMember> lz in p.AllMembers)

Console.WriteLine(lz.Value.GetMemberType());

……

最後,提一個不重要的東西,我們代碼中使用的類在

using System.ComponentModel.Composition;

using System.ComponentModel.Composition.Hosting;

請引用System.ComponentModel.Composition(.dll)程式集。

本文執行個體的完整代碼如下:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Reflection;

namespace MEFExam

{

}

MEF