• 轻量级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都知道这两个接口: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);
    }
    
  • 相关阅读:
    贾庆山老师对研究生做学术的几点建议
    normalization flow
    PP: Robust Anomaly Detection for Multivariate Time Series through Stochastic Recurrent Neural Network
    PP: Multi-Horizon Time Series Forecasting with Temporal Attention Learning
    Attention machenism
    PP: Modeling extreme events in time series prediction
    Learn from Niu 2020.1.28
    Big research problems (1)
    PP: UMAP: uniform manifold approximation and projection for dimension reduction
    Dimension reduction
  • 原文地址:https://www.cnblogs.com/qouoww/p/15798337.html
Copyright © 2020-2023  润新知