**
三、插件系统 **
上回书说到SharpDevelop入口Main函数的结构,ServiceManager.Service在InitializeServicesSubsystem方法中首次调用了AddInTreeSingleton的AddInTree实例,AddInTree在这里进行了初始化。本回进入AddInTree着重讲述SharpDevelop的插件系统。在叙述的时候为了方便起见,对于“插件”和插件具体的“功能模块”这两个词不会特别的区分,各位看官可以从上下文分辨具体的含义(而事实上,SharpDevelop中的“插件”是指.addin配置文件,每一个“插件”都可能会包含多个“功能模块”)。
1、插件的配置
既然说到插件系统,那么我们先来看一看SharpDevelop插件系统的组织形式。
很多时候,同一个事物从不同的角度来看会得出不一样的结论,SharpDevelop的插件系统也是如此。在看SharpDevelop的代码以前,按照我对插件的理解,我认为所谓的“插件”就是代表一个功能模块,插件的配置就是描述该插件并指定如何把这个插件挂到系统中。SharpDevelop中有插件树的思想,也就是每一个插件在系统中都有一个扩展点的路径。那么按照我最初对插件的理解,编写插件需要做的就是:
A、根据插件接口编写功能模块实现一个Command类
B、编写一个配置文件,指定Command类的扩展点(Extension)路径,挂到插件树中
之后按照这样的理解,我编写了一个察看插件树的插件AddinTreeView,打算挂到SharpDevelop中去。根据SharpDevelop对插件的定义,我把具体插件的AddinTreeViewCommand实现了之后,编写了一个配置文件AddinTreeView.addin如下:
< AddIn name = "AddinTreeView"
author = "SimonLiu"
copyright = "GPL"
url = "http://www.icsharpcode.net"
description = "Display AddinTree"
version = "1.0.0" >
< Runtime >
< Import assembly ="../../bin/ AddinTreeView.dll" />
< Extension path = "/SharpDevelop/Workbench/MainMenu/Tools" >
< MenuItem id = "AddinTreeView"
label = "View AddinTree"
class = "Addins.AddinTreeView.AddinTreeViewCommand" />
在配置文件中,Runtime节指定了插件功能模块所在的库文件Addins.dll的具体路径,在Extension节中指定了扩展点路径/SharpDevelop/Workbench/MainMenu/Tools(我是打算把它挂到主菜单的工具菜单下),然后在Extension内指定了它的Codon为 MenuItem以及具体的ID、标签、Command类名。这样做,SharpDevelop运行的很不错,我的插件出现在了Tools菜单下。之后,我又编写了一个SharpDevelop的资源管理器(ResourceEditor)的插件类ResourceEditor.dll并把它挂到Tool菜单下。同样的,我也写了一个ResourceEditor.addin文件来对应。系统工作的很正常。
如果我们对于每一个插件都编写这样的一个配置文件,那么插件的库文件(.dll)、插件配置文件(.addin)是一一对应的。不过这样就带来了一个小小的问题,在这样的一个以插件为基础的系统中,每一个菜单、工具栏按钮、窗体、面板都是一个插件,那么我们需要为每一个插件编写配置文件,这样就会有很多个配置文件(似乎有点太多了,不是很好管理)。SharpDevelop也想到了这个问题,于是它允许我们把多个插件的配置合并在一个插件的配置文件中。因此,我把我的两个插件库文件合并到一个Addins工程内生成了Addins.dll,又重新编写了我的插件配置文件MyAddins.addin如下:
< AddIn name = "MyAddins"
author = "SimonLiu"
copyright = "GPL"
url = "http://www.icsharpcode.net"
description = "Display AddinTree"
version = "1.0.0" >
< Runtime >
< Import assembly ="../../bin/Addins.dll" />
< Extension path = "/SharpDevelop/Workbench/MainMenu/Tools" >
< MenuItem id = "ResourceEditor"
label = "Resource Editor"
class = "Addins.ResourceEditor.Command.ResourceEditorCommand" />
< MenuItem id = "AddinTreeView"
label = "View AddinTree"
class = "Addins.AddinTreeView.AddinTreeViewCommand" />
这样,我把两个插件的功能模块使用一个插件配置文件来进行配置。同样的,我也可以把几十个功能模块合并到一个插件配置文件中。SharpDevelop把这个插件配置文件称为“Addin(插件)”,而把具体的功能模块封装为Codon,使用Command类来包装具体的功能。SharpDevelop本身的核心配置SharpDevelopCore.addin里面就包含了所有的基本菜单、工具栏、PAD的插件配置。
我们回过头来看一下,现在我们有了两颗树。首先,插件树本身是一个树形的结构,这个树是根据系统所有插件的各个Codon的扩展点路径构造的,表示了各个Codon在插件树中的位置,各位看官可以通过我写的这个小小的AddinTreeView来看看SharpDevelop中实际的结构。其次,插件的配置文件本身也具有了一个树形的结构,这个树结构的根节点是系统的各个插件配置文件,其下是根据这个配置文件中的Extension节点的来构成的,描述了每个Extension节点下具有的Codon。我们可以通过SharpDevelop的Tools菜单下的AddinScout来看看这个树的结构。
我为了试验,把SharpDevelop的插件精简了很多,构成了一个简单的小插件系统。下面是这个精简系统的两个树的截图。各位看官可以通过这两副图理解一下插件树和插件配置文件的关系(只是看同样问题的两个角度,一个是Codon的ExtensionPath,一个是配置文件的内容)。
总结一下SharpDevelop插件的配置文件格式。首先是
1<addin>节点,需要指定AddIn的名称、作者之类的属性。其次,在AddIn节点下的<runtime>节点内,使用<import …="">来指定本插件配置中Codon所在的库文件。如果分布在多个库文件中,可以一一指明。然后,编写具体功能模块的配置。每个功能模块的配置都以扩展点<extension>开始,指定了路径(Path)属性之后,在这个节点内配置在这个扩展点下具体的Codon。每个Codon根据具体不同的实现有不同的属性。各位看官可以研究一下SharpDevelop的核心配置文件SharpDevelopCore.addin的写法,相信很容易理解的。
2
3**2、插件系统的核心AddIn和AddInTree**
4前文讲到,在SharpDevelop的Main函数中,ServiceManager.Service在InitializeServicesSubsystem方法中首次调用了AddInTreeSingleton的AddInTree实例,AddinTree在这个时候进行了初始化。现在我们就来看看AddInTreeSingleton.AddInTree到底做了些什么事情,它定义在\src\Main\Core\AddIns\AddInTreeSingleton.cs文件中。
5
6 public static IAddInTree AddInTree
7   {
8 get
9   {
10 if (addInTree == null )
11   {
12 CreateAddInTree();
13 }
14 return addInTree;
15 }
16 }
17
18AddInTreeSingleton是插件树的一个Singleton(具体的可以去看《设计模式》了),AddInTreeSingleton.AddInTree是一个属性,返回一个IAddinTree接口。这里我注意到一点,AddInTreeSingleton是从DefaultAddInTree继承下来的。既然它是一个单件模式,包含的方法全部都是静态方法,没有实例化的必要,而且外部是通过AddInTree属性来访问插件树,为什么要从DefaultAddInTree继承呢?这好像没有什么必要。这也许是重构过程中被遗漏的一个小问题吧。
19
20我们先来看看IAddinTree接口的内容,它定义了这样的几个内容:
21A、属性ConditionFactory ConditionFactory 返回一个构造条件的工厂类,这里的条件是指插件配置中的条件,我们以后再详细说明。
22B、属性CodonFactory CodonFactory 返回一个构造Codon的工厂类。
23C、属性AddInCollection AddIns 返回插件树的根节点Addin(插件)集合。
24D、方法IAddInTreeNode GetTreeNode(string path) 根据扩展点路径(path)返回对应的树节点
25E、方法void InsertAddIn(AddIn addIn) 根据AddIn中的扩展点路径添加一个插件到树中
26F、方法void RemoveAddIn(AddIn addIn) 删除一个插件
27G、方法Assembly LoadAssembly(string assemblyFile) 读入插件中Runtime节的Import指定的Assembly,并构造相应的CodonFactory和CodonFactory类。
28
29AddInTreeSingleton在首次调用AddInTree的时候会调用CreateAddInTree方法来进行初始化。CreateAddInTree方法是这样实现的:
30
31
32 addInTree = new DefaultAddInTree();
33
34
35初始化插件树为DefaultAddInTree的实例,这里我感受到了一点重构的痕迹。首先,DefaultAddInTree从名称上看是默认的插件树(既然是默认,那么换句话说还可以有其他的插件树)。但是SharpDevelop并没有给外部提供使用自定义插件树的接口(除非我们修改这里的代码),也就是说这个名称并不像它本身所暗示的那样。其次,按照Singleton通常的写法以及前面提到AddInTreeSingleton是从DefaultAddInTree继承下来的疑问,我猜想DefaultAddinTree的内容本来是在AddinTreeSingleton里面实现的,后来也许为了代码的条理性,把实现IAddinTree内容的代码剥离了出去,形成了DefaultAddinTree类。至于继承DefaultAddInTree的问题,也许这里本来是一个AddInTree的基类。这是题外话,也未加证实,各位看官可以不必放在心上(有兴趣的可以去找找以前SharpDevelop的老版本的代码来看看)。
36这里有两个察看代码的线路,一个是DefaultAddInTree的构造函数的代码,在这个构造函数中构造了Codon和Condtion的工厂类。另外一个是CreateAddInTree后面的代码,搜索插件文件,并根据插件文件进行AddIn的构造。各位看官可以选择走分支线路,也可以选择先看主线(不过这样你会漏掉不少内容)。
37
38** 2.1 支线 (DefaultAddInTree的构造函数) **
39我们把CreateAddInTree的代码中断一下压栈先,跳到DefaultAddInTree的构造函数中去看一看。DefaultAddInTree定义在\src\Main\Core\AddIns\DefaultAddInTree.cs文件中。在DefaultAddInTree的构造函数中,注意到它具有一个修饰符internal,也就是说这个类只允许Core这个程序集中的类对DefaultAddInTree进行实例化(真狠啊)。构造函数中的代码只有一句:
40
41 LoadCodonsAndConditions(Assembly.GetExecutingAssembly());
42
43虽然只有一行代码,不过这里所包含的内容却很精巧,是全局的关键,要讲清楚我可有得写了。首先,通过全局的Assembly对象取得入口程序的Assembly,传入LoadCodonsAndConditions方法中。在该方法中,枚举传入的Assembly中的所有数据类型。如果不是抽象的,并且是AbstractCodon的子类,并且具有对应的CodonNameAttribute属性信息,那么就根据这个类的名称建立一个对应的CodonBuilder并它加入CodonFactory中(之后对Condition也进行了同样的操作,我们专注来看Codon部分,Condition跟Codon基本上是一样的)。
44这里的CodonFactory类和CodonBuilder类构成了SharpDevelop插件系统灵活的基础,各位看官可要看仔细了。
45我们以实例来演示,以前文我编写的AddinTreeViewCommand为例。在入口的Assembly中会搜索到MenuItemCodon,它是AbstractCodon的一个子类、包装MenuItem(菜单项)Command(命令)的Codon。符合条件,执行
46
47 codonFactory.AddCodonBuilder( new CodonBuilder(type.FullName, assembly));
48
49首先根据类名MenuItemCodon和assembly 构造CodonBuilder。CodonBuilder定义在\src\Main\Core\AddIns\Codons\CodonBuilder.cs文件中。在CodonBuilder的构造函数中根据MenuItemCodon的CodonNameAttribute属性信息取得该Codon的名称MenuItem。CodonNameAttribute描述了Codon的名称,这个MenuItem也就是在.addin配置文件中对应的<menuitem/>标签,后文会看到它的重要用途。在CodonBuilder中除了包含了该Codon的ClassName(类名)和CodonName属性之外,就只有一个方法BuildCodon了。
50
51 public ICodon BuildCodon(AddIn addIn)
52   {
53 ICodon codon;
54  try  {
55 // create instance (ignore case)
56 codon = (ICodon)assembly.CreateInstance(ClassName, true );
57
58 // set default values
59 codon.AddIn = addIn;
60  } catch (Exception)  {
61 codon = null ;
62 }
63 return codon;
64 }
65
66
67很明显,BuildCodon根据构造函数中传入的assembly和类型的ClassName,建立了具体的Codon的实例,并和具体的AddIn关联起来。
68之后,codonFactory调用AddCodonBuilder方法把这个CodonBuilder加入它的Builder集合中。我们向上一层,看看codonFactory如何使用这个CodonBuilder。
69在文件\src\Main\Core\AddIns\Codons\CodonFactory.cs中,codonFactory只有两个方法。AddCodonBuilder方法把CodonBuilder加入一个以CodonName为索引的Hashtable中。另外一个方法很重要:
70
71 public ICodon CreateCodon(AddIn addIn, XmlNode codonNode)
72   {
73 CodonBuilder builder = codonHashtable[codonNode.Name] as CodonBuilder;
74
75  if (builder != null )  {
76 return builder.BuildCodon(addIn);
77 }
78
79 throw new CodonNotFoundException(String.Format( " no codon builder found for <{0}> " , codonNode.Name));
80 }
81
82
83在这里,addin是这个配置文件的描述(也就是插件),而这个XmlNode类型的CodonNode是什么东西?
84还记得配置文件中在<extension>标签下的<class>、<menuitem/>、<pad>之类的标签吗?我曾经说过,这些就是Codon的描述,现在我们来看看到底是不是如此。以前文的AddinTreeView配置为例:
85
86 < Extension path = "/SharpDevelop/Workbench/MainMenu/Tools" >
87 < MenuItem id = "AddinTreeView"
88 label = "View AddinTree"
89 class = "Addins.AddinTreeView.AddinTreeViewCommand" />
90 </pad></class></extension>
91
92
93SharpDevelop在读入插件配置文件的<extension>标签之后,就把它的ChildNodes(XmlElement的属性)依次传入CodonFactory的CreateCodon方法中。这里它的ChildNodes[0]就是这里的<menuitem id=".....">节点,也就是codonNode参数了。这个XML节点的Name是MenuItem,因此CreateCodon的第一行
94
95 CodonBuilder builder = codonHashtable[codonNode.Name] as CodonBuilder;
96
97
98根据节点的名称(MenuItem)查找对应的CodonBuilder。记得前面的CodonBuilder根据CodonNameAttribute取得了MenuItemCodon的CodonName吗?就是这个MenuItem了。CodonFactory找到了对应的MenuItemCodon的CodonBuilder(这个是在DefaultAddInTree的构造函数中调用LoadCodonsAndConditions方法建立并加入CodonFactory中的,还记得么?),之后使用这个CodonBuilder建立了对应的Codon,并把它返回给调用者。
99就这样,通过CodonNameAttribute,SharpDevelop把addin配置文件的<menuitem/>节点、CodonBulder、MenuItemCodon三部分串起来形成了一个构造Codon的路线。
100
101我们回过头来整理一下思路,SharpDevelop进行了下面这样几步工作:
102A、建立各个Codon,使用CodonNameAttribute指明它在配置节点中的名称
103B、DefaultAddInTree的构造函数中调用LoadCodonsAndConditions方法,搜索所有的Codon,根据Codon的CodonNameAttribute建立对应的CodonBuilder加入CodonFactory中。
104C、读取配置文件,在<extension>标签下遍历所有的节点,根据节点的Name使用CodonFactory建立对应的Codon。
105其中,Codon的CodonNameAttribute、CodonBuilder的CodonName以及<extension>标签下XML节点的Name是一致的。对于Condition(条件)的处理也是一样。
106抱歉,我上网不是很方便也不太会在Blog里面贴图(都是为了省事的借口^o^),否则也许更好理解这里的脉络关系。
107
108好了,看到这里,我们看看SharpDevelop中插件的灵活性是如何体现的。首先,addin配置中的Extension节点下的Codon节点名称并没有在代码中和具体的Codon类联系起来,而是通过CodonNameAttribute跟Codon联系起来。这样做的好处是,SharpDevelop的Codon和XML的标签一样具有无限的扩展能力。假设我们要自己定义一个Codon类SplashFormCodon作用是指定某个窗体作为系统启动时的封面窗体。要做的工作很简单:首先,在SplashFormCodon中使用CodonNameAttribute指定CodonName为Splash,并且在SplashFormCodon中定义自己需要的属性。然后,在addin配置文件使用<splash>标签这样写:
109
110 < Extension path = "/SharpDevelop/ " >
111 < Splash id = "MySplashForm" class = "MySplashFormClass" />
112 </splash></extension>
113
114
115是不是很简单?另外,对于Condition(条件)的处理也是一样,也就是说我们也可以使用类似的方法灵活的加入自己定义的条件。
116
117这里我有个小小的疑问:不知道我对于设计模式的理解是不是有点小问题,我感觉CodonBuilder类的实现似乎并不如它的类名所暗示的是《设计模式》中的Builder模式,反而似乎应该是Proxy模式,因此我觉得改叫做CodonProxy是不是比较容易理解?各位看官觉得呢?
118另外,虽然稍微麻烦了一小点,不过我觉得配置如果这样写会让我们比较容易和代码中具体的类关联起来:
119
120 < Extension path = "/SharpDevelop/ " >
121 < Codon name =”Splash” id = "MySplashForm" class = "MySplashFormClass" />
122 </extension>
123
124
125** 2.2 主线 (AddInTreeSingleton. CreateAddInTree) **
126啊~我写的有点累了。不过还是让我们继续AddInTreeSingleton中CreateAddInTree的代码。
127在建立了DefaultAddInTree的实例后,AddInTreeSingleton在插件目录中搜索后缀为.addin的文件。还记得在SharpDevelop的Main函数中曾经调用过AddInTreeSingleton. SetAddInDirectories吗,就是搜索这个传入的目录。看来SharpDevelop把在插件目录中所有后缀为.addin的文件都看做是插件了。
128
129 FileUtilityService fileUtilityService = (FileUtilityService)ServiceManager.Services.GetService( typeof (FileUtilityService));
130
131先学习一下如何从ServiceManager取得所需要的服务,在SharpDevelop中要取得一个服务全部都是通过这种方式取得的。调用GetService传入要获取的服务类的类型作为参数,返回一个IService接口,之后转换成需要的服务。
132
133搜索插件目录找到一个addin文件后,调用InsertAddIns把这个addin文件中的配置加入到目录树中。
134
135 static StringCollection InsertAddIns(StringCollection addInFiles)
136   {
137 StringCollection retryList = new StringCollection();
138
139  foreach ( string addInFile in addInFiles)  {
140 AddIn addIn = new AddIn();
141  try  {
142 addIn.Initialize(addInFile);
143 addInTree.InsertAddIn(addIn);
144  } catch (CodonNotFoundException)  {
145 retryList.Add(addInFile);
146  } catch (ConditionNotFoundException)  {
147 retryList.Add(addInFile);
148  } catch (Exception e)  {
149 throw new AddInInitializeException(addInFile, e);
150 }
151 }
152
153 return retryList;
154 }
155
156
157InsertAddIns建立一个对应的AddIn(插件),调用AddInTree的InsertAddIn方法把它挂到插件树中。在这里有一个小小的处理,由于是通过Assembly查找和插件配置中Codon的标签对应的类,而Codon类所在的Assembly是通过Import标签导入的。因此在查找配置中某个Codon标签对应的Codon类的时候,也许Codon类所在的文件是在其他的addin文件中Import的。这个时候在前面支线中讲到CodonFactory中查找CodonBuilder会失败,因此必须等到Codon类所在的addin处理之后才能正确的找到CodonBuilder。这是一个依赖关系的处理问题。
158SharpDevelop在这里处理的比较简单,调用InsertAddIns方法的时候,凡是出现CodonNotFoundException的时候,都加入一个retryList列表中返回。在CreateAddinTree处理完所有的addin文件之后,再重新循环尝试处理retryList列表中的addin。如果某次循环中再也无法成功的加入retryList中的addin,那么才提示失败错误。
159
160我们回头来看看对AddIn的处理。
161
162** 2.2.1 addIn.Initialize (AddIn的初始化)
163** 建立了AddIn的实例后,调用Initialize 方法进行初始化。AddIn是对一个.addin文件的封装,定义在\src\Main\Core\AddIns\AddIn.cs文件中。其中包含了.addin文件的根元素<addin>的描述,包括名称、作者、版权之类的属性。在<addin>节点下包括两种节点:一个是<runtime>节点,包含了<import>指定要导入的Assembly;另外一个是<extension>节点,指定Codon的扩展点。在AddIn.Initialize方法中,使用XmlDocument对象来读取对应的addin文件。首先读取name、author 、copyright之类的基本属性,之后遍历所有的ChildNodes(子节点)。
164
165如果子节点是Runtime节点,则调用AddRuntimeLibraries方法。
166
167<DIV st</extension></import></runtime></addin></addin></menuitem></extension></extension></import></runtime></addin>