好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

利用插件系统从头开发项目

利用插件系统从头开发项目

利用插件系统从头开发项目

利用插件系统从头开发项目

本文将介绍在插件系统中,如何划分项目结构、定义软件UI框架(shell),以及和插件交互相关的接口定义方式。本文的重点不是如何开发一个plugin framework,是如何使用plugin framework。

下载 

基于OSGi.net的Winform Shell示例代码(C#) OSGi.net SDK下载

示例代码中有两个例子:

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/

    

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

版权信息

查看更多关于利用插件系统从头开发项目的详细内容...

  阅读:41次