ASP.NETMVC基于标注特性的Model验证:ValidationAttribute
通过前⾯的介绍我们知道ModelValidatorProviders的静态只读Providers维护着⼀个全局的ModelValidatorProvider列表,最终⽤于Model验证的ModelValidator都是通过这些ModelValidatorProvider来提供的。对于该列表默认包含的三种ModelValidatorProvider来说,DataAnnotationsModelValidatorProvider⽆疑是最重要的,ASP.NET MVC默认提供的基于数据标注特性的声明式Model验证就是通过DataAnnotationsModelValidatorProvider提供的DataAnnotationsModelValidator来实现的。[本⽂已经同步到《》中]
⽬录
⼀、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属性指定⼀个字符串作为验证错误消息,⼜通过ErrorMessageResourceNa
me/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)。”
受保护的IsValid⽅法除了包含⼀个表⽰被验证对象的参数value,还具有具有如下定义的类型为的参数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⽅法返回值表⽰验证结果的对象是⼀个具有如下定义的类型为的对象。与作为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⽅法验证某个指定的对象,该⽅法在验证失败的情况下会直接抛出⼀个异常,⽽作为该异常的消息是通过调⽤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属性,其中⽤于验证必要性的RequiredAt
tributeAdapter是系统⾃动添加的(因为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.DataAnnotationsModelValidatorasp 字符串转数组
4: System.Web.Mvc.RequiredAttributeAdapter
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论