在介绍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; 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输入验证系列的文章已经完工,谢谢大家的阅读。
文章评论
很强