利刃 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.
代码比较简单,注意标红的内容,设计一个红底白字的提示框,当源属性触发错误验证的时候,把验证对象集合中的错误内容显示出来。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Style x:Key="{x:Type TextBoxBase}" TargetType="{x:Type TextBoxBase}" BasedOn="{x:Null}">
<Setter Property="BorderThickness" Value=""/>
<Setter Property="Padding" Value="2,1,1,1"/>
<Setter Property="AllowDrop" Value="true"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
<Setter Property="SelectionBrush" Value="{DynamicResource Accent}" />
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<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>
</StackPanel>
<ControlTemplate.Triggers>
<DataTrigger Value="True">
<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>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBoxBase}">
<Border x:Name="Bd"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="true">
<ScrollViewer x:Name="PART_ContentHost" RenderOptions.ClearTypeHint="Enabled"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource InputTextDisabled}"/>
</Trigger>
<Trigger Property="IsReadOnly" Value="true">
<Setter Property="Foreground" Value="{DynamicResource InputTextDisabled}"/>
</Trigger>
<Trigger Property="IsFocused" Value="true">
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Accent}" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsReadOnly" Value="False"/>
<Condition Property="IsEnabled" Value="True"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" Value="{DynamicResource InputBackgroundHover}"/>
<Setter Property="BorderBrush" Value="{DynamicResource InputBorderHover}"/>
<Setter Property="Foreground" Value="{DynamicResource InputTextHover}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style BasedOn="{StaticResource {x:Type TextBoxBase}}" TargetType="{x:Type TextBox}">
</Style>
<Style BasedOn="{StaticResource {x:Type TextBoxBase}}" TargetType="{x:Type RichTextBox}">
</Style> </ResourceDictionary>
然后在App.Xaml中全局注册到整个应用中。
<Application x:Class="MVVMLightDemo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="View/BindingFormView.xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d1p1:Ignorable="d"
xmlns:d1p1="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:MVVMLightDemo.ViewModel"
xmlns:Common="clr-namespace:MVVMLightDemo.Common">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/MVVMLightDemo;component/Assets/TextBox.xaml" />
</ResourceDictionary.MergedDictionaries>
<vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
<Common:IntegerToSex x:Key="IntegerToSex" d:IsDataSource="True" />
</ResourceDictionary>
</Application.Resources>
</Application>
达到的效果如下:
下面详细描述下这三种验证模式
1、Exception 验证:
正如说明中描述的那样,在具有绑定关系的源字段模型上做验证异常的引发并抛出,在View中的Xaml对象上设置 ExceptionValidationRule 属性,响应捕获异常并显示。
View代码:
<GroupBox Header="Exception 验证" Margin="10 10 10 10" DataContext="{Binding Source={StaticResource Locator},Path=ValidateException}" >
<StackPanel x:Name="ExceptionPanel" Orientation="Vertical" Margin="0,10,0,0" >
<StackPanel>
<Label Content="用户名" Target="{Binding ElementName=UserNameEx}"/>
<TextBox x:Name="UserNameEx" Width="">
<TextBox.Text>
<Binding Path="UserNameEx" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
9 <ExceptionValidationRule></ExceptionValidationRule>
10 </Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
</StackPanel>
</GroupBox>
ViewModel代码:
/// <summary>
/// Exception 验证
/// </summary>
public class ValidateExceptionViewModel:ViewModelBase
{
public ValidateExceptionViewModel()
{ } private String userNameEx;
/// <summary>
/// 用户名称(不为空)
/// </summary>
public string UserNameEx
{
get
{
return userNameEx;
}
set
{
userNameEx = value;
RaisePropertyChanged(() => UserNameEx);
if (string.IsNullOrEmpty(value))
26 {
27 throw new ApplicationException("该字段不能为空!");
28 }
}
}
}
结果如图:
将验证失败的信息直接抛出来,这无疑是最简单粗暴的,实现也很简单,但是只是针对单一源属性进行验证, 复用性不高。
而且在组合验证(比如同时需要验证非空和其他规则)情况下,会导致Model中写过重过臃肿的代码。
2、ValidationRule 验证:
通过继承ValidationRule 抽象类,并重写他的Validate方法来扩展编写我们需要的验证类。该验证类可以直接使用在我们需要验证的属性。
View代码:
<GroupBox Header="ValidationRule 验证" Margin="10 20 10 10" DataContext="{Binding Source={StaticResource Locator},Path=ValidationRule}" >
<StackPanel x:Name="ValidationRulePanel" Orientation="Vertical" Margin="0,20,0,0">
<StackPanel>
<Label Content="用户名" Target="{Binding ElementName=UserName}"/>
<TextBox Width="" >
<TextBox.Text>
<Binding Path="UserName" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
9 <app:RequiredRule />
10 </Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel> <StackPanel>
<Label Content="用户邮箱" Target="{Binding ElementName=UserEmail}"/>
<TextBox Width="">
<TextBox.Text>
<Binding Path="UserEmail" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
22 <app:EmailRule />
23 </Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
</StackPanel>
</GroupBox>
重写两个ValidationRule,代码如下:
public class RequiredRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (value == null)
return new ValidationResult(false, "该字段不能为空值!");
if (string.IsNullOrEmpty(value.ToString()))
return new ValidationResult(false, "该字段不能为空字符串!");
return new ValidationResult(true, null);
}
} public class EmailRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
Regex emailReg = new Regex("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$"); if (!String.IsNullOrEmpty(value.ToString()))
{
if (!emailReg.IsMatch(value.ToString()))
{
return new ValidationResult(false, "邮箱地址不准确!");
}
}
return new ValidationResult(true, null);
}
}
创建了两个类,一个用于验证是否为空,一个用于验证是否符合邮箱地址标准格式。
ViewModel代码:
public class ValidationRuleViewModel:ViewModelBase
{
public ValidationRuleViewModel()
{ } #region 属性 private String userName;
/// <summary>
/// 用户名
/// </summary>
public String UserName
{
get { return userName; }
set { userName = value; RaisePropertyChanged(()=>UserName); }
} private String userEmail;
/// <summary>
/// 用户邮件
/// </summary>
public String UserEmail
{
get { return userEmail; }
set { userEmail = value;RaisePropertyChanged(()=>UserName); }
} #endregion
结果如下:
说明:相对来说,这种方式是比较不错的,独立性、复用性都很好,从松散耦合角度来说也是比较恰当的。
可以预先写好一系列的验证规则类,视图编码人员可以根据需求直接使用这些验证规则,服务端无需额外的处理。
但是仍然有缺点,扩展性差,如果需要个性化反馈消息也需要额外扩展。不符合日益丰富的前端验证需求。
3、IDataErrorInfo 验证:
3.1、在绑定数据源对象上实现 IDataErrorInfo 接口
3.2、在 Binding 对象上设置 ValidatesOnDataErrors 属性
Binding 将调用从绑定数据源对象公开的 IDataErrorInfo API。如果从这些属性调用返回非 null 或非空字符串,则将为该 Binding 设置验证错误。
View代码:
<GroupBox Header="IDataErrorInfo 验证" Margin="10 20 10 10" DataContext="{Binding Source={StaticResource Locator},Path=BindingForm}" >
<StackPanel x:Name="Form" Orientation="Vertical" Margin="0,20,0,0">
<StackPanel>
<Label Content="用户名" Target="{Binding ElementName=UserName}"/>
<TextBox Width=""
Text="{Binding UserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" >
</TextBox>
</StackPanel> <StackPanel>
<Label Content="性别" Target="{Binding ElementName=RadioGendeMale}"/>
<RadioButton Content="男" />
<RadioButton Content="女" Margin="8,0,0,0" />
</StackPanel>
<StackPanel>
<Label Content="生日" Target="{Binding ElementName=DateBirth}" />
<DatePicker x:Name="DateBirth" />
</StackPanel>
<StackPanel>
<Label Content="用户邮箱" Target="{Binding ElementName=UserEmail}"/>
<TextBox Width="150" Text="{Binding UserEmail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</StackPanel>
<StackPanel>
<Label Content="用户电话" Target="{Binding ElementName=UserPhone}"/>
<TextBox Width="150" Text="{Binding UserPhone, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</StackPanel>
</StackPanel>
</GroupBox>
ViewModel代码:
public class BindingFormViewModel :ViewModelBase, IDataErrorInfo
{
public BindingFormViewModel()
{ } #region 属性 private String userName;
/// <summary>
/// 用户名
/// </summary>
public String UserName
{
get { return userName; }
set { userName = value; }
} private String userPhone;
/// <summary>
/// 用户电话
/// </summary>
public String UserPhone
{
get { return userPhone; }
set { userPhone = value; }
} private String userEmail;
/// <summary>
/// 用户邮件
/// </summary>
public String UserEmail
{
get { return userEmail; }
set { userEmail = value; }
}
#endregion public String Error
{
get { return null; }
} public String this[string columnName]
{
get
{
Regex digitalReg = new Regex(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$");
Regex emailReg = new Regex("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$"); if (columnName == "UserName" && String.IsNullOrEmpty(this.UserName))
{
return "用户名不能为空";
} if (columnName == "UserPhone" && !String.IsNullOrEmpty(this.UserPhone))
{
if (!digitalReg.IsMatch(this.UserPhone.ToString()))
{
return "用户电话必须为8-11位的数值!";
}
} if (columnName == "UserEmail" && !String.IsNullOrEmpty(this.UserEmail))
{
if (!emailReg.IsMatch(this.UserEmail.ToString()))
{
return "用户邮箱地址不正确!";
}
} return null;
}
} }
继承IDataErrorInfo接口后,实现方法两个属性:Error 属性用于指示整个对象的错误,而索引器用于指示单个属性级别的错误。
每次的属性值发生变化,则索引器进行一次检查,看是否有验证错误的信息返回。
两者的工作原理相同:如果返回非 null 或非空字符串,则表示存在验证错误。否则,返回的字符串用于向用户显示错误。
结果如图:
利用 IDataErrorInfo 的好处是它可用于轻松地处理交叉耦合属性。但也具有一个很大的弊端:
索引器的实现通常会导致较大的 switch-case 语句(对象中的每个属性名称都对应于一种情况),
必须基于字符串进行切换和匹配,并返回指示错误的字符串。而且,在对象上设置属性值之前,不会调用 IDataErrorInfo 的实现。
为了避免出现大量的 switch-case,并且将校验逻辑进行分离提高代码复用,将验证规则和验证信息独立化于于每个模型对象中, 使用DataAnnotations 无疑是最好的的方案 。
所以我们进行改良一下:
View代码,跟上面那个一样:
<GroupBox Header="IDataErrorInfo+ 验证" Margin="10 20 10 10" DataContext="{Binding Source={StaticResource Locator},Path=BindDataAnnotations}" >
<StackPanel Orientation="Vertical" Margin="0,20,0,0">
<StackPanel>
<Label Content="用户名" Target="{Binding ElementName=UserName}"/>
<TextBox Width=""
Text="{Binding UserName,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}" >
</TextBox>
</StackPanel> <StackPanel>
<Label Content="性别" Target="{Binding ElementName=RadioGendeMale}"/>
<RadioButton Content="男" />
<RadioButton Content="女" Margin="8,0,0,0" />
</StackPanel>
<StackPanel>
<Label Content="生日" Target="{Binding ElementName=DateBirth}" />
<DatePicker />
</StackPanel>
<StackPanel>
<Label Content="用户邮箱" Target="{Binding ElementName=UserEmail}"/>
<TextBox Width="150" Text="{Binding UserEmail, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</StackPanel>
<StackPanel>
<Label Content="用户电话" Target="{Binding ElementName=UserPhone}"/>
<TextBox Width="150" Text="{Binding UserPhone,UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</StackPanel> <Button Content="提交" Margin="100,16,0,0" HorizontalAlignment="Left" Command="{Binding ValidFormCommand}" />
</StackPanel> </GroupBox>
VideModel代码:
using GalaSoft.MvvmLight;
using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using GalaSoft.MvvmLight.Command;
using System.Windows; namespace MVVMLightDemo.ViewModel
{
[MetadataType(typeof(BindDataAnnotationsViewModel))]
public class BindDataAnnotationsViewModel : ViewModelBase, IDataErrorInfo
{ public BindDataAnnotationsViewModel()
{ } #region 属性
/// <summary>
/// 表单验证错误集合
/// </summary>
private Dictionary<String, String> dataErrors = new Dictionary<String, String>(); private String userName;
/// <summary>
/// 用户名
/// </summary>
[Required]
public String UserName
{
get { return userName; }
set { userName = value; }
} private String userPhone;
/// <summary>
/// 用户电话
/// </summary>
[Required]
46 [RegularExpression(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$", ErrorMessage = "用户电话必须为8-11位的数值.")]
public String UserPhone
{
get { return userPhone; }
set { userPhone = value; }
} private String userEmail;
/// <summary>
/// 用户邮件
/// </summary>
[Required]
60 [StringLength(100,MinimumLength=2)]
61 [RegularExpression("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$", ErrorMessage = "请填写正确的邮箱地址.")]
public String UserEmail
{
get { return userEmail; }
set { userEmail = value; }
}
#endregion #region 命令 private RelayCommand validFormCommand;
/// <summary>
/// 验证表单
/// </summary>
public RelayCommand ValidFormCommand
{
get
{
if (validFormCommand == null)
return new RelayCommand(() => ExcuteValidForm());
return validFormCommand;
}
set { validFormCommand = value; }
}
/// <summary>
/// 验证表单
/// </summary>
private void ExcuteValidForm()
{
if (dataErrors.Count == ) MessageBox.Show("验证通过!");
else MessageBox.Show("验证失败!");
} #endregion public string this[string columnName]
{
get
{
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;
}
} public string Error
{
get
{
return null;
}
} #region 附属方法 /// <summary>
/// 移除字典
/// </summary>
/// <param name="dics"></param>
/// <param name="dicKey"></param>
private void RemoveDic(Dictionary<String, String> dics, String dicKey)
{
dics.Remove(dicKey);
} /// <summary>
/// 添加字典
/// </summary>
/// <param name="dics"></param>
/// <param name="dicKey"></param>
private void AddDic(Dictionary<String, String> dics, String dicKey)
{
if (!dics.ContainsKey(dicKey)) dics.Add(dicKey, "");
}
#endregion }
}
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 类,请注意标红部分:
public class ValidateModelBase : ObservableObject, IDataErrorInfo
{
public ValidateModelBase()
{ } #region 属性
/// <summary>
/// 表当验证错误集合
/// </summary>
private Dictionary<String, String> dataErrors = new Dictionary<String, String>(); 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 }
#endregion public string this[string columnName]
{
get
{
ValidationContext vc = new ValidationContext(this, null, null);
vc.MemberName = columnName;
var res = new List<ValidationResult>();
var result = Validator.TryValidateProperty(this.GetType().GetProperty(columnName).GetValue(this, null), vc, res);
if (res.Count > )
{
AddDic(dataErrors, vc.MemberName);
return string.Join(Environment.NewLine, res.Select(r => r.ErrorMessage).ToArray());
}
RemoveDic(dataErrors, vc.MemberName);
return null;
}
} public string Error
{
get
{
return null;
}
} #region 附属方法 /// <summary>
/// 移除字典
/// </summary>
/// <param name="dics"></param>
/// <param name="dicKey"></param>
private void RemoveDic(Dictionary<String, String> dics, String dicKey)
{
dics.Remove(dicKey);
} /// <summary>
/// 添加字典
/// </summary>
/// <param name="dics"></param>
/// <param name="dicKey"></param>
private void AddDic(Dictionary<String, String> dics, String dicKey)
{
if (!dics.ContainsKey(dicKey)) dics.Add(dicKey, "");
}
#endregion
}
验证的模型类:继承 ValidateModelBase
[MetadataType(typeof(BindDataAnnotationsViewModel))]
public class ValidateUserInfo : ValidateModelBase
{
#region 属性
private String userName;
/// <summary>
/// 用户名
/// </summary>
[Required]
public String UserName
{
get { return userName; }
set { userName = value; RaisePropertyChanged(() => UserName); }
} private String userPhone;
/// <summary>
/// 用户电话
/// </summary>
[Required]
[RegularExpression(@"^[-]?[1-9]{8,11}\d*$|^[0]{1}$", ErrorMessage = "用户电话必须为8-11位的数值.")]
public String UserPhone
{
get { return userPhone; }
set { userPhone = value; RaisePropertyChanged(() => UserPhone); }
} private String userEmail;
/// <summary>
/// 用户邮件
/// </summary>
[Required]
[StringLength(, MinimumLength = )]
[RegularExpression("^\\s*([A-Za-z0-9_-]+(\\.\\w+)*@(\\w+\\.)+\\w{2,5})\\s*$", ErrorMessage = "请填写正确的邮箱地址.")]
public String UserEmail
{
get { return userEmail; }
set { userEmail = value; RaisePropertyChanged(() => UserEmail); }
}
#endregion
}
ViewModel代码如下:
public class PackagedValidateViewModel:ViewModelBase
{
public PackagedValidateViewModel()
{
ValidateUI = new Model.ValidateUserInfo();
} #region 全局属性
private ValidateUserInfo validateUI;
/// <summary>
/// 用户信息
/// </summary>
public ValidateUserInfo ValidateUI
{
get
{
return validateUI;
} set
{
validateUI = value;
RaisePropertyChanged(()=>ValidateUI);
}
}
#endregion #region 全局命令
private RelayCommand submitCmd;
public RelayCommand SubmitCmd
{
get
{
if(submitCmd == null) return new RelayCommand(() => ExcuteValidForm());
return submitCmd;
} set
{
submitCmd = value;
}
}
#endregion #region 附属方法
/// <summary>
/// 验证表单
/// </summary>
private void ExcuteValidForm()
{
if (ValidateUI.IsValidated) MessageBox.Show("验证通过!");
else MessageBox.Show("验证失败!");
}
#endregion
}
结果如下:
转载请标明出处,谢谢
利刃 MVVMLight 5:绑定在表单验证上的应用的更多相关文章
- 走进AngularJs 表单及表单验证
年底了越来越懒散,AngularJs的学习落了一段时间,博客最近也没更新.惭愧~前段时间有试了一下用yeoman构建Angular项目,感觉学的差不多了想做个项目练练手,谁知遇到了一系列问题.yeom ...
- 走进AngularJs(九)表单及表单验证
年底了越来越懒散,AngularJs的学习落了一段时间,博客最近也没更新.惭愧~前段时间有试了一下用yeoman构建Angular项目,感觉学的差不多了想做个项目练练手,谁知遇到了一系列问题.yeom ...
- elementUI 级联选择框 表单验证
今天遇到了一个需求:进行级联选择框的表单验证,突然有点懵逼.感觉应该和正常的表单验证类似,但不是很清晰,后来还是在博客园找到了相关参考文章. 先上代码: <el-form :model=&quo ...
- vue+element 动态表单验证
公司最近的项目有个添加动态表单的需求,总结一下我在表单验证上遇到的一些坑. 如图是功能的需求,这个功能挺好实现的,但是表单验证真是耗费了我一些功夫. vue+element在表单验证上有一些限制,必须 ...
- AngularJS系列:表单全解(表单验证,radio必选,三级联动,check绑定,form提交验证)
一.查看$scope -->寻找Form控制变量的位置 Form控制变量 格式:form的name属性.input的name属性.$... formName.inputField.$pristi ...
- python_way day17 jQuery表单验证,事件绑定,插件,文本框架,正则表达式
python_way day17 1.jQuery表单验证 dom事件绑定 jquery事件绑定 $.each return值的判断 jquery扩展方法 2.前段插件 3.jDango文本框架 4. ...
- spring+thymeleaf实现表单验证数据双向绑定
前言 这个教程介绍了Thymeleaf与Spring框架的集成,特别是SpringMvc框架. 注意Thymeleaf支持同Spring框架的3.和4.版本的集成,但是这两个版本的支持是封装在thym ...
- jquery validate表单验证插件-推荐
1 表单验证的准备工作 在开启长篇大论之前,首先将表单验证的效果展示给大家. 1.点击表单项,显示帮助提示 2.鼠标离开表单项时,开始校验元素 3.鼠标离开后的正确.错误提示及鼠标移入时的帮 ...
- Bootstrap表单验证插件bootstrapValidator使用方法整理
插件介绍 先上一个图: 下载地址:https://github.com/nghuuphuoc/bootstrapvalidator 使用方法:http://www.cnblogs.com/huangc ...
随机推荐
- javascript 常用api
常用API合集 来源于:https://www.kancloud.cn/dennis/tgjavascript/241852 一.节点 1.1 节点属性 Node.nodeName //返回节点名称, ...
- lxd-启动篇分析
lxd是什么:lxd是基于lxc构筑的容器管理进程,提供镜像,网络,存储,以及容器的能力,对外暴漏restfull API.其与docker的区别是docker更切近与app container,以应 ...
- iOS实现视频和图片的上传
关于iOS如何实现视频和图片的上传, 我们先理清下思路 思路: #1. 如何获取图片? #2. 如何获取视频? #3. 如何把图片存到缓存路径中? #4. 如何把视频存到缓存路径中? #5. 如何上传 ...
- node.js爬虫爬取拉勾网职位信息
简介 用node.js写了一个简单的小爬虫,用来爬取拉勾网上的招聘信息,共爬取了北京.上海.广州.深圳.杭州.西安.成都7个城市的数据,分别以前端.PHP.java.c++.python.Androi ...
- AVFoundation自定义拍照
0.AVCapture <AVFoundation/AVFoundation.h> 媒体采集需要的几个对象: 1.AVCaptureDevice: 代表抽象的硬件设备(如前置摄像头,后置 ...
- 2017-3-22 HTML 表单 、框架
表单:<form action="" method="get/post" ></form> 表单元素:12个 1.文本类 文本框:< ...
- 2.sparkSQL--DataFrames与RDDs的相互转换
Spark SQL支持两种RDDs转换为DataFrames的方式 使用反射获取RDD内的Schema 当已知类的Schema的时候,使用这种基于反射的方法会让代码更加简洁而且效果也很好. 通 ...
- Dive in python Chapter4 实例
def info(object,spacing=10,collapse=1): """Print methods and doc strings. Takes modul ...
- MVC学习笔记1-MVC家族间的区别
ASP.NET下的MVC从原始的1.0走到2.0,再到3.0,现在走到4.0,也许明年5.0就问世了,先不管那些,那说说这些MVC在ASP.NET是如何变化发展的.对于.net编程人员来说可能会很熟悉 ...
- 如何进行SQL性能优化
在SQL查询中,为了提高查询的效率,我们常常采取一些措施对查询语句进行SQL性能优化.本文我们总结了一些优化措施,接下来我们就一一介绍. 1.查询的模糊匹配 尽量避免在一个复杂查询里面使用 LIKE ...