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

Windows Presentation Foundation 中的验证

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (128投票s)

2006 年 8 月 20 日

12分钟阅读

viewsIcon

918240

downloadIcon

18736

在本文中,我将引导您了解 Windows Presentation Foundation 中内置的验证类。然后,我将讨论一种可能更适合丰富领域层的替代验证方法,即创建一个自定义 WPF ErrorProvider。

Sample Image - wpfvalidation3.png

引言

自软件开发以来,对可靠信息最大的威胁一直是最终用户。经过几十年的计算机存在,为什么我们仍然需要告知用户“开始日期应始终小于结束日期”,或者“名字是必填字段”?由于 User.Current.Electrocute() 要到 .NET 4.0 才会添加到 .NET Framework 中,所以我们大多数人通常不得不诉诸显示某种 UI 提示来礼貌地告诉用户他们做错了什么,以及如何纠正。

在 Windows Forms 中,我们有 ErrorProvider,在 ASP.NET 中,我们有 Validator 控件。在 Windows Presentation Foundation 中,方法将发生变化,但目标保持不变:告知用户出了问题。在本文中,我将讨论 Windows Presentation Foundation(以下简称 WPF)中输入验证的设计处理方式。具体来说,我将讨论 ExceptionValidationRule 类、创建自定义 ValidationRule、如何显示错误、*何时*显示错误(UpdateSourceTrigger),最后,一种使用 Windows Forms ErrorProvider 的 WPF 实现的替代方法。

目录

  1. 欢迎来到 WPF 验证
  2. 非常简单的验证:ExceptionValidationRule
  3. 显示错误
  4. 自定义 ValidationRules
  5. 控制验证时机:UpdateSourceTriggers
  6. ErrorProvider 在哪里发挥作用?
  7. IDataErrorInfo
  8. 创建我们的 ErrorProvider
  9. 结论
  10. 特别感谢

欢迎来到 WPF 验证

如果您从未花太多时间研究 Windows Forms 或数据绑定,我希望本文足够简单,您可以轻松跟上。Windows Presentation Foundation 中的验证方法与 ASP.NET 验证非常相似,即大多数“业务规则”在用户界面上强制执行并应用于特定的控件和绑定。这种方法非常容易理解和实现,但一些“富领域模型”和面向对象设计的支持者(包括我自己)对此有些问题。在文章结束时,我将讨论为什么会这样,以及一种不同的处理方式,同时仍然利用 WPF 的一些优势。

另一方面,如果您花了很多时间使用 Windows Forms,并大量使用了 ErrorProvider/IDataErrorInfo 方法进行验证,您可能会失望地发现 Windows Presentation Foundation 中没有这些。幸运的是,由于我也对必须“在 UI 上”强制执行所有内容感到失望,因此在文章的最后,我将展示如何在 Windows Presentation Foundation 应用程序中创建 ErrorProvider。但是,即使您坚信在 UI 上进行验证是一个坏主意 (TM),我也鼓励您阅读全文,因为我将讨论 WPF 中您可以继续利用的一些其他验证功能。

警告:所有示例代码都使用 .NET 3.0 的BETA 2 版本编写,因此其中一些内容可能已发生更改。希望概念能够保持不变,否则这篇文章将大打折扣:)

非常简单的验证:ExceptionValidationRule

大多数时候,当我们谈论验证时,我们谈论的是验证用户输入(其他类型的验证超出了本文的范围)。让我们看看 WPF 提供的最简单的内置验证类型 - ExceptionValidationRule。为了开始我们的示例,让我们创建一个简单的 Customer

public class Customer 
{
    private string _name;
    
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

编写类很有趣,但您可能永远无法说服人们付钱给您,除非您可以添加某种 UI 让您与您的类进行交互。由于本文是关于 Windows Presentation Foundation 的,让我们使用 XAML 创建我们的 UI。作为聪明的开发者,我们还将使用数据绑定

<TextBox Text="{Binding Path=Name}" />

在我们继续之前,需要注意的是,上面看起来奇怪的标记实际上只是编写此内容的简写形式

<TextBox>
    <TextBox.Text>
        <Binding Path="Name" />
     </TextBox.Text>
</TextBox>

现在,假设您的要求之一是客户姓名是必填的。要实现此约束,您可以将客户的 Name 属性更改为如下所示

public string Name
{
    get { return _name; }
    set
    {
        _name = value;
        if (String.IsNullOrEmpty(value))
        {
            throw new ApplicationException("Customer name is mandatory.");
        }
    }
}

通过对数据绑定使用 WPF *验证规则*,我们可以自动显示此错误。我们只需要在绑定上使用 ValidationRules 属性,如下所示

<TextBox>
    <TextBox.Text>
        <Binding Path="Name">
            <Binding.ValidationRules>
                <ExceptionValidationRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

如果您运行了此代码,您将看到类似以下内容

A WPF window showing a TextBox that has been given a red border, indicating that it is invalid.

显示错误

Windows Presentation Foundation 在 System.Windows.Controls 命名空间中有一个静态类,其名称富有想象力,称为“Validation”。它有许多静态*依赖属性*,您可以将它们应用于任何控件。我们最感兴趣的是

  • Errors - 应用于此控件上绑定的错误消息列表。
  • HasError - 指示 Errors 属性中是否有任何错误。
  • ErrorTemplate - 如果有错误,我们可以应用的 ControlTemplate

默认情况下,Validation 类使用一个*ErrorTemplate*,该模板在控件周围有一个红色边框,这就是我们在上面的程序中得到红色边框的原因。如果默认的红色边框不是您喜欢的,我能理解。您可能想要带橙色感叹号的粗绿色边框

A custom control template used to display errors, with a green border and orange exclamation marks.

要达到您想要的外观,您可以定义自己的控件模板

<Application.Resources>
  <ControlTemplate x:Key="TextBoxErrorTemplate">
    <DockPanel LastChildFill="True">
      <TextBlock DockPanel.Dock="Right" 
        Foreground="Orange" 
        FontSize="12pt">!!!!</TextBlock>

      <Border BorderBrush="Green" BorderThickness="1">
         <AdornedElementPlaceholder />
      </Border>
    </DockPanel>
  </ControlTemplate>
</Application.Resources>

AdornerElementPlaceholder 用于表示“在此处放置无效控件”。创建此模板后,您可以通过设置 Validation.ErrorTemplate 附加属性来将其重用到客户姓名 TextBox

<TextBox 
    Validation.ErrorTemplate="{StaticResource TextBoxErrorTemplate}"> 
    [...]
    <TextBox>

或者,为了省去每次都设置 ErrorTemplate 的麻烦,您可以在 WPF Style 中完成。如果我们设置样式的 TargetTypeTextBox,并且不使用键,那么我们应用程序中的所有文本框都将自动获得此样式

<Style TargetType="{x:Type TextBox}">
    <Setter Property="Validation.ErrorTemplate">
        <Setter.Value>
            <ControlTemplate>
                <DockPanel LastChildFill="True">
                    <TextBlock DockPanel.Dock="Right" 
                        Foreground="Orange"
                        FontSize="12pt">
                        !!!!
                    </TextBlock>
                    <Border BorderBrush="Green" BorderThickness="1">
                        <AdornedElementPlaceholder />
                    </Border>
                </DockPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

现在,绿色边框固然不错,但它们并不能真正告诉用户他们做错了什么。由于我们在前面抛出的异常中包含了错误消息,我们可以利用静态 Validation.Errors 附加属性来获取此值并将其用作 TextBox 的属性。最常见的例子是设置 ToolTip,如下所示

<Style TargetType="{x:Type TextBox}">

    [... SNIP: The code from above ...]
    
    <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>

现在,如果我们鼠标悬停在处于错误状态的 TextBox 上,我们将获得一个工具提示,告知我们做错了什么

An ErrorTemplate with the ToolTip for the TextBox set to the error message.

我们所需要做的就是将该样式放入我们的应用程序资源中,确保将任何验证规则应用于我们的绑定,然后就完成了!

更高级地讲,如果您宁愿将错误显示在其他地方而不是 ToolTip 中,例如在橙色感叹号所在的 TextBlock 中,您可以使用如下样式

<Style TargetType="{x:Type TextBox}">
    <Setter Property="Validation.ErrorTemplate">
        <Setter.Value>
            <ControlTemplate>
                <DockPanel LastChildFill="True">
                    <TextBlock DockPanel.Dock="Right"
                        Foreground="Orange"
                        FontSize="12pt"
                        Text="{Binding ElementName=MyAdorner, 
                               Path=AdornedElement.(Validation.Errors)
                               [0].ErrorContent}">
                    </TextBlock>
                    <Border BorderBrush="Green" BorderThickness="1">
                        <AdornedElementPlaceholder Name="MyAdorner" />
                    </Border>
                </DockPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

An ErrorTemplate with a green border and the error message being displayed in a TextBlock as part of the adorner.

自定义 ValidationRules

我知道您在想:“我必须抛出异常吗?”

很高兴您问了这个问题。如果您还记得上面客户的 Name 属性,您会记得我们抛出了一个异常以使用 ExceptionValidationRule 显示错误。我相信大多数人会同意抛出异常并不是处理用户输入错误的最佳方式。除了性能损失外,我个人认为“用户输入”错误不算是“*异常*”情况,因此它们并不是异常的真正设计目的。

如果您在框架中查找,您会发现 ExceptionValidationRule 类继承自抽象的*ValidationRule*类。ValidationRule 类如下所示

public abstract class ValidationRule
{
    public abstract ValidationResult Validate(
        object value, 
        CultureInfo culture);
}

我们之前创建绑定时,有一些 XAML 看起来像这样

<Binding Path="Name">
    <Binding.ValidationRules>
        <ExceptionValidationRule />
    </Binding.ValidationRules>
</Binding>

事实是,我们可以使用任何继承自 ValidationRule 基类的东西。让我们创建一个自定义 ValidationRule,专门用于验证字符串的长度。

namespace MyValidators
{
    public class StringRangeValidationRule : ValidationRule
    {
        private int _minimumLength = -1;
        private int _maximumLength = -1;
        private string _errorMessage;
        
        public int MinimumLength
        {
            get { return _minimumLength; }
            set { _minimumLength = value; }
        }
        
        public int MaximumLength
        {
            get { return _maximumLength; }
            set { _maximumLength = 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();
            if (inputString.Length < this.MinimumLength ||
                   (this.MaximumLength > 0 &&
                    inputString.Length > this.MaximumLength))
            {
                result = new ValidationResult(false, this.ErrorMessage);
            }
            return result;
        }
    }
}

我们的验证器有三个属性:MinimumLengthMaximumLength 和一个 ErrorMessage 字符串,如果值不在范围内,则显示该字符串。现在我们准备在 XAML 中使用它。

首先,我们需要在文件顶部的 XML 命名空间中引用它

<Window [...]
    xmlns:validators="clr-namespace:MyValidators" />

现在我们可以将其添加到我们绑定的 ValidationRules

<Binding Path="Name">
    <Binding.ValidationRules>
         <validators:StringRangeValidationRule 
            MinimumLength="1" 
            ErrorMessage="A name is required." />
     </Binding.ValidationRules>
</Binding>

如果您添加了我上面提供的样式,那么您要做的就是这些,就可以显示验证消息了。

控制验证时机:UpdateSourceTriggers

如果您运行了上面任何一个示例代码,您会注意到验证只在您按 Tab 键或单击文本框外时发生。这在大多数情况下是一个不错的默认设置,但如果您希望在其他时间进行验证,可以使用绑定上的 UpdateSourceTrigger 属性。

UpdateSourceTrigger 属性可以接受以下三个可能值之一

  • 默认值是 LostFocus,这意味着在 UI 元素失去焦点时(例如,当您 Tab 键移开或单击另一个元素时),绑定的数据源将被更新和验证。
  • 第二个选项是 PropertyChanged,当您正在绑定的属性(上面的示例中的 TextBoxText 属性)更改时发生。这使您可以在用户键入或更改值时随时进行验证,而不必等到他们 Tab 键移开(这对于过滤项目列表非常有用)。
  • 最后,还有 Explicit 选项,仅在您指示它发生时才发生。

要在我们的代码中使用 UpdateSourceTrigger 属性,我们只需要在绑定中设置它

<TextBox>
    <TextBox.Text>
        <Binding Path="Name" UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <ExceptionValidationRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

有关 UpdateSourceTriggers 的更多信息,请参阅MSDN 上 UpdateSourceTrigger 枚举的页面

ErrorProvider 在哪里发挥作用?

我们已经讨论过 ExceptionValidationRule 不是一种很好的验证方式。您在创建自定义验证规则时可能遇到的问题,如我上面所示

  1. 我们的业务“规则”在标记中定义,隐藏在我们的绑定中,难以查找和维护,并且难以重用。
  2. 我们无法轻松获取表单上所有无效规则的“列表”,以便执行其他逻辑——例如,在单击“保存”按钮时确定客户是否可以保存。

为某些应用程序定义标记中的业务规则可能不是问题,但在具有丰富业务对象(如Trial Balance)的应用程序,或使用丰富框架(如CSLA)的应用程序,或规则在多个屏幕或应用程序之间共享的情况下,您可能会发现这是一个很大的限制。如果这对您来说不是问题,那么您可能无法从本文的其余部分获得太多收获。

这意味着 ValidationRules 方法对我们一些人来说不太有用。但是,并非一切都完了,因为我们仍然可以利用静态 Validation 类,以及它使用样式和控件模板来控制错误的*显示*。

IDataErrorInfo

IDataErrorInfo 接口自 .NET 1.1 以来就一直存在,并且在 DataSet 中得到了广泛使用,也被许多业务对象框架使用。您会在 System.ComponentModel 命名空间中找到这个宝藏。如果您一直在阅读我的博客一段时间,希望这个接口对您来说并不陌生。

IDataErrorInfo 的设计目的是从 UI 控件绑定的对象报告错误。Windows Forms 中的 DataGrid (1.1) 和 DataGridView (2.0) 都可以自动检测它们绑定的对象上的此接口是否存在,并显示任何错误,无需任何额外工作。Windows Forms *ErrorProvider*可以自动用于显示来自对象(以及 ErrorProvider)绑定的任何控件上的错误,所有这些都无需编写额外代码。

为了让您回忆起来,这是我们在 .NET 2.0 中使用此接口的方式

public class Customer : IDataErrorInfo
{
    private string _name;
    
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
    
    public string Error
    {
        get
        {
            return this[string.Empty];
        }
    }
    
    public string this[string propertyName]
    {
        get
        {
            string result = string.Empty;
            propertyName = propertyName ?? string.Empty;
            if (propertyName == string.Empty || propertyName == "Name")
            {
                if (string.IsNullOrEmpty(this.Name))
                {
                    result = "Name cannot be blank!";
                }
            }
            return result;
        }
    }
}

当时,我们只需要将一个网格绑定到我们的 Customer 对象,并且任何错误都会自动报告。如果我们将 CustomerName 绑定到 TextBox,我们只需要将一个 ErrorProvider 拖到窗体上,将其 DataSource 设置为我们的 Customer 对象,我们所有的验证就都完成了。

遗憾的是,IDataErrorInfo 在 WPF 中似乎已被弃用,因为似乎没有内置支持。这意味着 WPF 中也没有 ErrorProvider 组件。由于 ErrorProvider 的概念相当简单,让我们尝试自己做一个。

更多关于 IDataErrorInfo...

MSDN 上关于 IDataErrorInfo条目是一个很好的起点。要深入讨论 IDataErrorInfo 接口以及更好地实现业务对象验证,您可能会喜欢我最近在 CodeProject 上的委托和业务对象文章。 Rocky Lhotkas 精湛的CSLA 架构也广泛使用了 IDataErrorInfo 接口。

创建我们的 ErrorProvider

我构建的 ErrorProvider(可在随附的示例代码中找到)继承自 WPF Decorator 类,这意味着您可以“将其内部放入内容”。要使用它,您只需执行类似以下操作

<validators:ErrorProvider>
    <StackPanel>
        <TextBox Text="{Binding Path=Name}" />
        <TextBox Text="{Binding Path=Age}" />
    </StackPanel>
</validators:ErrorProvider>

我的 ErrorProvider 的工作原理是遍历其中所有控件,并查找其属性上的任何数据绑定。当其中一个绑定属性的值发生更改时,它会检查其 DataContext 是否实现了 IDataErrorInfo,如果实现了,则获取任何错误消息,并使用内置的静态 Validation 类显示它们。这意味着您可以使用我上面展示的样式和控件模板,同时将所有验证逻辑保留在另一个类中。

在 WPF 中遍历控件层次结构是通过LogicalTreeHelper 类完成的。代码本身有点长,无法在此处发布,但包含在页面顶部的示例代码下载中。

使用 WPF ErrorProvider 有许多好处

  • 您的 XAML 更紧凑,因为您不需要为每个绑定添加 ValidationRule 列表。
  • 您的所有验证都可以由您的业务对象完成,而不是在 UI 上完成。这允许您对对象进行出色的封装,同时仍然拥有信息丰富的 UI。
  • 您可以调用 ErrorProvider 上的 Validate() 方法来强制验证,并检查控件是否有效,而不是检查每个控件。
  • 您可以使用 GetFirstInvalidElement() 方法来获取表单上的第一个有错误信息的控件,以便轻松将其设置为焦点。例如,如果您的“保存”按钮已被单击,但您想告诉他们他们仍然有错误,这将非常有用。

结论

无论您选择使用自定义 ValidationRule、我的 ErrorProvider,还是仅仅抛出异常,我都希望我提供了足够的信息,让您做出适合您项目的明智决定。

如果您的应用程序具有非常丰富的域模型,您可能会发现 ErrorProvider 方法很有帮助。如果您的应用程序非常面向服务,或者非常以 UI 为中心,ValidationRules 可能就是您所需要的。

当然,如果这些方法都不符合您的要求,您总是可以等待 .NET 4.0 和 User.Current.Electrocute() 的推出:)

特别感谢

我想特别感谢 **Paul Czywcynski**,来自TempWorks,他对我编写的 ErrorProvider 进行了大量测试,并给了我几个 bug 修复,使其能够在一些我没有尝试过的地方工作。

我还想感谢 **Mike Brown**,他向我介绍了 LogicalTreeHelper,并且还向我介绍了 ValidationRules(当然,他是在我完成了我的 ErrorProvider *之后*才提及它们的)。

© . All rights reserved.