SailingEase WinForm Framework WinForm開發框架開發手冊:http://docs.shengxunwei.com/Home/Browser/sewinformfw/
本系列文章將詳細闡述客戶端應用程序的設計理念,實現方法。
本系列文章以 SailingEase WinForm Framework 為基礎進行設計並實現,但其中的設計理念及方法,亦適用於任何類型的客戶端應用程序的設計與開發。
目錄:
http://www.cnblogs.com/sheng_chao/p/6084144.html
SailingEase WinForm Framework
其實這是從 IDE 項目中提取出來的一個純開發框架,它沒有用戶管理、權限管理之類的現成功能,而是提供純開發角度的開發框架,概括來說提供了以下幾方面的功能:
a.宿主程序(殼)與功能模塊(插件)的加載、調度、通信等實現;
b.不同插件之間在完全接耦合的基礎上,同步/異步調用、狀態響應等機制的實現;
c.插件之間在代碼層面完全沒有互相引用關系,可以實現在缺少任意插件的情況下啟動應用,即使他們在UI層有交集;
d.支持模塊間的依存關系定義;
d.事件聚合器,用於在完全解耦的條件下,發布及訂閱事件;
d.宿主程序提供了統一的主菜單及右鍵菜單的注冊/吊銷/狀態控制機制;
e.宿主程序提供了統一的窗口調度/加載/銷毀功能;
f.宿主程序提供了統一的日志記錄、異常捕獲,Web頁面互操作等功能;
g.基於 GDI+ 自行實現的控件包,提供了高度的可擴展性;
h.基於zip格式的文件包管理器(基於zip的自定義文件格式,讀取或寫入指定的流);
i.對http、xml、磁盤io、反射、加解密等操作的增強與封裝;
j.其它……
第三章節:實現菜單/工具欄按鈕的解耦及狀態控制
我們回顧一下上一章節中的客戶端程序結構圖:
可以留意到,主菜單主主菜單下面的子菜單,以及工具欄按鈕,是允許模塊在其中注冊新項的,我們將在本章節中詳細講解這一細節的實現方法。
前文提到,一些入門級的模塊化程序,通常直接將工具欄或菜單的視圖,定義並公開,加載到宿主程序中,這樣做有幾具問題:
1.性能較差,由模塊直接負責相關視圖的切換或隱藏/顯示,對於復雜視圖性能極低;
2.可靠性低,同上,模塊負責操作宿主的視圖,非常容易破壞宿主的應用程序結構,並導致宿主無法handle其中的異常;
3.無法解耦菜單/工具欄的程序功能和視圖表現,例如將傳統菜單工具欄表現形式升級為Office 2016形式,此時必須全部模塊重寫相關視圖加宿主大改;
那么如何在應用程序的宿主中,向插件提供統一的菜單,工具欄注冊,更新,銷毀機制呢?以及如何做到UI無關的徹底解耦合?
這里的主要思路是:模塊不能直接定義菜單/工具欄的視圖,而是能過其它方式,定義它們的功能和意義,由宿主程序解析並呈現。
看兩個例子:
基於 Winform 的插件式應用程序:
http://www.cnblogs.com/sheng_chao/p/4387249.html
這是一個基於 Winform 的 IDE 程序,與 Visual Stuido 非常類似,這是一個典型的例子, Visual Studio 功能繁多,菜單和工具欄的切換非常頻繁。
主菜單及工具欄根據加載的模塊,以及當前激活的窗體有所不同,菜單及工具欄按鈕的狀態則根據當前激活窗體內的數據或行為的不同而有所不同。
圖中黃色背景的工具欄部分為窗體設計器所特有,也類似於在新版的 Word 中選中圖形或表格時出現的特定菜單項目。每當在窗體設計器中進行不同的操作時,工具欄中的項目將呈現不同的狀態。
基於 WPF 的插件式應用程序:
http://www.cnblogs.com/sheng_chao/p/4548146.html
這個是一個基於 WPF 開發的普通桌面應用程序,根據當前加載的模塊不同,上方主菜單顯示的項目有所不同。這個例子比較簡單,雖然主菜單是根據插件而加載的,但是加載之后不會有狀態變化。
上面的兩個例子,雖然一個是 Winform,一個是 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 環境。
在后續的章節中,我將繼續闡述如何基於 SailingEase Winform Framework 進行模塊化的客戶端應用程序設計。
歡迎加我QQ交流探討,共同學習:279060597,另外我在南京,有南京的朋友嗎?