qouoww

质量管理+软件开发=聚焦管理软件的开发与应用

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

介绍

想象一下场景...用户正在填写您精心编写的表单,并在应该输入电子邮件地址的位置输入他们的姓名。您需要检测到这一点,并以清晰的方式显示问题。

输入验证是一个很大的领域,有很多方法可以做到这一点。最简单、最吸引人的是在你的属性的 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 ,不添加任何额外的内容。当使用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接口(通常通过扩展AbstractValidator来执行此操作,来验证模型T)。然后,创建一个新的实例,并使用它来运行验证。例如,如果你有一个UserViewModel ,你将定义一个UserViewModelValidator,扩展AbstractValidator,因此实现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。它还需要接受一个IValidator ,作为构造函数参数,如下所示:


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);
}
posted on 2022-01-13 16:47  qouoww  阅读(678)  评论(0编辑  收藏  举报