天天看點

受夠了if (ModelState.IsValid)?ActionFitlter也是一路的坑啊!

這篇部落格真是幹貨,幹得估計還有點“磕牙”,是以還提供視訊和代碼。但基礎稍弱的同學,怕還是得自行補充一些基礎知識——就一篇文章,确實沒辦法面面俱到。

緣起

我忘了是不是在園子裡講過,我命名為“截斷式程式設計”的寫法。其主要目的,就是把簡單的、過濾條件、“非主幹的”邏輯放在最前面。比如在ASP.NET MVC的Action中,處理POST時,我們通常都要進行服務端驗證,于是我們就可以這樣寫:

受夠了if (ModelState.IsValid)?ActionFitlter也是一路的坑啊!
受夠了if (ModelState.IsValid)?ActionFitlter也是一路的坑啊!

View Code

由于采用了這種寫法,我們很快就發現了一個問題:if (!ModelState.IsValid) { return View(model); }到處都是。

受夠了if (ModelState.IsValid)?ActionFitlter也是一路的坑啊!

是不是有點“壞味道”的感覺?你是不是想怎麼“弄”它一下?

ActionFilter

首先想到的,當然就是ActionFilter了:在Action執行之前,用一個Filter進行檢查,不就OK了嗎?

我覺得這個想法不錯,但是,但是,請注意,一定要問一個為什麼!為什麼别人想不到呢?——這怎麼可能?!

是以,在自己動手之前,養成習慣,google/bing一下,看看别人是怎麼弄的。

而且,他想得比我更周全!看得我那個興奮啊……

是以,這裡我們得到的第一個經驗:動手之前先搜一搜,不要重複造輪子。

再引申開一點,

英文 + google = 偉大的程式員。 至少在目前,以及可預見的将來,對于開發人員而言,英語非常重要,非常重要,重要性怎麼強調都不為過。大家可以試一下,有沒有中文的類似的部落格資料等,我沒去試。但根據我的經驗,相比于英文資料,中文資料是非常匮乏的。 關于使用搜尋引擎,很多同學覺得這是一種“可恥的行為”,但其實不然。你一定要明白:你的目的是解決問題,而不是炫技,非得把什麼東西都記在腦子裡,非得什麼都自己寫出來……算了,這話可能很多人不接受,篇幅有限,懶得說了。懂的人一點就通,不懂的人你怎麼說都沒用。

最簡單的情形

一開始解決方案還是比較簡單的。我就直接放代碼了:

受夠了if (ModelState.IsValid)?ActionFitlter也是一路的坑啊!
受夠了if (ModelState.IsValid)?ActionFitlter也是一路的坑啊!

了解的難點大概就在于為什麼: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,哪怕傳回的實際上是同一個頁面。示例代碼如下:

受夠了if (ModelState.IsValid)?ActionFitlter也是一路的坑啊!
受夠了if (ModelState.IsValid)?ActionFitlter也是一路的坑啊!

我非常欣慰的是我們的系統架構,一開始就是按照這個原則搭建的。最早接觸PRG模式的時候,我也是有點迷迷糊糊的,但想到大家都這樣用,而且看起來也沒什麼壞處,就采用了,後來真的是,一次又一次的發現這種模式的便利。這次又是這樣搭上了“便車”。

記得我在直播裡說過的:如果架構中出現了很多稀奇古怪難以克服的問題,一般來說,就是因為你沒走在大道上。 通用的慣例模式,就是大道,别人已經走過的路啊。你循着别人走過的道走,碰到問題的時候,也一樣會比較容易的找到解決問題的方案;你要獨辟蹊徑——通常情況下可能不是你想獨辟蹊徑,呵呵,多半是自己走岔了吧——那荒山野嶺的,确實很難找到求助。

終極方案

思路就是:

在Action的POST的Filter裡,如果未通過驗證,就把ModelState存放在TempData之中;

給Aciton的GET也添加一個Filter,用于取出TempData中的ModelState,并Merge到自己的ModelState中。

于是,通過Redirect,GET的Action得到了錯誤提示資訊

非常清晰,我圖檔和代碼混用吧:

受夠了if (ModelState.IsValid)?ActionFitlter也是一路的坑啊!

最核心代碼:

受夠了if (ModelState.IsValid)?ActionFitlter也是一路的坑啊!
受夠了if (ModelState.IsValid)?ActionFitlter也是一路的坑啊!

就這麼簡單,完美!回味無窮。

其實,在POST的Action裡return View();并不是一個好套路。 在我的項目我就發現了這麼一個問題:當return的View()裡還含有@Html.Action()調用,且該調用需要區分GET和POST時,會進入POST所屬的ChildAction,這肯定是不符合邏輯的。(表述起來好吃力!慢慢看,一邊看一邊想,想不明白的看視訊吧……)

題外話

磨蹭了一天,終于寫完了。

寫得好累,感覺寫這一篇文章比那90分鐘的視訊還累,難怪現在好多開源項目都沒文檔,直接上視訊了……

推廣了好幾天了,有點效果,但唉呀我的媽呀!這推廣,比寫這篇部落格還累,咋整?

園子裡就不說這些了。“酒向知己飲,詩向會人吟”,以後部落格園隻會講技術,這也是部落格園歡迎的。想聊點程式設計以外的,歡迎加QQ群:

179742319(付費入群搶紅包),312423951(驗證入群)

以及關注微信公衆号:我們一起幫。

感謝評論區

如果采用PRG的方案,就不能達到你的要求:“把送出上來的資料再次呈現以便修改的”。要滿足這種需求,隻能在POST裡return View(model); model裡是帶着之前資料的。

該方案的優勢不是“減少”代碼量,而是從架構層面,解決POST中return View(); 造成的空異常問題。

繼續閱讀