【WPF】運用MEF實現窗口的動態擴展

若干年前,老周寫了幾篇有關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.

示例代碼下載

 

===================================================================

有熱心朋友給老周留言,問老周,爲何你的博文的右下角,老有人點「反對」,老周你是否是得罪人了。

謝謝朋友,你不說我還真沒注意,由於老周曆來不在乎那些虛的東西,故一直沒注意到這個。實話說,老周曆來不得罪人,老周只會得罪妖魔鬼怪,因此朋友多慮了。

至於說右下角那兩個按鈕,多是一些沒文化的人,原本是想點擊左邊的,因爲不認識漢字,錯點了右邊的按鈕。

總之,你們不要在乎這些可有可無的東西,若是你以爲老周寫的爛文對你有用,那你就姑且當娛樂新聞看看吧,畢竟老周的寫做水平不高,老周已經在努力優化了,爭取多讀點經典名著和大師著做,提高水平。

相關文章
相關標籤/搜索