MVVM 数据验证





5.00/5 (5投票s)
本文介绍了一个用于 MVVM WPF 控件的数据验证框架。
引言
本文介绍了一个可在 MVVM 环境中用于数据验证的基类。代码示例扩展了上一篇文章《WPF DataGrid 控件中的动态列》,并为描述的验证逻辑扩展了“小型应用程序框架”。
背景
本文介绍了 GUI 层下方的数据处理,用于验证用户输入。验证由数据项属性上的验证属性定义。每个验证规则都实现为一个分配给属性的属性。在我之前的文章中,模型数据直接绑定到 GUI 控件。在此解决方案中,我将模型数据封装在一个 DisplayItem
类中,该类是数据项的视图模型。通常,显示项具有与数据项相同的属性,并且将在这些属性上执行验证。
数据视图模型
下一个类图显示了 UserDisplayItem
类作为 UserRow
模型的视图模型。它显示了最有趣的类及其关系。
- 应用程序层,
DataGridRow
:网格控件中的行,其中包含用户显示项视图模型作为其DataContext
。 ViewModel
层,UserDisplayItem
:用户数据表示,具有第一个和最后一个姓名属性,并为其分配了验证属性。DataModel
层,UserRow
:包含用户数据的持久化模型类。SmallApplicationFramework
,DataViewModelBase
:视图模型基类,包含数据对象(UserRow
实例),其数据正在显示。此类包含验证逻辑,通过IDataErrorInfo
接口的string Error { get; }
和this[string propertyName] { get; }
属性通知验证错误。
那么,为什么要在控件和数据项之间放置这个视图模型呢?相同的验证属性可以应用于模型数据属性。确实如此,但此解决方案允许更大的灵活性。某些数据验证场景
- 数据网格控件:可以直接在数据项上进行验证。必须设置或更新数据项属性才能使验证生效,这是数据网格控件的典型情况。
- 编辑表单:这是一个模态对话框,在按“确定”按钮关闭对话框时更新数据。在这种情况下,数据暂时存储在视图模型中,最后应用于数据对象。在视图模型的属性上执行验证。
- 显示复杂或计算数据。在这种情况下,数据字段不是数据项的成员,而是从其他属性派生而来。由于数据不是模型数据的一部分,因此必须由视图模型类进行显示和验证。
第三种场景可以应用于第一种和第二种场景。因此,所有数据都通过视图模型类进行封装。这可以实现清晰的架构。清晰架构的关键问题之一是为所有相似的情况提供相同的解决方案。
错误通知
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
是一个基类,因此是小型应用程序框架的一部分。
它是一个泛型类,包含一个模型数据实例。下一个类图显示了 UserDisplayItem
和 RoleDisplayItem
类如何封装 UserRow
和 RoleRow
数据项。数据实例可以通过 DataObject
属性访问。
数据验证
数据验证由 WPF 框架触发。每个控件都绑定到一个视图模型属性,例如:
<DataGridTextColumn Header="Name"
Binding="{Binding Name, ValidatesOnDataErrors=True,
UpdateSourceTrigger=LostFocus}"/>
ValidatesOnDataErrors
标志启用数据验证,UpdateSourceTrigger
类型定义何时进行数据验证。
数据验证在属性中实现:
public string this[string propertyName]
{
get
{
// Data validation implementation
}
}
数据验证步骤是:
- 检查属性是否必须进行验证。业务逻辑(如果需要临时)可以取消属性验证。这可以通过覆盖
PropertyMustBeValidated(string propertyName)
方法来控制。 - 请求视图模型实现,以确定给定属性是否有效。这可以通过覆盖
PropertyIsValid(string propertyName, out string errorMessage)
方法来实现。 - 使用反射获取给定属性的所有验证属性。为了提高性能,属性会被缓存。
- 迭代验证器并收集错误结果。在字典中跟踪错误状态。该字典包含所有已验证属性的可能错误。视图模型的整体有效性取决于字典是否包含错误:没有错误 -> 视图模型有效。
自定义验证
可以通过创建派生自 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
时)检索数据。
数据检索步骤是:
- 通知视图模型数据检索开始。这允许视图模型检索非属性值,例如计算值。通过覆盖
RetrievingModelData()
方法来实现。 - 使用反射,请求数据实例的所有可读属性。迭代属性,并在视图模型中请求同名属性。如果视图模型属性可以设置,则写入数据字段值。
- 通知视图模型数据检索已完成,以便进行其他视图模型任务。通过覆盖
RetrievedModelData()
方法来实现。
数据更新
当设置视图模型属性时,模型数据将被验证,并且,如果验证没有发现错误,默认情况下会自动更新。可以通过将标志 UpdateModelOnPropertyChange
设置为 false
来禁用自动更新。
模型更新步骤是:
- 通知视图模型更新开始。可以通过覆盖
UpdatingModelData()
方法来执行附加逻辑或模型更新。 - 通过迭代视图模型的所有可读属性来更新模型,并为每个具有相同名称且可写入的属性更新模型。
- 最后,通知视图模型更新已完成。视图模型可以覆盖
UpdatedModelData()
方法进行附加逻辑。
结论
DataViewModelBase
类可用于所有类型的数据编辑情况。不仅限于数据网格情况,如示例代码所示,还可以用于编辑表单,我将在未来的文章中介绍。
本文是我下一篇关于命令处理的文章的前奏,我将在其中介绍一个处理用户命令控件(按钮、(上下文)菜单项等)的框架。
关注点
在“视图模型”部分,我试图解释为什么有必要在控件和模型数据之间设置视图模型。我的理由是,后续对模型数据的更改应该被缓冲,以便在用户按下“确定”按钮时一次性应用。我认为这个理由是完全合理的,但 DataSet
框架(我真的很喜欢 DataSet
)提供了一种解决办法。
DataSet
包含原始数据行的副本。可以通过调用行的 RejectChanges()
方法来撤销每一行修改。也可以通过调用 DataSet.RejectChanges()
来重置整个数据集的修改。我建议您了解此功能,因为它在使用数据集时可能非常有用。