用4種不同的程式設計模式驗證繫結引數

尛沫發表於2014-06-03

ASP.NET MVC採用Model繫結為目標Action生成了相應的引數列表,但是在真正執行目標Action方法之前,為了確保有效性,還需要對繫結的引數實施驗證,針對引數的驗證成為Model繫結。

手工驗證繫結的引數

在定義具體Action方法的時候,對已經成功繫結的引數實施手工驗證無疑是一種最為直接的程式設計方式,接下來我們通過一個簡單的例項來演示如何將引數驗證邏輯實現在對應的Action方法中,並在沒有通過驗證的情況下將錯誤資訊響應給客戶端。我們在一個ASP.NET MVC應用中定義瞭如下一個Person類作為被驗證的資料型別,它的Name、Gender和Age三個屬性分別表示一個人的姓名、性別和年齡。

public class Person  
{  
    [DisplayName("姓名")]  
    public string Name { get; set; }  

    [DisplayName("性別")]  
    public string Gender { get; set; }  

    [DisplayName("年齡")]  
    public int? Age { get; set; }  
} 

接下來我們定義瞭如下一個HomeController。在針對GET請求的Action方法Index中,我們建立了一個Person物件並將其作為Model呈現在對應的View中。另一個支援POST請求的Index方法具有一個Person型別的引數,我們在該Action方法中先呼叫Validate方法對這個輸入引數實施驗證。如果驗證成功(ModeState.IsValid屬性返回True),我們返回一個內容為“輸入資料通過驗證”的ContentResult,否則將此引數作為Model呈現在對應的View中。

public class HomeController : Controller  
{  
    [HttpGet]  
    public ActionResult Index()  
    {  
        return View(new Person());  
    }  

    [HttpPost]  
    public ActionResult Index(Person person)  
    {  
        Validate(person);  

        if (!ModelState.IsValid)  
        {  
            return View(person);  
        }  
        else 
        {  
            return Content("輸入資料通過驗證");  
        }  
    }  

    private void Validate(Person person)  
    {  
        if (string.IsNullOrEmpty(person.Name))  
        {  
            ModelState.AddModelError("Name", "'Name'是必需欄位");  
        }  

        if (string.IsNullOrEmpty(person.Gender))  
        {  
            ModelState.AddModelError("Gender", "'Gender'是必需欄位");  
        }  
        else if (!new string[] { "M", "F" }.Any(  
            g => string.Compare(person.Gender, g, true) == 0))  
        {  
            ModelState.AddModelError("Gender",   
            "有效'Gender'必須是'M','F'之一");  
        }  

        if (null == person.Age)  
        {  
            ModelState.AddModelError("Age", "'Age'是必需欄位");  
        }  
        else if (person.Age > 25 || person.Age < 18)  
        {  
            ModelState.AddModelError("Age", "有效'Age'必須在18到25週歲之間");  
        }  
    }  
} 

如上面的程式碼片斷所示,我們在Validate該方法中我們對作為引數的Person物件的3個屬性進行逐條驗證,如果提供的資料沒有通過驗證,我們會呼叫當前ModelState的AddModelError方法將指定的驗證錯誤訊息轉換為ModelError儲存起來。我們採用的具體的驗證規則如下。

  • Person物件的Name、Gender和Age屬性均為必需欄位,不能為Null(或者空字串)。
  • 表示性別的Gender屬性的值必需是“M”(Male)或者“F”(Female),其餘的均為無效值。
  • Age屬性表示的年齡必須在18到25週歲之間。

如下所示的是Action方法Index對應View的定義,這是一個Model型別為Person的強型別View,它包含一個用於編輯人員資訊的表單。我們直接呼叫HtmlHelper 的擴充套件方法EditorForModel將作為Model的Person物件以編輯模式呈現在表單之中。

@model Person  
<html> 
<head> 
    <title>編輯人員資訊</title> 
</head> 
<body> 
    @using (Html.BeginForm())  
    {   
        @Html.EditorForModel()  
        <input type="submit" value="儲存"/> 
    }  
</body> 
</html> 

使用ValidationAttribute特性

將針對輸入引數的驗證邏輯和業務邏輯定義在Action方法中並不是一種值得推薦的程式設計方式。在大部分情況下,同一個資料型別在不同的應用場景中具有相同的驗證規則,如果我們能將驗證規則與資料型別關聯在一起,讓框架本身來實施資料驗證,那麼最終的開發者就可以將關注點更多地放在業務邏輯的實現上面。實際上這也是ASP.NET MVC的Model驗證系統預設支援的程式設計方式。當我們在定義資料型別的時候,可以在型別及其資料成員上面應用相應的ValidationAttribute特性來定義預設採用的驗證規則。

“System.ComponentModel.DataAnnotations”名稱空間定義了一系列具體的ValidationAttribute特性型別,它們大都可以直接應用在自定義資料型別的某個屬性上對目標資料成員實施驗證。這些預定義驗證特性不是本章論述的重點,我們會在“下篇”中對它們作一個概括性的介紹。

常規驗證可以通過上面列出的這些預定義ValidationAttribute特性來完成,但是在很多情況下我們需要通過建立自定義的ValidationAttribute特性來解決一些特殊的驗證。比如上面演示例項中針對Person物件的驗證中,我們要求Gender屬性指定的表示性別的值必須是“M/m”和“F/f”兩者之一,這樣的驗證就不得不通過自定義的ValidationAttribute特性來實現。

針對 “某個值必須在指定的範圍內”這樣的驗證規則,我們定義一個DomainAttribute特性。如下面的程式碼片斷所示,DomainAttribute具有一個IEnumerable型別的只讀屬性Values提供了一個有效值列表,該列表在建構函式中被初始化。具體的驗證實現在重寫的IsValid方法中,如果被驗證的值在這個列表中,則視為驗證成功並返回True。為了提供一個友好的錯誤訊息,我們重寫了方法FormatErrorMessage。

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field,   AllowMultiple = false)]  
public class DomainAttribute : ValidationAttribute  
{  
    public IEnumerable<string> Values { get; private set; }  

    public DomainAttribute(string value)  
    {  
        this.Values = new string[] { value };  
    }  

    public DomainAttribute(params string[] values)  
    {  
        this.Values = values;  
    }  

    public override bool IsValid(object value)  
    {  
        if (null == value)  
        {  
            return true;  
        }  
        return this.Values.Any(item => value.ToString() == item);  
    }  

    public override string FormatErrorMessage(string name)  
    {  
        string[] values = this.Values.Select(value => string.Format("'{0}'",  value)).ToArray();  
        return string.Format(base.ErrorMessageString, name,string.Join(",",   values));  
    }  
} 

由於ASP.NET MVC在進行引數繫結的時候會自動提取應用在目標引數型別或者資料成員上的ValidationAttribute特性,並利用它們對提供的資料實施驗證,所以我們不再需要像上面演示的例項一樣自行在Action方法中實施驗證,而只需要在定義引數型別Person的時候應用相應的ValidationAttribute特性將採用的驗證規則與對應的資料成員相關聯。

如下所示的是屬性成員上應用了相關ValidationAttribute特性的Person型別的定義。我們在三個屬性上均應用了RequiredAttribute特性將它們定義成必需的資料成員,Gender和Age屬性上則分別應用了DomainAttribute和RangeAttribute特性對有效屬性值的範圍作了相應限制。

public class Person  
{  
[DisplayName("姓名")]  
[Required(ErrorMessageResourceName = "Required",    ErrorMessageResourceType = typeof(Resources))]  
public string Name { get; set; }  

[DisplayName("性別")]  
[Required(ErrorMessageResourceName = "Required",   ErrorMessageResourceType = typeof(Resources))]  
[Domain("M", "F", "m", "f", ErrorMessageResourceName = "Domain",  ErrorMessageResourceType = typeof(Resources))]  
public string Gender { get; set; }  

[DisplayName("年齡")]  
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(Resources))]  
[Range(18, 25, ErrorMessageResourceName = "Range",  ErrorMessageResourceType = typeof(Resources))]  
public int? Age { get; set; }  
} 

由於ASP.NET MVC會自動提取應用在繫結引數型別上的ValidationAttribute特性對繫結的引數實施自動化驗證,所以我們根本不需要在具體的Action方法中來對引數作手工驗證。如下面的程式碼片斷所示,我們在Action方法Index中不再顯式呼叫Validate方法,但是執行該程式並在輸入不合法資料的情況下提交表單後依然會得到如圖1所示的輸出結果。

public class HomeController : Controller  
{  
    //其他成員  
    [HttpPost]  
    public ActionResult Index(Person person)  
    {  
        if (!ModelState.IsValid)  
        {  
            return View(person);  
        }  
        else 
        {  
            return Content("輸入資料通過驗證");  
        }  
    }  
} 

讓資料型別實現IValidatableObject介面

除了將驗證規則通過ValidationAttribute特性直接定義在資料型別上並讓ASP.NET MVC在進行引數繫結過程中據此來驗證引數之外,我們還可以將驗證操作直接定義在資料型別中。既然我們將驗證操作直接實現在了資料型別上,意味著對應的資料物件具有“自我驗證”的能力,我們姑且將這些資料型別稱為“自我驗證型別”。這些自我驗證型別是實現了具有如下定義的介面IValidatableObject,該介面定義在“System.ComponentModel.DataAnnotations”名稱空間下。

public interface IValidatableObject  
{  
    IEnumerable<ValidationResult> Validate(  ValidationContext validationContext);  
} 

如上面的程式碼片斷所示,IValidatableObject介面具有唯一的方法Validate,針對自身的驗證就實現在該方法中。對於上面演示例項中定義的資料型別Person,我們可以按照如下的形式將它定義成自我驗證型別。

public class Person: IValidatableObject  
{  
    [DisplayName("姓名")]  
    public string Name { get; set; }  

    [DisplayName("性別")]  
    public string Gender { get; set; }  

    [DisplayName("年齡")]  
    public int? Age { get; set; }  

    public IEnumerable<ValidationResult> Validate( ValidationContext validationContext)  
    {  
        Person person = validationContext.ObjectInstance as Person;  
        if (null == person)  
        {  
            yield break;  
        }  
        if(string.IsNullOrEmpty(person.Name))  
        {  
            yield return new ValidationResult("'Name'是必需欄位", new string[]{"Name"});  
        }  

        if (string.IsNullOrEmpty(person.Gender))  
        {  
            yield return new ValidationResult("'Gender'是必需欄位", new string[] { "Gender" });  
        }  
        else if (!new string[]{"M","F"}.Any( g=>string.Compare(person.Gender,g, true) == 0))  
        {  
            yield return new ValidationResult("有效'Gender'必須是'M','F'之一",   new string[] { "Gender" });  
        }  

        if (null == person.Age)  
        {  
            yield return new ValidationResult("'Age'是必需欄位",    new string[] { "Age" });  
        }  
        else if (person.Age > 25 || person.Age < 18)  
        {  
            yield return new ValidationResult("'Age'必須在18到25週歲之間",    new string[] { "Age" });  
        }              
    }  
} 

如上面的程式碼片斷所示,我們讓Person型別實現了IValidatableObject介面。在實現的Validate方法中,我們從驗證上下文中獲取被驗證的Person物件,並對其屬性成員進行逐個驗證。如果資料成員沒有通過驗證,我們通過一個ValidationResult物件封裝錯誤訊息和資料成員名稱(屬性名),該方法最終返回的是一個元素型別為ValidationResult的集合。

本文為Anyforweb技術分享部落格,需要了解網站建設及更多Web應用相關資訊,請訪問anyforweb.com。

相關文章