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

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

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

独立观察员 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 事件:

%title插图%num

 

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

%title插图%num

 

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

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

 

3.1、方式一

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

%title插图%num

 

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

%title插图%num

 

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

 

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

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

%title插图%num

 

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

%title插图%num

 

看看是不是很简洁呀。

 

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

 

3.2、方式二

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

%title插图%num

 

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

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

%title插图%num

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

 

3.3、说明

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

 

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

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

%title插图%num

%title插图%num

 

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

 

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

 

四、恢复属性原始值

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

%title插图%num

 

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

%title插图%num

 

本节的代码如下:

#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:

%title插图%num

 

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

%title插图%num

 

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

%title插图%num

 

五、效果演示

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

%title插图%num

 

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

%title插图%num

 

六、总结

本文介绍了两部分内容:

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) 第一时间获取最新文章

%title插图%num

发表评论