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


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

示例代碼下載

 

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

有熱心朋友給老周留言,問老周,為什么你的博文的右下角,老有人點“反對”,老周你是不是得罪人了。

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

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

總之,大家不要在意這些無關緊要的東西,如果你覺得老周寫的爛文對你有用,那你就姑且當娛樂新聞看看吧,畢竟老周的寫作水平不高,老周已經在努力優化了,爭取多讀點經典名著和大師著作,提升水平。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM