很久没用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){objectinstance = 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 的扩展。