65.9K
CodeProject 正在变化。 阅读更多。
Home

MCTS 70-511 课程总结,验证

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (7投票s)

2015 年 7 月 14 日

CPOL

39分钟阅读

viewsIcon

20115

downloadIcon

1297

WPF 验证。

目录

引言

我最近参加了一个课程,以获得 MCTS 认证。MCTS 70-511 课程内容是关于使用 Microsoft .Net Framework 4 进行 Windows 应用程序开发。我想与大家分享我从这个 MCTS 课程中学到的东西。我将发布几篇文章,为您提供课程的概述。这些文章将补充一些有用的信息,但这些信息不在 MCTS 课程的涵盖范围内。在这第一篇文章中,我将介绍 WPF 和 Windows Forms 的验证。我的目的是展示使用标准 WPF 框架的验证可能性。本文讨论的验证方法将通过顶部的代码示例进行说明。用于创建本文的文章和文献可以在参考部分和代码注释中找到。如果您对本文有任何意见,请随时与我联系。

背景

在大多数应用程序中,用户通过用户界面输入应用程序信息。数据验证可确保在继续程序执行之前,输入的数据是否在可接受的参数范围内。换句话说,验证是捕获无效值并拒绝它们的逻辑。通过验证用户输入,可以减少输入错误的几率,并使应用程序更加健壮。WPF 和 Windows Forms 验证提供了多种捕获无效数据的方法,总结如下。

  • ExceptionValidationRule
  • ValidationRules
  • BindingGroups
  • IDataErrorInfo 接口
  • INotifyDataErrorInfo
  • 数据注解
  • ErrorProvider 组件 (Windows Forms)
  • 将验证直接构建到控件中

在本文中,我将对上述验证方法进行深入的讨论。每种验证方法都将通过一个代码示例进行说明,其中验证逻辑是在 person 类(模型)中实现的。在本文末尾,我将结合两种验证方法:INotifyDataErrorInfo 和数据注解,创建一个管理 person 记录的 MVVM 应用程序。应用程序中的验证规则通过用验证属性(数据注解)装饰其属性来应用于 person 类。应用程序中的验证错误将使用基于 Silverlight 的错误模板显示,从而实现漂亮的错误样式。

ExceptionValidationRule

一种与 WPF 数据绑定系统紧密配合的验证方法是,您可以在对象中引发错误,以通知 WPF 存在验证错误。通过在设置属性时简单地抛出异常,可以通知 WPF 存在错误。通常,WPF 会忽略在设置属性时抛出的任何异常。主要原因是保持应用程序流程并防止应用程序崩溃。但是,ExceptionValidationRule 是一个验证规则,它告诉 WPF 将所有异常报告为验证错误。ExceptionValidationRule 类继承自 ValidationRule 类,并使用 sealed 修饰符来防止派生自此类的类。

public sealed class ExceptionValidationRule : ValidationRule

如果抛出异常,WPF 绑定引擎会创建一个 ValidationError 对象并将其添加到绑定元素的 Validation.Errors 集合中。然后,可以通过 WPF 内置的机制显示验证错误。通过在 WPF 中定义错误模板,您可以定义验证错误的样式以及如何显示给应用程序用户。如何在第 响应验证错误节中讨论如何定义错误模板。下面的示例显示了如何在 XAML 代码中为文本框设置 ExceptionValidationRule。在下面的代码中,您还可以看到 NotifyOnValidationError 属性已设置为 true,这会在发生或清除验证错误时引发 Validation.Error 附加事件。Validation.Error 附加事件用于填充显示在视图底部的验证摘要列表。当视图中引发 Validation.Error 附加事件时,它会通过行为从视图推送到模型。

<TextBox>
    <TextBox.Text>
      <Binding Path="SSN" 
               Mode="TwoWay" 
               UpdateSourceTrigger="LostFocus" 
               NotifyOnValidationError="True">
          <Binding.ValidationRules>
              <ExceptionValidationRule></ExceptionValidationRule>    
          </Binding.ValidationRules>    
      </Binding>
    </TextBox.Text>
    <i:Interaction.Behaviors>
        <local:ValidationBehavior/>
    <i:Interaction.Behaviors>
</TextBox>
设置 ExceptionValidationRule 的替代语法是,在绑定上将 ValidatesOnExceptions 属性设置为 true。
<TextBox>
    <TextBox.Text>
        <Binding Path="SSN" 
                 Mode="TwoWay" 
                 UpdateSourceTrigger="LostFocus"
                 NotifyOnValidationError="True"
                 ValidatesOnExceptions="True">
        </Binding>
    </TextBox.Text>
    <i:Interaction.Behaviors>
        <local:ValidationBehavior/>
    <i:Interaction.Behaviors>
</TextBox>

示例代码中显示的验证逻辑可防止输入社会安全号码,这些号码不等于包含两个连字符(在第三和第六位)的十一个字符。使用正则表达式来强制执行此验证规则。有效社会安全号码的示例是:140-22-4532 (NJ),601-20-4562 (AZ)。

private const string RegexSSN = @"^(?!\b(\d)\1+-(\d)\1+-(\d)\1+\b)(?!123-45-6789|219-09-9999
                                |078-05-1120)(?!666|000|9\d{2})\d{3}-(?!00)\d{2}-(?!0{4})\d{4}$";
private string SSNValue = string.Empty;
public string SSN
{
    get { return SSNValue; }
    set
    {
        if (string.IsNullOrEmpty(value))
            throw new Exception("Social security number is required.");
        else if(!Regex.IsMatch(value, RegexSSN))
            throw new Exception("Please enter a valid social security number.");                
        SSNValue = value;
        NotifyPropertyChanged();
    }
}

在上面的代码示例中,文本框的 UpdateSourceTrigger 属性设置为 LostFocus,这意味着绑定源属性将在文本框失去焦点时更新。但是,应注意,仅当文本框的文本值与其先前的值不同时,绑定源属性才会被更新。如果初始文本框值是空字符串,即使文本框接收并失去焦点,绑定源属性也不会更新,而其值将保持为空。当您希望使用 ExceptionValidationRule 类验证必需字段时,此更新行为会导致问题,因为验证发生在绑定源属性更新时。一种解决此问题的方法是在用户单击“确定”按钮时手动更新所有绑定源属性。这可以通过一个方法来完成,该方法遍历视图中的所有 FrameworkElement,从而强制更新每个绑定源属性。此方法的实现如下所示。使用此方法,您可以确保在用户单击“确定”按钮时,所有绑定到视图的源属性都将被更新和验证。

private void OnOkCommandExecute(object o)
{
    Window mainWindow = (Window)o;
    if (IsValid(mainWindow))
        mainWindow.Close();
}

public bool IsValid(Visual myVisual)
{
    EnumVisual(myVisual);
    if (Errors.Count == 0)
        return true;
    else
        return false;
}

public void EnumVisual(Visual myVisual)
{
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(myVisual); i++)
    {
        Visual childVisual = (Visual)VisualTreeHelper.GetChild(myVisual, i);
        if (childVisual is FrameworkElement)
        {
            LocalValueEnumerator localValues = childVisual.GetLocalValueEnumerator();
            while (localValues.MoveNext())
            {
               BindingExpressionBase bindingExpression =
                 BindingOperations.GetBindingExpressionBase(childVisual, localValues.Current.Property);
               if (bindingExpression != null)
                 bindingExpression.UpdateSource();
            }
        }
        EnumVisual(childVisual);
    }
}

类型转换也可能导致 WPF 框架引发异常。当 WPF 框架无法将用户输入转换为绑定源属性的类型时,就会发生这种情况。例如,如果一个文本框绑定到模型中的一个整数,WPF 框架将尝试将输入的文本转换为整数。如果框架失败,它将生成一个验证错误(例如,“输入字符串的格式不正确”)。然后,可以通过 WPF 内置机制显示此验证错误。

ValidationRules

ValidationRules、IDataErrorInfo 和 INotifyDataErrorInfo 是验证方法,它们允许您在不抛出异常的情况下指示验证错误。在本节中,将解释 ValidationRules 的用法。使用 ValidationRules 的一个优点是,您可以轻松地在存储相似类型数据的其他绑定中重用它们。在下面的示例中,使用三个 ValidationRules 来验证 person 对象的 FirstName 属性。这三个规则可确保 FirstName 属性是必需的,并且其值介于两个和五十个字符之间。

<TextBox x:Name="txtFirstName">
    <TextBox.Text>
        <Binding Path="FirstName" 
                 Mode="TwoWay" 
                 UpdateSourceTrigger="LostFocus" 
                 NotifyOnValidationError="True">
            <Binding.ValidationRules>
                <local:RequiredValidationRule ErrorMessage="First name is required."/>
                <local:StringValidationRule MaxStringLength="50" MinStringLength="2"
                    ErrorMessage="First name needs to consist of between two and fifty chars."/>
                <local:NameValidationRule ErrorMessage="Only chars are allowed for first name."/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
    <i:Interaction.Behaviors>
        <local:ValidationBehavior/>
    </i:Interaction.Behaviors>
</TextBox>
当在第一个名字文本框中输入新值时,每个验证规则都会按照规则声明的顺序进行评估。在上面的示例中,将首先评估 RequiredValidationRule,然后是 StringValidationRule,最后是 NameValidationRule。当一个已评估的 ValidationRule 表示错误时,其他即将到来的 ValidationRules 将不会被评估。此外,绑定源属性不会用错误值更新,并保持不变。
 
您可以通过创建一个继承自抽象 ValidationRule 类的类来创建自定义验证规则。抽象 ValidationRule 类有一个需要在自定义 ValidationRule 类中重写的方法,即 validate 方法。此方法接受两个参数,第一个是 object 参数,表示正在评估的值;第二个参数表示 culture 对象,以防您需要提供自己的本地化代码。如下所示,validate 方法返回一个 ValidationResult 对象,该对象包含 IsValid 和 ErrorCondition 属性。IsValid 值为 true 的 ValidationResult 对象被视为有效,应用程序执行将正常进行。如果返回的 ValidationResult 对象的 IsValid 值为 false,则会创建一个新的 ValidationError 对象,并将描述性错误文本设置为 ErrorCondition 属性的内容。然后,生成的 ValidationError 对象将被添加到绑定元素的 Validation.Errors 集合中,并可以通过 WPF 内置机制显示。下面的代码显示了 StringValidationRule 类的实现。
public sealed class StringValidationRule : ValidationRule
{
    private int iMaxStringLength = 0;
    private int iMinStringLength = 0;
    private string sErrorMessage = string.Empty;

    public int MaxStringLength
    {
        get { return iMaxStringLength; }
        set { iMaxStringLength = value; }
    }

    public int MinStringLength
    {
        get { return iMinStringLength; }
        set { iMinStringLength = value; }
    }

    public string ErrorMessage
    {
        get { return sErrorMessage; }
        set { sErrorMessage = value; }
    }

    public override ValidationResult Validate(object value, System.Globalization.CultureInfo culture)
    {
        string val = value.ToString();
        ValidationResult aValidationResult = null;

        if (val.Length >= MinStringLength &&
            val.Length <= MaxStringLength)
            aValidationResult = new ValidationResult(true, null);
        else
            aValidationResult = new ValidationResult(false, ErrorMessage);

        return aValidationResult;
    }
}

使用 ValidationRules 的一个缺点是,它们不是从 DependencyObject 派生的。因此,您不能直接将依赖项属性绑定到 ValidationRules。我在网上找到了两篇关于此缺点的文章,分别是 文章 1文章 2。第一篇文章使用代理类来实现与 ValidationRules 的绑定。此方法将不再进一步讨论,如果您对此感兴趣,可以阅读引用的文章。第二篇文章使用代码隐藏将依赖项属性绑定到 ValidationRules。此方法将在下面讨论,示例代码可以在本文顶部的代码中找到。

视图的代码隐藏如下所示。在此验证方法的实现中有两个类起作用,它们是 StringValidationRule 和 StringLengthCheck 类。StringLengthCheck 类包含三个依赖项属性,它们在代码隐藏中绑定到视图。如下面的代码所示,StringLengthCheck 实例定义在视图的资源部分,并在代码隐藏中通过其资源键进行检索。检索后,设置绑定。为了访问 StringLengthCheck 类的绑定依赖项属性,StringValidationRule 将 StringLengthCheck 类的实例包含在其 ValidStringLength 属性中。

public void InitializeBinding()
{
    Binding MaxStringLengthBinding = new Binding();
    MaxStringLengthBinding.Source = MaxStringValueSlider;
    MaxStringLengthBinding.Path = new PropertyPath(Slider.ValueProperty);

    Binding MinStringLengthBinding = new Binding();
    MinStringLengthBinding.Source = MinStringValueSlider;
    MinStringLengthBinding.Path = new PropertyPath(Slider.ValueProperty);

    Binding ErrMsgBinding = new Binding();
    ErrMsgBinding.Source = txtboxErrorMsg;
    ErrMsgBinding.Path = new PropertyPath(TextBox.TextProperty);

    StringLengthCheck _aStringLengthCheckObj = 
        (StringLengthCheck)this.Resources["aStringLengthCheckObj"];
    BindingOperations.SetBinding(_aStringLengthCheckObj, 
        StringLengthCheck.MinStringLengthProperty, MinStringLengthBinding);
    BindingOperations.SetBinding(_aStringLengthCheckObj, 
        StringLengthCheck.MaxStringLengthProperty, MaxStringLengthBinding);
    BindingOperations.SetBinding(_aStringLengthCheckObj, 
        StringLengthCheck.ErrorMessageProperty, ErrMsgBinding); 
}
<Window.Resources>
   <local:StringLengthCheck x:Key="aStringLengthCheckObj"></local:StringLengthCheck>
</Window.Resources>
<TextBox x:Name="txtFirstName">
   <TextBox.Text>
      <Binding Path="FirstName" Mode="TwoWay" UpdateSourceTrigger="Explicit">
         <Binding.ValidationRules>
            <local:StringValidationRule ValidStringLength="{StaticResource aStringLengthCheckObj}"/>
         </Binding.ValidationRules>                                
      </Binding>
   </TextBox.Text>
</TextBox>

名字文本框的 UpdateSourceTrigger 设置为 explicit。Explicit 意味着 FirstName 源属性仅在您按下“验证”按钮后才会更新和验证,然后该按钮会调用其相应的处理程序并调用 UpdateSource() 方法。验证后,将弹出消息框显示验证结果。如果出现错误,则使用错误栏和基于 Silverlight 的错误模板显示错误。如果没有错误,则使用数据触发器隐藏错误栏。

private void btnValidate_Click(object sender, RoutedEventArgs e)
{
    string errorList = string.Empty;

    txtFirstName.GetBindingExpression(TextBox.TextProperty).UpdateSource();
    if (Validation.GetHasError(txtFirstName))
    {
        ReadOnlyObservableCollection<ValidationError> errorCollection 
            = Validation.GetErrors(txtFirstName);
        foreach (ValidationError err in errorCollection)
            errorList += err.ErrorContent + Environment.NewLine;
        System.Windows.MessageBox.Show(errorList);
    }   
    else
        System.Windows.MessageBox.Show("Validation succeeded for first name property.");
}

BindingGroups

到目前为止讨论的验证方法允许您验证单个属性。但是,有时您需要执行涉及多个属性的验证。例如,会员资格有效需要其会员到期日期晚于其开始日期。Binding groups 可用于这种涉及多个属性的验证。Binding groups 还允许您跨多个对象执行验证,此处有一个示例。Binding groups 的理念类似于上一节中讨论的实现自定义 ValidationRules。但不是将验证规则应用于单个绑定,而是将其附加到包含所有绑定控件的容器。这在下面的示例中得到了说明,通过设置其 binding group 属性为网格容器创建了一个 binding group。该 binding group 有一个名为 PersonValidationRule 的单个验证规则,该规则会自动应用于存储在网格 DataContext 中的绑定 person。

<Grid x:Name="grdCustomerInformation" Margin="10" DatePicker.LostFocus="Control_LostFocus">
    <Grid.BindingGroup>
        <BindingGroup x:Name="personBindingGroup">
            <BindingGroup.ValidationRules>
                <local:PersonValidationRule/>
            </BindingGroup.ValidationRules>
        </BindingGroup>
    </Grid.BindingGroup>
    <DatePicker>                     
        <DatePicker.SelectedDate>
            <Binding Path="MembershipStartDate" 
                     Mode="TwoWay" 
                     UpdateSourceTrigger="PropertyChanged"
                     BindingGroupName="personBindingGroup"></Binding>
        </DatePicker.SelectedDate>
    </DatePicker>
</Grid>

在上一节中讨论的验证规则中,Validate() 方法接收单个值对象进行检查。当使用 Binding groups 时,Validate() 方法接收一个 BindingGroup 对象而不是值对象,如下面的代码所示。验证首先检索 person 对象,对该对象的成员属性应用验证逻辑,并在未满足验证逻辑时返回错误。或者,您可以使用 BindingGroup.TryGetValue 或 BindingGroup.GetValue 来检索 person 对象的成员属性,然后可以对其进行验证。这两种方法允许在访问 person 对象的属性时进行更好的错误控制。如果 Binding group 中出现验证错误,WPF 绑定引擎将创建一个 ValidationError 对象,然后将其添加到与 BindingGroup 关联的元素的 Validation.Errors 集合中。然后,可以通过 WPF 内置机制样式化和显示生成的错误。

public sealed class PersonValidationRule : ValidationRule
{
    public override ValidationResult Validate(object value, 
        System.Globalization.CultureInfo cultureInfo)
    {
        ValidationResult aValidationResult = null;
        BindingGroup bindingGroup = (BindingGroup)value;
        Person person = (Person)bindingGroup.Items[0];

        if(person.MembershipEndDate > person.MembershipStartDate)
            aValidationResult = new ValidationResult(true, null);
        else
            aValidationResult = new ValidationResult(false, 
                "Start date must be earlier than end date.");
        return aValidationResult;
    }
}

Binding groups 使用 事务性编辑系统,这意味着您需要在验证逻辑运行之前提交编辑。最简单的方法是调用 BindingGroup.CommitEdit() 方法。您可以从按钮单击事件处理程序或编辑控件失去焦点时运行的事件处理程序调用此方法。在此示例中,在网格中捕获了两个 DatePicker 的 LostFocus 事件。这是可能的,因为 LostFocus 事件是冒泡事件,它会冒泡到视觉树,直到到达网格容器中的处理程序。LostFocus EventHandler 在这里显示。

private void Control_LostFocus(object sender, RoutedEventArgs e)
{
    personBindingGroup.CommitEdit();
}

如上所述,MembershipStartDate 和 MembershipEndDate 属性使用网格的 binding group 中定义的验证规则进行验证。Person 类中的其他属性使用 IDataErrorInfo 接口进行验证,该接口将在下一节中讨论。

当验证失败时,整个网格被视为无效,并用细红线边框括起来,如下图所示。您可以修改其错误模板来更改网格的边框。在下面的视图中,网格中控件的错误模板设置为 {x:Null},这会导致它们在发生错误时恢复到默认外观。因此,视图中的所有验证错误仅显示在验证摘要中。验证摘要由一个绑定到 Validation.Errors 集合的 ErrorContent 的 items 控件组成。当所有验证错误都清除后,将使用数据触发器隐藏错误摘要。此外,当没有错误时,“确定”按钮将被启用。

使用 Binding groups 的一个缺点是它们是紧密耦合的,这意味着自定义验证规则不太可能在其他 Binding group 中重用,而只能用于它们预期的 Binding group。

IDataErrorInfo

实现 IDataErrorInfo 接口使您能够将验证直接构建到模型中。IDataErrorInfo 接口包含两个成员,第一个成员是 Error 属性,它提供一个错误消息来指示整个对象的问题。可以通过直接查询属性值来访问 Error 属性的值,并且通常不实现它。如果您想使用 Error 属性,则需要实现自己的自定义错误报告。这将在本节后面讨论。第二个成员是 Item 属性,它提供错误消息来指示对象属性的问题。每当绑定源属性的值发生更改时,WPF 绑定引擎会通过将属性名传递给 IDataErrorInfo.Item 属性来验证新值。然后,生成的 ValidationError 对象将添加到绑定元素的 Validation.Errors 集合中,然后可以通过 WPF 内置机制显示。IDataErrorInfo 接口定义如下:

/* The System.ComponentModel.IDataErrorInfo interface */
public interface IDataErrorInfo
{    
    string Error { get; }
    string this[ string columnName ] { get; } //item property
}

应将绑定到实现 IDataErrorInfo 接口的对象控件的 ValidatesOnDataErrors 属性设置为 true,如下所示。

<TextBox>
    <TextBox.Text>
        <Binding Path="SSN" 
                 Mode="TwoWay" 
                 UpdateSourceTrigger="LostFocus" 
                 ValidatesOnDataErrors="True">
        </Binding>
    </TextBox.Text>
</TextBox>

在下面的示例中,在 person 类中实现了 IDataErrorInfo 接口。必需属性,如 FirstName、Birthday 和 Gender,通过检查它们是否包含值来验证。电子邮件地址和社会安全号码使用正则表达式进行检查。会员开始日期和结束日期使用跨属性验证进行验证。此跨属性验证是在 Item 属性中完成的,或者也可以在 Error 属性中完成。

public bool IsValid
{
    get { return string.IsNullOrEmpty(this.Error); }
}

public string Error
{
    get
    {
        List<string> errors = new List<string>();
        PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(this);
        foreach (PropertyDescriptor property in properties)
        {
            string msg = this[property.Name];
            if (!string.IsNullOrWhiteSpace(msg))
            {
                errors.Add(msg);
            }
        }
        return string.Join(Environment.NewLine, errors);
    }
}

public string this[string columnName]
{
    get
    {
        string result = string.Empty;
        switch (columnName)
        {
            case "FirstName" :
                if (string.IsNullOrEmpty(FirstName))
                    result = "First name is required.";
                else if (FirstName.Length < 2 || FirstName.Length > 50)
                    result = "First name needs to be between 2 and 50 chars.";
                break;
            case "Gender":
                if (Gender == null)
                    result = "Gender is required.";
                break;
            case "SSN" :
                if (string.IsNullOrEmpty(SSN))
                    result = "Social security number is required.";
                else if (!Regex.IsMatch(SSN, RegexSSN))
                    result = "Please enter a valid social security number.";
                break;
            case "Email":
                if (string.IsNullOrEmpty(Email))
                    result = "Email address is required.";
                else if (!Regex.IsMatch(Email, RegexEmail))
                    result = "Please enter a valid email address.";
                break;
            case "DayOfBirth":
                if (DayOfBirth == null)
                    result = "Day of birth is required.";
                else if (DateTime.Now.AddYears(-18) < DayOfBirth)
                    result = "You must be at least 18 years old to be an member.";
                break;
            case "MembershipStartDate":
            case "MembershipEndDate":
                if (MembershipStartDate > MembershipEndDate)
                    result = "Membership start date must be earlier than end date.";
                break;
            default:
                break;
        }                
        return result; 
    }
}

private void NotifyPropertyChanged(string propertyName)
{
    if (PropertyChanged != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));                
        /* Notify error and IsValid binding of change. */
        if (propertyName != "Error" && propertyName != "IsValid")
        {
            PropertyChanged(this, new PropertyChangedEventArgs("Error"));
            PropertyChanged(this, new PropertyChangedEventArgs("IsValid"));
        }
    }
}

下面的视图中有几点需要注意。首先要注意的是,验证摘要显示视图中的所有活动错误,这可以通过将 Error 属性绑定到摘要文本框来实现。Error 属性包含所有对象错误,用换行符分隔。当没有活动错误时,将使用绑定到 IsValid 属性的数据触发器隐藏错误摘要。在 IsValid 和 Error 属性的实现过程中,我注意到当这些属性发生变化时,对这些属性的绑定不会更新。我通过在 person 对象中的每个属性更改时触发这两个属性的 PropertyChanged 事件来解决此问题,如 NotifyPropertyChanged 方法所示。“确定”按钮绑定到一个命令,只有当 IsValid 属性返回 true 时才能执行该命令。

IDataErrorInfo 接口的一个缺点是,它在视图首次加载时就会显示验证错误,如上面的视图所示。设置 ValidatesOnDataErrors 属性为 true 的替代语法是实现 DataErrorValidationRule,该规则会检查 IDataErrorInfo 实现引发的错误。通过将 ValidatesOnTargetUpdated 属性设置为 false,您可以抑制在目标由源更新时显示验证错误。

<TextBox>
    <TextBox.Text>
        <Binding Path="FirstName" Mode="TwoWay" UpdateSourceTrigger="LostFocus">
            <Binding.ValidationRules>
                <DataErrorValidationRule ValidatesOnTargetUpdated="false"/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

INotifyDataErrorInfo

.NET Framework 4.5 中添加了 INotifyDataErrorInfo 接口,因此 MCTS 70-511 课程未涵盖此接口。与 IDataErrorInfo 类似,INotifyDataErrorInfo 接口允许您在不抛出异常的情况下指示错误。此外,它比其前身 IDataErrorInfo 提供了更大的灵活性,通常应在新类实现时使用。INotifyDataErrorInfo 接口具有以下优点:

  • 它支持每个属性的多个错误。
  • 它支持自定义错误对象(字符串以外的其他类型)。
  • 它可以在设置另一个属性时使属性无效(跨属性验证)。
  • 它支持异步服务器端验证。验证完成后,通过引发 ErrorsChanged 事件通知视图。

为了使控件能够使用 INotifyDataErrorInfo 接口,它们必须将 ValidatesOnNotifyDataErrors 属性设置为 true,这是默认值。但是,明确设置它以表明您的意图是一个好主意。

<TextBox>
    <TextBox.Text>
        <Binding Path="FirstName" 
                 Mode="TwoWay" 
                 UpdateSourceTrigger="LostFocus" 
                 ValidatesOnNotifyDataErrors="True">
        </Binding>
    </TextBox.Text>
</TextBox>

如下所示,INotifyDataErrorInfo 接口由三个成员组成。第一个成员是 HasErrors 属性,它返回 true 或 false 来指示对象中是否存在错误。GetErrors 返回指定属性的错误列表,或者在传入的 propertyname 为 null 时返回整个对象的错误列表。最后一个成员是 ErrorsChanged 事件,每当错误集合发生更改时都会引发该事件。

/* The System.ComponentModel.INotifyDataErrorInfo interface */
public interface INotifyDataErrorInfo
{    
   bool HasErrors { get; }
   event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
   IEnumerable GetErrors(string propertyName);
}

在实现这三个 INotifyDataErrorInfo 成员之前,您需要创建一个私有集合来跟踪对象中的错误。跟踪错误的最简单方法是使用线程安全的并发字典来存储错误对象。使用并发字典而不是常规字典是因为异步验证可能导致多个线程访问同一个字典。

private ConcurrentDictionary<string, ICollection<Error>> propertyErrors = 
    new ConcurrentDictionary<string, ICollection<Error>>();

字典的第一个参数是 propertyname。每个属性可以有一个或多个错误,如字典的第二个参数 (ICollection<Error>) 所示。在此示例中,字典存储自定义错误对象的列表。此类实现如下所示,它包含一个字符串属性,用于描述错误消息,以及一个枚举属性,用于指定错误的严重性。

public enum Severity
{
    INFORMATION,
    WARNING,
    ERROR
}
  
public class Error
{
    private Severity _severity = Severity.ERROR;
    private string _message = string.Empty; 

    public Error(string message, Severity severity)
    {
        Message = message;
        Severity = severity;
    }

    public string Message
    {
        get { return _message; }
        set { _message = value; }
    }

    public Severity Severity  
    {
        get { return _severity; }
        set { _severity = value; }
    }

    public override string ToString()
    {
        return Message;
    }
}

下面的 person 类显示了如何实现 INotifyDataErrorInfo 接口。为便于说明,仅显示了两个属性的验证:FirstName 和 HomePage。接口的完整实现可以在本文顶部的代码中找到。除了实现 INotifyDataErrorInfo 接口所需的三个强制成员外,person 类还添加了两个辅助方法,分别称为 SetErrors 和 ClearErrors。这两个方法简化了错误处理,并在修改错误集合时引发 ErrorsChanged 事件。FirstName 属性的验证实现在 IsFirstNameValid 方法中。此方法检查 FirstName 长度是否在两个和五十个字符之间,如果是,则清除所有错误。如果验证失败,则会将描述性错误添加到 FirstName 属性的错误集合中,并引发 ErrorsChanged 事件。

如前所述,INotifyDataErrorInfo 接口支持异步验证,从而在验证过程中保持视图的响应性。为便于说明,person 类实现了 HomePage URL 的异步验证。URL 的验证通过调用 UrlValidationAsync() 方法开始,该方法又调用 IsReachableURL() 方法。IsReachableURL() 方法使用带有线程休眠的任务来模拟长时间运行的阻塞调用。当此方法返回时,将使用 SetErrors 或 ClearErrors 方法更新视图。除了异步验证之外,INotifyDataErrorInfo 接口还支持方便的跨属性验证。这在 person 类中得到了说明,其中跨属性验证可确保会员开始日期始终早于结束日期。

public class Person : INotifyPropertyChanged, INotifyDataErrorInfo
{
    private string FirstNameValue = string.Empty;
    public string FirstName
    {
        get { return FirstNameValue; }
        set
        {
            if (FirstNameValue != value)
            {
                FirstNameValue = value;
                IsFirstNameValid(value);
                NotifyPropertyChanged();
            }
        }
    }

    private bool IsFirstNameValid(string firstName)
    {
        ICollection<Error> validationErrors = new List<Error>();
        bool IsValid = false;

        if (string.IsNullOrEmpty(firstName))
            validationErrors.Add(new Error("First name is required.", Severity.ERROR));
        else if(firstName.Length < 2 || firstName.Length > 50)
            validationErrors.Add(new Error("First name must be between 2 and 50 chars.",
                                            Severity.ERROR));

        IsValid = validationErrors.Count == 0;
        if (IsValid)
            ClearPropertyErrors("FirstName");
        else
            SetErrors("FirstName", validationErrors);

        return IsValid;
    }

    private string HomepageValue = string.Empty;  
    public string HomePage
    {
        get { return HomepageValue; }
        set
        {
            if (HomepageValue != value)
            {
                HomepageValue = value;
                UrlValidationAsync(value);
                NotifyPropertyChanged();
            }
        }
    }

    private async void UrlValidationAsync(string urlValue)
    {
        ICollection<Error> validationErrors = new List<Error>();
        bool IsValid = false;

        if (!string.IsNullOrEmpty(urlValue))
        {
            if (!Uri.IsWellFormedUriString(urlValue, UriKind.Absolute))
            {
                validationErrors.Add(new Error("Please enter a valid URL.", Severity.ERROR));
            }
            else
            {
                /* Inform the user about the validation progress.  */
                SetErrors("HomePage", new List<Error> { new Error("HomePage validation in progress.",
                           Severity.INFORMATION) });
                bool isReachableTask = await IsReachableURL(urlValue);
                if (!isReachableTask)
                    validationErrors.Add(new Error("URL is not reachable.", Severity.ERROR));
            }
        }

        IsValid = validationErrors.Count == 0;
        if (IsValid)
            ClearPropertyErrors("HomePage");
        else
            SetErrors("HomePage", validationErrors);
    }

    private Task<bool> IsReachableURL(string urlValue)
    {
        return Task<bool>.Factory.StartNew(() =>
        {
            bool isReachableURL = false;

            try
            {
                Thread.Sleep(5000);
                WebClient client = new WebClient();
                string urlContent = client.DownloadString(urlValue);
                isReachableURL = urlContent.Length > 0;
            }
            catch (Exception)
            {
                isReachableURL = false;
            }

            return isReachableURL;
        });
    }

    public bool HasErrors
    {
        get { return propertyErrors.Count > 0; }
    }

    public System.Collections.IEnumerable GetErrors(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName)) //Retrieve errors for entire object
            return propertyErrors.Values;
        else if (propertyErrors.ContainsKey(propertyName) &&
                (propertyErrors[propertyName] != null) &&
                propertyErrors[propertyName].Count > 0)
            return propertyErrors[propertyName].ToList();
        else
            return null;
    }

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    private void NotifyErrorChanged(string propertyName)
    {
        if (ErrorsChanged != null)
            ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    private void ClearPropertyErrors(string propertyName)
    {
        if (propertyErrors.ContainsKey(propertyName))
        {
            ICollection<Error> existingErrors = null;
            propertyErrors.TryRemove(propertyName, out existingErrors);
            NotifyErrorChanged(propertyName);
        }
    }

    private void SetErrors(string propertyName, ICollection<Error> errors)
    {
        if (propertyErrors.ContainsKey(propertyName))
        {
            ICollection<Error> existingErrors = null;
            propertyErrors.TryRemove(propertyName, out existingErrors);
        }
        propertyErrors.TryAdd(propertyName, errors);
        NotifyErrorChanged(propertyName);
    }
}

如前所述,WPF 通过突出显示有错误元素的红色边框(无工具提示)来通知用户验证错误。自定义错误模板的可能性将在 响应验证错误节中讨论。在寻找好的错误模板时,我找到了这篇 文章,它描述了一个基于 Silverlight 的 WPF 错误模板。为了有效使用它,我对这个错误模板进行了三处更改。我做的第一个更改是添加了为每个属性显示多个错误的功能。第二个更改是显示一个图像在错误消息旁边以指示错误严重性。第三个更改是用 IsKeyboardFocusWithin 事件替换了 datatrigger 中的 IsFocused 事件,这使得也可以将错误模板应用于 DatePicker。下面的 XAML 代码显示了显示错误消息及其相应图标的错误模板的一部分。

<ItemsControl ItemsSource="{Binding ElementName=adorner, Path=AdornedElement.(Validation.Errors)}">
   <ItemsControl.ItemTemplate>
      <DataTemplate>
         <StackPanel Orientation="Horizontal">
            <Image Margin="8,3,2,3">
               <Image.Style>
                  <Style TargetType="{x:Type Image}">
                     <Setter Property="Source" Value="/Images/Error_16x16.png"/>
                     <Style.Triggers>
                        <DataTrigger Binding="{Binding ErrorContent.Severity}"
                                     Value="{x:Static local:Severity.WARNING}">
                           <Setter Property="Source" Value="/Images/Warning_16x16.png"/>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding ErrorContent.Severity}"
                                     Value="{x:Static local:Severity.INFORMATION}">
                           <Setter Property="Source" Value="/Images/Information_16x16.png"/>
                        </DataTrigger>
                     </Style.Triggers>
                  </Style>                                                            
               </Image.Style>    
            </Image>
            <TextBlock Foreground="White"
                       Margin="2,3,8,3"
                       TextWrapping="Wrap"
                       Text="{Binding ErrorContent.Message}"/>
         </StackPanel>
      </DataTemplate>
   </ItemsControl.ItemTemplate>
</ItemsControl>

通过使用基于 Silverlight 的错误模板,验证错误会得到很好的样式化,如下面的视图所示。有错误的控件将用红色边框括起来。当有错误控件获得焦点或用户将鼠标悬停在工具提示角上时,将显示一个弹出窗口,其中包含所有错误。使用基于 Silverlight 的错误模板的主要优点是错误消息不会覆盖其他元素,因为装饰器始终在视觉上位于顶部。OK 按钮处理程序仅在 IsValid 属性返回 true(表示视图中没有更多错误)时关闭视图。

如前所述,输入的 HomePage URL 是异步验证的。在使用异步验证时,您需要告知用户验证进度。我实现了一个简单的解决方案,该解决方案向用户显示一个信息消息,表明异步验证正在进行中。当异步验证完成后,信息消息将被清除或替换为验证期间发生的验证错误的列表。

数据注解和 INotifyDataErrorInfo

另一种未包含在 MCTS 70-511 考试中的验证方法是使用数据注解,因为它易于使用,值得一提。尽管数据注解的主要用途是数据类(也称为实体类),但您也可以在自定义类中使用数据注解。在使用数据注解之前,需要添加对 System.ComponentModel.DataAnnotations 的引用,并在类中引用该命名空间。数据注解属性分为三类:验证属性、显示属性和数据建模属性。不详细说明,显示属性用于指定类数据的显示方式,数据建模属性用于指定类之间的关系。有关详细信息,请参阅此 msdn 文章。验证属性继承自 ValidationAttribute 类,并用于强制执行验证规则。所有支持的验证类型的完整概述可以在此 msdn 文章中找到。

可以通过用验证属性装饰类属性来指定验证规则和错误消息。您应该记住的一点是,DataGrid 控件是唯一自动应用验证属性的控件。如果您不使用 DataGrid 控件,则必须手动验证属性值。本节讨论的验证方法基于 INotifyDataErrorInfo 接口和数据注解的组合。使用此验证技术组合,可以通过用一个或多个验证属性装饰每个属性来在 person 类中实现验证。当在 person 类中设置源属性时,该属性的验证通过调用 ValidateProperty 方法进行,然后该方法检查新值是否满足其验证规则。

private string PhoneValue = string.Empty;
[Required(AllowEmptyStrings = false, ErrorMessage = "Phone number is required.")]
[Phone(ErrorMessage = "Enter a valid phone number.")]
public string Phone
{
    get { return PhoneValue; }
    set
    {
        if (PhoneValue != value)
        {
            ValidateProperty(value);
            PhoneValue = value;
            NotifyPropertyChanged();
        }
    }
}

private string EmailValue = string.Empty;
[Required(AllowEmptyStrings = false, ErrorMessage = "Email is required.")]
[EmailAddress(ErrorMessage = "Please enter a valid email address.")]
public string Email
{
    get { return EmailValue; }
    set
    {
        if (EmailValue != value)
        {
            ValidateProperty(value);
            EmailValue = value;
            NotifyPropertyChanged();
        }
    }
}

protected void ValidateProperty(object value, [CallerMemberName] string propertyName = "")
{
    ICollection<System.ComponentModel.DataAnnotations.ValidationResult> validationErrors =
        new List<System.ComponentModel.DataAnnotations.ValidationResult>();

    bool isValid =  Validator.TryValidateProperty(value, 
        new ValidationContext(this, null, null)
        { MemberName = propertyName },
        validationErrors);

    if (isValid)
        ClearPropertyErrors(propertyName);
    else
        SetErrors(propertyName, validationErrors);
}

除了 Validator.TryValidateProperty 之外,还有 Validator.TryValidateObject。当您想要验证整个对象时,此方法非常有用。Validator.TryValidateObject 的一个缺点是,当属性值为 null 时,除了 RequiredAttribute 之外,所有验证属性都不会被强制执行。如果实现了一个自定义 ValidationAttribute,它应该在值为 null 时执行一些特殊操作,那么这尤其是一个问题。您可以通过使用 Validator.TryValidateProperty 并遍历类的所有属性来克服此缺点,如下面的代码所示。

public bool IsValid()
{
    PropertyInfo[] properties = this.GetType().GetProperties();
    foreach (PropertyInfo property in properties)
    {
        if (property.GetCustomAttributes(typeof(ValidationAttribute), true).Any())
        {
            object value = property.GetValue(this, null);
            ValidateProperty(value, property.Name);
        }
    }

    return !HasErrors;
}

public bool HasErrors
{
    get { return propertyErrors.Count > 0; }
}

如果不同的支持验证类型不合适,您可以从 ValidationAttribute 派生并编写自己的实现。例如,下面的 AgeCheckAttribute 类就是这样。

private DateTime? DayOfBirthValue = null;
[Required(ErrorMessage = "Day of birth is required.")]
[AgeCheck(18, ErrorMessage = "You must be at least 18 years old to be a member.")]
public DateTime? DayOfBirth
{
    get { return DayOfBirthValue; }
    set
    {
        if (DayOfBirthValue != value)
        {
            ValidateProperty(value);
            DayOfBirthValue = value;
            NotifyPropertyChanged();
        }
    }
}

[AttributeUsageAttribute(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class AgeCheckAttribute : ValidationAttribute
{
    public AgeCheckAttribute(int minimumAge) { MinimumAge = minimumAge; }
    public AgeCheckAttribute(int minimumAge, string errorMessage) :
        base(() => errorMessage) { MinimumAge = minimumAge; }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        DateTime? birthDate = (DateTime)value;

        if (birthDate != null &&
            DateTime.Now.AddYears(-1 * MinimumAge) > birthDate)
            return ValidationResult.Success;
        else
            return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
    }

    public int MinimumAge { get; private set; }
}

或者,您可以创建一个自定义验证方法,如下所示。

private DateTime MembershipEndDateValue = DateTime.Now.AddYears(1);
[CustomValidation(typeof(Person), "IsTimeSpanValid")]
public DateTime MembershipEndDate
{
    get { return MembershipEndDateValue; }
    set
    {
        if (MembershipEndDateValue != value)
        {
            MembershipEndDateValue = value;
            ValidateProperty(value);
            NotifyPropertyChanged();
        }
    }
}

public static ValidationResult IsTimeSpanValid(object value, ValidationContext validationContext)
{
    Person aPerson = (Person) validationContext.ObjectInstance;

    if (aPerson.MembershipStartDate < aPerson.MembershipEndDate)
        return ValidationResult.Success;
    else
        return new ValidationResult("Start date must be earlier than end date.");
}

响应验证错误

WPF Binding 类建立目标属性和源属性之间的关系。Binding.Mode 属性决定绑定控件如何响应源值或目标值的更改。Binding.Mode 属性最常用的值是 TwoWay,允许对源属性进行数据编辑。OneWay 值通常用于显示数据但不允许编辑数据的应用程序。有时需要先转换值才能与源同步。

当绑定将数据写入底层源属性时,会发生验证。绑定将数据写入底层源的时刻由 Binding 的 UpdateSourceTrigger 属性决定。因此,验证发生的时刻也由 UpdateSourceTrigger 属性决定。文本框等控件的 UpdateSourceTrigger 设置为 LostFocus,这意味着在控件失去焦点时会发生验证。该值也可以设置为 PropertyChanged 或 Explicit,分别表示当控件值更改时发生验证,或者必须显式调用绑定上的验证。使用文本框时,您希望在用户输入数据后提供验证反馈。因为对键盘输入的即时验证可能会有问题。用户可以在不做任何“错误”操作的情况下通过一个无效的状态。例如,一个接受社会安全号码的文本框。当用户输入号码时,控件会在填充完整的社会安全号码之前一直通过无效状态。因此,您应该将文本框的 UpdateSourceTrigger 属性设置为 LostFocus。

WPF 验证错误可以通过不同方式显示给应用程序用户:

  • 使用自定义错误模板显示错误集合。
  • 处理 Validation.Error 附加事件以设置(例如)错误消息。
  • 响应 Validation.HasError 附加属性以设置(例如)控件的 ToolTip,使用触发器显示 Validation.Errors 集合中的第一个错误。

如前所述,当发生验证错误时,会创建一个新的 ValidationError 对象并将其添加到绑定元素的 Validation.Errors 集合中 。Validation.Errors 集合本身不可由用户设置,但您可以在错误模板中使用它。当 Validation.Errors 集合不为空时,绑定元素的 Validation.HasError 附加属性将设置为 true。此外,如果 Binding 的 NotifyOnValidationError 属性设置为 true,则绑定引擎会在元素上引发 Validation.Error 附加事件以指示新添加的错误。

默认情况下,WPF 通过用细红线边框突出显示有验证错误的元素来通知您错误。您可以通过提供自己的自定义错误模板并将其引用在模板使用的控件的 ErrorTemplate 附加属性中来更改此默认行为。ErrorTemplate 附加属性接受 ControlTemplate 值。通过使用自定义错误模板,您可以添加诸如图像和文本之类的视觉元素来指示验证错误。自定义错误模板的一个示例是上一节讨论的基于 Silverlight 的错误模板。

错误模板使用 adorner 层,这是一个存在于窗口内容之上的绘图层。adorner 层的核心是 AdornedElementPlaceholder,它表示控件本身。通过使用 AdornedElementPlaceholder,您可以相对于下面的控件排列错误内容。这使得可以使用不同的布局面板(如 dockpanel、grid 或 stackpanel)创建各种自定义错误模板。设计自定义错误模板时,您应该注意的一点是,adorner 层可能会覆盖视图中的其他元素。通过仔细设计您的错误模板,您可以避免这种情况。

下面的示例显示了一个使用红色边框的错误模板,并在具有无效输入的文本框上方添加了错误图像和错误文本。样式触发器设置了文本框的 ToolTip 属性,当 Validation.HasError 附加属性为 true(表示存在错误)时会调用该属性。如果出现错误,当鼠标悬停在文本框上时,错误将显示在 ToolTip 中。第二个 ToolTip 将在鼠标悬停在错误图像上时显示,如图像 ToolTip 属性所示。下面的错误模板是样式的一部分,将自动应用于文本框,如 TargetType 所示。或者,您可以通过设置 Validation.ErrorTemplate 附加属性来显式引用错误模板。

<Style TargetType="{x:Type TextBox}">
    <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="true">
            <Setter Property="ToolTip">
                <Setter.Value>
                    <Binding RelativeSource="{RelativeSource Mode=Self}" 
                             Path="(Validation.Errors)[0].ErrorContent"/>
                </Setter.Value>
            </Setter>
        </Trigger>
    </Style.Triggers>
    <Setter Property="Validation.ErrorTemplate">
        <Setter.Value>
            <ControlTemplate>
                <DockPanel LastChildFill="True">
                    <StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
                        <Image Width="16"
                                Height="16"
                                HorizontalAlignment="Center"
                                VerticalAlignment="Center"
                                Source="/Images/Error_16x16.png">
                            <Image.ToolTip>
                                <Binding ElementName="ErrorAdorner" 
                                Path="AdornedElement.(Validation.Errors)[0].ErrorContent"/>
                            </Image.ToolTip>
                        </Image>
                        <TextBlock Foreground="Red" FontSize="12" Margin="2,0,0,0">
                            <TextBlock.Text>
                               <Binding ElementName="ErrorAdorner" 
                                   Path="AdornedElement.(Validation.Errors)[0].ErrorContent"/>
                            </TextBlock.Text>
                        </TextBlock>
                    </StackPanel>
                    <Border Margin="0,2,0,0" BorderThickness="1" BorderBrush="Red">
                        <AdornedElementPlaceholder x:Name="ErrorAdorner"></AdornedElementPlaceholder>
                    </Border>
                </DockPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

通过处理 Validation.Error 附加事件也可以显示验证错误。要使用此事件,您需要将 NotifyOnValidationError 属性设置为 true。当 NotifyOnValidationError 设置为 true 时,当错误发生和清除时都会引发 Validation.Error 附加事件。Validation.Error 附加事件是冒泡事件。它首先在发生验证错误的控件中引发,然后冒泡到视觉树。因此,您可以创建一个本地错误处理程序,该处理程序处理特定控件的错误。或者,您可以创建一个在视觉树上层执行的错误处理程序来创建更通用的验证错误处理程序,如下所示。

<!--Local validation error handler -->
<TextBox Name="TextBox1"
         Validation.Error="TextBox1_Error" 
         NotifyOnValidationError="True"
         ValidatesOnExceptions="True"
         ValidatesOnDataErrors="True"/>

<!--A more generalized validation error handler -->
<Grid Validation.Error="Grid_Error" Name="mainGrid">

Validation.Error 附加事件包含 ValidationErrorEventArgs 的实例,该实例包含两个重要属性:Action,包含有关错误是添加还是清除的信息;以及 Error 对象,包含有关错误的信息。Error 对象包含以下属性:

  • BindingInError:包含导致验证错误的绑定对象的引用。
  • RuleInError:包含导致验证错误的 ValidationRule 的引用。
  • ErrorContent:包含由返回验证错误的 validationRule 对象设置的字符串。
  • Exception:包含导致验证错误的任何异常的引用。
private void Grid_Error(object sender, ValidationErrorEventArgs e)
{
    if (e.Action == ValidationErrorEventAction.Added) 
        //Handle the new added error.
    else (e.Action == ValidationErrorEventAction.Removed)
        //Handle the removed error.
}

验证摘要控件在本文中多次用于显示活动错误。这可以通过将 person 类中的错误集合与此控件绑定来实现。通过使用 Interaction.Behaviors 将 Validation.Error 附加事件从视图传递到 person 类来保持错误集合的最新状态。在可以使用 Interaction.Behaviors 之前,您需要添加对 System.Windows.Interactivity.dll 的引用,并在视图中引用命名空间,如下所示。 System.Windows.Interactivity.dll 程序集可用于 Visual Studio 2012 及更高版本。使用此程序集,您可以将事件从视图推送到 person 模型。

<Window xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity">

<TextBox x:Name="txtFirstName">
    <TextBox.Text>
        <Binding Path="FirstName" Mode="TwoWay" 
                 UpdateSourceTrigger="LostFocus" 
                 ValidatesOnDataErrors="True"
                 NotifyOnValidationError="True">
        </Binding>
    </TextBox.Text>
    <i:Interaction.Behaviors>
        <behaviors:ValidationBehavior/>
    </i:Interaction.Behaviors>
</TextBox>

ValidationBehavior 类的实现如下所示。该类从视图调用,并向 person 类添加或清除错误。

public class ValidationBehavior : Behavior<FrameworkElement>
{
    protected override void OnAttached()
    {
        AssociatedObject.AddHandler(System.Windows.Controls.Validation.ErrorEvent, 
            new EventHandler<ValidationErrorEventArgs>(OnValidationError));
    }

    private void OnValidationError(object sender, ValidationErrorEventArgs e)
    {
        Person aPerson = AssociatedObject.DataContext as Person;
        if (aPerson != null)
            if (e.Action == ValidationErrorEventAction.Added &&
                !aPerson.Errors.Contains(e.Error))
                aPerson.Errors.Add(e.Error);
            else if (e.Action == ValidationErrorEventAction.Removed &&
                    aPerson.Errors.Contains(e.Error))
                aPerson.Errors.Remove(e.Error);
    }
}

下面的视图演示了不同错误模板的用法。视图中的每个控件都使用不同的错误模板来演示自定义可能性。模板的实现细节可以在名为 ErrorTemplates.xaml 的资源字典中找到。电子邮件文本框的错误模板类似于 Windows Forms 中的 ErrorProvider,它在启动时闪烁三次,这是通过使用动画实现的。如前所述,我更倾向于基于 Silverlight 的错误模板。

将验证直接构建到控件中   

除了前面讨论的验证方法之外,您还可以直接将验证构建到控件中。这种验证称为字段级别或表单级别验证。字段级别验证在输入控件数据时检查数据,而表单级别验证在用户填写完所有字段后检查整个表单。表单级别验证可以由例如单击视图中的“确定”按钮来启动。

字段级别验证的一个示例是设置文本框的 maxlength 属性。maxlength 属性限制了文本框中可以输入的字符数,文本框将不再接受比 maxlength 属性更多的输入,并且系统会发出蜂鸣声以提醒用户。但是,设置 maxlength 属性仅对于包含固定长度数据的文本框有用,例如邮政编码或社会安全号码。另一个字段级别验证的示例是响应路由事件,并通过将 KeyEventArgs 实例的 handled 属性设置为 true 来拒绝无效字符。使用此方法,您可以例如限制对社会安全号码文本框的输入,使其仅接受数字和连字符。此方法在此处得到说明,其中 IsNumeric 方法从 PreviewKeyDown 事件处理程序调用,并过滤掉所有未输入数字或连字符的按键。

private void mainWindow_PreviewKeyDown(object sender, KeyEventArgs e)
{
    if (e.Source.GetType() == typeof(TextBox) &&
        ((TextBox)e.Source).Name == "txtSSN" &&
        !IsNumeric(e.Key))
    {
        e.Handled = true;
    }
}

private bool IsNumeric(Key key)
{
    Key [] allowedKeys = {  Key.D0, Key.D1, Key.D2, Key.D3, Key.D4, Key.D5, Key.D6, Key.D7, Key.D8 , 
                            Key.D9, Key.NumLock, Key.Back, Key.NumPad0, Key.NumPad1, Key.NumPad2, 
                            Key.NumPad3, Key.NumPad4, Key.NumPad5, Key.NumPad6, Key.NumPad7, 
                            Key.NumPad8, Key.NumPad9, Key.OemMinus };

    return Array.IndexOf(allowedKeys, key) != -1;
}

如前所述,表单级别验证是一次性验证表单上所有字段的过程。下面的代码示例说明了此方法。代码在单击“确定”按钮时检查表单上的所有文本框,并聚焦到遇到的第一个没有输入的文本框。除了设置焦点之外,还会显示错误消息以指示空文本框。为了枚举 maingrid 的所有后代,我使用了名为 VisualTreeHelper 的静态帮助类。

private void btnOK_Click(object sender, RoutedEventArgs e)
{
    EnumVisual(mainGrid, true);
}

// Enumerate all the descendants of the visual object. 
public void EnumVisual(Visual myVisual, bool firsttime = false)
{
    if(firsttime)
        txtblErrorMsg.Text = "";

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(myVisual); i++)
    {
        // Retrieve child visual at specified index value.
        Visual childVisual = (Visual)VisualTreeHelper.GetChild(myVisual, i);
        // Do processing of the child visual object. 
        if (childVisual.GetType() == typeof(TextBox) &&
            ((TextBox)childVisual).Text == "")
        {
            ((TextBox)childVisual).Focus();
            txtblErrorMsg.Text = ((TextBox)childVisual).Tag.ToString() + " is mandatory.";
            break;
        }
        // Enumerate children of the child visual object.
        EnumVisual(childVisual);
    }
}            

到目前为止讨论的示例说明了 WPF 中的表单和字段级别验证。在 Windows Forms 中也可以使用类似的验证方法。您只需要记住 Windows Forms 不支持路由事件。您可以使用其他事件,如 validating 事件,该事件在控件失去焦点之前发生,如这里所示:{Enter, GotFocus, Leave, Validating, Validated, LostFocus}。validating 事件允许您在控件失去焦点之前对其执行复杂的验证。例如,您可以测试文本框中输入的值是否与特定格式匹配。validating 事件包含 CancelEventArgs 类的实例,其中有一个属性 cancel。您可以使用此 cancel 属性来取消 validating 事件并将焦点返回到控件。为了使控件能够引发 validating 事件,您需要将该控件的 CausesValidation 属性设置为 true。

private void txtSSN_Validating(object sender, CancelEventArgs e)
{
    IsSSNValid();
}

private ErrorProvider SSN_ErrorProvider = new ErrorProvider(); 
private const string RegexSSN = @"^(?!\b(\d)\1+-(\d)\1+-(\d)\1+\b)(?!123-45-6789|219-09-9999
                                |078-05-1120)(?!666|000|9\d{2})\d{3}-(?!00)\d{2}-(?!0{4})\d{4}$";
private bool IsSSNValid()
{
    string SSNValue = txtSSN.Text;
    bool IsValid = false; 

    if (string.IsNullOrEmpty(SSNValue))
        SSN_ErrorProvider.SetError(txtSSN, "Social security number is required.");
    else if (!Regex.IsMatch(SSNValue, RegexSSN))
        SSN_ErrorProvider.SetError(txtSSN, "Please enter a valid social security number.");
    else
    {
        SSN_ErrorProvider.SetError(txtSSN, "");
        IsValid = true;
    }
    return IsValid;
}

此外,不详细说明,您还可以使用 Windows Forms 键盘事件(KeyDown、KeyPress 和 KeyUp)来验证控件。当按下键时,KeyDown 和 KeyUp 事件被引发;当释放键时,KeyUp 事件被引发。当用户按下具有相应 ASCII 值的键时,会引发 KeyPress 事件。一个有趣的点是,Windows Forms 有一个类似于 WPF 中隧道事件的表单级别键盘处理程序。使用此表单级别键盘处理程序,表单将为具有焦点的控件引发键盘事件(KeyPress、KeyDown 和 KeyUp)。您可以处理这些事件并通过将 KeyEventArgs 的 Handled 属性设置为 true 来阻止任何进一步的隧道。使用表单级别键盘处理程序,您可以创建快捷方式来填充文本框,如这里所示。为了使表单引发这些事件,必须将 KeyPreview 属性设置为 true。

private void mainForm_KeyDown(object sender, KeyEventArgs e)
{
    if (e.Alt == true && e.KeyCode == Keys.P)
        txtIdentification.Text = "Passport Id:";
    else if (e.Alt == true && e.KeyCode == Keys.M)
        cmbGender.SelectedIndex = 0; //Male 
    else if (e.Alt == true && e.KeyCode == Keys.F)
        cmbGender.SelectedIndex = 1; //Female
}

Windows Forms 中的表单级别验证与本节开头讨论的 WPF 验证类似。您可以循环遍历表单上的每个控件,并检查正在考虑的控件是否为文本框,以及它是否包含空字符串。如果找到一个包含空字符串的文本框,则将其聚焦,并显示错误消息通知用户。

ErrorProvider (Windows Forms 控件)

ErrorProvider 提供了一种在 Windows Forms 中显示验证错误的便捷方法。为了使错误条件显示在控件旁边,需要调用 ErrorProvider 的 SetError 方法。SetError 方法需要有错误的元素名称和错误消息。通常建议在控件的 validating 事件中设置 ErrorProvider。为了使控件能够引发 validating 事件,您需要将其 CausesValidating 属性设置为 true(如果尚未设置为 true)。上一节中说明了 ErrorProvider 的用法。

ErrorProvider 的不同属性会影响错误信息如何显示给用户。Icon 属性控制显示在元素旁边哪个图标,从而为用户提供视觉提示。您可能希望在一个表单上有多个 ErrorProvider,一个用于报告错误,一个用于报告警告。另一个属性是 blinkstyle 属性。此属性决定错误图标显示时是否闪烁。blink rate 属性决定图标闪烁的速度。

您可以通过创建一个数据对象来组合不同的验证方法,该对象会引发某些类型的错误异常,并使用 IDataErrorInfo 或 INotifyDataErrorInfo 来报告其他错误。在组合不同的验证方法时,请记住它们是不同的。当发生异常或验证规则失败时,有错误控件会被标记,并且绑定源属性不会用错误值进行更新。但是,当您使用 IDataErrorInfo 或 INotifyDataErrorInfo 接口时,是否更新绑定源属性取决于您的实现。

使用 MVVM 模式在 WPF 中进行验证

为了说明 MVVM 设计模式与 INotifyDataErrorInfo 接口和数据注解相结合,我创建了一个客户管理应用程序。该应用程序允许您创建、修改和删除 person 记录。删除 person 时,应用程序会显示一个消息框以确认删除。Person 记录使用 XML 序列化和反序列化进行持久化。客户管理应用程序可以在源代码目录 mvvm_validation_example 中找到。在 release 目录中,您可以找到可执行文件和可以加载到应用程序中的一个 XML 文件。MVVM 客户管理应用程序的开发受到本文 框架的启发。文章展示了在使用 MVVM 设计模式时如何从 ViewModel 打开对话框。我对这个 MVVM 框架做了一个修改,添加了“保存文件”对话框。在开发客户管理应用程序期间,我发现以下两篇文章对于理解 MVVM 模式非常有帮助:文章 1文章 2

在开发客户管理应用程序的过程中,我面临了几个挑战。我想与您分享这些挑战的解决方案。一个挑战是如何将 DateTime 值保存到文件中而不考虑系统的区域性。这可以通过将 DateTime 值保存为代表刻度数的 Int64 值来实现。使用此方法,您无需考虑存储和恢复 DateTime 值的系统的区域性。要保存 DateTime 值,您需要通过调用 ToUniversalTime 方法将 DateTime 值转换为 UTC。接下来,您需要通过调用 ticks 属性来检索 DateTime 对象中的刻度数。要恢复保存为整数的 DateTime 值,您需要通过将 Int64 值传递给 DateTime(Int64) 构造函数来实例化一个新的 DateTime 对象。然后,可以通过调用 ToLocalTime 方法将生成的 UTC 时间转换为本地时间。有关如何保存 DateTime 对象的详细描述和示例,请参阅 此处

private DateTime MembershipEndDateValue = DateTime.Now.AddYears(1);
[XmlElement(typeof(Int64), ElementName = "MembershipStartDate")]
public Int64 MembershipStartDateInTicks
{
    get { return MembershipStartDateValue.ToUniversalTime().Ticks; }
    set { MembershipStartDateValue = new DateTime(value).ToLocalTime(); }
}

客户管理应用程序使用数据网格显示 XML 文件中的所有 person。Person 记录的序列化和反序列化到 XML 文件非常简单。需要注意的一点是,XML 序列化器还会序列化父类的属性。在我的情况下,这是不期望的行为,我使用了 [XmlIgnore] 属性来跳过父属性不序列化到 XML 文件。另一点需要注意的是,您需要有一个默认/无参数构造函数才能序列化对象。最后需要注意的是,我使用 属性直接进行序列化,而不是属性的setter。原因是属性 setter 还会更新 person 对象的 Last modify date 和 dirty flag。下面显示了序列化 person 集合的 XML 文件结构。

<?xml version="1.0" encoding="utf-8"?>
<ArrayOfPerson xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
               xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Person>
    <ModifyDate>635659158502846032</ModifyDate>
    <CreationDate>635606470136152491</CreationDate>
    <UniqueId>Person_20150324175136690_b4b3dd9f-9186-46ba-b25a-275a8e3c40c3</UniqueId>
    <FirstName>Barbara</FirstName>
    <LastName>Strauss</LastName>
    <Gender>Female</Gender>
    <MaritalStatus>Married</MaritalStatus>
    <SSN>230-82-1365</SSN>
    <Email>Barbara.Strauss@gmail.com</Email>
    <Identification>Passport ID Number 8001015009087</Identification>
    <CellPhoneNumber>415-570-1889</CellPhoneNumber>
    <PhoneNumber>(425) 555-0123</PhoneNumber>
    <DayOfBirth>627518808000000000</DayOfBirth>
    <MembershipStartDate>635606470136152491</MembershipStartDate>
    <MembershipEndDate>635940838136162491</MembershipEndDate>
    <HomePage>https%3a%2f%2ffacebook.com%2fBarbara.straus</HomePage>
  </Person>
</ArrayOfPerson>  

数据网格的第一行包含一个展开器。单击展开器时,会显示 person 详细信息。此功能通过实现 DataGrid 的 RowDetailsTemplate 来实现。文件菜单、工具栏、DataGrid 上下文菜单和按钮执行相同的命令,这些命令在 MainWindowViewModel 中实现。Command 按钮始终启用,如命令的 CanExecute 方法所示,该方法始终返回 true。这样,用户就不会因为不清楚的原因而面对禁用的按钮。请注意,命令的 CanExecute 方法是可选的;如果未指定 CanExecute 方法,则命令按钮始终启用。命令的 execute 方法检查它是否可以执行,如果命令因某种原因无法执行,则会在状态栏上显示描述性错误消息。我使用窗口关闭事件来提示用户在离开之前保存更改。当用户单击关闭按钮或单击右上角的“X”关闭按钮时,会引发窗口关闭事件。Person 记录的序列化和反序列化是通过 BackgroundWorker 完成的。由于 BackgroundWorker,Person 集合由两个不同的线程访问,第一个是 UI 线程,第二个线程是 BackgroundWorker。由于多个线程访问同一个集合,我使用了一个线程安全的集合,可以在 此文章中找到。另一种方法是使用 Dispatcher.Invoke 包装负责更新集合的代码,如 此文章中所述。

错误处理通过结合使用 INotifyDataErrorInfo 接口和数据注解来进行。验证错误使用本文前面讨论过的基于 Silverlight 的错误模板显示。会员开始日期必须早于结束日期。此要求通过跨属性验证强制执行。跨属性验证是使用自定义验证器。我创建了一个验证摘要控件来显示所有活动错误。验证摘要控件使用一个简单的行为 ValidationBehavior 进行更新,该行为将验证错误从 View 传输到 ViewModelBase 类。当没有错误时,验证摘要控件将使用数据触发器隐藏。您可能已经注意到,下面的屏幕截图仅突出显示了九个错误中的两个。如前所述,错误仅在相应控件具有焦点或鼠标光标悬停在工具提示角上时才会突出显示。在下面的屏幕截图中,第一个名字文本框具有焦点,鼠标光标悬停在电话文本框的工具提示角上。验证摘要控件显示所有九个活动错误。

下面显示了客户管理应用程序的类图。您可以从 Visual Studio 中创建此类图。它允许您查看项目中的所有类型及其关系。要创建可以查看项目所有类型的类图,请执行以下操作。在解决方案资源管理器中,右键单击项目并选择“查看类图”。将创建一个新的类图并将其添加到解决方案中。我已折叠生成的类图类,以便您可以看到概览。下面的类图包含在源代码中,并允许您探索所有类属性。

客户应用程序由两个视图组成。第一个视图是 MainWindow 视图,它是默认视图,并在应用程序启动时显示给用户。它包含一个数据网格来显示所有加载的 person,并具有多个按钮来执行各种命令。第二个视图是 CustomerView,当用户想要编辑或添加新 person 时显示。这两个视图都不包含任何代码隐藏,而是将这两个视图的 DataContext 设置为它们各自的 viewmodel。

ViewModelBase 类包含一个 SetProperty 方法,该方法在 ValidatableViewModelBase 类中被重写。SetProperty 检查 person 属性是否已更改,如果是,则为该属性引发 PropertyChanged 事件,设置 IsDirty 标志,并更新该 person 对象的 Last Modified 日期。PersonService 负责加载和保存 person。当客户应用程序启动时,会创建一个 PersonService 实例并将其传递给 MainWindowViewModel,然后 MainWindowViewMode 类使用它来检索 person 的 Ilist。然后使用 CollectionViewSource 对此 person 的 Ilist 进行过滤。过滤是必需的,以便从视图中删除其删除标志被设置的 person。当用户从 MainWindowViewModel 中显示的集合中删除 person 时,会设置 person 的删除标志。当用户保存 person 集合时,所有标记有删除标志的 person 都将从集合中删除。除了删除标志之外,person 类还使用 IsNew 和 IsDirty 标志来跟踪更改。编辑 person 时,CustomerView 中所做的更改会直接应用于 person 对象。如果用户通过单击取消按钮取消所做的更改,则会执行回滚,如以下所示使用Memento模式。

Caretaker<Person> editableObject = new Caretaker<Person>(SelectedPersonValue);
editableObject.BeginEdit();
CustomerViewModel customerViewModel = new CustomerViewModel(ref SelectedPersonValue);
bool? dialogResult = _dialogService.ShowDialog<CustomerView>(this, customerViewModel, "Edit customer");
if (dialogResult.HasValue && dialogResult.Value)
    editableObject.EndEdit();
else
    editableObject.CancelEdit();

结论

从本文可以看出,有许多选项来实现验证规则。在到目前为止讨论的所有选项中,更倾向于 INotifyDataErrorInfo 接口。它比 IDataErrorInfo 接口提供了更多的灵活性,通常应在新类实现时使用。为了提高代码的可维护性,我建议您在验证中使用 INotifyDataErrorInfo 接口与数据注解相结合。使用基于 Silverlight 的错误模板结合错误摘要控件是我显示错误的首选方式。

关注点

请注意,Visual Studio 2010 会因未处理的异常而中断。因此,为了能够测试 ExceptionValidationRule 错误处理,您需要取消选中 Common Language Runtime Exceptions。要执行此操作,请按 CTRL + ALT + E 组合键,然后取消选中 Thrown 和 User-unhandled 复选框。   

另一个强大的验证控件(本文未讨论)是 MaskedTextbox。最初,它是为 Windows Forms 开发的,也可以通过将该控件托管在 WIndowsFormsHost 元素中来用于 WPF。此外,还有第三方开发了自己的 WPF MaskedTextbox 控件,可以在 此处找到列表。MaskedTextProvider 是 MaskedTextbox 的基础,MaskedTextProvider 可以被视为一个修改过的文本框控件,它允许您定义接受或拒绝用户输入的模式。

请注意,ValidationRules 可以在绑定过程的各种点执行,具体取决于您为 ValidationRule 的 ValidationStep 属性设置的值。默认值为 RawProposedValue,它在任何转换发生之前运行 ValidationRule。所有可能的 ValidationStep 枚举可以在 此处找到。此外,这篇 msdn 文章详细描述了验证过程。

使用消息框显示验证错误是一种不良做法,因为消息框过于侵入性。

通常,System.Windows.Interactivity.dll 程序集可以在以下位置找到:C:\Program Files\Microsoft SDKs\Expression\Blend\.NETFramework\v4.5\Libraries

参考文献

  • MCTS 自学培训套件 考试 70-511
  • Pro WPF 4.5 in Csharp 第 4 版

历史

日期,   程序集版本,   注释

2015 年 8 月 1 日,   1.0.0.0,   创建了文章。

© . All rights reserved.