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

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

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

魏刘宏 2022 年 4 月 17 日

一、前言

众所周知,无论是做网站开发还是软件开发,当涉及到需要用户填写信息之后提交的操作时,我们都需要对他填写的内容进行限制和验证,这类问题可以统称为表单验证问题。本文将针对 WPFTextBox 文本框,探究其中的一种验证方式 —— 使用 INotifyDataErrorInfo 在数据对象中进行验证。

 

二、参考

主要参考《WPF 编程宝典》一书的 19.4.1 一节:

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

 

之前在网上找资料,大多是提到了基于异常的验证(ExceptionValidationRule)和另一种数据错误验证 IDataErrorInfo,没有看到关于 INotifyDataErrorInfo 的描述。而按《WPF 编程宝典》一书的描述,INotifyDataErrorInfo 其实可以看作是 IDataErrorInfo 的升级版:

IDataErrorInfo 和 INotifyDataErrorInfo 接口具有共同的目标,即用更加人性化的错误通知系统替换未处理的异常。IDataErrorInfo 是初始的错误跟踪接口,可追溯至第一个.NET 版本,WPF 包含它是为了达到向后兼容的目的。INotifyDataErrorInfo 接口具有类似的作用,但界面更丰富,是针对 Silverlight 创建的,并且已移植到了 WPF 4.5。它还支持其他功能,如每个属性多个错误以及异步验证。  (《WPF 编程宝典》19.4.1 在数据对象中进行验证)

 

至于 ExceptionValidationRule,有个缺点就是在开发调试时,遇到抛出的异常,会进入中断状态。所以,本文直接研究 INotifyDataErrorInfo。后续可能还会研究其它不是针对数据对象的验证方式,这是后话了,暂且不表。

 

三、问题现象

我们在界面上构建一个加法计算的功能,有两个输入框可以用于输入两个加数,在右边显示计算结果,最右边是执行计算的按钮,如下图:

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

 

两个加数和一个结果都使用可绑定的属性;其中两个加数是完整属性的形式,方便之后添加验证代码;结果为自动属性形式,使用了 Fody 来实现变动通知;目前三个数都为 int 类型,如下:

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

 

加法命令就是简单的计算两个数相加,为了便于演示问题,先将结果置为 0,然后再延迟 200 毫秒,最后才是计算: 

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

 

演示如下(动图),正常计算没什么问题,如果将输入框内容清空,再进行计算,就可以看出不对的地方了 —— 前台绑定失败了,所以后台的值不变,进而导致计算结果还是保持了上次的状态,最终就形成了界面显示与数据结果不一致的尴尬局面:

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

 

其中输入框的水印为 TextBox 上指定的样式(文末会给出代码地址),验证失败的红框为 WPF 自带的。我们的目标是,这种情况,在点击计算时,能够进行拦截和提示。

 

四、实现验证接口

首先我们让绑定基类实现 INotifyDataErrorInfo 接口,实现该接口要实现三个成员:

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

 

具体为,一个获取错误列表的方法 GetErrors,一个指示是否存在错误的属性 HasErrors,以及一个错误变动事件 ErrorsChanged,如下:

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

 

《宝典》中还有如下辅助的代码,一个错误列表,一个设置错误的方法 SetErrors,以及一个清除错误的方法 ClearErrors。其中,错误列表是个字典,键为属性名,值为该属性的错误信息字符串列表。而两个方法主要是对错误列表进行相应的操作,并且触发变动事件。代码如下图:

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

 

本人添加了一个针对于属性的是否存在错误的方法 IsContainErrors(因为前面那个 HasErrors 是用于判断整体是否存在错误的),还有一个重载方法用于判断给定的几个属性是否存在错误。另外还重载了一个 GetErrors 方法,也是针对于同时处理几个属性的场景,并且之前返回类型为 List<List>,相当于只是把错误列表的 key 去掉了,并没有整合,而我这个方法返回值为 List,更方便使用。代码如下图:

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

 

还给了个验证是否为空的参考方法 ValidateBlank,主要就是使用了 SetErrors 和 ClearErrors 这两个方法:

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

 

所以最终改造后的绑定基类完整代码如下:

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
/*
 * 源码已托管:https://gitee.com/dlgcy/WPFTemplateLib
 */
namespace WPFTemplateLib.WpfHelpers
{
    /// <summary>
    /// WPF绑定属性基类;
    /// </summary>
    /// <example>
    /// <code>
    /// class Sample : BindableBase
    /// {
    ///     private List<string> _stuList;
    ///     public List<string> StuList
    ///     {
    ///         get => _stuList;
    ///         set => SetProperty(ref _stuList, value);
    ///     }
    /// }
    /// </code>
    /// </example>
    public abstract class BindableBase : INotifyPropertyChanged, INotifyDataErrorInfo
    {
        #region BindableBase

        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// 属性变动通知
        /// </summary>
        /// <param name="propertyName"></param>
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            if (Equals(storage, value)) return false;

            storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }

        protected bool SetPropertyWithoutCompare<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }

        #endregion

        #region 验证

        /// <summary>
        /// 错误列表
        /// </summary>
        private Dictionary<string, List<string>> _Errors = new Dictionary<string, List<string>>();

        private void SetErrors(string propertyName, List<string> propertyErrors)
        {
            //clear any errors that already exist for this property.
            _Errors.Remove(propertyName);

            //Add the list collection for the specified property.
            _Errors.Add(propertyName, propertyErrors);

            //Raise the error-notification event.
            if (ErrorsChanged != null)
                ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
        }

        private void ClearErrors(string propertyName)
        {
            //Remove the error list for this property.
            _Errors.Remove(propertyName);

            //Raise the error-notification event.
            if (ErrorsChanged != null)
                ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
        }

        /// <summary>
        /// 是否包含错误
        /// </summary>
        /// <param name="propertyName">属性名</param>
        public bool IsContainErrors(string propertyName)
        {
            return (GetErrors(propertyName) as List<string>)?.Count > 0;
        }

        /// <summary>
        /// 是否包含错误
        /// </summary>
        /// <param name="propertyName">属性名列表</param>
        public bool IsContainErrors(List<string> propertyNameList)
        {
            return propertyNameList.Exists(x => IsContainErrors(x));
        }

        /// <summary>
        /// 获取给定属性列表的错误列表(参数传空则获取所有错误列表)
        /// </summary>
        /// <param name="propertyName">属性名列表</param>
        /// <returns>错误列表(List<string>)</returns>
        public List<string> GetErrors(List<string> propertyNameList)
        {
            if (!propertyNameList?.Any() ?? true)
            {
                return _Errors.Values.SelectMany(x => x).ToList();
            }
            else
            {
                List<string> errors = new List<string>();
                foreach (string propertyName in propertyNameList)
                {
                    if (_Errors.ContainsKey(propertyName))
                    {
                        errors.AddRange(_Errors[propertyName]);
                    }
                }

                return errors;
            }
        }

        #region INotifyDataErrorInfo

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        /// <summary>
        /// 获取属性错误列表(属性名传空则获取所有错误列表)
        /// </summary>
        /// <param name="propertyName">属性名</param>
        /// <returns>错误列表(List<List<string>>)</returns>
        public IEnumerable GetErrors(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                //Provide all the error collections.
                return _Errors.Values;
            }
            else
            {
                //Provice the error collection for the requested property (if it has errors).
                if (_Errors.ContainsKey(propertyName))
                {
                    return _Errors[propertyName];
                }
                else
                {
                    return null;
                }
            }
        }

        /// <summary>
        /// 整个类是否存在错误
        /// </summary>
        public bool HasErrors => _Errors.Count > 0;

        #endregion

        #region 实用方法(供参考)

        /// <summary>
        /// 验证是否为空
        /// </summary>
        /// <returns>true-不为空,false-为空</returns>
        public virtual bool ValidateBlank(object value, string errMsg = "", [CallerMemberName] string propertyName = null)
        {
            bool valid = !string.IsNullOrWhiteSpace(value + "");
            if (!valid) //为空;
            {
                if (string.IsNullOrWhiteSpace(errMsg))
                {
                    errMsg = $"[{propertyName}] can't be blank";
                }

                SetErrors(propertyName, new List<string>() { errMsg });
            }
            else
            {
                ClearErrors(propertyName);
            }

            return valid;
        }

        #endregion

        #endregion
    }
}

 

五、使用

首先是 Xaml 中,在绑定时添加 ValidatesOnNotifyDataErrors=True :

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

 

实际上,按照《宝典》的说法,这也可以不加,因为默认就是 true,不过为了明确起见还是加上比较好:

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

 

然后是在需要验证的属性的 set 块中加上具体的验证代码,我这里使用了之前添加的验证是否为空的方法 ValidateBlank:

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

 

另外,之前这两个操作数是 int 类型,如果保持的话,当删除内容,红框还是会出现,但是 set 块没被执行,也就达不到验证的效果,没有找到解决方法,知道的朋友可以告知一下。我这里是把它们两个改成了 string 类型,满足本次需求。

 

然后是加法命令中的改造,主要就是使用了我加的那两个方法(IsContainErrors 和 GetErrors),传递的都是两个操作数属性名称列表,如果有错误(为空),就弹窗提示,并拦截代码执行逻辑(直接返回跳出):

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

 

六、效果演示及代码地址

首先来看看 Demo 的启动位置:

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

 

操作演示如下(动图),可以看到,输入框为空时点击计算,会弹出不能为空的提示:

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

 

最后给出代码地址,大家多多交流:https://gitee.com/dlgcy/DLGCY_WPFPractice/tree/Blog20220417 

 

全文完,祝大家生活愉快!

 

Leave a Reply