介绍
想象一下这样的场景……用户正在填写一份你煞费苦心编写的表格,他们在本应输入电子邮件地址的地方输入了自己的姓名。您需要检测到这一点,并以清晰的方式显示问题。
输入验证是一个很大的领域,有很多方法可以解决。最简单和最吸引人的是在您的属性的 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 派生自PropertyChangedBase,并由 Screen 继承。它建立在 PropertyChangeBase 的通知能力之上,当属性发生变化时运行并报告您的验证。
模型验证器
有很多方法可以运行验证,还有很多好的库可以帮助你。Stylet 无意提供另一个验证库,因此 Stylet 允许您提供自己的验证库以供 ValidatingModelBase 使用。
这体现在 ValidatingModelBase 的validator
属性中,它是一个IModelValidator
. 目的是您编写自己的 实现IModelValidator
,它包装您首选的验证库(我将在稍后介绍如何执行此操作的一些示例),以便 ValidatingModelBase 可以使用它。
这个接口有两个重要的方法:
Task<IEnumerable<string>> ValidatePropertyAsync(string propertyName);
Task<Dictionary<string, IEnumerable<string>>> ValidateAllPropertiesAsync();
当 ValidatingModelBase 要按名称验证单个属性时,第一个由 ValidatingModelBase 调用,并返回验证错误数组。当您要求它进行完整验证时,第二个由 ValidatingModelBase 调用,并返回一个 Dictionary of property name => array of validation errors
.
这些方法是异步的这一事实使您可以利用 的INotifyDataErrorInfo
异步验证功能,并根据需要在某些外部服务上运行验证。但是,预计此接口的大多数实现只会返回一个已完成的任务。
还有第三种方法:
void Initialize(object subject);
这是在第一次设置验证时由 ValidatingModelBase 调用的,并且它传入了它自己的一个实例。这允许 的实现IModelValidator
专门用于验证 ValidatingModelBase 的特定实例。当我们将事物绑定到 StyletIoC 中时,这更有意义,稍后会看到。
此接口还有一个通用版本 ,IModelValidator<T>
它只是扩展IModelValidator
,并没有添加任何额外内容。当 IoC 包含出现在图片中时,这再次很有用 - 稍后会详细介绍。
运行验证
首先,您必须记住将您的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<T>
(您通常通过扩展来执行此操作AbstractValidator<T>
,并且可以验证特定类型的模型T
)。然后您创建一个新实例,并使用它来运行您的验证。因此,例如,如果您有一个UserViewModel
,您将定义一个UserViewModelValidator
extendsAbstractValidator<UserViewModel
并因此实现IValidator<UserViewModel>
,如下所示:
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
(没有 ValidatingModelBase 的帮助),我们会做类似的事情:
public UserViewModel(UserViewModelValidator validator)
{
this.Validator = validator;
}
// ...
this.Validator.Validate(this);
但是,使用 ValidatingModelBase 的要点在于它将自动运行和报告验证。如前所述,我们需要UserViewModelValidator
以 ValidatingModelBase 知道如何与之交互的方式结束我们的。
最简单的方法是编写一个适配器,它可以采用任何实现IValidator<T>
(即您编写的任何自定义验证器),并以 ValidatingModelBase 理解的方式公开它。万一你迷路了,我将再次遍历预期的类层次结构:
ValidatingModelBase.Validator
是一个 IModelValidator- UserViewModelValidator 是一个 IValidator
- 我们将编写一个适配器 FluentValidationAdapter,它是一个 IModelValidator
- FluentValidationAdapter 将接受一个 IValidator,并将其包装起来,以便可以通过 IModelValidator 访问它
- 因此,FluentValidationAdapter 将采用 UserViewModelValidator,并将其公开为 IModelValidator;
到目前为止有意义吗?这听起来工作量很大,但我们可以让 IoC 容器完成大部分繁重的工作,我们很快就会看到。
现在,这在实践中会是什么样子?首先,还记得我说过它IModelValidator<T>
被定义为一个只实现了的接口IModelValidator
吗?我现在还不打算告诉你为什么,但请记住,它们基本上是同义词。
// 定义适配器
public class FluentValidationAdapter<T> : IModelValidator<T>
{
public FluentValidationAdapter(IValidator<T> validator)
{
// 存储验证器
}
// 使用存储的验证器实现所有 IModelValidator 方法
}
// 这实现了 IValidator<UserViewModel>
public class UserViewModelValidator : AbtractValidator<UserViewModel>
{
public UserViewModelValidator()
{
// 设置验证规则
}
}
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);
然而,我们可以配置 IoC 容器来为我们做这件事。这假定您使用的是 StyletIoC,尽管其他容器也可以进行类似配置。
在您的引导程序的覆盖中,首先告诉 StyletIoC在您请求 an 时ConfigureIoC
返回 a ,方法是:FluentValidationAdapter<T>``IModelValidator<T>
builder.Bind(typeof(IModelValidator<>)).To(typeof(FluentValidationAdapter<>));
因此,每当 StyletIoC 创建一个新UserViewModel
的IModelValidator<UserViewModel>
. 它知道它被告知如何创建一个IModelValidator<T>
- 通过实例化一个新的FluentValidationAdapter<T>
. 所以它会尝试创建一个新的FluentValidationAdapter<UserViewModel>
,发现_需要_一个新的IValidator<UserViewModel>
,然后因为找不到而失败。
因此,我们需要告诉 StyletIoC 如何创建一个新的IValidator<UserViewModel>
. 我们_可以_通过以下方式长期做到这一点:
// The long way
builder.Bind<IValidator<UserViewModel>>().To<UserViewModelValidator>();
但是,如果您有很多验证器,则需要多行配置。最好IValidator<T>
通过以下方式告诉 StyletIoC 发现所有实现并自行绑定它们:
// The short way
builder.Bind(typeof(IValidator<>)).ToAllImplementations();
我们开始了!当 StyletIoC 尝试创建一个新的 时FluentValidationAdapter<UserViewModel>
,它会发现它需要一个IValidator<UserViewModel>
,并将实例化一个新的UserViewModelValidator
.
现在你可以明白为什么我们在这里使用IModelValidator<T>
instead 了IModelValidator
。如果UserViewModel
需要一个IModelValidator
,StyletIoC 将无法计算出它应该创建一个FluentValidationAdapter<UserViewModel>
,而不是一个FluentValidationAdapter<LogInViewModel>
. 通过向 中添加类型信息IModelValidator
,我们为 IoC 容器提供了足够的信息以供使用。
使用预制的 IModelValidator
我编写了以下 IModelValidator 实现,欢迎您使用:
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)
{
// 如果有人同步调用我们,而 ValidationAsync 没有同步完成,我们将死锁,除非我们继续另一个线程。
return (await this.validator.ValidateAsync(this.subject, strategy => strategy.IncludeProperties(propertyName)).ConfigureAwait(false))
.Errors.Select(x => x.ErrorMessage);
}
public async Task<Dictionary<string, IEnumerable<string>>> ValidateAllPropertiesAsync()
{
// 如果有人同步调用我们,而 ValidationAsync 没有同步完成,我们将死锁,除非我们继续另一个线程。
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 库实现适配器,尽管您可以应用此处获得的知识为几乎任何库编写适配器。
现在,让我们假设我们所有的验证都是同步的。对于返回任务的方法,我们将只返回一个已完成的任务。简单的。
首先,我们将IModelValidator<T>
根据上一节中讨论的原因实施。它还需要接受一个IValidator<T>
, 作为构造函数参数,如下所示:
public class FluentValidationAdapter : IModelValidator<T>
{
private readonly IValidator<T> validator;
public FluentValidationAdapter(IValidator<T> validator)
{
this.validator = validator;
}
}
请记住,ValidatingModelBase
需要一个IModelValidator
专门用于验证特定 ViewModel 实例的方法,因为它增加了更多的灵活性。这意味着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
方法验证所有属性,并返回一个 Dictionary of { propertyName => array of validation errors }
。如果属性没有任何验证错误,您可以从字典中完全省略它,或者将其值设置为 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);
}
将所有这些放在一起,您就拥有了适配器!
实施 IModelValidator(异步)
实现异步验证(对于支持它的库来说有点棘手)。
首先,请记住它ValidatingModelBase
同时具有一组同步方法 ( Validate
, ValidateProperty
) 和异步方法 ( ValidateAsync
, ValidatePropertyAsync
)。在幕后,同步版本调用异步版本,但阻塞线程直到异步操作完成(使用Task.Wait()
)。
现在,如果您经常使用任务,这应该敲响警钟。你看,当你await DoSomethingAsync(); DoSomethingElse();
说“捕获当前线程 [ * ]。当*DoSomethingAsync()
异步操作完成时,我希望你向捕获的线程发送一条消息,告诉它运行DoSomethingElse()
*”。但是,如果该线程正在等待异步操作完成,它将永远不会收到该消息,操作也永远不会完成,您就会遇到死锁。
[*] 不完全正确 - 它捕获了当前的SynchronizationContext
. 不过,在 UI 线程上,这相当于相同。
换句话说,这意味着从 UI 线程运行的以下代码将死锁:
public async Task DoSomethingAsync()
{
await Task.Delay(100);
}
// ...
DoSomethingAsync().Wait();
DoSomethingElse();
当Task.Delay(100)
任务完成时,它会向 UI 线程发回一条消息,说“正确,运行DoSomethingElse()
”。但是,UI 线程卡在上面Wait()
,永远不会处理消息,您陷入了僵局。
为什么这是相关的?出色地。如果您编写IModelValidator<T>
如下所示的方法:
public async Task<IEnumerable<string>> ValidatePropertyAsync(string propertyName)
{
var result = await this.Validator.ValidateAsync(this.subject, propertyName);
return result.Errors.Select(x => x.ErrorMessage);
}
然后调用ValidateProperty
,就会死锁。
诀窍是告诉await
不要捕获当前线程,使用ConfigureAwait(false)
,即
public async Task<IEnumerable<string>> ValidatePropertyAsync(string propertyName)
{
var result = await this.Validator.ValidateAsync(this.subject, propertyName).ConfigureAwait(false);
return result.Errors.Select(x => x.ErrorMessage);
}
现在,该return result.Errors...
行将在另一个线程上运行(而不是发布到 UI 线程),并且不会发生死锁。
项目原地址:https://github.com/canton7/Stylet
当前文档原地址:https://github.com/canton7/Stylet/wiki/ValidatingModelBase
上一篇:WPF的MVVM框架Stylet开发文档 12.可绑定集合BindableCollection
下一篇:WPF的MVVM框架Stylet开发文档 14.1 StyletIoC 简介