WPF 属性变动后的业务处理及恢复原始值的方法
一、前言
本文主要介绍在 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
事件:
添加到原来的绑定基类中也是很容易的(当然您也可以使用现成的框架或库):
三、属性变动后的业务处理方法
这个其实我之前在做 “Wifi 固定器”(《Windows 小工具之 Wifi 固定器》)时已经用过了,当时用了两种方法:
3.1、方式一
在绑定基类中直接订阅 PropertyChanged
事件,不过处理方法是一个空的虚方法,方便在子类中重写,代码如下:
然后在 ViewModel 中就可以重写进行业务处理了,也就是 switch 属性名来判断需要的操作:
有人可能会说,为什么不直接在属性的 set 中进行处理呢?
1、首先,其实不太推荐在属性的 set 中放置业务代码,尤其是本来是自动属性的,因为需要处理些业务方面的东西就改为传统属性,多少有点不优雅。
此时又有人说了,WPF 里面需要绑定功能的属性,本来就不是最简洁的自动属性呀!其实是可以是最简洁的自动属性的,方法就是使用 PropertyChanged.Fody:
然后在需要实现属性变动通知的类上面加上 [AddINotifyPropertyChangedInterface]
特性就行了:
看看是不是很简洁呀。
2、不直接在 set 块中进行处理的另外原因可能是,如果那样的话业务逻辑就比较分散了,不利于维护,容易出 Bug。反观我上面使用的方式,业务代码都在一起,非常利于维护。
3.2、方式二
还是以 “Wifi 固定器” 中的代码为例:
也就是直接给需要的对象的 PropertyChanged
事件附加处理方法(方法里的具体代码和方式一中类似),当然,这个对象的类型也必须是直接或间接实现了 INotifyPropertyChanged
接口的(不然就没有 PropertyChanged
事件嘛)。
这种方式更加灵活,因为可以根据情况来随时附加和取消处理方法。比如,只在编辑状态时附加事件处理方法,在转为浏览状态时,取消该处理方法:
[图 3-2-1 按情况附加和取消方法(来自:DLGCY_WPFPractice)]
3.3、说明
其实这种属性变动后的业务处理的写法,我之前在网上并没有看到过(网上 WPF 的资料还是偏少啊),但是按理说这种应该很容易想到,所以我也不太确定这样写合不合适,大家有更好的方法欢迎提出。
言归正传,接下来说说我是怎么想到这种写法的吧。故事当然还要从绑定基类中的 PropertyChanged
事件说起,不知道大家学习 WPF 的时候有没有觉得很纳闷,这是一个事件,但是并没有看到有什么地方订阅它,那么整个逻辑是怎么走通的呢?其实之前没有去深究的时候,就是说服自己,这是微软的黑科技呗。不过大概也知道,就是 WPF 框架自己会去处理这个事。
后来,问了下 ChatGPT,一切就合理了起来:
也就是说,订阅 PropertyChanged
事件的,就是 Binding 对象。
然后就想到,既然是个事件,Binding 对象订阅得,我们这些尊贵的开发者岂有订阅不得的道理?所以我就给它订阅了,也就有了上面的故事。
四、恢复属性原始值
要恢复属性的原始值,就需要事先获取并存储了该原始值,这里的 获取 就要用到第二节中提到的 PropertyChanging
事件了,至于存储,我这里是用了个 Dictionary<string, object>
字典类型的成员变量来存储。具体就是,在 PropertyChanging 的方法中,使用反射获取属性值,以属性名作为 key,以属性值作为 value,存储到字典 _originPropertyValueDict 中(这部分代码是固定且通用的):
然后,既然是还原属性值,还是会导致属性变动,所以需要有个忽略操作,不然就死循环了。所以有个忽略列表 _revertPropertyList 用于存储本次需要忽略的属性名,进入方法时先判断如果存在于列表就跳过。至于还原操作,则是判断如果业务处理失败,就添加到忽略列表,然后从原始属性值字典 _originPropertyValueDict 中取出原始值,通过反射设置给相应的属性。代码截图如下,红框圈出的部分即为核心代码,也是通用的与业务无关的:
本节的代码如下:
#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:
然后在相关类上添加 [ImplementPropertyChanging]
特性:
由于 Fody 的 ImplementPropertyChanging 未成功,所以相关类还是改为 普通属性 绑定基类 的形式:
五、效果演示
先简单看下模拟的业务处理的代码:
也就是用户名设置成功有个气泡弹窗,然后年龄大于 200 岁会被还原。效果如下(动图):
六、总结
本文介绍了两部分内容:
1、属性变动后的业务处理方式。这部分其实主要就是通过订阅 PropertyChanged
事件来实现的,无论是借助于 自定义的绑定基类、PropertyChanged.Fody、还是其它框架或库(如 CommunityToolkit.Mvvm)都是可以的,因为它们都会引入 PropertyChanged
事件。
2、还原属性的原始值。这部分是综合应用了 PropertyChanged
事件和 PropertyChanging
事件;前者因为主要用于进行业务处理,所以属性原始值的还原操作的发起者一般也就是它了;后者则是用于获取和存储原始值。这部分由于 PropertyChanging.Fody(1.30.3)使用失败,所以只能用 自定义的绑定基类 或者 其它框架或库(需要他们能够引入 PropertyChanging
事件)。
最后给出代码地址,大家可以自己试一下:https://gitee.com/dlgcy/DLGCY_WPFPractice/tree/Blog20230226
原创文章,转载请注明: 转载自 独立观察员(dlgcy.com)
本文链接地址: [WPF 属性变动后的业务处理及恢复原始值的方法](https://dlgcy.com/wpf-after-propertychanged-and-restore-original-value/)
关注微信公众号 独立观察员博客(DLGCY_BLOG) 第一时间获取最新文章
发表评论