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

MVVM 数据验证

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2015年11月11日

CPOL

9分钟阅读

viewsIcon

20724

downloadIcon

702

本文介绍了一个用于 MVVM WPF 控件的数据验证框架。

引言

本文介绍了一个可在 MVVM 环境中用于数据验证的基类。代码示例扩展了上一篇文章《WPF DataGrid 控件中的动态列》,并为描述的验证逻辑扩展了“小型应用程序框架”。

背景

本文介绍了 GUI 层下方的数据处理,用于验证用户输入。验证由数据项属性上的验证属性定义。每个验证规则都实现为一个分配给属性的属性。在我之前的文章中,模型数据直接绑定到 GUI 控件。在此解决方案中,我将模型数据封装在一个 DisplayItem 类中,该类是数据项的视图模型。通常,显示项具有与数据项相同的属性,并且将在这些属性上执行验证。

数据视图模型

下一个类图显示了 UserDisplayItem 类作为 UserRow 模型的视图模型。它显示了最有趣的类及其关系。

  • 应用程序层,DataGridRow:网格控件中的行,其中包含用户显示项视图模型作为其 DataContext
  • ViewModel 层,UserDisplayItem:用户数据表示,具有第一个和最后一个姓名属性,并为其分配了验证属性。
  • DataModel 层,UserRow:包含用户数据的持久化模型类。
  • SmallApplicationFrameworkDataViewModelBase:视图模型基类,包含数据对象(UserRow 实例),其数据正在显示。此类包含验证逻辑,通过 IDataErrorInfo 接口的 string Error { get; }this[string propertyName] { get; } 属性通知验证错误。

那么,为什么要在控件和数据项之间放置这个视图模型呢?相同的验证属性可以应用于模型数据属性。确实如此,但此解决方案允许更大的灵活性。某些数据验证场景

  1. 数据网格控件:可以直接在数据项上进行验证。必须设置或更新数据项属性才能使验证生效,这是数据网格控件的典型情况。
  2. 编辑表单:这是一个模态对话框,在按“确定”按钮关闭对话框时更新数据。在这种情况下,数据暂时存储在视图模型中,最后应用于数据对象。在视图模型的属性上执行验证。
  3. 显示复杂或计算数据。在这种情况下,数据字段不是数据项的成员,而是从其他属性派生而来。由于数据不是模型数据的一部分,因此必须由视图模型类进行显示和验证。

第三种场景可以应用于第一种和第二种场景。因此,所有数据都通过视图模型类进行封装。这可以实现清晰的架构。清晰架构的关键问题之一是为所有相似的情况提供相同的解决方案。

错误通知

DisplayItem 类使用 IDataErrorInfo 接口进行错误通知。该接口有两个属性。

// Gets an error message indicating what is wrong with this object.
string Error { get; }

// Gets the error message for the property with the given name.
string this[string columnName] { get; }

数据验证使用第二个属性。WPF 框架会自动为绑定到 GUI 控件的所有属性以及在绑定中设置了 ValidatesOnDataErrors=true 标志的属性调用此属性。

当属性包含无效数据时,会返回一个错误文本。GUI 可以通过多种方式显示错误 string。在示例代码中,错误显示为工具提示,控件显示为红色边框。

数据验证

数据验证定义是通过将验证属性分配给视图模型的属性来完成的。

[Required(ErrorMessage = "Role name must be given")]
[UniqueRoleName(ErrorMessage = "Role name must be unique")]
public string Name { get; set; }

“.NET Framework 提供了多种验证属性,例如上面的 'Required' 属性。这些属性位于 System.ComponentModel.DataAnnotations 程序集中,在同一命名空间中。UniqueRoleName 是一个自定义属性,用于检查给定的角色名是否已存在。”

此外,视图模型也可以进行验证,而无需使用属性。验证框架还会调用一个虚拟方法,该方法可用于属性验证。

数据检索和更新

数据检索将数据从模型复制到视图模型。数据更新是用视图模型中的值更新模型数据。DataViewModelBase 类提供了自动化这些过程的功能。

数据检索过程在模型数据分配给视图模型的 DataObject 属性时完成。该功能类似于 AutoMapper,它扫描模型以查找其属性,并尝试在视图模型上找到匹配的属性。当可以从模型获取数据并可以设置到视图模型时,数据将被复制。

数据更新过程将数据从视图模型复制到模型。UpdateModelOnPropertyChange 标志控制是在每次属性值更改时更新模型,还是不更新。当数据在对话框中显示时,自动更新可能不理想,因为在这种情况下,数据应该在对话框被接受并关闭时更新。

其他虚拟方法允许进行专门的逻辑、数据检索或更新。可以覆盖 virtual 方法以在检索或更新数据时添加附加逻辑。这些方法是:

protected virtual void RetrievingModelData() { }
protected virtual void RetrievedModelData() { }
protected virtual void UpdatingModelData() { }
protected virtual void UpdatedModelData() { }

Using the Code

最初的想法来自 Cyle Witruk 的这篇文章。本文介绍了如何使用验证属性进行错误检查。

应用程序

该应用程序有两个视图,其中包含用户和角色的网格控件。每个视图都有自己的视图模型,包含视图逻辑。视图模型包含一个可观察的集合,其中包含显示项。该集合在应用程序启动时(主窗口加载事件)填充,并在插入或删除数据时更新。

示例项目在两个网格控件中都有验证。用户网格控件要求填写条目的第一个和最后一个姓名。角色网格控件也要求提供角色名。此外,角色名在角色表中必须是唯一的。

实例化 DataViewModelBase

DataViewModelBase 是一个基类,因此是小型应用程序框架的一部分。

它是一个泛型类,包含一个模型数据实例。下一个类图显示了 UserDisplayItemRoleDisplayItem 类如何封装 UserRowRoleRow 数据项。数据实例可以通过 DataObject 属性访问。

数据验证

数据验证由 WPF 框架触发。每个控件都绑定到一个视图模型属性,例如:

<DataGridTextColumn Header="Name"
                    Binding="{Binding Name, ValidatesOnDataErrors=True, 
                    UpdateSourceTrigger=LostFocus}"/>

ValidatesOnDataErrors 标志启用数据验证,UpdateSourceTrigger 类型定义何时进行数据验证。

数据验证在属性中实现:

public string this[string propertyName]
{
    get
    {
        // Data validation implementation
    }
}

数据验证步骤是:

  1. 检查属性是否必须进行验证。业务逻辑(如果需要临时)可以取消属性验证。这可以通过覆盖 PropertyMustBeValidated(string propertyName) 方法来控制。
  2. 请求视图模型实现,以确定给定属性是否有效。这可以通过覆盖 PropertyIsValid(string propertyName, out string errorMessage) 方法来实现。
  3. 使用反射获取给定属性的所有验证属性。为了提高性能,属性会被缓存。
  4. 迭代验证器并收集错误结果。在字典中跟踪错误状态。该字典包含所有已验证属性的可能错误。视图模型的整体有效性取决于字典是否包含错误:没有错误 -> 视图模型有效。

自定义验证

可以通过创建派生自 ValidationAttribute 的新类来实现自定义验证。逻辑放置在 IsValid 方法中。

public class UniqueRoleNameAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        ValidationResult validationResult = ValidationResult.Success;
        var roleItem = validationContext.ObjectInstance as RoleDisplayItem;
        if (roleItem != null)
        {
            if (RoleBusinessLogic.IsRoleNameUnique(value as string, roleItem.DataObject))
            {
                validationResult = new ValidationResult(this.ErrorMessage);
            }
        }

        return validationResult;
    }
}

前面的代码示例显示了角色名的唯一性验证。用户输入的角色名包含在 value 参数中。验证上下文包含对 RoleDisplayItem 的引用。ValidationResult 类包含验证错误。如果验证正确,则验证结果为 null 值,由 static ValidationResult.Success 属性定义。

数据检索

数据检索机制将字段值从数据项复制到视图模型。当数据项分配给视图模型时(当设置 DataObject 时)检索数据。

数据检索步骤是:

  1. 通知视图模型数据检索开始。这允许视图模型检索非属性值,例如计算值。通过覆盖 RetrievingModelData() 方法来实现。
  2. 使用反射,请求数据实例的所有可读属性。迭代属性,并在视图模型中请求同名属性。如果视图模型属性可以设置,则写入数据字段值。
  3. 通知视图模型数据检索已完成,以便进行其他视图模型任务。通过覆盖 RetrievedModelData() 方法来实现。

数据更新

当设置视图模型属性时,模型数据将被验证,并且,如果验证没有发现错误,默认情况下会自动更新。可以通过将标志 UpdateModelOnPropertyChange 设置为 false 来禁用自动更新。

模型更新步骤是:

  1. 通知视图模型更新开始。可以通过覆盖 UpdatingModelData() 方法来执行附加逻辑或模型更新。
  2. 通过迭代视图模型的所有可读属性来更新模型,并为每个具有相同名称且可写入的属性更新模型。
  3. 最后,通知视图模型更新已完成。视图模型可以覆盖 UpdatedModelData() 方法进行附加逻辑。

结论

DataViewModelBase 类可用于所有类型的数据编辑情况。不仅限于数据网格情况,如示例代码所示,还可以用于编辑表单,我将在未来的文章中介绍。

本文是我下一篇关于命令处理的文章的前奏,我将在其中介绍一个处理用户命令控件(按钮、(上下文)菜单项等)的框架。

关注点

在“视图模型”部分,我试图解释为什么有必要在控件和模型数据之间设置视图模型。我的理由是,后续对模型数据的更改应该被缓冲,以便在用户按下“确定”按钮时一次性应用。我认为这个理由是完全合理的,但 DataSet 框架(我真的很喜欢 DataSet)提供了一种解决办法。

DataSet 包含原始数据行的副本。可以通过调用行的 RejectChanges() 方法来撤销每一行修改。也可以通过调用 DataSet.RejectChanges() 来重置整个数据集的修改。我建议您了解此功能,因为它在使用数据集时可能非常有用。

© . All rights reserved.