站点图标 江湖人士

JHRS开发框架之ViewModel相互传参和弹框回传参的解决办法

asp.net core异步编程最佳实践

asp.net core异步编程最佳实践

继上一篇烂尾文章分享了一些WPF数据验证的话题,这一篇来说一下ViewModel相互传参的问题,这也是在搭建JHRS开发框架中遇到的一些问题,并把它封装了一下。

这个演示的WPF开发管理系统的框架是基于MVVM来搭建的,因此业务处理都是在ViewModel里面完成的,而在实际项目中,难免会有两个页面(Page)跳来跳去的,它们在跳来跳去的时候是需要传递参数的,因为在这个框架中引入了Prism,事情就变更加的简单了。为了方便描述呢,本文就将模态窗口称为弹框页面了。

JHRS开发框架之ViewModel相互传参和弹框回传参的解决办法

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开发框架之踩坑记

本系列相关阅读

  1. WPF企业级开发框架搭建指南(启示录)
  2. JHRS开发框架之基础类库
  3. JHRS开发框架之第三方框架选型
  4. JHRS开发框架之WPF调用Web API封装
  5. JHRS开发框架之客户端入口项目
  6. JHRS开发框架之各子系统如何整合
  7. JHRS开发框架之怎样设计合理的ViewModel基类
  8. JHRS开发框架之公用组件用户控件的封装
  9. JHRS开发框架之建议遵循的一些建目录文件原则
  10. JHRS开发框架之WPF数据验证
  11. JHRS开发框架之ViewModel相互传参和弹框回传参的解决办法
  12. JHRS开发框架之踩坑记(终章)
退出移动版