利用插件系统从头开发项目
本文将介绍在插件系统中,如何划分项目结构、定义软件UI框架(shell),以及和插件交互相关的接口定义方式。本文的重点不是如何开发一个plugin framework,是如何使用plugin framework。
下载
示例代码中有两个例子:
源码中附 基于OSGi.net快速开发插件化的Winform&WPF应用 简介.docx
介绍
目前有很多成熟的Plugin Framework,比如MEF、SCSF、 Sharpdevelop和OSGi.net等,他们在功能上各有特色,但无论哪种框架,使用的时候要考虑的事情基本相似,包括:
我们将以OSGi.net作为plugin framework简单介绍上面几点如何实现。
何时加载插件
一个完整的插件系统通常由两部分组成,启动程序+各种插件。启动程序也就是main函数所在的工程,通常只负责创建插件容器,并让插件加载插件。因此对于"何时加载插件?"这个问题的回答很简单,就是main函数里。不过这只适用于简单的系统,大型的系统通常需要热插拔,可以在系统正在运行过程中动态的安装、卸载插件。
组织项目结构的时候,建议最先创建一个Startup工程,里面仅仅包含main函数,main函数只需要做2件事,
备注:动态安装、卸载插件的功能取决于你所使用的plugin framework是否支持,像MEF、Mono Addin、SCSF等没有提供原生的热插拔支持,OSGi支持,不过如何动态安装、卸载不是本文的重点。
定义软件基本布局
定义软件的基本布局也就是定义软件的外观和用户体验是什么样的。理想的情况下,插件不应该依赖主程序的UI,但实际上,为了保持软件风格的统一,插件中UI的设计不可避免的受主程序的影响。比如主程序采用DockPannel方式布局,那么插件中UI的设计或多或少的也需要和DockPannel的风格保持一致。定义主程序的外观布局通常需要考虑如下几方面,
下面举几个常见的软件布局为例,
图1
图2
在本例子中我们的示例软件的布局如下:
1为菜单和工具栏,2为导航栏,3是工作区。
图3
插件如何展示到主窗口上
在上图3中,如果要实现菜单(区域1)是可动态扩展的,通常的做法是从配置文件中读取菜单配置项,然后动态创建菜单。在OSGi中,每个可以扩展的配置叫做扩展点,扩展点有个唯一的名字,这样可以在不同的plugin中进行扩展,然后主程序启动的时候会收集这些扩展信息,动态创建菜单。
OSGi的一个非常有用的功能是插件内核会一直监视每个扩展点的变化,这样主程序可以侦听扩展点变化的事件,当插件被动态安装、卸载时,动态改变UI的菜单项。除了OSGi外,我暂时还没有发现其它插件系统提供对扩展点的监视功能,不过开发小型的系统可能对这个功能的要求不高。
本文的例子中,系统定义了一个叫"MainMenu"的菜单扩展点,每个plugin可以往此菜单中添加自定义的菜单项,稍后将详细介绍如何扩展点的定义方式。
插件怎样和主窗口交互-Shell的诞生
上一节中已经介绍了如何将菜单动态添加到系统中,本例子中,有一个叫做"Monitor"的插件,它希望往菜单栏动态添加一个叫"Tools"的菜单,菜单如下:
图4
当点击子菜单的"Monitor"后,弹出报表页面,效果如下:
图5
现在的问题是,点击Monitor后,如何能够把自定义的控件展现到主窗口,这就是插件系统中需要一个Shell的原因。Shell就是外壳的含义,项目结构的划分上,它通常属于一个独立的工程,因此我们需要创建一个工程,叫做WorkspaceShell,它的功能包含:
图6
namespace WorkspaceShell
{
public interface IWorkspace
{
void AddNavigation(NavigationItem control);
void Show(object control, object controlInfo);
}
}
这样其它插件就可以调用Show函数,展示自己的UI了。真正的产品开发中,还需要Close等很多函数。
下面是需要注意的地方,
扩展点的定义
按照OSGi的规范,每个plugin都有一个Manifest.xml文件,它是描述每个插件的清单文件,定义了插件的名称、入口点、依赖、扩展点等。扩展点的定义非常简单,取一个唯一名,然后定义扩展点的格式。仍以菜单为例,一个菜单项一般有Text、Icon和事件处理类。本例子中,WorkspaceShell定义了几个扩展点,方式如下:
<?xml version="1.0" encoding="utf-8"?>
<Bundle xmlns="urn:uiosp-bundle-manifest-2.0" SymbolicName="WorkspaceShell" InitializedState="Active">
<Activator Type="WorkspaceShell.Activator" Policy="Immediate" />
<Runtime>
<Assembly Path="WorkspaceShell.dll" Share="false" />
</Runtime>
<ExtensionPoint Point="ToolBar"/>
<ExtensionPoint Point="MainMenu"/>
<ExtensionPoint Point="Navigation"/>
</Bundle>
扩展点定义好后,就是使用了,本例中,我们有另外一个插件叫"Monitor"提供对菜单、导航栏的扩展,并在点击菜单时需要在workspace中显示自定义控件,"Monitor"的Manifest.xml如下:
可以看到只需要本plugin的菜单放到Extension节点下,并制定Point名称。ToolbarItem节点是每个菜单项的定义,它的class代表点击按钮后要执行的命令,命令定义如下:
public class MonitorCommand : IViewCommand
{
protected UserControl1 _view = new UserControl1();
public void Run()
{
IWorkspace workspace = BundleRuntime.Instance.GetFirstOrDefaultService<IWorkspace>();
workspace.Show(_view, null);
}
}
其中IViewCommand接口是WorkspaceShell定义的。
插件间如何交互
场景1:Plugin1 提供数据访问的服务,如何在Plugin2使用此服务?
服务的共享是插件系统最常见的使用场景,下面给出常见的几种实现,
public ICustomerManager
{
string CreateCustomer(CustomerInfo info);
}
CustomerManagerImpl: ICustomerManager
{
…
}
context.AddService<ICustomerManager>(new CustomerManagerImpl());
var service = BundleRuntime.Instance.GetFirstOrDefaultService< ICustomerManager>();
service.CreateCustomer(…);
上面的方法基本适应于任何系统,如果使用OSGi的话,前两部可以通过在Manifest.xml中的一句简单配置完成。
场景2:Plugin1 创建了一个订单,负责订单处理的Plugin如何获取到通知?
这属于插件通信的一种场景,但实际的产品中,plugin1和plugin2可能在不同的服务器中运行,通常采用消息总线(又叫Message bus/service bus/event bus)来解决。它的原理比较简单,plugin1发布消息,plugin2侦听消息,这个消息既可以是本进程的,也可以是分布式的。
OSGi内置了MessageBus plugin,所以使用非常简单。其它的插件框架,可以集成第三方类库,封装成一个MessageBus的插件。如果对系统可靠性要求很高的话,使用消息总线时就要考虑MSDTC(分布式事务),我推荐使用ActiveMQ,MSMQ部署比较麻烦。
插件自动升级
插件升级是插件框架一个复杂的问题,主要的问题是:
总结
本文所描述的项目结构的划分方式适用于任何插件框架,未必是最佳方案,但是我所接触过的插件框架中最常用的。
参考