• 在不修改代码的情况下无限扩展应用项目


    在许多需要分模块开发,较为复杂的应用项目(如ERP之类)中,如何做到轻松扩展,往往是一个头疼的问题。

    在传统条件下,我们会把各个功能分布在不同的类库中,每添加一个功能就引用一个程序集,而这种方法,我们会发现,当你每添加一个新扩展后,都要对新增的程序集进行引用,这样也意味着,你每次都要重新编译一次主应用程序,这一来一往,维护成本也是有的。

    到了.NET 3.5时代,你可能会想到Addin,但这个方法也会带来扩展成本。

    而我们所追求的最完美解决方案是:

    如果我od 们编写完应用程序后,可以在原有程序不变的情况下,无限添加扩展就好了。也就是说,我把应用A在用户的机器上安装好了,后来我做了一点扩展,这个新功能我已经编译到fc.dll类库中了,可我不想每次升级都要把EXE文件和所有组件重新编译,我只需要把新的fc.dll复制到应用安装目录下就可以了。

    也许这样一来,维护成本可以大大降低了,到了.NET 4.0时代,利用MEF框架确实可以做到以上要求,但前提条件是:

    1、在开发项目前,对整个项目的结构和规范必须明确,至少整个应用程序是什么样子的,你必须在大脑里面有个底。

    2、编写一个公共类库,里面包含所有将来要进行扩展组件的行为规范,也就是在这个公共类库中定义所有将来可能被实现的接口,后面所有扩展的程序集都必须实现这些接口。

    本文涉及的内容可能有些深奥,但愿大家可以接受,接受不了也没关系,毕竟许多东西是需要时间去掌握的。

    我弄个简单的例子吧,比如,我现在要开发一个应用,在一个窗口上点击按钮后,显示对应球类的名字,如“足球”、“皮球”、“排球”等。但是,可能当初我只给出两个选项——足球和排球,这是我在把程序给客户前就扩展的两个程序集,但过了几天,我突然想起,还有“羽毛球”、“篮球”等,于是,我要为应用程序再加两个dll,但我希望应用程序扩展后不用修改代码,无论我将来扩展100个还是10000个dll我都不需要重新生成主程序,我只要把新的dll扔到应用程序中的Ext文件夹中就可以了。

    我们来看看如何实现。

    1、新建一个公共类库,写两个接口,IBall接口就是与球类信息有关的类,提供扩展时实现该接口。

    [csharp] view plain copy
     
    1. public interface IBall  
    2. {  
    3.     string GetInformation();  
    4. }  


    它有一个公共方法GetInformation,返回对应球类的名字,如“足球”.

    另一个接口是用来描述元数据的。

    [csharp] view plain copy
     
    1. public interface IMetaData  
    2. {  
    3.     string BallType { get; }  
    4. }  

    为什么要定义这个元数据的接口呢?就是为了识别我们应用程序调用了哪个扩展。

    比如,FootBall(足球)类扩展实现了IBall接口,VolleyBall(排球)类扩展也实现了IBall接口,BasketBall(篮球)类扩展也实现了IBall接口,可能以后会更多,所有的扩展都实现IBall,那么,我们怎么知道我们正在调用的足球?而不是篮球呢?所以,就需要这个IMetaData类,在进行扩展的导出类时,我们为每一个类型定义一下IMetaData的BallType属性,例如,我在定义足球类时,我定义BallType为“foot ball”,在定义排球类时,把BallType设置为“volley ball”,这样,在我们的应用程序中,就可以通过这个来判断我们正在调用哪个扩展,当然,如果你不需要明确知道调用哪个扩展,这个元数据就可以忽略。

    2、分别编写两个类库,符合以下两个条件:

    a、实现IBall接口。

    b、用ExportAttribute标识为导出类型。

    MEF的类都是来自System.ComponentModel.Composition程序集,在需要的地方引用就行了,如何引用程序集,我就不说了,这是基础知识。这些类分布在以下三个命名空间。

    System.ComponentModel.Composition

    System.ComponentModel.Composition.Hosting

    System.ComponentModel.Composition.Primitives

    [csharp] view plain copy
     
    1. using System;  
    2. using System.Collections.Generic;  
    3. using System.Linq;  
    4. using System.Text;  
    5. using System.Threading.Tasks;  
    6. using System.ComponentModel.Composition;  
    7. using System.ComponentModel.Composition.Hosting;  
    8.   
    9. namespace BallLibA  
    10. {  
    11.   
    12.     /// <summary>  
    13.     /// 足球  
    14.     /// </summary>  
    15.     [Export(typeof(CommonLib.IBall))]  
    16.     [ExportMetadata("BallType","Foot Ball")]  
    17.     public class FootBall:CommonLib.IBall  
    18.     {  
    19.         public string GetInformation()  
    20.         {  
    21.             return "足球";  
    22.         }  
    23.     }  
    24.   
    25.   
    26. }  


    在MEF中,我们不需要实现提供元数据的接口,只需要在ExportMetadata特性中直接为属性设置值就行,运行时会自动生成实现元数据(本例是IMetaData接口)的类。

    接照同样的方法,我们再做一个类库。

    [csharp] view plain copy
     
    1. /// <summary>  
    2. /// 排球  
    3. /// </summary>  
    4. [Export(typeof(CommonLib.IBall))]  
    5. [ExportMetadata("BallType", "Volley Ball")]  
    6. public class VolleyBall : CommonLib.IBall  
    7. {  
    8.     public string GetInformation()  
    9.     {  
    10.         return "排球";  
    11.     }  
    12. }  

    现在,我们的项目已经有两个扩展了。

    3、我们来实现我们的主应用程序,我们只需引用我们前面编写的公共类库中的接口即可,而扩展的dll我们不需要引用,MEF会自动寻找。因此,把所有扩展的程序集都生成dll文件,然后统一扔到与exe文件同一位置的Ext文件夹中就行了,你有1000个dll就全部扔到文件夹里就行,MEF会自动寻找。

    我们用一个WinForm程序作为主程序,如下图所示。



    在程序运行时,会根据Ext目录下的所有扩展的dll自动发现所有程序集,然后显示在ComboBox中,我们选择对应的球类,然后点击按钮,这样在文本框中就会对应地显示球类的名称。

    [csharp] view plain copy
     
    1. using System;  
    2. using System.Collections.Generic;  
    3. using System.ComponentModel;  
    4. using System.Data;  
    5. using System.Drawing;  
    6. using System.Linq;  
    7. using System.Text;  
    8. using System.Threading.Tasks;  
    9. using System.Windows.Forms;  
    10. using System.ComponentModel.Composition;  
    11. using System.ComponentModel.Composition.Hosting;  
    12.   
    13. namespace TestApp  
    14. {  
    15.     public partial class Form1 : Form  
    16.     {  
    17.         CompositionContainer myContainer = null;  
    18.         // 引入的组合类型  
    19.         [ImportMany]  
    20.         IEnumerable<Lazy<CommonLib.IBall, CommonLib.IMetaData>> mBalls;  
    21.         public Form1()  
    22.         {  
    23.             InitializeComponent();  
    24.             DirectoryCatalog catl = new DirectoryCatalog("Ext");  
    25.             myContainer = new CompositionContainer(catl);  
    26.             try  
    27.             {  
    28.                 myContainer.ComposeParts(this);//组合组件  
    29.             }  
    30.             catch (Exception ex)  
    31.             {  
    32.                 MessageBox.Show(ex.Message);  
    33.             }  
    34.             var resBalls = (from t in mBalls  
    35.                             select t.Metadata.BallType).ToArray();  
    36.             this.comboBox1.DataSource = resBalls;  
    37.   
    38.         }  
    39.   
    40.         private void button1_Click(object sender, EventArgs e)  
    41.         {  
    42.             if (this.comboBox1.SelectedIndex == -1)  
    43.             {  
    44.                 MessageBox.Show("请选择一个扩展。"); return;  
    45.             }  
    46.             string ballName = this.comboBox1.SelectedItem.ToString();  
    47.             // 取出要执行哪个扩展程序集  
    48.             var ballInstance = mBalls.FirstOrDefault(x => x.Metadata.BallType == ballName);  
    49.             if (ballInstance != null)  
    50.             {  
    51.                 this.txtResult.Text = ballInstance.Value.GetInformation();  
    52.             }  
    53.         }  
    54.     }  
    55. }  


    从上面的代码中,可以总结出MEF的用法,这方法你有兴趣的话可以背下来,因为无论你用到什么项目,思路都是一样的。

    1、声明一个CompositionContainer变量是必须的,因为它可以用来指示当前应用程序与哪些扩展程序集进行合并。

    2、在实例化CompositionContainer时,我使用DirectoryCatalog类,为什么?因为这个类好用,你只需要告诉它你扩展的dll放在哪个文件夹就行了。它会在你指定的文件夹里面自动找到导出的扩展类。

    3、有导出类,自然就有导入类,因为我们的所有扩展都是实现IBall接口的,所以,扩展的类的导出类型应使用IBall,这样,凡是声明为导出类的都会被MEF发现并自动加载。

    所以,导出是针对扩展的程序集而言的,那导入就好理解了,就是针对我们的主应用程序而言,像本例,WinForm应用作为主程序,所有扩展都是在这个WinForm中使用的,所以这个WinForm就必须对类型进行导入。因此才有了以下代码。

    [csharp] view plain copy
     
    1. // 引入的组合类型  
    2. [ImportMany]  
    3. IEnumerable<Lazy<CommonLib.IBall, CommonLib.IMetaData>> mBalls;  

    使用Lazy来延迟实例化的好处是提高性能,记住,加了Import的导入类型是不用new的,因为DirectoryCatalog在Ext文件夹下找到所有的dll都会自动实例化,这就是要用延迟实便化的原因,只有在用的时候才new,不然,如果我的扩展目录下有100000个dll,350000000个类,那你一运行就全部实例化,那这性能估计要把内存用爆。

    前面我说过,IMetaData用于标识元数据,我们不必自己去实现,而我们也不必指事实上哪个接口,因为上面代码中,Lazy<T, TMetadata>就有两个泛型参数,看到没?

    T是我们要导入的类型,本例中是IBall,注意,我们这里的类型一定要是公共的接口,不是扩展的具体类,不然就实现不了无限扩展的目的,接口用途就是它有通用性。

    TMetadata就是用来标识元数的类型,本例是IMetaData接口,所以,前面我为什么不用指定IMetaData的原因,因为这里会指定,MEF会沿着这个线索自动搜索它的属性BallType。

    在实例化CompositionContainer容器后,要记得调用扩展方法ComposeParts,告诉MEF,所有扩展的程序集将和当前实例进行组合,不然你将无法调用。

    现在,你运行一个这个WinForm,你就明白了。

    看到了吧,FootBall和VolleyBall类所在的两个程序集我并没有在项目中,引用,只是把它们扔到Ext目录下,应用程序就自动识别了。

    我们的WinForm程序不用修改一行代码。

    如果你还不信的话,我们接下来再增加一个dll,定义一个BasketyBall(篮球类),然后,把这个篮球类库也生成一个dll,同样扔到Ext目录下,而WinForm程序我根本不需要改动。

    [csharp] view plain copy
     
    1. using System;  
    2. using System.Collections.Generic;  
    3. using System.Linq;  
    4. using System.Text;  
    5. using System.Threading.Tasks;  
    6. using System.ComponentModel.Composition;  
    7. using System.ComponentModel.Composition.Hosting;  
    8.   
    9. namespace BallLibC  
    10. {  
    11.     /// <summary>  
    12.     /// 篮球  
    13.     /// </summary>  
    14.     [Export(typeof(CommonLib.IBall))]  
    15.     [ExportMetadata("BallType","Basket Ball")]  
    16.     public class BasketBall:CommonLib.IBall  
    17.     {  
    18.         public string GetInformation()  
    19.         {  
    20.             return "篮球";  
    21.         }  
    22.     }  
    23. }  

    同样道理,把这个类库编译成dll,然后扔到Ext文件下,然后你再运行一下WinForm程序看看。

    看到了吧,我没有对WinForm做任何修改,只是在Ext目录下多放了一个dll而已,运行后,程序就自动识别并找到对应的类型了。下拉列表框中就自动多了一个Basket Ball的选项了。选择它,并单击按钮,这个BadketBall类就被执行了,输出“篮球”。

    以此类推,你再添加一千个一万个dll,只要它符合IBall接口规范并设置导出,然后把这一千个一万个dll全放到Ext目录下,应用程序不需要做任何修改,运行后就会自动找到一千个一万个扩展类了。

    这样一来,是不是节约了不少维护和升级成本了?MEF(Managed Extensibility Framework)强大吧?

  • 相关阅读:
    pymongo
    活动专题,各种手机的适配
    怎么判断是qq浏览器还是uc浏览器?
    并的用法,存在并执行~~
    window.location.href在微信端不起作用的解决方法?
    div双击全屏,再双击恢复到原来的状态vue,js来做
    avalon结合原生js tab切换
    vue指令大全~~~
    jquery中$.get()如何让跨域请求携带cookie?
    git 的使用方法以及要注意的地方~/git stash的使用
  • 原文地址:https://www.cnblogs.com/weekbo/p/8682042.html
Copyright © 2020-2023  润新知