原文: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 { get; set; }        [DataType(DataType.Date)]        public DateTime Date { get; set; }        public bool TermsAccepted { get; set; }    }

可以看到,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 > )    {    <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 { get; set; }        [DataType(DataType.Date)]        [FutureDateValidator(ErrorMessage = "You must enter a date in the future")]        public DateTime Date { get; set; }        //[Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the terms")]        [MustBeTrue(ErrorMessage = "You must accept the terms")]        public bool TermsAccepted { get; set; }    }

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

    public class Appointment : IValidatableObject    {        public string ClientName { get; set; }        [DataType(DataType.Date)]        public DateTime Date { get; set; }        public bool TermsAccepted { get; set; }        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 ==  && 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..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(, MinimumLength = , ErrorMessage = "Please enter a string of whose length is between 3 and 10")]        [EmailAddress]        public string ClientName { get; set; }        [DataType(DataType.Date)]        [FutureDateValidator(ErrorMessage = "You must enter a date in the future")]        public DateTime Date { get; set; }        [MustBeTrue(ErrorMessage = "You must accept the terms")]        public bool TermsAccepted { get; set; }    }

其中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 { get; set; }

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

全部源码download

Model Validation in Asp.net MVC的更多相关文章

  1. Model Validation in ASP.NET Web API

    Model Validation in ASP.NET Web API 原文:http://www.asp.net/web-api/overview/formats-and-model-binding ...

  2. [转]Creating an Entity Framework Data Model for an ASP.NET MVC Application (1 of 10)

    本文转自:http://www.asp.net/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/creating-a ...

  3. Model Validation in ASP.NET Web API By Mike Wasson|July 20, 2012 268 of 294 people found this helpful

    using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using ...

  4. manually Invoking Model Binding / Model Binding /Pro asp.net mvc 5

    限制绑定器 数据源

  5. ASP.NET MVC模型绑定的6个建议(转载)

    ASP.NET MVC模型绑定的6个建议 发表于2011-08-03 10:25| 来源博客园| 31 条评论| 作者冠军 validationasp.netmvc.netasp 摘要:ASP.NET ...

  6. [转]ASP.NET MVC 2: Model Validation

    本文转自:http://weblogs.asp.net/scottgu/archive/2010/01/15/asp-net-mvc-2-model-validation.aspx?CommentPo ...

  7. 转载:Unobtrusive JavaScript in ASP.NET MVC 3 隐式的脚本在MVC3

    Unobtrusive JavaScript 是什么? <!--以下是常规Javascript下写出来的Ajax--> <div id="test"> &l ...

  8. [引]ASP.NET MVC 4 Content Map

    本文转自:http://msdn.microsoft.com/en-us/library/gg416514(v=vs.108).aspx The Model-View-Controller (MVC) ...

  9. ASP.NET MVC Framework

    ASP.NET MVC Framework是微软在ASP.NET中所添加的一组类库,这组类库可以使用Model-View-Controller的设计模式来开发ASP.NET的应用程序.它与现有的ASP ...

随机推荐

  1. 自己动手写CPU之第八阶段(4)——转移指令实现过程2

    将陆续上传本人写的新书<自己动手写CPU>,今天是第36篇,我尽量每周四篇 开展晒书评送书活动,在亚马逊.京东.当当三大图书站点上,发表<自己动手写CPU>书评的前十名读者,均 ...

  2. iOS当该装置是水平屏,frame和bounds分别

    project那里有两个ViewControllers.间ViewController它是root view controller,红色背景,有一个顶button,点击加载后GreenViewCont ...

  3. executeBatch()相关操作汇总

    环境:oracle使用PreparedStatement的executeBatch方法,如果DML操作成功,返回值[-2,-2,...]an array of update counts contai ...

  4. UVa 10397 Connect the Campus

    最小生成树 Kruskal #include<cmath> #include<iostream> #include<cstdio> #include<algo ...

  5. 渲染优化 之fixed与返回顶部 以及开启GPU Hack

    fixed元素,常见网站右侧出现一个返回顶部的按钮,滚动的时候,会发现返回顶部这个区域在不停的进行重绘,而返回顶部是position:fixed定位的.这也解释了为什么fixed定位是最耗性能的属性之 ...

  6. java离request获取当前从访问完成url至

    request.getHeader("REFERER") 得到的完整路径到原始访问路径,其他参数 版权声明:本文博主原创文章.博客,未经同意不得转载.

  7. HDInsight HBase概观

    HDInsight HBase概观 什么是HBase的? HBase它是基于HadoopApache开源NoSQL数据库.它提供了很多非结构化和半结构化数据一致性的随机存取能力的.它是仿照谷歌的Big ...

  8. tc-SRM-626-DIV1-250

    枚举alice投掷骰子得到的结果的每一种情况极其数量. 枚举bob投掷骰子得到的结果的每一种情况极其数量. 然后枚举alice投掷骰子得到的结果的数量和bob投掷骰子比alice低的数量. 然后计算结 ...

  9. Unity 4.5.2 for Mac 下载+安装+破解

    因为课程须要, 须要安装unity, 就捣腾了一下, 顺便Mark. 须要准备的资源: Unity 4.5.2官方安装包 : http://netstorage.unity3d.com/unity/u ...

  10. Microsoft.AlphaImageLoader过滤评论

    Microsoft.AlphaImageLoader是IE滤镜的一种,其主要作用就是对图片进行透明处理.尽管FireFox和IE7以上的IE浏览器已经支持透明的PNG图片,可是就IE5-IE6而言还是 ...