CommunityToolkit.Mvvm学习笔记(5)——ObservableValidator


一、引言

很久没用WPF了,最近有个桌面端的项目,所以又回来使用了。

MVVM工具包这个系列,我之前写了1、2、3、4、6、7节,第5节的内容跳过了,这次补上。

第5节的内容是验证(validation)相关的,上次看到时还是初学,内容没看太懂,快速过一遍官方文档,有点囫囵吞枣的意思。

这次回过头来学呢,是因为我需要实现一个功能,又因为我当时瞄了一眼这节内容是与验证相关的,还有点印象,所以我直接就决定写篇博客,系统学习下。

我要实现的功能很常见,就是过滤掉一些前端的错误输入,然后给出一定反馈。

废话不多说,直接进入主题。

二、ObservableValidator

这节的标题—— ObservableValidator (可监视的验证器),一看这个单词就知道,它与ObservableObject有关,能监视到属性值更新,并且与验证有关。

ObservableValidator是实现了 INotifyDataErrorInfo 接口的基类,为验证暴露给其他程序模块的属性提供支持。它也继承自 ObservableObject ,所以它也实现了 INotifyPropertyChangedINotifyPropertyChanging 。它可用作需要同时支持属性更改通知和属性验证的所有类型的起点
在这里插入图片描述

它继承自ObservableObject意味着它是个自动通知类。
实现了 INotifyDataErrorInfo 接口:
这里的接口是代码层面的接口,而非更高层的模块间的接口。代码上的接口意味着,继承它的类都要实现这个接口,意味着这些类需要有这个功能(尽管实现起来各异)。就从这个接口的名称可以看出来,它是用于通知数据错误信息的,很明显,与验证前端输入这点非常符合。
总之,若一个类继承了 ObservableValidator ,那就说明该类存在一些希望进行验证的属性。

2.1. 它是如何工作的?

ObservableValidator 有以下主要特性:

  • 提供了 INotifyDataErrorInfo 的基本实现,暴露了 ErrorsChanged 事件和其他必要的api。
  • 提供了一系列额外的 SetProperty 重载(在基本 ObservableObject 类提供的重载之上),这些重载提供了在更新值之前自动验证属性和引发必要事件的能力。
  • 暴露了许多 TrySetProperty 重载,这些重载与 SetProperty 类似,但只有在验证成功时才能更新目标属性,并返回生成的错误(若有)以供进一步检查。
  • 还暴露了 ValidateProperty 方法,如果某个属性值没有更新,但它的验证依赖于已更新的另一个属性的值,那么该方法对于手动触发特定属性的验证非常有用。
  • 暴露了 ValidateAllProperties 方法,该方法自动执行当前实例中所有公有实例属性的验证,前提是它们至少有一个[ValidationAttribute]。
  • 暴露了一个 ClearAllErrors 方法,该方法在重置绑定到用户可能想要再次填写的表单的model时非常有用。
  • 还提供了许多构造函数,允许传递不同的参数来初始化 ValidationContext 实例,该实例将用于验证属性。当使用可能需要额外服务或选项才能正常工作的自定义验证属性时,这尤其有用。

2.2. 简单属性

简单属性就是单个的属性,不是复合的。

下面有个例子,演示了如何实现一个同时支持更改通知和验证的属性:

public class RegistrationForm : ObservableValidator
{
    
    
	private string name;
	[Required]
	[MinLength(2)]
	[MaxLength(100)]
	public string Name
	{
    
    
		get => name;
		set => SetProperty(ref name, value, true);
	}
}

这里调用了 ObservableValidator 暴露的 SetProperty<T>(ref T, T, bool, string) 方法,
在这里插入图片描述

额外参数 bool 设置为 true 时表示在属性更新时会验证该属性。ObservableValidator 将自动在每个新值(属性上方应用了attribute指定的)上进行验证。接着,其他组件(如UI控件)可以与viewmodel交互,并修改状态来反映当前存在于viewmodel中的错误,方法是通过注册到 ErrorsChanged 并使用 GetErrors(string) 方法检索已被修改的每个属性的错误列表。

2.3. 自定义验证方法

有时验证一个属性需要viewmodel去访问其他的服务、数据或API。这边提供了多种方法向属性添加自定义验证,使用哪种方法取决于场景和灵活度需求。下面有个示例,它说明如何使用 [CustomValidationAttribute] 类型来指示调用特定方法进行属性的额外验证:

public class RegistrationForm : ObservableValidator
{
    
    
    private readonly IFancyService service;

    public RegistrationForm(IFancyService service)
    {
    
    
        this.service = service;
    }

    private string name;

    [Required]
    [MinLength(2)]
    [MaxLength(100)]
    [CustomValidation(typeof(RegistrationForm), nameof(ValidateName))]
    public string Name
    {
    
    
        get => this.name;
        set => SetProperty(ref this.name, value, true);
    }

    public static ValidationResult ValidateName(string name, ValidationContext context)
    {
    
    
        RegistrationForm instance = (RegistrationForm)context.ObjectInstance;
        bool isValid = instance.service.Validate(name);

        if (isValid)
        {
    
    
            return ValidationResult.Success;
        }

        return new("The name was not validated by the fancy service");
    }
}

在本例中,有一个静态的 ValidateName 方法,它通过注入到viewmodel中的服务对 Name 属性执行验证(依赖注入章节有介绍)。该方法接收 name 属性值和使用中的 ValidationContext 实例为参数,其中包含viewmodel实例、正在验证的属性的名称、可选的服务提供者和一些我们使用或设置的自定义标志。在本例中,我们从 validation 上下文中检索 RegistrationForm 实例,然后从那使用注入的服务来验证属性。注意,该验证被执行,在其他attribute中指定的验证之后,所以我们可以自由组合自定义验证方法和现有的验证attribute。

上面代码示例中的Name属性上有一串 [] attribute,验证可以一个个排下去执行。

2.4. 自定义验证attribute

另一种自定义验证的方式就是实现一个自定义的 [ValidationAttribute] ,然后将验证逻辑插入重写的 IsValid 方法中。与上面的方法相比,这提供了额外的灵活性,因为它可以很容易地在多个地方重用相同的attribute。

假设我们希望根据属性关于同一viewmodel中的另一个属性的相对值来验证一个属性。首先应该定义一个自定义 [GreaterThanAttribute] ,如下所示:

public sealed class GreaterThanAttribute : ValidationAttribute
{
    
    
    public GreaterThanAttribute(string propertyName)
    {
    
    
        PropertyName = propertyName;
    }

    public string PropertyName {
    
     get; }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
    
    
        object
            instance = validationContext.ObjectInstance,
            otherValue = instance.GetType().GetProperty(PropertyName).GetValue(instance);

        if (((IComparable)value).CompareTo(otherValue) > 0)
        {
    
    
            return ValidationResult.Success;
        }

        return new("The current value is smaller than the other one");
    }
}

现在,我们可以将该attribute添加到viewmodel中了:

public class ComparableModel : ObservableValidator
{
    
    
    private int a;

    [Range(10, 100)]
    [GreaterThan(nameof(B))]
    public int A
    {
    
    
        get => this.a;
        set => SetProperty(ref this.a, value, true);
    }

    private int b;

    [Range(20, 80)]
    public int B
    {
    
    
        get => this.b;
        set
        {
    
    
            SetProperty(ref this.b, value, true);
            ValidateProperty(A, nameof(A));
        }
    }
}

本例中,有两个数字属性,它们必须在指定范围内,并且彼此间具有特定的关系(A需要大于B)。我们已经在第一个属性上添加了新的 [GreaterThanAttribute] ,并且在B的Setter中添加了对 ValidateProperty 的调用,这样每当B改变时,a都会再次被验证(因为它的验证依赖于b)。我们只需要viewmodel中的这两行代码来启动这个自定义验证,并且我们还获得了一个可重用的自定义验证attribute的好处,这个attribute在应用程序的其他viewmodel中也有用。这种方法还有助于代码模块化,因为验证逻辑现在完全与viewmodel定义本身解耦了。

三、结尾

验证器的代码并不复杂,属于应用型的内容,上面几小节中的示例直接拷贝出来改一改就能起作用,相当于是对 ObservableObject 的扩展。

猜你喜欢

转载自blog.csdn.net/BadAyase/article/details/128943859