在介绍IDataErrorInfo验证之前,我们先看一段视频。

视频中我们可以看到,用很少的代码量就完成了数据验证的功能,并且在XAML代码中只加了一条语“ValidatesOnDataErrors=True”,后端也仅仅是在属性加入了一组自定义特性。之所以能够使用简洁的代码实现验证功能,这得益于IDataErrorInfo、Attribute以及Reflection的应用。
实现方式
第一步:创建自定义特性,该特性用于验证属性的值是否满足要求。该类主要有一个IsValid方法和ErrorMessage属性,IsValid方法是用来判断该属性值是否满足要求,ErrorMessage是如果不满足要求情况下,返回的错误信息。
public abstract class AttributeBase : Attribute
{
/// <summary>
/// 验证失败时给出的消息
/// </summary>
public string ErrorMessage { get; protected set; }
/// <summary>
/// 是否满足验证规则,如果满足返回true
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public virtual bool IsValid(object obj)
{
return true;
}
}
public class NumberRuleAttribute : AttributeBase
{
public NumberRuleAttribute(double minValue, double maxValue, string errorMessage = null)
{
MinValue = minValue;
MaxValue = maxValue;
if (string.IsNullOrEmpty(errorMessage))
{
var sb = new StringBuilder(1024);
sb.Append("The value should between ");
sb.Append(minValue);
sb.Append(" and ");
sb.Append(maxValue);
ErrorMessage = sb.ToString();
}
else
{
ErrorMessage = errorMessage;
}
}
private double MinValue { get; }
private double MaxValue { get; }
public override bool IsValid(object obj)
{
if (obj == null)
{
return false;
}
// 如果输入的是非数值
if (!double.TryParse(obj.ToString(), out var value))
{
return false;
}
// 如果不满足最大值和最小值限制
if (value > MaxValue || value < MinValue)
return false;
return true;
}
}
第二步:创建实体类基类,该类继承并实现IDataErrorInfo接口,主要属性string this[string propertyName]的实现方法可以参考如下代码。在使用反射获取该类的属性时,会获取到Error和Item属性,该属性是父类的属性,不需要进行判断。在遍历子类属性时,如果存在自定义特性,则按照自定义特性的验证方法进行验证。该基类中,我们添加了一个函数IsValidated(),该函数的功能是验证该类及子类中所有的属性是否满足要求,这个是用于页面提交时使用的函数。该该函数与string this[string propertyName]的区别是:string this[string propertyName]只验证特定属性是否满足要求,UI显示用的;IsValidated()方法用于判断所有属性是否满足要求,后台commit使用的。
class ViewModeBase : INotifyPropertyChanged, IDataErrorInfo
{
private string error;
public event PropertyChangedEventHandler PropertyChanged;
protected void InvokePropertyChanged(string property)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(property));
}
}
public string Error
{
get
{
return error;
}
}
public string this[string propertyName]
{
get
{
var property = GetType().GetProperty(propertyName);
if (property != null)
{
var value = property.GetValue(this, null);
var attributes = property.GetCustomAttributes(false);
foreach (var attribute in attributes)
{
if (attribute is NumberRuleAttribute numberRule)
{
if (!numberRule.IsValid(value))
{
error = numberRule.ErrorMessage;
return error;
}
}
else if (attribute is StringRuleAttribute stringRule)
{
if (!stringRule.IsValid(value))
{
error = stringRule.ErrorMessage;
return error;
}
}
}
}
return null;
}
}
public bool IsValidated()
{
// 通过反射获取所有带自定义特性的属性,判断属性值是否满足要求
var properties = GetType().GetProperties(BindingFlags.Public| BindingFlags.Instance);
foreach (var property in properties)
{
// 属性Error和Item来自接口IDataErrorInfo,无需进行验证
if (property.Name == "Error" || property.Name == "Item")
{
continue;
}
var value = property.GetValue(this, null);
var attributes = property.GetCustomAttributes(false);
foreach (var attribute in attributes)
{
if (attribute is NumberRuleAttribute numberRule)
{
if (!numberRule.IsValid(value))
{
error = numberRule.ErrorMessage;
return false;
}
}
else if (attribute is StringRuleAttribute stringRule)
{
if (!stringRule.IsValid(value))
{
error = stringRule.ErrorMessage;
return false;
}
}
}
}
return true;
}
}
第三步:创建实体类子类。该类就比较简单,只需要在属性上加一个特性就行,特性名称可以省略Attribute,例如NumberRule和NumberRuleAttribute是等效的,特性是可以叠加的。
class Student : ViewModeBase
{
private string name;
<span style="font-size:0px; color:#ff0000;"><a href="https://www.lambdapy.com/">壯陽藥</a>
</span> private double age;
private string telephone;
[StringRule(2, 5)]
public string Name
{
get { return name; }
set
{
name = value;
InvokePropertyChanged("Name");
}
}
[NumberRule(0,150)]
public double Age
{
get { return age; }
set
{
age = value;
InvokePropertyChanged("Age");
}
}
[StringRule(11, 11,"手机号必须是11位数")]
public string Telephone
{
get { return telephone; }
set
{
if (telephone != value)
{
telephone = value;
InvokePropertyChanged("Telephone");
}
}
}
}
第四步:修改TextBox的模板,具体代码可参考《WPF的TextBox输入验证之Exception验证》或者下载源代码,关键代码如下。
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Horizontal" >
<Border >
<Grid>
<AdornedElementPlaceholder x:Name="adorner" />
</Grid>
</Border>
<Grid Width="10"/>
<Popup Name="popup" AllowsTransparency="True" Placement="Right">
<Border x:Name="errorBorder" Background="#ffdc000c" Opacity="0" MinHeight="30" >
<TextBlock Margin="5,0" Text="{Binding ElementName=adorner,
Path=AdornedElement.(Validation.Errors).CurrentItem.ErrorContent}"
Foreground="White" TextWrapping="Wrap" VerticalAlignment="Center"/>
</Border>
</Popup>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
第五步:创建View。核心代码是“ValidatesOnDataErrors=True”
// 前台绑定
TextBox Text="{Binding Path=Age,UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}" />
// 后台提交验证
private void btnSubmit_Click(object sender, RoutedEventArgs e)
{
if (stu.IsValidated())
{
MessageBox.Show("Verificate successfully.");
}
else
{
MessageBox.Show("Please input correct data in red textbox", "Warning", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
总结
通过以上5个步骤,使用IDataErrorInfo来验证对象的数据有效性,对于无效的属性及时通知UI界面。其中用到了反射技术和特性,节约了大量的代码量,但是会有性能损失。至此WPF的TextBox输入验证系列的文章已经完工,谢谢大家的阅读。
文章评论
很强