若干年前,老周寫了幾篇有關MEF的爛文,簡單地說,MEF是一種動態擴展技術,好比能夠指定以某個程序集或某個目錄爲搜索範圍,應用程序在運行時會自動搜索符合條件的類型,並自動完成導入,這樣作的好處是,主程序的代碼不用改來改去,只須要把擴展的程序集放到對應的目錄下就能夠了。windows
MEF不只能夠用於「看不見」的類型擴展上,對於「看得見」的類型照樣適用,好比窗口、控件之屬,你要是夠牛逼的話,甚至能夠把它用到ASP.NET上,不過這個玩意兒估計要配合重寫路由規則才能實現,根據URL傳的參數來跳轉到具體的頁面。佈局
較爲簡單的,像Windows Forms中的窗口,WPF中的窗口或控件,就能夠直接運用MEF來完成擴展,主應用程序界面能夠動態生成菜單項或按鈕來打開窗口就能夠了。而各個窗口的實現代碼能夠寫在一個類庫項目中。測試
下面,我們用一個實實在在的例子來講明一下。優化
新建一個類庫項目,而後在裏面作三個WPF窗口,XAML文檔如何與代碼類關聯,這個不要問我,問MSDN姐姐去。ui
由於這是作測試,窗口的UI佈局你能夠隨便設計。spa
給你們一個提示吧,XAML文件和窗口類的代碼文件的關聯方法,和ASP.NET中.aspx文件與代碼文件的關聯方法同樣。例如XAML文件名叫 test.xaml,那麼對應的代碼文件名就是test.xaml.cs(VB語言的話,是test.xaml.vb)。設計
對窗口來講,通常是從Window類派生,因此,XAML文檔的根元素要寫Window,好比3d
<Window> …… </Window>
XAML中有兩個必備的命名空間要引入:code
<Window x:Class="wpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" …………
.../xaml/presentation 表示的WPF中的UI類型,好比Button、Canvas等;而那個帶x前綴的.../xaml表示的是XAML語法自己特有的東西,好比x:Class,這個特性就是聯合XAML文件和代碼文件的關鍵,用它來指定窗口類的名字,類名要包括命名空間名。orm
下面的步驟至關重要,否則就沒法MEF了。
打開窗口的代碼文件,在窗口類聲明上添加導出聲明。以下
[Export(typeof(Window))] [ExportMetadata("name", "窗口 A")] public partial class DemoWindow1 : Window { public DemoWindow1() { InitializeComponent(); Title = $"演示窗體 -- {nameof(DemoWindow1)}"; } }
聲明導出須要一個協定,由於類型是能夠動態擴展的,因此這些擴展的類型必需要向運行時代表它們有一個共同點,以便讓MEF可以找到它,這就是類型協定。咱們知道,全部窗口類都有一個共同點——從Window類派生,故而在聲明ExportAttribute時,用Window類的Type來標註協定。
ExportMetadataAttribute表示的是元數據,它是可選的,指定方式和字典的key - value形式差很少,name是字符串,value是Object類型,雖然能夠指任何類型的value,但最好是可序列化的類型或者基礎類型(byte,string,int等),這樣方便傳遞。在接收擴展的代碼中,能夠用IDictionary<string, object>類型來接收元數據,也能夠自定義一個類型(接口、類)來接收,只要屬性/字段的名字和ExportMetadataAttribute中的name相等就好了,這樣元數據就會自動填充到類型的屬性/字段成員中。
好比,若是你指定元數據的name爲「Age」,value爲25,那麼你自定義的類型只要公開一個名爲Age的屬性或字段便可,獲取時會自動填充數據。
這裏我一口氣作了三個窗口,最後,能夠定義一個類,把上面的N個窗口批量導入這個類的一個屬性中,隨後導出這個類的這個屬性。
class WindowsCompos { [Export("extWindows")] [ImportMany(typeof(Window))] public IEnumerable<ExportFactory<Window, IDictionary<string,object>>> ExtWindows { get; set; } }
ImportMany能夠一次性導入多個類型,由於擴展的窗口有N個,因此要使用這個特性來批量導入,還記得吧,前面的窗口都是以Window的Type做爲協定來導出的,因此在導入時,必定指定匹配的協定,否則沒法導入。
由於類型有多個,因此要用IEnumerable<T>(協變)來存放,而其中的T爲ExportFactory<T, TMetadata>,原本用ExportFactory<T>就能夠了,但因爲我爲每一個窗口的導出定義了元數據,因此要使用支持獲取元數據的工廠類型。
這個類能夠不定義爲public,由於導出的是它的屬性,並且對於MEF來講,非public的成員均可以導出,只要你指定導出協定便可。
對於ExtWindows屬性,導出聲明就沒必要使用Type做爲協定了,直接指定一個名字來作協定就能夠了,本例是extWindows,注意這個協定名是區分大小寫的,ext和Ext被視爲不一樣的協定。
一般,接收擴展類型用的是Lazy<T>,以達到延遲實例化,可是,這個項目比較特殊,不能用Lazy來承載類型。WPF的窗口類有個特色,就是每次顯示窗口必須使用新的實例,由於窗口一旦Close以後,就不能再次Show了,只能從新new一個實例才能Show。基於這緣由,用ExportFactory類最好,這個類每次訪問都能從新建立實例,調用CreateExport方法能建立一個ExportLifetimeContext<T>實例,再經過這個ExportLifetimeContext<T>實例的Value屬性來獲得窗口實例。
ExportLifetimeContext<T>實現了IDisposable接口,能夠寫在using語句中,用完後釋放掉。
如今回到主應用程序項目,開始導入擴展窗口。
主窗口用一個菜單就好了,每一個導入的窗口類型將做爲菜單項。
<Grid> <Menu VerticalAlignment="Top"> <MenuItem Header="窗口" Name="menuWindows"> <!-- ****** --> </MenuItem> </Menu> </Grid>
下面代碼將獲取導出對象,因爲剛纔用IEnumable<T>來導入了窗口類型,因此此處只須要獲取這個屬性的值便可。
IEnumerable<ExportFactory<Window, IDictionary<string, object>>> ext_windowslist; CompositionContainer container = null; public MainWindow() { InitializeComponent(); Assembly extAss = Assembly.Load(nameof(ExtWindowLib)); AssemblyCatalog catelog = new AssemblyCatalog(extAss); container = new CompositionContainer(catelog); CompositionExtWindows(); AddExtToMenuitems(); menuWindows.AddHandler(MenuItem.ClickEvent, new RoutedEventHandler(OnMenuItemClicked)); }
CompositionContainer是個容器,用它能夠組合全部獲取到的擴展類型,實例化容器時,要指定一個搜索範圍,這裏我指定它從剛纔那個類庫項目中搜索。由於我已經引用了這個類庫項目,因此調用Assembly.Load(程序集名)就能夠直接加載了。
CompositionExtWindows方法負責從容器中獲取導出的IEnumrable<T>對象,代碼以下:
private void CompositionExtWindows() { if (container == null) return; ext_windowslist = container.GetExportedValue<IEnumerable<ExportFactory<Window, IDictionary<string, object>>>>("extWindows"); }
直接調用GetExportedValue方法就能夠獲取到導出的屬性值,參數是剛剛給ExtWindows屬性指定的協定名。
AddExtToMenuitems方法把獲取到的擴展窗口類型添加到子菜單項,這樣一來,有多少個擴展窗口,就有多少個菜單項。
private void AddExtToMenuitems() { foreach (var factory in ext_windowslist) { // 元數據 IDictionary<string, object> metadata = factory.Metadata; string hd = metadata["name"] as string; MenuItem mnitem = new MenuItem(); mnitem.Header = hd; mnitem.Tag = factory; menuWindows.Items.Add(mnitem); } }
讓菜單項的Tag屬性引用 ExportFactory實例,以便在Click事件處理方法中訪問。
菜單項的Click事件處理以下:
private void OnMenuItemClicked(object sender, RoutedEventArgs e) { MenuItem item = e.Source as MenuItem; ExportFactory<Window> fact = item.Tag as ExportFactory<Window>; if (fact != null) { using (var lifeobj = fact.CreateExport()) { Window w = lifeobj.Value; w.Show(); } } }
從Value屬性中獲取窗口實例,就能夠調用Show方法來顯示窗口了。
來,運行一下,看看如何。運行後,會自動添加三個菜單項,由於我剛剛作了三個窗口。
點擊對應的菜單,就能打開對應窗口。
如今,不妨往類庫項目中再添加一個窗口。
[Export(typeof(Window))] [ExportMetadata("name", "窗口 D")] public partial class DemoWindow4 : Window { public DemoWindow4() { InitializeComponent(); Title = $"演示窗體 -- {nameof(DemoWindow4)}"; } }
主應用程序的代碼不用作任何改動,而後直接運行。
此時,你會看到,第4個窗口也自動加進來了。
有沒有發現,這幾個菜單項的排序好像不太好看,要是能按必定順序排列多好。這個實現起來不難,老周就不實現了,你本身試着幹吧。
老周能夠給個提示,還記得在ExportAttribute聲明導出類型時,能夠指定元數據,例子中,老周指定了一個叫name的元數據,你能夠指定一個叫order的元數據,值爲數值,好比第一個窗口爲1,第二個窗口爲2……
而後,在主程序項目中獲取組合擴展時,能夠用IEnumerable<T>的擴展方法進行排序,也能夠用LinQ語法來排序。
好了,文章就寫到這裏吧,See you.
===================================================================
有熱心朋友給老周留言,問老周,爲何你的博文的右下角,老有人點「反對」,老周你是否是得罪人了。
謝謝朋友,你不說我還真沒注意,由於老周曆來不在乎那些虛的東西,故一直沒注意到這個。實話說,老周曆來不得罪人,老周只會得罪妖魔鬼怪,因此朋友多慮了。
至於說右下角那兩個按鈕,多是一些沒文化的人,原本是想點擊左邊的,因爲不認識漢字,錯點了右邊的按鈕。
總之,你們不要在乎這些可有可無的東西,若是你以爲老周寫的爛文對你有用,那你就姑且當娛樂新聞看看吧,畢竟老周的寫做水平不高,老周已經在努力優化了,爭取多讀點經典名著和大師著做,提高水平。