若干年前,老周寫了幾篇有關MEF的爛文,簡單地說,MEF是一種動態擴展技術,比如可以指定以某個程序集或某個目錄為搜索范圍,應用程序在運行時會自動搜索符合條件的類型,並自動完成導入,這樣做的好處是,主程序的代碼不用改來改去,只需要把擴展的程序集放到對應的目錄下就可以了。
MEF不僅可以用於“看不見”的類型擴展上,對於“看得見”的類型照樣適用,比如窗口、控件之屬,你要是夠牛逼的話,甚至可以把它用到ASP.NET上,不過這個玩意兒估計要配合重寫路由規則才能實現,根據URL傳的參數來跳轉到具體的頁面。
較為簡單的,像Windows Forms中的窗口,WPF中的窗口或控件,就可以直接運用MEF來完成擴展,主應用程序界面可以動態生成菜單項或按鈕來打開窗口就可以了。而各個窗口的實現代碼可以寫在一個類庫項目中。
下面,咱們用一個實實在在的例子來說明一下。
新建一個類庫項目,然后在里面做三個WPF窗口,XAML文檔如何與代碼類關聯,這個不要問我,問MSDN姐姐去。
因為這是做測試,窗口的UI布局你可以隨便設計。
給大家一個提示吧,XAML文件和窗口類的代碼文件的關聯方法,和ASP.NET中.aspx文件與代碼文件的關聯方法一樣。例如XAML文件名叫 test.xaml,那么對應的代碼文件名就是test.xaml.cs(VB語言的話,是test.xaml.vb)。
對窗口來說,一般是從Window類派生,所以,XAML文檔的根元素要寫Window,比如
<Window> …… </Window>
XAML中有兩個必備的命名空間要引入:
<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文件和代碼文件的關鍵,用它來指定窗口類的名字,類名要包括命名空間名。
下面的步驟相當重要,不然就無法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.
===================================================================
有熱心朋友給老周留言,問老周,為什么你的博文的右下角,老有人點“反對”,老周你是不是得罪人了。
謝謝朋友,你不說我還真沒注意,因為老周從來不在意那些虛的東西,故一直沒注意到這個。實話說,老周從來不得罪人,老周只會得罪妖魔鬼怪,所以朋友多慮了。
至於說右下角那兩個按鈕,可能是一些沒文化的人,本來是想點擊左邊的,由於不認識漢字,錯點了右邊的按鈕。
總之,大家不要在意這些無關緊要的東西,如果你覺得老周寫的爛文對你有用,那你就姑且當娛樂新聞看看吧,畢竟老周的寫作水平不高,老周已經在努力優化了,爭取多讀點經典名著和大師著作,提升水平。