題外話
最近都沒怎么寫博客,主要是最近在看WPF方面的書《wpf-4-unleashed.pdf》,挑了比較重要的幾個章節學習了下WPF基礎技術。另外,也把這本書推薦給目前正在從事WPF開發的程序猿。 現在書看完了也該實踐實踐,寫了個WPF項目,主要以界面框架為主。 最近的幾篇博客也主要圍繞這個WPF項目,介紹下WPF搭建界面框架以及怎樣寫自定義的Windows界面和控件。
這也許是寫最后幾篇關於.Net技術的博客。做.Net開發也快五年了,感覺自己搞得不溫不火,另外工作中正好有一個機會轉做前段開發。穩穩當當的做年幾年.Net開發,也想給自己一個挑戰的機會。所以這幾篇博客完成后,更多的可能是涉及前段開發。
雷軍通知常常愛說:不忘初心,不忘初心、,不忘初心。重要的事說三遍。走在程序猿的路上,別忘了自己的初心,是做一個牛逼的程序猿呢還是做一個非常牛逼的程序猿呢?come on-打着雞血一般的奮斗吧!
設計思路
前段時間看了下ASP.NET請求過程的管道運行,其中包含有IHttpModule接口,通過接口的擴展,可以讓各個模塊獨立化,例如登錄認證、權限認證以及處理IHttpHandler。同理,我們可以讓WPF系統啟動過程中獨立加載各個模塊,包括系統資源模塊、主題資源模塊、登錄模塊、用戶認證模塊、權限管理模塊、以及自定義模塊等。先來看下系統僅有的兩個配置文件,分別是Application.xml和Startup.xml。Application.xml結構如下圖所示:
<?xml version="1.0" encoding="utf-8" ?> <Application xmlns="http://tempuri.org/ApplicationSchema.xsd" xmlns:mstns="http://tempuri.org/ApplicationSchema.xsd"> <Attributes> <Attr mstns:Name="stsdb" mstns:Value="Data/db.stsdb4"/> </Attributes> </Application>
上圖中,我們很容易的看出Application.xml主要存放字典資源,例如數據庫的配置。sttsdb是數據庫的字典名,而"Data/db.stsdb4"是數據庫的路徑。節點Attr表示一個字典項。
除了Application.xml文件,Startup.xml配置文件是整個系統的核心部分,因為我們系統啟動的整個流程都體現在配置內容中。先看看節點結構:
<?xml version="1.0" encoding="utf-8" ?> <Startup xmlns="http://tempuri.org/StartupSchema.xsd" xmlns:xsi="http://tempuri.org/StartupSchema.xsd"> <Startup.Modules> <ResourceModules> <Add xsi:Type="HeaviSoft.Documentor.Theme.Implements.ThemeModule, HeaviSoft.Documentor.Theme"/> </ResourceModules> <LoginModules> <Add xsi:Name="LoginModule" xsi:Type="HeaviSoft.Documentor.Presentation.Login.Implements.LoginModule,HeaviSoft.Documentor.Presentation.Login"/> </LoginModules> <AuthenticationModules> <Add xsi:Name="AuthenticationModule" xsi:Type="HeaviSoft.Documentor.Application.Security.AuthenticationModule, HeaviSoft.Documentor.Application.Security" /> </AuthenticationModules> <AuthorizationModules> </AuthorizationModules> <ExecutionModules> <Add xsi:Type="HeaviSoft.Documentor.Presentation.Docking.Implements.ExecutionModule, HeaviSoft.Documentor.Presentation.Docking"/> </ExecutionModules> </Startup.Modules> </Startup>
系統的所有資源存放在節點Startup.Modules節點下。節點說明:
節點 | 說明 |
ResourceModules | 存放所有的資源模塊,例如Theme資源以及語言資源,可添加多個。 |
LoginModules | 存放登錄模塊資源,一般我們只用添加一個Login模塊。但也支持添加多個。 |
AuthenticationModules | 驗證系統用戶的身份是否合法,可添加多個。 |
AuthorizationModules | 系統的授權管理,可添加多個。 |
ExecutionModules | 通用執行模塊,前面是個模塊執行完后,可添加自己的功能模塊。例如我們可以把主界面的顯示流程添加到這個模塊。 |
談談框架
比較常見的系統框架一般都包含兩個部分:基礎框架和業務框架。基礎框架一般不會太涉及業務,主要為業務框架提供平台,提供上下文環境。本次設計的基礎框架主要包括了上下文環境、安全策略、日志以及自定義組件等等。而業務框架我們主要就是考慮業務模塊的功能實現。在實現業務功能的同時,我們得考慮分層設計,保證業務的高擴展性以及系統的可持續性。
基礎框架
基礎框架包含了七個模塊,框架結構如下圖所示:
接下來我們就分別介紹上圖中比較重要的幾個模塊:Core、utility、Resource。
基礎框架-Core
Core包含了Core模塊和Configuration模塊,兩個模塊都有一個共同的功能,都為系統的啟動過程服務。Configuration模塊定義了系統配置文件的模板。Configuration的整個工程結構如下所示:
圖中的Schema文件夾下定義了兩個配置規則文件,分別對應Application.xml和Startup.xml。這兩個文件的具體內容在前面的“設計思路”部分已經給出。
配置已經有了,我們分析Startup.xml中的節點內容,例如:<Add xsi:Type="HeaviSoft.FrameworkBase.Client.Implements.ThemeModule, HeaviSoft.FrameworkBase.Client"/>。這一句代碼的實際意義是什么?屬性Type的值代表一個類的名稱以及所在命名空間。在系統啟動的過程中,可通過反射機制動態創建節點類型的實體對象。這些通過動態創建的類都有一個共同的特點,都需要實現某個接口來擴展一個模塊的功能。而Core工程下為我們定義了這些模塊接口,如下圖所示:
主題和語言接口包括:IResourceModule、IThemeResourceModule、ILanguageResourceModule。IResourceModule是父接口,而IThemeResourceModule和ILanguageResourceModule是IResourceModule的擴展接口。IResourceModule定義如下:
/// <summary> /// 資源加載模塊 /// </summary> public interface IResourceModule { /// <summary> /// 資源加載中 /// </summary> /// <param name="app">應用對象</param> /// <param name="name">資源名稱</param> /// <returns></returns> bool Loading(ExtendedApplicationBase app, string name); /// <summary> /// 資源加載完成 /// </summary> /// <param name="app">應用對象</param> /// <param name="name">資源名稱</param> /// <returns></returns> bool Loaded(ExtendedApplicationBase app, string name); /// <summary> /// 資源卸載中 /// </summary> /// <param name="app">應用對象</param> /// <param name="name">資源名稱</param> /// <returns></returns> bool UnLoading(ExtendedApplicationBase app, string name); /// <summary> /// 資源卸載完成 /// </summary> /// <param name="app">應用對象</param> /// <param name="name">資源名稱</param> /// <returns></returns> bool UnLoaded(ExtendedApplicationBase app, string name); }
我們可以在Loading中執行加載資源,並且接收兩個參數:app和name,app的類型為ExtendedApplicationBase,ExtendedApplicationBase就是我們的應用基類,它幾乎存儲了系統的所有上下文信息,稍后再詳解。在IResourceModule接口的方法中,我們可以獲取系統的任何上下文信息。
IThemeResourceModule是IResourceModule的一個子接口,用於加載主題資源;和IThemeResourceModule相似,ILanguageResourceModule用戶加載管理語言資源。如果我們要切換主題或資源,可先掉調用IResourceModule接口的UnLoading方法先卸載主題和語言資源。然后再調用新資源的Loading方法。
登錄接口:ILoginModule。系統的登錄操作可通過實現ILoginModule接口來執行,代碼如下:
/// <summary> /// 登錄模塊 /// </summary> public interface ILoginModule { /// <summary> /// 登錄接口 /// </summary> /// <param name="app">應用對象</param> /// <returns>返回登陸操作狀態</returns> bool Login(ExtendedApplicationBase app); /// <summary> /// 登錄成功 /// </summary> /// <param name="app">應用對象</param> /// <param name="message">成功消息</param> void LoginSuccessed(ExtendedApplicationBase app, object message); /// <summary> /// 登錄失敗 /// </summary> /// <param name="app">應用對象</param> /// <param name="message">失敗消息</param> void LoginFailed(ExtendedApplicationBase app, object message); }
登錄過程在Login方法中實現,登錄驗證后,如果登錄成功則會調用LoginSuccessed方法,如果登錄失敗,則調用LoginFailed。
身份認證:IAuthenticationModule接口。ILoginModule接口執行后,上下文信息中緩存了加密后的登錄信息,IAuthenticationModule主要對上下文中的登錄信息做驗證。驗證有沒有通過我們可以通過app中的Context.User.Identity.IsAuthenticated來判斷。
授權接口:IAuthorizationModule。IAuthenticationModule執行后,我們能夠知道用戶身份是否合法,如果身份有效,IAuthorizationModule可到服務器上獲取用戶的權限信息,並保存在上下文中。如果用戶要使用某個業務模塊,可從上下文中調用接口IPrincipal的IsInRole方法驗證授權。IPrincipal的定義如下:
public interface IPrincipal { IIdentity Identity { get; } bool IsInRole(string role); }
模塊的接口就介紹到這里,在啟動系統時,怎樣把這些接口串聯的執行。這就不得不提到系統基礎ExtendedApplicationBase。
基礎框架-ExtendedApplicationBase
首先,我們需要考慮系統啟動后,Startup.xml中模塊集合存放在哪里。ExtendedApplicationBase就是比較合適的一個容器,它定義了像ThemeResourceModules、LanguageResourceMudules等模塊集合。容器有了,那么執行的步驟怎樣串聯起來?ExtendedApplicationBase實現了一個抽象方法BuildSteps(),用於構建系統的啟動步驟。而整個系統的執行需要實現抽象方法ExecuteSteps()。還是先看看代碼再分析:
/// <summary> /// 應用對象類 /// </summary> public abstract class ExtendedApplicationBase : Application { public readonly string PROPERTY_ACCOUNT = "ExtendedApplicationBase_Account"; public readonly string PROPERTY_PASSWORD = "ExtendedApplicationBase_Password"; /// <summary> /// 當前應用實例 /// </summary> public static ExtendedApplicationBase Current { get; set; } public ExtendedApplicationBase() : base() { Data = new Dictionary<object, object>(); ThemeResourceModules = new List<IThemeResourceModule>(); LanguageResourceMudules = new List<ILanguageResourceModule>(); LoginModules = new List<ILoginModule>(); AuthenticationModules = new List<IAuthenticationModule>(); AuthorizationModules = new List<IAuthorizationModule>(); ExecutionModules = new List<IExecutionModule>(); Context = new AppContext(); } /// <summary> /// 應用上下文 /// </summary> public AppContext Context { get; private set; } public Dictionary<object, object> Data { get; private set; } protected List<IThemeResourceModule> ThemeResourceModules { get; private set; } protected List<ILanguageResourceModule> LanguageResourceMudules { get; private set; } protected List<ILoginModule> LoginModules { get; private set; } protected List<IAuthenticationModule> AuthenticationModules { get; private set; } protected List<IAuthorizationModule> AuthorizationModules { get; private set; } protected List<IExecutionModule> ExecutionModules { get; private set; } /// <summary> /// 構建步驟 /// </summary> public abstract void BuildSteps(); /// <summary> /// 執行步驟 /// </summary> /// <returns>執行狀態</returns> public virtual bool ExecuteSteps() { if (!ExecuteThemeResourceModulesCore()) return false; if (!ExecuteLanguageResourceModulesCore()) return false; if (!ExecuteLoginModulesCore()) return false; if (!ExecuteAuthorizationModulesCore()) return false; if (!ExecuteExecutionModulesCore()) return false; return true; } /// <summary> /// 關閉系統 /// </summary> public virtual void ExitEx() { Environment.Exit(0); } /// <summary> /// 執行主題資源加載 /// </summary> /// <returns></returns> protected abstract bool ExecuteThemeResourceModulesCore(); /// <summary> /// 執行語言資源加載 /// </summary> /// <returns></returns> protected abstract bool ExecuteLanguageResourceModulesCore(); /// <summary> /// 執行登錄流程 /// </summary> /// <returns></returns> protected abstract bool ExecuteLoginModulesCore(); /// <summary> /// 執行身份驗證 /// </summary> /// <returns></returns> public abstract bool ExecuteAutheticationModulesCore(); /// <summary> /// 執行身份授權 /// </summary> /// <returns></returns> protected abstract bool ExecuteAuthorizationModulesCore(); /// <summary> /// 執行常規流程 /// </summary> /// <returns></returns> protected abstract bool ExecuteExecutionModulesCore(); /// <summary> /// 激活當前應用 /// </summary> public void Activate() { this.MainWindow.Show(); this.MainWindow.Activate(); }
代碼中,ExecuteSteps方法分別調用了ExecuteThemeResourceModulesCore、ExecuteLanguageResourceModulesCore、ExecuteLoginModulesCore、ExecuteAuthorizationModulesCore、ExecuteExecutionModulesCore。這幾個方法都是抽象方法。具體的實現都是在子類中。我們還看到類中包括一個Data屬性,類型為Dictionary。之前我們提到了Application.xml,這里邊配置的所有Attr字典配置數據都會存儲在Data中,取值直接通過ExtendedApplicationBase.Current.Data[Key]讀取。
基礎框架-ExtendedApplicationBase實現類
在系統中添加一個ExtendedApplicationBase的具體實現類ExtendedApplication。代碼如下:
/// <summary> /// 系統應用 /// </summary> internal class ExtendedApplication : ExtendedApplicationBase { public ExtendedApplication() : base() { ShutdownMode = ShutdownMode.OnExplicitShutdown; } #region 系統事件 /// <summary> /// 流程啟動 /// </summary> /// <param name="e"></param> protected override void OnStartup(StartupEventArgs e) { //注冊事件 RegistEvent(); //開始構建步驟 try { this.BuildSteps(); //執行步驟 if (!this.ExecuteSteps()) { this.ExitEx(); } } catch (Exception ex) { //寫日志 Logger.Error("Error occured during appication start-up.", ex); this.ExitEx(); } } private void RegistEvent() { //捕獲UI線程 this.DispatcherUnhandledException += ExtendedApplication_DispatcherUnhandledException; //捕獲其他線程異常 TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; } private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { //寫日志 Logger.Error("Unknown error.", e.Exception); //處理異常 } /// <summary> /// 未捕獲的異常 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void ExtendedApplication_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) { HandleUnKnownExcpetion(e.Exception); } private void HandleUnKnownExcpetion(Exception e) { //寫日志 Logger.Error("Unknown error.", e); //啟動過程中發生了異常。 if (e is StartupException) { //啟動異常提示 } else if (e is FatalException) { //中斷異常提示 } else { //其他異常 } } #endregion #region 父類抽象方法實現 public override void BuildSteps() { #region Application Attributes var applicationRoot = ConfigurationHelper.GetApplicationConfigRoot(); if (!applicationRoot.IsNull()) { var eleLayouts = new string[] { ConfigurationHelper.Config_Node_Attriutes, ConfigurationHelper.Config_Node_Attr }; var attributes = applicationRoot.GetElements(eleLayouts); foreach (var element in attributes) { var name = element.GetAttributeValue("Name"); var value = element.GetAttributeValue("Value"); if (!this.Data.ContainsKey(name)) { this.Data.Add(name, value); } } } #endregion #region Startup var startupRoot = ConfigurationHelper.GetStartupConfigRoot(); if (!startupRoot.IsNull()) { //加載ResourceModules var startups = startupRoot.GetElements(new string[] { ConfigurationHelper.Config_Node_Modules, ConfigurationHelper.Config_Node_ResourceModules, ConfigurationHelper.Config_Node_Operation }); var resourcesTypes = startups.ToList().Select(el => el.GetAttributeValue("Type")); var resourceModules = new List<IResourceModule>(); foreach (var type in resourcesTypes) { resourceModules.Add(CreateInstanceByType<IResourceModule>(type)); } this.ThemeResourceModules.AddRange(resourceModules.Where(res => res is IThemeResourceModule).Cast<IThemeResourceModule>()); this.LanguageResourceMudules.AddRange(resourceModules.Where(res => res is ILanguageResourceModule).Cast<ILanguageResourceModule>()); //加載LoginModules var loginTypes = startupRoot.GetElements(new string[] { ConfigurationHelper.Config_Node_Modules, ConfigurationHelper.Config_Node_LoginModules, ConfigurationHelper.Config_Node_Operation }).ToList().Select(el => el.GetAttributeValue("Type")); foreach (var type in loginTypes) { this.LoginModules.Add(CreateInstanceByType<ILoginModule>(type)); } //加載AuthenticationModules var authenticationTypes = startupRoot.GetElements(new string[] { ConfigurationHelper.Config_Node_Modules, ConfigurationHelper.Config_Node_AuthenticationModules, ConfigurationHelper.Config_Node_Operation }).ToList().Select(el => el.GetAttributeValue("Type")); foreach (var type in authenticationTypes) { this.AuthenticationModules.Add(CreateInstanceByType<IAuthenticationModule>(type)); } //加載AutorizationModules var authorizationTypes = startupRoot.GetElements(new string[] { ConfigurationHelper.Config_Node_Modules, ConfigurationHelper.Config_Node_AuthorizationModules, ConfigurationHelper.Config_Node_Operation }).ToList().Select(el => el.GetAttributeValue("Type")); foreach (var type in authorizationTypes) { this.AuthorizationModules.Add(CreateInstanceByType<IAuthorizationModule>(type)); } //加載執行流程 var executionTypes = startupRoot.GetElements(new string[] { ConfigurationHelper.Config_Node_Modules, ConfigurationHelper.Config_Node_ExecutionModules, ConfigurationHelper.Config_Node_Operation }).ToList().Select(el => el.GetAttributeValue("Type")); foreach (var type in executionTypes) { this.ExecutionModules.Add(CreateInstanceByType<IExecutionModule>(type)); } } #endregion } protected override bool ExecuteThemeResourceModulesCore() { //加載主題資源 foreach (var module in ThemeResourceModules) { if (!module.Loading(this, Context.CurrentTheme)) { throw new StartupException("Error occured when loading theme resource."); } } //主題資源加載完成 foreach (var module in ThemeResourceModules) { if (!module.Loaded(this, Context.CurrentTheme)) { throw new StartupException("Error occured when loaded theme resource."); } } return true; } protected override bool ExecuteLanguageResourceModulesCore() { //加載語言資源 foreach (var module in LanguageResourceMudules) { if (!module.Loading(this, Context.CurrentLanguage)) { throw new StartupException("Error occured when loading language resource."); } } //語言資源加載完成 foreach (var module in LanguageResourceMudules) { if (!module.Loaded(this, Context.CurrentLanguage)) { throw new StartupException("Error occured when loaded language resource."); } } return true; } protected override bool ExecuteLoginModulesCore() { foreach (var login in LoginModules) { try { if (!login.Login(this)) { return false; } } catch (Exception ex) { throw ex; } } return true; } public override bool ExecuteAutheticationModulesCore() { var result = true; var message = "Authenticating user is successed."; //執行認證 foreach (var auth in AuthenticationModules) { if (!auth.Authenticate(this)) { result = false; message = "Invalid user, please check inputed user info!"; break; } } //用戶認證失敗,判斷是否需要重新啟動登錄 if (!result) { //是否需要重新登陸 foreach (var login in LoginModules) { login.LoginFailed(this, message); } } //用戶認證成功 else { foreach (var login in LoginModules) { login.LoginSuccessed(this, message); } } return result; } protected override bool ExecuteAuthorizationModulesCore() { //授權之前檢查用戶是否驗證通過 if (!Context.User.Identity.IsAuthenticated) { Logger.Info("User must be autenticated before executing authorizationModule."); return false; } //用戶授權操作 foreach (var autor in AuthorizationModules) { if (!autor.Authorize(this)) { Logger.Info("User authorizate failed."); return false; } } return true; } protected override bool ExecuteExecutionModulesCore() { //加載數據s foreach (var excute in ExecutionModules) { excute.Execute(this); } return true; } #endregion #region 私有方法 private T CreateInstanceByType<T>(string typeInfo) { try { var array = typeInfo.Split(','); object instance = null; try { instance = Assembly.LoadFrom(string.Format("{0}.dll", array[1].Trim())).CreateInstance(array[0].Trim()); } catch (FileNotFoundException) { instance = Assembly.LoadFrom(string.Format("{0}.exe", array[1].Trim())).CreateInstance(array[0].Trim()); } return (T)instance; } catch (Exception ex) { throw new StartupException(string.Format("Error occured when executing CreateInstanceByType method, parameter:{0}", typeInfo), ex); } } #endregion }
首先說說重寫的BuildSteps方法,先從Application.xml中讀取配置字典,然后從Startup.xml讀取模塊配置。按順序先后讀取ResourceModules、LoginModules、AuthenticationModules、AutorizationModules、ExecutionModules。並且在每讀取一個模塊,調用CreateInstanceByType<IXXXModule>(moduleTypeName)實例化具體類型的對象。每創建一個實體對象,都會存放在模塊集合中,例如創建的ThemeResourceModule對象就會存放在ExtendedApplicationBase的ThemeResourceModules集合中。在ExtendedApplicationBase類中我們也看到幾個模塊集合對象,如下圖所示:
public Dictionary<object, object> Data { get; private set; } protected List<IThemeResourceModule> ThemeResourceModules { get; private set; } protected List<ILanguageResourceModule> LanguageResourceMudules { get; private set; } protected List<ILoginModule> LoginModules { get; private set; } protected List<IAuthenticationModule> AuthenticationModules { get; private set; } protected List<IAuthorizationModule> AuthorizationModules { get; private set; } protected List<IExecutionModule> ExecutionModules { get; private set; }
所有的模塊對象都准備就緒,現在要考慮整個系統的執行步驟怎樣實現。我們知道系統的啟動都是以Application的OnStartup方法作為入口,那看看OnStartup代碼:
/// <summary> /// 流程啟動 /// </summary> /// <param name="e"></param> protected override void OnStartup(StartupEventArgs e) { //注冊事件 RegistEvent(); //開始構建步驟 try { this.BuildSteps(); //執行步驟 if (!this.ExecuteSteps()) { this.ExitEx(); } } catch (Exception ex) { //寫日志 Logger.Error("Error occured during appication start-up.", ex); this.ExitEx(); } }
代碼先執行RegistEvent方法注冊異常捕獲事件,一般都是注冊事件到DispatcherUnhandledException捕獲UI線程的未知異常,但別忘了捕獲工作線程,工作線程的異常使用TaskScheduler.UnobservedTaskException捕獲。事件注冊之后執行BuildSteps方法,前面剛剛說過了。步驟構建完了,就該調用ExecuteSteps方法執行具體模塊。 ExecuteSteps的實現是在ExtendedApplicationBase中,代碼如下:
/// <summary> /// 執行步驟 /// </summary> /// <returns>執行狀態</returns> public virtual bool ExecuteSteps() { if (!ExecuteThemeResourceModulesCore()) return false; if (!ExecuteLanguageResourceModulesCore()) return false; if (!ExecuteLoginModulesCore()) return false; if (!ExecuteAuthorizationModulesCore()) return false; if (!ExecuteExecutionModulesCore()) return false; return true; }
這里列舉一個ExecuteThemeResourceModulesCore的實現做簡要說明,其他的模塊執行流程相似。ExecuteThemeResourceModulesCore實現如下:
protected override bool ExecuteThemeResourceModulesCore() { //加載主題資源 foreach (var module in ThemeResourceModules) { if (!module.Loading(this, Context.CurrentTheme)) { throw new StartupException("Error occured when loading theme resource."); } } //主題資源加載完成 foreach (var module in ThemeResourceModules) { if (!module.Loaded(this, Context.CurrentTheme)) { throw new StartupException("Error occured when loaded theme resource."); } } return true; }
前面已經給出了IResouceModule接口的定義。ExecuteThemeResourceModulesCore先遍歷ThemeResouceModules的對象,調用Loading方法。緊接着再次遍歷ThemeResourceModules集合,調用對象的Loaded方法。其他的幾個模塊的執行原理相似。
本篇總結
本篇介紹的內容比較簡單,首先介紹了系統的兩個配置文件Application.xml和Startup.xml,根據這兩個配置文件提出系統的設計思路。其次,給出了Core中的7個接口代碼,每個接口代表了一個獨立的模塊,像資源模塊、認證模塊、授權模塊等。然后,分析自定義的Application類:ExtendedApplicationBase,它包括了系統流程的構建方法BuildSteps、系統流程的執行方法ExecuteSteps。最后給出ExtendedApplicationBase的實現類ExtendedApplication,並且給出代碼,根據代碼分析了步驟如何構建,步驟如何執行,並且列舉了資源模塊的執行過程ExecuteThemeResourceModulesCore方法。
搭建一個WPF界面框架,肯定少不了Custom Window和Custom Control。因為開發一個給客戶使用的系統,肯定會根據客戶設計不同的UI。那么,如何開發這些具有獨特風格的UI以及自定義控件,並且包含可替換的主題。下一篇再做詳解。
如果本篇內容對大家有幫助,請點擊頁面右下角的關注。如果覺得不好,也歡迎拍磚。你們的評價就是博主的動力!下篇內容,敬請期待!
源代碼
完整的代碼存放在GitHub上,代碼路徑:https://github.com/heavis/Documentor_V01R01/。