這篇博客真是乾貨,幹得估計還有點“磕牙”,所以還提供視頻和代碼。但基礎稍弱的同學,怕還是得自行補充一些基礎知識——就一篇文章,確實沒辦法面面俱到。 視頻和代碼下載:Demo - 百度雲盤 · 一起幫 參考原文:Automatic ModelState validation in ASP.NET MV ...
這篇博客真是乾貨,幹得估計還有點“磕牙”,所以還提供視頻和代碼。但基礎稍弱的同學,怕還是得自行補充一些基礎知識——就一篇文章,確實沒辦法面面俱到。
- 視頻和代碼下載:Demo - 百度雲盤 · 一起幫
- 參考原文:Automatic ModelState validation in ASP.NET MVC(英文強,基礎好的同學可以直接看原文了)。
緣起
我忘了是不是在園子里講過,我命名為“截斷式編程”的寫法。其主要目的,就是把簡單的、過濾條件、“非主幹的”邏輯放在最前面。比如在ASP.NET MVC的Action中,處理POST時,我們通常都要進行服務端驗證,於是我們就可以這樣寫:
[HttpPost] public ActionResult Send(MessageSendModel model) { #region 非截斷式寫法 //if (ModelState.IsValid) //{ // //假設發送了一個消息 // Response.Write("消息已經發送"); //} //return View(model); #endregion #region 截斷式編程 //過濾條件 if (!ModelState.IsValid) { return View(model); } //主幹程式:假設發送了一個消息 Response.Write("消息已經發送"); return RedirectToAction("Send"); #endregion }View Code
由於採用了這種寫法,我們很快就發現了一個問題:if (!ModelState.IsValid) { return View(model); }到處都是。
是不是有點“壞味道”的感覺?你是不是想怎麼“弄”它一下?
ActionFilter
首先想到的,當然就是ActionFilter了:在Action執行之前,用一個Filter進行檢查,不就OK了嗎?
我覺得這個想法不錯,但是,但是,請註意,一定要問一個為什麼!為什麼別人想不到呢?——這怎麼可能?!
所以,在自己動手之前,養成習慣,google/bing一下,看看別人是怎麼弄的。
果不其然,找到一篇博客::Automatic ModelState validation in ASP.NET MVC ,和我的思路一模一樣!\(^o^)/
而且,他想得比我更周全!看得我那個興奮啊……
所以,這裡我們得到的第一個經驗:動手之前先搜一搜,不要重覆造輪子。
再引申開一點,
英文 + google = 偉大的程式員。
至少在目前,以及可預見的將來,對於開發人員而言,英語非常重要,非常重要,重要性怎麼強調都不為過。大家可以試一下,有沒有中文的類似的博客資料等,我沒去試。但根據我的經驗,相比於英文資料,中文資料是非常匱乏的。
關於使用搜索引擎,很多同學覺得這是一種“可恥的行為”,但其實不然。你一定要明白:你的目的是解決問題,而不是炫技,非得把什麼東西都記在腦子裡,非得什麼都自己寫出來……算了,這話可能很多人不接受,篇幅有限,懶得說了。懂的人一點就通,不懂的人你怎麼說都沒用。
最簡單的情形
一開始解決方案還是比較簡單的。我就直接放代碼了:
public class ValidateModelStateAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { if (!viewData.ModelState.IsValid) { filterContext.Result = new ViewResult(); } } }View Code
理解的難點大概就在於為什麼:return View(); 和 filterContext.Result = new ViewResult(); 是等價的。
我覺得有這個問題的根源還是沒有理解“面向對象”,這一直是.NET陣營程式員所缺乏的。
一起幫的QQ群里有時候就有同學犯迷糊,聊了之後,我就明白了,他始終把return View();認為是“轉到那個View頁面”的意思,而沒有能夠理解成:這就是一個方法,返回的是一個ActionResult對象。大家能明白我的意思吧?所有的Action都是一個方法,一個返回ActionResult對象的方法,然後ASP.NET MVC框架根據這個ActionResult對象,找到相應的View進行呈現(Render)。這中間多了一個環節,但這不是“脫了褲子放屁”,是非常有必要的,而且非常“精妙”的一個架構設計。
你看,這樣Filter里就可以通過給filterContext.Result賦值而達到頁面呈現的效果。當然,這麼做最大的好處還在於UI層的“可測試化”,這又是一個非常龐雜的話題,此處先略過。
多說一句,一定要說這一句:感謝這位同學提出問題,讓我知道作為初學者,那些地方是難點。話說,我做直播這麼久,要收到點反饋可真難啊!
然而,這裡有一個問題
如果沒有bing(話說不能google真特麽的悲劇)一下,我肯定就到此為止了。然而,高手就是高手,Ben Foster想到了另外一個問題:
假設頁面中需要由後臺傳來的數據才能正常呈現時,如何在Filter里賦值,再傳遞給View?
這樣說有點暈,所以我才專門弄了一個Demo,用DropdownList做例子。DropdownList的可選項數據是從後臺獲取的,由於HTTP的“無狀態協議”特征,POST不能“繼承”GET時獲取的數據,咋辦呢?
TempData解圍
很多種辦法,Ben Foster的博客里是封裝一個方法,GET和POST都調用。
我以前項目,是利用的MVC中的TempData,在兩個請求見共用數據。這樣,如果可選項數據是從資料庫獲取的,可以減少一次查詢,有一點點性能上的提高。
TempData真是一個非常好的東西,這就是我喜歡.NET的原因,它非常的“貼心”,給人的感覺是“始終面向一線開發人員”的,它知道你在寫代碼的時候可能會遇到什麼問題,從而事先給你準備好解決方案。
思路和實現都很簡單:GET的時候,就把POST需要的數據存到TempData中;POST的時候,就把TempData中的數據取出來(需要一個強制轉換,因為數據是存放為Object類型的)。
代碼就不貼了,因為這不是最終的解決方案。
但FilterAction里腫麽辦
在Controller裡面你怎麼玩都行,但我們現在要“封裝”啊,我們要在Filter里解決這個問題啊!
傻眼了。關鍵的問題在於我們需要在OnActionExecuting(Action執行之前)進行驗證,而此時Action的ViewModel並沒有生成,我們可以從ControllerBase.ActionParameters 中取值,但取出來的是一個Object類型,你不知道要把它轉換成什麼類型的(當然你可以用反射做,但非常複雜,複雜到你都覺得沒有必要),TempData也是一樣的問題。
每次這個時候,我就會想起ASP.NET WebForm中的ViewState來,那些年認為它是“性能殺手”,對它口誅筆伐。MVC的誕生並流行,估計和這玩意就有非常大(多大呢?我亂說的,三成吧)的關係。
但現在MVC沒ViewState了,要自己處理,呵呵,又有點懷戀以前WebForm開發的“便捷”了。
再展開來說,DateSet,Linq to SQL,Entity Framework……一系列的技術,一經推出,都是吵吵嚷嚷,不可開交。其實何必呢,各有各的用處,各有各的適用場景,脫離了具體的業務要求,能爭出個什麼高下來?
所以我一直強調,軟體工程,技術選擇以解決問題為標準。問題千差萬別,所以使用的技術不可能整齊劃一,一句話,“沒有銀彈”。
PRG模式
如果是我的話,這時候就放棄了。但Ben Foster給出了一個非常棒的解決方案:利用PRG模式。
PRG是Post, Redirect, Get的縮寫,意思是所有的POST請求,都Redirect到GET的Action,哪怕返回的實際上是同一個頁面。示例代碼如下:
[HttpPost] public ActionResult Send(MessageSendModel model) { //主幹程式:假設發送了一個消息 Response.Write("消息已經發送"); //註意:不是return View() //1、是Redirect //2、重定向的這個Action是和自己同名的,都是Send return RedirectToAction("Send"); }View Code
我非常欣慰的是我們的系統架構,一開始就是按照這個原則搭建的。最早接觸PRG模式的時候,我也是有點迷迷糊糊的,但想到大家都這樣用,而且看起來也沒什麼壞處,就採用了,後來真的是,一次又一次的發現這種模式的便利。這次又是這樣搭上了“便車”。
記得我在直播里說過的:如果架構中出現了很多稀奇古怪難以剋服的問題,一般來說,就是因為你沒走在大道上。
通用的慣例模式,就是大道,別人已經走過的路啊。你循著別人走過的道走,碰到問題的時候,也一樣會比較容易的找到解決問題的方案;你要獨闢蹊徑——通常情況下可能不是你想獨闢蹊徑,呵呵,多半是自己走岔了吧——那荒山野嶺的,確實很難找到求助。
終極方案
思路就是:
- 在Action的POST的Filter里,如果未通過驗證,就把ModelState存放在TempData之中;
- 給Aciton的GET也添加一個Filter,用於取出TempData中的ModelState,並Merge到自己的ModelState中。
- 於是,通過Redirect,GET的Action得到了錯誤提示信息
非常清晰,我圖片和代碼混用吧:
最核心代碼:
/// <summary> /// Exports the current ModelState to TempData (available on the next request). /// </summary> protected static void ExportModelStateToTempData(ControllerContext context) { context.Controller.TempData[Key] = context.Controller.ViewData.ModelState; } /// <summary> /// Populates the current ModelState with the values in TempData /// </summary> protected static void ImportModelStateFromTempData(ControllerContext context) { var prevModelState = context.Controller.TempData[Key] as ModelStateDictionary; context.Controller.ViewData.ModelState.Merge(prevModelState); }View Code
就這麼簡單,完美!回味無窮。
其實,在POST的Action里return View();並不是一個好套路。
在我的項目我就發現了這麼一個問題:當return的View()里還含有@Html.Action()調用,且該調用需要區分GET和POST時,會進入POST所屬的ChildAction,這肯定是不符合邏輯的。(表述起來好吃力!慢慢看,一邊看一邊想,想不明白的看視頻吧……)
題外話
磨蹭了一天,終於寫完了。
寫得好累,感覺寫這一篇文章比那90分鐘的視頻還累,難怪現在好多開源項目都沒文檔,直接上視頻了……
最後說說“一起幫”吧,現在已經被當初的乞丐版強多了,隔三差五的也有同學在上面提問,算是有些生氣了,但人氣還是遠遠的不夠,根據QQ群里投票結果,現在主要精力應放在做推廣上。
推廣了好幾天了,有點效果,但唉呀我的媽呀!這推廣,比寫這篇博客還累,咋整?
園子里就不說這些了。“酒向知己飲,詩向會人吟”,以後博客園只會講技術,這也是博客園歡迎的。想聊點編程以外的,歡迎加QQ群:
179742319(付費入群搶紅包),312423951(驗證入群)
以及關註微信公眾號:我們一起幫。
感謝評論區
@敲代碼的吃貨:
如果採用PRG的方案,就不能達到你的要求:“把提交上來的數據再次呈現以便修改的”。要滿足這種需求,只能在POST里return View(model); model里是帶著之前數據的。
@stoneniqiu:
該方案的優勢不是“減少”代碼量,而是從架構層面,解決POST中return View(); 造成的空異常問題。