ASP.NET MVC基于标注特性的Model验证:ValidationAttribute

通过前面的介绍我们知道ModelValidatorProviders的静态只读Providers维护着一个全局的ModelValidatorProvider列表,最终用于Model验证的ModelValidator都是通过这些ModelValidatorProvider来提供的。对于该列表默认包含的三种ModelValidatorProvider来说,DataAnnotationsModelValidatorProvider无疑是最重要的,ASP.NET MVC默认提供的基于数据标注特性的声明式Model验证就是通过DataAnnotationsModelValidatorProvider提供的DataAnnotationsModelValidator来实现的。[本文已经同步到《How ASP.NET MVC Works?》中]

目录 
一、ValidationAttribute特性 
二、验证消息的定义 
三、验证的执行 
四、预定义ValidationAttribute 
五、应用ValidationAttribute特性的唯一性

一、ValidationAttribute特性

与通过数据标注特性定义Model元数据类似,我们可以在作为Model的数据类型及其属性上应用相应的标注特性来定义Model验证规则。所有的验证特性都直接或者间接继承自抽象类型System.ComponentModel.DataAnnotations.ValidationAttribute。如下面的代码片断所示,ValidationAttribute具有一个字符串类型的ErrorMessage属性用于指定验证错误消息。出于对本地化或者对错误消息单独维护的需要,我们可以采用资源文件的方式来保存错误消息,在这种情况下我们只需要通过ErrorMessageResourceName和ErrorMessageResourceType这两个属性指定错误消息所在资源项的名称和类型即可。

   1: public abstract class ValidationAttribute : Attribute
   2: {     
   3:     public string ErrorMessage { get; set; }
   4:     public string ErrorMessageResourceName { get; set; }
   5:     public Type ErrorMessageResourceType { get; set; }
   6:     protected string ErrorMessageString {get;}  
   7:  
   8:     public virtual string FormatErrorMessage(string name);
   9:  
  10:     public virtual bool IsValid(object value); 
  11:     protected virtual ValidationResult IsValid(object value, ValidationContext validationContext)
  12:  
  13:     public void Validate(object value, string name);
  14:     public ValidationResult GetValidationResult(object value, ValidationContext validationContext);
  15: }

二、验证消息的定义

如果我们通过ErrorMessage属性指定一个字符串作为验证错误消息,又通过ErrorMessageResourceName/ErrorMessageResourceType属性指定了错误消息资源项对应的名称和类型,后者具有更高的优先级。ValidationAttribute具有一个受保护的只读属性ErrorMessageString用于返回最终的错误消息文本

对于错误消息的定义,我们可以定义一个完整的消息,比如“年龄必需在18至25之间”。但是对于像资源文件这种对错误消息进行独立维护的情况,为了让定义的资源文本能够最大限度地被重用,我们倾向于定义一个包含占位符的文本模板,比如“{DisplayName}必需在{LowerBound}和{UpperBound}之间”,这样消息适用于所有基于数值范围的验证。对于后者,模板中的占位符可以在虚方法FormatErrorMessage中进行替换。该方法中的参数name实际上代表的是对应的显示名称,即对应ModelMetadata的DisplayName属性。

FormatErrorMessage方法在ValidationAttribute中的默认实现仅仅是简单地调用String的静态方法Format将参数name作为替换占位符的参数,具体的定义如下。所以在默认的情况下,我们在定义错误消息模板的时候,只允许包含唯一一个针对显示名称的占位符“{0}”。如果具有额外的占位符,或者不需要采用基于序号(“{0}”)的定义方法(比如采用类似于“{DisplayName}”这种基于文字的占位符更具可读性),只需要重写FormatErrorMessage方法即可。

   1: public abstract class ValidationAttribute : Attribute
   2: {
   3:     //其他成员
   4:     public virtual string FormatErrorMessage(string name)
   5:     {
   6:         return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, new object[] { name });
   7:     }
   8: }

三、验证的执行

当我们通过继承ValidationAttribute创建我们自己的验证特性的时候,可以通过重写公有方法IsValid或者受保护方法IsValid来实现我们自定义的验证逻辑。我们之所以能够通过重写任一个IsValid方法是我们自定义验证逻辑生效的原因在于这两个方法在ValidationAttribute特殊的定义方法。按照这两个方法在ValidationAttribute中的定义,它们之间存在相互调用的关系,而这种相互调用必然造成“死循环”,所以我们需要重写至少其中一个方法比避免“死循环”的方法。这里的“死循环”被加上的引号,是因为ValidationAttribute在内部作了处理,当这种情况出现的时候会抛出一个NotImplementedException异常。

   1: //调用公有IsValid方法
   2: public class ValidatorAttribute : ValidationAttribute
   3: {
   4:     static void Main()
   5:     {        
   6:         ValidatorAttribute validator = new ValidatorAttribute();
   7:         validator.IsValid(new object());
   8:     }
   9: }
  10:  
  11: //调用受保护IsValid方法
  12: public class ValidatorAttribute : ValidationAttribute
  13: {    
  14:     static void Main()
  15:     {        
  16:         ValidatorAttribute validator = new ValidatorAttribute();
  17:         validator.IsValid(new object(),null);
  18:     }
  19: }

为了验证对虚方法IsValid重写的必要性,我们来做一个简单的实例演示。在一个控制台应用中我们分别编写了如上两段程序,其中通过继承ValidationAttribute定义了一个ValidatorAttribute,但是没有重写任何一个IsValid方法。当我们在Debug模式下分别运行这两段程序的时候,都会抛出如下图所示的NotImplementedException异常,提示“此类尚未实现 IsValid(object value)。首选入口点是 GetValidationResult(),并且类应重写 IsValid(object value, ValidationContext context)。”

image

受保护的IsValid方法除了包含一个表示被验证对象的参数value,还具有具有如下定义的类型为ValidationContext的参数validationContext。顾名思义,ValidationContext旨在为当前的验证维护相应的上下文信息,这些信息包括通过ObjectInstance和ObjectType属性表示的验证对象及其类型,通过MemberName和DisplayName属性表示的成员名称(一般指属性名称)和显示名称。

   1: public sealed class ValidationContext
   2: {    
   3:     //其他成员
   4:     public ValidationContext(object instance);    
   5:     public ValidationContext(object instance, IDictionary<object, object> items);
   6:   
   7:     public string DisplayName { get; set; }
   8:     public string MemberName {  get;  set; }
   9:     public object ObjectInstance {  get; }
  10:     public Type ObjectType { get; }
  11: }

作为该IsValid方法返回值表示验证结果的对象是一个具有如下定义的类型为ValidationResult的对象。与作为ModelValidator验证结果的ModelValidationResult类型类似,ValidationResult依然是错误消息和成员名称的组合。不过ModelValidationResult对应某个单一的成员名称,而ValidationResult包含一组相关成员名称的列表。

   1: public class ValidationResult
   2: {    
   3:     //其他成员
   4:     public ValidationResult(string errorMessage);
   5:     public ValidationResult(string errorMessage, IEnumerable<string> memberNames);
   6:  
   7:     public string ErrorMessage {  get;  set; }
   8:     public IEnumerable<string> MemberNames {  get; }
   9: }

对于定义在ValidationAttribute中的IsValid方法的默认实现来说,在验证失败的情况下会返回一个具体的ValidationResult对象,如果指定的ValidationContext不为Null,那么其MemberName属性表示的成员名称将会包含在该ValidationResult对象的MemberNames列表中。而ValidationContext的DisplayName属性将会作为调用FormatErrorMessage的参数,该方法调用得到的完整的错误消息将会作为ValidationResult的ErrorMessage属性。如果通过验证,则直接返回Null。

我们可以通过调用ValidationAttribute的公有方法GetValidationResult对指定的对象实施验证并得到以ValidationResult对象形式返回的验证结果,最终返回的实际上就是调用受保护方法IsValid的返回值。我们也可以调用Validate方法验证某个指定的对象,该方法在验证失败的情况下会直接抛出一个ValidationException异常,而作为该异常的消息是通过调用FormatErrorMessage方法(将参数name表示的字符串作为参数)格式化后的错误消息。

四、预定义ValidationAttribute

在System.ComponentModel.DataAnnotations命名空间下定义了一系列继承自ValidationAttribute的验证特性,这些验证特性大都直接应用在自定义数据类型的某个属性上根据相应的验证规则对属性值实施验证。这些预定义验证特性不是本篇文章论述的重点,所以我们在这里只是对它们作一个概括性的介绍:

  • RequiredAttribute:用于验证必需数据字段。
  • RangeAttribute:用于验证数值字段的值是否在指定的范围之内。
  • StringLengthAttribute:用于验证目标字段的字符串长度是否在指定的范围之内。
  • MaxLengthAttribute/MinLengthAttribute:用于验证字符/数组字典的长度是否小于/大于指定的上/下限。
  • RegularExpressionAttribute:用于验证字符串字段的格式是否与指定的正则表达式相匹配。
  • CompareAttribute:用于验证目标字段的值是否与另一个字段值一致,在用户注册场景中可以用于确认两次输入密码的一致性。
  • CustomValidationAttribute:指定一个用于验证目标成员的验证类型和验证方法。

五、应用ValidationAttribute特性的唯一性

对于上面列出的这些预定义ValidationAttribute,它们都具有一个相同的特性,那就是在同一个目标元素中只能应用一次,这可以通过应用在它们之前的AttributeUsageAttribute特性的定义看出来。以如下所示的RequiredAttribute为例,应用在该类型上的AttributeUsageAttrribute特性的AllowMultiple属性被设置为False

   1: [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple=false)]
   2: public class RequiredAttribute : ValidationAttribute
   3: {
   4:    //省略成员
   5: }

但是是否意味着如果我们在定义ValidationAttribute的时候将应用在它上面的AttributeUsageAttrribute特性的AllowMultiple设置为True就可以将它们多次应用到被验证的属性或者类型上了呢?我们不妨通过实例演示的方式来说明这个问题。

我们知道RangeAttribute可以帮助我们验证目标字段值的范围,但是有时候我们需要进行“条件性范围验证”。举个例子,我们现在对于对某个员工的薪水进行验证,但是不同级别的员工的薪水范围是不同的,为此我们创建了一个名为RangeIfAttribute的验证特性辅助我们针对不同级别的薪水范围进行验证。如下面的代码片断所示,我们将三个RangeIfAttribute特性应用到了表示薪水的Salary属性上,分别针对三个级别(G7、G8和G9)的薪水范围作了设定。

   1: public class Employee
   2: {
   3:     public string Name { get; set; }
   4:     public string Grade { get; set; }
   5:  
   6:     [RangeIf("Grade", "G7", 2000, 3000)]
   7:     [RangeIf("Grade", "G8", 3000, 4000)]
   8:     [RangeIf("Grade", "G9", 4000, 5000)]
   9:     public decimal Salary { get; set; }
  10: }

RangeIfAttribute特性的定义如下所示,它直接继承自RangeAttribute。RangeIfAttribute实际上就是根据容器对象的另一个属性值来决定是否对目标属性值实施验证,属性Property和Value就分别代表这个这个属性和与之匹配的值。在重写的IsValid方法中,我们通过反射获取到了容器对象用于匹配的属性值,如果该值与Value属性值相匹配,则调用基类同名法方法对指定对象进行验证,否则直接返回ValidationResult.Success(Null)。而应用在RangeIfAttribute上的AttributeUsageAttribute特性的AllowMultiple被设置为True。

   1: [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
   2: public class RangeIfAttribute: RangeAttribute
   3: {
   4:     public string Property { get; set; }
   5:     public string Value { get; set; }
   6:  
   7:     public RangeIfAttribute(string property, string value, double minimum, double maximum)
   8:         : base(minimum, maximum)
   9:     {
  10:         this.Property = property;
  11:         this.Value = value??"";
  12:     }
  13:  
  14:     protected override ValidationResult IsValid(object value, ValidationContext validationContext)
  15:     {
  16:        ...
  17:         PropertyInfo property = validationContext.ObjectType.GetProperty(this.Property);
  18:         object propertyValue = property.GetValue(validationContext.ObjectInstance, null);
  19:         propertyValue = propertyValue ?? "";
  20:         if (propertyValue.ToString()!= this.Value)
  21:         {
  22:             return ValidationResult.Success;
  23:         }
  24:         return base.IsValid(value, validationContext);
  25:     }  
  26: }

那么这样一个RangeIfAttribute特性真的能够按照我们期望的方式进行验证吗?为此我们通过Visual Studio的ASP.NET MVC项目模板创建了一个空的Web应用,我们将上面的Employee类型定义其中,然后创建一个具有如下定义的HomeController。在Action方法Index中,我们创建了一个DataAnnotationsModelValidatorProvider对象,通过它获取针对Employee的Salary属性的所有DataAnnotationsModelValidator并将其类型名称呈现出来。

   1: public class HomeController : Controller
   2: {
   3:     public void Index()
   4:     {
   5:         DataAnnotationsModelValidatorProvider provider = 
   6:         new DataAnnotationsModelValidatorProvider();
   7:         ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(() => new Employee(), typeof(Employee));
   8:         metadata = metadata.Properties.FirstOrDefault(p => p.PropertyName == "Salary");
   9:         var validators = ModelValidatorProviders.Providers.GetValidators(metadata, ControllerContext);
  10:         foreach (var validator in validators.OfType<DataAnnotationsModelValidator>())
  11:         {
  12:             Response.Write(validator + "<br/>");
  13:         }
  14:     }        
  15: }

当我们运行该程序时,会在浏览器上呈现如下所示的输出结果。该输出结果意味着只有两个DataAnnotationsModelValidator最终应用到Employee的Salary属性,其中用于验证必要性的RequiredAttributeAdapter是系统自动添加的(因为Salary属性为非空值类型,被认为是必需的),另一个自然来源于应用在该属性上的RangeIfAttribute特性。但是我们一共应用了三个RangeIfAttribute特性在Salary属性上,为何只有一个DataAnnotationsModelValidator被创建呢

   1: System.Web.Mvc.DataAnnotationsModelValidator
   2: System.Web.Mvc.RequiredAttributeAdapter

我们知道Attribute具有一个名为TypeId的object类型属性,默认返回代表自身类型的Type对象。Model验证系统在根据ValidationAttribute特性创建相应的DataAnnotationsModelValidator对象的时候会根据该TypeId属性值进行分组,同一组的ValidationAttribute只会选择第一个。这就意味着对于多个应用到相同目标元素的同类ValidationAttribute,有且只有一个是有效的。那么如何来解决这个问题呢?其实很简单,既然Model验证系统在根据Attribute的TypeId进行验证特性的筛选,我们只需要通过重写TypeId属性是每个ValidationAttribute具有不同的属性值就可以了。为此我们按照如下的方式在RangeIfAttribute中重写了TypeId属性。

   1: [AttributeUsage( AttributeTargets.Field| AttributeTargets.Property, AllowMultiple = true)]
   2: public class RangeIfAttribute: RangeAttribute
   3: {
   4:     //其他成员
   5:     private object typeid;
   6:     public override object TypeId
   7:     {
   8:         get{ return typeid?? (typeid= new object());}
   9:     }
  10: }

再次运行我们的程序将会在浏览器中得到如下的输出结果,针对三个RangeIfAttribute特性的三个DataAnnotationsModelValidator被创建出来了。关于通过重写TypeId而允许多个ValidationAttribute同时应用到相同的目标属性或者类型的方式不适合客户端验证,因为这会导致多组相同的验证规则被生成,而这是不允许的。(S608)

   1: System.Web.Mvc.DataAnnotationsModelValidator
   2: System.Web.Mvc.DataAnnotationsModelValidator
   3: System.Web.Mvc.DataAnnotationsModelValidator
   4: System.Web.Mvc.RequiredAttributeAdapter

 

ASP.NET MVC基于标注特性的Model验证:ValidationAttribute 
ASP.NET MVC基于标注特性的Model验证:DataAnnotationsModelValidator 
ASP.NET MVC基于标注特性的Model验证:DataAnnotationsModelValidatorProvider 
ASP.NET MVC基于标注特性的Model验证:将ValidationAttribute应用到参数上 
ASP.NET MVC基于标注特性的Model验证:一个Model,多种验证规则

作者: Artech
出处: http://artech.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

你可能感兴趣的:(validation)