• 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" /> 
    }

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

     

    如果不想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 

  • 相关阅读:
    记一个centos分区大小调整过程
    破解StarUML3.01最新版 for Linux(Ubuntu16LTS)
    为什么我们要使用int类型来保存时间类型的数据。
    sphinx-doc的中文搜索
    ubuntu下file_get_contents返回空字符串
    PSR-PHP开发规范(本文版权归作者:luluyrt@163.com)
    PHP单例模式
    PHP中 PCRE正则表达式模式修饰符“u” 的使用。
    Mysql 插入时间时报错Incorrect datetime value: '' for column 'createtime'
    如何给list清空
  • 原文地址:https://www.cnblogs.com/Langzi127/p/2724885.html
Copyright © 2020-2023  润新知