zoukankan      html  css  js  c++  java
  • 【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.

    示例代码下载

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

    有热心朋友给老周留言,问老周,为什么你的博文的右下角,老有人点“反对”,老周你是不是得罪人了。

    谢谢朋友,你不说我还真没注意,因为老周从来不在意那些虚的东西,故一直没注意到这个。实话说,老周从来不得罪人,老周只会得罪妖魔鬼怪,所以朋友多虑了。

    至于说右下角那两个按钮,可能是一些没文化的人,本来是想点击左边的,由于不认识汉字,错点了右边的按钮。

    总之,大家不要在意这些无关紧要的东西,如果你觉得老周写的烂文对你有用,那你就姑且当娱乐新闻看看吧,毕竟老周的写作水平不高,老周已经在努力优化了,争取多读点经典名著和大师著作,提升水平。

  • 相关阅读:
    百度之星资格赛1001——找规律——大搬家
    HDU1025——LIS——Constructing Roads In JGShining's Kingdom
    DP(递归打印路径) UVA 662 Fast Food
    递推DP UVA 607 Scheduling Lectures
    递推DP UVA 590 Always on the run
    递推DP UVA 473 Raucous Rockers
    博弈 HDOJ 4371 Alice and Bob
    DFS(深度) hihoCoder挑战赛14 B 赛车
    Codeforces Round #318 [RussianCodeCup Thanks-Round] (Div. 2)
    DP(DAG) UVA 437 The Tower of Babylon
  • 原文地址:https://www.cnblogs.com/tcjiaan/p/5844619.html
Copyright © 2011-2022 走看看