上一篇介绍了各子系统如何整合这个话题,如果您阅读到这篇文章,就表示我们已经进入到业务阶段代码的一些探讨了,JHRS框架是基于WPF技术采用MVVM搭建的一个框架,当然开源出来的并非完美的功能,如果您拿来用的话,肯定会有很多地方会调整的,主要的目的就是分享怎样解决实际项目中遇到的问题。
合理设计ViewModel基类
在WPF中应用MVVM来做项目,我不知道有多少团队是严格遵循的,如果你不了解MVVM,google上可以搜索到很多介绍这些内容的博客。
如果不使用MVVM来撸WPF的项目的话,最简单粗暴的做法就是在后置代码写各种处理事件就行了,例如按钮的点击事件,DataGrid的绑定数据等等。一旦遵循MVVM了,就不能直接在ViewModel里面做到一定程度的随心所欲了,例如你不能在ViewModel里面控制某个按钮的隐藏显示与否,背景颜色以及和界面的一些行为;因此合理的设计ViewModel的基类就显得尤为重要了。
JHRS框架ViewModel的基类
任何一个项目,都大致可以按照功能点归结为管理列表页面,详情页面,新增编辑页面,悬浮提示页面(效果),弹出页面,消息提示等;而处理这些功能点的ViewModel的基类具有共通性的方法就可以提炼到基类里面来,这样就可以减少很多重复代码;怎样提炼这依赖于团队中熟悉业务和技术过硬的小伙伴来做了,通常这可能是团队中架构师的工作或者高级开发人员的职责。
例如在上图中就是JHRS框架中根据功能而定义的一个公共基类(BaseViewModel)和两个和业务相关的基类,分别是用于管理列表页面的基类(BaseManagePageViewModel)和新增编辑页面的基类(BaseDialogPageViewModel),因为是使用模态窗口进行新增和编辑操作,故而命名为BaseDialogPageViewModel。
BaseViewModel基类
首先这个BaseViewModel类继承自Prism提供的BindableBase类,做为所有ViewModel的基类,其它不同功能的基类再继承BaseViewModel,用于扩展相同业务的共通功能;从上图中可以看到,框架里面定义的管理列表页面的基类和新增编辑页面的基类都继承自BaseViewModel类。
这个基类主要是定义了在整个系统里面公用方法和属性,当前的代码中,主要是包含了打开,关闭模态窗口,并实现上下文参数传递,事件聚合器,日志属性,查询规则的动态拼接。
例如在基类中提供的GetQueryRules方法,就是用于拼接查询规则用的,它的功能是将任 一 管理列表页面上面拥有多个查询条时,当用户输入查询数据自动获取出页面上输入数据,按一定规则拼成json数据传入到后台web api执行查询从而获得数据的;当然对应的web api会自动解析成Linq表达式(Expression),类似于在服务器端调用这个代码传入的参数【expression】一样。
IQueryable<T> Where(Expression<Func<T, bool>> expression);
简单的来说可以理解为在拼SQL 中的 where条件一样。
因为查询功能是所有系统不可或缺的,因此这些方法就直接放到基类,当然它是一个虚方法,子类可以重写;但大多数情况下,一般是不会这么做的。
这个类也可以供一些简单的功能页面(Page)的ViewModel继承,例如仅仅是一个详情展示的页面就可以继承它。
BaseManagePageViewModel基类
从命名上可以看出,这个类是框架中提供的给可以分页查询带管理功的页面使用的,主要提供了3个公共属性(PagingData、PageData、SelectedList)和页面加载事件方法(子类需要实现)、新增方法、绑定数据方法,以及更改业务状态之前的方法和回调方法。
还是用框架中演示界面来说明问题,一个典型的管理列表页面如下图所示一样(不考虑界面如何展示和摆放位置,只说明核心问题即可),有一个查询按钮,右侧有各种按钮,常见如新增、导出、批量打印等按钮,在表格底部有一个分页控件(图中没有显示出来,可能程序bug,未修复),点击页面会跳转到相应的页加载数据,这儿就有一个事件,因此可以将这些封装到管理功能的基类里面去,子类viewmodel实现相关业务即可。
这只是在演示框架中这样设计的,实际项目中您可以根据自己需求和产品设计出来的界面进行抽象扩展。
PagingData:分页相关的封装,包含页码(PageIndex)、每页显示数据条数(PageSize)、总条数(Total)、总页数(MaxPageCount),调用分页接口(web api)后赋值即可。
PageData:DataGrid的数据源,这个对象是IEnumerable<object>类型的,一般调用web api拿到数据后给它赋值。
SelectedList:DataGrid有复选框功能时,所选中的数据集合。
子类在继承这个类时,如果功能不复杂,即没有多余的按钮事件要处理,一般只需要实现4个方法就可以了,如下AccountListViewModel.cs代码所示:
/// <summary> /// 賬號管理業務 /// </summary> public class AccountListViewModel : BaseManagePageViewModel { /// <summary> /// 賬號管理業務構造函數 /// </summary> /// <param name="container"></param> public AccountListViewModel(IContainerExtension container) : base(container) { PagingData.PageSize = 20; } public override DelegateCommand AddCommand => new DelegateCommand(()=> { }); public override void PageLoaded(Page page) { } protected override Task<object> BindPagingData() { throw new NotImplementedException(); } protected override Task<object> UpdateDataStatus<TEntity>(TEntity entity) { throw new NotImplementedException(); } } }
BaseDialogPageViewModel基类
这个基类是专门用于新增和编辑页面,子类继承它并实现保存方法就可以了,一般来说,一个页面,即做新增也做修改的话,那么对于新增来说倒是方便,但对于编辑功能来说,需要在进入到编辑模式时,需要对页面上的相关表单控件进行初始化并赋值,例如下拉框(Comobox)需要绑定数据源并选中原来的值,文本框则要赋值原来的输入值,好在WPF是双向绑定,因此可以省很多事情。
一个典型的新增编辑子类是这样实现的。
/// <summary> /// 新增預約掛號,編輯預約掛號功能頁面viewmodel,用於處理業務邏輯 /// 調用web api保存數據 /// </summary> public class AddOrEditReservationViewModel : BaseDialogPageViewModel { /// <summary> /// 新增預約掛號業務處理構造函數 /// </summary> /// <param name="container"></param> public AddOrEditReservationViewModel(IContainerExtension container) : base(container) { InintData(); } /// <summary> /// 編輯模式下初始化界面數據 /// </summary> private void InintData() { ReservationOutputDto current = this.GetContext<ReservationOutputDto>(); //不爲null表示處於編輯模式 if (current != null) { Dto = new ReservationInputDto { DepartmentID = current.DepartmentID, DepartmentName = current.DepartmentName, DoctorName = current.DoctorName, Gender = current.Gender, Index = current.Index, BusinessNumber = current.BusinessNumber, Name = current.Name, ReservationTime = current.ReservationTime }; } } private ReservationInputDto _dto = new ReservationInputDto(); /// <summary> /// 界面上輸入的信息 /// </summary> public ReservationInputDto Dto { get { return _dto; } set { SetProperty(ref _dto, value); } } /// <summary> /// 新增,編輯保存方法,從viewmodel獲取數據保存即可. /// </summary> /// <returns></returns> [WaitComplete] protected async override Task SaveCommand() { var current = this.GetContext<ReservationOutputDto>(); if (current == null) { if (!IsDevelopment) { var response = await RestService.For<IReservationApi>(AuthClient).Add(Dto); AlertPopup(response.Message, response.Succeeded ? MessageType.Success : MessageType.Error, (d) => { if (response.Succeeded) this.CloseDialog(returnValue:"已經添加成功啦,這裏可以是任何參數和對象喲,父窗體可以接收到此回傳參數。"); }); } } else { if (!IsDevelopment) { var response = await RestService.For<IReservationApi>(AuthClient).Update(Dto); AlertPopup(response.Message, response.Succeeded ? MessageType.Success : MessageType.Error, (d) => { if (response.Succeeded) this.CloseDialog(returnValue: "已經修改成功啦,這裏可以是任何參數和對象喲,父窗體可以接收到此回傳參數。"); }); } } } }
而WPF界面上的xaml代码就是一些表单控件,可以参见这里查看源码。
总结一下
在真实项目中,要合理的设计ViewModel基类得根据实际项目来,本文只是提供了一种思路,实际应用中可以灵活的把控,这也是身在江湖中程序猿杀人越货在必备技能。正所谓人在江湖,身不由已,干了这一行,遇到问题有的时候还是得想想招才行。
下一篇将分享一下用WPF开发项目中,一些公用组件封装成用户控件的话题,感兴趣的可以关注一下。