Model Validation in Asp.net MVC

原文: Model Validation in Asp.net MVC

     本文用于记录Pro ASP.NET MVC 3 Framework中阐述的数据验证的方式。 

     先说服务器端的吧。最简单的一种方式自然是直接在Action方法中来进行了,如下:

        [HttpPost]
         public ViewResult MakeBooking(Appointment appt)
        {        
             if (String.IsNullOrWhiteSpace(appt.ClientName))
            {
                ModelState.AddModelError( " ClientName "" Please enter your name ");
            }
             if (ModelState.IsValidField( " Date ") && DateTime.Now > appt.Date)
            {
                ModelState.AddModelError( " Date "" Please enter a date in the future ");
            }
             if (!appt.TermsAccepted)
            {
                ModelState.AddModelError( " TermsAccepted "" You must accept the terms ");
            }
             if (ModelState.IsValidField( " ClientName ") && ModelState.IsValidField( " Date ") &&
                appt.ClientName ==  " Joe " && appt.Date.DayOfWeek == DayOfWeek.Monday)
            {
                ModelState.AddModelError( """ Joe cannot book appointments on Mondays ");
            }

             if (ModelState.IsValid)
            {
                repository.SaveAppointment(appt);
                 return View( " Completed ", appt);
            }
             else
            {
                 return View();
            } 
        }

补充Appointment类源码如下:

 

     public  class Appointment
    {
         public  string ClientName {  getset; }

        [DataType(DataType.Date)]
         public DateTime Date {  getset; }

         public  bool TermsAccepted {  getset; }
    }

 

可以看到,Appointment类很POCO,其中Date属性上的DataType属性,不过是标注Date属性值为DateTime的Date部分(去掉Time部分)。再看action内部,将传入的appointment对象属性进行了一个遍历校验。最后,ModelState.AddModelError("""Joe cannot book appointments on Mondays"); 是标注一个对象模型级别的错误(方法的key参数为空),模型级别错误可以标注多个,它们均将通过@Html.ValidationSummary()显示错误信息。

      上述action对应的view为:

@model PageValidation.Models.Appointment
           
@{
    ViewBag.Title =  " Make A Booking ";
}
<h4>Book an Appointment</h4>
@using (Html.BeginForm())
{
    @Html.ValidationSummary();
                             
    <p>
        Your name: @Html.EditorFor(m => m.ClientName)     
        @Html.ValidationMessageFor(m => m.ClientName)   
    </p>
    <p>
        Appointment Date: @Html.EditorFor(m => m.Date)
        @Html.ValidationMessageFor(m => m.Date)
    </p>
    <p>
        @Html.EditorFor(m => m.TermsAccepted) I accept the terms & conditions   
        @Html.ValidationMessageFor(m => m.TermsAccepted)
    </p>
    <input type= " submit " value= " Make Booking " /> 
}

这个时候,运行程序,神马都不填写然后提交时,页面提示如下:

Model Validation in Asp.net MVC 

如果不想form中错误提示重复(顶部的summary和顶部的detail),将@Html.ValidationSummary(); 更新为@Html.ValidationSummary(true); 即可。这个时候,顶部Validation Summary部分只会提示model-level错误了,比如上文中的ModelState.AddModelError("""Joe cannot book appointments on Mondays");。 关于@Html.ValidationSummary()更多细节,请MSDN。

      另外,还有一个view的问题是,Firefox和Chrome等一些浏览器上,对checkbox样式的设置不取作用,上图中的效果是通过在checkbox外层包一个div,将checkbox样式转移到div上来实现的。具体为:在项目Views\Shared\EditorTemplates目录下,建立一个Boolean.cshtml文件以覆盖asp.net mvc默认的行为。文件内容如下:

@model  bool?      
           
@if (ViewData.ModelMetadata.IsNullableValueType)
{
    @Html.DropDownListFor(m => m,  new SelectList( new[] {  " Not Set "" True "" False " }, Model));
}
else
{
    ModelState state = ViewData.ModelState[ViewData.ModelMetadata.PropertyName];
     bool value = Model ??  false;
     if (state !=  null && state.Errors.Count >  0)
    {
    <div  class = " input-validation-error "  style= " float: left " >
        @Html.CheckBox( "" , value)
    </div>
    }
     else
    {
    @Html.CheckBox( "", value)
    }
}

     

       服务器端验证第2种方式是通过Model Binder了。我们继承DefaultModelBinder来写一个Appointment需要的类:

     public  class ValidatingModelBinder : DefaultModelBinder
    {
         protected  override  void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, 
            PropertyDescriptor propertyDescriptor,  object value)
        {
             //  make sure we call the base implementation
             base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);

             //  perform our property-level validation
             switch (propertyDescriptor.Name)
            {
                 case  " ClientName ":
                     if ( string.IsNullOrEmpty(( string)value))
                    {
                        bindingContext.ModelState.AddModelError( " ClientName "" Please enter your name ");
                    }
                     break;
                 case  " Date ":
                     if (bindingContext.ModelState.IsValidField( " Date ") && DateTime.Now > ((DateTime)value))
                    {
                        bindingContext.ModelState.AddModelError( " Date "" Please enter a date in the future ");
                    }
                     break;
                 case  " TermsAccepted ":
                     if (!(( bool)value))
                    {
                        bindingContext.ModelState.AddModelError( " TermsAccepted "" You must accept the terms ");
                    }
                     break;
            }
        }

         protected  override  void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
             //  make sure we call the base implementation
             base.OnModelUpdated(controllerContext, bindingContext);

            Appointment model = bindingContext.Model  as Appointment;
             //  apply our model-level validation
             if (model !=  null && bindingContext.ModelState.IsValidField( " ClientName ") && bindingContext.ModelState.IsValidField( " Date "
                && model.ClientName ==  " Joe " && model.Date.DayOfWeek == DayOfWeek.Monday)
            {
                bindingContext.ModelState.AddModelError( """ Joe cannot book appointments on Mondays ");
            }
        }
    }

其中,OnModelUpdated方法是当给model所有属性赋值时触发,SetProperty方式是当单个属性变化时即触发。接下来要做的,就是在global的Application_Start方法中注册了:

ModelBinders.Binders.Add( typeof(Appointment),  new ValidatingModelBinder());

然后,MakeBooking action就可以解脱出来,只需要如下几行代码:

             if (ModelState.IsValid)
            {
                repository.SaveAppointment(appt);
                 return View( " Completed ", appt);
            }
             else
            {
                 return View();
            }   

此时,效果和第1种方式完全一样。

     

     第3种方式是通过MetaData了。Asp.net MVC内置了5个meta data验证属性:Compare、Range、RegularExpression、Required、StringLength。基于这5个属性的一些限制,为了更适切Appointment类,自定义几个验证属性如下:

futureDate验证属性: 


     public  class FutureDateValidatorAttribute : ValidationAttribute
    {
         public  override  bool IsValid( object value)
        {
             var isDate = value  is DateTime;
             if(isDate)
            {
                 var date = Convert.ToDateTime(value);
                 if (date <= DateTime.Now)
                {
                     return  false;
                }
            }  

             return  true;
        }
    }

MustBeTrue验证属性:

     public  class MustBeTrueAttribute : ValidationAttribute
    {
         public  override  bool IsValid( object value)
        {
             return value  is  bool && ( bool)value;
        }
    }

Appointment验证属性:

     public  class AppointmentValidatorAttribute : ValidationAttribute
    {
         public AppointmentValidatorAttribute()
        {
            ErrorMessage =  " Joe cannot book appointments on Mondays ";
        }

         public  override  bool IsValid( object value)
        {
            Appointment app = value  as Appointment;
             if (app ==  null ||  string.IsNullOrEmpty(app.ClientName) || app.Date ==  null)
            {
                 //  we don't have a model of the right type to validate, or we don't have
                
//  the values for the ClientName and Date properties we require
                 return  true;
            }
             else
            {
                 return !(app.ClientName ==  " Joe " && app.Date.DayOfWeek == DayOfWeek.Monday);
            }
        }
    }

再来定义Appointment类:

    [AppointmentValidator]
     public  class Appointment
    {
        [Required(ErrorMessage =  " Please enter your name ")]
         public  string ClientName {  getset; }

        [DataType(DataType.Date)]
        [FutureDateValidator(ErrorMessage =  " You must enter a date in the future ")]
         public DateTime Date {  getset; }

         // [Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the terms")]
        [MustBeTrue(ErrorMessage =  " You must accept the terms ")]
         public  bool TermsAccepted {  getset; }
    }


    第4种方式:通过实现IValidatableObject接口,定义自验证model。还是Appointment类,如下:

     public  class Appointment : IValidatableObject
    {
         public  string ClientName {  getset; }

        [DataType(DataType.Date)]
         public DateTime Date {  getset; }

         public  bool TermsAccepted {  getset; }

         public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            List<ValidationResult> errors =  new List<ValidationResult>();
             if ( string.IsNullOrEmpty(ClientName))
            {
                errors.Add( new ValidationResult( " Please enter your name "new  string[] {  " ClientName " }));
            }
             if (DateTime.Now > Date)
            {
                errors.Add( new ValidationResult( " Please enter a date in the future "new  string[] {  " Date " }));
            }
             if (errors.Count ==  0 && ClientName ==  " Joe "
                && Date.DayOfWeek == DayOfWeek.Monday)
            {
                errors.Add( new ValidationResult( " Joe cannot book appointments on Mondays "));
            }
             if (!TermsAccepted)
            {
                errors.Add( new ValidationResult( " You must accept the terms "new  string[] {  " TermsAccepted " }));
            }
             return errors;
        }
    }

可以看到,它的核心不过是:将类对象验证内容移入到Valiate方法中。

 

      第5种方式,通过继承ModelValidationProvider,创建自定义ValidationProvider. 如下:

     public  class CustomValidationProvider : ModelValidatorProvider
    {
         public  override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
        {
             if (metadata.ContainerType ==  typeof(Appointment))
            {
                 return  new ModelValidator[] {
                     new AppointmentPropertyValidator(metadata, context)
                };
            }
             else  if (metadata.ModelType ==  typeof(Appointment))
            {
                 return  new ModelValidator[] {
                     new AppointmentValidator(metadata, context)
                };
            }

             return Enumerable.Empty<ModelValidator>();
        }
    }

AppointmentPropertyValidator代码如下:

     public  class AppointmentPropertyValidator : ModelValidator
    {
         public AppointmentPropertyValidator(ModelMetadata metadata, ControllerContext context)
            :  base(metadata, context)
        {
        }

         public  override IEnumerable<ModelValidationResult> Validate( object container)
        {
            Appointment appt = container  as Appointment;
             if (appt !=  null)
            {
                 switch (Metadata.PropertyName)
                {
                     case  " ClientName ":
                         if ( string.IsNullOrEmpty(appt.ClientName))
                        {
                             return  new ModelValidationResult[]
                                       {
                                            new ModelValidationResult
                                               {
                                                    // MemberName = "ClientName",
                                                   Message =  " Please enter your name "
                                               }
                                       };
                        }
                         break;
                     case  " Date ":
                         if (appt.Date ==  null || DateTime.Now > appt.Date)
                        {
                             return  new ModelValidationResult[]
                                       {
                                            new ModelValidationResult
                                               {
                                                   MemberName =  "",
                                                   Message =  " Please enter a date in the future "
                                               }
                                       };
                        }
                         break;
                     case  " TermsAccepted ":
                         if (!appt.TermsAccepted)
                        {
                             return  new ModelValidationResult[]
                                       {
                                            new ModelValidationResult
                                               {
                                                   MemberName =  "",
                                                   Message =  " You must accept the terms "
                                               }
                                       };
                        }
                         break;
                }
            }
             return Enumerable.Empty<ModelValidationResult>();
        }
    }

 

注意,上文代码中MemberName不能填写,获取赋值为空,否则error提交到ModelState时,key值会重叠,比如ClientName会成为ClientName.ClientName。AppointmentValidator代码如下:
     public  class AppointmentValidator : ModelValidator
    {
         public AppointmentValidator(ModelMetadata metadata, ControllerContext context)
            :  base(metadata, context)
        {
        }

         public  override IEnumerable<ModelValidationResult> Validate( object container)
        {
            Appointment appt = (Appointment)Metadata.Model;
             if (appt.ClientName ==  " Joe " && appt.Date.DayOfWeek == DayOfWeek.Monday)
            {
                 return  new ModelValidationResult[]
                                       {
                                            new ModelValidationResult
                                               {
                                                   MemberName =  "",
                                                   Message =  " Joe cannot book appointments on Mondays "
                                               }
                                       };
            }

             return Enumerable.Empty<ModelValidationResult>();
        }
    }

做完这些工作,然后就是注册启用CustomerValidationProvider了。在Application_Start中加入:

ModelValidatorProviders.Providers.Add(new CustomValidationProvider());

就完毕了。

      关于CustomerValidationProvider这种方式,作者建议仅用于复杂场合。如:需要从db中动态加载validation rule,或者实现自己的一些验证框架时才使用。这里有一个案例:http://www.codeproject.com/Articles/463900/Creating-a-custom-ModelValidatorProvider-in-ASP-NE 

 

 

      好吧,再看浏览器端的验证。

      第1步先启用客户端验证:

    <add key= " ClientValidationEnabled " value= " true "/>
    <add key= " UnobtrusiveJavaScriptEnabled " value= " true "/>

 或者在Application_Start中增加:

    HtmlHelper.ClientValidationEnabled =  true;
    HtmlHelper.UnobtrusiveJavaScriptEnabled =  true;

还有,view当中确保没有:

HtmlHelper.ClientValidationEnabled =  false;

默认情况下,它是true。如果要禁用,上述3个区域任意一个设置为false即可。

      第2步,view中加载4个必须文件:

    <link href= " @Url.Content( "~/Content/Site.css " ) " rel= " stylesheet " type= " text/css " />
    <script src= " @Url.Content( "~/Scripts/jquery- 1.5. 1.min.js " ) " type= " text/javascript "></script>          

    <script src= " @Url.Content( "~/Scripts/jquery.validate.min.js " ) " type= " text/javascript "></script>
    <script src= " @Url.Content( "~/Scripts/jquery.validate.unobtrusive.min.js " ) " type= " text/javascript "></script>

      第3步,据说最简单的方式是利用meta data属性:

    [AppointmentValidator]
     public  class Appointment
    {
        [Required(ErrorMessage =  " Please enter your name ")]
        [StringLength( 10, MinimumLength =  3, ErrorMessage =  " Please enter a string of whose length is between 3 and 10 ")]
        [EmailAddress]
         public  string ClientName {  getset; }

        [DataType(DataType.Date)]
         [FutureDateValidator(ErrorMessage = "You must enter a date in the future")]
         public DateTime Date {  getset; }

        [MustBeTrue(ErrorMessage =  " You must accept the terms ")]
         public  bool TermsAccepted {  getset; }
    }

其中EmailAddress是新实现的一个可供客户端验证用的metadata属性。如下: 


     public  class EmailAddressAttribute : ValidationAttribute, IClientValidatable
    {
         private  static  readonly Regex emailRegex =  new Regex( " .+@.+\\..+ ");

         public EmailAddressAttribute()
        {
            ErrorMessage =  " Enter a valid email address ";
        }

         public  override  bool IsValid( object value)
        {
             return ! string.IsNullOrEmpty(( string) value) &&
                   emailRegex.IsMatch(( string) value);
        }

         public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
             return  new List<ModelClientValidationRule>
                       {
                            new ModelClientValidationRule
                               {
                                   ValidationType =  " email ",
                                   ErrorMessage =  this.ErrorMessage
                               },
                            // new ModelClientValidationRule
                           
//     {
                           
//         ValidationType = "required",
                           
//         ErrorMessage = this.ErrorMessage
                           
//     }
                       };
        }
    }

 它实现了一个IClientValidatable 接口,所以能够直接在客户端交互。

       关于它的实现原理,它不过是在server端将view上需要验证的全部信息都render并且隐藏在页面,然后基于jQuery的validation组件来交互。 看一下html片段:


< p >
         Your name:  < input  data-val ="true"  data-val-email ="Enter a valid email address"  data-val-length ="Please enter a string of whose length is between 3 and 10"  data-val-length-max ="10"  data-val-length-min ="3"  data-val-required ="Please enter your name"  id ="ClientName"  name ="ClientName"  type ="text"  value =""   />            

         < span  class ="field-validation-valid"  data-valmsg-for ="ClientName"  data-valmsg-replace ="true" ></ span >   

     </ p >

     < p >

        Appointment Date:  < input  class ="text-box single-line"   data-val ="true"  data-val-remote ="&amp;#39;Date&amp;#39; is invalid."  data-val-remote-additionalfields ="*.Date"  data-val-remote-url ="/Appointment/ValidateDate"  data-val-required ="The Date field is required."  id ="Date"  name ="Date"  type ="text"  value ="2012/10/16"   />

         < span  class ="field-validation-valid"  data-valmsg-for ="Date"  data-valmsg-replace ="true" ></ span >
     </ p >

所以,在客户端,其实你可以脱离mvc框架自己来写。如:


$(document).ready( function () {
$('form').validate({
errorLabelContainer: '#validtionSummary',
wrapper: 'li',
rules: {
ClientName: {
required:  true,
}
},
messages: {
ClientName: "Please enter your name"
}
});
});

同时,在view中render时,你也可以按照自己的方式来做。如将原有的ClientName显示方式换为:                    

    <p>               
         Your name: @Html.TextBoxFor(m => m.ClientName,  new { data_val =  " true ",
data_val_email =  " Enter a valid email address ",
                                                data_val_required =  " Please enter your name "})            
        @Html.ValidationMessageFor(m => m.ClientName)   
    </p>

 因为-在C#中是非法变量名字符,所以用_替代,同时asp.net mvc生成html时会将它替换为-。

 

       最后一个问题是,当客户端验证需要使用服务器端资源时,怎么办? 这时就要使用到Remote Validation了。首先,自然是得后端有一个ajax调用的action了:

         public JsonResult ValidateDate( string Date)
        {
            DateTime parsedDate;
             if (!DateTime.TryParse(Date,  out parsedDate))
            {
                 return Json( " Please enter a valid date (mm/dd/yyyy) ", JsonRequestBehavior.AllowGet);
            }
             else  if (DateTime.Now > parsedDate)
            {
                 return Json( " Please enter a date in the future ", JsonRequestBehavior.AllowGet);
            }
             else
            {
                 return Json( true, JsonRequestBehavior.AllowGet);
            }
        }

然后,在Appointment类的Date属性上加个Remote特性:

        [DataType(DataType.Date)]
         // [FutureDateValidator(ErrorMessage = "You must enter a date in the future")]
         [Remote( " ValidateDate " " Appointment " )]
         public DateTime Date {  getset; }

至此,它就完成了。当你输入date结束后,就会调用ValidateDate(string Date)方法。 我在想,这里Appointment得是真正的ViewModel了,要不然就太别扭了。因为它实际上是调用了controller的action方法了。

 

全部源码download 

你可能感兴趣的:(validation)