SharpDevelop插件系统完全分析
前言
2005年2月,我申报了一个学校组织的大学生SRTP项目,项目的题目是数据结构动画演示系统。当初在做项目之前,我无意中买了一本书,书名为《SharpDevelop软件项目开发全程剖析》。买这本书的目的显而易见,就是想看看人家老外是怎么做项目的。谁知买来一看,基本看不懂。随后,我在书里介绍的网站上http://www.icsharpcode.net下载了其项目的源代码。解压一看,更是傻掉了,搞了半天还不知道整个程序的主窗体是怎么显示的,是什么时候显示的。尽管这样,我还是没有气馁,因为我对它整个程序有很多感兴趣的东西:如可拖动的面板,可高亮度显示的文本,专业的菜单,那些有着良好定义的XML属性配置文件,等等。所以我就开始认真学习它的源代码。但是在研究的过程中发现它的工程文件只能用自己才能打开,而且发现SharpDevelop不能调试,使我分析源代码很不方便。所以,我先从全局分析整个解决方案,最后终于把那些不熟悉项目全部都转换成了VS中能运行的项目,而且最后整个程序能够在VS中运行和调试。那是,我为了和大家一起分享我的成果,特地把SharpDevelop的VS版本放在了互联网上,希望能对那些也想学习其源代码的朋友有所帮助。从那之后,我就开始研究其插件(Add In)系统。现在,我已经对其中的三个项目很熟悉,并且将它们完全整合到了我的项目中去,在研究这三个项目的过程中,我对其源代码文件的目录结构按照我的理解作了调整,修改和优化了其中的部分源代码,另外为了帮助理解,我添加了很多的注视。好了,讲了很多的废话,下面我开始分析其源代码:
1. 一些基本的概念:
AddInTree(插件树)
SharpDevelop中的所有东西都是被挂接在一棵插件树中的。这棵插件树是在程序运行时动态创建的,树的所有路径(这些路径是逻辑路径)都定义在插件文件中。如果某个插件文件中有以下片断:
<Extension path = "/Workspace/Services">
<Service id = "FileUtilityService"
class = "NetFocus.DataStructure.Services.FileUtilityService"/>
<Service id = "MessageService"
class = "NetFocus.DataStructure.Services.MessageService"/>
<Service id = "TaskService"
class = "NetFocus.DataStructure.Services.TaskService"/>
</Extension>
<Extension path = "/Workspace/Icons">
<Icon id = "C#File"
extensions = ".cs"
resource = "C#.FileIcon"/>
<Icon id = "TextFileIcon"
extensions = ".txt,.doc"
resource = "Icons.16x16.TextFileIcon"/>
</Extension>
则说明插件树的根节点下有一个子节点Workspace,Workspace下又有Services和Icons两个子节点,Services下又有FileUtilityService,MessageService,TaskService三个子节点。。。
AddIn(插件)
SharpDevelop中的一个插件由一个插件文件(以addin为后缀名)定义,该插件文件中定义了很多的插件树的路径(由Extension节点指定)。另外,该插件文件中还指定了运行该插件所需的所有的程序集。如果一个插件文件中有一个节点如下:
<Runtime>
<Import assembly="..\bin\Base.dll"/>
</Runtime>
则说明,运行该插件需要一个名为Base.dll的程序集,该程序集的路径为一个相对目录(相对于当前插件文件的目录)。
Codon(代码子)
按照我的理解,代码子是一个对象创建器。被创建的对象具有某个具体的功能。如ServiceCodon(服务代码子,这个名字在SharpDevelop中叫作ClassCodon)通过其BuildItem方法可以创建一个服务对象,该服务对象可为程序提供某种具体的服务;PadCodon(面板代码子)通过其BuildItem方法可以创建一个面板对象,该面板对象可能是一个属性面板,文件浏览面板,等等。
<Service id = "FileUtilityService"
class = "NetFocus.DataStructure.Services.FileUtilityService"/>
<Icon id = "C#File"
extensions = ".cs"
resource = "C#.FileIcon"/>
上面的两个XML元素代表两个代码子节点,这里我把他们叫做代码子节点是因为它们在XML文件中以元素(节点)出现,而在程序运行时这两个元素会被初始化成两个代码子对象。像上面这样,如果某个代码子节点有Class属性,则说明该代码子对象可以根据这个属性值创建一个对象,如果没有提供Class属性,则该代码子对象直接将自己返回。这里,值得一提的是,所有的Class属性所指定的类必须在上面介绍的<Runtime>节点中指定的某个程序集中定义过。这一点是我对SharpDevelop源代码的修改,因为在SharpDevelop中,如果Class定义的某个类在当前插件文件的<Runtime>节点中没有指定,程序会将当前未初始化好的代码子对象先保存起来,待当前插件初始化完成之后,再尝试从后面的插件文件中所指定的程序集中创建该代码子。
AddInTreeNode(插件树节点)
既然整个程序的结构是一棵插件树,就肯定有很多插件树节点。那么插件树节点有什么功能呢?其实,在整棵插件树中并不是所有的节点都有功能,有些节点没有任何功能,只是起到构成路径的作用;而有些节点如上面所讲的FileUtilityService,MessageService,TaskService三个子节点才有具体的功能,因为它们在整棵树中不仅代表一个节点,而且还代表一个代码子。实际上,在创建整棵树的过程中,如果为当前节点指定了一个代码子对象,则该树节点就具有具体的功能,如果没有指定,则该节点只是起到构成路径的作用。
2. 整个插件系统的创建过程:
为了方便分析,我用我自己的项目代码来给大家分析,各位看官不必担心,因为我只是稍微修改了其插件系统,大部分代码没有变化。所以在阅读下面的内容之前建议大家去我的个人网站(http://www.netfocus.cn)下载程序源代码“数据结构动画演示系统“,以方便你理解本文。
和SharpDevelop一样,程序的入口点在\src\StartUp\DataStructureMain.cs的Main函数中。至于前面语句我在这里就不在解释了,我已经标了注视。下面从AddInTreeSingleton.CreateAddInTree()方法开始分析。首先,从AddInTreeSingleton这个类的名在来看,这个类应该采用Singleton设计模式。下面转入CreateAddInTree方法,
首先判断插件树是否为空,即插件树是否已经创建,如果没有创建,则实例化一个默认的插件树对象。代码如下:
if(addInTree == null)
{
addInTree = new DefaultAddInTree();
InternalFileService fileUtilityService = new InternalFileService();
StringCollection addInFiles = null;
if (ignoreDefaultCoreAddInDirectory == false) //如果没有忽略默认的插件路径,即采用默认的插件路径
{
addInFiles = fileUtilityService.SearchDirectory(defaultCoreAddInDirectory, "*.addin");
InsertAddIns(addInFiles);
}
else //如果忽略默认的插件文件的路径
{
if (addInDirectories != null)
{
foreach(string path in addInDirectories)
{
addInFiles = fileUtilityService.SearchDirectory(Application.StartupPath + Path.DirectorySeparatorChar + path, "*.addin");
InsertAddIns(addInFiles);
}
}
}
}
在DefaultAddInTree的构造函数中,通过LoadCodonsAndConditions(Assembly.GetExecutingAssembly());
从核心项目Core程序集中加载所有的代码子和条件对象。那么这个函数到底作了什么呢?
首先从程序集中得到所有的类型,然后判断如果某个类型是从AbstractCodon抽象类继承而来并且这个类型上有CodonNameAttribute特性,则说明当前类是一个具有某个功能的代码子类,然后立即创建一个CodonBuilder对象,并将该对象添加到一个Factory工厂对象的Builders哈希表中。添加时以CodonName为主键。另外,对于条件操作也是类似操作。
继续回到CreateAddInTree方法中,判断是否忽略默认的插件文件路径,如果不忽略,则使用默认的插件文件路径“..\addIns“来搜索所有的插件文件,并放到一个字符串集合中。然后调用InsertAddIns方法。下面是这个方法的源代码:
static void InsertAddIns(StringCollection addInFiles)
{
foreach (string addInFile in addInFiles)
{
AddIn addIn = new AddIn();//先新建一个插件实例
try
{
addIn.Initialize(addInFile);//通过当前插件文件来初始化这个插件实例
addInTree.InsertAddIn(addIn);//将这个初始化好的插件插入到插件树中
}
catch (Exception e)
{
throw new AddInInitializeException(addInFile, e);
}
}
}
这个方法的功能是接受一个插件文件的集合列表参数,然后对每个插件文件都创建一个插件对象,并将该插件对象插入到插件树中。在这个过程中,关键是插件对象如何创建?以及创建好的对象如何插入到插件树中?所以,如果这两个问题解释清楚了,那么,整个插件系统的创建过程就清楚了。现面先分析插件对象如何创建,转入AddIn文件的Initialize方法:
该方法的关键是在foreach循环中,它对插件文件根节点下的所有子节点进行迭代,判断子节点的类型,
如果为RunTime节点,则将该节点的所有子节点所指定的程序集加载到内存,并将该程序集对象添加到一个哈希表中。当然,这里并不是只是简单的根据程序集文件名创建一个程序集对象,在创建了程序集对象之后,还像刚开始从核心项目Core程序集加载所有的代码子对象一样加载了当前程序集中的所有的代码子和条件对象。
如果为Extension节点,则说明当前节点是一个功能扩展,所谓功能扩展是指在某个插件树路径下的功能集合。一般情况下一个功能扩展的特点是:
一个路径(Path):指定该功能扩展在整个系统中的逻辑路径;
一些可以嵌套定义的代码子节点;
一些作用在代码子节点上的条件节点;
所以,在SharpDevelop中特别定义了一个Extension类,专门用来描述XML文件中定义的Extension节点。
在遇到Extension节点时调用AddExtensions方法,该方法接受一个Extension XML节点,在AddExtensions方法内部关键是AddCodonsToExtension方法,该方法比较复杂,下面我分析其中最关键的default部分:
首先通过ICodon codon = AddInTreeSingleton.AddInTree.CodonFactory.CreateCodon(this, curEl);这句话创建一个代码子对象,大家一定还不清楚这句话是如何工作的。下面,我再详细解释:
AddInTree这个对象我在前面已经显式创建好了,大家应该还记得。然后是CodonFactory的CreateCodon方法。不知大家是否还记得,在先前加载每个程序集的时候,都调用了LoadCodonsAndConditions这个方法,在这个方法内部扫描当前程序集的所有类型,并把继承自AbstractCodon类的所有子类都封装在一个对应的CodonBuilder对象中,最后把这些CodonBuilder都添加到了CodonFactory的Builders集合中。所以,当现在调用CodonFactory的CreateCodon方法时,会先根据代码子名(CodonName)找到一个相应的CodonBuilder对象,然后调用该对象的BuildCodon方法,在该方法中有这么一句话:
codon = (ICodon)assembly.CreateInstance(ClassName, true);
它通过一个具体的类名从一个程序集中创建一个代码子对象,最后将该代码子对象和当前插件对象关联。
知道了代码子对象如何创建之后,程序通过以下这句话:
AutoInitializeAttributes(codon, curEl);
上面这个函数的功能是全面检索当前被创建的代码子对象的所有的属性(包括基类的属性),
如果被扫描到的属性在XML文件中指定了值,则把该值赋给这个属性,如果没有指定,则不赋值。扫描时属性分为两类,一般属性和数组类型的属性。然后,程序对代码子进行排序之后将当前代码子对象插入到扩展对象e的一个集合中。最后,如果当前节点有子节点(如<MenuItem>节点中有子<MenuItem>节点),则对其子节点进行递归操作。
好了,下面回到Initialize方法。当该方法按照上面那样执行结束之后,系统应该做了以下两件重要事情:1)系统已经加载了该插件运行所需的全部程序集;2)系统已经将这些程序集中的所有的代码子类和条件类全部创建完成,并都添加到了相应的Extension对象的codonCollection和Conditions集合中。
下面开始讨论AddInTree的InsertAddIn方法。
第一步,先将传过来的插件对象加入到一个插件集合中,刚才谈到一个Extension对象的codonCollection集合包含了该插件的某些代码子对象。所以,接下来先在外层循环对该插件对象的Extensions集合列表进行迭代,在该循环内部,先通过Extension对象所指定的Path属性创建一个插件树节点,注意创建该节点时总是以root为树的根节点,因为Path所指定的逻辑插件路径总是从根节点出发的。如下面的语句所示:
DefaultAddInTreeNode currentNode = CreateTreeNode(root, extension.Path);
接下来,再对Extension里的codonCollection集合进行迭代,并把这些codon对象作为插件树的节点一个个插入到相应的位置。并将当前插件树节点与一个代码子对象关联,使其具有某种具体的功能。注意:这里插入时,将代码子对象的ID属性作为了路径的一部分,其实就是将一个代码子对象也作为一个树节点看待,节点的名字就是该代码子对象的ID属性。这里有一个关于插件树节点如何创建的问题。这个功能是通过以下函数完成的:
DefaultAddInTreeNode CreateTreeNode(DefaultAddInTreeNode currentNode, string path)
该函数接受一个当前节点对象和一个树路径字符串信息。首先将该路径字符串进行分割,放在一个数组中(这个数组中的每个元素代表一个树节点的名称),然后根据这个数组中的节点名逐个构造树节点。有两种情况:
1) 如果由某个节点名指定的节点对象已经创建好了,则直接将该节点作为下一次的当前节点,并进入下一个循环继续创建其子节点。
2) 如果由某个节点名指定的节点对象还没有创建,则马上创建该节点并将其作为当前节点的子节点。
所以从插件树中节点的创建过程来看,整棵插件树在逻辑上是一棵树,但在内部其实是为每个节点都设置了一个ChildNodes集合,用来存放当前节点的子节点。另外对于条件对象也是一样,这里就不在详细讨论了。当InsertAddIn方法执行完毕时,已经顺利地将当前插件对象插入到插件树中。最后当AddInSingleton的InsertAddIns方法执行完成时,整棵插件树就应该成功地被创建了。
到这里,各位看官应该已经了解了整棵插件树是怎样被创建和组织起来的。下面为了清楚起见,我再简单回顾一下整个创建流程:
1:在Main函数中显式调用AddInTreeSingleton的CreateAddInTree()方法
AddInTreeSingleton.CreateAddInTree();
2:先获取所有插件文件列表,然后调用InsertAddIns()方法
InsertAddIns(addInFiles);
3:对文件类表进行迭代,对每个插件文件都创建一个插件,并将其插入到插件树中
static void InsertAddIns(StringCollection addInFiles)
4:用一个XML插件文件来初始化一个插件对象
addIn.Initialize(addInFile);
5:在上面的插件初始化函数中,先分析<RunTime>元素节点,并创建一些CodonBuilder和ConditionBuilder对象,这些对象可以创建Codon和Condition对象。用以下函数实现:
AddRuntimeLibraries(curEl);
6:分析<Extension>元素节点,根据这些节点并利用上面所提到的CodonBuilder和ConditionBuilder对象来创建Codon和Condition对象。
AddExtensions(curEl); ――> AddCodonsToExtension
7:插件对象创建好之后,将该插件对象插入到插件树中。
addInTree.InsertAddIn(addIn);
8:在将插件对象插入到树的过程中,Extension对象的Path属性只用于构成树的路径,而Extension对象的CodonCollection集合中的每一个Codon对象不仅作为一个树节点(构成路径),而且具有某个具体的功能。创建树节点的函数如下:
DefaultAddInTreeNode CreateTreeNode(DefaultAddInTreeNode currentNode, string path);
9:最后层层返回,最终完成整棵插件树的创建。
小结:
整个插件系统的创建应该说比较复杂,其中用到了一些设计模式:
1.因为插件树在一个程序一次运行中只有一个实例,所以采用了Singleton设计模式;
2.代码子和条件对象的创建方式,程序中使用了“Builder”字眼,看上去好像是使用了Builder设计模式,但我认为这跟Builder设计模式的真正用途是不一样的。因为Builder设计模式强调一个对象的创建过程,并最终将该对象返回。而这里只是简单的创建了一个代码子或条件对象,真正的创建任务是由代码子或条件对象来完成的。所以,这样来看这用方式应该更加符合Proxy设计模式,各位看官觉得如何呢?
3.CodonFactory这个类,一看就是工厂设计模式。但它不是一个一般的工厂模式,而是对一个工厂模式的优化,因为它可以创建任意种类的Codon对象,它并没有提供一个固定的创建一系列产品的方法列表接口。而是提供了一个集合,集合重的元素可以动态添加,每个元素都可以创建一个Codon或Condition对象。
插件系统如何创建今天就先介绍到这里,明天介绍Command、Service以及插件树节点中的每个功能是如何来体现和使用的。