WPF 表单验证之 INotifyDataErrorlnfo 接口的使用示例
一、前言
众所周知,无论是做网站开发还是软件开发,当涉及到需要用户填写信息之后提交的操作时,我们都需要对他填写的内容进行限制和验证,这类问题可以统称为表单验证问题。本文将针对 WPF 的 TextBox 文本框,探究其中的一种验证方式 —— 使用 INotifyDataErrorInfo 在数据对象中进行验证。
二、参考
主要参考《WPF 编程宝典》一书的 19.4.1 一节:
之前在网上找资料,大多是提到了基于异常的验证(ExceptionValidationRule)和另一种数据错误验证 IDataErrorInfo,没有看到关于 INotifyDataErrorInfo 的描述。而按《WPF 编程宝典》一书的描述,INotifyDataErrorInfo 其实可以看作是 IDataErrorInfo 的升级版:
IDataErrorInfo 和 INotifyDataErrorInfo 接口具有共同的目标,即用更加人性化的错误通知系统替换未处理的异常。IDataErrorInfo 是初始的错误跟踪接口,可追溯至第一个.NET 版本,WPF 包含它是为了达到向后兼容的目的。INotifyDataErrorInfo 接口具有类似的作用,但界面更丰富,是针对 Silverlight 创建的,并且已移植到了 WPF 4.5。它还支持其他功能,如每个属性多个错误以及异步验证。 (《WPF 编程宝典》19.4.1 在数据对象中进行验证)
至于 ExceptionValidationRule,有个缺点就是在开发调试时,遇到抛出的异常,会进入中断状态。所以,本文直接研究 INotifyDataErrorInfo。后续可能还会研究其它不是针对数据对象的验证方式,这是后话了,暂且不表。
三、问题现象
我们在界面上构建一个加法计算的功能,有两个输入框可以用于输入两个加数,在右边显示计算结果,最右边是执行计算的按钮,如下图:
两个加数和一个结果都使用可绑定的属性;其中两个加数是完整属性的形式,方便之后添加验证代码;结果为自动属性形式,使用了 Fody 来实现变动通知;目前三个数都为 int 类型,如下:
加法命令就是简单的计算两个数相加,为了便于演示问题,先将结果置为 0,然后再延迟 200 毫秒,最后才是计算:
演示如下(动图),正常计算没什么问题,如果将输入框内容清空,再进行计算,就可以看出不对的地方了 —— 前台绑定失败了,所以后台的值不变,进而导致计算结果还是保持了上次的状态,最终就形成了界面显示与数据结果不一致的尴尬局面:
其中输入框的水印为 TextBox 上指定的样式(文末会给出代码地址),验证失败的红框为 WPF 自带的。我们的目标是,这种情况,在点击计算时,能够进行拦截和提示。
四、实现验证接口
首先我们让绑定基类实现 INotifyDataErrorInfo 接口,实现该接口要实现三个成员:
具体为,一个获取错误列表的方法 GetErrors,一个指示是否存在错误的属性 HasErrors,以及一个错误变动事件 ErrorsChanged,如下:
《宝典》中还有如下辅助的代码,一个错误列表,一个设置错误的方法 SetErrors,以及一个清除错误的方法 ClearErrors。其中,错误列表是个字典,键为属性名,值为该属性的错误信息字符串列表。而两个方法主要是对错误列表进行相应的操作,并且触发变动事件。代码如下图:
本人添加了一个针对于属性的是否存在错误的方法 IsContainErrors(因为前面那个 HasErrors 是用于判断整体是否存在错误的),还有一个重载方法用于判断给定的几个属性是否存在错误。另外还重载了一个 GetErrors 方法,也是针对于同时处理几个属性的场景,并且之前返回类型为 List<List>,相当于只是把错误列表的 key 去掉了,并没有整合,而我这个方法返回值为 List,更方便使用。代码如下图:
还给了个验证是否为空的参考方法 ValidateBlank,主要就是使用了 SetErrors 和 ClearErrors 这两个方法:
所以最终改造后的绑定基类完整代码如下:
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
:
实际上,按照《宝典》的说法,这也可以不加,因为默认就是 true,不过为了明确起见还是加上比较好:
然后是在需要验证的属性的 set 块中加上具体的验证代码,我这里使用了之前添加的验证是否为空的方法 ValidateBlank:
另外,之前这两个操作数是 int 类型,如果保持的话,当删除内容,红框还是会出现,但是 set 块没被执行,也就达不到验证的效果,没有找到解决方法,知道的朋友可以告知一下。我这里是把它们两个改成了 string 类型,满足本次需求。
然后是加法命令中的改造,主要就是使用了我加的那两个方法(IsContainErrors 和 GetErrors),传递的都是两个操作数属性名称列表,如果有错误(为空),就弹窗提示,并拦截代码执行逻辑(直接返回跳出):
六、效果演示及代码地址
首先来看看 Demo 的启动位置:
操作演示如下(动图),可以看到,输入框为空时点击计算,会弹出不能为空的提示:
最后给出代码地址,大家多多交流:https://gitee.com/dlgcy/DLGCY_WPFPractice/tree/Blog20220417
全文完,祝大家生活愉快!
原创文章,转载请注明: 转载自 独立观察员(dlgcy.com)
本文链接地址: [WPF 表单验证之 INotifyDataErrorlnfo 接口的使用示例](https://dlgcy.com/wpf-validate-inotifydataerrorlnfo-demo/)
关注微信公众号 独立观察员博客(DLGCY_BLOG) 第一时间获取最新文章
发表评论