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

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

本文将介绍在插件系统中,如何划分项目结构、定义软件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件事,

  1. 加载plugin
  2. 显示主窗口

    本例中的代码如下:

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

     

    接下来在Startup工程的输出目录创建一个叫plugins的文件夹,以后其它插件的输出目录都放到这个plugins下。

备注:动态安装、卸载插件的功能取决于你所使用的plugin framework是否支持,像MEF、Mono Addin、SCSF等没有提供原生的热插拔支持,OSGi支持,不过如何动态安装、卸载不是本文的重点。

定义软件基本布局

定义软件的基本布局也就是定义软件的外观和用户体验是什么样的。理想的情况下,插件不应该依赖主程序的UI,但实际上,为了保持软件风格的统一,插件中UI的设计不可避免的受主程序的影响。比如主程序采用DockPannel方式布局,那么插件中UI的设计或多或少的也需要和DockPannel的风格保持一致。定义主程序的外观布局通常需要考虑如下几方面,

  1. 是否需要菜单?如果有菜单那么显示在什么位置?
  2. 是否需要工具栏? 是否需要导航栏?
  3. 菜单、工具栏和工作区如何摆放?

下面举几个常见的软件布局为例,

  1. 记事本很简单,主要有菜单和工作区两部分。

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

图1

  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,它的功能包含:

  1. 定义软件布局,本例子中的,我们在WorkspaceShell工程中创建了一个叫ShellForm的窗口,它将作为本程序的主窗口,如下:

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

    图6

  2. 监听扩展点信息,动态绘制UI。图6中的菜单栏会根据"MainMenu"下的扩展信息动态绘制菜单。
  3. 定义插件间、插件和Shell直接交互的接口。插件如果需要能够将自定义控件放到图6中,我们需要定义接口IWorkspace,

    namespace WorkspaceShell

    {

    public interface IWorkspace

    {

    void AddNavigation(NavigationItem control);

    void Show(object control, object controlInfo);

    }

    }

    这样其它插件就可以调用Show函数,展示自己的UI了。真正的产品开发中,还需要Close等很多函数。

下面是需要注意的地方,

  1. 这个WorkspaceShell同样也是一个plugin,因此如果以后不想用WorkspaceShell作为主程序的Shell,只需要开发一个其它plugin替换而已。
  2. 由于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定义的。

插件间如何交互

场景1Plugin1 提供数据访问的服务,如何在Plugin2使用此服务?

服务的共享是插件系统最常见的使用场景,下面给出常见的几种实现,

  1. 通过服务容器,通过接口的方式交互,步骤为:
    1. Plugin1定义并实现数据访问的接口,例如:

    public ICustomerManager

    {

    string CreateCustomer(CustomerInfo info);

    }

    CustomerManagerImpl: ICustomerManager

    {

    }

    1. Plugin1启动时将ICustomerManager注册到插件的服务容器中,比如:

    context.AddService<ICustomerManager>(new CustomerManagerImpl());

    1. Plugin2中用如下方式获取服务实例,

    var service = BundleRuntime.Instance.GetFirstOrDefaultService< ICustomerManager>();

    service.CreateCustomer(…);

    上面的方法基本适应于任何系统,如果使用OSGi的话,前两部可以通过在Manifest.xml中的一句简单配置完成。

  2. 通过IoC方式,在plugin2中创建对象时,获取到plugin1提供的服务,这种方式最简单,也是我推荐的方式。但需要插件框架支持IoC,如果框架本身没有提供,需要自己手动封装。

场景2Plugin1 创建了一个订单,负责订单处理的Plugin如何获取到通知?

这属于插件通信的一种场景,但实际的产品中,plugin1plugin2可能在不同的服务器中运行,通常采用消息总线(又叫Message bus/service bus/event bus)来解决。它的原理比较简单,plugin1发布消息,plugin2侦听消息,这个消息既可以是本进程的,也可以是分布式的。

OSGi内置了MessageBus plugin,所以使用非常简单。其它的插件框架,可以集成第三方类库,封装成一个MessageBus的插件。如果对系统可靠性要求很高的话,使用消息总线时就要考虑MSDTC(分布式事务),我推荐使用ActiveMQMSMQ部署比较麻烦。

插件自动升级

插件升级是插件框架一个复杂的问题,主要的问题是:

  1. Plugindll被使用时不能直接覆盖,升级。除非每个plugin在一个AppDomain中,把这个app domain卸载掉,这个方法基本没有可行性,因为基于性能、易用性等考虑,不可能把每个plugin独立放到一个AppDomain中。
  2. Plugin1依赖Plugin21.0版本,Plugin2升级到2.0后,Plugin1可能就不再可用。这就是插件件依赖和依赖解析的问题,OSGi提供了依赖解析的功能,当Plugin1的依赖不再满足时,就不允许Plugin1启动,不过这种场景只在中大型项目中出现。
  3. 自动升级,当有新的插件出现时,系统能够自动下载最新版本,并在下次系统启动时将插件升级到最新版。同样因为只有中大型系统才有这样的需求,所以目前只发现OSGi.net提供此插件仓库,和"一键部署"的功能。

总结

本文所描述的项目结构的划分方式适用于任何插件框架,未必是最佳方案,但是我所接触过的插件框架中最常用的。

参考

OSGi.NET 学习笔记

你可能感兴趣的:(插件)