【WPF 数据验证机制】一、开篇 【大纲】 Validation
参考:https://blog.magnusmontin.net/2013/08/26/data-validation-in-wpf/
【WPF验证机制概览】
Windows Presentation Foundation (WPF) 具有一个丰富数据绑定系统。除了作为通过 Model-View-ViewModel (MVVM) 模式从支持逻辑和数据对 UI 定义进行松散耦合的关键推动力之外,数据绑定系统还为业务数据验证方案提供强大而灵活的支持。WPF 中的数据绑定机制包括多个选项,可用于在创建可编辑视图时校验输入数据的有效性。WPF的数据验证 都是在bing表达式上实现的。因此有必要详细了解一下bing表达有关验证的属性:
MVVM验证示意图
bing表达式上有关验证的属性设置:
只有当Bing引擎的模式是twoway或onewayToSource模式的绑定时才开启验证。
1、指定使用何种属性绑定验证方式
- ValidatesOnExceptions =true:启用wpf 默认的异常验证。
- ValidatesOnDataErrors =true:默认false,对应IDataErrorsInfo 数据结构验证。
- ValidatesOnNotifyDataErrors =true:模式true,对应 INotifyDataErrorInfo,启用 (IDataErrorInfo + DataAnnotations)
2、
- NotifyOnSourceUpdated
- NotifyOnTargetUpdated,ValidatesOnTargetUpdated = true;这条语句的的作用是当目标控件的属性值(binding指定)改变时,rangeValidationRule就会执行Validate方法进行校验。如果把属性值设置为false,则rangeValidationRule不会校验。
- NotifyOnValidationError,启用Error事件,当存储或者清除错误时会引发该事件。binding.NotifyOnValidationError = true;这个语句的作用是当rangeValidationRule检验数据不合法时,是不是要通知binding。
当binding接收到数据不合法时,就会调用textBox1.AddHandler(Validation.ErrorEvent, new RoutedEventHandler(ValidationError))这条语句指定数据不合法的处理函数。如果binding.NotifyOnValidationError = false则ValidationError函数不会执行
3、ValidationRules 验证规则,设置验证规则。
4、ValidationStep指定validationRule
何时开始验证
validationRule.ValidationStep属性确定了验证的时机,是一个枚举值:
RawProposedValue(0),默认值,即先验证,后转换,再赋值给源,如果验证失败,不会继续执行转换和赋值
ConvertedProposedValue(1),先执行Converter,再执行Validate,后赋值到源,如果验证失败,则不执行赋值
UpdatedValue(2),先执行Converter,再执行赋值,最后执行Validate,即无论Validate是否成功,源数据都会更新
CommittedValue(3),先执行Converter,再执行赋值,最后执行Validate,即无论Validate是否成功,源数据都会更新
注意,在3、4中,自定义验证方法中的Value是System.Windows.Data.BindingExpression,而不再是目标值,这时应该先从Binding表达式中找到目标值再进行Validate。
<Binding Mode="TwoWay" Path="UserName" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <ExceptionValidationRule ValidationStep="RawProposedValue" /> </Binding.ValidationRules> </Binding>
WPF包含5种的验证方式
方式 | 工作方式 | 父类 | 对应接口 | 处理方式 | 简便写法 |
默认值 |
ExceptionValidationRule | 同步、单属性、返回当个错误信息 | ValidationRule | 无,wpf默认 | 检查在更新数据源(Source)时抛出的异常 | ValidatesOnExceptions=True | false |
DataErrorValidationRule | 实体级、跨属性、同步、返回string错误信息 | ValidationRule |
IDataErrorInfo |
检查实现了IDataError接口的类生成的错误 | ValidatesOnDataErrors=True | false |
NotifyDataErrorValidationRule | 实体级、跨属性、异步、返回一组(IEnumerable)错误信息 | ValidationRule | INotifyDataErrorInfo | 检查实现了INotifyDataErrorInfo接口的对象生成的错误 | ValidatesOnNotifyDataErrors=True | true |
ValidationRules | 在xaml 中直接使用,单属性 | ValidationRule | 无 | 无 | 无 | |
DataAnnotation验证机制 | 验证对象,属性和特性,结合IDataErrorInfo或INotifyDataErrorInfo 使用 | ValidationAttribute | 无 | 无 | 无 | 无 |
当异常被触发时,在数据对象中的属性不被更新。但是当你使用IDataErrorInfo或INotifyDataErrorInfo接口时,无效的值被允许但是被标记。数据对象被更新,但是你可以使用通知和BindingValidationFailed事件去通知用户。
【ValidatesOnExceptions 异常验证方式】
通过在某个 Binding 对象上设置 ValidatesOnExceptions 属性,如果在尝试对源对象属性设置已修改的值的过程中引发异常,则将为该 Binding 设置验证错误。
如果将 ValidatesOnExceptions 设置为 true,则告知绑定引擎捕捉当从 TwoWay 绑定中的目标更新源对象时发生的异常。 这些异常以两种方式发生:
a). 从源对象的 setter 函数引发。
b). 由类型转换器引发。
为了收到已发生这些异常的通知,必须将 NotifyOnValidationError 属性设置为 true,获取或设置一个值,该值指示是否在绑定对象上引发“ErrorEvent”事件。
如果将此属性设置为 false,则该应用程序将在发生验证异常时失败而不给出任何提示。
【IDataErrorInfo验证方式】
此接口使数据实体类能够实现自定义验证规则并公开用户界面的验证结果。 通常实现此接口来提供相对简单的客户端验证逻辑。
IDataErrorInfo.Item 属性为整个对象返回一条验证错误消息。 Error 属性为整个对象返回一条错误消息。 这些单一消息中的每条消息都可表示多个错误。
使用方法
将控件绑定到实现 IDataErrorInfo 的实体的属性上,并将 Binding.ValidatesOnDataErrors 属性设置为true。 然后,每当绑定实体的属性更改值时,绑定引擎都会通过将属性名传递到 IDataErrorInfo.Item 验证新值。 绑定引擎使用这些验证结果,为该绑定更新 Validation.Errors 集合。 首先,绑定引擎为来自于异常或 IDataErrorInfo 验证的绑定属性,移除任何现有的错误。 然后,如果新值是无效,绑定引擎会为此属性添加一个新错误。
【INotifyDataErrorInfo 验证方式 】
public interface INotifyDataErrorInfo { bool HasErrors { get; } event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; IEnumerable GetErrors(string propertyName); }
此接口使数据实体类能够实现自定义验证规则并公开用户界面的验证结果。 通常实现此接口来提供服务器端验证逻辑等异步验证逻辑。 此接口还支持自定义错误对象、每个属性的多个错误、跨属性错误和实体级错误。
跨属性错误是影响多个属性的错误。 您可以将这些错误与一个或所有受影响的属性关联,或者您也可以将它们视为实体级错误。 实体级别错误为影响多个属性或影响整个实体的错误,但不影响特定属性。
【使用特性标记验证非RIA元数据类】
通过RIA服务暴露的实体可以使用特性标记来进行验证,很方便,但是常规的元数据类如果使用这种标记则不会正常实现验证功能,需要做一些设置.Silverlight的DataAnnotation验证机制,在添加验证属性后,不需要在Setter中进行验证判断,仅需要在Setter中激活该验证属性即可,而要实现激活验证,则需要使用ValidationContext和Validator类。为了更好的理解Silverlight DataAnnotation验证机制,我们来对这两个类进行简单的讲解.
首先说说Validator类,该类是一个静态类,主要用来当数据成员被指定验证元数据属性时,验证对象,属性和方法。简单的理解就是包含了各种具体验证方法的类。例如Require验证属性,Validator类将会根据该验证属性执行对应的验证方法,对目标值进行判断。在该类中,包含ValidateProperty方法(验证错误返回异常)和TryValidateProperty方法(验证错误返回False),可以分别对当前属性进行验证操作。
而ValiationContext类,该类是对当前执行的数据验证提供上下文描述的。简单的理解,也就是为验证提供数据传输,属性标识等任务。
Bing数据验证的流程:
在使用 WPF 中的数据绑定来呈现业务数据时,通常应使用 Binding 对象在目标控件的单个属性与数据源对象属性之间提供数据管道。
若要使绑定验证有效,首先需要进行 TwoWay 数据绑定。这意味着,除了从源属性流向目标属性以进行显示的数据之外,编辑过的数据也会从目标流向源。
当 TwoWay 数据绑定中输入或修改数据时,将启动以下工作流:
Validation处理过程数据验证发生在TwoWay和OneWayToSource的Binding中
下面介绍了验证过程:
-
【清除Errors】当值从目标属性传输到源属性时,数据绑定引擎首先删除可能已添加到System.Windows.Controls.Validation.Errors绑定元素的附加属性中的任何ValidationError值。
-
【验证RawProposedValue 类型的ValidationRule 】绑定引擎检查是否为该 Binding 定义了任何将 ValidationStep 设置为 RawProposedValue 的自定义 ValidationRule 对象,在这种情况下,它会在每个 s 上ValidationRule调用Validate该方法,直到其中一个进入错误,或者直到所有自定义项都通过。
(所有验证规则通用)一旦有一个不传递的自定义规则,绑定引擎将创建一个 ValidationError 对象并将其添加到 System.Windows.Controls.Validation.Errors 绑定元素的集合中。 如果 System.Windows.Controls.Validation.Errors 不为空,则元素的 System.Windows.Controls.Validation.HasError 附加属性设置为true
。 此外,如果 Binding 的 NotifyOnValidationError 属性设置为true
,则绑定引擎将在元素上引发 System.Windows.Controls.Validation.Error 附加事件。 -
【调用转化器】如果所有规则都通过,则绑定引擎会调用转换器(如果存在)。
- 【验证ConvertedProposedValue类型的ValidationRule】如果转换器成功后,则绑定引擎会检查是否为该 Binding 定义了任何将 ValidationStep 设置为 ConvertedProposedValue 的自定义 ValidationRule 对象,在这种情况下,绑定引擎将对每个 ValidationRule(将 ValidationStep 设置为 ConvertedProposedValue)调用 Validate 方法,直到其中一个出错或直到全部通过。
- 【绑定引擎设置源属性】
-
【验证UpdatedValue类型的ValidationRule】绑定引擎检查是否为该 Binding 定义了任何将 ValidationStep 设置为 UpdatedValue 的自定义 ValidationRule 对象,在这种情况下,绑定引擎将对每个 ValidationRule(将 ValidationStep 设置为 UpdatedValue)调用 Validate 方法,直到其中一个出错或直到全部通过。 如果 DataErrorValidationRule 与绑定关联并且其 ValidationStep 设置为默认的 UpdatedValue,则此时将检查 DataErrorValidationRule。 此时检查将 ValidatesOnDataErrors 设置为 true 的所有绑定。
- 【验证CommittedValue类型的ValidationRule】绑定引擎检查是否为该 Binding 定义了任何将 ValidationStep 设置为 CommittedValue 的自定义 ValidationRule 对象,在这种情况下,绑定引擎将对每个 ValidationRule(将 ValidationStep 设置为 CommittedValue)调用 Validate 方法,直到其中一个出错或直到全部通过。
如果 ValidationRule 在整个过程中的任何时间都没有通过,则绑定引擎会创建 ValidationError 对象并将其添加到绑定元素的 Validation.Errors 集合中。 绑定引擎在任何给定步骤运行 ValidationRule 对象之前,它会删除在执行该步骤期间添加到绑定元素的 Validation.Errors 附加属性的所有 ValidationError。 例如,如果将 ValidationStep 设置为 UpdatedValue 的 ValidationRule 失败,则下次执行验证过程时,绑定引擎会在调用将 ValidationStep 设置为 UpdatedValue 的任何 ValidationRule 之前删除 ValidationError。当异常被触发时,在数据对象中的属性不被更新。但是当你使用IDataErrorInfo或INotifyDataErrorInfo接口时,无效的值被允许但是被标记。数据对象被更新,但是你可以使用通知和BindingValidationFailed事件去通知用户。
如果 Validation.Errors 不为空,则元素的 Validation.HasError 附加属性设置为 true。 此外,如果 Binding 的 NotifyOnValidationError 属性设置为 true,则绑定引擎将在元素上引发 Validation.Error 附加事件。
另请注意,任何方向(目标到源或源到目标)的有效值传输操作都会清除 Validation.Errors 附加属性。
如果绑定具有关联的 ExceptionValidationRule,或将 ValidatesOnExceptions 属性设置为 true,并且在绑定引擎设置源时引发异常,则绑定引擎将检查是否存在 UpdateSourceExceptionFilter。 可以使用 UpdateSourceExceptionFilter 回叫来提供用于处理异常的自定义处理程序。 如果未在 Binding 上指定 UpdateSourceExceptionFilter,则绑定引擎会创建具有异常的 ValidationError 并将其添加到绑定元素的 Validation.Errors 集合中。
UI层 显示验证信息
在介绍数据验证之前,有必要介绍一下如何显示错误信息.方式很简单,定义一个样式触发器,将错误信息和 ToolTip绑定,如下:
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
本篇主要介绍MVVM模式下, IDataErrorInfo 的校验以及结合 DataAnnotation 的实现方法。比较来说,在 set 里直接 throw Exception 实现最为简单,但不适合组合校验且Model里需要编写过重的校验代码;ValidationRules 更适合在用户控件或者自定义控件场合使用。IDataErrorInfo 则是比较普遍且灵活的校验实现方式。
提供视觉反馈
如果用户输入的值无效,你可能希望在应用 UI 上提供一些有关错误的反馈。 提供此类反馈的一种方法是将 Validation.ErrorTemplate 附加属性设置为自定义 ControlTemplate。 如前面部分所示,StartDateEntryForm TextBox 使用名为 validationTemplate 的 ErrorTemplate。 以下示例显示了 validationTemplate 的定义。
<ControlTemplate x:Key="validationTemplate">
<DockPanel>
<TextBlock Foreground="Red" FontSize="20">!</TextBlock>
<AdornedElementPlaceholder/>
</DockPanel>
</ControlTemplate>
AdornedElementPlaceholder 元素指定应放置待装饰控件的位置。
此外,还可以使用
ToolTip 来显示错误消息。 StartDateEntryForm 和 StartPriceEntryFormTextBox 都使用样式
textStyleTextBox,该样式创建显示错误消息的 ToolTip。 以下示例显示了 textStyleTextBox 的定义。
如果绑定元素属性上的一个或多个绑定出错,则附加属性 Validation.HasError 为 true。
<Style x:Key="textStyleTextBox" TargetType="TextBox">
<Setter Property="Foreground" Value="#333333" />
<Setter Property="MaxLength" Value="40" />
<Setter Property="Width" Value="392" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding (Validation.Errors).CurrentItem.ErrorContent, RelativeSource={RelativeSource Self}}" />
</Trigger>
</Style.Triggers>
</Style>
使用自定义 ErrorTemplate 和 ToolTip 时,StartDateEntryForm TextBox 在发生验证错误时如下所示。
如果 Binding 具有关联的验证规则,但未在绑定控件上指定 ErrorTemplate,则发生验证错误时,将使用默认的 ErrorTemplate 通知用户。 默认的 ErrorTemplate 是一个控件模板,它在装饰层中定义红色边框。 使用默认的 ErrorTemplate 和 ToolTip 时,StartPriceEntryForm TextBox 的 UI 在发生验证错误时如下所示。
有关如何提供逻辑以验证对话框中所有控件的示例,请参阅对话框概述中的“自定义对话框”部分。
定制验证错误显示模板
默认情况下,错误模板(ErrorTemplate
)会用一个红色的边框框住输入对象TextBox
,看上去会不太好看。定义一个验证模板:
<ControlTemplate x:Key="validationTemplate"> <Border BorderThickness="1" BorderBrush="Blue" UseLayoutRounding="True"> <DockPanel> <TextBlock DockPanel.Dock="Right" Foreground="Red" FontSize="20" VerticalAlignment="Center" Text="!" /> <!--Hold住元素的装饰器--> <AdornedElementPlaceholder/> </DockPanel> </Border> </ControlTemplate>
使用验证错误模板:
<TextBox Validation.ErrorTemplate="{StaticResource validationTemplate}" ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"> <TextBox.Text> <Binding Path="DateTime" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <local:DateTimeValidationRule /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox>
验证机制一、ValidatesOnExceptions验证规则
如果一个Binding的ValidationRules设置了ExceptionValidationRule或在Binding中直接指定ValidateOnException=true,那么它捕获属性中抛出的异常,
1)ValidatesOnExceptions是WPF预定义的验证规则,它会捕捉任何位置上的异常,包括类型转换异常,属性设置器异常,值转换器异常等.捕捉到异常的时候,输入的边框会变成红色,当然也可以自定义错误的模板(Validation.ErrorTemplate).想要ValidatesOnExceptions生效,将绑定属性中的ValidatesOnExceptions设置为true即可.
Xaml设置
<Window x:Class="Validate.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:good="clr-namespace:MyValidate" xmlns:local="clr-namespace:Validate" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.Resources> <good:Goods Price="20" x:Key="test"/> </Window.Resources> <Grid> <WrapPanel DataContext="{StaticResource test}"> <Label Content="Customer _type:" HorizontalAlignment="Right" Target="{Binding ElementName=customerTypeCmb}" /> <!-- 当 price 不符合验证规则时候 文本框为红色--> <TextBox Width="100" Text="{Binding Price, UpdateSourceTrigger=PropertyChanged , ValidatesOnExceptions=True}"/> <ComboBox x:Name="customerTypeCmb" ItemsSource="{Binding Path=CustomerTypeOptions, Mode=OneTime}" SelectedItem="{Binding Path=CustomerType, ValidatesOnDataErrors=True}" Validation.ErrorTemplate="{x:Null}"/> <ContentPresenter Content="{Binding ElementName=customerTypeCmb, Path=(Validation.Errors)[0].ErrorContent}" /> </WrapPanel> </Grid> </Window>
Goods.cs类
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyValidate { public class Goods : INotifyPropertyChanged { private int _price; public int Price { get { return _price; } set { if (Equals(value, _price)) return; if (value < 0) { throw new ArgumentException("数值不能小于0"); } else { _price = value; if (PropertyChanged != null) { PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Price")); } } } } public event PropertyChangedEventHandler? PropertyChanged; public string[] _customerTypeOptions; public string[] CustomerTypeOptions { get { if (_customerTypeOptions == null) { _customerTypeOptions = new string[] { "NotSpecified", "Person", "Company" }; } return _customerTypeOptions; } } } }
错误时候结果显示:
此时文本框为红色
正确时显示
此时文本框文蓝色
优化一下,格式化错误信息显示
Xaml.cs中添加错误信息显示方式 代码如下:
<Window x:Class="Validate.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:good="clr-namespace:MyValidate" xmlns:local="clr-namespace:Validate" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.Resources> <good:Goods Price="20" x:Key="test"/> <Style TargetType="TextBox"> <Style.Triggers> <Trigger Property="Validation.HasError" Value="true"> <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" /> </Trigger> </Style.Triggers> </Style> </Window.Resources> <Grid> <WrapPanel DataContext="{StaticResource test}"> <Label Content="Customer _type:" HorizontalAlignment="Right" Target="{Binding ElementName=customerTypeCmb}" /> <!-- 当 price 不符合验证规则时候 文本框为红色--> <TextBox Width="100" Text="{Binding Price, UpdateSourceTrigger=PropertyChanged , ValidatesOnExceptions=True}"/> <ComboBox x:Name="customerTypeCmb" ItemsSource="{Binding Path=CustomerTypeOptions, Mode=OneTime}" SelectedItem="{Binding Path=CustomerType, ValidatesOnDataErrors=True}" Validation.ErrorTemplate="{x:Null}"/> <ContentPresenter Content="{Binding ElementName=customerTypeCmb, Path=(Validation.Errors)[0].ErrorContent}" /> </WrapPanel> </Grid> </Window>
效果如下:
当鼠标停留在文本框片刻,会出现错误提示信息
IDataErrorInfo验证机制
IDataErrorInfo支持显示同一个属性多个 验证规则的错误信息,例如属性:
public partial class Person_MetaData : PropertyValidateModel { [Required] [StringLength(30, ErrorMessage = "不能超过10个字符")] [RegularExpression(@"\b[\w]{0,30}\b", ErrorMessage = "不能输入空白字符")] public string FirstName { get; set; } }
通常情况 IDataErrorInfo 单独使用,也可以结合DataAnnotatio 一起使用。结合使用请往下找。
public interface IDataErrorInfo { string this[string columnName] { get;//返回绑定属性的错误信息 提供给Validation 使用,对应Validation.Errors和Validation.HasError, } /// <devdoc> /// <para>[To be supplied.]</para> /// </devdoc> string Error { get;//返回类的错误信息 , } }
.NET 3.5 以来一直存在的 IDataErrorInfo 接口基本上只提供了返回一个字符串的功能,该字符串指定单个给定属性出了什么问题
我们先来看看 IDataErrorInfo 的实现方法: Error 属性用于指示整个对象的错误,而索引器用于指示单个属性级别的错误。
两者的工作原理相同:如果返回非 null 或非空字符串,则表示存在验证错误。否则,返回的字符串用于向用户显示错误。
Person.cs 的 Age 属性进行了校验。
public class Person : INotifyPropertyChanged, IDataErrorInfo { private string _name; public string Name { get { return _name; } set { if (_name != value) { _name = value; RaisePropertyChanged("Name"); } } } private int _age; public int Age { get { return _age; } set { if (_age != value) { _age = value; RaisePropertyChanged("Age"); } } } public string Error { get { return ""; } } public string this[string columnName] { get { if (columnName == "Age") { if (_age < 18) { return "年龄必须在18岁以上。"; } } return string.Empty; } } public event PropertyChangedEventHandler PropertyChanged; internal virtual void RaisePropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
Xmal 绑定:
<TextBox Text="{Binding Age, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" Grid.Row="1" Grid.Column="1" Margin="5"/>
(注意: TextBox 默认的 UpdateSourceTrigger 是 LostFocus,如果你想内容一改变立刻进行校验那么需要修改为 PropertyChanged )
那么,在输入非法的年龄时提示出错误信息。
利用 IDataErrorInfo 的好处是它可用于轻松地处理交叉耦合属性。但也具有一个很大的弊端:
索引器的实现通常会导致较大的 switch-case 语句(对象中的每个属性名称都对应于一种情况),
必须基于字符串进行切换和匹配,并返回指示错误的字符串。而且,在对象上设置属性值之前,不会调用 IDataErrorInfo 的实现。
为了避免出现大量的 switch-case,并且将校验逻辑进行分离提高代码复用,于是 DataAnnotations 华丽登场。
改造下上面的 Person 类,加上 [Range] ValidationAttribute:(需要添加 System.ComponentModel.DataAnnotations.dll)
[Range(19, 99, ErrorMessage="年龄必须在18岁以上。")] public int Age { get { return _age; } set { if (_age != value) { _age = value; RaisePropertyChanged("Age"); } } }
修改 IDataErrorInfo 的索引器,让它通过 Validator 校验属性:
public string this[string columnName] { get { var 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 > 0) { return string.Join(Environment.NewLine, res.Select(r => r.ErrorMessage).ToArray()); } return string.Empty; } }
用 DataAnnotions 后,Model 的更加简洁,校验也更加灵活。还可以利用 CustomerValidation 或者 自定义 ValidationAttribute 来进行校验逻辑的进一步分离,错误消息格式化。并且通过反射等技术,完全可以将 IDataErrorInfo 的实现抽成一个抽象类进行封装,编程更加便利。
(1) 自定义 ValidationAttribute
添加了一个针对上面 Person 的 Name 属性是否存在的校验:
class NameExists : ValidationAttribute { public override bool IsValid(object value) { var name = value as string; // 这里可以到数据库等存储容器中检索 if (name != "Felix") { return false; } return true; } public override string FormatErrorMessage(string name) { return "请输入存在的用户名。"; } }
将 NameExistsAttribute 添加到 Person.Name 属性上:
[NameExists] public string Name { get { return _name; } set { if (_name != value) { _name = value; RaisePropertyChanged("Name"); } } }
(2) 利用 CustomerValidationAttribute
先实现一个 public static 的校验方法(必须返回 ValidationResult )
public class CustomerValidationUtils { public static ValidationResult CheckName(string value) { if (value.Length < 8) { return new ValidationResult("名字长度必须大于等于8位。"); } return ValidationResult.Success; } }
然后在 Person 的 Name 属性上加上 CustomerValidation 特性:
[CustomValidation(typeof(CustomerValidationUtils), "CheckName")] public string Name { get { return _name; } set { if (_name != value) { _name = value; RaisePropertyChanged("Name"); } } }
在实际开发中,我们还经常使用 EF 等 ORM 来做数据访问层,Model 通常会由这个中间件自动生成(利用T4等代码生成工具)。而他们通常是 POCO 数据类型,这时候如何能把属性的校验特性加入其中呢。这时候, TypeDescriptor.AddProviderTransparent + AssociatedMetadataTypeTypeDescriptionProvider 可以派上用场,它可以实现在另一个类中增加对应校验特性来增强对原类型的元数据描述。按照这种思路,将上面的 Person 类分离成两个文件:第一个分离类,可以想象是中间件自动生成的 Model 类。第二个分离类中实现 IDataErrorInfo,并定义一个Metadata 类来增加校验特性。(EF CodeFirst 也可以使用这一思路)
Person.cs (原生的 Person 类,没有校验特性)
public partial class Person : INotifyPropertyChanged { private string _name; public string Name { get { return _name; } set { if (_name != value) { _name = value; RaisePropertyChanged("Name"); } } } private int _age; public int Age { get { return _age; } set { if (_age != value) { _age = value; RaisePropertyChanged("Age"); } } } public event PropertyChangedEventHandler PropertyChanged; internal virtual void RaisePropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
PersonMetadata.cs (分离的 Person 类中,实现 IDataErrorInfo 接口,在其内部类中增加了校验特性)
public partial class Person: IDataErrorInfo { class PersonMetadata { [Required] [NameExists] [CustomValidation(typeof(CustomerValidationUtils), "CheckName")] public string Name { get; set; } [Range(19, 99, ErrorMessage = "年龄必须在18岁以上。")] public string Age { get; set; } } public string Error { get { throw new NotImplementedException(); } } public string this[string columnName] { get { return this.ValidateProperty<PersonMetadata>(columnName); } } }
ValidateProperty 方法,是一个基于 object 类型的扩展方法。通过泛型<MetadataType>指定增强信息的类型。
public static class ValidationExtension { public static string ValidateProperty (this object obj, string propertyName) { if (string.IsNullOrEmpty(propertyName)) return string.Empty; var targetType = obj.GetType(); //你也可以利用 MetadataType 在分离类上声明 //var targetMetadataAttr = targetType.GetCustomAttributes(false) // .FirstOrDefault(a => a.GetType() == typeof(MetadataTypeAttribute)) as MetadataTypeAttribute; //if (targetMetadataAttr != null && targetType != targetMetadataAttr.MetadataClassType) //{ // TypeDescriptor.AddProviderTransparent( // new AssociatedMetadataTypeTypeDescriptionProvider(targetType, targetMetadataAttr.MetadataClassType), targetType); //} if (targetType != typeof(MetadataType)) { TypeDescriptor.AddProviderTransparent( new AssociatedMetadataTypeTypeDescriptionProvider(targetType, typeof(MetadataType)), targetType); } var propertyValue = targetType.GetProperty(propertyName).GetValue(obj, null); var validationContext = new ValidationContext(obj, null, null); validationContext.MemberName = propertyName; var validationResults = new List (); Validator.TryValidateProperty(propertyValue, validationContext, validationResults); if (validationResults.Count > 0) { return validationResults.First().ErrorMessage; } return string.Empty; } }
利用这一思路,可以很容易的实现各种 POCO Model 类型的校验逻辑分离,这对于开发分层架构的应用框架时非常有用。
INotifyDataErrorInfo验证机制
.NET Framework 4.5 引入了一个新的系统.ComponentModel.INotifyDataErrorInfo 接口 – 自版本 4 以来,Silverlight 中一直存在相同的接口 – 它使您能够异步执行服务器端验证,然后在验证完成后通过引发错误更改事件来通知视图。同样,它可以在设置另一个属性时使一个属性无效,并且它还支持为每个属性设置多个错误以及除 System.String (string) 以外的其他类型的自定义错误对象。
public interface INotifyDataErrorInfo
{
bool HasErrors { get; }//提供给Validation 使用,对应Validation.HasError
event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
IEnumerable GetErrors(string propertyName);//提供给Validation 使用,对应Validation.Errors
}
INotifyDataErrorInfo示例
public class BaseViewModel1 : ViewModelBase, INotifyDataErrorInfo { protected Dictionary<string, string> errorList = new Dictionary<string, string>(); public bool HasErrors => errorList.Count > 0; public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; public IEnumerable GetErrors(string propertyName) { if (errorList.ContainsKey(propertyName)) { yield return errorList[propertyName]; } } }
private string email; public string Email { get { return email; } set { if (string.IsNullOrWhiteSpace(value)) { errorList.Add("Email", "Email不能为空"); return; } if (value != email) { email = value; RaisePropertyChanged(() => this.Email); } } }
<TextBox Grid.Row="1" Grid.Column="1" Width="200" HorizontalAlignment="Left" Style="{DynamicResource validationTextBoxStyle}" Text="{Binding Email, Mode=TwoWay, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
INotifyDataErrorInfo高级示例(使用特性——Attribute实现验证)
<TextBox Grid.Row="1" Grid.Column="1" Width="200" HorizontalAlignment="Left" Style="{DynamicResource validationTextBoxStyle}" Text="{Binding Email, Mode=TwoWay, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
其实ValidatesOnNotifyDataErrors默认是true,不用设置也可以,下面的RequiredLooseAttribute继承自:ValidationAttribute,在
System.ComponentModel.DataAnnotations命名空间下,需要引用该dll。
/// <summary> /// 默认允许为null和string.empty,不允许为空白字符 /// </summary> [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class EmailAddressEmptyAttribute : RequiredLooseAttribute { protected override bool IsValidString(string inputString) { string regPattern = @"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$"; Regex reg = new Regex(regPattern); return reg.IsMatch(inputString); } }
public class ExceptionValidationViewModel : BaseViewModelWithValidation { private string email = "11"; [EmailAddressEmpty(AllowNullOrEmptyString = false, AllowWhiteSpace = false, ErrorMessage = "请输入合法的Email")] public string Email { get { return email; } set { if (value != email) { email = value; RaisePropertyChanged(() => this.Email); } } } }
public abstract class BaseViewModelWithValidation : BaseViewModel, INotifyDataErrorInfo { private Dictionary<string, List<string>> allErrorList = new Dictionary<string, List<string>>(); public bool HasErrors => allErrorList != null && allErrorList.Count > 0; public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; protected void NotifyErrorsChanged(string propName) { ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propName)); } public IEnumerable GetErrors(string propertyName) { if (allErrorList != null && allErrorList.ContainsKey(propertyName)) { List<string> errors = allErrorList[propertyName]; foreach (var item in errors) { yield return item; } } yield break; } public override void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression) { base.RaisePropertyChanged(propertyExpression); ValidateProperty(propertyExpression); } protected bool ValidateProperty<T>(Expression<Func<T>> propExpression) { string propName = GetPropertyName(propExpression); object propValue = GetType().GetProperty(propName).GetValue(this); bool isValid = ValidateProperty(propName, propValue); return isValid; } protected bool ValidateProperty(string propName, object value) { var validationResults = new List<ValidationResult>(); ValidationContext context = new ValidationContext(this) { MemberName = propName }; bool isValid = Validator.TryValidateProperty(value, context, validationResults); if (isValid) { RemoveErrorsForProperty(propName);//移除所有的Error } else { AddErrorsForProperty(propName, validationResults);//添加Error } return isValid; } }
总结
通过三种验证方式的比较,实际项目中用的最多的就是后两种,IDataErrorInfo和INotifyDataErrorInfo,即实现好通用的ViewModel基类之后,使用特性Attribute来给Property校验。
值得一提的是,IDataErrorInfo也可以通过自定义Attribute来实现验证,通过实现IDataErrorInfo的BaseViewModel中通过反射获取属性的Attribute实现
因为Exception方式要对每个属性都写单独的验证代码(set方法中或者写ValidationRule),而且抛出异常本身就会影响性能,加剧WPF程序的性能问题(本身就够臃肿了……)
DataAnnotation验证机制验证机制
通过RIA服务暴露的实体可以使用特性标记来进行验证,很方便,但是常规的元数据类如果使用这种标记则不会正常实现验证功能,需要做一些设置.Silverlight的DataAnnotation验证机制,在添加验证属性后,不需要在Setter中进行验证判断,仅需要在Setter中激活该验证属性即可,而要实现激活验证,则需要使用ValidationContext和Validator类。为了更好的理解Silverlight DataAnnotation验证机制,我们来对这两个类进行简单的讲解.
首先说说Validator类,该类是一个静态类,主要用来当数据成员被指定验证元数据属性时,验证对象,属性和方法。简单的理解就是包含了各种具体验证方法的类。例如Require验证属性,Validator类将会根据该验证属性执行对应的验证方法,对目标值进行判断。在该类中,包含ValidateProperty方法(验证错误返回异常)和TryValidateProperty方法(验证错误返回False),可以分别对当前属性进行验证操作。
而ValiationContext类,该类是对当前执行的数据验证提供上下文描述的。简单的理解,也就是为验证提供数据传输,属性标识等任务。
系统自带的验证特性,继承ValidationAttribute,也可以自定义。
CompareAttribute:用来对比数据模型中另一个字段是否与套用的字段一致。此属性可用在需要输入两次密码的窗体上,也就是在会员注册页面时,可能会需要输入两次相同的密码,避免使用者的输入错误
CustomValidationAttribute:一个类,提供一个验证方法,改方法返回值必须是ValidationResult。
MaxLengthAttribute:验证字符串和数组属性的长度
MinLengthAttribute:验证字符串和数组属性的长度
RangeAttribute:验证数字范围
RegularExpressionAttribute:用于正则匹配验证字符串,指定匹配的正则规则。
RequiredAttribute:必填, required 属性规定必须在提交之前填写输入域(不能为空)
StringLengthAttribute:用于字符串属性,判断字符长度范围
MembershipPasswordAttribute:验证密码字段是否符合成员资格提供者当前的密码需求。此属性可用在密码输入字段,通过Membership 提供者所定义的密码复杂度要求进行检查
Required验证属性
private string _email; [Required(ErrorMessage = "你不想提交数据之前输入一个值")] public string email { get{ return _email ;} set { var tempValidator=new ValidationContext(this, null ,null ); tempValidator.MebmerName="email"; Validator.ValidateProperty(value,tempValidator); _email = value; }
StringLength验证属性
public class Model { [Required(ErrorMessage = "You must enter a username.")] [StringLength(10, MinimumLength = 4, ErrorMessage = "The username must be between 4 and 10 characters long")] [RegularExpression(@"^[a-zA-Z]+$", ErrorMessage = "The username must only contain letters (a-z, A-Z).")] public string Username { get; set; } [Required(ErrorMessage = "You must enter a name.")] public string Name { get; set; } }
RangeAttribute验证属性
using System; using System.Web.DynamicData; using System.ComponentModel.DataAnnotations; using System.ComponentModel; [MetadataType(typeof(ProductMetaData))] public partial class Product { } public class ProductMetaData { [Range(10, 1000, ErrorMessage = "Value for {0} must be between {1} and {2}.")] public object Weight; [Range(300, 3000)] public object ListPrice; [Range(typeof(DateTime), "1/2/2004", "3/4/2004", ErrorMessage = "Value for {0} must be between {1} and {2}")] public object SellEndDate; }
RegularExpression
验证属性
using System; using System.Web.DynamicData; using System.ComponentModel.DataAnnotations; [MetadataType(typeof(CustomerMetaData))] public partial class Customer {
//poco类型,单纯的属性和字段,验证信息存放在 元数据中 } //作为Customer的元数据类型,存放的是验证信息 public class CustomerMetaData { // Allow up to 40 uppercase and lowercase // characters. Use custom error. [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$", ErrorMessage = "Characters are not allowed.")] public object FirstName; // Allow up to 40 uppercase and lowercase // characters. Use standard error. [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")] public object LastName; }
CustomerValidationAttribute验证属性
先实现一个 public static 的校验方法(必须返回 ValidationResult )
public class CustomerValidationUtils { public static ValidationResult CheckName(string value) { if (value.Length < 8) { return new ValidationResult("名字长度必须大于等于8位。"); } return ValidationResult.Success; } }
然后在 Person 的 Name 属性上加上 CustomerValidation 特性:
[CustomValidation(typeof(CustomerValidationUtils), "CheckName")] public string Name { get { return _name; } set { if (_name != value) { _name = value; RaisePropertyChanged("Name"); } } }
在上文代码中,我们定义一个ValidationContext实例,该实例中包含了需要验证对象的引用,并且,我们定义了验证对象的MemberName,通过调用Validator.ValidateProperty静态方法,检查目标数据是否符合当前验证属性,如果返回False,则抛出一个ValidationException。
上面代码也可简写为:
Validator.ValidateProperty(value, new ValidationContext(this, null, null) { MemberName = "email" });
再比如:
private string _password; [StringLength(6, ErrorMessage="密码不能超过6个字符")] public string password { get { return _password; } set { Validator.ValidateProperty(value, new ValidationContext(this, null, null) { MemberName = "password" }); _password = value; } }
可以看到,除了实现上稍有麻烦,使用方法都是类似的。
对象验证
使用Validator类进行对象验证通常都在IEditableObject接口的EndEdit方法中进行,在该方法中可以调用下列返回值为Boolean的方法,如果返回True则正常更新,如果返回值为False,则处理验证结果:
public bool Validate() { var validationContext = new ValidationContext(this, null, null); var validationResults = new List<ValidationResult>(); bool isValid = Validator.TryValidateObject(this, validationContext, validationResults, true); _validationErrors.Clear(); foreach (ValidationResult result in validationResults) { foreach (string propertyName in result.MemberNames) { List<string> errors = null; if (_validationErrors.ContainsKey(propertyName)) { errors = _validationErrors[propertyName]; } else { errors = new List<string>(); _validationErrors[propertyName] = errors; } errors.Add(result.ErrorMessage); if (ErrorsChanged != null) ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName)); } } return isValid; }
自定义ValidateAttribute
.net 提供的 ValidateAttribute不够用怎么搞?自定义呗,
public class PriceAttribute : ValidationAttribute { public double MinPrice { get; set; } public override bool IsValid(object value) { if (value == null) { return false; } var price = (double)value; if (price < MinPrice) { return false; } return true; } public override string FormatErrorMessage(string name) { return "Min Price is "+MinPrice; } } //使用方法和.net 提供的一样:
public class Product
{
[Required]
[StringLength(10,MinimumLength =5)]
public string Name { get; set; }
[Required]
[Price(MinPrice =2)]
public decimal? UnitPrice { get; set; }
}
将数据验证和数据内容分开
实际应用中遇到的问题:
1、在实际开发中,我们还经常使用 EF 等 ORM 来做数据访问层,Model 通常会由这个中间件自动生成(利用T4等代码生成工具)。而他们通常是 POCO 数据类型,这时候如何能把属性的校验特性加入其中呢。
2、会误操作将属性给删除。
【如何解决】
.EF Db first中使用Data Annotation
巧妙使用partial 类,将数据和验证分开
//数据验证
public class ProductMetaData
{
[Required]
[StringLength(10, MinimumLength = 5)]
public string Name { get; set; }
[Required]
[Price(MinPrice = 2)]
public decimal? UnitPrice { get; set; }
}
/*
或者
[MetadataType(typeof(ProductMetaData))]
public partial class Product
{
}
//poco
public partial class Product { public string Name { get; set; } public decimal? UnitPrice { get; set; } }
*/
//poco 数据
[MetadataType(typeof(ProductMetaData))]
public partial class Product { public string Name { get; set; } public decimal? UnitPrice { get; set; } }
这样关联之后,可以做一些初始化的工作,还可以做一些验证,以及自定义的验证错误信息,相当于是扩展了Model实体
因为做验证都是在Model层做的,这样好维护,所以需要做元数据,与ADO.NET实体数据模型关联起来。
如何使用这些信息 为我们自己所用呢?
wpf中应用程序中
private string _password; [StringLength(6, ErrorMessage="密码不能超过6个字符")] public string password { get { return _password; } set { Validator.ValidateProperty(value, new ValidationContext(this, null, null) { MemberName = "password" }); _password = value; } }
控制台应用程序中
添加辅助类:
public class ModelValidationError
{
public string FieldName { get; set; }
public string Message { get; set; }
}
public static class DataAnnotationHelper
{
public static IEnumerable<ModelValidationError> IsValid<T>(this T o)
{
var descriptor = GetTypeDescriptor(typeof(T));
foreach (PropertyDescriptor propertyDescriptor in descriptor.GetProperties())
{
var validations = propertyDescriptor.Attributes.OfType<ValidationAttribute>();
foreach (var validationAttribute in validations)
{
var v = propertyDescriptor.GetValue(o);
if (!validationAttribute.IsValid(v))
{
yield return new ModelValidationError() { FieldName = propertyDescriptor.Name, Message = validationAttribute.FormatErrorMessage(propertyDescriptor.Name) };
}
}
}
}
private static ICustomTypeDescriptor GetTypeDescriptor(Type type)
{
return new AssociatedMetadataTypeTypeDescriptionProvider(type).GetTypeDescriptor(type);
}
}
如何使用:
class Program
{
static void Main(string[] args)
{
Product product = new Product();
foreach (var item in product.IsValid())
{
Console.WriteLine("FieldName:{0} Error Message:{1}", item.FieldName, item.Message);
}
Console.ReadKey();
}
}
DataAnnotatio+IDataErrorInfo验证机制
效果如下