通過擴展改善ASP.NET MVC的驗證機製[使用篇]
ASP.NET MVC提供一種基於元數據的驗證方式是我們可以將相應的驗證特性應用到作為Model實體的類型或者屬性/字段上,但是這依然具有很多的不足。在這篇文章中,我結合EntLib的VAB(Validation Application Block)的一些思想通過擴展為ASP.NET MVC提供一種更為完善的驗證機製。[源代碼從這裏下載]
目錄:
一、擴展旨在解決怎樣的驗證問題
二、一個簡單的消息維護組件
三、多語言的支持
四、基於某個驗證規則的驗證
五、驗證規則的一致性
這個基於驗證的擴展可以實現如下幾個ASP.NET MVC無法實現驗證問題:
- 目前我們可以通過“硬編碼”和“資源文件”兩種驗證錯誤消息的提供機製,但是如果能夠提供一種獨立的機製來提供驗證的錯誤消息無疑是一種更好的選擇。原因很簡單,驗證消息是呈現給最終的用戶的,應該是可以單獨進行維護的,當我們發現某個驗證消息不夠友好,應該以一種對現有應用毫無影響的方式進行修改。此外,消息的定義最好是基於“模板”,模板中定義相應的占位符,這樣可以省去很多冗餘消息的定義。比如對於某個區間的驗證消息就可以定義成“{0}必須在{1}與{2}之間”;
- 和ASP.NET MVC基於資源文件(所有的ValidationAttribute可以通過指定屬性Name和ResourceType使我們可以在資源文件中定義相應的消息)不同,消息模板對多語言的支持可以通過獨立的消息維護組件/框架來解決,但是我們需要解決用於替換占位符的參數的多語言支持;
- 對於同一個實體對象,在不同的場景中具有不同的驗證規則。比如說我們做一個招聘網站,針對不同工作崗位對應聘者的性別、年齡、學曆、身高和體重等屬性的要求都是不一樣的,所以我們應該針對基於工作崗位的驗證場景定義不同的驗證規則,並針對某個具體的驗證規則對實體對象實施驗證。
為了演示消息提供機製的分離,我們定義了一個簡單的消息維護組件MessageManager。如下麵的代碼所示,抽象類MessageManager具有唯一的FormatMessage方法用於獲取一個經過格式化好的最終消息文本,參數category、id和args分別代表對應消息條目的類型、ID和作為替換占位符的參數。
1: public abstract class MessageManager
2: {
3: public abstract string FormatMessage(string category, string id, params object[] args);
4: }
我們定義了如下一個默認的DefaultMessageManager,它維護了一組代表消息條目的MessageEntry列表,而MessageEntry是支持多語言的。在重寫的FormatMessage方法中,直接通過類型和ID在列表中找到相應的MessageEntry,並傳輸占位符參數根據當前線程的CurrentUICulture對消息文本進行格式。從如下的代碼可以看出,我們僅僅定義了一個表示“必需字段”的消息,在en-US和zh-CN這兩種語言文化下的文本分別是“{0} is mandatory!”和“請輸入{0}!”。該MessageEntry得類型和ID分別是“Validation”和“MandatoryField”。
1: public class DefaultMessageManager : MessageManager
2: {
3: public DefaultMessageManager()
4: {
5: var messages = new List<MessageEntry>();
6: var messageEntry = new MessageEntry("Validation", "MandatoryField");
7: messageEntry.AddMessageText("{0} is mandatory!", new CultureInfo("en-US"));
8: messageEntry.AddMessageText("請輸入{0}!", new CultureInfo("zh-CN"));
9: messages.Add(messageEntry);
10: this.Messages = messages;
11: }
12:
13: public IEnumerable<MessageEntry> Messages { get; private set; }
14: public override string FormatMessage(string category, string id, params object[] args)
15: {
16: MessageEntry messageEntry = (from message in this.Messages
17: where message.Category == category && message.Id == id
18: select message).FirstOrDefault();
19: if (null == messageEntry)
20: {
21: throw new Exception("...");
22: }
23:
24: return messageEntry.Format(args);
25: }
26: }
我們並沒有列出MessageEntry的定義,有興趣的朋友可以下載本例的源代碼。最終我們定義了如下靜態工廠MessageManagerFactory來創建相應的MessageManager,簡單起見,我們直接創建上述的DefaultMessageManager。
1: public static class MessageManagerFactory
2: {
3: public static MessageManager GetMessageManager()
4: {
5: return new DefaultMessageManager();
6: }
7: }
在本篇文章中我們不談具體實現,隻談具體的使用方法。我們以登錄場景為例,如下所示的LoginInfo類型表示包含代表用戶名和密碼的Model類型。應用在屬性上的RequiredValidatorAttribute特性是我們自定義的ValidationAttribute,它實現了RequiredAttribute一樣的驗證功能。以應用在UserName屬性上的RequiredValidatorAttribute為例([RequiredValidator("Validation", "MandatoryField", "用戶名", Name = "RequiredValidator", Culture = "zh-CN")]),構造函數參數分別代表通過MessageManager維護的對應消息條目的類型(Validation)、ID(MandatoryField)以及占位符參數(用戶名)。Culture屬性則代表對應的語言文化,如果沒有對該屬性進行顯式指定,則代表“語言文化中性”的驗證器。
1: public class LoginInfo
2: {
3: [Display(ResourceType = typeof(Resources), Name = "UserName")]
4: [RequiredValidator("Validation", "MandatoryField", "User Name")]
5: [RequiredValidator("Validation", "MandatoryField", "用戶名", Culture = "zh-CN")]
6: public string UserName { get; set; }
7:
8: [RequiredValidator("Validation", "MandatoryField", "Password")]
9: [RequiredValidator("Validation", "MandatoryField", "密碼", Culture = "zh-CN")]
10: [DataType(DataType.Password)]
11: [Display(ResourceType = typeof(Resources), Name = "Password")]
12: public string Password { get; set; }
13: }
在進行驗證器的選擇的過程中,總是會根據當前線程的CurrentUICulture選擇相匹配的驗證器。如果找不到完全匹配的驗證器,則會選擇語言文化中性驗證器(這樣的驗證器隻允許有一個)。對於本例來說,如果當前的語言文化為zh-CN,那麼隻有應用在UserName和Password屬性上Culture屬性為zh-CN的RequiredValidatorAttribute有效,而在其他的語言文化環境中則會選擇沒有對Culture屬性進行顯式設置的RequiredValidatorAttribute。我們來看看用於進行用戶登錄的AccountController的定義:
1: public class AccountController : BaseController
2: {
3: public ActionResult SignIn()
4: {
5: return View(new LoginInfo());
6: }
7: [HttpPost]
8: public ActionResult SignIn(LoginInfo logInfo)
9: {
10: if (ModelState.IsValid)
11: {
12: return this.View();
13: }
14: else
15: {
16: return this.View();
17: }
18: }
19: }
下麵是SignIn操作默認的View的所有內容:
1: @using Artech.Mvc.Validation.Properties
2: @using Artech.Mvc.Validation.Models
3: @model LoginInfo
4:
5: @{
6: ViewBag.Title = "SignIn";
7: }
8: @Html.ValidationSummary()
9: @using(Html.BeginForm())
10: {
11: @Html.EditorForModel()
12: <input type="submit" value="@Resources.SignIn"/>
13: }
在我們的例子中語言的設置是通過URL來體現的,為了我們在Global.asax中進行了如下的路由映射,即controller之前的部分代表語言文化代碼,默認為zh-CN。
1: public class MvcApplication : System.Web.HttpApplication
2: {
3: public static void RegisterGlobalFilters(GlobalFilterCollection filters)
4: {
5: filters.Add(new HandleErrorAttribute());
6: }
7:
8: public static void RegisterRoutes(RouteCollection routes)
9: {
10: //...
11: routes.MapRoute(
12: "Default", // Route name
13: "{culture}/{controller}/{action}/{id}", // URL with parameters
14: new {culture="zh-CN", controller = "Account", action = "SignIn", id = UrlParameter.Optional } // Parameter defaults
15: );
16:
17: }
18:
19: protected void Application_Start()
20: {
21: //...
22: RegisterRoutes(RouteTable.Routes);
23: }
24: }
運行我們的程序並在分別以en-US和zh-CN訪問主頁,在沒有輸入用戶名和密碼的情況下將會得到如下的驗證消息。
現在我們來演示基於某個驗證規則的驗證方式。對於登錄,我們都應該有這樣的體會,在開發階段為了測試的時候避免頻繁地輸入用戶名和密碼,我們會設置一個默認的密碼。在這裏我們可以通過定義驗證規則來屏蔽對密碼的驗證。為此我們我們對應用在LoginInfo的Password屬性上的RequiredValidatorAttribute特性稍加改動,對其RuleName屬性進行了顯式設置(RuleName = "Production"),意味著隻有當前驗證規則為“Production”(產品階段)的時候,基於它們的驗證才會生效。
1: public class LoginInfo
2: {
3: //...
4: [RequiredValidator("Validation", "MandatoryField", "Password",RuleName = "Production")]
5: [RequiredValidator("Validation", "MandatoryField", "密碼", Culture = "zh-CN", RuleName = "Production")]
6: [DataType(DataType.Password)]
7: [Display(ResourceType = typeof(Resources), Name = "Password")]
8: public string Password { get; set; }
9: }
而對當前采用怎樣地驗證規則,則可以在Controller或者Action方法上應用我們自定義的RuleNameAttribute來設定。如下麵的代碼片斷所示,我們在AccountController上直接應用了RuleNameAttribute特性並將當前的驗證規則設置為“Dev”(開發階段)。
1: [ValidationRule("Dev")]
2: public class AccountController : BaseController
3: {
4: //...
5: }
那麼在程序運行的時候就不會對密碼進行任何驗證,這可以通過如下的截圖可以看出來:
如果我們通過應用在AccountController上的RuleNameAttribute將驗證規則設置為“Production”
1: [ValidationRule("Production")]
2: public class AccountController : BaseController
3: {
4: //...
5: }
那麼針對密碼的驗證就會生效了:
值得一提的是:我們擴展的驗證體係依然也為客戶端認證提供支持,但是在進行基於驗證規則的驗證是確有一個小小的機關。同樣以AccountController的兩個SignIn操作為例,進行客戶端驗證的規則是基於第一個SignIn操作(HttpGet)生成的,服務端驗證則是基於第二個SignIn操作(HttpPost)的驗證規則進行的,如果我們將RuleNameAttribute應用到兩個SignIn操作上,比如確保它們的規則名稱一致方能保證客戶端驗證和服務端認證的一致性。
1: public class AccountController : BaseController
2: {
3: [ValidationRule("Production")]
4: public ActionResult SignIn()
5: {
6: //...
7: }
8: [HttpPost]
9: [ValidationRule("Production")]
10: public ActionResult SignIn(LoginInfo logInfo)
11: {
12: //...
13: }
14: }
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-26 14:04:42