上回书说到SharpDevelop入口Main函数的结构,ServiceManager.Service在InitializeServicesSubsystem方法中首次调用了AddInTreeSingleton的AddInTree实例,AddInTree在这里进行了初始化。本回进入AddInTree着重讲述SharpDevelop的插件系统。在叙述的时候为了方便起见,对于“插件”和插件具体的“功能模块”这两个词不会特别的区分,各位看官可以从上下文分辨具体的含义(而事实上,SharpDevelop中的“插件”是指.addin配置文件,每一个“插件”都可能会包含多个“功能模块”)。 1、插件的配置 之后按照这样的理解,我编写了一个察看插件树的插件AddinTreeView,打算挂到SharpDevelop中去。根据SharpDevelop对插件的定义,我把具体插件的AddinTreeViewCommand实现了之后,编写了一个配置文件AddinTreeView.addin如下: <AddIn name = "AddinTreeView" author = "SimonLiu" copyright = "GPL" url = "http://www." description = "Display AddinTree" version = "1.0.0"> <Runtime> <Import assembly="../../bin/ AddinTreeView.dll"/> </Runtime> <Extension path = "/SharpDevelop/Workbench/MainMenu/Tools"> <MenuItem id = "AddinTreeView" label = "View AddinTree" class = "Addins.AddinTreeView.AddinTreeViewCommand"/> </Extension> </AddIn>
如果我们对于每一个插件都编写这样的一个配置文件,那么插件的库文件(.dll)、插件配置文件(.addin)是一一对应的。不过这样就带来了一个小小的问题,在这样的一个以插件为基础的系统中,每一个菜单、工具栏按钮、窗体、面板都是一个插件,那么我们需要为每一个插件编写配置文件,这样就会有很多个配置文件(似乎有点太多了,不是很好管理)。SharpDevelop也想到了这个问题,于是它允许我们把多个插件的配置合并在一个插件的配置文件中。因此,我把我的两个插件库文件合并到一个Addins工程内生成了Addins.dll,又重新编写了我的插件配置文件MyAddins.addin如下: <AddIn name = "MyAddins" author = "SimonLiu" copyright = "GPL" url = "http://www." description = "Display AddinTree" version = "1.0.0"> <Runtime> <Import assembly="../../bin/Addins.dll"/> </Runtime> <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"/> </Extension> </AddIn>
2、插件系统的核心AddIn和AddInTree public static IAddInTree AddInTree { get { if (addInTree == null) { CreateAddInTree(); } return addInTree; } } AddInTreeSingleton是插件树的一个Singleton(具体的可以去看《设计模式》了),AddInTreeSingleton.AddInTree是一个属性,返回一个IAddinTree接口。这里我注意到一点,AddInTreeSingleton是从DefaultAddInTree继承下来的。既然它是一个单件模式,包含的方法全部都是静态方法,没有实例化的必要,而且外部是通过AddInTree属性来访问插件树,为什么要从DefaultAddInTree继承呢?这好像没有什么必要。这也许是重构过程中被遗漏的一个小问题吧。 我们先来看看IAddinTree接口的内容,它定义了这样的几个内容: AddInTreeSingleton在首次调用AddInTree的时候会调用CreateAddInTree方法来进行初始化。CreateAddInTree方法是这样实现的: addInTree = new DefaultAddInTree(); 初始化插件树为DefaultAddInTree的实例,这里我感受到了一点重构的痕迹。首先,DefaultAddInTree从名称上看是默认的插件树(既然是默认,那么换句话说还可以有其他的插件树)。但是SharpDevelop并没有给外部提供使用自定义插件树的接口(除非我们修改这里的代码),也就是说这个名称并不像它本身所暗示的那样。其次,按照Singleton通常的写法以及前面提到AddInTreeSingleton是从DefaultAddInTree继承下来的疑问,我猜想DefaultAddinTree的内容本来是在AddinTreeSingleton里面实现的,后来也许为了代码的条理性,把实现IAddinTree内容的代码剥离了出去,形成了DefaultAddinTree类。至于继承DefaultAddInTree的问题,也许这里本来是一个AddInTree的基类。这是题外话,也未加证实,各位看官可以不必放在心上(有兴趣的可以去找找以前SharpDevelop的老版本的代码来看看)。 2.1 支线 (DefaultAddInTree的构造函数) LoadCodonsAndConditions(Assembly.GetExecutingAssembly()); 虽然只有一行代码,不过这里所包含的内容却很精巧,是全局的关键,要讲清楚我可有得写了。首先,通过全局的Assembly对象取得入口程序的Assembly,传入LoadCodonsAndConditions方法中。在该方法中,枚举传入的Assembly中的所有数据类型。如果不是抽象的,并且是AbstractCodon的子类,并且具有对应的CodonNameAttribute属性信息,那么就根据这个类的名称建立一个对应的CodonBuilder并它加入CodonFactory中(之后对Condition也进行了同样的操作,我们专注来看Codon部分,Condition跟Codon基本上是一样的)。 codonFactory.AddCodonBuilder(new CodonBuilder(type.FullName, assembly)); 首先根据类名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了。 public ICodon BuildCodon(AddIn addIn) { ICodon codon; try { // create instance (ignore case) codon = (ICodon)assembly.CreateInstance(ClassName, true); // set default values codon.AddIn = addIn; } catch (Exception) { codon = null; } return codon; }
public ICodon CreateCodon(AddIn addIn, XmlNode codonNode) { CodonBuilder builder = codonHashtable[codonNode.Name] as CodonBuilder; if (builder != null) { return builder.BuildCodon(addIn); } throw new CodonNotFoundException(String.Format("no codon builder found for <{0}>", codonNode.Name)); } 在这里,addin是这个配置文件的描述(也就是插件),而这个XmlNode类型的CodonNode是什么东西? <Extension path = "/SharpDevelop/Workbench/MainMenu/Tools"> <MenuItem id = "AddinTreeView" label = "View AddinTree" class = "Addins.AddinTreeView.AddinTreeViewCommand"/> </Extension> SharpDevelop在读入插件配置文件的<Extension>标签之后,就把它的ChildNodes(XmlElement的属性)依次传入CodonFactory的CreateCodon方法中。这里它的ChildNodes[0]就是这里的<MenuItem id = ..... />节点,也就是codonNode参数了。这个XML节点的Name是MenuItem,因此CreateCodon的第一行 CodonBuilder builder = codonHashtable[codonNode.Name] as CodonBuilder; 根据节点的名称(MenuItem)查找对应的CodonBuilder。记得前面的CodonBuilder根据CodonNameAttribute取得了MenuItemCodon的CodonName吗?就是这个MenuItem了。CodonFactory找到了对应的MenuItemCodon的CodonBuilder(这个是在DefaultAddInTree的构造函数中调用LoadCodonsAndConditions方法建立并加入CodonFactory中的,还记得么?),之后使用这个CodonBuilder建立了对应的Codon,并把它返回给调用者。 我们回过头来整理一下思路,SharpDevelop进行了下面这样几步工作: 好了,看到这里,我们看看SharpDevelop中插件的灵活性是如何体现的。首先,addin配置中的Extension节点下的Codon节点名称并没有在代码中和具体的Codon类联系起来,而是通过CodonNameAttribute跟Codon联系起来。这样做的好处是,SharpDevelop的Codon和XML的标签一样具有无限的扩展能力。假设我们要自己定义一个Codon类SplashFormCodon作用是指定某个窗体作为系统启动时的封面窗体。要做的工作很简单:首先,在SplashFormCodon中使用CodonNameAttribute指定CodonName为Splash,并且在SplashFormCodon中定义自己需要的属性。然后,在addin配置文件使用<Splash>标签这样写: <Extension path = "/SharpDevelop/ "> <Splash id = "MySplashForm" class = "MySplashFormClass"/> </Extension> 是不是很简单?另外,对于Condition(条件)的处理也是一样,也就是说我们也可以使用类似的方法灵活的加入自己定义的条件。 这里我有个小小的疑问:不知道我对于设计模式的理解是不是有点小问题,我感觉CodonBuilder类的实现似乎并不如它的类名所暗示的是《设计模式》中的Builder模式,反而似乎应该是Proxy模式,因此我觉得改叫做CodonProxy是不是比较容易理解?各位看官觉得呢? <Extension path = "/SharpDevelop/ "> <Codon name=”Splash” id = "MySplashForm" class = "MySplashFormClass"/> </Extension>
FileUtilityService fileUtilityService = (FileUtilityService)ServiceManager.Services.GetService(typeof(FileUtilityService)); 先学习一下如何从ServiceManager取得所需要的服务,在SharpDevelop中要取得一个服务全部都是通过这种方式取得的。调用GetService传入要获取的服务类的类型作为参数,返回一个IService接口,之后转换成需要的服务。 搜索插件目录找到一个addin文件后,调用InsertAddIns把这个addin文件中的配置加入到目录树中。 static StringCollection InsertAddIns(StringCollection addInFiles) { StringCollection retryList = new StringCollection(); foreach (string addInFile in addInFiles) { AddIn addIn = new AddIn(); try { addIn.Initialize(addInFile); addInTree.InsertAddIn(addIn); } catch (CodonNotFoundException) { retryList.Add(addInFile); } catch (ConditionNotFoundException) { retryList.Add(addInFile); } catch (Exception e) { throw new AddInInitializeException(addInFile, e); } } return retryList; } InsertAddIns建立一个对应的AddIn(插件),调用AddInTree的InsertAddIn方法把它挂到插件树中。在这里有一个小小的处理,由于是通过Assembly查找和插件配置中Codon的标签对应的类,而Codon类所在的Assembly是通过Import标签导入的。因此在查找配置中某个Codon标签对应的Codon类的时候,也许Codon类所在的文件是在其他的addin文件中Import的。这个时候在前面支线中讲到CodonFactory中查找CodonBuilder会失败,因此必须等到Codon类所在的addin处理之后才能正确的找到CodonBuilder。这是一个依赖关系的处理问题。 我们回头来看看对AddIn的处理。 2.2.1 addIn.Initialize (AddIn的初始化) 如果子节点是Runtime节点,则调用AddRuntimeLibraries方法。 foreach (object o in el.ChildNodes) { XmlElement curEl = (XmlElement)o; string assemblyName = curEl.Attributes["assembly"].InnerText; string pathName = Path.IsPathRooted(assemblyName) ? assemblyName : fileUtilityService.GetDirectoryNameWithSeparator(path) + assemblyName; Assembly asm = AddInTreeSingleton.AddInTree.LoadAssembly(pathName); RuntimeLibraries[assemblyName] = asm; } 通过AddInTreeSingleton.AddInTree.LoadAssembly方法把Assembly中所有的Codon和Condition的子类加入对应Factory类中(调用了LoadCodonsAndConditions方法,我们在DefaultAddInTree的构造函数中见过了),并且把该文件和对应的Assembly保存到RuntimeLibraries列表中。 如果子节点是Extension节点,则调用AddExtensions方法。 Extension e = new Extension(el.Attributes["path"].InnerText); AddCodonsToExtension(e, el, new ConditionCollection()); extensions.Add(e); 根据这个扩展点的XML描述建立Extension对象加入到AddIn的Extensions列表中,并通过AddCodonsToExtension方法把其中包括的Codon加入到建立的Extension对象中。Extension对象是AddIn的一个内嵌类,其中一个重要的属性就是CodonCollection这个列表。AddCodonsToExtension就是把在配置中出现的Codon都加入到这个列表中保存。 来看看AddCodonsToExtension方法。在代码中我略过了对Condition(条件)的处理的分析和一些无关紧要的部分,我们把注意力集中在插件的处理。首先是一个 foreach (object o in el.ChildNodes) 遍历<Extension>下所有的子节点,对于每个子节点的处理如下: XmlElement curEl = (XmlElement)o; switch (curEl.Name) { (对条件的处理) default: ICodon codon = AddInTreeSingleton.AddInTree.CodonFactory.CreateCodon(this, curEl); AutoInitializeAttributes(codon, curEl); (对codon.InsertAfter 和codon.InsertBefore 的处理,主要是处理codon在列表中的顺序问题,这一点在对于MenuItemCodon的处理上比较重要) e.CodonCollection.Add(codon); if (curEl.ChildNodes.Count > 0) { Extension newExtension = new Extension(e.Path + '/' + codon.ID); AddCodonsToExtension(newExtension, curEl, conditions); extensions.Add(newExtension); } break; } 我们看到了一个期待已久的调用 AddInTreeSingleton.AddInTree.CodonFactory.CreateCodon(this, curEl); 经过了上文支线2.1代码中的铺垫,SharpDevelop使用建立好的CodonFactory,调用CreateCodon方法根据<Extension>下的节点构造出实际的Codon对象,一切尽在不言中了吧。 2.2.2 addInTree.InsertAddIn(把AddIn添加到AddInTree中) addIns.Add(addIn); foreach (AddIn.Extension extension in addIn.Extensions) { AddExtensions(extension); } 在DefaultAddInTree中,保存了两课树。一个是根据插件文件的结构形成的树,每个插件文件作为根节点,往下依次是Extension、Codon节点。addIns.Add(addIn);就是把插件加入到这个树结构中。另外一个树是根据Extension的Path+Codon的ID作为路径构造出来的,每一个树节点是一个AddInTreeNode类,包含了在这个路径上的Codon对象。嵌套在这个节点中的Codon在通过它子节点来访问。在DefaultAddInTree中可以通过GetTreeNode来指定一个路径获得插件树上某一个节点的内容。 3、最后一公里(Codon和Command的关联) public override object BuildItem(object owner, ArrayList subItems, ConditionCollection conditions) { System.Diagnostics.Debug.Assert(Class != null && Class.Length > 0); return AddIn.CreateObject(Class); } 调用AddIn的CreateObject,传入Codon的Class(类名)作为参数,建立这个类的实例。例如这个配置 <Extension path = "/Workspace/Autostart"> <Class id = "InitializeWorkbenchCommand" class = "ICSharpCode.SharpDevelop.Commands.InitializeWorkbenchCommand"/> </Extension> 而Codon的中的Class(类名)属性就是ICSharpCode.SharpDevelop.Commands.InitializeWorkbenchCommand。也就是说,Codon的Class指的是实现具体功能模块的Command类的名称。在读取addin配置中的<Runtime>节的时候,AddInTree把Assembly保存到了RuntimeLibraries中,因此CreateObject方法可以通过它们来查找并建立类的实例。 4、问题 下一回书,应某位网友的要求,分析一下SharpDevelop中的服务。 |
|