之前承诺会对 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 即可进行相关操作。
在此抛砖引玉,欢迎批评指正。 :)
一点小感想:许多开发工作,看起来简单,想要做好却不容易,例如本篇中所阐述的这个问题,我所经历过的一个大型软件项目,在各个插件的 Ribbon 菜单控制上,反反复复,产生了许多的问题,浪费了很多人力及时间。严重的时候一半以上的时间在和菜单较劲(CAD软件,功能繁杂,界面复杂)。但是这些问题本是可以轻易避免的,开发团队本身对问题的认识不够深刻,领导的不重视亦是很大因素,对于软件开发工作不够了解,思想停留在拖控件,画界面上,不愿对团队投入更多的人力,资金支持,导致团队成员疲于奔命,不断的延期。 却找不到症结所在,我想如果团队对于开发中的细节问题予以重视,只要找个会议室关起门来找办法,有许多的弯路是不必走的。所以遇到反复的问题,最好的办法是停下来,重新审视。
小广告
博主正在留意南京的相关高级职位
江苏电信10000号前技术经理,现任某外资企业Team Leader
使用 .NET WinForm 开发所见即所得的 IDE 开发环境,实现不写代码直接生成应用程序:
http://www.cnblogs.com/sheng_chao/p/4387249.html
使用 WPF+ ASP.NET MVC 开发 在线客服系统 (一):
http://www.cnblogs.com/sheng_chao/p/4548146.html