继上一篇烂尾文章分享了一些WPF数据验证的话题,这一篇来说一下ViewModel相互传参的问题,这也是在搭建JHRS开发框架中遇到的一些问题,并把它封装了一下。
这个演示的WPF开发管理系统的框架是基于MVVM来搭建的,因此业务处理都是在ViewModel里面完成的,而在实际项目中,难免会有两个页面(Page)跳来跳去的,它们在跳来跳去的时候是需要传递参数的,因为在这个框架中引入了Prism,事情就变更加的简单了。为了方便描述呢,本文就将模态窗口称为弹框页面了。
ViewModel相互传参
常见的业务场景就是在管理页列页面中,点击表格里面(DataGrid)某一行数据的编辑按钮,进行编辑,这时就会跳转到另外一个页面或者使用模态窗口打开一个弹框页面,在这个页面上进行编辑操作,在跳转的时候就会将参数进行传递,不然怎么在新开的页面知道你要干嘛呢?
在ViewModel相互传参的时候,可以使用对象的方式,也可以使用基本类型进行传递;因为在BaseViewModel这个基类中提供了打开弹框(模态窗口)窗体的功能,并且提供了一个object对象,参数为args的可选参数,可用于传参,代码如下:
/// <summary> /// 打开模态窗口 /// </summary> /// <param name="page">内容页面</param> /// <param name="icon">图标</param> /// <param name="pageTitle">页面标题</param> /// <param name="args">页面传参</param> /// <param name="callback">关闭窗体后执行的回调函数</param> /// <param name="disableArea">是否禁用弹框页面的保存,取消区域(即隐藏保存,取消按钮)</param> protected void ShowDialog(string page, IconEnum icon, string pageTitle = "未设置标题", object args = null, Action<IDialogResult> callback = null, bool disableArea = false) { IDialogWindow dialogWindow = Container.Resolve<IDialogWindow>("dialog"); dialogWindow.ConfigureDialogWindowEvents(callback); DialogParameters pars = new DialogParameters(); pars.Add("page", page); pars.Add("icon", icon.ToDescription()); pars.Add("title", pageTitle); if (disableArea) pars.Add("disableArea", true); if (args != null) pars.Add("args", args.ToJson()); dialogWindow.ConfigureDialogWindowContent("CommonDialogPage", pars); dialogWindow.ShowDialog(); }
可以看到args 是一个object对象,当判断到args不为空引用时,将其序列化成json再利用Prism的DialogParameters对象将其添加到参数里面去,这样就完成了页面跳转时候两个ViewModel之间的传参,当然在框架中的这种应用场景是通过打开弹框页面(模态窗口),在弹框页面做类似新增编辑的操作时传的参数。
ShowDialog这个方法是调用IDialogWindow接口的ShowDialog方法来打开模态窗口,模态窗口是需要在启动时注册的,并且模态窗口的ViewModel是实现了IDialogAware接口,在它的OnDialogOpened这个方法中,原理是利用IRegion接口的Context对象进行传参的,可以通过查看源码的方式了解到具体实现。在下面的OnDialogOpened方法中体现了是怎样设置args这个object参数的。
/// <summary> /// 弹框打开后触发 /// </summary> /// <param name="parameters"></param> public void OnDialogOpened(IDialogParameters parameters) { Page = parameters.GetValue<string>("page"); if (!string.IsNullOrWhiteSpace(Page)) { var args = parameters.GetValue<object>("args"); if (args != null) RegionManager.Regions[RegionNames.DialogRegin].Context = args; Navigate(RegionNames.DialogRegin, Page); } var icon = parameters.GetValue<string>("icon"); if (!string.IsNullOrEmpty(icon)) { WindowIcon = Application.Current.FindResource(icon) as UIElement; } if (parameters.GetValue<bool>("disableArea")) { EventAggregator.GetEvent<DisableDialogPageButtonEvent>().Publish(); } Title = parameters.GetValue<string>("title"); }
完整的代码点击这里直接在guthub上查看。
弹框回传参数解决方法
有时候会是遇上这种需求,就是当你打开了一个弹框页面后,在该页面做了某些操作,需要向父窗体传递一个参数,父窗体根据这个参数可以做是否刷新数据,是否在界面上改变某些值的行为,或者是否向表格(DataGrid)添加一行数据等等;还可以适用于这种场景,在编辑模式下,当完成编辑行为后,服务器端肯定会返回一个操作结果的值,当操作成功后关闭弹框页面并刷新主界面的表格数据,这时就需要回传参数了。
那框架中是如何解决的呢?其实答案还是和Prism相关的,因为它已经给我们完成了这个操作,先来看看主界面是如何接收回传参数的。
/// <summary> /// 編輯事件 /// </summary> public DelegateCommand<object> EditCommand => new DelegateCommand<object>((item) => { this.ShowDialog(typeof(AddOrEditReservation).FullName, IconEnum.編輯頁面圖標, "修改預約掛號記錄", args: item, callback: async (d) => { if (d.Parameters.GetValue<bool>("success")) await BindPagingData(); }); });
就像在上面ViewModel中处理编辑事件那样,首先调用了ShowDialog跳转到AddOrEditReservation这个页面(Page),在该页面上完成了编辑操作之后(即调用web api成功保存数据),然后回调函数 callback参数里面的匿名委托代码块里面,是通过d.Parameters.GetValue<bool>(“returnValue”)来接收参数的,当接收到这个参数后,就可以为欲为了。
再来看看弹框页面的保存按钮是怎样处理返回值的。
/// <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: "已經修改成功啦,這裏可以是任何參數和對象喲,父窗體可以接收到此回傳參數。"); }); } } }
上面代码就是弹框页面的保存代码逻辑,分为新增和修改2种情况。但最终都是根据服务器端的响应结果response.Succeeded来决定下一步行为,是通过调用CloseDialog方法,设置returnValue参数,这样就可以在主页面顺利的接收该回传参数了。而CloseDialog是放在ViewModel的基类里面的,下面是基类CloseDialog的实现。
/// <summary> /// 关闭对话框 /// </summary> /// <param name="isClose">是否关闭</param> /// <param name="returnValue">返回值</param> protected void CloseDialog(bool isClose = true, object returnValue = null) { if (isClose) { DialogParameters pars = new DialogParameters(); pars.Add("success", "true"); if (returnValue != null) pars.Add("returnValue", returnValue); EventAggregator.GetEvent<CloseDialogEvent>().Publish(pars); } }
主要原理还是使用DialogParameters来传递对话框的参数。
Prism事件聚合传参
说到参数传递,不得不提一下Prism的事件聚合,它是利用发布订阅机制来实现的,如果要跨页面,ViewModel传参都是可以的,那么它有什么用处呢?
假如你要在ViewModel里面控制一个按钮,当所有数据验证都通过了之后,才让按钮处于可点击状态,这时你在ViewModel(后台C#代码)做完了所有的验证,因为你拿不到界面控件,这时你就可以通过发布一个事件,相关界面订阅你的这个事件,更新按钮状态就可以了。
XAML后台代码订阅事件
/// <summary> /// CommonDialogPage.xaml 的交互逻辑 /// </summary> public partial class CommonDialogPage : Page { public CommonDialogPage() { InitializeComponent(); ConstrolStateEvent controlEvent = ServiceLocator.Current.TryResolve<IEventAggregator>().GetEvent<ConstrolStateEvent>(); controlEvent.Subscriptions.Clear(); controlEvent.Subscribe((state) => { SaveButton.IsEnabled = state.IsEnabled; }); } }
上面的代码节取自框架中的CommonDialogPage这个公共弹框页面的代码,在这里面订阅了一个控件状态相关的事件,完成的功能是当有其它功能发布了禁用/启用保存按钮事件后,决定该保存按钮是不是可点击状态。
发布一个事件
在发布事件之前,你需要定义一个继承自PubSubEvent类的事件对象,可以通过该对象的泛型版本完成传参。
public class ConstrolStateEvent : PubSubEvent<ControlState> { public new ICollection<IEventSubscription> Subscriptions => base.Subscriptions; }
接下来发布事件,如果是在继承了BaseViewModel的子类里面发布事件,可以直接用基类提供的事件聚合属性【EventAggregator】来发布,如果在其它地方,也可以通过Prism提供的服务定位器(ServiceLocator对象)来完成发布事件。
服务器定位器发布事件
ServiceLocator.Current.TryResolve<IEventAggregator>().GetEvent<ConstrolStateEvent>().Publish(new ControlState { IsEnabled = true });
ViewModel发布事件
/// <summary> /// 保存事件 /// </summary> public DelegateCommand SaveCommand => new DelegateCommand(() => { EventAggregator.GetEvent<CommonSaveEvent>().Publish(); EventAggregator.GetEvent<ConstrolStateEvent>().Publish(new ControlState { IsEnabled = false }); });
如上面代码所示,通过这句代码就完成了事件发布了
EventAggregator.GetEvent<ConstrolStateEvent>().Publish(new ControlState { IsEnabled = false });
最后总结一下
一个框架是应该提供这种完整的机制来解决这些非核心业务问题,但缺了这些机制,代码写起来感觉会不顺手的也别扭;ViewModel相互传参和弹框回传参数,其实都是站在Prism这个巨人的肩膀上来完成的;下一篇将是JHRS开发框架之踩坑记。