(转)《Pro ASP.NET MVC 3 Framework》学习笔记之三十一 【模型验证】

原文地址:http://www.cnblogs.com/mszhangxuefei/archive/2012/05/28/mvcnotes_31.html

 

 

模型验证是确保接收的数据适合绑定到model的这样的一个处理过程,当不适合的时候能够提供一些有用的信息来帮助用户改正他们问题。模型验证可以分为两个部分:1.检查我们接收的数据。2.帮助用户修正问题。非常庆幸的是,MVC框架对模型验证提供可扩展支持,本章会展示基本功能的使用以及阐释一些针对验证过程的高级技术。

添加一个ModelValidation项目
添加一个视图模型Appointment,如下:

View Code
复制代码
using System.Web;
using System.ComponentModel.DataAnnotations;

namespace ModelValidation.Models
{
    public class Appointment
    {
        public string ClientName { get; set; }
        [DataType(DataType.Date)]
        public DateTime Date { get; set; }
        public bool TermsAccepted { get; set; }
    }
}
复制代码

接着添加视图MakeBooking.cshtml,如下:

View Code
复制代码
//视图MakeBooking
@model ModelValidation.Models.Appointment
@{
    ViewBag.Title = "预定会议";
}
<h4>
    预定会议</h4>
@using (Html.BeginForm())
{ 
    @Html.ValidationSummary()
    <p>
        你的名字:@Html.EditorFor(m => m.ClientName)
        @Html.ValidationMessageFor(m => m.ClientName)
    </p>
    <p>
        会议日期:@Html.EditorFor(m => m.Date)
        @Html.ValidationMessageFor(m => m.Date)</p>
    <p>@Html.EditorFor(m => m.TermsAccepted)我接受各项条款
        @Html.ValidationMessageFor(m => m.TermsAccepted)
    </p>
    <input type="submit" value="预定" />
}


//视图Completed
@model ModelValidation.Models.Appointment

@{
    ViewBag.Title = "已确认";
}

<h4>您的会议已经确认</h4>

<p>你的名字: @Html.DisplayFor(m => m.ClientName)</p>
<p>会议日期: @Html.DisplayFor(m => m.Date)</p>

//Model
namespace ModelValidation.Models
{
    public interface IAppointmentRepository
    {
        void SaveAppointment(Appointment app);
    }
}

//Controller
using System.Web.Mvc;
using ModelValidation.Models;

namespace ModelValidation.Controllers
{
    public class AppointmentController : Controller
    {
        private IAppointmentRepository repository;
        public AppointmentController(IAppointmentRepository repo)
        {
            repository = repo;
        }

        public ViewResult MakeBooking()
        {
            return View(new Appointment { Date = DateTime.Now });
        }

        [HttpPost]
        public ViewResult MakeBooking(Appointment appt)
        {
            if (string.IsNullOrEmpty(appt.ClientName))
            {
                ModelState.AddModelError("clientName", "输入你的名字");
            }
            if (ModelState.IsValidField("Date") && DateTime.Now > appt.Date)
            {
                ModelState.AddModelError("Date", "日期不能是过去的");
            }
            if (!appt.TermsAccepted)
            {
                ModelState.AddModelError("TermsAccepted", "你必须接受条款");
            }

            if (ModelState.IsValidField("ClientName") && ModelState.IsValidField("Date")
                && appt.ClientName == "张雪飞" && appt.Date.DayOfWeek == DayOfWeek.Monday)
            {
                ModelState.AddModelError("", "张雪飞 不能在星期一预定会议");
            }

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

    }
}
复制代码

在我们验证了所有的model对象的属性后,可以通过读取ModelState.IsValid属性来检查是否有错误。这个时候运行程序会出现“没有为该对象定义无参数的构造函数。”的错误,因为这里添加了构造器,为了后面使用DI准备的,所以可以先注释掉控制器里面的接口定义和相应的构造器,并且设置下路由的默认值,运行程序就不会报错了。
模版视图的辅助方法会对模型属性验证的错误生成常规的编辑样式,如果有错误报告给了一个属性,辅助方法会添加"input-validation-error"样式到input里面,Site.css里面包含了一个默认的样式如:.input-validation-error {border: 1px solid #ff0000;background-color: #ffeeee;},效果如下:

注:有些浏览器,包括Chrome和FireFox,会忽略对CheckBox的样式。解决的这个问题可以使用自定义的模版Boolean.cshtml替换原来的编辑模版,并且将CheckBox封装在div里面,如下:

View Code
@model bool?
@if (ViewData.ModelMetadata.IsNullableValueType)
{
    @Html.DropDownListFor(m => m, new SelectList(new[] { "未设置", "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);
    }
}

展示验证信息(Displaying Validation Messages)
有很多方便的HTML辅助方法来展现验证的信息,如@Html.ValidationSummary(),在MakeBooking.cshtml添加如下:

View Code
复制代码
@model ModelValidation.Models.Appointment
@{
    ViewBag.Title = "预定会议";
}
<h4>
    预定会议</h4>
@using (Html.BeginForm())
{ 
    @Html.ValidationSummary()
    <p>
        你的名字:@Html.EditorFor(m => m.ClientName)</p>
    <p>
        会议日期:@Html.EditorFor(m => m.Date)</p>
    <p>@Html.EditorFor(m => m.TermsAccepted) 我介绍各项条款</p>
    <input type="submit" value="预定" />
}
复制代码

显示效果如下:


Html.ValidationSummary()还很多重载的版本:
Html.ValidationSummary():生成所有的验证信息
Html.ValidationSummary(bool):如果为true则仅仅展示model级别的验证信息,如果为false,则展示所有
Html.ValidationSummary(string):在展示所有的信息之前,显示string参数的内容。
Html.ValidationSummary(bool, string):综合上面两个
上面有写重载的方法允许我们指定是否只展示model级别 的信息,注册到ModelState里面的错误信息是属性级别 的,这意思是有一个提供的值有问题并且通过改变这个值就可以解决这个问题;相比之下model级别的错误可以用在当有两个或多个属性值交互时产生的一些问题。上面的代码里面有一个简单的场景,就是名叫"张雪飞"的用户不能在星期一预定会议,那么是怎么强制执行这条规则并作为model级别的验证错误信息呈报。代码如下:
if (ModelState.IsValidField("ClientName") && ModelState.IsValidField("Date")
  && appt.ClientName == "张雪飞" && appt.Date.DayOfWeek == DayOfWeek.Monday)
{
      ModelState.AddModelError("", "张雪飞 不能在星期一预定会议");
}
在检查"张雪飞"是否预定会议的时间为星期一之前,我们使用ModelState.IsValidField方法来确保ClientName和Date值是符合要求的,也就是说,在前面两个属性验证通过之前是不会生成model级别的错误的。通过传递一个""的参数给AddModelError方法来注册一个model级别的错误。运行效果如下:

展示属性级别的验证信息(Displaying Property-Level Validation Messages)
我们可能想限制验证信息到model级别的原因是我们能够在字段旁边展示属性级别的错误信息。如下所示:

View Code
@model ModelValidation.Models.Appointment
@{
    ViewBag.Title = "预定会议";
}
<h4>
    预定会议</h4>
@using (Html.BeginForm())
{ 
    @Html.ValidationSummary(true)
    <p>
        你的名字:@Html.EditorFor(m => m.ClientName)
        @Html.ValidationMessageFor(m=>m.ClientName)
    </p>
    <p>
        会议日期:@Html.EditorFor(m => m.Date)
        @Html.ValidationMessageFor(m=>m.Date)
    </p>
    <p>@Html.EditorFor(m => m.TermsAccepted) 我接受各项条款
          @Html.ValidationMessageFor(m=>m.TermsAccepted)
    </p>
    <input type="submit" value="预定" />
}

运行效果如下:


使用其他可选的验证技术(Using Alternative Validation Techniques)
在action方法里面执行model验证仅仅是MVC框架里面验证技术中的一种,下面会介绍其他的方式来进行验证:
在模型绑定中执行验证(Performing Validation in the Model Binder)
默认的model binder执行验证是作为了绑定过程一个部分,看下下面的情况:


可以看见,提示了Date字段是必须的,这个信息就是model binder添加的,因为我们提交的时候该字段为空不能创建一个DateTime对象。model binder对每一个对象的属性执行了一些基本的验证,如果一个值没有提供则会显示上面的信息;如果提供了一个值但是这个值不能转换为模型的属性的类型,也会提示错误,如下:


内置的DefaultModelBinder类,提供了一些非常有用的方法来让我们重写,从而能够添加一个验证到binder。有两个方法是:
OnModelUpdated:当binder试图给所有的模型对象的属性赋值时调用(应用模型元数据定义的验证规则并注册任何错误到ModelState)
SetProperty:当binder想应用一个值到具体的属性时调用(如果属性不能获取一个null或没有值提供的时候,就会提示上面显示的字段是必需的信息,如果值不能转换也会提示相应的无效信息)
下面介绍对这些方法的重写:

View Code
using System;
using System.ComponentModel;
using System.Web.Mvc;
using ModelValidation.Models;

namespace ModelValidation.Infrastructure
{
    public class ValidatingModelBinder : DefaultModelBinder
    {
        protected override void SetProperty(ControllerContext controllerContext,
    ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor,
    object value)
        {
            // 保证调用基类的方法实现
            base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);

            // 实现我们自己的属性级别的验证
            switch (propertyDescriptor.Name)
            {
                case "ClientName":
                    if (string.IsNullOrEmpty((string)value))
                    {
                        bindingContext.ModelState.AddModelError("ClientName",
                            "请输入你的名字");
                    }
                    break;
                case "Date":
                    if (bindingContext.ModelState.IsValidField("Date") &&
                        DateTime.Now > ((DateTime)value))
                    {
                        bindingContext.ModelState.AddModelError("Date",
                            "日期不能是过去的");
                    }
                    break;
                case "TermsAccepted":
                    if (!((bool)value))
                    {
                        bindingContext.ModelState.AddModelError("TermsAccepted",
                            "你必须接受条款");
                    }
                    break;
            }
        }

        protected override void OnModelUpdated(ControllerContext controllerContext,
            ModelBindingContext bindingContext)
        {
            // 保证调用基类的方法实现
            base.OnModelUpdated(controllerContext, bindingContext);

            // 获取Model
            Appointment model = bindingContext.Model as Appointment;

            // 应用model级别的验证
            if (model != null &&
                bindingContext.ModelState.IsValidField("ClientName") &&
                bindingContext.ModelState.IsValidField("Date") &&
                model.ClientName == "张雪飞" &&
                model.Date.DayOfWeek == DayOfWeek.Monday)
            {
                bindingContext.ModelState.AddModelError("",
                    "张雪飞不能在星期一预定会议");
            }
        }
    }
}

model binder验证看起来比实际上的更加复杂,验证逻辑跟我们在Action方法里面写的是完全一样的,在SetProperty方法里面做属性级别的验证,我们通过PropertyDescriptor参数来获取属性名,要赋给来自对象参数属性的值,以及通过BindingContext参数访问ModelState。在OnModelUpdated方法里面做model级别的验证。注意:当使用这种方式时(包括属性基本和model级别),非常重要的就是要调用基类的SetProperty和OnModelUpdated方法实现。如果不这样做,那么我们会失去很多关键性功能的支持,比如使用元数据来验证model。
我们需要在Global里面注册自定义的验证binder,如:ModelBinders.Binders.Add(typeof(Appointment), new ValidatingModelBinder());因为我们将验证的逻辑放到了binder里面,所以前面的action方法就可以简化了如:
[HttpPost]
public ViewResult MakeBooking(Appointment appt)
{
    if (ModelState.IsValid) {
        //repository.SaveAppointment(appt);
        return View("Completed", appt);
    } else {
        return View();
    }
}

使用元数据指定验证规则(Specifying Validation Rules Using Metadata)
MVC支持使用元数据来展现验证规则,使用元数据的好处就是我们的定义的规则能够在所有用到该model的地方生效,这点跟仅仅定义在action方法里面是不同的。验证规则通过内置的DefaultModelBinder检测并强制执行。修改Appointment模型如下:

View Code
using System.ComponentModel.DataAnnotations;

namespace ModelValidation.Models
{
    public class Appointment
    {
        [Required]
        public string ClientName { get; set; }
        [DataType(DataType.Date)]
        [Required(ErrorMessage = "请输入日期")]
        public DateTime Date { get; set; }
        [Range(typeof(bool), "true", "true", ErrorMessage = "你必须接受条款")]
        public bool TermsAccepted { get; set; }
    }
}

上面使用了两种特性Required和Range,内置的验证特性有:Compare Range RegularExpression  Required  StringLength
所有的特性都允许我们通过ErrorMessage指定一个自定义的错误信息,如果不指定,会使用默认的值。这些验证的特性是非常基本的,仅仅让我们做属性级别的验证。即使如此,我们仍然能够使用一些技巧来实现我们想要的东西,例如这里用到的Range:
[Range(typeof(bool), "true", "true", ErrorMessage="你必须接受条款")]
public bool TermsAccepted { get; set; }
我们要确保用户选择了接受条款的CheckBox,这里不能使用Required,因为模版的辅助方法针对bool类型的值生成了一个隐藏的HTML元素来保证即使不选中CheckBox也能获取一个值。为了解决这个问题,我们使用Range特性,提供字符串类型的上限和下限,将两个值都设置成true,这样就创建了一个等效的Required来保证必须选中CheckBox才能通过验证。
注:DataType特性不能被用来验证用户的输入,仅仅在呈现值的时候提供一个暗示(前面章节模型模版里面有介绍),所以,不要期望诸如DataType(DataType.EmailAddress)的特性来强制执行一个具体的格式。

创建自定义的属性验证特性(Creating a Custom Property Validation Attribute)
在上面使用Range特性在再创建一个等效的Required的方式有点笨拙,因为我们不局限于只使用内置的验证特性,可以通过从ValidationAttribute类派生来实现我们自己的验证逻辑,例如:

View Code
using System.ComponentModel.DataAnnotations;

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

上面创建了一个MustBeTrue的特性,重写了IsValid方法。这个方法是binder调用的,传递用户作为参数提供的值。在上面的例子中,验证逻辑非常简单,一个值是bool类型并且为true就能通过验证,然后可以在model里面使用,如:
[MustBeTrue(ErrorMessage="你必须接受条款")]
public bool TermsAccepted { get; set; }
这样的方式显然会比使用Range来的简单吧,使用派生的方式还可以扩展其他的功能。例如:

View Code
using System.ComponentModel.DataAnnotations;

namespace ModelValidation.Infrastructure
{
    public class FutureDateAttribute : RequiredAttribute
    {
        public override bool IsValid(object value)
        {
            return base.IsValid(value)&&value is DateTime &&((DateTime)value>DateTime.Now);
        }
    }
}

同样可以在model里面直接使用:
[DataType(DataType.Date)]
//[Required(ErrorMessage = "请输入日期")]
[FutureDate(ErrorMessage = "日期不能是过去的")]
public DateTime Date { get; set; }

创建模型验证特性(Creating a Model Validation Attribute)
到目前为止,介绍的验证特性都是应用到单个的model属性,也就是说这还是属于属性级别的验证错误。我们可以使用验证整个的model,如下:

View Code
using System.Web.Mvc;
using ModelValidation.Models;
using System.ComponentModel.DataAnnotations;

namespace ModelValidation.Infrastructure
{
    public class AppointmentValidatorAttribute:ValidationAttribute
    {
        public AppointmentValidatorAttribute()
        {
            ErrorMessage = "张雪飞不能在星期一预定会议";
        }

        public override bool IsValid(object value)
        {
            Appointment app = value as Appointment;
            if (app == null || string.IsNullOrEmpty(app.ClientName) || app.Date == null)
            {
                return true;
            }
            else
            {
                return !(app.ClientName == "张雪飞" && app.Date.DayOfWeek == DayOfWeek.Monday);
            }
        }
    }
}

model binder要传递给IsValid方法的object参数将会是Appointment模型对象,必须在模型类上应用整个特性才会生效,例如:

View Code
using System.ComponentModel.DataAnnotations;
using ModelValidation.Infrastructure;

namespace ModelValidation.Models
{
    [AppointmentValidator]
    public class Appointment
    {
        //[Required]
        public string ClientName { get; set; }
        [DataType(DataType.Date)]
        //[Required(ErrorMessage = "请输入日期")]
        [FutureDate(ErrorMessage = "日期不能是过去的")]
        public DateTime Date { get; set; }
        //[Range(typeof(bool), "true", "true", ErrorMessage = "你必须接受条款")]
        [MustBeTrue(ErrorMessage = "你必须接受条款")]
        public bool TermsAccepted { get; set; }
    }
}

运行可以看到效果:



我们model验证特性在属性级别的验证特性通过之前是不会生效的。这个不同于我们在action方法里面直接定义了验证逻辑。这样我们冒了一个风险,就是暴露了用户更正输入的的过程。

定义自验证的模型(Defining Self-validating Models)
另一种验证技术是创建一个自验证的模型,也就是验证逻辑包含在模型里面。可以通过实现IValidatableObject接口指定一个自验证的模型类,如下:

View Code
using System.ComponentModel.DataAnnotations;
using ModelValidation.Infrastructure;

namespace ModelValidation.Models
{
    //[AppointmentValidator]
    public class Appointment : IValidatableObject
    {
        //[Required]
        public string ClientName { get; set; }
        [DataType(DataType.Date)]
        //[Required(ErrorMessage = "请输入日期")]
        //[FutureDate(ErrorMessage = "日期不能是过去的")]
        public DateTime Date { get; set; }
        //[Range(typeof(bool), "true", "true", ErrorMessage = "你必须接受条款")]
        //[MustBeTrue(ErrorMessage = "你必须接受条款")]
        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("请输入你的名字"));
            }
            if (DateTime.Now > Date)
            {
                errors.Add(new ValidationResult("日期不能是过去的"));
            }
            if (errors.Count == 0 && ClientName == "张雪飞" && Date.DayOfWeek == DayOfWeek.Monday)
            {
                errors.Add(new ValidationResult("张雪飞不能在星期一预定会议"));
            }
            if (!TermsAccepted)
            {
                errors.Add(new ValidationResult("你必须接受条款"));
            }
            return errors;
        }
    }
}

IValidatableObject接口定义了一个方法Validate,这个方法需要一个ValidationContext参数。
如果model类实现了IValidatableObject接口,那么Validate方法将会在model binder给每一个model属性赋值以后被调用。这种方式结合了把验证逻辑放在action方法的灵活性,但是却让这个跟每一次模型绑定过程实例化模型类型的实例黏在一起。

创建自定义的验证提供者(Creating a Custom Validation Provider)
还有一种可替代的验证方式就是创建一个自定义的验证提供者,通过从ModelValidationProvider类派生并重写GetValidators方法来实现,例如:

View Code
using System.Web.Mvc;
using ModelValidation.Models;

namespace ModelValidation.Infrastructure
{
    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 AppointmentPropertyValidator(metadata,context)
                };
            }
            return Enumerable.Empty<ModelValidator>();
        }
    }

    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="请输入你的名字"
                            }
                            };
                        }
                        break;
                    case "Date":
                        if (appt.Date == null || DateTime.Now > appt.Date)
                        {
                            return new ModelValidationResult[] { 
                             new ModelValidationResult{
                             MemberName="Date",
                             Message="日期不能是过去的"
                            }
                            };
                        }
                        break;
                    case "TermsAccepted":
                        if (!appt.TermsAccepted)
                        {
                            return new ModelValidationResult[] { 
                            new ModelValidationResult{
                            MemberName="TermsAccepted",
                            Message="你必须接受条款"
                            }
                            };
                        }
                        break;
                }
            }
            return Enumerable.Empty<ModelValidationResult>();
        }
    }
    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 == "张雪飞" && appt.Date.DayOfWeek == DayOfWeek.Monday)
            {
                return new ModelValidationResult[] {
                new ModelValidationResult {
                    MemberName = "",
                    Message = "张雪飞不能预定星期一的会议"
                }};
            }
            else
            {
                return Enumerable.Empty<ModelValidationResult>();
            }
        }

    }

}

GetValidation方法会被每一个model的属性调用一次,然后被model本身调用一次,方法的返回值是一个ModelValidator对象的枚举。每一个返回的ModelValidator对象会被请求去验证属性或者模型。我们可以用任何喜欢的方式来响应GetValidation方法的调用,如果我们不想为某个属性和model提供验证,仅仅只需要返回一个空的枚举就行了。为了阐释验证提供者的功能,我们选择实现一个属性的验证和一个对Appointment类的验证。我通过读取ModelMetadata对象的属性值来判断什么在请求验证。代码在前面已经给出了。这里介绍下关于ModelMetadata类非常有用的属性:
ContainerType:当为一个model属性提供验证时,这个属性返回包含该属性的model的类型。
PropertyName:返回为其提供验证的属性名。
ModelType:当我们为一个model提供验证时,返回model对象的类型。

Tips:接下来的例子仅仅为了阐述自定义的验证提供者怎么嵌入到框架里面。我们不需要在正常的验证场景中使用这个技术,因为metadata特性或IValidatableObject已经足够使用而且比较简单。自定义的验证提供者倾向于在非常高级的场景使用,例如,从数据库动态加载验证规则或者实现我们自己的验证框架等等。

在上面的代码里面有验证整个model的部分稍微有点不同,没有container,所以container参数是null。我们通过Metadata.Model属性获取model并且执行我们的验证,为了报告model级别的验证错误,我们设置了ModelValidationResult对象的MemberName属性为"".

Tips:MVC框架仅仅当没有报告任何属性级别的错误时才会调用我们的model级别的验证。这是非常合理的并且这样假定如果有属性级别的错误,那么是没有进行model级别的验证的。

注册自定义的验证提供者(Registering a Custom Validation Provider)
必须在Global里面注册我们自定义的验证提供者:ModelValidatorProviders.Providers.Add(new CustomValidationProvider());
如果要移除其他的Providers可以这样:ModelValidatorProviders.Providers.Clear();

执行客户端验证(Performing Client-Side Validation)
前面介绍的所有验证技术都是基于服务端的,也就是用户提交数据到服务器,然后服务器发送验证结果给用户。为了提升用户体验可以使用JS做客户端验证,将验证通过的数据发送给服务端。MVC框架支持无入侵式(unobtrusive)的客户端验证,这意味着验证规则会通过添加属性到生成的HTML元素的方式来展现。这些通过包含在MVC框架里面的JS库来执行的。我们会在JavaScript的上下文中广泛遇到这个单词"无入侵(unobtrusive)",这是一个非常宽松的词汇,
包含了三个关键的特征:1.js执行的验证保持跟HTML元素分开,即不用在视图里面包含验证脚本
                               2.验证是渐进增强执行的,即如果用户的浏览器不支持js,那么验证会使用更加简单的方法。例如,如果用户禁用了js,那么服务端的验证会在不给用户
                                  带来任何不便的情况下无缝的执行
                               3.有一序列的最佳实践来平缓浏览器的不一致和行为

下面的部分会展示内置的验证支持原理并阐释如何扩展自定义的客户端验证功能。

Tips:客户端的验证是关注在属性上。实际上也很难设置model级别的客户端验证,为了达到这个目的,大多数MVC3应用程序针对属性级别的情况使用客户端验证,而依靠服务端验证来针对整个model级别。

启用/禁用客户端验证(Enabling and Disabling Client-Side Validation)
客户端验证有Web.config里面的两个设置来控制,如下:
<configuration>
  <appSettings>
    <add key="ClientValidationEnabled" value="true"/> 
    <add key="UnobtrusiveJavaScriptEnabled" value="true"/> 
  </appSettings>
两个属性必须全部为true才能启用客户端验证,创建项目时MVC默认设置为true,还有一种替代的方式如下:
protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    HtmlHelper.ClientValidationEnabled = true;
    HtmlHelper.UnobtrusiveJavaScriptEnabled = true;
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}
我们也可以针对单个的视图启用或禁用客户端验证,下面采用编程的方式来实现:
@model MvcApp.Models.Appointment
@{
    ViewBag.Title = "Make A Booking";
    HtmlHelper.ClientValidationEnabled = false;
}
...
所有这些设置必须为true才能执行客户端验证,也就是任何一个设置为false就禁用了客户端验证。为了配置设置,必须引入js库,如可以在_layout添加如下:

View Code
<!DOCTYPE html> 
<html> 
<head> 
    <title>@ViewBag.Title</title> 
    <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> 
</head> 
<body> 
    @RenderBody() 
</body> 
</html> 

也可以在每个视图里面添加,来决定是否使用客户端验证。添加的顺序非常重要,如果改变了顺序,验证不会执行。

对JS库使用CDN(USING A CDN FOR JAVASCRIPT LIBRARIES)
在上面的例子里面我们是从/Scripts文件夹引入的jQuery库文件,一个可替代的方式就是从微软AjaxCDN加载这些文件,这是微软提供的免费的服务(我没用过还,有用过的同学请留言哈,呵呵 ),有一个在地理上分散的服务器群集并使用离用户最近的服务器来对MVC js库的请求进行服务。使用CDN有很多好处:1.用户通过浏览器加载程序的时间会减少,因为CDN更快也更靠近用户。2.节省了服务器的空间和带宽。jQuery文件通常是MVC程序在传递给浏览器的项目里面占用带宽最大的一项,让浏览器从微软的服务器获取会降低运营成本。为了能够利用CDN的优势,我们需要改变script元素的src属性,如下:
http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.5.1.min.js
http://ajax.aspnetcdn.com/ajax/jquery.validate/1.7/jquery.validate.min.js
http://ajax.aspnetcdn.com/ajax/mvc/3.0/jquery.validate.unobtrusive.min.js
像这样的:<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.5.1.min.js" type="text/javascript"></script>

使用客户端验证(Using Client-Side Validation)
一旦我们启用了客户端验证并且jQuery库也在引入了,我们就可以执行客户端验证了。最简单的方式就是使用元数据特性,例如Required,Range以及StringLength,如下:

View Code
public class Appointment 
{ 
    [Required] 
    [StringLength(10, MinimumLength=3)] 
    public string ClientName { get; set; } 
 
    [DataType(DataType.Date)] 
    [Required(ErrorMessage="请输入日期")] 
    public DateTime Date { get; set; } 
 
    public bool TermsAccepted { get; set; } 
} 

MakeBooking.cshtml如下:

View Code
@model ModelValidation.Models.Appointment
@{
    ViewBag.Title = "预定会议";
}
<h4>
    预定会议</h4>
@using (Html.BeginForm())
{ 
    @Html.ValidationSummary()
    <p>
        你的名字:@Html.EditorFor(m => m.ClientName)
        @Html.ValidationMessageFor(m => m.ClientName)
    </p>
    <p>
        会议日期:@Html.EditorFor(m => m.Date)
        @Html.ValidationMessageFor(m => m.Date)</p>
    <p>@Html.EditorFor(m => m.TermsAccepted)我接受各项条款
        @Html.ValidationMessageFor(m => m.TermsAccepted)
    </p>
    <input type="submit" value="预定" />
}

运行程序可以看看效果,注意要在_layout.cshtml里面或者是视图里面引入两个js才行哦。

理解客户端验证的运行原理(Understanding How Client-Side Validation Works)
使用MVC框架提供的客户端验证的一个好处就是我们不用写任何js脚本,而且,验证规则使用HTML属性来表现。下面是一个通过Html.EditorFor辅助方法呈现的:
<input class="text-box single-line" id="ClientName" name="ClientName"  type="text" value="" />,当开启了验证以后会呈现如下:
<input class="text-box single-line" data-val="true" data-val-length="The field ClientName must be a string with a minimum length of 3 and a maximum length of 10." data-val-length-max="10" data-val-length-min="3" data-val-required="The ClientName field is required."  id="ClientName" name="ClientName" type="text" value="" />
MVC客户端验证支持不生成任何js脚本和json数据导向验证过程,依靠就是MVC的约定。下面介绍下开启验证后多的HTML属性:
首先添加的是data-val:应用验证规则的名字。例如,当我们在model的属性上添加了Required后,生成的就是data-val-required属性,跟属性关联的是值就是规则的错误信息。如data-val-length,显示是与字符串长度有关的信息。MVC客户端验证里面一个非常好的功能是我们指定相同的规则在客户端和服务端进行验证,也就是说如果浏览器不支持javascript,那么跟支持的一样,而不需要我们做其他的任何努力。

MVC客户端验证VS jQuery验证(MVC CLIENT VALIDATION VS. JQUERY VALIDATION)
MVC客户端验证功能是建立jQuery验证库的基础之上,如果我们乐意,可以直接使用jQuery的验证库而忽略MVC的这项功能。这个验证库功能丰富而且灵活,非常值得我们探究。下面是一个关于jQuery验证库的例子:

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

MVC客户端验证功能隐藏了javascript,并且对客户端和服务端都有效。两种方式都可以用在MVC中使用

自定义客户端验证(Customizing Client-Side Validation)
内置的客户端验证非常棒,但只有六种属性供我们使用。jQuery验证库支持很多复杂验证规则并且MVC无入侵式的验证库让我们仅仅需要做一点额外的工作就可以利用。

显示创建验证HTML属性(Explicitly Creating Validation HTML Attributes)
利用额外的验证规则最直接的方式就是手动在view里面生成一个Required属性,如下:
你的名字:@Html.TextBoxFor(m => m.ClientName, new {data_val="true",data_val_email="邮件格式错误",data_val_required="请输入你的名字" })
@Html.ValidationMessageFor(m => m.ClientName)
如果我们也想提供额外的属性,就不能使用模版辅助方法对某个属性生成编辑的HTML,所以使用Html.TextBoxFox代替并且使用可以接收匿名类型的重载版本。

Tips:可能已经注意到前面的HTML的属性名都是用"-"分隔的,但是这在C#里面不符合变量名命中规则的。为了解决这个问题,我们在匿名类型里面指定属性名时用下划线"_"分隔,生成的时候会自动转成"-"分隔的HTML。

上面的View代码生成的HTML如下:
你的名字:<input data-val="true" data-val-email="邮件格式错误" data-val-length="The field ClientName must be a string with a minimum length of 3 and a maximum length of 10." data-val-length-max="10" data-val-length-min="3" data-val-required="请输入你的名字" id="ClientName" name="ClientName" type="text" value="" />

非常有用的jQuery验证规则,如下:
Required  Length  Range  Regex  Equalto  Email  Url  Date  Number  Digits  Creditcard

创建支持客户端验证的模型特性(Creating Model Attributes That Support Client-Side Validation)
添加HTML特性到视图里面是非常简便直接,但是这仅仅用在了客户端。我们可以通过在action方法或模型绑定里面执行同样的验证来满足在服务端也能验证的需求,并且一个非常好用的技术就是创建自定义的验证属性,其运行的原理跟内置的一样,同时触发客户端和服务端验证。下面是一个执行服务端验证Email地址的示例:

View Code
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using System.Web.Mvc;

namespace ModelValidation.Infrastructure
{
    public class EmailAddressAttribute : ValidationAttribute, IClientValidatable
    {
        private static readonly Regex emailRegex = new Regex(".+@.+\\..+");
        public EmailAddressAttribute()
        {
            ErrorMessage = "请输入合法的邮件地址";
        }

        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}
            };
        }
    }
}

这里创建服务端验证特性跟前面的介绍的方式是一样的,从ValidationAttribute派生并重写IsValid方法来实现自己的验证逻辑。为了启用客户端验证,必须实现IClientValidatable接口。应用这个特性就跟内置一样,如:
public class Appointment
{
    [EmailAddress]
    public string ClientName { get; set; }
...
创建自定义的客户端验证规则(Creating Custom Client-Side Validation Rules)
在上面列举的客户端验证规则非常好用,但是还不够全面,如果我们想写更少的js脚本,需要创建自己的规则。在jQuery验证规则的基础上,我们被限制对MVC客户端验证添加支持。本质上,这意味着我们可以扭转一些已经存在的验证规则,但是如果我们想创建更加复杂,那么就必须放弃MVC客户端验证直接使用jQuery。例如,创建一个新的客户端验证规则应用到CheckBoxs上,如下:

View Code
<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <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="http://www.cnblogs.com/Scripts/jquery.validate.min.js" type="text/javascript"></script>
    <script src="http://www.cnblogs.com/Scripts/jquery.validate.unobtrusive.min.js" type="text/javascript"></script>
    <script type="text/javascript">
        jQuery.validator.unobtrusive.adapters.add("checkboxtrue", function (options) {
            if (options.element.tagName.toUpperCase() == "INPUT" && options.element.type.toUpperCase() == "CHECKBOX") {
                options.rules["required"] = true;
                if (options.message) {
                    options.messages["required"] = options.message;
                }
            }
        });
    </script>
</head>
<body>
    @RenderBody()
</body>
</html>

我们创建了一个新的规则:checkboxtrue,来确保一个checkbox被选中。这里已经创建了一个客户端验证规则,我能够创建一个特性来引用它。下面展示了怎样创建一个服务端验证特性来保证一个checkbox被选中。如下:

View Code
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
using System.Collections.Generic;

namespace ModelValidation.Infrastructure
{
    public class MustBeTrueAttribute : ValidationAttribute, IClientValidatable
    {
        public override bool IsValid(object value)
        {
            return value is bool && (bool)value;
        }

        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            return new ModelClientValidationRule[] {
            new ModelClientValidationRule {
                ValidationType = "checkboxtrue",
                ErrorMessage = this.ErrorMessage
            }};
        }
    }
}

这样就可以对bool类型的属性应用该特性。

执行远程验证(Performing Remote Validation)
这是一个调用action方法来执行验证的客户端验证技术。一个非常常用的远程验证的例子就是检查一个用户是否用,比如是否已经存在。其实就是通过Ajax的方式来跟服务端交互。下面在AppointmentController添加一个验证日期的action来说明:

复制代码
        public JsonResult ValidateDate(string Date)
        {
            DateTime parseDate;
            if (!DateTime.TryParse(Date, out parseDate))
            {
                return Json("请输入符合要求的日期(mm/dd/yyyy)",JsonRequestBehavior.AllowGet);
            }
            else if (DateTime.Now>parseDate)
            {
                return Json("日期不能是过去的", JsonRequestBehavior.AllowGet);
            }
            else
            {
                return Json(true, JsonRequestBehavior.AllowGet);
            }
        }
复制代码

支持远程验证的Action方法必须返回JsonResult类型,方法的参数名必须跟验证的字段名相同。这里例子里面是Date。

Tips:我能够利用模型绑定的优势使我们的Action方法的参数成为Datetime对象,但是这样做意味着如果用户输入一个apple,那么我们Action方法就不会被调用。这是因为model binder不能从apple创建一个DateTime对象并且抛出异常。远程验证是没办法抛出异常的,所以它被丢弃了。这样有一个不好的效果就是用户会觉得输入是通过验证的。作为一个生成的规则,对远程验证最好的方式就是接收一个字符串参数并执行任何类型的转换(这里是转换为Datetime)。

我们使用Json方法来展现验证的结果(创建一个Json格式的结果让客户端远程脚本能够转换和处理)。如果我们处理的值符合需求,就传递true,如:
return Json(true, JsonRequestBehavior.AllowGet);
如果这不是我要的值可以返回,错误信息:return Json("日期不能是过去的", JsonRequestBehavior.AllowGet);
在上面两中结果里面,我们必须传递JsonRequestBehavior.AllowGet作为参数,这是因为MVC框架默认不允许Get请求来处理Json。如果不传递这个参数,就不会有任何验证的错误能够传递给客户端。

好了,本章的笔记到这里结束,欢迎路过的朋友留下你们的views:-)

posted on 2012-05-29 17:51  黑子范  阅读(471)  评论(0编辑  收藏  举报

导航