WPF 表单验证之 INotifyDataErrorlnfo 接口的使用示例
目录
WPF 表单验证之 INotifyDataErrorlnfo 接口的使用示例
一、前言
二、参考
三、问题现象
四、实现验证接口
五、使用
六、效果演示及代码地址
独立观察员 2022 年 4 月 17 日
一、前言
众所周知,无论是做网站开发还是软件开发,当涉及到需要用户填写信息之后提交的操作时,我们都需要对他填写的内容进行限制和验证,这类问题可以统称为表单验证问题。本文将针对 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 这两个方法:
所以最终改造后的绑定基类完整代码如下:
代码语言:javascript复制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
全文完,祝大家生活愉快!