在许多需要分模块开发,较为复杂的应用项目(如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接口就是与球类信息有关的类,提供扩展时实现该接口。
public interface IBall { string GetInformation(); }
它有一个公共方法GetInformation,返回对应球类的名字,如“足球”.
另一个接口是用来描述元数据的。
public interface IMetaData { string BallType { get; } }
为什么要定义这个元数据的接口呢?就是为了识别我们应用程序调用了哪个扩展。
比如,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
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; namespace BallLibA { /// <summary> /// 足球 /// </summary> [Export(typeof(CommonLib.IBall))] [ExportMetadata("BallType","Foot Ball")] public class FootBall:CommonLib.IBall { public string GetInformation() { return "足球"; } } }
在MEF中,我们不需要实现提供元数据的接口,只需要在ExportMetadata特性中直接为属性设置值就行,运行时会自动生成实现元数据(本例是IMetaData接口)的类。
接照同样的方法,我们再做一个类库。
/// <summary> /// 排球 /// </summary> [Export(typeof(CommonLib.IBall))] [ExportMetadata("BallType", "Volley Ball")] public class VolleyBall : CommonLib.IBall { public string GetInformation() { return "排球"; } }
现在,我们的项目已经有两个扩展了。
3、我们来实现我们的主应用程序,我们只需引用我们前面编写的公共类库中的接口即可,而扩展的dll我们不需要引用,MEF会自动寻找。因此,把所有扩展的程序集都生成dll文件,然后统一扔到与exe文件同一位置的Ext文件夹中就行了,你有1000个dll就全部扔到文件夹里就行,MEF会自动寻找。
我们用一个WinForm程序作为主程序,如下图所示。
在程序运行时,会根据Ext目录下的所有扩展的dll自动发现所有程序集,然后显示在ComboBox中,我们选择对应的球类,然后点击按钮,这样在文本框中就会对应地显示球类的名称。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; namespace TestApp { public partial class Form1 : Form { CompositionContainer myContainer = null; // 引入的组合类型 [ImportMany] IEnumerable<Lazy<CommonLib.IBall, CommonLib.IMetaData>> mBalls; public Form1() { InitializeComponent(); DirectoryCatalog catl = new DirectoryCatalog("Ext"); myContainer = new CompositionContainer(catl); try { myContainer.ComposeParts(this);//组合组件 } catch (Exception ex) { MessageBox.Show(ex.Message); } var resBalls = (from t in mBalls select t.Metadata.BallType).ToArray(); this.comboBox1.DataSource = resBalls; } private void button1_Click(object sender, EventArgs e) { if (this.comboBox1.SelectedIndex == -1) { MessageBox.Show("请选择一个扩展。"); return; } string ballName = this.comboBox1.SelectedItem.ToString(); // 取出要执行哪个扩展程序集 var ballInstance = mBalls.FirstOrDefault(x => x.Metadata.BallType == ballName); if (ballInstance != null) { this.txtResult.Text = ballInstance.Value.GetInformation(); } } } }
从上面的代码中,可以总结出MEF的用法,这方法你有兴趣的话可以背下来,因为无论你用到什么项目,思路都是一样的。
1、声明一个CompositionContainer变量是必须的,因为它可以用来指示当前应用程序与哪些扩展程序集进行合并。
2、在实例化CompositionContainer时,我使用DirectoryCatalog类,为什么?因为这个类好用,你只需要告诉它你扩展的dll放在哪个文件夹就行了。它会在你指定的文件夹里面自动找到导出的扩展类。
3、有导出类,自然就有导入类,因为我们的所有扩展都是实现IBall接口的,所以,扩展的类的导出类型应使用IBall,这样,凡是声明为导出类的都会被MEF发现并自动加载。
所以,导出是针对扩展的程序集而言的,那导入就好理解了,就是针对我们的主应用程序而言,像本例,WinForm应用作为主程序,所有扩展都是在这个WinForm中使用的,所以这个WinForm就必须对类型进行导入。因此才有了以下代码。
// 引入的组合类型 [ImportMany] 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程序我根本不需要改动。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; namespace BallLibC { /// <summary> /// 篮球 /// </summary> [Export(typeof(CommonLib.IBall))] [ExportMetadata("BallType","Basket Ball")] public class BasketBall:CommonLib.IBall { public string GetInformation() { return "篮球"; } } }
同样道理,把这个类库编译成dll,然后扔到Ext文件下,然后你再运行一下WinForm程序看看。
看到了吧,我没有对WinForm做任何修改,只是在Ext目录下多放了一个dll而已,运行后,程序就自动识别并找到对应的类型了。下拉列表框中就自动多了一个Basket Ball的选项了。选择它,并单击按钮,这个BadketBall类就被执行了,输出“篮球”。
以此类推,你再添加一千个一万个dll,只要它符合IBall接口规范并设置导出,然后把这一千个一万个dll全放到Ext目录下,应用程序不需要做任何修改,运行后就会自动找到一千个一万个扩展类了。
这样一来,是不是节约了不少维护和升级成本了?MEF(Managed Extensibility Framework)强大吧?