【转载】SharpDevelop代码分析
【原文地址】http://www.cnblogs.com/passos/archive/2004/10/04/48950.html
序
??? 最近開始學習.Net,遇到了一個比較不錯的開源的IDE SharpDevelop。這個開發工具是使用C#開發的,比較吸引我的一點就是它是采用了和Eclipse類似的插件技術來實現整個系統的。而這個插件系統是我最感興趣的地方,因此開始了一段代碼的研究。在本篇之后,我會陸續把我研究的心得寫下來。由于是在網吧上網,有諸多不便,因此可能會拖比較長的時間。
一、基本概念
??? 首先,我們先來對 SharpDevelop 有一個比較感性的認識。你可以從這里下載到它的可執行程序和代碼包????http://www.icsharpcode.com/? ,安裝的廢話就不說了,先運行一下看看。感覺跟VS很像吧?不過目前的版本是1.0.0.1550,還有很多地方需要完善。關于代碼和系統結構,SharpDevelop的三個作者寫了一本書,各位看官可以參考一下,不過我看過之后還是有很多地方不太理解。
??? 然后,讓我來解釋一下什么叫插件以及為什么要使用插件系統。我們以往的系統,開發人員編譯發布之后,系統就不允許進行更改和擴充了,如果要進行某個功能的擴充,則必須要修改代碼重新編譯發布。這就給我們帶來了比較大的不方便。解決的方法有很多,例如提供配置等等方法。在解決方案之中,插件是一個比較好的解決方法。大家一定知道PhotoShop、WinAmp吧,他們都有“插件”的概念,允許其他開發人員根據系統預定的接口編寫擴展功能(例如PhotoShop中各種各樣的濾鏡)。所謂的插件就是系統的擴展功能模塊,這個模塊是以一個獨立文件的形式出現的,與系統是相對獨立。在系統設計期間并不知道插件的具體功能,僅僅是在系統中為插件留下預定的接口,系統啟動的時候根據插件的配置尋找插件,根據預定的接口把插件掛接到系統中。
??? 這樣的方式帶來什么樣的優點呢?首先是系統的擴展性大大的增強了,如果我們在系統發布后需要對系統進行擴充,不必重新編譯,只需要修改插件就可以了。其次有利與團隊開發,各個功能模塊由于是以插件的形式表現在系統中,系統的每日構造就很簡單了,不會因為某個模塊的錯誤而導致整個系統的BUILD失敗。失敗的僅僅是一個插件而已。
??? PhotoShop和Winamp的插件系統是比較簡單的,他們首先實現了一個基本的系統,然后在這個系統的基礎上掛接其他擴展的功能插件。而SharpDevelop的插件系統更加強大,它的整個系統的基礎就僅僅是一個插件管理系統,而你看到的所有的界面、功能統統都是以插件的形式掛入的。在這樣的一個插件系統下,我們可以不修改基本系統,僅僅使用插件就構造出各種各樣不同的系統。
??? 現在讓我們來看看它的插件系統。進入到SharpDevelop的安裝目錄中,在Bin目錄下的SharpDevelop.exe 和 SharpDevelop.Core.dll是這個系統的基本的插件系統。在Addins目錄下有兩個后綴是addin的文件,其中一個 SharpDevelopCore.addin 就是它的核心插件的定義(配置)文件,里面定義的各個功能模塊存在于Bin\Sharpdevelop.Base.dll 文件中,另外還有很多其他的插件定義在Addins目錄下的addin文件中。
??? 分析SharpDevelop的代碼,首先要弄清楚幾個基本的概念,這些概念和我以前的預想有一些區別,我深入了代碼之后才發現我的困惑所在。
1、AddInTree??插件樹
??? SharpDevelop 中的插件被組織成一棵插件樹結構,樹的結構是通過 Extension(擴展點)中定義的Path(路徑)來定義的,類似一個文件系統的目錄結構。系統中的每一個插件都在配置文件中指定了 Extension,通過Extension中指定的 Path 掛到這棵插件樹上。在系統中可以通過 AddTreeSingleton對象來訪問各個插件,以實現插件之間的互動。
2、?AddIn 插件
??? 在 SharpDevelop 的概念中,插件是包含多個功能模塊的集合(而不是我過去認為的一個功能模塊)。在文件的表現形式上是一個addin配置文件,在系統中對應 AddIn 類。
3、Extension 擴展點
??? SharpDevelop中的每一個插件都會被掛到 AddInTree(插件樹) 中,而具體掛接到這個插件樹的哪個位置,則是由插件的 Extension 對象中的 Path?指定的。在addin 配置文件中,對應于 <Extension> 。例如下面這個功能模塊的配置
?????????<Class?id????=?".NET"?class?=?"ICSharpCode.SharpDevelop.Services.NetAmbience"/>
?</Extension>
指定了擴展點路徑為 /SharpDevelop/Workbench/Ambiences ,也就是在插件樹中的位置。
4、Codon
??? 這個是一個比較不好理解的東西,在 SharpDevelop 的三個作者寫的書的中譯版中被翻譯為密碼子,真是個糟糕的翻譯,可以跟Handle(句柄)有一拼了。詞典中還有一個翻譯叫“基碼”,我覺得這個也不算好,不過還稍微有那么一點意思。(這里我原來誤寫為“代碼子”,在評論中有位仁兄說這個翻譯不錯,現在我覺得也好像確實不錯 ^o^)
??? 根據我對代碼的理解,Codon 的功能是描述(包裝)一個功能模塊(一個功能模塊對應一個實現了具體功能的 Command 類)。為了方便訪問各個插件中的功能模塊, Codon 給各種功能定義了基本的屬性,分別是 ID (功能模塊的標識),Name (功能模塊的類型。別誤會,這個Name 是addin文件定義中Codon的XML結點的名稱,ID才是真正的名稱),其中Name可能是Class(類)、MenuItem(菜單項)、Pad(面板)等等。根據具體的功能模塊,可以繼承Codon定義其他的一些屬性,SharpDevelop中就定義了 ClassCodon、MenuItemCodon、PadCodon等等,你可以根據需要自己定義其他類型的Codon。在addin定義文件中,Codon對應于 <Extension> 標簽下的內容。例如下面這個定義
?????????<Class?id????=?".NET"?class?=?"ICSharpCode.SharpDevelop.Services.NetAmbience"/>
?</Extension>
<Extension ...> 內部定義了一個Codon,<Class ...>? 表示該Codon是一個 Class(類),接著定義了該Codon的?ID和具體實現該Codon的類名ICSharpCode.SharpDevelop.Services.NetAmbience。運行期間將通過反射來找到對應的類并創建出來,這一點也是我們無法在以前的語言中實現的。
再例如這一個定義
????????????????<MenuItem?id?=?"Compile"
??????????????????????????label?=?"${res:XML.MainMenu.RunMenu.Compile}"?
??????????????????????????class?=?"ICSharpCode.SharpDevelop.Commands.Compile"/>
????????????????<MenuItem?id?=?"CompileAll"
??????????????????????????label?=?"${res:XML.MainMenu.RunMenu.CompileAll}"?
??????????????????????????class?=?"ICSharpCode.SharpDevelop.Commands.CompileAll"/>
????????????????<MenuItem?id?=?"CombineBuildGroupSeparator"?label?=?"-"?/>
?.
</Extension>
這個擴展點中定義了三個菜單項,以及各個菜單項的名字、標簽和實現的類名。這里的Codon就對應于系統中的MenuCodon對象。
5、Command 命令
??? 正如前文所述,Codon描述了一個功能模塊,而每個功能模塊都是一個 ICommand 的實現。最基本的 Command 是? AbstractCommand,根據Codon的不同對應了不同的 Command。例如 MenuItemCodon 對應 MenuItemCommand 等等。
6、Service 服務
??? 插件系統中,有一些功能是整個系統都要使用的,例如文件訪問、資源、消息等等。這些功能都作為插件系統的一個基本功能為整個系統提供服務,我們就叫“服務”好了。為了便于訪問,這些服務都統一通過 ServiceManager 來管理。其實服務也是一種類型的插件,它們的擴展點路徑在目錄樹中的 /Workspace/Services 中。
??? 理解了這幾個基本的概念之后,就可以看看 SharpDevelop 的代碼了。從 src\main\startup.cs 看起吧,之后是addin.cs、addinTree.cs?等等。?
二、主程序+隱藏的初始化
?????在SharpDevelop的代碼中,由于很多的接口和插件的原因,很多代碼在看到某個地方會突然失去函數/方法調用的線索。例如看某個函數的實現的時候會跳到一個接口里面去,那是因為這部分功能在運行期才會給一個實現了這個接口的對象來進行具體的執行。從這個角度來說,設計模式也給我們研究代碼稍微帶來了一點小小的難度。在看Linux下源代碼的時候也經常遇到這種問題,在這個時候尋找代碼線索比較好的方法是用一個文本搜索工具來搜索相關的關鍵字。在Linux下我經常會用grep,Windows下面類似UltraEdit的“批量文件查找”功能會很好用(或者“Search And Replace”之類的工具)。這個是我讀代碼的一點小小的經驗,如果你知道有更好的方法,請告訴我讓我也學習一下 ? 。
?????我不想大段大段的貼代碼出來占地方(空間、帶寬,還有各位看官的注意力),在需要的地方我會貼上主要的代碼,因此最好能夠找代碼來對應著看。把代碼包解壓縮,我把它解到了“F:\SharpDevelop”(如果沒有說明,下文都是以此為代碼的根目錄了)。由于SharpDevelop本身對于察看代碼不是很方便,沒有“轉到定義”之類的功能,因此我建議你把它的代碼轉成VS的工程來看。不過很可惜,SharpDevelop的工程導出功能現在有問題,如果導出\src\SharpDevelop.cmbx 這個總的復合工程的話會失敗(我記得RC1版本是可以成功的,不知道為什么后來的版本反而會出問題),所以只能一個一個工程的導出。
?????好了,讓我們來看SharpDevelop的代碼吧。
1、起點
??? 在主程序的起點在\src\Main\StartUp\SharpDevelopMain.cs,找到Main函數這就是整個程序的起點了。開始的部分是顯示封面窗體并加上命令行控制,其中SplashScreenForm 定義在\src\Main\Base\Gui\Dialogs\SplashScreen.cs文件中,這部分我就不多說了。之后是 Application.ThreadException?+=?new?ThreadExceptionEventHandler(ShowErrorBox);
??? SharpDevelop為了有效的進行錯誤報告,因此自己進行了異常的控制。系統出現異常的時候,SharpDevelop會攔截下來彈出它自己的異常提示報告對話框。這個代碼就是在這一行實現的。其中 ShowErrorBox 這個方法就在類SharpDevelopMain中,ExceptionBox 定義在\src\Main\StartUp\Dialogs\ExceptionBox.cs中。如果需要進行自己的異常控制,可以學習一下這里的技巧。
2、充滿玄機的初始化
string?[]?addInDirs?=?ICSharpCode.SharpDevelop.AddInSettingsHandler.GetAddInDirectories(?out?ignoreDefaultPath?);AddInTreeSingleton.SetAddInDirectories(addInDirs,?ignoreDefaultPath);
??? 通過AddInSettingsHandler取得插件的目錄,并告知AddInTreeSingleton。AddInSettingsHandler定義在\src\Main\StartUp\Dialogs\AddInTreeSettingsHandler.cs中,它通過讀取系統配置(App.config)文件中的AddInDirectory節點的Path屬性來確定插件的目錄位置,或者你也可以通過自己定義的AddInDirectories節來指定插件目錄。如果你沒有做這些配置,默認的目錄在SharpDevelop運行目錄的..\Addins目錄下。
ServiceManager.Services.AddService(new?MessageService());ServiceManager.Services.AddService(new?ResourceService());
ServiceManager.Services.AddService(new?IconService());
??? 通過ServiceManager(服務管理器)加入三個系統默認的服務,消息服務、資源服務、圖標服務。這三個服務中,消息服務是顯示各種信息提示,另外兩個是屬于系統的資源,SharpDevelop通過服務來進行統一調用和管理。
ServiceManager.Services.InitializeServicesSubsystem("/Workspace/Services");
??? 初始化其他的服務。SharpDevelop把服務定義在插件樹的/Workspace/Services這個路徑中,凡是在這個路徑下的插件都被認為是服務,因此如果你自己定義了一個服務的話,也需要掛到這個路徑下(這里就是系統服務的擴展點了)。
??? 注意!這一步中,在我們的眼皮子底下悄悄的進行了一個重要的初始化工作。各位看官請看,ServiceManager 定義在\src\Main\Core\Services\ ServiceManager.cs文件中,察看它的InitializeServicesSubsystem方法,我們發現這樣一行
??? 在這里,AddInTreeSingleton首次調用了AddInTree(插件樹)的實例。按照Singleton模式,只有在首次調用的時候才會初始化實例,這里也是同樣如此。整個系統的AddInTree是在這一步中進行了初始化工作,稍候我們將詳細介紹AddInTree如何進行初始化工作,先順便看看服務的初始化。在ServiceManager的InitializeServicesSubsystem方法中,通過AddInTree檢索服務插件路徑下的所有配置,并通過它來讀取、建立具體的對象,然后加入到服務列表中。之后通過一個循環,逐個的調用各個服務的InitializeService方法初始化服務。
??? AddInTree的初始化工作容我們稍候再看,先把主體的代碼看完。
commands = AddInTreeSingleton.AddInTree.GetTreeNode("/Workspace/Autostart").BuildChildItems(null);for (int i = 0; i < commands.Count - 1; ++i)
{
?((ICommand)commands[i]).Run();
}
??? /Workspace/Autostart是系統自動運行命令的擴展點路徑,定義在這個路徑下的插件會在系統啟動的時候自動運行。在這里,通過插件樹初始化建立處于這個路徑下的Command(命令),并逐一執行。BuildChildItems方法的功能是建立這個擴展點下的Command列表,我會在介紹AddTree的時候具體說明它的實現。
???? 主程序代碼的最后,初始化完畢、關閉封面窗體,然后執行命令列表中最后一個命令(也就是系統的主界面)。在主界面退出的時候,系統卸載所有的服務。
??? 在這部分代碼中,我們知道了兩個系統指定的擴展點路徑 /Workspace/Services 和 /Workspace/Autostart ,我們實現服務和指定系統自動運行命令的時候就可以掛到這兩個擴展點路徑下了。
???? 托反射的福,ServiceManager.Services可以通過類型(接口)來查找具體的實例,也就是GetServices方法。但是ServiceManager的具體實現我們可以容后再看,這里已經不是最緊要的部分了。
???? 接下來,我們來看看整個插件系統的核心-AddinTree的代碼,看看它是如何通過插件配置進行初始化并建立起整個系統的插件樹骨干。
?? 上回書說到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="http://www.cnblogs.com/bin/?AddinTreeView.dll"/>
?</Runtime>
?<Extension?path?=?"/SharpDevelop/Workbench/MainMenu/Tools">
??<MenuItem?id?=?"AddinTreeView"?
???label?=?"View?AddinTree"?
???class?=?"Addins.AddinTreeView.AddinTreeViewCommand"/>
?</Extension>?
</AddIn>
?? 在配置文件中,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="http://www.cnblogs.com/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>
?? 這樣,我把兩個插件的功能模塊使用一個插件配置文件來進行配置。同樣的,我也可以把幾十個功能模塊合并到一個插件配置文件中。SharpDevelop把這個插件配置文件稱為“Addin(插件)”,而把具體的功能模塊封裝為Codon,使用Command類來包裝具體的功能。SharpDevelop本身的核心配置SharpDevelopCore.addin里面就包含了所有的基本菜單、工具欄、PAD的插件配置。
我們回過頭來看一下,現在我們有了兩顆樹。首先,插件樹本身是一個樹形的結構,這個樹是根據系統所有插件的各個Codon的擴展點路徑構造的,表示了各個Codon在插件樹中的位置,各位看官可以通過我寫的這個小小的AddinTreeView來看看SharpDevelop中實際的結構。其次,插件的配置文件本身也具有了一個樹形的結構,這個樹結構的根節點是系統的各個插件配置文件,其下是根據這個配置文件中的Extension節點的來構成的,描述了每個Extension節點下具有的Codon。我們可以通過SharpDevelop的Tools菜單下的AddinScout來看看這個樹的結構。
我為了試驗,把SharpDevelop的插件精簡了很多,構成了一個簡單的小插件系統。下面是這個精簡系統的兩個樹的截圖。各位看官可以通過這兩副圖理解一下插件樹和插件配置文件的關系(只是看同樣問題的兩個角度,一個是Codon的ExtensionPath,一個是配置文件的內容)。
總結一下SharpDevelop插件的配置文件格式。首先是 <AddIn>節點,需要指定AddIn的名稱、作者之類的屬性。其次,在AddIn節點下的<Runtime>節點內,使用<Import …>來指定本插件配置中Codon所在的庫文件。如果分布在多個庫文件中,可以一一指明。然后,編寫具體功能模塊的配置。每個功能模塊的配置都以擴展點<Extension>開始,指定了路徑(Path)屬性之后,在這個節點內配置在這個擴展點下具體的Codon。每個Codon根據具體不同的實現有不同的屬性。各位看官可以研究一下SharpDevelop的核心配置文件SharpDevelopCore.addin的寫法,相信很容易理解的。
2、插件系統的核心AddIn和AddInTree
?? 前文講到,在SharpDevelop的Main函數中,ServiceManager.Service在InitializeServicesSubsystem方法中首次調用了AddInTreeSingleton的AddInTree實例,AddinTree在這個時候進行了初始化。現在我們就來看看AddInTreeSingleton.AddInTree到底做了些什么事情,它定義在\src\Main\Core\AddIns\AddInTreeSingleton.cs文件中。
???{
??????get?
??????{
?????????if?(addInTree?==?null)?
?????????{
????????????CreateAddInTree();
?????????}
?????????return?addInTree;
??????}
???}
?? AddInTreeSingleton是插件樹的一個Singleton(具體的可以去看《設計模式》了),AddInTreeSingleton.AddInTree是一個屬性,返回一個IAddinTree接口。這里我注意到一點,AddInTreeSingleton是從DefaultAddInTree繼承下來的。既然它是一個單件模式,包含的方法全部都是靜態方法,沒有實例化的必要,而且外部是通過AddInTree屬性來訪問插件樹,為什么要從DefaultAddInTree繼承呢?這好像沒有什么必要。這也許是重構過程中被遺漏的一個小問題吧。
?? 我們先來看看IAddinTree接口的內容,它定義了這樣的幾個內容:
????? A、屬性ConditionFactory ConditionFactory 返回一個構造條件的工廠類,這里的條件是指插件配置中的條件,我們以后再詳細說明。
????? B、屬性CodonFactory CodonFactory 返回一個構造Codon的工廠類。
????? C、屬性AddInCollection AddIns 返回插件樹的根節點Addin(插件)集合。
????? D、方法IAddInTreeNode GetTreeNode(string path) 根據擴展點路徑(path)返回對應的樹節點
????? E、方法void InsertAddIn(AddIn addIn) 根據AddIn中的擴展點路徑添加一個插件到樹中
????? F、方法void RemoveAddIn(AddIn addIn) 刪除一個插件
????? G、方法Assembly LoadAssembly(string assemblyFile)? 讀入插件中Runtime節的Import指定的Assembly,并構造相應的CodonFactory和CodonFactory類。
?? AddInTreeSingleton在首次調用AddInTree的時候會調用CreateAddInTree方法來進行初始化。CreateAddInTree方法是這樣實現的:
addInTree?=?new?DefaultAddInTree();????? 初始化插件樹為DefaultAddInTree的實例,這里我感受到了一點重構的痕跡。首先,DefaultAddInTree從名稱上看是默認的插件樹(既然是默認,那么換句話說還可以有其他的插件樹)。但是SharpDevelop并沒有給外部提供使用自定義插件樹的接口(除非我們修改這里的代碼),也就是說這個名稱并不像它本身所暗示的那樣。其次,按照Singleton通常的寫法以及前面提到AddInTreeSingleton是從DefaultAddInTree繼承下來的疑問,我猜想DefaultAddinTree的內容本來是在AddinTreeSingleton里面實現的,后來也許為了代碼的條理性,把實現IAddinTree內容的代碼剝離了出去,形成了DefaultAddinTree類。至于繼承DefaultAddInTree的問題,也許這里本來是一個AddInTree的基類。這是題外話,也未加證實,各位看官可以不必放在心上(有興趣的可以去找找以前SharpDevelop的老版本的代碼來看看)。
這里有兩個察看代碼的線路,一個是DefaultAddInTree的構造函數的代碼,在這個構造函數中構造了Codon和Condtion的工廠類。另外一個是CreateAddInTree后面的代碼,搜索插件文件,并根據插件文件進行AddIn的構造。各位看官可以選擇走分支線路,也可以選擇先看主線(不過這樣你會漏掉不少內容)。
2.1 支線 (DefaultAddInTree的構造函數)
?? 我們把CreateAddInTree的代碼中斷一下壓棧先,跳到DefaultAddInTree的構造函數中去看一看。DefaultAddInTree定義在\src\Main\Core\AddIns\DefaultAddInTree.cs文件中。在DefaultAddInTree的構造函數中,注意到它具有一個修飾符internal,也就是說這個類只允許Core這個程序集中的類對DefaultAddInTree進行實例化(真狠啊)。構造函數中的代碼只有一句:
?? 雖然只有一行代碼,不過這里所包含的內容卻很精巧,是全局的關鍵,要講清楚我可有得寫了。首先,通過全局的Assembly對象取得入口程序的Assembly,傳入LoadCodonsAndConditions方法中。在該方法中,枚舉傳入的Assembly中的所有數據類型。如果不是抽象的,并且是AbstractCodon的子類,并且具有對應的CodonNameAttribute屬性信息,那么就根據這個類的名稱建立一個對應的CodonBuilder并它加入CodonFactory中(之后對Condition也進行了同樣的操作,我們專注來看Codon部分,Condition跟Codon基本上是一樣的)。
?? 這里的CodonFactory類和CodonBuilder類構成了SharpDevelop插件系統靈活的基礎,各位看官可要看仔細了。
?? 我們以實例來演示,以前文我編寫的AddinTreeViewCommand為例。在入口的Assembly中會搜索到MenuItemCodon,它是AbstractCodon的一個子類、包裝MenuItem(菜單項)Command(命令)的Codon。符合條件,執行
?? 首先根據類名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;
??}
?? 很明顯,BuildCodon根據構造函數中傳入的assembly和類型的ClassName,建立了具體的Codon的實例,并和具體的AddIn關聯起來。
?? 之后,codonFactory調用AddCodonBuilder方法把這個CodonBuilder加入它的Builder集合中。我們向上一層,看看codonFactory如何使用這個CodonBuilder。
?? 在文件\src\Main\Core\AddIns\Codons\CodonFactory.cs中,codonFactory只有兩個方法。AddCodonBuilder方法把CodonBuilder加入一個以CodonName為索引的Hashtable中。另外一個方法很重要:
??{
???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>標簽下的<Class>、<MenuItem>、<Pad>之類的標簽嗎?我曾經說過,這些就是Codon的描述,現在我們來看看到底是不是如此。以前文的AddinTreeView配置為例:
??<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,并把它返回給調用者。
?? 就這樣,通過CodonNameAttribute,SharpDevelop把addin配置文件的<MenuItem>節點、CodonBulder、MenuItemCodon三部分串起來形成了一個構造Codon的路線。
?? 我們回過頭來整理一下思路,SharpDevelop進行了下面這樣幾步工作:
????? A、建立各個Codon,使用CodonNameAttribute指明它在配置節點中的名稱
????? B、DefaultAddInTree的構造函數中調用LoadCodonsAndConditions方法,搜索所有的Codon,根據Codon的CodonNameAttribute建立對應的CodonBuilder加入CodonFactory中。
????? C、讀取配置文件,在<Extension>標簽下遍歷所有的節點,根據節點的Name使用CodonFactory建立對應的Codon。
?? 其中,Codon的CodonNameAttribute、CodonBuilder的CodonName以及<Extension>標簽下XML節點的Name是一致的。對于Condition(條件)的處理也是一樣。
?? 抱歉,我上網不是很方便也不太會在Blog里面貼圖(都是為了省事的借口^o^),否則也許更好理解這里的脈絡關系。
?? 好了,看到這里,我們看看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是不是比較容易理解?各位看官覺得呢?
?? 另外,雖然稍微麻煩了一小點,不過我覺得配置如果這樣寫會讓我們比較容易和代碼中具體的類關聯起來:
????????????<Codon?name=”Splash”?id?=?"MySplashForm"?class?=?"MySplashFormClass"/>?
??????</Extension>
2.2 主線 (AddInTreeSingleton. CreateAddInTree)
?? 啊~我寫的有點累了。不過還是讓我們繼續AddInTreeSingleton中CreateAddInTree的代碼。
?? 在建立了DefaultAddInTree的實例后,AddInTreeSingleton在插件目錄中搜索后綴為.addin的文件。還記得在SharpDevelop的Main函數中曾經調用過AddInTreeSingleton. SetAddInDirectories嗎,就是搜索這個傳入的目錄。看來SharpDevelop把在插件目錄中所有后綴為.addin的文件都看做是插件了。
?? 先學習一下如何從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。這是一個依賴關系的處理問題。
?? SharpDevelop在這里處理的比較簡單,調用InsertAddIns方法的時候,凡是出現CodonNotFoundException的時候,都加入一個retryList列表中返回。在CreateAddinTree處理完所有的addin文件之后,再重新循環嘗試處理retryList列表中的addin。如果某次循環中再也無法成功的加入retryList中的addin,那么才提示失敗錯誤。
?? 我們回頭來看看對AddIn的處理。
2.2.1? addIn.Initialize (AddIn的初始化)
?? 建立了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(子節點)。
?? 如果子節點是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對象,一切盡在不言中了吧。
?? e.CodonCollection.Add(codon);把構造出來的Codon對象加入到Extension對象的CodonCollection列表中。
?? 之后,在形如菜單的這種允許無限嵌套的結構中,SharpDevelop對此進行了處理。如果該節點有嵌套的子節點,那么構造一個新的Extension對象,遞歸調用AddCodonsToExtension添加到這個Extension對象中。注意一點,這個新構造的Extension對象并不是分開保存在Codon中,而是直接保存在AddIn的擴展點列表中。這樣是為了方便查找,畢竟保存在具體的Codon中也沒有什么用處,我們可以通過Extension對象的Path屬性得知它在插件樹中的具體位置。
2.2.2 addInTree.InsertAddIn(把AddIn添加到AddInTree中)
?? 對AddIn的構造完成之后,需要把AddIn的實例對象添加AddInTree中管理。
??????foreach?(AddIn.Extension?extension?in?addIn.Extensions)
??????{
?????????AddExtensions(extension);
??????}
?? 在DefaultAddInTree中,保存了兩課樹。一個是根據插件文件的結構形成的樹,每個插件文件作為根節點,往下依次是Extension、Codon節點。addIns.Add(addIn);就是把插件加入到這個樹結構中。另外一個樹是根據Extension的Path+Codon的ID作為路徑構造出來的,每一個樹節點是一個AddInTreeNode類,包含了在這個路徑上的Codon對象。嵌套在這個節點中的Codon在通過它子節點來訪問。在DefaultAddInTree中可以通過GetTreeNode來指定一個路徑獲得插件樹上某一個節點的內容。
?? AddExtensions方法很簡單,遍歷Extension中所有的Codon,把Extension的Path+Codon的ID作為路徑,創建這個路徑上的所有節點,并把Codon連接到這個AddInTreeNode上。由于Codon的ID是全局唯一的,因此每一個AddInTreeNode都具有一個唯一的Codon。
3、最后一公里(Codon和Command的關聯)
?? 在插件樹的討論中,我們依次把AddIn-Extension-Codon的配置和他們對應的類關聯了起來。不過我們一直沒有涉及到Codon和它包含的Command是如何關聯的。由于這個關聯調用是在插件樹外部的(記得在講述SharpDevelop程序入口Main函數中,提到ServiceManager的方法InitializeServicesSubsystem么?AddServices((IService[])AddInTreeSingleton.AddInTree.GetTreeNode(servicesPath).BuildChildItems(this).ToArray(typeof(IService))); 這里就調用了BuildChildItems),因此單獨在這里說明。實現這個關聯的就是AddInTreeNode的BuildChildItems和BuildChildItem方法以及Codon的BuildItem方法。
?? BuildChildItem方法和BuildChildItems方法僅有一字之差,BuildChildItem是根據指定的Codon的ID在所屬AddInTreeNode的子節點下查找包含該Codon的節點并調用該Codon的BuildItem方法;而BuildChildItems則是首先遍歷所屬AddInTreeNode的所有子節點,依次調用各個子節點的Codon的BuildItem方法,之后再調用所屬AddInTreeNode的Codon的BuildItem方法(也就是一個樹的后根遍歷)。
?? 重點在Codon的BuildItem方法。在AbstractCodon中,這個方法是一個抽象方法,SharpDevelop的代碼注釋中并沒有明確說清楚這個方法是做什么用的。但是我們可以找一個Codon的實例來看看。例如ClassCodon的BuildItem:
??????{
?????????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方法可以通過它們來查找并建立類的實例。
?? 各位看官可以再看看MenuItemCodon的實現,同樣是建立了對應的SdMenuCommand。
?? 這樣,SharpDevelop本身的插件結構可以和具體的對象建立分離開來,實際的對象建立是在各個Codon的BuildItem中進行的。因此我們可以發現在SharpDevelop整個是基礎插件系統部分沒有任何GUI的操作,實現了很好的解耦效果。
4、問題
?? 好了,本文對插件樹構造的分析到此告一段落。我提一個小小的問題給各位看官思考:在構造插件樹的過程中,如果Codon的某一個節點路徑不存在(也就是說它的依賴項不存在),那么SharpDevelop會提示失敗并且終止程序運行。可是實際上可能因為部署的原因或者權限的原因,某些Codon的失敗并不會影響整個系統的使用,例如試用版本僅僅提供部分插件給客戶使用,而并不希望系統因此而終止運行。那么就存在一個Codon依賴項失敗而允許繼續運行的問題。另外,我希望各個插件不在系統啟動的時候全部調入系統,而是在運行期實際調用的時候才調入系統,也就是一個緩存機制,這樣就可以實現系統插件的熱部署。如何修改SharpDevelop的插件系統來實現這兩個功能呢?
轉載于:https://www.cnblogs.com/sea_peak/archive/2011/04/08/2009910.html
總結
以上是生活随笔為你收集整理的【转载】SharpDevelop代码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 植物神经紊乱去医院应该挂什么科室呢?
- 下一篇: Ubuntu下设置电信拨号上网(10.0