【WPF 数据验证机制】三、INotifyDataErrorInfo接口+DataAnnotation数据特性实现model属性验证机制
环境
vs2022+.net6.0+wpf+MVVM+EFcore6.0
MVVM验证示意图
INotifyDataErrorInfo接口功能
public interface INotifyDataErrorInfo { bool HasErrors { get; }//提供给Validation 使用,对应Validation.HasError event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; IEnumerable GetErrors(string propertyName);//获取一组验证信息, 多个 ValidationAttribute验证后返回来的信息,提供给Validation 使用,对应Validation.ErrorsBing会在绑定的控件上生成 附加属性Validation.Errors,保存验证信息。 }
1、异步验证
2、跨属性验证
3、返回一组(IEnumerable)错误信息,可以分行显示或者连接成一组字符窜显示。以下案例分别展示这两种方式。
实现方式
1、反射 :实现Model实体映射和数据验证分离。
2、Validator验证器:Model实体映射和数据验证混合在一起
3、以上两者都可以实现异步验证。
Bing在UI绑定中作用
1、属性验证,并且记录结果。
2、Bing会在绑定的控件上生成 附加属性Validation.Errors,保存验证信息。
反射实现属性验证
目的:实现实体映射和数据验证分离。
具体步骤:
1、新建(或自动生成)Student类,这个类不做修改,保存原样。该类主要实现实体映射或者知识单纯的PoCo。
2、新建Student_Matedata的元数类,将Student_Matedata类附加Student类的元数据。Student_Matedata的元数类只有ValidationAttribute 特性。
3、新建StudentViewModel,该类实现INotifyDataErrorInfo接口,实现验证功能。
4、UI层通过Bing绑定控件,Bing会在绑定的控件上生成Validation.Errors附加属性,保存验证信息。
Model
Student类 、Student_Matedata类。
1、新建(或自动生成)Student类,这个类不做修改,保存原样。该类主要实现实体映射或者知识单纯的PoCo。
2、新建Student_Matedata的元数类,将Student_Matedata类附加Student类的元数据。Student_Matedata的元数类只有ValidationAttribute 特性。
Student类
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text; using System.Threading.Tasks; using CommunityToolkit.Mvvm; using CommunityToolkit.Mvvm.ComponentModel; namespace CTMvvmDemo.MVVM.Models { [MetadataType(typeof(Student_MateData))] [Table("Students")] public sealed partial class Student:ObservableValidator { [Key]//主键 [DatabaseGeneratedAttribute(DatabaseGeneratedOption.None)]//非自增长,自增长为Identity public int StudentID { get; set; } [Column("name")] [DataType(DataType.Text)] public string Name { get; set; } [Column("age")] public int Age { get; set; } [Column("tel")] [DataType(DataType.PhoneNumber)] public string Tel { get; set; } [Column("email")] [DataType(DataType.EmailAddress)] public string Email { get; set; } } }
Student_Matedata类
using CommunityToolkit.Mvvm.ComponentModel; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.ComponentModel.DataAnnotations; namespace CTMvvmDemo.MVVM.Models { public sealed class Student_MateData : ObservableValidator { [Required(ErrorMessage = "名字不能为空")] [StringLength(20, ErrorMessage = "名字超过20个字符了")] [RegularExpression("^[\u4e00-\u9fa5|a-z]+$", ErrorMessage = "请输入汉族或者小写拼英")] public string Name { get; set; } [Range(0, 200, ErrorMessage = "年龄输入不正确")] public int Age { get; set; } [RegularExpression(@"^1[34578]\d{9}$", ErrorMessage = "电话号码不正确")] [Required(ErrorMessage = "不能为空")] [DataType(DataType.PhoneNumber)] public string Tel { get; set; } [RegularExpression("^[\\w-]+@[\\w-]+\\.(com|net|org|edu|mil|tv|biz|info)$", ErrorMessage = "邮箱不正确")] [Required(ErrorMessage = "不能为空")] [DataType(DataType.EmailAddress)] public string Email { get; set; } } }
ViewModel
新建StudentViewModel,该类实现INotifyDataErrorInfo接口,实现验证功能。
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CTMvvmDemo.MVVM.Models; using CTMvvmDemo.Respositoies; namespace CTMvvmDemo.MVVM.ViewsModels { public sealed partial class StudentViewsModel : ViewModelBase, INotifyDataErrorInfo { private Student student; public StudentViewsModel(Student _student) => this.student = _student; public string Name { get => student.Name; set { //先设置后验证,暂时保存本地,除非sava实体后才会更新数据库 SetProperty(student.Name, value, student, (student, name) => student.Name = name); ValidateProperty2(student.Name, nameof(Name)); //ValidateProperty3(student, nameof(Name)); } } public int Age { get => student.Age; set { //先设置后验证,暂时保存本地,除非sava实体后才会更新数据库 SetProperty(student.Age, value, student, (u, n) => u.Age = n); ValidateProperty2(student.Age, nameof(Age)); // ValidateProperty3(student, nameof(Age)); } } public int StudentID { get => student.StudentID; set { SetProperty(student.StudentID, value, student, (u, n) => u.StudentID = n); ValidateProperty2(student.StudentID, nameof(StudentID)); // ValidateProperty3(student, nameof(StudentID)); } } public string Tel { get => student.Tel; set { SetProperty(student.Tel, value, student, (u, n) => u.Tel = n); ValidateProperty2(student.Tel, nameof(Tel)); // ValidateProperty3(student, nameof(Tel)); } } public string Email { get => student.Email; set { SetProperty(student.Email, value, student, (u, n) => u.Email = n); ValidateProperty2(student.Email, nameof(Email)); // ValidateProperty3(student, nameof(Email)); } } private readonly Dictionary<string, ICollection<string>> _validationErrors = new Dictionary<string, ICollection<string>>(); public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged; /// <summary> /// 判断是否验证 /// </summary> public bool HasErrors => _validationErrors is not null && _validationErrors.Count > 0; /// <summary> /// /// </summary> /// <param name="propertyName"></param> private void RaiseErrorsChanged(string propertyName) { if (ErrorsChanged != null) ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName)); } /// <summary> /// 向Bing提供错误信息。 /// </summary> /// <param name="propertyName"></param> /// <returns></returns> /// <exception cref="NotImplementedException"></exception> public IEnumerable GetErrors(string? propertyName) { return _validationErrors.ContainsKey(propertyName) ? _validationErrors[propertyName] : null; } /// <summary> /// 方法2 /// 用Validator 实现验证具体步骤 /// 1、获取model 实体中属性属性的数据特性。 /// 2、创建包含model实例的和属性 ValidationContext /// 3、用Validator.TryValidateProperty方法验证,并且记录验证结果 /// 4、将验证结果保存在字典中,该字典提供给GetErrors()使用,GetErrors()是向bing提供验证结果 /// 验证属性 验证结果保存在_validationErrors中,GetErrors会提取相应错误信息 /// 该方法要求 数据特性添加在model属性上,不能添加在model的数据中。 /// </summary> /// <param name="val">属性值 例如:student.Name</param> /// <param name="propertyName">属性名</param> private bool ValidateProperty2(object val, [CallerMemberName] string propertyName = null) { if (_validationErrors.ContainsKey(propertyName)) _validationErrors.Remove(propertyName); ValidationContext context = new ValidationContext(student) { MemberName = propertyName }; List<ValidationResult> results = new(); bool isvild = Validator.TryValidateProperty(val, context, results); if (!isvild) { _validationErrors[propertyName] = results.Select(x => x.ErrorMessage).ToList(); } ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); return !isvild; } protected override void Dispose(bool IsDisposed) { student = null; _validationErrors.Clear(); } } }
View
UI层通过Bing绑定控件,Bing会在绑定的控件上生成Validation.Errors附加属性,保存验证信息。
<UserControl x:Class="CTMvvmDemo.MVVM.Views.StudentView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:CTMvvmDemo.MVVM.Views" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <UserControl.Resources> <ControlTemplate x:Key="displayErrorMsgFomatte"> <WrapPanel> <!-- Placeholder for the TextBox itself --> <AdornedElementPlaceholder x:Name="textBox"/> <ItemsControl ItemsSource="{Binding}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </WrapPanel> </ControlTemplate> </UserControl.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="100"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">学号:</TextBlock> <TextBox Grid.Row="0" Height="40" Padding="10,0" Margin="0,10" Grid.Column="1" MinWidth="200" VerticalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalAlignment="Left" Validation.ErrorTemplate="{StaticResource displayErrorMsgFomatte}" Text="{Binding StudentID ,UpdateSourceTrigger=PropertyChanged }" /> <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">学生名字:</TextBlock> <TextBox Grid.Row="1" Height="40" Padding="10,0" Grid.Column="1" MinWidth="200" VerticalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalAlignment="Left" Validation.ErrorTemplate="{StaticResource displayErrorMsgFomatte}" Text="{Binding Name ,UpdateSourceTrigger=PropertyChanged }" /> <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">年龄:</TextBlock> <TextBox Grid.Row="2" Margin="0,10,0,0" Height="40" Padding="10,0" Grid.Column="1" MinWidth="200" VerticalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalAlignment="Left" Validation.ErrorTemplate="{StaticResource displayErrorMsgFomatte}" Text="{Binding Age ,UpdateSourceTrigger=PropertyChanged }" /> <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">Tel:</TextBlock> <TextBox Grid.Row="3" Margin="0,10,0,0" Height="40" Padding="10,0" Grid.Column="1" MinWidth="200" VerticalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalAlignment="Left" Validation.ErrorTemplate="{StaticResource displayErrorMsgFomatte}" Text="{Binding Tel ,UpdateSourceTrigger=PropertyChanged }" /> <TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">Email:</TextBlock> <TextBox Grid.Row="4" Margin="0,10,0,0" Height="40" Padding="10,0" Grid.Column="1" MinWidth="200" VerticalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalAlignment="Left" Validation.ErrorTemplate="{StaticResource displayErrorMsgFomatte}" Text="{Binding Email ,UpdateSourceTrigger=PropertyChanged }" /> </Grid> </UserControl>
效果
Validator验证器实现属性验证
Model
新建Student实体,该实体有数据验证和实体映射混合, 混乱 容易搞错。
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text; using System.Threading.Tasks; using CommunityToolkit.Mvvm; using CommunityToolkit.Mvvm.ComponentModel; namespace CTMvvmDemo.MVVM.Models { [MetadataType(typeof(Student_MateData))] [Table("Students")] public sealed partial class Student:ObservableValidator { [Key]//主键 [DatabaseGeneratedAttribute(DatabaseGeneratedOption.None)]//非自增长,自增长为Identity public int StudentID { get; set; } [Column("name")] [DataType(DataType.Text)] [Required(ErrorMessage = "名字不能为空")] [StringLength(20, ErrorMessage = "名字超过20个字符了")] [RegularExpression("^[\u4e00-\u9fa5|a-z]+$", ErrorMessage = "请输入汉族或者小写拼英")] public string Name { get; set; } [Column("age")] [Range(0, 200, ErrorMessage = "年龄输入不正确")] public int Age { get; set; } [Column("tel")] [RegularExpression(@"^1[34578]\d{9}$", ErrorMessage = "电话号码不正确")] [Required(ErrorMessage = "不能为空")] [DataType(DataType.PhoneNumber)] public string Tel { get; set; } [Column("email")] [RegularExpression("^[\\w-]+@[\\w-]+\\.(com|net|org|edu|mil|tv|biz|info)$", ErrorMessage = "邮箱不正确")] [Required(ErrorMessage ="不能为空")] [DataType(DataType.EmailAddress)] public string Email { get; set; } } }
ViewModel
/// 用Validator 实现验证具体步骤
/// 1、获取model 实体中属性属性的数据特性。
/// 2、创建包含model实例的和属性 ValidationContext
/// 3、用Validator.TryValidateProperty方法验证,并且记录验证结果
/// 4、将验证结果保存在字典中,该字典提供给GetErrors()使用,GetErrors()是向bing提供验证结果
/// 验证属性 验证结果保存在_validationErrors中,GetErrors会提取相应错误信息
/// 该方法要求 数据特性添加在model属性上,不能添加在model的数据中。
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CTMvvmDemo.MVVM.Models; using CTMvvmDemo.Respositoies; namespace CTMvvmDemo.MVVM.ViewsModels { public sealed partial class StudentViewsModel : ViewModelBase, INotifyDataErrorInfo { private Student student; public StudentViewsModel(Student _student) => this.student = _student; public string Name { get => student.Name; set { //先设置后验证,暂时保存本地,除非sava实体后才会更新数据库 SetProperty(student.Name, value, student, (student, name) => student.Name = name); // ValidateProperty2(student.Name, nameof(Name)); ValidateProperty3(student, nameof(Name)); } } public int Age { get => student.Age; set { //先设置后验证,暂时保存本地,除非sava实体后才会更新数据库 SetProperty(student.Age, value, student, (u, n) => u.Age = n); // ValidateProperty2(student.Age, nameof(Age)); ValidateProperty3(student, nameof(Age)); } } public int StudentID { get => student.StudentID; set { SetProperty(student.StudentID, value, student, (u, n) => u.StudentID = n); // ValidateProperty2(student.StudentID, nameof(StudentID)); ValidateProperty3(student, nameof(StudentID)); } } public string Tel { get => student.Tel; set { SetProperty(student.Tel, value, student, (u, n) => u.Tel = n); // ValidateProperty2(student.Tel, nameof(Tel)); ValidateProperty3(student, nameof(Tel)); } } public string Email { get => student.Email; set { SetProperty(student.Email, value, student, (u, n) => u.Email = n); // ValidateProperty2(student.Email, nameof(Email)); ValidateProperty3(student, nameof(Email)); } } private readonly Dictionary<string, ICollection<string>> _validationErrors = new Dictionary<string, ICollection<string>>(); public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged; /// <summary> /// 判断是否验证 /// </summary> public bool HasErrors => _validationErrors is not null && _validationErrors.Count > 0; /// <summary> /// /// </summary> /// <param name="propertyName"></param> private void RaiseErrorsChanged(string propertyName) { if (ErrorsChanged != null) ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName)); } /// <summary> /// 向Bing提供错误信息。 /// </summary> /// <param name="propertyName"></param> /// <returns></returns> /// <exception cref="NotImplementedException"></exception> public IEnumerable GetErrors(string? propertyName) { return _validationErrors.ContainsKey(propertyName) ? _validationErrors[propertyName] : null; } /// <summary> /// 方法3 /// 用反射实现验证 具体步骤 /// 1、获取model类上的元数据,该元数据是独立的类通过特性附加在model上。该元数据保存着model属性的数据特性 /// 2、运行ValidationAttribute的IsValid()方法验证特性,并且返回验证信息。 /// 3、将信息保存在字典中, 该字典提供给GetErrors()使用,GetErrors()是向bing提供验证结果 /// 获取model类的上的 MetadataType 特性 中的数据特性 用于验证,这样可以实现model和model数据验证分离。 /// /// </summary> /// <param name="obj"></param> /// <param name="propertyName"></param> public void ValidateProperty3(object obj, string propertyName) { // 获取model类的上的 MetadataType 特性 , GetCustomAttributes(true)表示 是否搜索该成员的继承链以查找这些特性 Type metadatatype = obj.GetType().GetCustomAttributes(true).OfType<MetadataTypeAttribute>().First().MetadataClassType; // 在元数据上获取指定属性propertyName的元数据,如果不存该属性就return,如果存在就获取值。 PropertyInfo property = metadatatype.GetProperty(propertyName); if (property is null) return; // 在实例上获取指定属性propertyName的属性值 object value = obj.GetType().GetProperty(propertyName).GetValue(obj, null); // 运行属性上附加特性的IsValid(IsValid) 进行验证,并且返回验证信息 v.ErrorMessage List<string> errors = (from v in property.GetCustomAttributes(true).OfType<ValidationAttribute>() where !v.IsValid(value) select v.ErrorMessage).ToList(); List<string> vs = new(); vs.Add(String.Join(",", errors)); //将指定属性 错误信息保存到字典中,该字典提供给GetErrors()使用,GetErrors()是向bing提供验证结果。 _validationErrors[propertyName] = (errors.Count > 0) ? vs : null; } protected override void Dispose(bool IsDisposed) { student = null; _validationErrors.Clear(); } } }
View
将保存在bing控件的附加属性中的验证信息显示出来。
<UserControl x:Class="CTMvvmDemo.MVVM.Views.StudentView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:CTMvvmDemo.MVVM.Views" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <UserControl.Resources> <ControlTemplate x:Key="displayErrorMsgFomatte"> <WrapPanel> <!-- Placeholder for the TextBox itself --> <AdornedElementPlaceholder x:Name="textBox"/> <ItemsControl ItemsSource="{Binding}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </WrapPanel> </ControlTemplate> </UserControl.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="100"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">学号:</TextBlock> <TextBox Grid.Row="0" Height="40" Padding="10,0" Margin="0,10" Grid.Column="1" MinWidth="200" VerticalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalAlignment="Left" Validation.ErrorTemplate="{StaticResource displayErrorMsgFomatte}" Text="{Binding StudentID ,UpdateSourceTrigger=PropertyChanged }" /> <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">学生名字:</TextBlock> <TextBox Grid.Row="1" Height="40" Padding="10,0" Grid.Column="1" MinWidth="200" VerticalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalAlignment="Left" Validation.ErrorTemplate="{StaticResource displayErrorMsgFomatte}" Text="{Binding Name ,UpdateSourceTrigger=PropertyChanged }" /> <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">年龄:</TextBlock> <TextBox Grid.Row="2" Margin="0,10,0,0" Height="40" Padding="10,0" Grid.Column="1" MinWidth="200" VerticalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalAlignment="Left" Validation.ErrorTemplate="{StaticResource displayErrorMsgFomatte}" Text="{Binding Age ,UpdateSourceTrigger=PropertyChanged }" /> <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">Tel:</TextBlock> <TextBox Grid.Row="3" Margin="0,10,0,0" Height="40" Padding="10,0" Grid.Column="1" MinWidth="200" VerticalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalAlignment="Left" Validation.ErrorTemplate="{StaticResource displayErrorMsgFomatte}" Text="{Binding Tel ,UpdateSourceTrigger=PropertyChanged }" /> <TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">Email:</TextBlock> <TextBox Grid.Row="4" Margin="0,10,0,0" Height="40" Padding="10,0" Grid.Column="1" MinWidth="200" VerticalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalAlignment="Left" Validation.ErrorTemplate="{StaticResource displayErrorMsgFomatte}" Text="{Binding Email ,UpdateSourceTrigger=PropertyChanged }" /> </Grid> </UserControl>
效果
异步验证
反射和验证器方法都是可以实现异步验证,这边拿ValidateProperty2()方法做demo,其他代码和反射验证一样。
private async void ValidateProperty2(object val, [CallerMemberName] string propertyName = null) { bool allowvalid = await Task.Run<bool>(() => { Thread.Sleep(2000);return true; }); if (_validationErrors.ContainsKey(propertyName)) _validationErrors.Remove(propertyName); ValidationContext context = new ValidationContext(student) { MemberName = propertyName }; List<ValidationResult> results = new(); bool isvild = Validator.TryValidateProperty(val, context, results); if (!isvild) { _validationErrors[propertyName] = results.Select(x => x.ErrorMessage).ToList(); } ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); }