** SharpDevelop ** ** 插件系统完全分析 ** **
**
** 前言 ** **
**
2005 年 2 月,我申报了一个学校组织的大学生 SRTP 项目,项目的题目是数据结构动画演示系统。当初在做项目之前,我无意中买了一本书,书名为《 SharpDevelop 软件项目开发全程剖析》。买这本书的目的显而易见,就是想看看人家老外是怎么做项目的。谁知买来一看,基本看不懂。随后,我在书里介绍的网站上 http://www.icsharpcode.net 下载了其项目的源代码。解压一看,更是傻掉了,搞了半天还不知道整个程序的主窗体是怎么显示的,是什么时候显示的。尽管这样,我还是没有气馁,因为我对它整个程序有很多感兴趣的东西:如可拖动的面板,可高亮度显示的文本,专业的菜单,那些有着良好定义的 XML 属性配置文件,等等。所以我就开始认真学习它的源代码。但是在研究的过程中发现它的工程文件只能用自己才能打开,而且发现 SharpDevelop 不能调试,使我分析源代码很不方便。所以,我先从全局分析整个解决方案,最后终于把那些不熟悉项目全部都转换成了 VS 中能运行的项目,而且最后整个程序能够在 VS 中运行和调试。那是,我为了和大家一起分享我的成果,特地把 SharpDevelop 的 VS 版本放在了互联网上,希望能对那些也想学习其源代码的朋友有所帮助。从那之后,我就开始研究其插件( Add In )系统。现在,我已经对其中的三个项目很熟悉,并且将它们完全整合到了我的项目中去,在研究这三个项目的过程中,我对其源代码文件的目录结构按照我的理解作了调整,修改和优化了其中的部分源代码,另外为了帮助理解,我添加了很多的注视。好了,讲了很多的废话,下面我开始分析其源代码:
** 1. ** ** 一些基本的概念: ** **
**
** AddInTree ** ** (插件树) ** **
**
SharpDevelop 中的所有东西都是被挂接在一棵插件树中的。这棵插件树是在程序运行时动态创建的,树的所有路径(这些路径是逻辑路径)都定义在插件文件中。如果某个插件文件中有以下片断 :
1<extension path="/Workspace/Services">
2<service class="NetFocus.DataStructure.Services.FileUtilityService" id="FileUtilityService"></service>
3<service class="NetFocus.DataStructure.Services.MessageService" id="MessageService"></service>
4<service class="NetFocus.DataStructure.Services.TaskService" id="TaskService"></service>
5</extension>
1<extension path="/Workspace/Icons">
2<icon extensions=".cs" id="C#File" resource="C#.FileIcon"></icon>
3<icon extensions=".txt,.doc" id="TextFileIcon" resource="Icons.16x16.TextFileIcon"></icon>
4</extension>
则说明插件树的根节点下有一个子节点 Workspace , Workspace 下又有 Services 和 Icons 两个子节点, Services 下又有 FileUtilityService , MessageService , TaskService 三个子节点。。。
** AddIn ** ** (插件)
**
SharpDevelop 中的一个插件由一个插件文件(以 addin 为后缀名)定义,该插件文件中定义了很多的插件树的路径(由 Extension 节点指定)。另外,该插件文件中还指定了运行该插件所需的所有的程序集。如果一个插件文件中有一个节点如下:
1<runtime>
2<import assembly="..\bin\Base.dll"></import>
3</runtime>
则说明,运行该插件需要一个名为 Base.dll 的程序集,该程序集的路径为一个相对目录(相对于当前插件文件的目录)。
** Codon ** ** (代码子)
**
按照我的理解,代码子是一个对象创建器。被创建的对象具有某个具体的功能。如 ServiceCodon (服务代码子,这个名字在 SharpDevelop 中叫作 ClassCodon )通过其 BuildItem 方法可以创建一个服务对象,该服务对象可为程序提供某种具体的服务; PadCodon (面板代码子)通过其 BuildItem 方法可以创建一个面板对象,该面板对象可能是一个属性面板,文件浏览面板,等等。
1<service class="NetFocus.DataStructure.Services.FileUtilityService" id="FileUtilityService"></service>
1<icon extensions=".cs" id="C#File" resource="C#.FileIcon"></icon>
上面的两个 XML 元素代表两个代码子节点,这里我把他们叫做代码子节点是因为它们在 XML 文件中以元素(节点)出现,而在程序运行时这两个元素会被初始化成两个代码子对象。像上面这样,如果某个代码子节点有 Class 属性,则说明该代码子对象可以根据这个属性值创建一个对象,如果没有提供 Class 属性,则该代码子对象直接将自己返回。这里,值得一提的是,所有的 Class 属性所指定的类必须在上面介绍的
1<runtime> 节点中指定的某个程序集中定义过。这一点是我对 SharpDevelop 源代码的修改,因为在 SharpDevelop 中,如果 Class 定义的某个类在当前插件文件的 <runtime> 节点中没有指定,程序会将当前未初始化好的代码子对象先保存起来,待当前插件初始化完成之后,再尝试从后面的插件文件中所指定的程序集中创建该代码子。
2
3** AddInTreeNode ** ** (插件树节点)
4
5**
6
7既然整个程序的结构是一棵插件树,就肯定有很多插件树节点。那么插件树节点有什么功能呢?其实,在整棵插件树中并不是所有的节点都有功能,有些节点没有任何功能,只是起到构成路径的作用;而有些节点如上面所讲的 FileUtilityService , MessageService , TaskService 三个子节点才有具体的功能,因为它们在整棵树中不仅代表一个节点,而且还代表一个代码子。实际上,在创建整棵树的过程中,如果为当前节点指定了一个代码子对象,则该树节点就具有具体的功能,如果没有指定,则该节点只是起到构成路径的作用。
8
9** 2. ** ** 整个插件系统的创建过程:
10
11**
12
13为了方便分析,我用我自己的项目代码来给大家分析,各位看官不必担心,因为我只是稍微修改了其插件系统,大部分代码没有变化。所以在阅读下面的内容之前建议大家去我的个人网站( http://www.netfocus.cn )下载程序源代码“ 数据结构动画演示系统 “,以方便你理解本文。
14
15和 SharpDevelop 一样,程序的入口点在 \src\StartUp\DataStructureMain.cs 的 Main 函数中。至于前面语句我在这里就不在解释了,我已经标了注视。下面从 AddInTreeSingleton.CreateAddInTree ()方法开始分析。首先,从 AddInTreeSingleton 这个类的名在来看,这个类应该采用 Singleton 设计模式。下面转入 CreateAddInTree 方法,
16
17首先判断插件树是否为空,即插件树是否已经创建,如果没有创建,则实例化一个默认的插件树对象。代码如下:
18
19if (addInTree == null )
20
21{
22
23addInTree = new DefaultAddInTree();
24
25InternalFileService fileUtilityService = new InternalFileService();
26
27StringCollection addInFiles = null ;
28
29if (ignoreDefaultCoreAddInDirectory == false ) // 如果没有忽略默认的插件路径 , 即采用默认的插件路径
30
31{
32
33addInFiles = fileUtilityService.SearchDirectory(defaultCoreAddInDirectory, "*.addin");
34
35InsertAddIns(addInFiles);
36
37}
38
39else // 如果忽略默认的插件文件的路径
40
41{
42
43if (addInDirectories != null )
44
45{
46
47foreach ( string path in addInDirectories)
48
49{
50
51addInFiles = fileUtilityService.SearchDirectory(Application.StartupPath + Path.DirectorySeparatorChar + path, "*.addin");
52
53InsertAddIns(addInFiles);
54
55}
56
57}
58
59}
60
61}
62
63在 DefaultAddInTree 的构造函数中,通过 LoadCodonsAndConditions(Assembly.GetExecutingAssembly());
64
65从核心项目 Core 程序集中加载所有的代码子和条件对象。那么这个函数到底作了什么呢?
66
67首先从程序集中得到所有的类型,然后判断如果某个类型是从 AbstractCodon 抽象类继承而来并且这个类型上有 CodonNameAttribute 特性,则说明当前类是一个具有某个功能的代码子类,然后立即创建一个 CodonBuilder 对象,并将该对象添加到一个 Factory 工厂对象的 Builders 哈希表中。添加时以 CodonName 为主键。另外,对于条件操作也是类似操作。
68
69继续回到 CreateAddInTree 方法中,判断是否忽略默认的插件文件路径,如果不忽略,则使用默认的插件文件路径“ ..\addIns “来搜索所有的插件文件,并放到一个字符串集合中。然后调用 InsertAddIns 方法。下面是这个方法的源代码:
70
71static void InsertAddIns(StringCollection addInFiles)
72
73{
74
75foreach ( string addInFile in addInFiles)
76
77{
78
79AddIn addIn = new AddIn(); // 先新建一个插件实例
80
81try
82
83{
84
85addIn.Initialize(addInFile); // 通过当前插件文件来初始化这个插件实例
86
87addInTree.InsertAddIn(addIn); // 将这个初始化好的插件插入到插件树中
88
89}
90
91catch (Exception e)
92
93{
94
95throw new AddInInitializeException(addInFile, e);
96
97}
98
99}
100
101}
102
103这个方法的功能是接受一个插件文件的集合列表参数,然后对每个插件文件都创建一个插件对象,并将该插件对象插入到插件树中。在这个过程中,关键是插件对象如何创建?以及创建好的对象如何插入到插件树中?所以,如果这两个问题解释清楚了,那么,整个插件系统的创建过程就清楚了。现面先分析插件对象如何创建,转入 AddIn 文件的 Initialize 方法:
104
105该方法的关键是在 foreach 循环中,它对插件文件根节点下的所有子节点进行迭代,判断子节点的类型,
106
107如果为 RunTime 节点,则将该节点的所有子节点所指定的程序集加载到内存,并将该程序集对象添加到一个哈希表中。当然,这里并不是只是简单的根据程序集文件名创建一个程序集对象,在创建了程序集对象之后,还像刚开始从核心项目 Core 程序集加载所有的代码子对象一样加载了当前程序集中的所有的代码子和条件对象。
108
109如果为 Extension 节点,则说明当前节点是一个功能扩展,所谓功能扩展是指在某个插件树路径下的功能集合。一般情况下一个功能扩展的特点是:
110
111一个路径( Path ):指定该功能扩展在整个系统中的逻辑路径;
112
113一些可以嵌套定义的代码子节点;
114
115一些作用在代码子节点上的条件节点;
116
117所以,在 SharpDevelop 中特别定义了一个 Extension 类,专门用来描述 XML 文件中定义的 Extension 节点。
118
119在遇到 Extension 节点时调用 <sp</runtime></runtime>