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

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

76df7cac4088cfd31ec12806f75d5ac4.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 事件:

a29b1fe93b69b2c89e2be67aa4d0714c.png

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

ad90b86846b49805168bc4ac28f4f1e8.png

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

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

3.1、方式一

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

2eec900c91388c1b2a1f13e96b75e1d6.png

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

e543de9cc220fc79ddc905484ceb2acf.png

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

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

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

b9638db80e911364665c64d31f2bf9fe.png

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

85a35b776d12d91510670cb615e0f017.png

看看是不是很简洁呀。

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

3.2、方式二

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

0f04039ec424442bb47e0c9a9d40711d.png

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

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

3bbec81cc631b7f3a4b1d026e8909c6c.png

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

3.3、说明

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

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

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

fac7275703c12faa81906a0141ae6254.png

124a2b652b6741be6bd9f2108ba72cf1.png

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

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

四、恢复属性原始值

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

87c6c3ab9a0b787b238d39f0028f24df.png

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

a33868f0a47b452e89e6831ba63ee70e.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:

f8c62c09fa73988d55313e2564ee935d.png

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

b54a75e881dacf5d1b2b7af02056688a.png

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

9bfce398d4ab3212059a9fabf5432a8b.png

五、效果演示

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

d65204b727673d2c7d5b906b968ca107.png

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

d71c825578d2129ca1e39dc1860ae6d3.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

原创文章,转载请注明: 转载自 独立观察员

本文链接地址: WPF 属性变动后的业务处理及恢复原始值的方法 [http://dlgcy.com/wpf-after-propertychanged-and-restore-original-value/]

WPF

我向 ChatGPT 讨教了一下 WPF 中的行为 Behavior

使用通用附加属性来减少 WPF 元素自定义样式的多余代码

几十款 WPF 控件 - UI 库,总有一款适合你

WPF 用户控件分享之边上带输入框的圆圈

分享一个 WPF 气泡弹框

WPF 表单验证之 INotifyDataErrorlnfo 接口的使用示例

[翻译] WPF 中用户控件 DataContext/Binding 和依赖属性的问题

OxyPlot 导出图片及 WPF 元素导出为图片的方法

让 WPF 的 RadioButton 支持再次点击取消选中的功能

WPF DataGrid 如何将被选中行带到视野中

WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题

WPF DataGrid 通过自定义表头模拟首行固定

WPF ComboBox 使用 ResourceBinding 动态绑定资源键并支持语言切换

【翻译】WPF 中附加行为的介绍 Introduction to Attached Behaviors in WPF

WPF 使用 Expression Design 画图导出及使用 Path 画图

WPF MVVM 弹框之等待框

解决 WPF 绑定集合后数据变动界面却不更新的问题(使用 ObservableCollection)

WPF 消息框 TextBox 绑定新数据时让光标和滚动条跳到最下面

真・WPF 按钮拖动和调整大小

WPF MVVM 模式下的弹窗

WPF 让一组 Button 实现 RadioButton 的当前样式效果

WPF 原生绑定和命令功能使用指南

WPF 用户控件的自定义依赖属性在 MVVM 模式下的使用备忘

在WPF的MVVM模式中使用OCX组件

第三方库使用

WPF 依赖注入之 Microsoft.Extensions.DependencyInjection

WPF 表格控件 ReoGrid 的简单使用

OxyPlot.WPF 公共属性一览

OxyPlot.Wpf 图表控件使用备忘

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值