《Pro ASP.NET MVC 3 Framework》學習筆記之三十一 【模型驗證】


模型驗證是確保接收的數據適合綁定到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:-)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM