這是昨天一個同事遇到的問題,我覺得這是一個蠻大的問題,而且不像是ASP.NET MVC的設計者有意為之,換言之,這可能是ASP.NET MVC的一個Bug(不過也有可能是保持原始請求數據而作的妥協)。StackOverflow上也有對這個問題的描述http://stackoverflow.com/questions/1775170/asp-net-mvc-modelstate-clear
閑話少說,我們通過一個簡單的問題重新這個問題。首先我們 定義了如下一個默認的HomeController,它具有一個默認Action方法Index。該方法接受一個類型為DemoModel的參數,定義其中的邏輯非常簡單:我們對該參數的三個屬性略加修改后,將其作為Model呈現在對應的View中。
public class HomeController : Controller { public ActionResult Index(DemoModel model) { model.Foo += ":Changed"; model.Bar += ":Changed"; model.Baz += ":Changed"; return View("index", model); } } public class DemoModel { public string Foo { get; set; } public string Bar { get; set; } public string Baz { get; set; } }
對於Action方法Index對應的View(Index.cshtml),我們可以采用如下三種定義方式將Model對象以編譯模式呈現出來。
//第一種形式
@model DemoModel
@Html.LabelFor(m=>m.Foo)
@Html.TextBoxFor(m => m.Foo)
@Html.LabelFor(m => m.Bar)
@Html.TextBoxFor(m => m.Bar)
@Html.LabelFor(m => m.Baz)
@Html.TextBoxFor(m => m.Baz)
//第二種形式
@model DemoModel
@Html.LabelFor(m=>m.Foo)
@Html.EditorFor (m => m.Foo)
@Html.LabelFor(m => m.Bar)
@Html.EditorFor (m => m.Bar)
@Html.LabelFor(m => m.Baz)
@Html.EditorFor (m => m.Baz)
//第三種形式
@model DemoModel
@Html.EditorForModel
現在我們運行該程序,並通過Query String的形式提供作為Action方法Index參數的數據(?foo=123&bar=456&baz=789),我們可以看到界面上呈現出來的總是原始值,也就是說我們在Action方法Index中對原始數據的修改沒有起到任何效果。
通過查看ASP.NET MVC框架自身的代碼,我想這個問題的根源應該源於InputExtensions類型的InputHelper方法。如下所示,當InputHelper在指定表單元素值得時候,會先從當前ModelState中獲取,如果該值在ModelState中不存在,才會從當前ViewData中獲取。對於本例來說,ModelState中的值是原始值,ViewData的值采用修改后的值。
public static class InputExtensions { private static MvcHtmlString InputHelper(HtmlHelper htmlHelper, InputType inputType, ModelMetadata metadata, string name, object value, bool useViewData, bool isChecked, bool setId, bool isExplicitValue, string format, IDictionary<string, object> htmlAttributes); } private static MvcHtmlString InputHelper(HtmlHelper htmlHelper, InputType inputType, ModelMetadata metadata, string name, object value, bool useViewData, bool isChecked, bool setId, bool isExplicitValue, string format, IDictionary<string, object> htmlAttributes) { … switch (inputType) { … default: { string str4 = (string) htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof(string)); tagBuilder.MergeAttribute("value", str4 ?? (useViewData ? htmlHelper.EvalString(fullHtmlFieldName, format) : str2), isExplicitValue); goto Label_016C; } } … }
我覺得rinsen的評論說得有道理,這也可能是為了保持請求的原始數據而作的妥協。不過我還是覺得這樣的設計有違MVC的基本原則,MVC處理請求的流程很清楚:客戶端(瀏覽器)向定義在Controller中的某個Action方法發送請求,Action方法處理這個請求,並呈現出相應的View來對請求做最后的響應。換言之,最終呈現怎么的View應該完全由Action方法決定,對於我們的例子來說,Action方法很明顯的意圖就是將更新過的Model呈現出來。而且這是一種非常典型的場景:服務端對原始數據進行簡單的加工后再呈現出來。
其實我覺得嚴格來說也是無奈之舉吧,
拿Update場景來說
比如說Model里面的某個Property可能是Int的,但是你傳入的Form值卻可能是任意的字符串,這時后台ModelState.IsValid是false,然后你就需要返回View讓用戶繼續修改,並把用戶輸入的值帶入到Form中。
這時候model其實是有的(反正不為null,而Property也是有默認值的:0)而回顯顯然不可能回顯為0,而是用戶的輸入。
所以這個角度看來ModelState的優先級是比較高的。
[你總不能回顯一個0,然后錯誤提示“你輸入的不是數值類型”吧]