【WPF】数据绑定与数据验证
数据验证概述
数据验证在任何用户界面程序中都是不可缺少的一部分.在WPF中,数据验证更是和绑定紧紧联系在一起,下面简单介绍MVVM模式下常用的几种验证方式.,在本文中,您将了解如何使用 IDataErrorInfo 接口实现、ValidationRules、BindingGroups、异常以及与验证相关的附加特性和事件来满足数据验证需要。 错误信息显示 在介绍数据验证之前,有必要介绍一下如何显示错误信息.方式很简单,定义一个样式触发器。微软为WPF提供了ValidationRule类,用于数据绑定的数据验证。WPF有多种数据验证的方式。
几乎每当您在应用程序中输入或修改数据时,都需要确保数据是有效的,以避免与这些更改的来源(在这种情况下为用户)相去甚远。而且,您需要在用户输入的数据无效时向他们提供清晰指示,还能够向其提供一些有关如何更正数据的指示。只要您知道需使用何种功能以及何时使用,便可通过 WPF 相当轻松地完成这些任务。
在使用 WPF 中的数据绑定来呈现业务数据时,通常应使用 Binding 对象在目标控件的单个属性与数据源对象属性之间提供数据管道。若要使验证是相关的,通常需进行 TwoWay 数据绑定 — 这意味着,除了从源属性流向目标属性以进行显示的数据之外,编辑过的数据也会从目标流向源,如图 1 所示。
图 1 TwoWay 数据绑定中的数据流
可使用三种机制来确定通过数据绑定控件输入的数据是否有效。图 2 对这些机制进行了总结。
图 2 绑定验证机制
验证机制 | 说明 |
异常 | 通过在某个 Binding 对象上设置 ValidatesOnExceptions 属性,如果在尝试对源对象属性设置已修改的值的过程中引发异常,则将为该 Binding 设置验证错误。 |
ValidationRules | Binding 类具有一个用于提供 ValidationRule 派生类实例的集合的属性。这些 ValidationRules 需要覆盖某个 Validate 方法,该方法由 Binding 在每次绑定控件中的数据发生更改时进行调用。如果 Validate 方法返回无效的 ValidationResult 对象,则将为该 Binding 设置验证错误。
提供创建自定义规则的一个方式,旨在检查用户输入的有效性。WPF 数据绑定模型使你可以与对象Binding相关联ValidationRules,将数据验证放置在Mvvm的View层(将数据验证前置到UI层)。IDataErrorInfo 的优点是UI层单属性验证 验证用户输入的数据,不验证绑定源的数据变更。 |
IDataErrorInfo | 通过在绑定数据源对象上实现 IDataErrorInfo 接口并在 Binding 对象上设置 ValidatesOnDataErrors 属性,Binding 将调用从绑定数据源对象公开的 IDataErrorInfo API。如果从这些属性调用返回非 null 或非空字符串,则将为该 Binding 设置验证错误。通过在绑定数据源对象上实现 IDataErrorInfo 接口并在 Binding 对象上设置 ValidatesOnDataErrors 属性,Binding 将调用从绑定数据源对象公开的 IDataErrorInfo API。如果从这些属性调用返回非 null 或非空字符串,则将为该 Binding 设置验证错误。IDataErrorInfo 的优点是,它可用于轻松地处理交叉耦合属性 |
方法三、FluentValidation类
方法四、ValidationAttribute类
当用户在 TwoWay 数据绑定中输入或修改数据时,将启动以下工作流:
- 用户通过击键、鼠标、触摸或与各元素间的手写笔交互来输入或修改数据,从而更改元素的属性。
- 如果需要,可将数据转换为数据源属性类型。
- 设置源属性值。
- 触发 Binding.SourceUpdated 附加事件。
- 如果数据源属性上的 setter 引发异常,则异常会由 Binding 捕获,并可用于指示验证错误。
- 如果实现了 IDataErrorInfo 属性,则会对数据源对象调用这些属性。
- 向用户呈现验证错误指示,并触发 Validation.Error 附加事件。
如您所见,该过程中有多个位置可以产生验证错误,具体取决于所选择的机制。列表中未显示触发 ValidationRule 的位置。这是因为,根据为 ValidationRule 上的 ValidationStep 属性设置的值,可以在该过程中的各个位置触发 ValidationRule,包括在类型转换之前、转换之后、更新属性之后或提交更改的值时(如果数据对象实现 IEditableObject)。默认值为 RawProposedValue,它在类型转换之前发生。数据从目标控件属性类型转换为数据源对象属性类型的位置通常隐式产生,不会触及代码的任何部分(如 TextBox 中的数字输入)。此类型转换过程可能引发异常,这些异常应该用于向用户指示验证错误。
如果无法将值写入源对象属性,则该值显然是无效输入。如果选择挂接 ValidationRules,则会在该过程中由 ValidationStep 属性指示的位置处调用 ValidationRules,它们可基于嵌入其中或从其调用的任何逻辑来返回验证错误。如果源对象属性 setter 引发异常,则几乎应总是将该异常视为验证错误,这与类型转换的情况相同。
最后,如果实现 IDataErrorInfo,则将针对为了基于从接口返回的字符串来检查是否存在验证错误而设置的属性,来调用向该接口的数据源对象添加的索引器属性。稍后我会更详细地介绍每种机制。
您必须决定需要何时进行验证。验证将在 Binding 向基础源对象属性写入数据时进行。何时进行验证由 Binding 的 UpdateSourceTrigger 属性来指定,对于大多数属性,该属性设置为 PropertyChanged。某些属性(如 TextBox.Text)会将该值更改为 FocusChange,这意味着验证将在焦点离开正用于编辑数据的控件时发生。也可将该值设置为 Explicit,这意味着必须对绑定显式调用验证。在本文后面讨论的 BindingGroup 将使用 Explicit 模式。
在验证方案中(尤其是对于 TextBoxes),通常需要立即向用户提供反馈。若要对此提供支持,应将 Binding 上的 UpdateSourceTrigger 属性设置为 PropertyChanged:
Text="{Binding Path=Activity.Description, UpdateSourceTrigger=PropertyChanged}
事实证明,对于许多实际验证方案,您需要利用这些机制中的多种机制。根据您所关心的验证错误类型以及验证逻辑的位置,每种机制各有其优缺点。
数据验证要解决的问题
1、 单个属性级别的简单规则 数据验证, 单属性验证优先使用ValidationRule类
2、在交叉耦合属性中,一个属性的有效性取决于另一个属性的值。然而,通过 WPF 数据绑定中的验证支持,可以轻松地解决这些难题。多属性交叉耦合优先使用IDataErrorInfo
ValidationRule 方式验证数据
1、{bing}表达式设置
{Bing }表达式提供了对数据验证的支持,以下是bing属性:
ValidatesOnDataErrors属性:默认false,设置为true开启数据验证
ValidationRules属性:验证规则,该属性是Collection<ValidationRule>集合。自定义验证规则需要继承ValidationRule类然后重写
Validate()方法。
Validation.HasError属性:默认false,判断是否有错误信息、再控件触发器中用于触发显示错误信息。
2、控件设置
设置数据错误信息的显示、显示样式、触发 。以下用给TextBox 控件添加数据验证为例加以说明。
Validation.ErrorTemplate附加属性:是ControlTemplate类型,给控件添加附加模板属性,用于显示 证信息样式。例如:给textbox添加 显示验证信息的模板
<TextBox Validation.ErrorTemplate="{StaticResource validationTemplate}"
错误信息模板
<ControlTemplate x:Key="validationTemplate"> <DockPanel> <TextBlock Foreground="Red" FontSize="20">!</TextBlock> <AdornedElementPlaceholder/> </DockPanel> </ControlTemplate>
textbox触发器设置,触发显示错误信息,显示错误消息的 ToolTip 使用名为 textBoxInError
的样式创建。 如果 HasError 的值为 true
,则触发器将当前 TextBox 的工具提示设置为其首个验证错误。 将 RelativeSource 设置为 Self,并引用当前元素。
<Style x:Key="textBoxInError" TargetType="{x:Type TextBox}"> <Style.Triggers> <Trigger Property="Validation.HasError" Value="true"> <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)/ErrorContent}"/>
</Trigger> </Style.Triggers> </Style>
3、定义绑定规则
public class AgeRangeRule : ValidationRule { public int Min { get; set; } public int Max { get; set; } public AgeRangeRule() { } public override ValidationResult Validate(object value, CultureInfo cultureInfo) { int age = 0; try { if (((string)value).Length > 0) age = Int32.Parse((String)value); } catch (Exception e) { return new ValidationResult(false, $"Illegal characters or {e.Message}"); } if ((age < Min) || (age > Max)) { return new ValidationResult(false, $"Please enter an age in the range: {Min}-{Max}."); } return ValidationResult.ValidResult; } }
4、应用
<TextBox Name="textBox1" Width="50" FontSize="15" Validation.ErrorTemplate="{StaticResource validationTemplate}" Style="{StaticResource textBoxInError}" Grid.Row="1" Grid.Column="1" Margin="2"> <TextBox.Text> <Binding Path="Age" Source="{StaticResource ods}" UpdateSourceTrigger="PropertyChanged" > <Binding.ValidationRules> <c:AgeRangeRule Min="21" Max="130"/> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox>
效果
IDataErrorInfo 验证
IDataErrorInfo 接口需要实现者公开一个属性和一个索引器:
public interface IDataErrorInfo
{
string Error { get; }//只有在将对象显示在 DataGrid 或 BindingGroup 中的这种方案中,才使用 Error 属性。Error 属性用于指示行级别的错误 string this[string propertyName] { get; } //而索引器用于指示单元格级别的错误。
}
Error 属性用于指示整个对象的错误,而索引器用于指示单个属性级别的错误。两者的工作原理相同:如果返回非 null 或非空字符串,则表示存在验证错误。此外,返回的字符串可用于向用户显示错误(我将在后面加以说明)。
当使用绑定到数据源对象上的各个属性的各个控件时,该接口的最重要部分就是索引器。只有在将对象显示在 DataGrid 或 BindingGroup 中的这种方案中,才使用 Error 属性。Error 属性用于指示行级别的错误,而索引器用于指示单元格级别的错误。
实现 IDataErrorInfo 具有一个很大的弊端:索引器的实现通常会导致较大的 switch-case 语句(对象中的每个属性名称都对应于一种情况),您必须基于字符串进行切换和匹配,并返回指示错误的字符串。而且,在对象上设置属性值之前,不会调用 IDataErrorInfo 的实现。如果其他对象订阅了该对象上的 INotifyPropertyChanged.PropertyChanged,则已通知这些对象发生了更改,它们可能已基于 IDataErrorInfo 实现要声明为无效的数据开始工作。如果这对于您的应用程序可能是个问题,则需要在对设置的值不满意时从属性 setter 引发异常。
IDataErrorInfo 的优点是,它可用于轻松地处理交叉耦合属性。例如,除了使用 ValidationRule 来验证 Inventory 字段的输入格式之外,还应记住这一要求:当 ActivityType 为 Install 时,必须填写 Inventory 字段。ValidationRule 本身无权访问数据绑定对象上的其他属性。它只是传递为 Binding 挂接的属性所设置的值。若要满足此要求,当设置了 ActivityType 属性时,您需要使验证在 Inventory 属性上发生,并在 ActivityType 设置为 Install 而 Inventory 的值为空时返回无效结果。
若要实现此目的,您需要使用 IDataErrorInfo,以便可在评估 Inventory 时检查 Inventory 和 ActivityType 属性,如下所示:
public string this[string propertyName] { get { return IsValid(propertyName); } } private string IsValid(string propertyName) { switch (propertyName) { ... case "Inventory": if (ActivityType != null && ActivityType.Name == "Install" && string.IsNullOrWhiteSpace(Inventory)) return "Inventory expended must be entered for installs"; break; }
此外,您需要让 Inventory Binding 在 ActivityType 属性发生更改时调用验证。通常,仅当 UI 中的该属性发生更改时,Binding 才查询 IDataErrorInfo 实现或调用 ValidationRules。在本例中,即使 Inventory 属性尚未更改但相关 ActivityType 已更改,我也需要触发 Binding 验证的重新评估。
可通过两种方式让 Inventory Binding 在 ActivityType 属性发生更改时进行刷新。第一种(也是较为简单)的方式是在设置 ActivityType 时为 Inventory 发布 PropertyChanged 事件:
ActivityType _ActivityType; public ActivityType ActivityType { get { return _ActivityType; } set { if (value != _ActivityType) { _ActivityType = value; PropertyChanged(this, new PropertyChangedEventArgs("ActivityType")); PropertyChanged(this, new PropertyChangedEventArgs("Inventory")); } } }
这会使 Binding 刷新并重新评估该 Binding 的验证。
第二种方式是在 ActivityType ComboBox 或其父元素之一的上面挂接 Binding.SourceUpdated 附加事件,并从该事件的代码隐藏处理程序来触发 Binding 刷新:
<ComboBox Name="activityTypeIdComboBox" Binding.SourceUpdated="OnPropertySet"... private void OnPropetySet(object sender, DataTransferEventArgs e) { if (activityTypeIdComboBox == e.TargetObject) { inventoryTextBox.GetBindingExpression( TextBox.TextProperty).UpdateSource(); } }
以编程方式调用 Binding 上的 UpdateSource 会使其将绑定目标元素中的当前值写入源属性,从而如同用户刚刚编辑过控件那样来触发验证链。