科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道基础软件在C#程序中实现插件架构

在C#程序中实现插件架构

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

这篇文章将讲述如何利用这些奇妙的特性,用插件(plug-ins)机制建立可扩展的解决方案

作者:Sunmast翻译 来源:论坛 2007年11月13日

关键字:

  • 评论
  • 分享微博
  • 分享邮件
插件(Plug-Ins)

  上面的示例程序包括了两个插件的执行.这些插件在EmployeePlug.cs和CustomerPlug.cs中定义.列表三展示了EmployeePlug类的部分定义.下面是一些关键点.

  1.这个类实现了IPlug接口.由于主程序根本不会知道插件内部的类是如何定义的,这非常重要,主程序需要使用IPlug接口和各个插件通信.这种设计利用了面向对象概念里面的"多态性".多态性允许运行时,可以通过指向基类的引用,来调用实现派生类中的方法.

  2.这个类被两个属性标识,这样主程序可以判断这个插件是不是有效的.在C#中,要给一个类标识一个属性,你得在类的定义之前声明属性,内容附在括号内.

  3.简明起见,例子只是使用了直接写入代码的数据.而如果这个插件是个正式的产品,那么数据总是应该放在数据库中或者文件中,各自所有的数据都应该仅仅由插件本身来管理.EmployeePlug类的数据在这里用EmployeeData对象来存储,那也是一个类型并且实现了IPlugData接口.IPlugData接口在IPlugData.cs中定义,它提供了最基础的数据交换功能,用于主程序和插件之间的通讯.所有支持IPlugData接口的对象在下层数据变化的时候将提供一个通知.这个通知实际上就是DataChanged事件的发生.

  4.当主程序需要显示某个插件所含数据列表的时候,它会调用GetData方法.这个方法返回IPlugData对象的一个数组.这样主程序就可以对数组中的每个对象使用ToString方法得到数据以建立树的各个节点.ToString方法是EmployeeData类的一个重载,用于显示雇员的名字.

  5.IPlug接口也定义了Save和Print方法.定义这两个方法的目的在于当有需要打印或者保存数据的时候,要通知一个插件.EmployeePlug类就是用于实现打印和保存数据的功能的.在使用Save方法的时候,需要保存数据的位置将会在方法调用的时候提供.这里假设主程序会向用户查询路径等信息.路径信息的查询是主程序提供给各个插件的服务.对于Print方法,主程序将把选项和内容传递到System.Drawing.Printing.PrintDocument类的实例.这两种情况下,和用户的交互操作都是一致的由主程序提供的.

  反射(Reflection)

  在一个插件定义好之后,下一步要做的就是查看主程序是怎么加载插件的.为了实现这个目标,主程序使用了反射机制.反射是.NET中用于运行时查看类型信息的.在反射机制的帮助下,类型信息将被加载和查看.这样就可以通过检查这个类型以判断插件是否有效.如果类型通过了检查,那么插件就可以被添加到主程序的界面中,就可以被用户操作.

  示例程序使用了.NET框架的三个内置类来使用反射:System.Reflection.Assembly,System.Type,和System.Activator.

  System.Reflection.Assembly类描述了.NET的程序集.在.NET中,程序集是配置单元.对于一个典型的Windows程序,程序集被配置为单一的Win32可执行文件,并且带有特定的附加信息,使之适应.NET运行环境.程序集也可以配置为Win32的DLL(动态链接库),同样需要带有.NET需要的附加信息.System.Reflection.Assembly类可以在运行的时候取得程序集的信息.这些信息包括程序集包含的类型信息.

  System.Type类描述了类型定义.一个类型声明可以是一个类,接口,数组,结构体,或者枚举.在加载了一个类之后,System.Type类可以被用于枚举该类支持的方法,属性,事件和接口.

  System.Activator类用于创建一个类的实例.

  加载插件

  列表四展示了LoadPlugs方法.LoadPlugs方法在HostForm.cs中定义,是HostForm类的一个private的非静态方法.LoadPlugs方法使用.NET的反射机制来加载可用的插件文件,并且验证它们是否符合被主程序使用的要求,然后把它们添加到主程序的树形显示区中.这个方法包含了下面几个步骤:

  1.通过使用System.IO.Directory类,我们的代码可以用通配符来查找所有的以.plug为扩展名的文件.而Directory类的静态方法GetFiles能够返回一个System.String类型的数组,以得到每个符合要求的文件的物理路径.
  
  2.在得到路径字符串数组之后,就可以开始把文件加载到System.Reflection.Assembly实例中了.建立Asdsembly对象的代码使用了try/catch代码块,这样如果某个文件并不是一个有效地.NET程序集,就会抛出异常,程序此时将弹出一个MessageBox对话框,告诉用户无法加载该文件.循环一直进行直到所有文件都已遍历完成.

  3.在一个程序集加载之后,代码将遍历所有可访问到的类型信息,检查是否支持了HostCommon.IPlug接口.

  4.如果所有类型都支持HostCommon.IPlug接口,那么代码继续验证这些类型,检查是否支持那些已预先为插件定义好的属性.如果没有支持,那么一个HostCommon.PlugNotValidException类型的异常将会被抛出,同样,主程序将会弹出一个MessageBox,告诉用户出错的具体信息.循环一直进行直到所有文件都已遍历完成.

  5.最后,如果这些类型支持HostCommon.IPlug接口,也已定义了所有需要定义的属性,那么它将被包装为一个PlugTreeNode实例.这个实例就会被添加到主程序的树形显示区.

  实现

  主程序框架被设计为两个程序集.第一个程序集是Host.exe,它提供了主程序的Windows窗体界面.第二个程序集是HostCommon.dll,它提供了主程序和插件之间进行通信所需的所有类型定义.比如,IPlug接口就是在HostCommon.dll里面配置的,这样它可以被主程序和插件等价的访问.这两个程序集在一个文件夹内,同样的,附加的作为插件的程序集也需要被配置在一起.那些程序集被配置在plugs文件夹内(主程序目录的一个子文件夹).EmployeePlug类在Employee.plug程序集中定义,而CustomerPlug类在Customer.plug程序集中定义.这个例子指定插件文件以.plug为扩展名.事实上这些插件就是个普通的.NET类库文件,只是通常库文件使用.dll扩展名,这里用.plug罢了.特殊的扩展名对于程序运行是完全没有影响的,但是它可以让用户更明确的知道这是个插件文件.

  设计的比较

  并不是一定要像例子程序这样设计才算正确的.比如,在开发一个带有插件的C#程序时,并不一定需要使用属性.例子里使用了两个自定义的属性,其实也可以新定义两个IPlug接口的参数来实现.这里选择用属性,是因为插件的名字和它的描述在本质上确实就是一个事物的属性,符合规范.当然了,使用属性会造成主程序需要更多的关于反射的代码.对于不同的需求,设计者总是需要做出合理的决定.

  总结

  示例程序被设计为尽量的简单,以帮助理解主程序和插件之间的通信.在实际做产品的时候,可以做很多的改进以满足实用要求.比如:

  1.通过对IPlug接口增加更多的方法,属性,事件,可以增加主程序和插件之间的通信点.两者间的更多的交互操作使得插件可以做更多的事情.

  2.可以允许用户主动选择需要加载的插件.
    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章