一、前言
本文主要介绍在 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 中(这部分代码是固定且通用的):
然后,既然是还原属性值,还是会导致属性变动,所以需要有个忽略操作,不然就死循环了。所以有个忽略列表%20_revertPropertyList%20用于存储本次需要忽略的属性名,进入方法时先判断如果存在于列表就跳过。至于还原操作,则是判断如果业务处理失败,就添加到忽略列表,然后从原始属性值字典%20_originPropertyValueDict%20中取出原始值,通过反射设置给相应的属性。代码截图如下,红框圈出的部分即为核心代码,也是通用的与业务无关的:
本节的代码如下:
#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,需要再安装一下%20PropertyChanging.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