文章出處

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

 

緣起

我忘了是不是在園子里講過,我命名為“截斷式編程”的寫法。其主要目的,就是把簡單的、過濾條件、“非主干的”邏輯放在最前面。比如在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(); 造成的空異常問題。

 


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()