【WPF】MVVM模式下的输入校验(IDataErrorInfo + DataAnnotations)
【前言】
Windows Presentation Foundation (WPF) 具有一个丰富数据绑定系统。除了作为通过 Model-View-ViewModel (MVVM) 模式从支持逻辑和数据对 UI 定义进行松散耦合的关键推动力之外,数据绑定系统还为业务数据验证方案提供强大而灵活的支持。WPF 中的数据绑定机制包括多个选项,可用于在创建可编辑视图时校验输入数据的有效性。
验证机制说明异常通过在某个 Binding 对象上设置 ValidatesOnExceptions 属性,如果在尝试对源对象属性设置已修改的值的过程中引发异常,则将为该 Binding 设置验证错误。ValidationRulesBinding 类具有一个用于提供 ValidationRule 派生类实例的集合的属性。这些 ValidationRules 需要覆盖某个 Validate 方法,该方法由 Binding 在每次绑定控件中的数据发生更改时进行调用。如果 Validate 方法返回无效的 ValidationResult 对象,则将为该 Binding 设置验证错误。IDataErrorInfo通过在绑定数据源对象上实现 IDataErrorInfo 接口并在 Binding 对象上设置 ValidatesOnDataErrors 属性,Binding 将调用从绑定数据源对象公开的 IDataErrorInfo API。如果从这些属性调用返回非 null 或非空字符串,则将为该 Binding 设置验证错误。
在使用 WPF 中的数据绑定来呈现业务数据时,通常应使用 Binding 对象在目标控件的单个属性与数据源对象属性之间提供数据管道。
若要使绑定验证有效,首先需要进行 TwoWay 数据绑定。这意味着,除了从源属性流向目标属性以进行显示的数据之外,编辑过的数据也会从目标流向源。
当 TwoWay 数据绑定中输入或修改数据时,将启动以下工作流:
用户通过击键、鼠标、触摸或与各元素间的手写笔交互来输入或修改数据,从而更改元素的属性。如果需要,可将数据转换为数据源属性类型。设置源属性值。触发 Binding.SourceUpdated 附加事件。如果数据源属性上的 setter 引发异常,则异常会由 Binding 捕获,并可用于指示验证错误。如果实现了 IDataErrorInfo 接口,则会对数据源对象调用该接口的方法获得该属性的错误信息。向用户呈现验证错误指示,并触发 Validation.Error 附加事件。
本篇主要介绍MVVM模式下, IDataErrorInfo 的校验以及结合 DataAnnotation 的实现方法。比较来说,在 set 里直接 throw Exception 实现最为简单,但不适合组合校验且Model里需要编写过重的校验代码;ValidationRules 更适合在用户控件或者自定义控件场合使用。IDataErrorInfo 则是比较普遍且灵活的校验实现方式。
【实例分析】
我们先来看看 IDataErrorInfo 的实现方法:Error 属性用于指示整个对象的错误,而索引器用于指示单个属性级别的错误。
两者的工作原理相同:如果返回非 null 或非空字符串,则表示存在验证错误。否则,返回的字符串用于向用户显示错误。
Person.cs 的 Age 属性进行了校验。
public static class ValidationExtension { public static string ValidateProperty利用这一思路,可以很容易的实现各种 POCO Model 类型的校验逻辑分离,这对于开发分层架构的应用框架时非常有用。(this object obj, string propertyName) { if (string.IsNullOrEmpty(propertyName)) return string.Empty; var targetType = obj.GetType(); //你也可以利用 MetadataType 在分离类上声明 //var targetMetadataAttr = targetType.GetCustomAttributes(false) // .FirstOrDefault(a => a.GetType() == typeof(MetadataTypeAttribute)) as MetadataTypeAttribute; //if (targetMetadataAttr != null && targetType != targetMetadataAttr.MetadataClassType) //{ // TypeDescriptor.AddProviderTransparent( // new AssociatedMetadataTypeTypeDescriptionProvider(targetType, targetMetadataAttr.MetadataClassType), targetType); //} if (targetType != typeof(MetadataType)) { TypeDescriptor.AddProviderTransparent( new AssociatedMetadataTypeTypeDescriptionProvider(targetType, typeof(MetadataType)), targetType); } var propertyValue = targetType.GetProperty(propertyName).GetValue(obj, null); var validationContext = new ValidationContext(obj, null, null); validationContext.MemberName = propertyName; var validationResults = new List (); Validator.TryValidateProperty(propertyValue, validationContext, validationResults); if (validationResults.Count > 0) { return validationResults.First().ErrorMessage; } return string.Empty; }}