利用插件系统从头开发项目
利用插件系统从头开发项目
利用插件系统从头开发项目
本文将介绍在插件系统中,如何划分项目结构、定义软件UI框架(shell),以及和插件交互相关的接口定义方式。本文的重点不是如何开发一个plugin framework,是如何使用plugin framework。
下载
示例代码中有两个例子:
SimpleShell.sln,本文就是基于此例子讲解如何使用plugin framework,仅包含最简单的插件使用方式。截图:
DockPanelShell.sln,基于DockPanel开发的更加复杂的Shell。截图:
源码中附 基于OSGi.net快速开发插件化的Winform&WPF应用 简介.docx
介绍
目前有很多成熟的Plugin Framework,比如MEF、SCSF、 Sharpdevelop和OSGi.net等,他们在功能上各有特色,但无论哪种框架,使用的时候要考虑的事情基本相似,包括:
何时加载插件(插件的生命周期) 定义软件基本布局(也就是主程序、主窗口的布局) 插件如何展示到主窗口上 插件怎样和主窗口交互-Shell的诞生 扩展点的定义 插件间如何交互 插件自动升级我们将以OSGi.net作为plugin framework简单介绍上面几点如何实现。
何时加载插件
一个完整的插件系统通常由两部分组成,启动程序+各种插件。启动程序也就是main函数所在的工程,通常只负责创建插件容器,并让插件加载插件。因此对于"何时加载插件?"这个问题的回答很简单,就是main函数里。不过这只适用于简单的系统,大型的系统通常需要热插拔,可以在系统正在运行过程中动态的安装、卸载插件。
组织项目结构的时候,建议最先创建一个Startup工程,里面仅仅包含main函数,main函数只需要做2件事,
加载plugin显示主窗口
本例中的代码如下:
接下来在Startup工程的输出目录创建一个叫plugins的文件夹,以后其它插件的输出目录都放到这个plugins下。
备注: 动态安装、卸载插件的功能取决于你所使用的plugin framework是否支持,像MEF、Mono Addin、SCSF等没有提供原生的热插拔支持,OSGi支持,不过如何动态安装、卸载不是本文的重点。
定义软件基本布局
定义软件的基本布局也就是定义软件的外观和用户体验是什么样的。理想的情况下,插件不应该依赖主程序的UI,但实际上,为了保持软件风格的统一,插件中UI的设计不可避免的受主程序的影响。比如主程序采用DockPannel方式布局,那么插件中UI的设计或多或少的也需要和DockPannel的风格保持一致。定义主程序的外观布局通常需要考虑如下几方面,
是否需要菜单?如果有菜单那么显示在什么位置? 是否需要工具栏? 是否需要导航栏? 菜单、工具栏和工作区如何摆放?下面举几个常见的软件布局为例,
记事本很简单,主要有菜单和工作区两部分。
图1
VS的布局比较复杂,主要包括1菜单,2工具栏,3工作区,4导航栏。
图2
在本例子中我们的示例软件的布局如下:
1为菜单和工具栏,2为导航栏,3是工作区。
图3
插件如何展示到主窗口上
在上图3中,如果要实现菜单(区域1)是可动态扩展的,通常的做法是从配置文件中读取菜单配置项,然后动态创建菜单。在OSGi中,每个可以扩展的配置叫做扩展点,扩展点有个唯一的名字,这样可以在不同的plugin中进行扩展,然后主程序启动的时候会收集这些扩展信息,动态创建菜单。
OSGi的一个非常有用的功能是插件内核会一直监视每个扩展点的变化,这样主程序可以侦听扩展点变化的事件,当插件被动态安装、卸载时,动态改变UI的菜单项。除了OSGi外,我暂时还没有发现其它插件系统提供对扩展点的监视功能,不过开发小型的系统可能对这个功能的要求不高。
本文的例子中,系统定义了一个叫" MainMenu "的菜单扩展点,每个plugin可以往此菜单中添加自定义的菜单项,稍后将详细介绍如何扩展点的定义方式。
插件怎样和主窗口交互-Shell的诞生
上一节中已经介绍了如何将菜单动态添加到系统中,本例子中,有一个叫做"Monitor"的插件,它希望往菜单栏动态添加一个叫"Tools"的菜单,菜单如下:
图4
当点击子菜单的"Monitor"后,弹出报表页面,效果如下:
图5
现在的问题是,点击Monitor后,如何能够把自定义的控件展现到主窗口,这就是插件系统中需要一个Shell的原因。Shell就是外壳的含义,项目结构的划分上,它通常属于一个独立的工程,因此我们需要创建一个工程,叫做WorkspaceShell,它的功能包含:
定义软件布局,本例子中的,我们在WorkspaceShell工程中创建了一个叫ShellForm的窗口,它将作为本程序的主窗口,如下:
图6
监听扩展点信息,动态绘制UI。图6中的菜单栏会根据" MainMenu "下的扩展信息动态绘制菜单。定义插件间、插件和Shell直接交互的接口。插件如果需要能够将自定义控件放到图6中,我们需要定义接口IWorkspace,
namespace WorkspaceShell
{
public interface IWorkspace
{
void AddNavigation(NavigationItem control);
void Show(object control, object controlInfo);
}
}
这样其它插件就可以调用Show函数,展示自己的UI了。真正的产品开发中,还需要Close等很多函数。
下面是需要注意的地方,
这个WorkspaceShell同样也是一个plugin,因此如果以后不想用WorkspaceShell作为主程序的Shell,只需要开发一个其它plugin替换而已。由于IWorkspace等接口需要被每个plugin使用,因此理想情况下,它需要单独放到一个接口工程。但真正的项目开发中,你会发现定义一套完全可以通用的Shell接口将会有很大的工作量,而且看起来会是过度设计,因为WPF、WebForm,Winform等依赖不同的Assembly,所以WorkspaceShell同时作为其他plugin的接口assembly使用。
扩展点的定义
按照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 使用此服务 ?
服务的共享是插件系统最常见的使用场景,下面给出常见的几种实现,
通过服务容器,通过接口的方式交互,步骤为:
Plugin1 定义并实现数据访问的接口,例如:public ICustomerManager
{
string CreateCustomer(CustomerInfo info);
}
CustomerManagerImpl: ICustomerManager
{
…
}
在 Plugin1 启动时将 ICustomerManager 注册到插件的服务容器中,比如:context.AddService<ICustomerManager>(new CustomerManagerImpl());
在 Plugin2 中用如下方式获取服务实例,var service = BundleRuntime .Instance.GetFirstOrDefaultService< ICustomerManager >();
service.CreateCustomer(…);
上面的方法基本适应于任何系统,如果使用 OSGi 的话,前两部可以通过在 Manifest.xml 中的一句简单配置完成。
通过 IoC 方式,在 plugin2 中创建对象时,获取到 plugin1 提供的服务,这种方式最简单,也是我推荐的方式。但需要插件框架支持 IoC ,如果框架本身没有提供,需要自己手动封装。场景 2 : Plugin1 创建了一个订单,负责订单处理的 Plugin 如何获取到通知?
这属于插件通信的一种场景,但实际的产品中, plugin1 和 plugin2 可能在不同的服务器中运行,通常采用消息总线(又叫 Message bus/service bus/event bus )来解决。它的原理比较简单, plugin1 发布消息, plugin2 侦听消息,这个消息既可以是本进程的,也可以是分布式的。
OSGi 内置了 MessageBus plugin ,所以使用非常简单。其它的插件框架,可以集成第三方类库,封装成一个 MessageBus 的插件。如果对系统可靠性要求很高的话,使用消息总线时就要考虑 MSDTC (分布式事务),我推荐使用 ActiveMQ , MSMQ 部署比较麻烦。
插件自动升级
插件升级是插件框架一个复杂的问题,主要的问题是:
Plugin 的 dll 被使用时不能直接覆盖,升级。除非每个 plugin 在一个 AppDomain 中,把这个 app domain 卸载掉,这个方法基本没有可行性,因为基于性能、易用性等考虑,不可能把每个 plugin 独立放到一个 AppDomain 中。 Plugin1 依赖 Plugin2 的 1.0 版本, Plugin2 升级到 2.0 后, Plugin1 可能就不再可用。这就是插件件依赖和依赖解析的问题, OSGi 提供了依赖解析的功能,当 Plugin1 的依赖不再满足时,就不允许 Plugin1 启动,不过这种场景只在中大型项目中出现。 自动升级,当有新的插件出现时,系统能够自动下载最新版本,并在下次系统启动时将插件升级到最新版。同样因为只有中大型系统才有这样的需求,所以目前只发现 OSGi.net 提供此插件仓库,和"一键部署"的功能。总结
本文所描述的项目结构的划分方式适用于任何插件框架,未必是最佳方案,但是我所接触过的插件框架中最常用的。
参考
OSGi.NET 学习笔记
作者: Leo_wl
出处: http://HdhCmsTestcnblogs测试数据/Leo_wl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
版权信息