WPF MVVM 应用程序中的基于属性的验证






4.88/5 (30投票s)
描述了一种使用属性在 WPF MVVM 应用程序中执行用户输入验证的方法
引言
在本文中,我将分享一个简单的 MVVM 应用程序,它使用基于特性的方法进行验证。基本上,这意味着您将能够使用以下语法来描述验证规则
[Required(ErrorMessage = "Field 'FirstName' is required.")]
public string FirstName
{
get
{
return this.firstName;
}
set
{
this.firstName = value;
this.OnPropertyChanged("FirstName");
}
}
这是演示应用程序运行时的截图

演示应用程序的 UI 需求如下
- 在 3 个不同的选项卡中填写各种信息,并在字段不正确时向用户提供有意义的错误消息和反馈
- 使用 3 种方法实时反馈填写过程的完成度
- 当一个选项卡完全填写完毕后,其颜色会从红色变为绿色
- 使用进度条向用户显示进度
- 当所有内容都填写完毕后,“保存”按钮将被激活
- 当一个选项卡完全填写完毕后,其颜色会从红色变为绿色
背景
WPF 提供了几种技术来验证用户在应用程序中的输入。
ValidationRules
摘自 MSDN 关于 ValidationRules 的文章
当您使用 WPF 数据绑定模型时,您可以将 ValidationRules 与您的绑定对象关联。要创建自定义规则,需要创建此类的子类并实现 Validate 方法。每当绑定引擎将输入值(即绑定目标属性值)传输到绑定源属性时,它都会检查与绑定关联的每个 ValidationRule。
IDataErrorInfo
摘自 Marianor 关于将特性与 IDataErrorInfo
接口结合使用的原始博客文章
WPF 通过 IDataErrorInfo 接口为绑定场景提供了另一种验证基础结构。基本上,您必须实现 Item[columnName] 属性,为每个需要验证的属性放入验证逻辑。在 XAML 中,您需要将 ValidatesOnDataErrors 设置为 true,并决定何时希望绑定调用验证逻辑(通过 UpdateSourceTrigger)。
结合 IDataErrorInfo 和特性
在本文中,我们使用一种结合了验证特性(对每种验证特性的解释超出了本文的范围)和 IDataErrorInfo
接口的技术。
总体设计
这是应用程序类的类图

这里并没有什么真正的新东西。MainWindow
的 Content 被设置为 MainFormView
视图。其关联的 ViewModel
,即 MainFormViewModel
,管理一组 FormViewModel
,这些 FormViewModel
是视图中每个选项卡的基 ViewModel
类。
为了指定 WPF 引擎如何渲染 FormViewModelBase
,我们所需要做的就是创建一个 DataTemplate
并给出正确的 TargetType
。例如,为了将 ProfileFormViewModel
与 ProfileFormView
关联起来,我们创建了这个 DataTemplate
<DataTemplate DataType="{x:Type ViewModel:ProfileFormViewModel}">
<View:ProfileFormView />
</DataTemplate>
然后,DataBinding
会使用这个非常简单的 XAML 来完成填充 TabControl
内容的所有工作
<TabControl ItemsSource="{Binding Forms}" />
这与我几个月前在博客中介绍的一种方法非常相似(更多细节请参见这里)。新内容在于 ValidationViewModelBase
类,这是我为需要支持验证的 ViewModel
类引入的新基类。这也是本文下一节的目标。
基于特性的验证和 IDataError 接口
我在这里使用的结合特性和 IDataError
接口的解决方案是基于 Marianor 的工作(完整文章在此)。我对他的代码做了一些调整,以便得到一个通用的解决方案(这也是 ValidationViewModelBase
类的目标)。其基本思想是能够使用 System.ComponentModel
中的特性,以一种通用的方式实现 IDataErrorInfo
接口(及其两个属性)。该类广泛使用 LINQ 来执行验证。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using WpfFormsWithValidationDemo.Toolkit.Behavior;
namespace WpfFormsWithValidationDemo.Toolkit.ViewModel
{
/// <summary>
/// A base class for ViewModel classes which supports validation
/// using IDataErrorInfo interface. Properties must defines
/// validation rules by using validation attributes defined in
/// System.ComponentModel.DataAnnotations.
/// </summary>
public class ValidationViewModelBase : ViewModelBase,
IDataErrorInfo, IValidationExceptionHandler
{
private readonly Dictionary<string,
Func<ValidationViewModelBase, object>> propertyGetters;
private readonly Dictionary<string, ValidationAttribute[]> validators;
/// <summary>
/// Gets the error message for the property with the given name.
/// </summary>
/// <param name="propertyName">Name of the property</param>
public string this[string propertyName]
{
get
{
if (this.propertyGetters.ContainsKey(propertyName))
{
var propertyValue = this.propertyGetters[propertyName](this);
var errorMessages = this.validators[propertyName]
.Where(v => !v.IsValid(propertyValue))
.Select(v => v.ErrorMessage).ToArray();
return string.Join(Environment.NewLine, errorMessages);
}
return string.Empty;
}
}
/// <summary>
/// Gets an error message indicating what is wrong with this object.
/// </summary>
public string Error
{
get
{
var errors = from validator in this.validators
from attribute in validator.Value
where !attribute.IsValid(this.propertyGetters
[validator.Key](this))
select attribute.ErrorMessage;
return string.Join(Environment.NewLine, errors.ToArray());
}
}
/// <summary>
/// Gets the number of properties which have a
/// validation attribute and are currently valid
/// </summary>
public int ValidPropertiesCount
{
get
{
var query = from validator in this.validators
where validator.Value.All(attribute =>
attribute.IsValid(this.propertyGetters[validator.Key](this)))
select validator;
var count = query.Count() - this.validationExceptionCount;
return count;
}
}
/// <summary>
/// Gets the number of properties which have a validation attribute
/// </summary>
public int TotalPropertiesWithValidationCount
{
get
{
return this.validators.Count();
}
}
public ValidationViewModelBase()
{
this.validators = this.GetType()
.GetProperties()
.Where(p => this.GetValidations(p).Length != 0)
.ToDictionary(p => p.Name, p => this.GetValidations(p));
this.propertyGetters = this.GetType()
.GetProperties()
.Where(p => this.GetValidations(p).Length != 0)
.ToDictionary(p => p.Name, p => this.GetValueGetter(p));
}
private ValidationAttribute[] GetValidations(PropertyInfo property)
{
return (ValidationAttribute[])property.GetCustomAttributes
(typeof(ValidationAttribute), true);
}
private Func<ValidationViewModelBase, object>
GetValueGetter(PropertyInfo property)
{
return new Func<ValidationViewModelBase, object>
(viewmodel => property.GetValue(viewmodel, null));
}
private int validationExceptionCount;
public void ValidationExceptionsChanged(int count)
{
this.validationExceptionCount = count;
this.OnPropertyChanged("ValidPropertiesCount");
}
}
}
请注意,我还公开了有效属性的数量和带验证属性的总数(这用于计算进度条的值)。从开发者的角度来看,在现有的 ViewModel
中使用这个类非常直接:继承自新的 ValidationViewModelBase
类,而不是你传统的 ViewModelBase
类,然后在需要验证的属性上添加验证特性。
可用特性 (来自 System.ComponentModel.DataAnnotations
命名空间)
名称 | 描述 |
RequiredAttribute | 指定数据字段值为必需项 |
RangeAttribute | 指定数据字段值的数值范围约束 |
StringLengthAttribute | 指定数据字段中允许的最小和最大字符长度 |
RegularExpressionAttribute | 指定数据字段值必须与指定的正则表达式匹配 |
CustomValidationAttribute | 指定在运行时调用的自定义验证方法(您必须实现 IsValid() 方法) |
处理验证异常
在使用这种基于特性的新方法时,我面临一个问题:如何处理验证异常。当用户输入不正确时,就会发生验证异常,例如,如果一个 TextBox
的 Text
属性绑定到一个 int 属性,那么像“abc
”这样的输入(当然不能转换为 int)就会引发异常。
我提出的方法是基于一种行为(behavior)。关于行为的完整描述超出了本文的范围。有关精彩的描述,您可以查看这篇文章。
我提出的行为必须附加到包含可能引发异常的输入控件的父 UI 元素上。这在 XAML 中只需几行代码
<Grid>
<i:Interaction.Behaviors>
<Behavior:ValidationExceptionBehavior />
</i:Interaction.Behaviors>
<!-- rest of the code... -->
当此行为加载时,它会为 ValidationError.ErrorEvent RoutedEvent
添加一个处理程序,以便在每次发生验证错误时得到通知。发生这种情况时,该行为会调用 ViewModel
上的一个方法(通过一个简单的接口来限制耦合),这样 ViewModel
就可以跟踪验证错误的数量。以下是该行为的代码
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
namespace WpfFormsWithValidationDemo.Toolkit.Behavior
{
/// <summary>
/// A simple behavior that can transfer the number of
/// validation error with exceptions
/// to a ViewModel which supports the INotifyValidationException interface
/// </summary>
public class ValidationExceptionBehavior : Behavior<FrameworkElement>
{
private int validationExceptionCount;
protected override void OnAttached()
{
this.AssociatedObject.AddHandler(Validation.ErrorEvent,
new EventHandler<ValidationErrorEventArgs>(this.OnValidationError));
}
private void OnValidationError(object sender, ValidationErrorEventArgs e)
{
// we want to count only the validation error with an exception
// other error are handled by using the attribute on the properties
if (e.Error.Exception == null)
{
return;
}
if (e.Action == ValidationErrorEventAction.Added)
{
this.validationExceptionCount++;
}
else
{
this.validationExceptionCount--;
}
if (this.AssociatedObject.DataContext is IValidationExceptionHandler)
{
// transfer the information back to the viewmodel
var viewModel = (IValidationExceptionHandler)
this.AssociatedObject.DataContext;
viewModel.ValidationExceptionsChanged(this.validationExceptionCount);
}
}
}
}
进度报告
我的一个需求是“当一个选项卡完全填写完毕后,其颜色会从红色变为绿色”。为了实现这个特定功能,我在 FormViewModelBase
类(它是 TabControl
中所有 ViewModel
的基类)中添加了一个“IsValid
”属性。每当发生 PropertyChanged
时,此属性就会通过检查 Error
属性(IDataErrorInfo
接口的属性)是否为空来更新
protected override void PropertyChangedCompleted(string propertyName)
{
// test prevent infinite loop while settings IsValid
// (which causes an PropertyChanged to be raised)
if (propertyName != "IsValid")
{
// update the isValid status
if (string.IsNullOrEmpty(this.Error) &&
this.ValidPropertiesCount == this.TotalPropertiesWithValidationCount)
{
this.IsValid = true;
}
else
{
this.IsValid = false;
}
}
}
然后在 XAML 中使用一个简单的触发器就足以实现我所描述的视觉效果
<Style TargetType="{x:Type TabItem}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FormName}" VerticalAlignment="Center" Margin="2" />
<Image x:Name="image"
Height="16"
Width="16"
Margin="3"
Source="../Images/Ok16.png"/>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsValid}" Value="False">
<Setter TargetName="image"
Property="Source" Value="../Images/Warning16.png" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
为了让进度条工作,我在父 ViewModel
(拥有各种 FormViewModelBase ViewModel
的那个)中计算总体进度
/// <summary>
/// Gets a value indicating the overall progress (from 0 to 100)
/// of filling the various properties of the forms.
/// </summary>
public double Progress
{
get
{
double progress = 0.0;
var formWithValidation = this.Forms.Where(f =>
f.TotalPropertiesWithValidationCount != 0);
var propertiesWithValidation = this.Forms.Sum(f =>
f.TotalPropertiesWithValidationCount);
foreach (var form in formWithValidation)
{
progress += (form.ValidPropertiesCount * 100.0) / propertiesWithValidation;
}
return progress;
}
}
关注点
正如我在引言中所说,这种技术的目标是替代传统的 ValidationRules
方法,后者会使 XAML 变得非常复杂。使用 LINQ 和特性是实现这种新可能性的一种很好的方式。在快要写完这篇文章时,我注意到 Mark Smith 开发的一个著名的 MVVM 框架(MVVM Helpers)中也提供了类似的解决方案。
致谢
我要衷心感谢帮助我审阅这篇文章的人们:我的同事 Charlotte 和 Sacha Barber(CodeProject 和微软 MVP)。
历史
- 2010年7月28日:初始版本