WPF的MVVM框架Stylet开发文档 13.验证模型基类ValidatingModelBase

介绍

想象一下这样的场景……用户正在填写一份你煞费苦心编写的表格,他们在本应输入电子邮件地址的地方输入了自己的姓名。您需要检测到这一点,并以清晰的方式显示问题。

输入验证是一个很大的领域,有很多方法可以解决。最简单和最吸引人的是在您的属性的 setter 中抛出异常,如下所示:

private string _name;
public string Name
{
   get { return this._name; }
   set
   {
      if (someConditionIsFalse)
         throw new ValidationException("Message");
      this._name = value;
   }

当绑定设置此属性时,它会注意到是否抛出异常,并适当地更新控件的验证状态。

然而,这最终是一个彻底的坏主意。这意味着您的属性只能_在_设置时进行验证(例如,当用户单击“提交”时,您无法通过并验证整个表单),并且它会导致具有大量重复逻辑的大型属性设置器。可怕。

C# 还定义了两个 WPF 都知道的接口:IDataErrorInfoINotifyDataErrorInfo。这两者都为 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,您将定义一个UserViewModelValidatorextendsAbstractValidator<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 创建一个新UserViewModelIModelValidator<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 实现,欢迎您使用:

  1. 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)
    {
        // 如果有人同步调用我们,而 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 简介

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值