介绍
想象一下场景...用户正在填写您精心编写的表单,并在应该输入电子邮件地址的位置输入他们的姓名。您需要检测到这一点,并以清晰的方式显示问题。
输入验证是一个很大的领域,有很多方法可以做到这一点。最简单、最吸引人的是在你的属性的 setter 中抛出一个异常,如下所示:
private string _name;
public string Name
{
get { return this._name; }
set
{
if (someConditionIsFalse)
throw new ValidationException("Message");
this._name = value;
}
当绑定设置此属性时,它会注意到是否引发异常,并相应地更新控件的验证状态。
但是,这最终是一个彻头彻尾的坏主意****。这意味着您的属性只能在设置时进行验证(例如,当用户单击"提交"时,您无法浏览并验证整个表单),并且它会导致具有大量重复逻辑的冗余属性设置器。可怕。
C#还定义了两个接口,WPF都知道这两个接口:IDataErrorInfo和INotifyDataErrorInfo。这两者都为 ViewModel 提供了一种方法,通过事件和 PropertyChanged 通知告诉 View 一个或多个属性具有一个或多个验证错误。其中,INotifyDataErrorInfo较新,更易于使用,并允许异步验证。
但是,驱动INotifyDataErrorInfo仍然有点不直观:它允许您广播一个或多个属性有错误的事实,但没有提供运行验证的简单方法,并且要求您记录哪些错误与哪些属性相关联。
ValidatingModelBase旨在解决这个问题,并提供一种直观简便的方法来运行和报告您的验证。
ValidatingModelBase
ValidatingModelBase 派生自PropertyChangedBase,并被 Screen 继承。它建立在 PropertyChangeBase 的功能之上,该功能可以注意到属性何时发生更改以运行和报告您的验证。
IModelValidator
有许多方法可以运行验证,并且有许多好的库可以帮助您。Stylet 不打算提供另一个验证库,因此 Stylet 允许您提供自己的验证库供 ValidatingModelBase 使用。
这体现在 ValidatingModelBase 的属性validator中,该属性是 IModelValidator.目的是您编写自己的IModelValidator 实现,该实现封装了您的首选验证库(我将在后面介绍一些如何执行此操作的示例),以便 ValidatingModelBase 可以使用它。
此接口有两个重要方法:
Task<IEnumerable<string>> ValidatePropertyAsync(string propertyName);
Task<Dictionary<string, IEnumerable<string>>> ValidateAllPropertiesAsync();
当 ValidatingModelBase 想要按名称验证单个属性时,第一个属性由 ValidatingModelBase 调用,并返回一个验证错误数组。当您要求 ValidatingModelBase 执行完整验证时,将调用第二个,并返回 的字典:property name => array of validation errors
这些方法是异步的,这一事实允许您利用INotifyDataErrorInfo的异步验证功能,并根据需要在某些外部服务上运行验证。但是,预计此接口的大多数实现将仅返回已完成的任务。
还有第三种方法:
void Initialize(object subject);
这是由 ValidatingModelBase 在首次设置其验证时调用的,并且它传入了自己的实例。这允许 IModelValidator实现 专门用于验证 ValidatingModelBase 的特定实例。当我们将事物与StyletIoC联系起来时,这具有更大的相关性,稍后将看到。
此接口还有一个范型版本,IModelValidator
运行验证
首先,您必须记住将IModelValidator实现传递给ValidatingModelBase 。可以通过设置validator属性或调用适当的构造函数来执行此操作:
public class MyViewModel : ValidatingModelBase
{
public MyViewModel(IModelValidator validator) : base(validator)
{
}
}
默认情况下,每当属性发生更改时,ValidatingModelBase 都会运行该属性的验证(前提是您调用SetAndNotify、使用NotifyOfPropertyChange 或使用PropertyChanged.Fody使得 PropertyChangedBase 中定义的机制引发PropertyChanged通知)。然后,它将使用接口INotifyDataErrorInfo中定义的机制报告该属性的验证状态的任何更改。它还将更改HasErrors属性的值。
如果要禁用此自动验证行为,请将AutoValidate属性设置为 。false
如果需要,可以通过调用ValidateProperty("PropertyName") 或 ValidateProperty(() => this.PropertyName)来手动运行单个属性的验证。如果您的验证是异步的,则还有这些的异步版本 - 稍后会详细介绍。如果要在设置单个属性时对其进行验证,可以执行以下操作:
private string _name
public string Name
{
get { return this._name; }
set
{
SetAndNotify(ref this._name, value);
ValidateProperty();
}
}
此外,还可以通过调用 Validate()对所有属性运行验证。
如果希望在验证状态更改(任何属性的验证错误更改)时运行某些自定义代码,请重写 OnValidationStateChanged()。
了解和使用IModelValidator
在接下来的几节中,我将带您完成一个使用FluentValidation库实现验证的示例。
FluentValidation的工作原理是创建一个新类,该类实现IValidator
public class UserViewModel : Screen
{
private string _name;
public string Name
{
get { return this._name; }
set { SetAndNotify(ref this._name, value); }
}
}
public class UserViewModelValidator : AbstractValidator<UserViewModel>
{
public UserViewModelValidator()
{
RuleFor(x => x.Name).NotEmpty();
}
}
如果我们直接使用UserViewModelValidator(没有VerifiedModelBase的帮助),我们会做这样的事情:
public UserViewModel(UserViewModelValidator validator)
{
this.Validator = validator;
}
// ...
this.Validator.Validate(this);
但是,使用 ValidatingModelBase 的要点是,它将自动运行和报告验证。如前所述,我们需要包装UserViewModelValidator 以确保VerifiedModelBase知道如何与之交互的方式结束我们的工作。
最简单的方法是编写一个适配器,该适配器可以采用任何实现IValidator
(即您编写的任何自定义验证程序),并以 ValidatingModelBase 理解的方式公开它。 类层次结构如下:
- ValidatingModelBase.Validator 是一个 IModelValidator
- UserViewModelValidator 是一个 IValidator
- 我们将编写一个适配器,FluentValidationAdapter,这是一个IModelValidator。
- FluentValidationAdapter将接受IValidator,并将其包装起来,以便可以通过IModelValidator访问它
- 因此,FluentValidationAdapter将采用UserViewModelValidator,并将其公开为IModelValidator;
到目前为止,看起来需要做很多工作,但我们可以让我们的IoC容器完成大部分繁重的工作,正如我们很快就会看到的那样。
// Define the adapter
public class FluentValidationAdapter<T> : IModelValidator<T>
{
public FluentValidationAdapter(IValidator<T> validator)
{
// Store the validator
}
// Implement all IModelValidator methods, using the stored validator
}
// This implements IValidator<UserViewModel>
public class UserViewModelValidator : AbtractValidator<UserViewModel>
{
public UserViewModelValidator()
{
// Set up validation rules
}``
}
public class UserViewModel
{
public UserViewModel(IModelValidator<UserViewModel> validator) : base(validator)
{
// ...
}
}
然后手动实例化一个新的UserViewModel:
var validator = new UserViewModelValidator();
var validatorAdapter = new FluentValidationAdapter<UserViewModel>(validator);
var viewModel = new UserViewModel(validatorAdapter);
使用预制的IModelValidator
我已经编写了以下IModelValidator实现:FluentValidationAdapter,欢迎您使用它们:
public class FluentModelValidator<T> : IModelValidator<T>
{
private readonly IValidator<T> validator;
private T subject;
public FluentModelValidator(IValidator<T> validator)
{
this.validator = validator;
}
public void Initialize(object subject)
{
this.subject = (T)subject;
}
public async Task<IEnumerable<string>> ValidatePropertyAsync(string propertyName)
{
// If someone's calling us synchronously, and ValidationAsync does not complete synchronously,
// we'll deadlock unless we continue on another thread.
return (await _validator.ValidateAsync(_subject, x => x.IncludeProperties(propertyName)).ConfigureAwait(false))
.Errors.Select(x => x.ErrorMessage);
}
public async Task<Dictionary<string, IEnumerable<string>>> ValidateAllPropertiesAsync()
{
// If someone's calling us synchronously, and ValidationAsync does not complete synchronously,
// we'll deadlock unless we continue on another thread.
return (await this.validator.ValidateAsync(this.subject).ConfigureAwait(false))
.Errors.GroupBy(x => x.PropertyName)
.ToDictionary(x => x.Key, x => x.Select(failure => failure.ErrorMessage));
}
}
实现 IModelValidator(同步)
编写IModelValidator的实现在概念上很简单,但有一些陷阱。与前面一样,本节将假设我们正在为FluentValidation库实现一个适配器,尽管您可以应用此处获得的知识为几乎任何库编写适配器。
现在,让我们假设我们所有的验证都是同步的。对于返回 Tasks 的方法,我们只返回已完成的任务。
首先,我们将出于上一节中讨论的原因实现IModelValidator
public class FluentValidationAdapter : IModelValidator<T>
{
private readonly IValidator<T> validator;
public FluentValidationAdapter(IValidator<T> validator)
{
this.validator = validator;
}
}
请记住,ValidatingModelBase 需要一个专门用于验证特定 ViewModel 实例的 IModelValidator,因为它增加了更多灵活性。 这意味着 ValidationModelBase 可以调用 ValidateAllPropertiesAsync(),并且正确的 ViewModel 实例将被验证。 然而,这里我们有一个先有鸡还是先有蛋的情况——为了专门化适配器,ViewModel 必须存在。 但是,在验证适配器之前无法实例化 ViewModel,因为 ViewModel 需要适配器作为构造函数参数。
解决方案是 Initialize(object subject) 方法。 ValidatingModelBase 在传递一个新的适配器时调用它,它将自己作为参数传递。 然后适配器将存储此实例,并在运行验证时使用它。 像这样:
public class FluentValidationAdapter : IModelValidator<T>
{
private readonly IValidator<T> validator;
private T subject;
public FluentValidationAdapter(IValidator<T> validator)
{
this.validator = validator;
}
public void Initialize(object subject)
{
this.subject = (T)subject;
}
}
现在,实现 ValidatePropertyAsync。 这应该验证单个属性,并返回验证错误列表,如果没有则返回 null/emptyarray。 使用 FluentValidation 执行同步验证,可能如下所示:
public Task<IEnumerable<string>> ValidatePropertyAsync(string propertyName)
{
var errors = this.validator.Validate(this.subject, propertyName).Errors.Select(x => x.ErrorMessage);
return Task.FromResult(errors);
}
同样,ValidateAllPropertiesAsync 方法验证所有属性,并返回 { propertyName => array of validation errors } 的字典。 如果属性没有任何验证错误,您可以从 Dictionary 中完全忽略它,或者将其值设置为 null/emptyarray。
public Task<Dictionary<string, IEnumerable<string>>> ValidateAllPropertiesAsync()
{
var errors = this.validator.Validate(this.subject).Errors.GroupBy(x => x.PropertyName).ToDictionary(x => x.Key, x => x.Select(failure => failure.ErrorMessage));
return Task.FromResult(errors);
}