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

WPF 中的验证

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (58投票s)

2015 年 1 月 9 日

CPOL

22分钟阅读

viewsIcon

157663

downloadIcon

6631

WPF 验证的一般探索

引言

该项目是关于 WPF MVVM 项目中的验证。本文探讨了各种选项,并构建了一个简单的 WPF 应用程序,该应用程序将两个数字相加。加法器应用程序实现为一个用户控件,包含两个用于输入的 TextBox 和一个 Calculate 按钮。

架构审查

下图显示了一个典型的“企业级”WPF 应用程序。

1. 包括 MVVM 的企业架构。

该架构试图通过将应用程序组织成层来降低其复杂性,遵循“关注点分离”的原则。

  1. 数据库提供持久化服务。
  2. 存储库提供了一个简单的数据库接口,并隐藏了实际存储的细节。服务提供可替换/共享模块的接口。
  3. 控制器响应用户输入,指导代码执行。
  4. 模型包含“业务对象”,其中包含建模业务操作的逻辑(可能还有瞬态数据)。
  5. 视图模型包含在视图中显示的数据。视图模型中的数据需要与视图紧密对应,因为绑定提供了视图中的元素和视图模型中的属性之间的一一对应关系。它在与视图直接交互的模型上提供了一个“门面”——它与关系数据库中的视图具有相同的目的。
  6. 视图显示来自视图模型的内容并与用户直接交互。

使用 Code-behind 通常被认为是糟糕的做法。这有两个很好的理由。首先,在视图模型上定义的验证允许视图中数据的不同表示。例如,可以在视图中使用单选按钮或 ComboBox 选择调用或放置的选项类型。如果验证逻辑在视图模型中定义,则可以在任一情况下应用。应该可以在不更改 XAML 之外的任何内容的情况下从一种表示更改为另一种表示。其次,更重要的是,在视图模型上定义单元测试相对容易,因此将验证和其他用户界面逻辑放在那里,可以将它们纳入单元测试的范围。

命令处理

表单需要一些触发设备(例如按钮或菜单)来调用处理。通常,相同的处理会由两者调用(应用程序可能同时具有“保存”按钮和“保存”菜单选项)。如果按钮和菜单选项使用相同的机制,代码会更简洁,熟悉 Win32 的人知道这种机制已经存在——Windows 命令。

.NET 组件接收类似 Windows 的命令,它们实现了下面所示的 ICommand 接口。

	interface ICommand
	{
		bool CanExecute(Object parameter);
		void Execute(Object parameter);
	}

下图显示了与命令处理相关的控制流。WPF 基础设施查询绑定到可视组件(按钮或菜单项)的对象 (RelayCommand) 的 ICommand 接口,RelayCommand 反过来查询视图模型以确定它是否处于允许处理继续的状态。如果是,则可视组件(按钮或菜单项)被启用;如果不是,则可视组件未启用(灰色显示)。

如果该可视组件(按钮或菜单项)已启用,并且用户单击它,则执行将通过 RelayCommand 传递到控制器。RelayCommand 绕过了标准事件处理(冒泡、隧道)

2. 包括 MVVM 和命令处理的企业架构。

在下面的代码中,一个按钮绑定到视图模型中的 CalculateCommand (ICommand) 对象。当用户单击视图中的按钮时,框架调用视图模型中 CalculateCommand.ICommand 上的 Execute 方法。

CalculateCommand 最初为 null——视图模型不应该知道任何命令处理。

CalculatorView.xaml:

	<Button Content="Calculate" ... Command="{Binding Path=CalculateCommand}"... />

AdderViewModel.cs:

	public ICommand CalculateCommand { get { return CalculateCmd; } }   // Must be a property to allow the button to bind to it.

	public ICommand CalculateCmd = null;

启动时,控制器将 RelayCommand 实例“注入”到视图模型中,其目的是将命令中继到控制器进行处理。RelayCommand 使用一个委托进行初始化,该委托用于执行命令处理,以及在视图模型上调用的方法,以确定它是否处于可以进行命令处理的状态。

Controller.cs:

	ViewModel.CalculateCmd = new RelayCommand((object z) =>
	{
		try
		{
			// Execute command.
		}
		catch(Exception)
		{
			...
		}
	},
	ViewModel.CanCalculate);

什么是验证?

在讨论中,什么被认为是“验证规则”,什么被认为是“业务规则”并不总是很清楚。例如,“期权类型必须是看涨或看跌”这样的约束会被大多数人认为是验证规则,而“买方必须是现有客户”这样的约束会被大多数人认为是业务规则。这种区别似乎是务实的——验证规则可以在视图/视图模型级别应用,而业务规则需要进入模型以访问业务逻辑。因此,什么是验证规则和什么是业务规则可以取决于实现!

另一些人则认为,如果约束可能发生变化,则应将其视为业务规则。例如,“必须提供有效的 CUSIP 代码”的规则可能会被“必须提供有效的 ISIN 代码”的规则取代,因此该规则应被视为业务规则。这种分类需要准确评估变化的 likelihood,因此在实践中并不是特别有用。

我们将假设验证主要关注发现视图和视图模型中的数据错误。

应该注意的是,用户不会做这种区分。对他们来说,两者都只是“规则”,有些简单,有些复杂,当规则被打破时,结果只是被视为输入错误。

验证错误集合

MVVM 模型要求计算和控制功能基于视图模型中的数据,而不是视图中的可视元素。当发生验证错误时,视图模型不会更新,在这种情况下,视图将包含无效数据,视图模型将包含上次有效的输入。即视图和视图模型变得不同步。因此,只查看视图模型的计算和控制逻辑需要了解发生了哪些验证错误,以便它可以确定显示给用户的数据的真实状态。

框架将 System.Windows.Controls.ValidationError 集合与每个输入控件相关联,但是从视图模型内部访问它并不容易或不合适——视图模型可以与多个视图一起使用,因此它不应该包含特定于视图的逻辑。相反,更简单的方法是在发生验证错误时捕获它们,并将它们存储在视图模型中单独的用户定义的验证错误集合中。

验证错误有两个来源

  1. 框架检测到的验证错误,包括数据转换错误和注册到视图的验证规则。
  2. 视图模型中的用户验证代码——在这种情况下,用户代码可以直接从集合中添加或删除验证错误。

如果 ValidatesOnExceptions 绑定属性设置为 True,框架将在更新视图模型中的基础数据期间检查抛出的异常。如果抛出异常,框架将向与该控件关联的 Validation.Errors 集合添加一个系统 ValidationError。如果 NotifyOnValidationError 绑定属性也设置为 True,框架将引发一个路由的 System.Windows.Controls.Validation.ErrorEvent,该事件可以由用户框架捕获。

CalculateView.xaml:

	<TextBox ...>
		<Binding ... Path="y" ValidatesOnExceptions="True" NotifyOnValidationError="True" .../>
	</TextBox>

以下代码演示了如何捕获验证错误事件。事件处理程序从视图模型验证错误集合中添加或删除验证错误。

ViewBase.cs:

	public virtual void OnLoad(object sender, System.Windows.RoutedEventArgs e)
	{
		...
		AddHandler(System.Windows.Controls.Validation.ErrorEvent, new RoutedEventHandler(listener.Handler), true);
	}

	public virtual void OnUnload(object sender, System.Windows.RoutedEventArgs e)
	{
		RemoveHandler(System.Windows.Controls.Validation.ErrorEvent, new RoutedEventHandler(listener.Handler));
	}

加法器 CanCalcute 方法的实现变得微不足道——在下面的代码中,如果验证错误集合不为空,则 HasErrors 返回 true。

AdderViewModel.cs:

	public bool CanCalculate(object z)
	{
		return    x.HasValue
			   && y.HasValue 
			   && !HasErrors;
	}

该项目包含用于在视图模型中创建和维护用户定义的错误验证集合的库代码。

验证触发器

验证可能由于以下事件而发生

  • 用户按下某个键将文本输入到控件中。
  • 用户使用鼠标进行选择。
  • 控件失去焦点。
  • 用户单击按钮执行某个操作。

应用程序可以通过主动管理显示给用户的选项来确保用户只选择有效数据,例如在下拉列表中。因此,将不再讨论这种输入模式。

开发人员可以通过将控件的绑定 UpdateSourceTrigger 属性分别设置为 PropertyChangeLoseFocus 来选择立即验证键盘输入或在失去焦点时进行验证。

立即验证键盘输入可能会出现问题。用户可能会在没有做任何“错误”的情况下通过一个无效状态。例如,假设一个 TextBox 接受 880 到 1100 之间的整数值。TextBox 中包含“999”,但用户需要将其更改为“899”——用户别无选择,只能删除其中一个 9,在这种情况下,控件会经过一个无效状态。

例行使用消息框显示验证错误被认为是糟糕的做法,因为它们通常被认为过于 intrusive。(想象一下,如果在上一个示例中每次检测到无效输入时都显示一个消息框,用户会怎么想!)这并不排除它们用于报告主命令处理失败,尽管即使在这里也有更好的选择。

类型转换

隐式验证是由于框架将用户输入转换为视图模型中绑定的数据类型而发生的。例如,如果一个 TextBox 绑定到视图模型中的一个 Double,框架将尝试将用户文本转换为 Double。如果框架失败,它将生成一个验证错误(例如,“输入字符串格式不正确”)。

更准确地说,如果绑定属性 ValidatesOnExceptions=True,框架将捕获在更新底层数据期间抛出的任何异常。如果绑定属性 NotifyOnValidationError="True",则可以设置一个处理程序来捕获 System.Windows.Controls.Validation.ErrorEvent;然后处理程序可以将错误添加到用户定义的错误容器中。(参见 ViewBase.cs)

必填字段

如果一个 TextBox 绑定到一个数字字段(double、int、decimal 等),并且用户尝试删除文本,框架会将其视为验证错误,因为它无法将空白文本转换为数字值。实际上,值转换器将简单的数字字段视为必填字段。

如果一个字段是必填的,那么最好使用单独的错误消息来表明,而不是指望用户在遇到错误空控件时正确解释“输入字符串格式不正确”。

建议在视图模型中始终对数字字段使用 Nullable 类型。由绑定属性 TargetNullValue 指定的值将转换为 null 值(通常 TargetNullValue="",因此空白输入映射到 null 数据值)。

请注意,WPF *通常*验证用户输入,而不是字段值,因此有趣的是,当带有必填字段的表单启动时,没有验证错误。(参见图 3)一旦使用了某个字段,清除后它将生成验证错误。(参见图 4)。然而 IDataErrorInfo 在这方面不一致;如果应用程序使用 IDataErrorInfo,框架将在启动时查询接口以获取错误!

3. 启动时。
图 4. 清除文本后。

验证处理

内置 ValidationRules

  • 如果 ValidateOnDataError=True,则内置的 DataErrorValidationRule 将添加到控件的 ValidationRules 列表中。

  • 如果 ValidateOnExceptions=True,则 ExceptionValidationRule 将添加到控件的 ValidationRules 列表中。请注意,即使 ExceptionValidationRule 在控件的 ValidationRules 列表中,只有当 NotifyOnValidationError = "True" 时才会调用任何处理程序。

  • 如果 ValidatesOnNotifyDataErrors=True,则 NotifyDataErrorValidationRule 将添加到控件的 ValidationRules 列表中。NotifyDataErrorValidationRule.Validate 调用 INotifyDataErrorInfo.GetErrors 方法,并以绑定的属性名称作为其参数。

DataErrorValidationRuleExceptionValidationRuleValidatesOnNotifyDataErrors 可以直接通过 XAML 添加到绑定中,而不是通过 ValidateOnDataErrorValidateOnExceptionsValidatesOnNotifyDataErrors 隐式添加。

请注意,如果在 XAML 中指定了内置的 ValidationRule 并且设置了匹配的绑定属性,则验证规则的两个实例将与控件关联,并且将应用这两个规则实例。

      <TextBox Name="xInput" Height="Auto" Width="60" Margin="0,5,0,0" Grid.Column="2" Grid.Row="1">
            <Binding Path="x" TargetNullValue="" ValidatesOnExceptions="True" UpdateSourceTrigger="PropertyChanged">
                <Binding.ValidationRules>
                    <ExceptionValidationRule/>
                </Binding.ValidationRules>
            </Binding>
        </TextBox>
图 4.5. 定义并应用了 ExceptionValidationRule 的两个实例。

验证处理过程如下。

  1. 触发验证。如果 UpdateSourceTrigger=PropertyChange,则在输入新数据时触发验证。如果 UpdateSourceTrigger=LoseFocus,则在控件失去焦点时触发验证。

  2. 应用任何具有 ValidationStep = RawProposedValueValidationRule

  3. 如果存在,输入的数据将通过适当的 IDataConverter 内部转换为所需的底层类型。如果文本转换为底层数据类型时发生错误,绑定引擎将抛出异常。如果 ExceptionValidationRule 已添加到控件的 ValidationRules 列表(无论是显式添加还是通过设置 ValidateOnExceptions=True),则将引发 Validation.Error 事件(类型为 System.Windows.Controls.Validation.ErrorEvent),并可能被用户代码捕获。

    ValidationError 包含对抛出异常的引用

  4. 应用任何具有 ValidationStep = ConvertedProposedValueValidationRule

  5. 底层值被设置为转换后的值。

  6. 应用任何具有 ValidationStep = UpdatedValueValidationRule。这包括调用视图模型 IDataErrorInfoDataValidationErrorRule。如果使用 ValidateOnDataError=True 触发 IDataErrorInfo 验证,它将在应用所有其他具有 ValidationStep = UpdatedValue 的规则之后调用。请注意,IDataErrorInfo 的调用顺序可以通过使用 DataValidationErrorRule 更改其在 ValidationRules 列表中的位置来操纵。

  7. 如果 NotifyOnSourceUpdate=True,则会触发 Binding.SourceUpdated 事件。

  8. 应用任何具有 ValidationStep = CommittedValueValidationRule

一旦发生 ValidationRule 错误,将不再应用其他 ValidationRule。如果存在验证错误,则

  • 绑定引擎创建 ValidationError 并将其添加到控件的 Validation.Errors 集合中。

    这里的行为在不同版本的 .NET 中有所不同。在 .NET4 之前,任何现有的 ValidationError 都会在新错误添加之前从 Validation.Errors 中删除。从 .NET 4 开始,新错误会在旧错误删除之前添加。

  • 向用户提供错误的视觉通知。如果为控件定义了自定义 ErrorTemplate,则应用它。如果没有用户定义的 ErrorTemplate,则使用默认值(控件周围的红色边框)。

如果加法器应用程序在 Visual Studio 中运行,则 Debug.WriteLine 调用的输出可以在“输出”窗口中看到。每个视图中最顶部的 TextBox 都有多个附加的 TraceValidationRules,其唯一目的是显示上述验证处理的进度。

请注意,调用 INotifyPropertyChanged.PropertyChangedINotifyDataErrorInfo.RaiseErrorChanged 会导致重新验证,因此有可能导致意外的递归。

显示验证错误

验证错误消息通常使用以下三种机制之一显示

  • 错误模板.
  • 内容呈现器.
  • 错误条.

ErrorTemplate

实现相邻错误消息的最常用方法可能是使用控件模板来定义当发生验证错误时控件(包括错误消息)应该是什么样子。控件模板由输入控件的 Validation.ErrorTemplate 属性指定,通常定义为应用程序/窗口/控件资源。

CalculatorViewWithErrorTemplate.xaml:

	<TextBox ... Validation.ErrorTemplate="{StaticResource ValidatedTextBoxTemplate}">

控件模板呈现为装饰。即,新指定的控件外观绘制在控件顶部的图层中。原始控件(例如 TextBox)只有在使用 AdornedElementPlaceholder 元素在控件模板中指定时才可见。

图 5. 使用 ErrorTemplate 显示错误。

AdornedElementPlaceholder 元素用于确定图层相对于控件的渲染位置。AdornedElementPlaceholder 仅适用于错误模板。对网络的调查表明,尝试在超出其原始目的的情况下使用它并不少见,这通常会导致问题。

错误模板仅在工具提示中显示错误消息(图 5)并不少见,但这要求用户具有一定的复杂性,特别是如果他们只是应用程序的临时用户。如果发生错误,红色的“星号”或任何其他符号都不会立即触发用户响应——“哦,我需要将鼠标悬停在这部分屏幕上才能获取错误消息”。它需要耐心,而一些用户,例如交易员,并不以表现出耐心而闻名。这可以说是一种非常糟糕的 UI 设计,不建议使用。

使用工具提示显示错误可能是一种节省屏幕空间的方法,因为它在没有错误并且表单上没有专门为其分配空间时才显示。

内容呈现器

CalculatorViewUsingContentPresenter.xaml:

	...
	<TextBox Name="yInput" ... TextBox>
	<ContentPresenter ... Content="{Binding ElementName=yInput, Path=(Validation.Errors).CurrentItem}" />
	...

如果屏幕空间不是问题,则将内容呈现器与每个输入控件关联是一种特别简单有效的显示错误消息的方法(图 6)。

图 6. 使用 ContentPresenter 显示系统验证错误

ContentPresenter 需要一个 DataTemplate,以便 WPF 知道如何渲染 System.Windows.Controls.ValidationError

CalculatorViewUsingContentPresenter.xaml:

	<UserControl.Resources>
		<DataTemplate DataType="{x:Type ValidationError}">
			<TextBlock FontStyle="Italic" Foreground="Red" HorizontalAlignment="Right" Margin="0,1" Text="{Binding Path=ErrorContent}"/>
		</DataTemplate>
	</UserControl.Resources>

将错误消息显示在输入控件旁边或下方(图 6)应被视为良好的 GUI 设计。

错误条

如果屏幕空间非常宝贵,在表单底部“栏”中显示错误消息可能是一种解决方案(图 7)。

此方法要求将验证错误存储在用户定义的验证错误集合中,错误显示绑定到集合中最新或最相关的错误。

这种方法的主要问题是,如果存在多个错误消息,通常很难决定应该显示哪个错误消息。加法器应用程序选择显示最后一个验证控件的错误列表中的最后一个错误。

可以通过重载 CurrentValidationError 属性来实现自定义显示算法。对于任何要实现的自定义算法来说,唯一硬性规则可能是,如果其中一个输入控件具有焦点并且有错误,则显示的错误消息应与该控件相关。

或者,可以在应用程序的单独部分中显示完整的错误列表。

7. 带错误栏的加法器。

以下显示了加法器应用程序采用的方法。它在 TextBox 中显示用户定义的验证错误集合的 CurrentValidationError 属性。

CalculatorView.xaml:

	<!-- Error Display -->
	<TextBox ... Text="{Binding Path=CurrentValidationError, Mode=OneWay}" />

设置验证

验证有多种方法,包括

本文仅关注前三种方法。

警告:不要在同一对象上同时实现 IDataErrorInfo 和 INotifyDataErrorInfo。不要使用与 IDataErrorInfo 或 INotifyDataErrorInfo 接口中任何方法同名的方法。作者在开发 Adder 应用程序时就犯了这样的错误,导致了一些非常难以理解的行为。

ValidationRule

ValidationRule 对象可用于实现并将验证规则附加到特定字段。

自定义验证规则派生自 ValidationRule 并实现 Validate 方法。Validate 方法返回一个 ValidationResult - 构造函数的第一个参数表示检查的字段是否有效。第二个参数包含“错误内容”,通常是错误消息。

下面 MandatoryRule 的代码取自 Adder 项目。Name 属性在相关的 XAML 中设置,用于构造错误消息。代码还使用 ValidationResult.ValidResult,它是一个常量,等同于 ValidationResult(true, null)

    public class MandatoryRule : ValidationRule
    {
        public string Name
        {
            get;
            set;
        }

        public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
        {
            if(String.IsNullOrEmpty((string)value))
            {
                if (Name.Length == 0)
                    Name = "Field";
                return new ValidationResult(false, Name + " is mandatory.");
            }
            return ValidationResult.ValidResult;
        }
	}

MandatoryRule 通过在控件的 XAML 的 Binding.ValidationRules 部分声明来附加到控件。请注意,Name 属性也在 XAML 中设置。

      <TextBox Height="Auto" Width="60" Margin="0,5,0,0" Grid.Column="1" Grid.Row="3" Validation.ErrorTemplate="{StaticResource ValidatedTextBoxTemplate}">
            <Binding Path="y" TargetNullValue="" ValidatesOnExceptions="True" NotifyOnValidationError="True" UpdateSourceTrigger="PropertyChanged">
                <Binding.ValidationRules>
                    <common:MandatoryRule Name="y"/>
                </Binding.ValidationRules>
            </Binding>
        </TextBox>

请注意

  • ValidationRules 在视图中定义。验证应该是可测试的——这意味着它应该在视图模型中声明,而不是视图中。ValidationRules 与 MVVM 不兼容。

  • 一个 ValidationRule 附加到单个可视组件。它并不真正适合跨控件验证。(例如,如果两个输入的总和必须小于某个固定值,则必须在两个输入控件上定义该规则)。

  • ValidationRules 提供了一种非常模块化的单控件验证方法——编写一个 ValidationRule 以使其可重用并不困难。

尽管存在限制,ValidationRules 仍是 WPF 验证的主力。然而,由于非 MVVM 应用程序将来可能转换为 MVVM,因此不完全推荐使用 ValidationRule

本文不涵盖使用 ValidationRule 的机制——它在其他地方有详细的文档,并且 Adder 应用程序包含该对象的多个实例。

IDataErrorInfo

IDataErrorInfo(如下)可用于替换 ValidationRules。索引器用于检索与特定字段关联的验证错误。通过返回空字符串表示没有错误。

public interface IDataErrorInfo
{
	string Error { get; }
	string this[string propertyName] { get; }
}

IDataErrorInfo 有一些怪癖

  • Error 属性应该返回一个“整个对象”验证错误。(例如,假期加上工作天数必须等于特定时期内的总天数)。然而,绑定引擎从不使用 Error 属性。很难理解微软为什么会将一个方法定义为其标准基础设施的一部分,然后又不使用它。然而,Error 属性确实提供了框架确定命令处理是否可以进行所需的功能类型。

  • IDataErrorInfo 不是“模块化的”。IDataErrorInfo 实现往往是大型 switch 语句,并且验证逻辑的重用需要额外的框架或编码约定。相比之下,可以构建一个 ValidationRule 对象库,例如 Adder 应用程序中的 MandatoryRuleNumberRangeRule,这些对象可以轻松重用。

  • 通常 WPF 会验证输入——当表单首次显示时,空字段不会被验证,以防止用户被一片红色淹没。(参见上文)。但 IDataErrorInfo 则不然。

IDataErrorInfoValidationRules 更适合 MVVM,因为验证是在视图模型中完成的,因此是可测试的。但是,有一个更好的替代方案。

INotifyDataErrorInfo

INotifyDataErrorInfo 是 Microsoft 定义的、用于用户定义验证错误容器的接口。支持 INotifyDataErrorInfo 的容器可以直接由 WPF 框架查询。

INotifyDataErrorInfo 从 Silverlight 导入到 WPF 中,它是异步验证基础设施支持的一部分。它在 .NET 4.5 中可用。

public interface INotifyDataErrorInfo
{
	bool HasErrors { get; }
	IEnumerable GetErrors(string propertyName);
	event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
}

请注意

  • 错误容器中错误的任何更改都应引发 ErrorsChanged 事件。
  • INotifyDataErrorInfo.GetErrors 返回的错误对象必须实现 string ToString()

INotifyDataErrorInfo 接口包含了作者在开发 Adder 应用程序过程中显而易见的许多经验教训。它与用户定义的错误容器接口有着惊人的相似之处。见下文。事实上,在它们更改之前,原始版本 IValidationErrorContainer 中使用的方法名称与 INotifyDataErrorInfo 冲突,不得不更改!出于实际原因,ErrorsChanged 事件由两个接口共享。

    public interface IValidationErrorContainer
    {
        bool AddError(ValidationError error, bool isWarning = false);
        bool RemoveError(string propertyName, string errorID);

        int ErrorCount { get; }
        IEnumerable GetPropertyErrors(string propertyName);
        event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    }

INotifyDataErrorInfo 存在与 IDataErrorInfo 相同的某些限制。例如,它不是特别模块化——实现往往由大型 switch 语句组成。然而,IDataErrorInfo 的实现将用户定义的错误容器视为额外的附带品,仅用于禁用命令控件和更新错误条。另一方面,INotifyDataErrorInfo 将用户定义的错误容器视为组织验证逻辑的主要结构。

为什么 ErrorsChanged 事件是必需的?错误条等可视组件需要用新错误进行更新。错误容器不应该了解这些组件,在这种情况下,通过订阅更新事件进行通知是显而易见的解决方案。

Adder 应用程序中基于 INotifyDataErrorInfo 的集合实现有点笨拙,因为它在实现了几乎相同的 IValidationErrorContainer 接口的容器之上实现了 INotifyDataErrorInfo 接口。通过直接在 ValidationErrorContainer 上实现 INotifyDataErrorInfo 接口,可以大大简化实现。出于教学和实际原因(请参阅上文“设置验证”),没有这样做。ValidationToolkit 库不应以任何方式被视为权威实现,因为它服务于两个主要目标:IDataErrorInfoINotifyDataErrorInfo。如果您对此感兴趣(当然您会感兴趣),请参阅 Microsoft Prism 中的验证 ErrorContainer

项目

描述

该项目在同一个窗口中实现了 4 个简单的加法器控件实例。每个加法器将两个数字相加并显示结果。每个都使用 MVVM 模式,并且每个都以略有不同的方式实现。这 4 个加法器可以描述为

  • 左上:使用 ValidationRule 完成验证。错误显示在输入控件下方的内容呈现器中。
  • 左下:使用 ValidationRule 完成验证。错误使用错误模板显示。
  • 右上:验证使用 IDataErrorInfo。用户定义的验证错误集合中的错误显示在控件底部的错误栏中。
  • 右下:验证使用 INotifyDataErrorInfo。用户定义的验证错误集合中的错误显示在控件底部的错误栏中。

8. 加法器应用程序。

加法器的输入是必填的,并且必须大于或等于 0。

每个加法器都使用 RelayCommand 对象将命令传递给控制器,并且每个 ICommand.CanCalculate 都只引用视图模型。

该项目包含实现错误容器和视图模型及视图基类的库代码。请注意,该库的主要目的是演示潜在的用法和设计,而不是可重用性。

ValidationRule

以下 ValidationRules 在 Adder 应用程序中定义。

  • 必填规则
  • 整数范围规则
  • 跟踪验证规则

ValidationRules 仅用于左侧的两个加法器。

 

TraceValidationRule 的唯一目的是在 Visual Studio 的“输出”窗口中跟踪验证规则的执行。

INotifyErrorDataInfo

该项目定义了一个自定义的 ValidationError 对象。右下方的加法器在错误容器上实现了 INotifyErrorDataInfo,如下所示。

AdderViewModel_INotifyDataErrorInfo.cs:

	// INotifyErrorDataInfo.
	public System.Collections.IEnumerable GetErrors(string propertyName)
	{
		return base.GetPropertyErrors(propertyName);
	}

	// INotifyErrorDataInfo.
	public bool HasErrors
	{
		get { return ErrorCount != 0; }
	}

	// Helper
	protected void RaiseErrorsChanged(string propertyName)
	{
		if (this.ErrorsChanged != null)
			this.ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
	}

验证发生在属性 setter 中的 ValidateProperty 方法中。参见下文。

	
AdderViewModel.cs:

	public Nullable<double> y
	{
		get { return y_; }
		set 
		{ 
			y_ = value;
			ValidateProperty("y");
			NotifyPropertyChanged("y");
			Sum = null;
		}
	}

应用程序特有的验证逻辑(如下)展示了 AddErrorRemoveError 方法的使用,以更新错误集合。ValidateProperty 从视图模型 setter 中调用。

AdderViewModel_INotifyDataErrorInfo.cs:

	public const string Constraint_Mandatory = "IsMandatory";
	public const string Constraint_MustBeNonNegative = "NonNegative";

	...

	void ValidateNonNegative(Nullable<double> x, string fieldName)
	{
		if (x.HasValue && x.Value < 0.0)
			AddError(new ValidationError(fieldName, Constraint_MustBeNonNegative, fieldName + ": must be non-negative"));
		else
			RemoveError(fieldName, Constraint_MustBeNonNegative);
	}

	void ValidateMandatory(Nullable<double> x, string fieldName)
	{
		if (!x.HasValue)
			AddError(new ValidationError(fieldName, Constraint_Mandatory, fieldName + ": is mandatory"));
		else
			RemoveError(fieldName, Constraint_Mandatory);
	}

	public override void ValidateProperty(string propertyName)
	{
		Tracer.LogValidation("INotifyDataErrorInfo.ValidateProperty called. Validating " + propertyName);
		switch (propertyName)
		{
			case "x":
				{
					ValidateNonNegative(x, "x");
					ValidateMandatory(x, "x");
				}
				break;

			case "y":
				{
					ValidateNonNegative(y, "y");
					ValidateMandatory(y, "y");
				}
				break;
		}
		if (String.IsNullOrEmpty(propertyName))
		{
			Tracer.LogValidation("No cross-property validation errors.");
		}
	}

单元测试

最后,该项目包含单元测试,用于测试 INotifyDataErrorInfo 加法器的服务和视图模型代码。

单元测试无法测试处理无效数字输入的代码(例如,当控件需要数字时输入“hello”)。视图模型的目的是将数据格式化,使其易于视图使用。Microsoft 建议更进一步:视图应始终绑定到与视图控件预期类型相同的数据。在加法器应用程序中,TextBoxs 期望字符串,因此控件将绑定到视图模型中的字符串值。字符串到数字的转换将在视图模型中的附加层中完成,该层也可以进行单元测试。

结论

MVVM 应用程序中的验证绝非易事。它有多种实现方式,但用户验证错误容器是所有 MVVM 方法的核心。

本文仅触及可能性的皮毛。其他感兴趣的主题包括异步验证、基于属性的验证、自定义控件、基于规则的验证、计算器字段(接受“45/360+3”等输入)和 Prism。

希望这个项目能对需要做出验证决策的其他人有所帮助。

历史

暂无更新。

© . All rights reserved.