在上一篇文章中我们讨论了有关WPF绑定的知识点,现在我们可以很容易的将业务数据作为源绑定到WPF控件并可以通过创建不同的Data Template后来以特定的样式展现。而作为这个最常用的功能我们可以通过Two Way的绑定模式与界面交互,而在这个时候我们就需要类似于ASP.NET中Validator一样的东西来限制或者说校验数据有效性。ValidationRule就是为这样的应用而生的。本文将详细讨论如何应用验证规则和自定义错误模板。
我们首先来看看WPF中有关数据验证规则相关的几个常用类:
· ValidationRule : 所有自定义验证规则的基类。提供了让用户定义验证规则的入口。
· ExceptionValidation :表示一个规则,该规则检查在绑定源属性更新过程中引发的异常。它是一个内置的规则,它检查在绑定源属性更新过程中引发的异常。
· ValidationResult : 数据验证结果的表现方式。ValidationRule对象的Validate方法执行完毕后通过ValidationResult来表示验证的结果。这里包含了错误信息—ErrorContent,数据是否有效—IsValid。ValidResult 为 ValidationResult 的有效实例。
· ValidationError :表示一个验证错误,该错误在 ValidationRule 报告验证错误时由绑定引擎创建。
对于WPF中绑定的验证和转换值我们需要注意:
1. 在将值从目标属性传输到源属性时,数据绑定引擎首先移除可能已添加到所绑定元素的 Validation.Errors 附加属性的任何 ValidationError。然后,数据绑定引擎检查是否为该 Binding 定义了自定义验证规则;如果定义了自定义验证规则,那么它将调用每个 ValidationRule 上的 Validate 方法,直到其中一个规则失败或者全部规则都通过为止。如果某个自定义规则未通过,则绑定引擎会创建一个 ValidationError 对象,并将该对象添加到绑定元素的 Validation.Errors 集合。如果 Validation.Errors 不为空,则元素的 Validation.HasError 附加属性被设置为 true。此外,如果 Binding 的 NotifyOnValidationError 属性设置为 true,则绑定引擎将引发该元素上的 Validation.Error 附加事件。
2. 如果所有规则都通过,则绑定引擎会调用转换器(如果存在的话)。
3. 如果转换器通过,则绑定引擎会调用源属性的 setter。
4. 如果绑定具有与其关联的 ExceptionValidationRule,并且在步骤 3 或 4 中引发异常,则绑定引擎将检查是否存在 UpdateSourceExceptionFilter。使用 UpdateSourceExceptionFilter 回调可以提供用于处理异常的自定义处理程序。如果未对 Binding 指定 UpdateSourceExceptionFilter,则绑定引擎将对异常创建 ValidationError 并将其添加到绑定元素的 Validation.Errors 集合中。
任何方向(目标到源或源到目标)的有效值传输操作都将清除 Validation.Errors 附加属性。
· 简单验证:使用ExceptionValidationRule
对于大多数验证来说我们都是在验证用户输入。ExceptionValidateRule作为WPF内置的简单验证器可以捕捉在绑定上发生的任何异常。我们可以用ExceptionValidateRule来作为一个笼统的错误收集器,来暴露出内部数据验证规则的异常信息。
模拟一个Employee信息更改的窗体。假设我们定义了一个Employee实体类,在实体类中我们对数据的有效性都做了简单的验证。
public string Title { get { return strTitle; } set { strTitle = value; if (String.IsNullOrEmpty(strTitle)) { throw new ApplicationException("Please input Title."); } } } public DateTime Birthday { get { return objBirthday; } set { objBirthday =value; if (objBirthday.Year >= DateTime.Now.Year) { throw new ApplicationException("Please enter a valid date."); } } } |
在XAML中添加对字段的绑定,并对各个单独的控件的Text属性设置Bindg.ValidationRules. 这样当这个绑定的实体对象对应的属性出现验证错误时,错误信息会被ExceptionValidationRule抛出来。其默认行为是在当前控件LostFocus时触发验证,并将出错源控件加亮。
<TextBox Grid.Column="1" Grid.Row="2" Width="200" HorizontalAlignment="Left"> <TextBox.Text> <Binding Path="Title"> <Binding.ValidationRules> <ExceptionValidationRule /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> |
上边的验证是正确了,但是我们并不知道具体的错误信息。这对于一个String类型的字段来说可能是可以理解的,我可以试出来。但对于特殊的字段来说,我们根本无法得知到底发生了什么,我怎么填写数据才能正确。那么如何去控制显示呢?
· 自定义错误验证规则和定制显示
我们在上一篇里边提到过,所有的验证其实是有一个System.Windows.Controls.Validation类来完成的,对于这个类我们可以看到它有几个附加属性和一个附加事件Error.所谓附加事件/属性就是说可以被加载到另外的控件上来触发某个对象的事件或操作其属性,类似于我们有一个接口,通过这个接口你可以操作别的对象。在这里我们着重需要注意下边几个属性:
Gets the collection of all active ValidationError objects on the bound element. |
|
Gets a value that indicates whether any binding on the binding target element has a ValidationError. |
|
Gets or sets the ControlTemplate used to generate validation error feedback on the adorner layer. |
回想一下上一篇的最后部分,我们用到了触发器,用触发器来判断HasError属性,然后设置其错误模板。很容易我们可以做到这一点,这也是我们经常用到的办法。为了其复用性,我们可以将它定义为一个公用的Style。
<Style TargetType="{x:Type TextBox}"> <Style.Triggers> <Trigger Property="Validation.HasError" Value="true"> <Setter Property="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <DockPanel LastChildFill="True"> <TextBlock DockPanel.Dock="Right" Foreground="Red" FontSize="12pt" Text="{Binding ElementName=MyAdorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"> </TextBlock> <Border BorderBrush="Red" BorderThickness="1"> <AdornedElementPlaceholder Name="MyAdorner" /> </Border> </DockPanel> </ControlTemplate> </Setter.Value> </Setter> </Trigger> </Style.Triggers> </Style> |
样式属于一个implicit key的样式,表明它针对的是所有的TargetType所指明的类型,这里是TextBox. 有个问题是,我们想在这里边的值改变时就需要触发验证而不是LostFocus,怎么改变呢?Binding对象有一个属性叫UpdateSourceTrigger,这是一个枚举值来指定什么时候触发数据源更新,更新的时候才会调用验证。
Members |
Description |
Default |
The default UpdateSourceTrigger value of the binding target property. The default value for most dependency properties is PropertyChanged, while the Text property has a default value of LostFocus. |
PropertyChanged |
Updates the binding source immediately whenever the binding target property changes. |
LostFocus |
Updates the binding source whenever the binding target element loses focus. |
Explicit |
Updates the binding source only when you call the UpdateSource method. |
<Binding Path="Title" UpdateSourceTrigger="PropertyChanged">就可以帮我们完成任务。
接下来我们可以定义一个对象化一些的错误处理类了。定义自己的规则来验证数据是ValidationRule所处理的主要任务,扩展这些规则就要使我们的类继承于这个父类:
public class EnumValidationRule : ValidationRule { private string _enumClass; private string _errorMessage; public string EnumClass { get { return "Allan.WPFBinding.ValidationDemo." + _enumClass; } set { _enumClass = value; } } public string ErrorMessage { get { return _errorMessage; } set { _errorMessage = value; } } public override ValidationResult Validate(object value, CultureInfo cultureInfo) { ValidationResult result = new ValidationResult(true, null); string inputString = (value ?? string.Empty).ToString(); Type type = Type.GetType(EnumClass); if (string.IsNullOrEmpty(inputString) || !ValidateInput(type,inputString)) { result = new ValidationResult(false, this.ErrorMessage); } return result; } } |
更改绑定参数设置:将验证规则换成EnumValidationRule并设置相关参数。
<Binding Path="Title" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:EnumValidationRule EnumClass="TitleEnum" ErrorMessage="输入值不存在。请重新输入。" />
</Binding.ValidationRules>
</Binding>
· 用BindingGroups实现对List的验证
到目前为止我们所有看到的都是针对每个单个的控件输入的验证,那么有没有办法对比如某个ListView的每个item进行绑定验证呢?用上边的方法是不可以的,否则我们也没必要针对每个空间都去那么繁琐的设置各种信息啊。随同.NET Framework SP1发布了新的功能点,那就是BindingGroups。BindingGroups是对list列表中的绑定之间的关系进行了封装,使得你在获得一个上下文对象时可以和BIdingGroups关联来得到与当前上下文对象相关联的所有绑定。而这个关系是你需要显式去设定的—通过BindingGroupName。
FrameworkElement和FrameworkContentElement有一个叫做BindingGroup的依赖属性,而ItemsControl继承于FrameworkElement,所以其具有BindingGroup属性。而对于BindingBase对象添加了BindingGroupName属性。
public class BindingBase { public string BindingGroupName { get; set; } } |
我们刚才讲过,BindingGroup是需要显式的去声明,并且对于一个BindingGroup来说,拿到它就相当于拿到了当前对象相关上下文的所有与其有关系的绑定。而它依然本质上又回到了基本的ValidationRule。
在上边的类图中我们可以看到几个很有意思的东西:
· BindingExpressions: 保存了与当前上下文有关的绑定对象。我们给DateContext中绑定了四个字段的值(对应四个控件),BindingExpressions就保存了所有归属到当前BindingGroup的所有Binding对象。例如你有四个字段,那就有四个BidingExpression,有5个字段绑定,你这里就有5个BindingExpression,即便他们很可能都是一样的。
· Items :这个属性保存了与当前上下文对象有关的绑定。例如,我们在示例程序中只绑定了一个单独的Employee对象,那么BindingExpressions里就保留了这个上下文对象。换句话讲,当我们按照对象的方式来多个验证的时候(因为Item在ListView, DataGrid等控件里就代表一个单个对象),我们所操作的对象传递到验证规则时是以BindingExpression的target来表示的。这时,你得到了行的源,就可以做你想做的事情了。
· ValidationRules : 关联到我们给bindingGroup加载的验证规则。
· BeginEdit, CancelEdit, CommitEdit : 实际上这些是和IEditableCollectionView关联的一些事件,表明对数据视图的操作。这里我们可以简单认为对数据进行更新和提交。
· UpdateSources : 这是一个方法,表明我们要调用ValidationRule来验证数据。
· ValidateWithoutUpdate : 方法:验证数据而不更新数据。
现在我们来应用BindingGroup给基于一个Form的绑定来做验证吧。首先是定义BindingGroup.
<Grid.BindingGroup> <BindingGroup> <BindingGroup.ValidationRules> <local:EmployeeValidationRule ValidationStep="ConvertedProposedValue" /> </BindingGroup.ValidationRules> </BindingGroup> </Grid.BindingGroup> |
注意这里应用了ValidationStep,和前边的UpdateSourceTrigger很相似,它是用来表示在什么时候来应用验证规则的。是个枚举值:
· RawProposedValue = 0, 在没有变化的原数据上验证
· ConvertedProposedValue = 1,用户更改过数据后验证 default
· UpdatedValue = 2, 在将数据提交给Source时验证
· CommittedValue = 3 在将数据提交到数据库后验证
接下来定义我们的规则。现在的规则是针对每个对象而不是单个属性的,所以与前边的有些许不同,但很容易,因为你可以拿到绑定到当前项的对象。
public override ValidationResult Validate(object value, CultureInfo cultureInfo) { BindingGroup bindingGroup = (BindingGroup)value; Employee emp = (Employee)bindingGroup.Items[0]; object startDateObj = bindingGroup.GetValue(emp, "StartDate"); DateTime? startDate = (DateTime)startDateObj; object endDateObj = bindingGroup.GetValue(emp, "EndDate"); DateTime? endDate = (DateTime)endDateObj; // check start and end date together if (startDate.Value > endDate.Value) { return new ValidationResult(false, string.Format("StartDate: {0}, cannot be greater than EndDate: {1}",startDate,endDate)); } else { return new ValidationResult(true, null); } } |
Ok了,你可以再去定义一下出错时的ErrorTempalte,然后去享受这样的成果了:)
点击这里获取DEMO:)