WPF 属性变动后的业务处理及恢复原始值的方法

WPF 属性变动后的业务处理及恢复原始值的方法

a3a0edfee395c47fdf57ddaec219051c.png

独立观察员 2023 年 2 月 26 日

一、前言

本文主要介绍在 WPF 中,当属性变动后,如何依据是哪个属性变动了,以及其变动的值的情况来进行相应业务处理的推荐的方式;以及如果要恢复属性的原始值,可以怎么做。

阅读本文需要有一定的 WPF 基础(WPF 绑定基类),如果是刚入门的朋友,可以先看看我以前写的文章《WPF 原生绑定和命令功能使用指南》。

二、INotifyPropertyChanging

之前定义绑定基类的时候,大家都是只关注 INotifyPropertyChanged 这个接口,也就是只会在绑定基类中添加 PropertyChanged 事件,其实这样对于基础使用确实也够了。最近在使用 CommunityToolkit.Mvvm 框架时,发现它的绑定基类里面不知道什么时候添加了 INotifyPropertyChanging 接口的实现(源码为:https://github.com/CommunityToolkit/dotnet/blob/main/src/CommunityToolkit.Mvvm/ComponentModel/ObservableObject.cs),本文介绍的方法也会用到这个,所以来介绍一下。

INotifyPropertyChanging 这个接口,顾名思义,作用就是规范了实现类需要有属性变化通知功能(INotifyPropertyChanged 是属性变化通知功能)。里面也只有一个成员,也就是 PropertyChanging 事件:

1c7d97e67ac5d4b64a1b386afc3fe81f.png

添加到原来的绑定基类中也是很容易的(当然您也可以使用现成的框架或库):

356570a435425d66574b50ff6b6f2db8.png

三、属性变动后的业务处理方法

这个其实我之前在做 “Wifi 固定器”(《Windows 小工具之 Wifi 固定器》)时已经用过了,当时用了两种方法:

3.1、方式一

在绑定基类中直接订阅 PropertyChanged 事件,不过处理方法是一个空的虚方法,方便在子类中重写,代码如下:

59a6a0b7f35342b5d9c02f87d36862fb.png

然后在 ViewModel 中就可以重写进行业务处理了,也就是 switch 属性名来判断需要的操作:

58e60b86c368d9ae8f0ad2456ececa13.png

有人可能会说,为什么不直接在属性的 set 中进行处理呢?

1、首先,其实不太推荐在属性的 set 中放置业务代码,尤其是本来是自动属性的,因为需要处理些业务方面的东西就改为传统属性,多少有点不优雅。

此时又有人说了,WPF 里面需要绑定功能的属性,本来就不是最简洁的自动属性呀!其实是可以是最简洁的自动属性的,方法就是使用 PropertyChanged.Fody:

e188fb4517f6bba9d601f9f8b454b966.png

然后在需要实现属性变动通知的类上面加上 [AddINotifyPropertyChangedInterface] 特性就行了:

3618ab2b1c9d0402ba22c7597a47373e.png

看看是不是很简洁呀。

2、不直接在 set 块中进行处理的另外原因可能是,如果那样的话业务逻辑就比较分散了,不利于维护,容易出 Bug。反观我上面使用的方式,业务代码都在一起,非常利于维护。

3.2、方式二

还是以 “Wifi 固定器” 中的代码为例:

30bdaf78377cb57658879b5e724b8c5f.png

也就是直接给需要的对象的 PropertyChanged 事件附加处理方法(方法里的具体代码和方式一中类似),当然,这个对象的类型也必须是直接或间接实现了 INotifyPropertyChanged 接口的(不然就没有 PropertyChanged 事件嘛)。

这种方式更加灵活,因为可以根据情况来随时附加和取消处理方法。比如,只在编辑状态时附加事件处理方法,在转为浏览状态时,取消该处理方法:

119e96dedf283c5adab56b42b447c531.png

[图 3-2-1 按情况附加和取消方法(来自:DLGCY_WPFPractice)]

3.3、说明

其实这种属性变动后的业务处理的写法,我之前在网上并没有看到过(网上 WPF 的资料还是偏少啊),但是按理说这种应该很容易想到,所以我也不太确定这样写合不合适,大家有更好的方法欢迎提出。

言归正传,接下来说说我是怎么想到这种写法的吧。故事当然还要从绑定基类中的 PropertyChanged 事件说起,不知道大家学习 WPF 的时候有没有觉得很纳闷,这是一个事件,但是并没有看到有什么地方订阅它,那么整个逻辑是怎么走通的呢?其实之前没有去深究的时候,就是说服自己,这是微软的黑科技呗。不过大概也知道,就是 WPF 框架自己会去处理这个事。

后来,问了下 ChatGPT,一切就合理了起来:

3d4c5fa5b4fcd091055397d1b351dbb9.png

14e265382ce9c83521b28bfbac5f6ded.png

也就是说,订阅 PropertyChanged 事件的,就是 Binding 对象。

然后就想到,既然是个事件,Binding 对象订阅得,我们这些尊贵的开发者岂有订阅不得的道理?所以我就给它订阅了,也就有了上面的故事。

四、恢复属性原始值

要恢复属性的原始值,就需要事先获取并存储了该原始值,这里的 获取 就要用到第二节中提到的 PropertyChanging 事件了,至于存储,我这里是用了个 Dictionary<string, object> 字典类型的成员变量来存储。具体就是,在 PropertyChanging 的方法中,使用反射获取属性值,以属性名作为 key,以属性值作为 value,存储到字典 _originPropertyValueDict 中(这部分代码是固定且通用的):

8f400c0827190eec840440f81598f44c.png

然后,既然是还原属性值,还是会导致属性变动,所以需要有个忽略操作,不然就死循环了。所以有个忽略列表 _revertPropertyList 用于存储本次需要忽略的属性名,进入方法时先判断如果存在于列表就跳过。至于还原操作,则是判断如果业务处理失败,就添加到忽略列表,然后从原始属性值字典 _originPropertyValueDict 中取出原始值,通过反射设置给相应的属性。代码截图如下,红框圈出的部分即为核心代码,也是通用的与业务无关的:

9b1d4f589cdbb87c990163d7e8c3f44c.png

本节的代码如下:

#region 属性变动处理


/// <summary>
/// 属性变更中(记录原始值)
/// </summary>
private void User_PropertyChanging(object sender, PropertyChangingEventArgs e)
{
  Type type = sender.GetType();
  PropertyInfo propertyInfo = type.GetProperty(e.PropertyName);
  if (propertyInfo != null)
  {
    _originPropertyValueDict[e.PropertyName] = propertyInfo.GetValue(sender);
  }
}


/// <summary>
/// 原始的属性值字典
/// </summary>
private Dictionary<string, object> _originPropertyValueDict = new Dictionary<string, object>();


/// <summary>
/// 正在被还原的属性名列表
/// </summary>
private List<string> _revertPropertyList = new List<string>();


/// <summary>
/// 属性变更后(业务处理)
/// </summary>
private void User_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
  try
  {
    //如果属性存在于忽略列表中,则从忽略列表中移除,并跳过此次执行(业务处理)
    if (_revertPropertyList.Contains(e.PropertyName))
    {
      _revertPropertyList.Remove(e.PropertyName);
      return;
    }


    bool isSuccess = true; //业务处理是否成功;
    var defaultObject = default(User); //只是用于获取属性名称;


    try
    {
      //业务处理;
      switch (e.PropertyName)
      {
        case nameof(defaultObject.UserName):
          {
          ToastToScreenCmd.Execute($"用户名设置成功:{SelectedItem.UserName}"); //模拟业务处理;
          break;
        }
        case nameof(defaultObject.Age):
          {
          //模拟还原属性原始值;
          if (SelectedItem.Age > 200)
          {
            isSuccess = false;
            ToastToScreenCmd.Execute("人不可能这么大年龄,请重新设置!");
          }
          break;
        }
        default:
          {
          //isSuccess = false;
          //ToastToScreenCmd.Execute("无对应项");
          break;
        }
      }
    }
    catch (Exception ex)
    {
      isSuccess = false;
      Console.WriteLine($"异常:{ex}");
    }


    if (isSuccess)
    {
      ToastToScreenCmd.Execute("设置完成");
    }
    else //不成功则还原值
    {
      //添加到忽略列表,避免循环;
      _revertPropertyList.Add(e.PropertyName);


      //还原原始值
      Type type = sender.GetType();
      PropertyInfo propertyInfo = type.GetProperty(e.PropertyName);
      if (propertyInfo != null)
      {
        propertyInfo.SetValue(sender, _originPropertyValueDict[e.PropertyName], null);
      }
    }
  }
  catch (Exception ex)
  {
    Console.WriteLine($"异常:{ex}");
  }
}


  #endregion

另外,如果要使用 Fody,需要再安装一下 PropertyChanging.Fody:

be2c59384b08a82ec30f60fb5303e113.png

然后在相关类上添加 [ImplementPropertyChanging] 特性:

ff70ad6e40d013105000fdb7ca42259c.png

由于 Fody 的 ImplementPropertyChanging 未成功,所以相关类还是改为 普通属性 绑定基类 的形式:

c233d435339d2a08feb8d00f177c1739.png

五、效果演示

先简单看下模拟的业务处理的代码:

e50ac9e3f4b71badbd9fce1f952e9290.png

也就是用户名设置成功有个气泡弹窗,然后年龄大于 200 岁会被还原。效果如下(动图):

ab3e7710c7471926793fc573a6b2baff.gif

六、总结

本文介绍了两部分内容:

1、属性变动后的业务处理方式。这部分其实主要就是通过订阅 PropertyChanged 事件来实现的,无论是借助于 自定义的绑定基类、PropertyChanged.Fody、还是其它框架或库(如 CommunityToolkit.Mvvm)都是可以的,因为它们都会引入 PropertyChanged 事件。

2、还原属性的原始值。这部分是综合应用了 PropertyChanged 事件和 PropertyChanging 事件;前者因为主要用于进行业务处理,所以属性原始值的还原操作的发起者一般也就是它了;后者则是用于获取和存储原始值。这部分由于 PropertyChanging.Fody(1.30.3)使用失败,所以只能用 自定义的绑定基类 或者 其它框架或库(需要他们能够引入 PropertyChanging 事件)。

最后给出代码地址,大家可以自己试一下:https://gitee.com/dlgcy/DLGCY_WPFPractice/tree/Blog20230226

-

技术群:添加小编微信并备注进群

小编微信:mm1552923   

公众号:dotNet编程大全    

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值