SailingEase WinForm Framework WinForm開發框架開發手冊:http://docs.shengxunwei.com/Home/Browser/sewinformfw/
之前承諾會對 Winform IDE,WPF 客服程序的開發進行進一步的分解記錄,很抱歉一直沒有太多時間認真梳理。
本篇博客抽取了這兩個應用程序的一個共通功能的實現方法進行說明,即在插件式應用程序中,對菜單及工具欄的控制。
對於復雜的應用程序開發,我們可能會將程序的功能進行分解,模塊化,插件化;那么如何在應用程序的宿主中,向插件提供統一的菜單,工具欄注冊,更新,銷毀機制呢?以及如何做到UI無關的徹底解耦合?
看兩個例子:
基於 Winform 的插件式應用程序: http://www.cnblogs.com/sheng_chao/p/4387249.html
這是一個基於 Winform 的 IDE 程序,主菜單及工具欄根據加載的模塊,以及當前激活的窗體有所不同,菜單及工具欄按鈕的狀態則根據當前激活窗體內的數據或行為的不同而有所不同。
圖中黃色背景的工具欄部分為窗體設計器所特有,類似於在新版的 Word 中選中圖形或表格時出現的特定菜單項目。每當在窗體設計器中進行不同的操作時,工具欄中的項目將呈現不同的狀態。

基於 WPF 的插件式應用程序: http://www.cnblogs.com/sheng_chao/p/4548146.html
這個是一個基於 WPF 開發的普通桌面應用程序,根據當前加載的模塊不同,上方主菜單顯示的項目有所不同。這個例子比較簡單,雖然主菜單是根據插件而加載的,但是加載之后不會有狀態變化。

一般來說,宿主程序在加載插件時,會根據某種預先配置的插件信息(如配置文件),讀取與插件相關的信息進行加載。
過去的許多應用程序,通過將菜單及工具欄的配置通過配置文件來向宿主進行聲明,這種方式的優點是實現簡單,開發容易,幾乎沒有難度,缺點是幾乎只能以靜態方式對菜單及工具欄進行配置,如果需要在程序運行時動態更新、吊銷菜單或工具欄,按此思路實現起來已不是最優選擇。
第二種方式也是我經常看到的,就是開發人員直接把菜單或工具欄從UI層拋給插件去實現,宿主只提供一個基本UI容器去承載插件所提供的UI對象,比如整個 UserControl。這種方式如果一定要說有什么優點,那就是開發實現比較簡單,缺點則比第一種方式更多,首先宿主程序失去了對插件的絕對控制,插件程序可以通過提供自己形態各異的UI,使主程序的相關功能呈現,控制,不再統一,其次使主程序變得非常脆弱,宿主程序無法有效的,完全的 Handle 來自這些UI的異常,也無法監控,控制這些UI中的方法調用,例如對超時的方法調用顯示等待UI,或強行中止,無法調度這些方法調用。當宿主程序因升級而修改了菜單和工具欄的呈現形態時,或需要支持換膚功能時,插件提供的UI完全不受控。此外這種方式可能帶來大量的重復勞動,浪費開發人員生產性,因為大多數的菜單,工具欄項目的呈現,都是相似的,有一定規律的,可以通過自動化的方式來處理。
第三種方式的思路是由宿主程序提供接口,供插件進行調用,從而使插件能夠對菜單及工具欄進行動態控制,這樣做的好處一是不存在上述方法二中的問題,二是解決了方法一中,靜態加載所不能實現的動態控制。
實現的方式有許多,過去我們見到過提供一系列方法來供插件調用的情況,這樣做有一個顯著缺點,就是復雜,會使代碼復雜化,邏輯復雜化。需要提供一系列的注冊,更新,吊銷方法,以及許多不同的參數重載以實現相應的功能。當開發中存在新需求時,如對菜單及工具欄項綁定權限 Key,就需要一系列的接口修改或參數修改。
我在上面兩個例子中,將菜單和工具欄資源化,通過一種 類似URI,統一資源標識符的方式 來控制,最大程度的將插件開發的工作量降到最低,最容易,使實習生水平的開發人員,通過10分鍾的講解,就可以從容掌握。
實現效果:
private void InitializeNavigation() { _navigationService.Register("MainMenu://Session[Text='會話']/Session/"); _navigationService.Register("MainMenu://Session/Session/Contact[Text='聯系人']", new Action(() => { ContactView.ShowInstance(); })); _navigationService.Register("MainMenu://Setup[Text='設置']/Contact/"); _navigationService.Register("MainMenu://Setup/Contact/CustomerCategory[Text='業務類型',AuthorizeKey='ManageCustomerCategory']", new Action(() => { CustomerCategoryListView.ShowInstance(); })); _navigationService.Register("MainMenu://Setup/Contact/CustomerImportentLevel[Text='重要級別',AuthorizeKey='ManageCustomerImportentLevel']", new Action(() => { CustomerImportentLevelListView.ShowInstance(); })); }
相信稍具經驗的開發人員,無需解釋亦能明白這段代碼的含義。
插件在得到宿主提供的 INavigationService (_navigationService)接口后,只需調用 Register 方法,傳入 URI 及相關參數,即可實現對菜單或工具欄項目的動態注冊。
INavigationService 接口的定義非常簡單:
public interface INavigationService { void Register(string path);
void Register(string path, Action action); void Register(NavigationCodon codon); void Update(string path); }
從字面意思即可完全理解,避免了傳統的大段方法來提供相關的功能,核心就在於參數 path ,統一資源標識符。

協議部分根據宿所能提供的功能實現既可,如:
MainMenu:主菜單;Toolbar:工具欄:QuickStart:快速啟動工具等等
以 MainMenu 為例:
路徑路分即指明當前目標菜單的“層級”,在這個例子中,路徑的第一部分 Setup,在上文 Winform 應用的例子中,實現為頂層菜單,而對於第二個 WPF 例子,采用了 Ribbon 式的菜單,則實現為 Tab 頁;路徑第二部分的 Contact 實現為二級菜單,或忽略,在 Ribbon 式菜單中,實現為 Tab 頁下的 Group;第三部分 CustomerCategory 則指明了具體的菜單項目“業務類型”。
路徑的第三部分 CustomerCategory 僅指定了該菜單項的 Name,其它屬性均通過以中括號括起的屬性語法來指定,即:Text='業務類型',AuthorizeKey='ManageCustomerCategory'。
在具體實現中,屬性語法中的可用屬性,經過特別處理,允許框架無關,UI無關,允許動態擴展。對於屬性語法中的可用屬性進行擴展,非常容易。與 INavigationService 本身的實現,是完全解耦的,無關的。
意味着隨着應用程序開發的深入,需求的變化,出現新功能需要對應時,只需在特定位置指明新的屬性名及實現其功能即可,與框架,與INavigationService 皆無關。
所有的新屬性對應,甚至是原有屬性的去除,都可以不影響現有任何代碼,新屬性實現不影響原有代碼,而原有代碼中屬性的屬性如果需要取消,取消相關對應即可,INavigationService 在解析時找不到對應的實現,可在記錄日志后直接忽略,例如1.0版本的宿主支持指定菜單的顏色,到了2.0不支持了,原有在1.0下工作的代碼,完全不會受影響,僅僅是該指定到了2.0變為無效,從而實現良好的向下兼容性。
INavigationService 還提供了 Update 方法用於更新菜單或工具欄項目的狀態,同時,直接在 path 中使用屬性語法即可,如:
_navigationService.Update("MainMenu://Setup/Contact/CustomerCategory[Enable='False']",
此外,INavigationService 接口支持一個更復雜的參數對象 NavigationCodon
public class NavigationCodon { public NavigationPath Path { get; private set; }
public Action Action { get; set; } public Func<bool> IsEnableFunc { get; set; } public Func<Visibility> VisibilityFunc { get; set; } public NavigationCodon(string path) { this.Path = new NavigationPath(path); } }
可實現在更為復雜的場景下對菜單及工具欄項目的精細控制,如上文中的 Winform IDE 環境。
通過將菜單及工具欄項目資源化,不但實現了宿主與插件之間的完全解耦合,也為插件自身提供了菜單工具欄解耦合的方法,插件在實現自己的業務時,亦無需得到對菜單及工具欄項目的強引用,通過 INavigationService 即可進行相關操作。
在此拋磚引玉,歡迎批評指正。 :)
