绑定在表单验证上的应用
表单验证是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 }
结果如下: