概述
所谓的“插件”就是代表一个功能模块,插件的配置就是描述该插件并指定如何把这个插件挂到系统中。也就是每一个插件在系统中都有一个扩展点的路径。
例如:
<
AddIn
name
= "AddinTreeView"
author
= "MALONG"
copyright
= "GPL"
url
= "http://www.CCC.net"
description
= "Display AddinTree"
version
= "1.0.0">
<
Runtime
>
<
Import
assembly
="../../bin/ AddinTreeView.dll"/>
</
Runtime
>
<
Extension
path
= "/Gdesigner/Workbench/MainMenu/Tools">
<
MenuItem
id
= "AddinTreeView"
label
= "View AddinTree"
class
= "Addins.AddinTreeView.AddinTreeViewCommand"/>
</
Extension
>
</
AddIn
>
在配置文件中,Runtime节指定了插件功能模块所在的库文件Addins.dll的具体路径,在Extension节中指定了扩展点路径/
Gdesigner
/Workbench/MainMenu/Tools
(是打算把它挂到主菜单的工具菜单下),然后在Extension内指定了它的Codon为 MenuItem以及具体的ID、标签、Command类名。
如果我们对于每一个插件都编写这样的一个配置文件,那么插件的库文件(.dll)、插件配置文件(.addin)是一一对应的。不过这样就带来了一个小小的问题,在这样的一个以插件为基础的系统中,每一个菜单、工具栏按钮、窗体、面板都是一个插件,那么我们需要为每一个插件编写配置文件,这样就会有很多个配置文件(似乎有点太多了,不是很好管理)。于是我们把多个插件的配置合并在一个插件的配置文件中。因此,我把我的两个插件库文件合并到一个Addins工程内生成了Addins.dll,又重新编写了插件配置文件MyAddins.addin如下:
<
AddIn
name
= "MyAddins"
author
= "SimonLiu"
copyright
= "GPL"
url
= "http://www.ccc.net"
description
= "Display AddinTree"
version
= "1.0.0">
<
Runtime
>
<
Import
assembly
="../../bin/Addins.dll"/>
</
Runtime
>
<
Extension
path
= "/Gdesigner/Workbench/MainMenu/Tools">
<
MenuItem
id
= "ResourceEditor"
label
= "Resource Editor"
class
= "Addins.ResourceEditor.Command.ResourceEditorCommand"/>
<
MenuItem
id
= "AddinTreeView"
label
= "View AddinTree"
class
= "Addins.AddinTreeView.AddinTreeViewCommand"/>
</
Extension
>
</
AddIn
>
这样,把两个插件的功能模块使用一个插件配置文件来进行配置。同样的,我也可以把几十个功能模块合并到一个插件配置文件中。
我们回过头来看一下,现在我们有了两颗树。首先,插件树本身是一个树形的结构,其次,插件的配置文件本身也具有了一个树形的结构,这个树结构的根节点是系统的各个插件配置文件,其下是根据这个配置文件中的Extension节点的来构成的。
总结一下插件的配置文件格式。首先是 <AddIn>节点,需要指定AddIn的名称、作者之类的属性。其次,在AddIn节点下的<Runtime>节点内,使用<Import …>来指定本插件配置所在的库文件。如果分布在多个库文件中,可以一一指明。然后,编写具体功能模块的配置。每个功能模块的配置都以扩展点<Extension>开始,指定了路径(Path)属性之后,在这个节点内配置在这个扩展点下具体的库文件。每个库文件根据具体不同的实现有不同的属性。
2、结构
1
、AddInTree 插件树
插件被组织成一棵插件树结构,树的结构是通过 Extension(扩展点)中定义的Path(路径)来定义的,类似一个文件系统的目录结构。系统中的每一个插件都在配置文件中指定了 Extension,通过Extension中指定的 Path 挂到这棵插件树上。
2
、 AddIn 插件
插件是包含多个功能模块的集合。在文件的表现形式上是一个addin配置文件,在系统中对应 AddIn 类进行管理。
3
、Extension 扩展点
每一个插件都会被挂到 AddInTree(插件树) 中,而具体挂接到这个插件树的哪个位置,则是由插件的 Extension 对象中的 Path 指定的。在addin 配置文件中,对应于 <Extension> 。例如下面这个功能模块的配置
<
Extension
path
= "/Gdesigner/Workbench/Ambiences">
<
Class
id
= ".NET"
class
= "IGdesigner.Gdesigner.Services.NetAmbience"/>
</
Extension
>
指定了扩展点路径为 /
Gdesigner
/Workbench/Ambiences
,也就是在插件树中的位置。
4
、包装功能模块
为了方便访问各个插件中的功能模块,给各种功能定义了基本的属性,分别是 ID (功能模块的标识),Name (功能模块的类型。别误会,这个Name 是addin文件定义中的XML结点的名称,ID才是真正的名称),其中Name可能是Class(类)、MenuItem(菜单项)、Pad(面板)等等。根据具体的功能模块。在addin定义文件中,对应于 <Extension> 标签下的内容。例如下面这个定义
<
Extension
path
= "/Gdesigner/Workbench/Ambiences">
<
Class
id
= ".NET"
class
= "IGdesigner.Gdesigner.Services.NetAmbience"/>
</
Extension
>
<Extension ...>
内部定义了一个功能模块,<Class ...> 表示该功能模块是一个 Class(类),接着定义了该功能模块的 ID和具体实现该功能模块的类名。运行期间将通过反射来找到对应的类并创建出来,这一点也是我们无法在以前的语言中实现的。
再例如这一个定义
<
Extension
path
= "/ Gdesigner /Views/ProjectBrowser/ContextMenu/CombineBrowserNode">
<
MenuItem
id
= "Compile"
label
= "${res:XML.MainMenu.RunMenu.Compile}"
class
= " IGdesigner. Gdesigner.Commands.Compile"/>
<
MenuItem
id
= "CompileAll"
label
= "${res:XML.MainMenu.RunMenu.CompileAll}"
class
= " IGdesigner. Gdesigner.Commands.CompileAll"/>
<
MenuItem
id
= "CombineBuildGroupSeparator"
label
= "-"
/>
.
</
Extension
>
这个扩展点中定义了三个菜单项,以及各个菜单项的名字、标签和实现的类名。
5
、
Command
命令
正如前文所述,
功能模块
描述了一个功能模块,而每个功能模块都是一个 ICommand 的实现。最基本的 Command 是 AbstractCommand,根据
功能模块
的不同对应了不同的 Command。例如 MenuItemCodon 对应 MenuItemCommand 等等。
总结如下,定义一个接口ICommand,声明void DoCommand()方法,新增插件必须实现此接口;单击菜单项或工具栏按钮时需要与主窗体交互,这可以通过在ICommand中定义属性MainForm或在void DoCommand(MainForm frm)中增加方法参数来传递主窗体的引用,这些实现起来倒也简单。接下来的问题是如何通知应用程序新增加了插件呢,答案是使用xml配置文件,怎么组织这个配置文件的结构呢?这个问题其实成了实现插件功能的重点和难点,配置文件中希望说明新增插件的dll位置、类名、插接入主程序的菜单还是工具栏项、插接位置。
Command模式应用
在剖析
Command
模式之前,先来看一下
.NET Framework
中的菜单及工具栏处理存在的一些缺点:
1.
无法自动同步更新菜单项与工具项的状态以及相关的行为及特性,需要手动去处理两个事件,从而造成了相同的两个操作需要两次处理,也就造成了需要手动地同时维护多份相同的操作;
2.
无法重用菜单项这部分的功能,因为在
dotnet
中菜单项的单击需要集成在
UI
中;
3.
无法很好地扩展,如需要增加或删除菜单项,则需要更改原有的代码及
UI
部分的设计,从而违反了
OCP(
开闭
)
原则。按照
OCP
原则,当扩展相关的功能时,不应该修改原始的代码,而应该扩展该代码;
4.
无法将相关的消息当作原子状态处理,试想一下,如果一个系统不仅通过菜单、工具栏来操作用户接口,也可通过命令(如
Visual Studio
就可以通过输入命令而执行相关的操作)来执行,如果不将相关的操作抽象出来的话,维护这个操作的成本太大。并且如果想实现更进一步的控制比如精确到原子状态的权限控制,则很难实现。
我们可以运用Command模式,将菜单项相应的操作抽象出来,定义一个命令接口ICommand,此接口仅具有Run方法,可以作为任何命令的基接口。但是由于菜单还具有一些其它的特性,如是否可见,是否可用,文本,需要执行此菜单项的宿主,所以再定义了一个IMenuCommand接口继承此接口,加入了一些新的特性,并定义了一个实现IMenuCommand接

口的抽象基类
MenuCommand
,其它类可以派生此类。
ICommand
接口:是一个最基本的接口,代表一个动作的行为;
IMenuCommand
接口:派生于
ICommand
接口,作为菜单项或工具项所实现的行为及特性来使用;
IStatusUpdate
接口:描述一个能够更新状态的接口,当对象需要更新状态时将调用此接口,如菜单项需要动态地更新
Enable
,
Visible
特性;
MenuCommand抽象基类:实现了IMenuCommand接口,任何菜单命令可以从此继承;
CustomCommandBarItem:继承于CommandBar组件的CommandBarButton类,类似于菜单项或工具项,实现了IStatusUpdate接口,以根据IMenuCommand接口的Visible、Enabled属性的改变而相应地更新其Enabled,Visible属性;

CustomCommandBarMenu:继承于CommandBar组件的CommandBarMenu类,类似于菜单条。
当我们需要创建命令时,只需写相关的类继承于MenuCommand
在这里可以设计组件
CommandBar。CustomCommandBarItem类的构造函数通过传入一个IMenuCommand类的实例与IMenuCommand关联起来,将单击事件(OnClick方法)委托给ICommand接口的Run方法来执行,其Update方法用于更新状态。CustomCommandBarMenu类中的OnDropDown方法用于当打开菜单条时通过调用IStatusUpdate的Update方法自动更新所有菜单项的状态。
可以将权限控制写在IMenuCommand中的Enabled中,在执行Run方法之前先检测Enabled,如果Enabled为false则禁止执行,我先前写过一个权限框架,通过Attribute及Reflect技术来实现权限的自动控制。