博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
准备.Net转前端开发-WPF界面框架那些事,搭建基础框架
阅读量:7041 次
发布时间:2019-06-28

本文共 19760 字,大约阅读时间需要 65 分钟。

题外话

    最近都没怎么写博客,主要是最近在看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结构如下图所示:

 

    上图中,我们很容易的看出Application.xml主要存放字典资源,例如数据库的配置。sttsdb是数据库的字典名,而"Data/db.stsdb4"是数据库的路径。节点Attr表示一个字典项。

    除了Application.xml文件,Startup.xml配置文件是整个系统的核心部分,因为我们系统启动的整个流程都体现在配置内容中。先看看节点结构:

    系统的所有资源存放在节点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定义如下:

///     /// 资源加载模块    ///     public interface IResourceModule    {        ///         /// 资源加载中        ///         /// 应用对象        /// 资源名称        /// 
bool Loading(ExtendedApplicationBase app, string name); /// /// 资源加载完成 /// /// 应用对象 /// 资源名称 ///
bool Loaded(ExtendedApplicationBase app, string name); /// /// 资源卸载中 /// /// 应用对象 /// 资源名称 ///
bool UnLoading(ExtendedApplicationBase app, string name); /// /// 资源卸载完成 /// /// 应用对象 /// 资源名称 ///
bool UnLoaded(ExtendedApplicationBase app, string name); }

    我们可以在Loading中执行加载资源,并且接收两个参数:app和name,app的类型为ExtendedApplicationBase,ExtendedApplicationBase就是我们的应用基类,它几乎存储了系统的所有上下文信息,稍后再详解。在IResourceModule接口的方法中,我们可以获取系统的任何上下文信息。

    IThemeResourceModule是IResourceModule的一个子接口,用于加载主题资源;和IThemeResourceModule相似,ILanguageResourceModule用户加载管理语言资源。如果我们要切换主题或资源,可先掉调用IResourceModule接口的UnLoading方法先卸载主题和语言资源。然后再调用新资源的Loading方法。

    登录接口:ILoginModule。系统的登录操作可通过实现ILoginModule接口来执行,代码如下:

///     /// 登录模块    ///     public interface ILoginModule    {        ///         /// 登录接口        ///         /// 应用对象        /// 
返回登陆操作状态
bool Login(ExtendedApplicationBase app); /// /// 登录成功 /// /// 应用对象 /// 成功消息 void LoginSuccessed(ExtendedApplicationBase app, object message); /// /// 登录失败 /// /// 应用对象 /// 失败消息 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()。还是先看看代码再分析:

///     /// 应用对象类    ///     public abstract class ExtendedApplicationBase : Application    {        public readonly string PROPERTY_ACCOUNT = "ExtendedApplicationBase_Account";        public readonly string PROPERTY_PASSWORD = "ExtendedApplicationBase_Password";        ///         /// 当前应用实例        ///         public static ExtendedApplicationBase Current { get; set; }        public ExtendedApplicationBase() : base()        {            Data = new Dictionary
(); ThemeResourceModules = new List
(); LanguageResourceMudules = new List
(); LoginModules = new List
(); AuthenticationModules = new List
(); AuthorizationModules = new List
(); ExecutionModules = new List
(); Context = new AppContext(); } ///
/// 应用上下文 /// public AppContext Context { get; private set; } public Dictionary
Data { get; private set; } protected List
ThemeResourceModules { get; private set; } protected List
LanguageResourceMudules { get; private set; } protected List
LoginModules { get; private set; } protected List
AuthenticationModules { get; private set; } protected List
AuthorizationModules { get; private set; } protected List
ExecutionModules { get; private set; } ///
/// 构建步骤 /// public abstract void BuildSteps(); ///
/// 执行步骤 /// ///
执行状态
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; } ///
/// 关闭系统 /// public virtual void ExitEx() { Environment.Exit(0); } ///
/// 执行主题资源加载 /// ///
protected abstract bool ExecuteThemeResourceModulesCore(); ///
/// 执行语言资源加载 /// ///
protected abstract bool ExecuteLanguageResourceModulesCore(); ///
/// 执行登录流程 /// ///
protected abstract bool ExecuteLoginModulesCore(); ///
/// 执行身份验证 /// ///
public abstract bool ExecuteAutheticationModulesCore(); ///
/// 执行身份授权 /// ///
protected abstract bool ExecuteAuthorizationModulesCore(); ///
/// 执行常规流程 /// ///
protected abstract bool ExecuteExecutionModulesCore(); ///
/// 激活当前应用 /// 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。代码如下:

///     /// 系统应用    ///     internal class ExtendedApplication : ExtendedApplicationBase    {        public ExtendedApplication() : base()        {            ShutdownMode = ShutdownMode.OnExplicitShutdown;        }        #region 系统事件        ///         /// 流程启动        ///         ///         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);            //处理异常        }        ///         /// 未捕获的异常        ///         ///         ///         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
(); foreach (var type in resourcesTypes) { resourceModules.Add(CreateInstanceByType
(type)); } this.ThemeResourceModules.AddRange(resourceModules.Where(res => res is IThemeResourceModule).Cast
()); this.LanguageResourceMudules.AddRange(resourceModules.Where(res => res is ILanguageResourceModule).Cast
()); //加载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
(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
(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
(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
(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
(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
Data { get; private set; } protected List
ThemeResourceModules { get; private set; } protected List
LanguageResourceMudules { get; private set; } protected List
LoginModules { get; private set; } protected List
AuthenticationModules { get; private set; } protected List
AuthorizationModules { get; private set; } protected List
ExecutionModules { get; private set; }

    所有的模块对象都准备就绪,现在要考虑整个系统的执行步骤怎样实现。我们知道系统的启动都是以Application的OnStartup方法作为入口,那看看OnStartup代码:

///         /// 流程启动        ///         ///         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中,代码如下:

///         /// 执行步骤        ///         /// 
执行状态
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上,代码路径:。

转载地址:http://pdhal.baihongyu.com/

你可能感兴趣的文章
Ansible 复制文件
查看>>
用python实现接口测试(七、查询快递单号)
查看>>
程序员死磕电梯算法的那些趣事?
查看>>
Docker 的Dockerfile指令
查看>>
webpack教程(二)
查看>>
spring boot + maven使用profiles进行环境隔离
查看>>
学习Hadoop大数据基础框架
查看>>
Ansible-playbook roles安装mysql实例(学习笔记二十七)
查看>>
HT for Web列表和3D拓扑组件的拖拽应用
查看>>
javascript replaceAll方法
查看>>
Hacker News 12 月招聘趋势:React 已霸榜 19 个月
查看>>
SpringBoot配置devtools实现热部署
查看>>
实验吧 ---- 隐写术之so beautiful so white
查看>>
sklearn调包侠之线性回归
查看>>
《Linux命令行与shell脚本编程大全》 第四章
查看>>
Flask开发微电影网站(四)
查看>>
使用Maven搭建Springboot版ssm框架
查看>>
中介者模式
查看>>
怎么就死循环了!
查看>>
Jmeter之tomcat性能测试+性能改进措施
查看>>