SharpDevelop插件系统创建过程全面分析
前言
2005年2月,我申報了一個學(xué)校組織的大學(xué)生SRTP項目,項目的題目是數(shù)據(jù)結(jié)構(gòu)動畫演示系統(tǒng)。當(dāng)初在做項目之前,我無意中買了一本書,書名為《SharpDevelop軟件項目開發(fā)全程剖析》。買這本書的目的顯而易見,就是想看看人家老外是怎么做項目的。誰知買來一看,基本看不懂。隨后,我在書里介紹的網(wǎng)站上http://www.icsharpcode.net下載了其項目的源代碼。解壓一看,更是傻掉了,搞了半天還不知道整個程序的主窗體是怎么顯示的,是什么時候顯示的。盡管這樣,我還是沒有氣餒,因為我對它整個程序有很多感興趣的東西:如可拖動的面板,可高亮度顯示的文本,專業(yè)的菜單,那些有著良好定義的XML屬性配置文件,等等。所以我就開始認(rèn)真學(xué)習(xí)它的源代碼。但是在研究的過程中發(fā)現(xiàn)它的工程文件只能用自己才能打開,而且發(fā)現(xiàn)SharpDevelop不能調(diào)試,使我分析源代碼很不方便。所以,我先從全局分析整個解決方案,最后終于把那些不熟悉項目全部都轉(zhuǎn)換成了VS中能運行的項目,而且最后整個程序能夠在VS中運行和調(diào)試。那是,我為了和大家一起分享我的成果,特地把SharpDevelop的VS版本放在了互聯(lián)網(wǎng)上,希望能對那些也想學(xué)習(xí)其源代碼的朋友有所幫助。從那之后,我就開始研究其插件(Add In)系統(tǒng)。現(xiàn)在,我已經(jīng)對其中的三個項目很熟悉,并且將它們完全整合到了我的項目中去,在研究這三個項目的過程中,我對其源代碼文件的目錄結(jié)構(gòu)按照我的理解作了調(diào)整,修改和優(yōu)化了其中的部分源代碼,另外為了幫助理解,我添加了很多的注視。好了,講了很多的廢話,下面我開始分析其源代碼:
1.?一些基本的概念:
AddInTree(插件樹)
SharpDevelop中的所有東西都是被掛接在一棵插件樹中的。這棵插件樹是在程序運行時動態(tài)創(chuàng)建的,樹的所有路徑(這些路徑是邏輯路徑)都定義在插件文件中。如果某個插件文件中有以下片斷:
<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>
???????則說明插件樹的根節(jié)點下有一個子節(jié)點Workspace,Workspace下又有Services和Icons兩個子節(jié)點,Services下又有FileUtilityService,MessageService,TaskService三個子節(jié)點。。。
?
AddIn(插件)
???????SharpDevelop中的一個插件由一個插件文件(以addin為后綴名)定義,該插件文件中定義了很多的插件樹的路徑(由Extension節(jié)點指定)。另外,該插件文件中還指定了運行該插件所需的所有的程序集。如果一個插件文件中有一個節(jié)點如下:
???????<Runtime>
?????????????<Import assembly="../bin/Base.dll"/>
???????</Runtime>
則說明,運行該插件需要一個名為Base.dll的程序集,該程序集的路徑為一個相對目錄(相對于當(dāng)前插件文件的目錄)。
Codon(代碼子)
???????按照我的理解,代碼子是一個對象創(chuàng)建器。被創(chuàng)建的對象具有某個具體的功能。如ServiceCodon(服務(wù)代碼子,這個名字在SharpDevelop中叫作ClassCodon)通過其BuildItem方法可以創(chuàng)建一個服務(wù)對象,該服務(wù)對象可為程序提供某種具體的服務(wù);PadCodon(面板代碼子)通過其BuildItem方法可以創(chuàng)建一個面板對象,該面板對象可能是一個屬性面板,文件瀏覽面板,等等。
<Service id????= "FileUtilityService"
?????????????class = "NetFocus.DataStructure.Services.FileUtilityService"/>
<Icon id?????????= "C#File"
????????????extensions = ".cs"
????????????resource???= "C#.FileIcon"/>
上面的兩個XML元素代表兩個代碼子節(jié)點,這里我把他們叫做代碼子節(jié)點是因為它們在XML文件中以元素(節(jié)點)出現(xiàn),而在程序運行時這兩個元素會被初始化成兩個代碼子對象。像上面這樣,如果某個代碼子節(jié)點有Class屬性,則說明該代碼子對象可以根據(jù)這個屬性值創(chuàng)建一個對象,如果沒有提供Class屬性,則該代碼子對象直接將自己返回。這里,值得一提的是,所有的Class屬性所指定的類必須在上面介紹的<Runtime>節(jié)點中指定的某個程序集中定義過。這一點是我對SharpDevelop源代碼的修改,因為在SharpDevelop中,如果Class定義的某個類在當(dāng)前插件文件的<Runtime>節(jié)點中沒有指定,程序會將當(dāng)前未初始化好的代碼子對象先保存起來,待當(dāng)前插件初始化完成之后,再嘗試從后面的插件文件中所指定的程序集中創(chuàng)建該代碼子。
AddInTreeNode(插件樹節(jié)點)
???????既然整個程序的結(jié)構(gòu)是一棵插件樹,就肯定有很多插件樹節(jié)點。那么插件樹節(jié)點有什么功能呢?其實,在整棵插件樹中并不是所有的節(jié)點都有功能,有些節(jié)點沒有任何功能,只是起到構(gòu)成路徑的作用;而有些節(jié)點如上面所講的FileUtilityService,MessageService,TaskService三個子節(jié)點才有具體的功能,因為它們在整棵樹中不僅代表一個節(jié)點,而且還代表一個代碼子。實際上,在創(chuàng)建整棵樹的過程中,如果為當(dāng)前節(jié)點指定了一個代碼子對象,則該樹節(jié)點就具有具體的功能,如果沒有指定,則該節(jié)點只是起到構(gòu)成路徑的作用。
?
?
?
?
?
?
2.?整個插件系統(tǒng)的創(chuàng)建過程:
為了方便分析,我用我自己的項目代碼來給大家分析,各位看官不必?fù)?dān)心,因為我只是稍微修改了其插件系統(tǒng),大部分代碼沒有變化。所以在閱讀下面的內(nèi)容之前建議大家去我的個人網(wǎng)站(http://www.netfocus.cn)下載程序源代碼“數(shù)據(jù)結(jié)構(gòu)動畫演示系統(tǒng)“,以方便你理解本文。
和SharpDevelop一樣,程序的入口點在/src/StartUp/DataStructureMain.cs的Main函數(shù)中。至于前面語句我在這里就不在解釋了,我已經(jīng)標(biāo)了注視。下面從AddInTreeSingleton.CreateAddInTree()方法開始分析。首先,從AddInTreeSingleton這個類的名在來看,這個類應(yīng)該采用Singleton設(shè)計模式。下面轉(zhuǎn)入CreateAddInTree方法,
首先判斷插件樹是否為空,即插件樹是否已經(jīng)創(chuàng)建,如果沒有創(chuàng)建,則實例化一個默認(rèn)的插件樹對象。代碼如下:
if(addInTree ==?null)
??????????????{
???????????????????addInTree =?new?DefaultAddInTree();
???????????????????InternalFileService fileUtilityService =?new?InternalFileService();
???????????????????StringCollection addInFiles =?null;
???????????????????if?(ignoreDefaultCoreAddInDirectory ==?false)?//如果沒有忽略默認(rèn)的插件路徑,即采用默認(rèn)的插件路徑
???????????????????{
???????????????????????addInFiles = fileUtilityService.SearchDirectory(defaultCoreAddInDirectory, "*.addin");
???????????????????????InsertAddIns(addInFiles);
???????????????????}
???????????????????else??//如果忽略默認(rèn)的插件文件的路徑
???????????????????{
???????????????????????if?(addInDirectories !=?null)
???????????????????????{
????????????????????????????foreach(string?path?in?addInDirectories)
????????????????????????????{
?????????????????????????????????addInFiles = fileUtilityService.SearchDirectory(Application.StartupPath + Path.DirectorySeparatorChar + path, "*.addin");
?????????????????????????????????InsertAddIns(addInFiles);
????????????????????????????}
???????????????????????}
???????????????????}
??????????????}
在DefaultAddInTree的構(gòu)造函數(shù)中,通過LoadCodonsAndConditions(Assembly.GetExecutingAssembly());
從核心項目Core程序集中加載所有的代碼子和條件對象。那么這個函數(shù)到底作了什么呢?
首先從程序集中得到所有的類型,然后判斷如果某個類型是從AbstractCodon抽象類繼承而來并且這個類型上有CodonNameAttribute特性,則說明當(dāng)前類是一個具有某個功能的代碼子類,然后立即創(chuàng)建一個CodonBuilder對象,并將該對象添加到一個Factory工廠對象的Builders哈希表中。添加時以CodonName為主鍵。另外,對于條件操作也是類似操作。
繼續(xù)回到CreateAddInTree方法中,判斷是否忽略默認(rèn)的插件文件路徑,如果不忽略,則使用默認(rèn)的插件文件路徑“../addIns“來搜索所有的插件文件,并放到一個字符串集合中。然后調(diào)用InsertAddIns方法。下面是這個方法的源代碼:
?????????static?void?InsertAddIns(StringCollection addInFiles)
?????????{
??????????????foreach?(string?addInFile?in?addInFiles)
??????????????{
???????????????????AddIn addIn =?new?AddIn();//先新建一個插件實例
???????????????????try
???????????????????{
???????????????????????addIn.Initialize(addInFile);//通過當(dāng)前插件文件來初始化這個插件實例
???????????????????????addInTree.InsertAddIn(addIn);//將這個初始化好的插件插入到插件樹中
???????????????????}
???????????????????catch?(Exception e)
???????????????????{
???????????????????????throw?new?AddInInitializeException(addInFile, e);
???????????????????}
??????????????}
?????????????
?????????}
這個方法的功能是接受一個插件文件的集合列表參數(shù),然后對每個插件文件都創(chuàng)建一個插件對象,并將該插件對象插入到插件樹中。在這個過程中,關(guān)鍵是插件對象如何創(chuàng)建?以及創(chuàng)建好的對象如何插入到插件樹中?所以,如果這兩個問題解釋清楚了,那么,整個插件系統(tǒng)的創(chuàng)建過程就清楚了。現(xiàn)面先分析插件對象如何創(chuàng)建,轉(zhuǎn)入AddIn文件的Initialize方法:
該方法的關(guān)鍵是在foreach循環(huán)中,它對插件文件根節(jié)點下的所有子節(jié)點進(jìn)行迭代,判斷子節(jié)點的類型,
如果為RunTime節(jié)點,則將該節(jié)點的所有子節(jié)點所指定的程序集加載到內(nèi)存,并將該程序集對象添加到一個哈希表中。當(dāng)然,這里并不是只是簡單的根據(jù)程序集文件名創(chuàng)建一個程序集對象,在創(chuàng)建了程序集對象之后,還像剛開始從核心項目Core程序集加載所有的代碼子對象一樣加載了當(dāng)前程序集中的所有的代碼子和條件對象。
如果為Extension節(jié)點,則說明當(dāng)前節(jié)點是一個功能擴(kuò)展,所謂功能擴(kuò)展是指在某個插件樹路徑下的功能集合。一般情況下一個功能擴(kuò)展的特點是:
一個路徑(Path):指定該功能擴(kuò)展在整個系統(tǒng)中的邏輯路徑;
一些可以嵌套定義的代碼子節(jié)點;
一些作用在代碼子節(jié)點上的條件節(jié)點;
所以,在SharpDevelop中特別定義了一個Extension類,專門用來描述XML文件中定義的Extension節(jié)點。
在遇到Extension節(jié)點時調(diào)用AddExtensions方法,該方法接受一個Extension XML節(jié)點,在AddExtensions方法內(nèi)部關(guān)鍵是AddCodonsToExtension方法,該方法比較復(fù)雜,下面我分析其中最關(guān)鍵的default部分:
?
?
首先通過ICodon codon = AddInTreeSingleton.AddInTree.CodonFactory.CreateCodon(this, curEl);這句話創(chuàng)建一個代碼子對象,大家一定還不清楚這句話是如何工作的。下面,我再詳細(xì)解釋:
AddInTree這個對象我在前面已經(jīng)顯式創(chuàng)建好了,大家應(yīng)該還記得。然后是CodonFactory的CreateCodon方法。不知大家是否還記得,在先前加載每個程序集的時候,都調(diào)用了LoadCodonsAndConditions這個方法,在這個方法內(nèi)部掃描當(dāng)前程序集的所有類型,并把繼承自AbstractCodon類的所有子類都封裝在一個對應(yīng)的CodonBuilder對象中,最后把這些CodonBuilder都添加到了CodonFactory的Builders集合中。所以,當(dāng)現(xiàn)在調(diào)用CodonFactory的CreateCodon方法時,會先根據(jù)代碼子名(CodonName)找到一個相應(yīng)的CodonBuilder對象,然后調(diào)用該對象的BuildCodon方法,在該方法中有這么一句話:
codon = (ICodon)assembly.CreateInstance(ClassName,?true);
它通過一個具體的類名從一個程序集中創(chuàng)建一個代碼子對象,最后將該代碼子對象和當(dāng)前插件對象關(guān)聯(lián)。
知道了代碼子對象如何創(chuàng)建之后,程序通過以下這句話:
AutoInitializeAttributes(codon, curEl);
上面這個函數(shù)的功能是全面檢索當(dāng)前被創(chuàng)建的代碼子對象的所有的屬性(包括基類的屬性),
如果被掃描到的屬性在XML文件中指定了值,則把該值賦給這個屬性,如果沒有指定,則不賦值。掃描時屬性分為兩類,一般屬性和數(shù)組類型的屬性。然后,程序?qū)Υa子進(jìn)行排序之后將當(dāng)前代碼子對象插入到擴(kuò)展對象e的一個集合中。最后,如果當(dāng)前節(jié)點有子節(jié)點(如<MenuItem>節(jié)點中有子<MenuItem>節(jié)點),則對其子節(jié)點進(jìn)行遞歸操作。
好了,下面回到Initialize方法。當(dāng)該方法按照上面那樣執(zhí)行結(jié)束之后,系統(tǒng)應(yīng)該做了以下兩件重要事情:1)系統(tǒng)已經(jīng)加載了該插件運行所需的全部程序集;2)系統(tǒng)已經(jīng)將這些程序集中的所有的代碼子類和條件類全部創(chuàng)建完成,并都添加到了相應(yīng)的Extension對象的codonCollection和Conditions集合中。
?
下面開始討論AddInTree的InsertAddIn方法。
第一步,先將傳過來的插件對象加入到一個插件集合中,剛才談到一個Extension對象的codonCollection集合包含了該插件的某些代碼子對象。所以,接下來先在外層循環(huán)對該插件對象的Extensions集合列表進(jìn)行迭代,在該循環(huán)內(nèi)部,先通過Extension對象所指定的Path屬性創(chuàng)建一個插件樹節(jié)點,注意創(chuàng)建該節(jié)點時總是以root為樹的根節(jié)點,因為Path所指定的邏輯插件路徑總是從根節(jié)點出發(fā)的。如下面的語句所示:
DefaultAddInTreeNode currentNode = CreateTreeNode(root, extension.Path);
接下來,再對Extension里的codonCollection集合進(jìn)行迭代,并把這些codon對象作為插件樹的節(jié)點一個個插入到相應(yīng)的位置。并將當(dāng)前插件樹節(jié)點與一個代碼子對象關(guān)聯(lián),使其具有某種具體的功能。注意:這里插入時,將代碼子對象的ID屬性作為了路徑的一部分,其實就是將一個代碼子對象也作為一個樹節(jié)點看待,節(jié)點的名字就是該代碼子對象的ID屬性。這里有一個關(guān)于插件樹節(jié)點如何創(chuàng)建的問題。這個功能是通過以下函數(shù)完成的:
DefaultAddInTreeNode CreateTreeNode(DefaultAddInTreeNode currentNode,?string?path)
該函數(shù)接受一個當(dāng)前節(jié)點對象和一個樹路徑字符串信息。首先將該路徑字符串進(jìn)行分割,放在一個數(shù)組中(這個數(shù)組中的每個元素代表一個樹節(jié)點的名稱),然后根據(jù)這個數(shù)組中的節(jié)點名逐個構(gòu)造樹節(jié)點。有兩種情況:
1)??如果由某個節(jié)點名指定的節(jié)點對象已經(jīng)創(chuàng)建好了,則直接將該節(jié)點作為下一次的當(dāng)前節(jié)點,并進(jìn)入下一個循環(huán)繼續(xù)創(chuàng)建其子節(jié)點。
2)??如果由某個節(jié)點名指定的節(jié)點對象還沒有創(chuàng)建,則馬上創(chuàng)建該節(jié)點并將其作為當(dāng)前節(jié)點的子節(jié)點。
所以從插件樹中節(jié)點的創(chuàng)建過程來看,整棵插件樹在邏輯上是一棵樹,但在內(nèi)部其實是為每個節(jié)點都設(shè)置了一個ChildNodes集合,用來存放當(dāng)前節(jié)點的子節(jié)點。另外對于條件對象也是一樣,這里就不在詳細(xì)討論了。當(dāng)InsertAddIn方法執(zhí)行完畢時,已經(jīng)順利地將當(dāng)前插件對象插入到插件樹中。最后當(dāng)AddInSingleton的InsertAddIns方法執(zhí)行完成時,整棵插件樹就應(yīng)該成功地被創(chuàng)建了。
到這里,各位看官應(yīng)該已經(jīng)了解了整棵插件樹是怎樣被創(chuàng)建和組織起來的。下面為了清楚起見,我再簡單回顧一下整個創(chuàng)建流程:
1:在Main函數(shù)中顯式調(diào)用AddInTreeSingleton的CreateAddInTree()方法
???????AddInTreeSingleton.CreateAddInTree();
2:先獲取所有插件文件列表,然后調(diào)用InsertAddIns()方法
???????InsertAddIns(addInFiles);
3:對文件類表進(jìn)行迭代,對每個插件文件都創(chuàng)建一個插件,并將其插入到插件樹中
???????static?void?InsertAddIns(StringCollection addInFiles)
4:用一個XML插件文件來初始化一個插件對象
???????addIn.Initialize(addInFile);
5:在上面的插件初始化函數(shù)中,先分析<RunTime>元素節(jié)點,并創(chuàng)建一些CodonBuilder和ConditionBuilder對象,這些對象可以創(chuàng)建Codon和Condition對象。用以下函數(shù)實現(xiàn):
???????AddRuntimeLibraries(curEl);
6:分析<Extension>元素節(jié)點,根據(jù)這些節(jié)點并利用上面所提到的CodonBuilder和ConditionBuilder對象來創(chuàng)建Codon和Condition對象。
???????AddExtensions(curEl);?――>?AddCodonsToExtension
7:插件對象創(chuàng)建好之后,將該插件對象插入到插件樹中。
???????addInTree.InsertAddIn(addIn);
8:在將插件對象插入到樹的過程中,Extension對象的Path屬性只用于構(gòu)成樹的路徑,而Extension對象的CodonCollection集合中的每一個Codon對象不僅作為一個樹節(jié)點(構(gòu)成路徑),而且具有某個具體的功能。創(chuàng)建樹節(jié)點的函數(shù)如下:
???????DefaultAddInTreeNode CreateTreeNode(DefaultAddInTreeNode currentNode,?string?path);
9:最后層層返回,最終完成整棵插件樹的創(chuàng)建。
?
?
小結(jié):
整個插件系統(tǒng)的創(chuàng)建應(yīng)該說比較復(fù)雜,其中用到了一些設(shè)計模式:
1.因為插件樹在一個程序一次運行中只有一個實例,所以采用了Singleton設(shè)計模式;
2.代碼子和條件對象的創(chuàng)建方式,程序中使用了“Builder”字眼,看上去好像是使用了Builder設(shè)計模式,但我認(rèn)為這跟Builder設(shè)計模式的真正用途是不一樣的。因為Builder設(shè)計模式強調(diào)一個對象的創(chuàng)建過程,并最終將該對象返回。而這里只是簡單的創(chuàng)建了一個代碼子或條件對象,真正的創(chuàng)建任務(wù)是由代碼子或條件對象來完成的。所以,這樣來看這用方式應(yīng)該更加符合Proxy設(shè)計模式,各位看官覺得如何呢?
3.CodonFactory這個類,一看就是工廠設(shè)計模式。但它不是一個一般的工廠模式,而是對一個工廠模式的優(yōu)化,因為它可以創(chuàng)建任意種類的Codon對象,它并沒有提供一個固定的創(chuàng)建一系列產(chǎn)品的方法列表接口。而是提供了一個集合,集合重的元素可以動態(tài)添加,每個元素都可以創(chuàng)建一個Codon或Condition對象。
插件系統(tǒng)如何創(chuàng)建今天就先介紹到這里,明天介紹Command、Service以及插件樹節(jié)點中的每個功能是如何來體現(xiàn)和使用的。
總結(jié)
以上是生活随笔為你收集整理的SharpDevelop插件系统创建过程全面分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入Redis内部-Redis 源码讲解
- 下一篇: 自己动手写文件系统