基于 SailingEase WinForm Framework 开发客户端程序(3:实现菜单/工具栏按钮的解耦及状态控制)
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,另外我在南京,有南京的朋友吗?