ObservableValidator 是实现 INotifyDataErrorInfo 接口的基类,它支持验证向其他应用程序模块公开的属性。 它也继承自 ObservableObject,因此它还可实现 INotifyPropertyChanged 和 INotifyPropertyChanging。 它可用作需要支持属性更改通知和属性验证的各种对象的起点。
工作原理
ObservableValidator 包含以下主要功能:
- 它提供对
INotifyDataErrorInfo的基本实现,从而公开ErrorsChanged事件和其他必要的 API。 - 它提供一系列额外的
SetProperty重载(ObservableObject基类提供的重载除外),这些重载提供在更新属性值之前自动验证属性和引发必要事件的功能。 - 它公开了许多
TrySetProperty重载,这些重载类似于SetProperty但仅在验证成功时更新目标属性,并在出错时返回生成的错误以供进一步检查。 - 它公开了
ValidateProperty方法,这对于手动触发对特定属性的验证非常有用,以防其值尚未更新,但其验证依赖于已更新的另一个属性的值。 - 它公开了
ValidateAllProperties方法,这会自动执行对当前实例中所有公共实例属性的验证,前提是它们至少应用了一个[ValidationAttribute]。 - 它公开了
ClearAllErrors方法,该方法在重置绑定到用户可能需要再次填充的某个表单的模型时非常有用。 - 它提供许多构造函数,这些函数允许传递不同的参数来初始化将用于验证属性的
ValidationContext实例。 使用可能需要其他服务或选项才能正常工作的自定义验证特性时,这尤其有用。
简单属性
下面是如何实现支持更改通知和验证功能的属性的示例:
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 将使用应用于该属性的特性指定的所有检查自动对每个新值运行验证。 然后,其他组件(如 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 实例、正在验证的属性的名称以及(可选)服务提供程序和一些我们可以使用或设置的自定义标志等内容。 在本例中,我们从验证上下文中检索 RegistrationForm 实例,然后在其中使用注入的服务来验证属性。 请注意,此验证将与其他特性中指定的验证同时执行,因此我们可以任意方式自由组合自定义验证方法和现有的验证属性。
自定义验证属性
执行自定义验证的另一种方法是实现自定义 [ValidationAttribute],然后将验证逻辑插入到重写的 IsValid 方法中。 与上述方法相比,这可实现额外的灵活性,因为它可以轻松地在多个位置重复使用同一特性。
假设我们想要根据属性相对于同一 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");
}
}
接下来,我们可以将此特性添加到 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(因为它的验证状态取决于它本身)。 我们只需在 viewmodel 中使用这两行代码来启用此自定义验证,同时还可享受可重用的自定义验证特性带来的好处,该特性在应用程序中的其他 viewmodel 中也很有用。 此方法还有助于实现代码模块化,因为验证逻辑现在已与 viewmodel 定义本身完全分离。