利刃 MVVMLight 5:绑定在表单验证上的应用
表单验证是MVVM体系中的重要一块。而绑定除了推动 Model-View-ViewModel (MVVM) 模式松散耦合 逻辑、数据 和 UI定义 的关系之外,还为业务数据验证方案提供强大而灵活的支持。
WPF 中的数据绑定机制包括多个选项,可用于在创建可编辑视图时校验输入数据的有效性。
常见的表单验证机制有如下几种:
验证类型 | 说明 |
Exception 验证 | 通过在某个 Binding 对象上设置 ValidatesOnExceptions 属性,如果源对象属性设置已修改的值的过程中引发异常,则抛出错误并为该 Binding 设置验证错误。 |
ValidationRule 验证 |
Binding 类具有一个用于提供 ValidationRule 派生类实例的集合的属性。这些 ValidationRules 需要覆盖某个 Validate 方法,该方法由 Binding 在每次绑定控件中的数据发生更改时进行调用。 如果 Validate 方法返回无效的 ValidationResult 对象,则将为该 Binding 设置验证错误。 |
IDataErrorInfo 验证 |
通过在绑定数据源对象上实现 IDataErrorInfo 接口并在 Binding 对象上设置 ValidatesOnDataErrors 属性,Binding 将调用从绑定数据源对象公开的 IDataErrorInfo API。 如果从这些属性调用返回非 null 或非空字符串,则将为该 Binding 设置验证错误。 |
验证交互的关系模式如图:
我们在使用 WPF 中的数据绑定来呈现业务数据时,通常会使用 Binding 对象在目标控件的单个属性与数据源对象属性之间提供数据管道。
如果要使得绑定验证有效,首先需要进行 TwoWay 数据绑定。这表明,除了从源属性流向目标属性以进行显示的数据之外,编辑过的数据也会从目标流向源。
这就是伟大的双向数据绑定的精髓,所以在MVVM中做数据校验,会容易的多。
当 TwoWay 数据绑定中输入或修改数据时,将启动以下工作流:
1、 | 用户通过键盘、鼠标、手写板或者其他输入设备来输入或修改数据,从而改变绑定的目标信息 |
2、 | 设置源属性值。 |
3、 | 触发 Binding.SourceUpdated 事件。 |
4、 | 如果数据源属性上的 setter 引发异常,则异常会由 Binding 捕获,并可用于指示验证错误。 |
5、 | 如果实现了 IDataErrorInfo 接口,则会对数据源对象调用该接口的方法获得该属性的错误信息。 |
6、 | 向用户呈现验证错误指示,并触发 Validation.Error 附加事件。 |
绑定目标向绑定源发送数据更新的请求,而绑定源则对数据进行验证,并根据不同的验证机制进行反馈。
下面我们用实例来对比下这几种验证机制,在此之前,我们先做一个事情,就是写一个错误触发的样式,来保证错误触发的时候直接清晰的向用户反馈出去。
我们新建一个资源字典文件,命名为TextBox.xaml,下面这个是资源字典文件的内容,目标类型是TextBoxBase基础的控件,如TextBox和RichTextBox.
代码比较简单,注意标红的内容,设计一个红底白字的提示框,当源属性触发错误验证的时候,把验证对象集合中的错误内容显示出来。
1 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 2 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> 3 4 <Style x:Key="{x:Type TextBoxBase}" TargetType="{x:Type TextBoxBase}" BasedOn="{x:Null}"> 5 <Setter Property="BorderThickness" Value="1"/> 6 <Setter Property="Padding" Value="2,1,1,1"/> 7 <Setter Property="AllowDrop" Value="true"/> 8 <Setter Property="FocusVisualStyle" Value="{x:Null}"/> 9 <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/> 10 <Setter Property="Stylus.IsFlicksEnabled" Value="False"/> 11 <Setter Property="SelectionBrush" Value="{DynamicResource Accent}" /> 12 <Setter Property="Validation.ErrorTemplate"> 13 <Setter.Value> 14 <ControlTemplate> 15 <StackPanel Orientation="Horizontal"> 16 <Border BorderThickness="1" BorderBrush="#FFdc000c" VerticalAlignment="Top"> 17 <Grid> 18 <AdornedElementPlaceholder x:Name="adorner" Margin="-1"/> 19 </Grid> 20 </Border> 21 <Border x:Name="errorBorder" Background="#FFdc000c" Margin="8,0,0,0" 22 Opacity="0" CornerRadius="0" 23 IsHitTestVisible="False" 24 MinHeight="24" > 25 <TextBlock Text="{Binding ElementName=adorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" 26 Foreground="White" Margin="8,2,8,3" TextWrapping="Wrap" VerticalAlignment="Center"/> 27 </Border> 28 </StackPanel> 29 <ControlTemplate.Triggers> 30 <DataTrigger Value="True"> 31 <DataTrigger.Binding> 32 <Binding ElementName="adorner" Path="AdornedElement.IsKeyboardFocused" /> 33 </DataTrigger.Binding> 34 <DataTrigger.EnterActions> 35 <BeginStoryboard x:Name="fadeInStoryboard"> 36 <Storyboard> 37 <DoubleAnimation Duration="00:00:00.15" 38 Storyboard.TargetName="errorBorder" 39 Storyboard.TargetProperty="Opacity" 40 To="1"/> 41 </Storyboard> 42 </BeginStoryboard> 43 </DataTrigger.EnterActions> 44 <DataTrigger.ExitActions> 45 <StopStoryboard BeginStoryboardName="fadeInStoryboard"/> 46 <BeginStoryboard x:Name="fadeOutStoryBoard"> 47 <Storyboard> 48 <DoubleAnimation Duration="00:00:00" 49 Storyboard.TargetName="errorBorder" 50 Storyboard.TargetProperty="Opacity" 51 To="0"/> 52 </Storyboard> 53 </BeginStoryboard> 54 </DataTrigger.ExitActions> 55 </DataTrigger> 56 </ControlTemplate.Triggers> 57 </ControlTemplate> 58 </Setter.Value> 59 </Setter> 60 <Setter Property="Template"> 61 <Setter.Value> 62 <ControlTemplate TargetType="{x:Type TextBoxBase}"> 63 <Border x:Name="Bd" 64 BorderThickness="{TemplateBinding BorderThickness}" 65 BorderBrush="{TemplateBinding BorderBrush}" 66 Background="{TemplateBinding Background}" 67 Padding="{TemplateBinding Padding}" 68 SnapsToDevicePixels="true"> 69 <ScrollViewer x:Name="PART_ContentHost" RenderOptions.ClearTypeHint="Enabled" 70 SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> 71 </Border> 72 <ControlTemplate.Triggers> 73 <Trigger Property="IsEnabled" Value="false"> 74 <Setter Property="Foreground" Value="{DynamicResource InputTextDisabled}"/> 75 </Trigger> 76 <Trigger Property="IsReadOnly" Value="true"> 77 <Setter Property="Foreground" Value="{DynamicResource InputTextDisabled}"/> 78 </Trigger> 79 <Trigger Property="IsFocused" Value="true"> 80 <Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Accent}" /> 81 </Trigger> 82 <MultiTrigger> 83 <MultiTrigger.Conditions> 84 <Condition Property="IsReadOnly" Value="False"/> 85 <Condition Property="IsEnabled" Value="True"/> 86 <Condition Property="IsMouseOver" Value="True"/> 87 </MultiTrigger.Conditions> 88 <Setter Property="Background" Value="{DynamicResource InputBackgroundHover}"/> 89 <Setter Property="BorderBrush" Value="{DynamicResource InputBorderHover}"/> 90 <Setter Property="Foreground" Value="{DynamicResource InputTextHover}"/> 91 </MultiTrigger> 92 </ControlTemplate.Triggers> 93 </ControlTemplate> 94 </Setter.Value> 95 </Setter> 96 </Style> 97 <Style BasedOn="{StaticResource {x:Type TextBoxBase}}" TargetType="{x:Type TextBox}"> 98 </Style> 99 <Style BasedOn="{StaticResource {x:Type TextBoxBase}}" TargetType="{x:Type RichTextBox}"> 100 </Style> 101 102 </ResourceDictionary>
然后在App.Xaml中全局注册到整个应用中。
1 <Application x:Class="MVVMLightDemo.App" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 StartupUri="View/BindingFormView.xaml" 5 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 6 d1p1:Ignorable="d" 7 xmlns:d1p1="http://schemas.openxmlformats.org/markup-compatibility/2006" 8 xmlns:vm="clr-namespace:MVVMLightDemo.ViewModel" 9 xmlns:Common="clr-namespace:MVVMLightDemo.Common"> 10 <Application.Resources> 11 <ResourceDictionary> 12 <ResourceDictionary.MergedDictionaries> 13 <ResourceDictionary Source="/MVVMLightDemo;component/Assets/TextBox.xaml" /> 14 </ResourceDictionary.MergedDictionaries> 15 <vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" /> 16 <Common:IntegerToSex x:Key="IntegerToSex" d:IsDataSource="True" /> 17 </ResourceDictionary> 18 </Application.Resources> 19 </Application>
达到的效果如下:
下面详细描述下这三种验证模式
1、Exception 验证:
正如说明中描述的那样,在具有绑定关系的源字段模型上做验证异常的引发并抛出,在View中的Xaml对象上设置 ExceptionValidationRule 属性,响应捕获异常并显示。
View代码:
1 <GroupBox Header="Exception 验证" Margin="10 10 10 10" DataContext="{Binding Source={StaticResource Locator},Path=ValidateException}" > 2 <StackPanel x:Name="ExceptionPanel" Orientation="Vertical" Margin="0,10,0,0" > 3 <StackPanel> 4 <Label Content="用户名" Target="{Binding ElementName=UserNameEx}"/> 5 <TextBox x:Name="UserNameEx" Width="150"> 6 <TextBox.Text> 7 <Binding Path="UserNameEx" UpdateSourceTrigger="PropertyChanged"> 8 <Binding.ValidationRules> 9 <ExceptionValidationRule></ExceptionValidationRule> 10 </Binding.ValidationRules> 11 </Binding> 12 </TextBox.Text> 13 </TextBox> 14 </StackPanel> 15 </StackPanel> 16 </GroupBox>
ViewModel代码:
1 /// <summary> 2 /// Exception 验证 3 /// </summary> 4 public class ValidateExceptionViewModel:ViewModelBase 5 { 6 public ValidateExceptionViewModel() 7 { 8 9 } 10 11 private String userNameEx; 12 /// <summary> 13 /// 用户名称(不为空) 14 /// </summary> 15 public string UserNameEx 16 { 17 get 18 { 19 return userNameEx; 20 } 21 set 22 { 23 userNameEx = value; 24 RaisePropertyChanged(() => UserNameEx); 25 if (string.IsNullOrEmpty(value)) 26 { 27 throw new ApplicationException("该字段不能为空!"); 28 } 29 } 30 } 31 }
结果如图:
将验证失败的信息直接抛出来,这无疑是最简单粗暴的,实现也很简单,但是只是针对单一源属性进行验证, 复用性不高。
而且在组合验证(比如同时需要验证非空和其他规则)情况下,会导致Model中写过重过臃肿的代码。
2、ValidationRule 验证:
通过继承ValidationRule 抽象类,并重写他的Validate方法来扩展编写我们需要的验证类。该验证类可以直接使用在我们需要验证的属性。
View代码:
1 <GroupBox Header="ValidationRule 验证" Margin="10 20 10 10" DataContext="{Binding Source={StaticResource Locator},Path=ValidationRule}" > 2 <StackPanel x:Name="ValidationRulePanel" Orientation="Vertical" Margin="0,20,0,0"> 3 <StackPanel> 4 <Label Content="用户名" Target="{Binding ElementName=UserName}"/> 5 <TextBox Width="150" > 6 <TextBox.Text> 7 <Binding Path="UserName" UpdateSourceTrigger="PropertyChanged"> 8 <Binding.ValidationRules> 9 <app:RequiredRule /> 10 </Binding.ValidationRules> 11 </Binding> 12 </TextBox.Text> 13 </TextBox> 14 </StackPanel> 15 16 <StackPanel> 17 <Label Content="用户邮箱" Target="{Binding ElementName=UserEmail}"/> 18 <TextBox Width="150"> 19 <TextBox.Text> 20 <Binding Path="UserEmail" UpdateSourceTrigger="PropertyChanged"> 21 <Binding.ValidationRules> 22 <app:EmailRule /> 23 </Binding.ValidationRules> 24 </Binding> 25 </TextBox.Text> 26 </TextBox> 27 </StackPanel> 28 </StackPanel> 29 </GroupBox>
重写两个ValidationRule,代码如下:
1 public class RequiredRule : ValidationRule 2 { 3 public override ValidationResult Validate(object value, CultureInfo cultureInfo) 4 { 5 if (value == null) 6 return new ValidationResult(false, "该字段不能为空值!"); 7 if (string.IsNullOrEmpty(value.ToString())) 8 return new ValidationResult(false, "该字段不能为空字符串!"); 9 return new ValidationResult(true, null); 10 } 11 } 12 13 public class EmailRule : ValidationRule 14 { 15 public override ValidationResult Validate(object value, CultureInfo cultureInfo) 16 { 17 Regex emailReg = new Regex("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$"); 18 19 if (!String.IsNullOrEmpty(value.ToString())) 20 { 21 if (!emailReg.IsMatch(value.ToString())) 22 { 23 return new ValidationResult(false, "邮箱地址不准确!"); 24 } 25 } 26 return new ValidationResult(true, null); 27 } 28 }
创建了两个类,一个用于验证是否为空,一个用于验证是否符合邮箱地址标准格式。
ViewModel代码:
1 public class ValidationRuleViewModel:ViewModelBase 2 { 3 public ValidationRuleViewModel() 4 { 5 6 } 7 8 #region 属性 9 10 private String userName; 11 /// <summary> 12 /// 用户名 13 /// </summary> 14 public String UserName 15 { 16 get { return userName; } 17 set { userName = value; RaisePropertyChanged(()=>UserName); } 18 } 19 20 21 22 private String userEmail; 23 /// <summary> 24 /// 用户邮件 25 /// </summary> 26 public String UserEmail 27 { 28 get { return userEmail; } 29 set { userEmail = value;RaisePropertyChanged(()=>UserName); } 30 } 31 32 #endregion
结果如下:
说明:相对来说,这种方式是比较不错的,独立性、复用性都很好,从松散耦合角度来说也是比较恰当的。
可以预先写好一系列的验证规则类,视图编码人员可以根据需求直接使用这些验证规则,服务端无需额外的处理。
但是仍然有缺点,扩展性差,如果需要个性化反馈消息也需要额外扩展。不符合日益丰富的前端验证需求。
3、IDataErrorInfo 验证:
3.1、在绑定数据源对象上实现 IDataErrorInfo 接口
3.2、在 Binding 对象上设置 ValidatesOnDataErrors 属性
Binding 将调用从绑定数据源对象公开的 IDataErrorInfo API。如果从这些属性调用返回非 null 或非空字符串,则将为该 Binding 设置验证错误。
View代码:
1 <GroupBox Header="IDataErrorInfo 验证" Margin="10 20 10 10" DataContext="{Binding Source={StaticResource Locator},Path=BindingForm}" > 2 <StackPanel x:Name="Form" Orientation="Vertical" Margin="0,20,0,0"> 3 <StackPanel> 4 <Label Content="用户名" Target="{Binding ElementName=UserName}"/> 5 <TextBox Width="150" 6 Text="{Binding UserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" > 7 </TextBox> 8 </StackPanel> 9 10 <StackPanel> 11 <Label Content="性别" Target="{Binding ElementName=RadioGendeMale}"/> 12 <RadioButton Content="男" /> 13 <RadioButton Content="女" Margin="8,0,0,0" /> 14 </StackPanel> 15 <StackPanel> 16 <Label Content="生日" Target="{Binding ElementName=DateBirth}" /> 17 <DatePicker x:Name="DateBirth" /> 18 </StackPanel> 19 <StackPanel> 20 <Label Content="用户邮箱" Target="{Binding ElementName=UserEmail}"/> 21 <TextBox Width="150" Text="{Binding UserEmail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" /> 22 </StackPanel> 23 <StackPanel> 24 <Label Content="用户电话" Target="{Binding ElementName=UserPhone}"/> 25 <TextBox Width="150" Text="{Binding UserPhone, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" /> 26 </StackPanel> 27 </StackPanel> 28 </GroupBox>
ViewModel代码:
1 public class BindingFormViewModel :ViewModelBase, IDataErrorInfo 2 { 3 public BindingFormViewModel() 4 { 5 6 } 7 8 #region 属性 9 10 private String userName; 11 /// <summary> 12 /// 用户名 13 /// </summary> 14 public String UserName 15 { 16 get { return userName; } 17 set { userName = value; } 18 } 19 20 21 22 private String userPhone; 23 /// <summary> 24 /// 用户电话 25 /// </summary> 26 public String UserPhone 27 { 28 get { return userPhone; } 29 set { userPhone = value; } 30 } 31 32 33 34 private String userEmail; 35 /// <summary> 36 /// 用户邮件 37 /// </summary> 38 public String UserEmail 39 { 40 get { return userEmail; } 41 set { userEmail = value; } 42 } 43 #endregion 44 45 public String Error 46 { 47 get { return null; } 48 } 49 50 public String this[string columnName] 51 { 52 get 53 { 54 Regex digitalReg = new Regex(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$"); 55 Regex emailReg = new Regex("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$"); 56 57 58 if (columnName == "UserName" && String.IsNullOrEmpty(this.UserName)) 59 { 60 return "用户名不能为空"; 61 } 62 63 if (columnName == "UserPhone" && !String.IsNullOrEmpty(this.UserPhone)) 64 { 65 if (!digitalReg.IsMatch(this.UserPhone.ToString())) 66 { 67 return "用户电话必须为8-11位的数值!"; 68 } 69 } 70 71 if (columnName == "UserEmail" && !String.IsNullOrEmpty(this.UserEmail)) 72 { 73 if (!emailReg.IsMatch(this.UserEmail.ToString())) 74 { 75 return "用户邮箱地址不正确!"; 76 } 77 } 78 79 return null; 80 } 81 } 82 83 }
继承IDataErrorInfo接口后,实现方法两个属性:Error 属性用于指示整个对象的错误,而索引器用于指示单个属性级别的错误。
每次的属性值发生变化,则索引器进行一次检查,看是否有验证错误的信息返回。
两者的工作原理相同:如果返回非 null 或非空字符串,则表示存在验证错误。否则,返回的字符串用于向用户显示错误。
结果如图:
利用 IDataErrorInfo 的好处是它可用于轻松地处理交叉耦合属性。但也具有一个很大的弊端:
索引器的实现通常会导致较大的 switch-case 语句(对象中的每个属性名称都对应于一种情况),
必须基于字符串进行切换和匹配,并返回指示错误的字符串。而且,在对象上设置属性值之前,不会调用 IDataErrorInfo 的实现。
为了避免出现大量的 switch-case,并且将校验逻辑进行分离提高代码复用,将验证规则和验证信息独立化于于每个模型对象中, 使用DataAnnotations 无疑是最好的的方案 。
所以我们进行改良一下:
View代码,跟上面那个一样:
1 <GroupBox Header="IDataErrorInfo+ 验证" Margin="10 20 10 10" DataContext="{Binding Source={StaticResource Locator},Path=BindDataAnnotations}" > 2 <StackPanel Orientation="Vertical" Margin="0,20,0,0"> 3 <StackPanel> 4 <Label Content="用户名" Target="{Binding ElementName=UserName}"/> 5 <TextBox Width="150" 6 Text="{Binding UserName,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}" > 7 </TextBox> 8 </StackPanel> 9 10 <StackPanel> 11 <Label Content="性别" Target="{Binding ElementName=RadioGendeMale}"/> 12 <RadioButton Content="男" /> 13 <RadioButton Content="女" Margin="8,0,0,0" /> 14 </StackPanel> 15 <StackPanel> 16 <Label Content="生日" Target="{Binding ElementName=DateBirth}" /> 17 <DatePicker /> 18 </StackPanel> 19 <StackPanel> 20 <Label Content="用户邮箱" Target="{Binding ElementName=UserEmail}"/> 21 <TextBox Width="150" Text="{Binding UserEmail, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" /> 22 </StackPanel> 23 <StackPanel> 24 <Label Content="用户电话" Target="{Binding ElementName=UserPhone}"/> 25 <TextBox Width="150" Text="{Binding UserPhone,UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" /> 26 </StackPanel> 27 28 <Button Content="提交" Margin="100,16,0,0" HorizontalAlignment="Left" Command="{Binding ValidFormCommand}" /> 29 </StackPanel> 30 31 </GroupBox>
VideModel代码:
1 using GalaSoft.MvvmLight; 2 using System; 3 using System.Collections.Generic; 4 using System.Linq; 5 using System.ComponentModel; 6 using System.ComponentModel.DataAnnotations; 7 using GalaSoft.MvvmLight.Command; 8 using System.Windows; 9 10 namespace MVVMLightDemo.ViewModel 11 { 12 [MetadataType(typeof(BindDataAnnotationsViewModel))] 13 public class BindDataAnnotationsViewModel : ViewModelBase, IDataErrorInfo 14 { 15 16 public BindDataAnnotationsViewModel() 17 { 18 19 } 20 21 #region 属性 22 /// <summary> 23 /// 表单验证错误集合 24 /// </summary> 25 private Dictionary<String, String> dataErrors = new Dictionary<String, String>(); 26 27 28 private String userName; 29 /// <summary> 30 /// 用户名 31 /// </summary> 32 [Required] 33 public String UserName 34 { 35 get { return userName; } 36 set { userName = value; } 37 } 38 39 40 41 private String userPhone; 42 /// <summary> 43 /// 用户电话 44 /// </summary> 45 [Required] 46 [RegularExpression(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$", ErrorMessage = "用户电话必须为8-11位的数值.")] 47 public String UserPhone 48 { 49 get { return userPhone; } 50 set { userPhone = value; } 51 } 52 53 54 55 private String userEmail; 56 /// <summary> 57 /// 用户邮件 58 /// </summary> 59 [Required] 60 [StringLength(100,MinimumLength=2)] 61 [RegularExpression("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$", ErrorMessage = "请填写正确的邮箱地址.")] 62 public String UserEmail 63 { 64 get { return userEmail; } 65 set { userEmail = value; } 66 } 67 #endregion 68 69 70 #region 命令 71 72 private RelayCommand validFormCommand; 73 /// <summary> 74 /// 验证表单 75 /// </summary> 76 public RelayCommand ValidFormCommand 77 { 78 get 79 { 80 if (validFormCommand == null) 81 return new RelayCommand(() => ExcuteValidForm()); 82 return validFormCommand; 83 } 84 set { validFormCommand = value; } 85 } 86 /// <summary> 87 /// 验证表单 88 /// </summary> 89 private void ExcuteValidForm() 90 { 91 if (dataErrors.Count == 0) MessageBox.Show("验证通过!"); 92 else MessageBox.Show("验证失败!"); 93 } 94 95 #endregion 96 97 98 public string this[string columnName] 99 { 100 get 101 { 102 ValidationContext vc = new ValidationContext(this, null, null); 103 vc.MemberName = columnName; 104 var res = new List<ValidationResult>(); 105 var result = Validator.TryValidateProperty(this.GetType().GetProperty(columnName).GetValue(this, null), vc, res); 106 if (res.Count > 0) 107 { 108 AddDic(dataErrors,vc.MemberName); 109 return string.Join(Environment.NewLine, res.Select(r => r.ErrorMessage).ToArray()); 110 } 111 RemoveDic(dataErrors,vc.MemberName); 112 return null; 113 } 114 } 115 116 public string Error 117 { 118 get 119 { 120 return null; 121 } 122 } 123 124 125 #region 附属方法 126 127 /// <summary> 128 /// 移除字典 129 /// </summary> 130 /// <param name="dics"></param> 131 /// <param name="dicKey"></param> 132 private void RemoveDic(Dictionary<String, String> dics, String dicKey) 133 { 134 dics.Remove(dicKey); 135 } 136 137 /// <summary> 138 /// 添加字典 139 /// </summary> 140 /// <param name="dics"></param> 141 /// <param name="dicKey"></param> 142 private void AddDic(Dictionary<String, String> dics, String dicKey) 143 { 144 if (!dics.ContainsKey(dicKey)) dics.Add(dicKey, ""); 145 } 146 #endregion 147 148 } 149 }
DataAnnotations相信很多人很熟悉,可以使用数据批注来自定义用户的模型数据,记得引用 System.ComponentModel.DataAnnotations。
他包含如下几个验证类型:
验证属性 | 说明 |
CustomValidationAttribute | 使用自定义方法进行验证。 |
DataTypeAttribute | 指定特定类型的数据,如电子邮件地址或电话号码。 |
EnumDataTypeAttribute | 确保值存在于枚举中。 |
RangeAttribute | 指定最小和最大约束。 |
RegularExpressionAttribute | 使用正则表达式来确定有效的值。 |
RequiredAttribute | 指定必须提供一个值。 |
StringLengthAttribute | 指定最大和最小字符数。 |
ValidationAttribute | 用作验证属性的基类。 |
这边我们使用到了RequiredAttribute、StringLengthAttribute、RegularExpressionAttribute 三项,如果有需要进一步了解 DataAnnotations 的可以参考微软官网:
https://msdn.microsoft.com/en-us/library/dd901590(VS.95).aspx
用 DataAnnotions 后,Model 的更加简洁,校验也更加灵活。可以叠加组合验证 , 面对复杂验证模式的时候,可以自由的使用正则来验证。
默认情况下,框架会提供相应需要反馈的消息内容,当然也可以自定义错误消息内容:ErrorMessage 。
这边我们还加了个全局的错误集合收集器 :dataErrors,在提交判断时候判断是否验证通过。
这边我们进一步封装索引器,并且通过反射技术读取当前字段下的属性进行验证。
结果如下:
=====================================================================================================================================
=====================================================================================================================================
封装ValidateModelBase类:
上面的验证比较合理了,不过相对于开发人员还是太累赘了,开发人员关心的是Model的DataAnnotations的配置,而不是关心在这个ViewModel要如何做验证处理,所以我们进一步抽象。
编写一个ValidateModelBase,把需要处理的工作都放在里面。需要验证属性的Model去继承这个基类。如下:
ValidateModelBase 类,请注意标红部分:
1 public class ValidateModelBase : ObservableObject, IDataErrorInfo 2 { 3 public ValidateModelBase() 4 { 5 6 } 7 8 #region 属性 9 /// <summary> 10 /// 表当验证错误集合 11 /// </summary> 12 private Dictionary<String, String> dataErrors = new Dictionary<String, String>(); 13 14 /// <summary> 15 /// 是否验证通过 16 /// </summary> 17 public Boolean IsValidated 18 { 19 get 20 { 21 if (dataErrors != null && dataErrors.Count > 0) 22 { 23 return false; 24 } 25 return true; 26 } 27 } 28 #endregion 29 30 public string this[string columnName] 31 { 32 get 33 { 34 ValidationContext vc = new ValidationContext(this, null, null); 35 vc.MemberName = columnName; 36 var res = new List<ValidationResult>(); 37 var result = Validator.TryValidateProperty(this.GetType().GetProperty(columnName).GetValue(this, null), vc, res); 38 if (res.Count > 0) 39 { 40 AddDic(dataErrors, vc.MemberName); 41 return string.Join(Environment.NewLine, res.Select(r => r.ErrorMessage).ToArray()); 42 } 43 RemoveDic(dataErrors, vc.MemberName); 44 return null; 45 } 46 } 47 48 public string Error 49 { 50 get 51 { 52 return null; 53 } 54 } 55 56 57 #region 附属方法 58 59 /// <summary> 60 /// 移除字典 61 /// </summary> 62 /// <param name="dics"></param> 63 /// <param name="dicKey"></param> 64 private void RemoveDic(Dictionary<String, String> dics, String dicKey) 65 { 66 dics.Remove(dicKey); 67 } 68 69 /// <summary> 70 /// 添加字典 71 /// </summary> 72 /// <param name="dics"></param> 73 /// <param name="dicKey"></param> 74 private void AddDic(Dictionary<String, String> dics, String dicKey) 75 { 76 if (!dics.ContainsKey(dicKey)) dics.Add(dicKey, ""); 77 } 78 #endregion 79 }
验证的模型类:继承 ValidateModelBase
1 [MetadataType(typeof(BindDataAnnotationsViewModel))] 2 public class ValidateUserInfo : ValidateModelBase 3 { 4 #region 属性 5 private String userName; 6 /// <summary> 7 /// 用户名 8 /// </summary> 9 [Required] 10 public String UserName 11 { 12 get { return userName; } 13 set { userName = value; RaisePropertyChanged(() => UserName); } 14 } 15 16 17 18 private String userPhone; 19 /// <summary> 20 /// 用户电话 21 /// </summary> 22 [Required] 23 [RegularExpression(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$", ErrorMessage = "用户电话必须为8-11位的数值.")] 24 public String UserPhone 25 { 26 get { return userPhone; } 27 set { userPhone = value; RaisePropertyChanged(() => UserPhone); } 28 } 29 30 31 32 private String userEmail; 33 /// <summary> 34 /// 用户邮件 35 /// </summary> 36 [Required] 37 [StringLength(100, MinimumLength = 2)] 38 [RegularExpression("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$", ErrorMessage = "请填写正确的邮箱地址.")] 39 public String UserEmail 40 { 41 get { return userEmail; } 42 set { userEmail = value; RaisePropertyChanged(() => UserEmail); } 43 } 44 #endregion 45 }
ViewModel代码如下:
1 public class PackagedValidateViewModel:ViewModelBase 2 { 3 public PackagedValidateViewModel() 4 { 5 ValidateUI = new Model.ValidateUserInfo(); 6 } 7 8 #region 全局属性 9 private ValidateUserInfo validateUI; 10 /// <summary> 11 /// 用户信息 12 /// </summary> 13 public ValidateUserInfo ValidateUI 14 { 15 get 16 { 17 return validateUI; 18 } 19 20 set 21 { 22 validateUI = value; 23 RaisePropertyChanged(()=>ValidateUI); 24 } 25 } 26 #endregion 27 28 #region 全局命令 29 private RelayCommand submitCmd; 30 public RelayCommand SubmitCmd 31 { 32 get 33 { 34 if(submitCmd == null) return new RelayCommand(() => ExcuteValidForm()); 35 return submitCmd; 36 } 37 38 set 39 { 40 submitCmd = value; 41 } 42 } 43 #endregion 44 45 #region 附属方法 46 /// <summary> 47 /// 验证表单 48 /// </summary> 49 private void ExcuteValidForm() 50 { 51 if (ValidateUI.IsValidated) MessageBox.Show("验证通过!"); 52 else MessageBox.Show("验证失败!"); 53 } 54 #endregion 55 }
结果如下:
转载请标明出处,谢谢