很久没用WPF了,最近有个桌面端的项目,所以又回来使用了。
MVVM工具包这个系列,我之前写了1、2、3、4、6、7节,第5节的内容跳过了,这次补上。
第5节的内容是验证(validation)相关的,上次看到时还是初学,内容没看太懂,快速过一遍官方文档,有点囫囵吞枣的意思。
这次回过头来学呢,是因为我需要实现一个功能,又因为我当时瞄了一眼这节内容是与验证相关的,还有点印象,所以我直接就决定写篇博客,系统学习下。
我要实现的功能很常见,就是过滤掉一些前端的错误输入,然后给出一定反馈。
废话不多说,直接进入主题。
这节的标题—— ObservableValidator (可监视的验证器),一看这个单词就知道,它与ObservableObject有关,能监视到属性值更新,并且与验证有关。
ObservableValidator是实现了 INotifyDataErrorInfo 接口的基类,为验证暴露给其他程序模块的属性提供支持。它也继承自 ObservableObject ,所以它也实现了 INotifyPropertyChanged 和 INotifyPropertyChanging 。它可用作需要同时支持属性更改通知和属性验证的所有类型的起点。
它继承自ObservableObject意味着它是个自动通知类。
实现了 INotifyDataErrorInfo 接口:
这里的接口是代码层面的接口,而非更高层的模块间的接口。代码上的接口意味着,继承它的类都要实现这个接口,意味着这些类需要有这个功能(尽管实现起来各异)。就从这个接口的名称可以看出来,它是用于通知数据错误信息的,很明显,与验证前端输入这点非常符合。
总之,若一个类继承了 ObservableValidator ,那就说明该类存在一些希望进行验证的属性。
ObservableValidator 有以下主要特性:
简单属性就是单个的属性,不是复合的。
下面有个例子,演示了如何实现一个同时支持更改通知和验证的属性:
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
额外参数 bool 设置为 true 时表示在属性更新时会验证该属性。ObservableValidator 将自动在每个新值(属性上方应用了attribute指定的)上进行验证。接着,其他组件(如UI控件)可以与viewmodel交互,并修改状态来反映当前存在于viewmodel中的错误,方法是通过注册到 ErrorsChanged 并使用 GetErrors(string) 方法检索已被修改的每个属性的错误列表。
有时验证一个属性需要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,验证可以一个个排下去执行。
另一种自定义验证的方式就是实现一个自定义的 [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 的扩展。