在Asp.Net MVC
1.0編程中,我們經常遇見這樣的場景,在新建一個對象時候,通過HtmlHelper的方式在View模型中渲染Html控件,當填寫完相關內容后,通過Form把需要新建的內容Post回View對應Controller的Action(例如:Create),指定的Action可以通過接受FormCollection參數、值參數或者某個類的實例參數(比如:Movie類),完成新建的操作。(主要指HtmlHelper.TextBox)
當我們通過傳遞FormCollection參數進行操作時,如果不使用UpdateModel方法,而利用ModelState.IsValid及ModelState.AddModelError實現錯誤校驗提示等操作。這個時候,小心陷阱。
【注:本文章源代碼通過VS2008創建】
1 、View模型中HtmlHelper綁定數據的順序
開始前,讓我們先了解下View模型中HtmlHelper綁定數據的順序(主要指HtmlHelper.TextBox,其它還未研究)
我們知道,當View使用了HtmlHelper進行控件渲染的時候,HtmlHelper會通過鍵值嘗試填充我們曾經填寫過的數據,以防止用戶從頭填寫。(比如:我們填寫表單,提交,當出現驗證錯誤的時候,我們希望表單刷新后曾經填寫的內容依然存在,而不是全部要重新填寫。而HtmlHelper就是這樣幫助我們的)。HtmlHelper填充數據的順序如下:
(1) 通過鍵值調用ModelState集合對應的System.Web.Mvc.ModelState實例的Value屬性獲取
(2) 通過HtmlHelper指定的值填充(Html.TextBox("Title",指定值))
(3) 通過鍵值獲取ViewData內的對應數據
(4) 通過鍵值獲取View中強類型的Model對象對應屬性的數據
(5) 不填充
先看一個簡單的例子(源代碼下載)。View通過Post傳遞FormCollection參數到對應Controller的Create
Action,Create
Action檢驗參數是否合法。如果合法,暫時什么都不做;如果不合法,則通過ModelState的AddModelError添加錯誤信息,並通過ModelState.
IsValid判斷,如果無效,重新返回該View。
(1) View代碼(沒有任何特殊的地方,HtmlHelper使用Html.TextBox("Title")的方式):


ID ="Content1"
ContentPlaceHolderID ="TitleContent"
runat ="server" >

Create


ID ="Content2"
ContentPlaceHolderID ="MainContent"
runat ="server" >

< h2 > Create </ h2 >

<% =
Html.ValidationSummary( " Create was unsuccessful.
Please correct the errors and try again. " ) %>

<% using (Html.BeginForm())
{ %>

< fieldset >

< legend > Fields </ legend >

< p >

< label for ="Title" > Title: </ label >

<% = Html.TextBox( " Title " ) %>

<% =
Html.ValidationMessage( " Title " , " * " ) %>

</ p >

< p >

< label for ="Director" > Director: </ label >

<% = Html.TextBox( " Director " ) %>

<% =
Html.ValidationMessage( " Director " , " * " ) %>

</ p >

< p >

< label for ="Remark" > Remark: </ label >

<% = Html.TextBox( " Remark " ) %>

<% =
Html.ValidationMessage( " Remark " , " * " ) %>

</ p >

< p >

< input type ="submit"
value ="Create" />

</ p >

</ fieldset >

<% } %>

< div >

<% = Html.ActionLink( " Back to List " , " Index " ) %>

</ div >


(2) Controller中Create Action代碼
對應的Create
Action代碼如下,我們通過UpdateModel來進行Movie類的填充,而是直接創建了一個Movie類的實例(我直接在Controller的Action中驗證參數,雖然我知道這樣做不對,這里只是個例子。):
public ActionResult Create(FormCollection collection)

{

//手動實例化

{

Title =
collection["Title"],

Director = collection["Director"],

Remark =
collection["Remark"]};

if
(m.Title.Trim().Length == 0)

{

ModelState.AddModelError("Title", "Title 不能為空!");

}

if
(m.Director.Trim().Length == 0)

{

ModelState.AddModelError("Director", "Director 不能為空!");

}

if
(!ModelState.IsValid)

{

return
View();

}

try

{

//TODO
SaveToDB

Content("OK");

}

catch

{

return
View();

}

(3) 運行結果
大家可以下載代碼運行,結果如下:當不輸入參數,提交表單時,我們希望這個時候能夠提示“Title 不能為空!”和“Director
不能為空!”。但是,很不幸,報錯了。
3、傳遞FormCollection,使用UpdateModel
現在,View的代碼不變,我們在Create
Action中使用UpdateModel方法,代碼如下(源代碼下載):

public ActionResult Create(FormCollection collection)

{

Movie m = new Movie();

//使用UpdateModel方法



if
(m.Title.Trim().Length == 0)

{

ModelState.AddModelError("Title", "Title 不能為空!");

}

if
(m.Director.Trim().Length == 0)

{

ModelState.AddModelError("Director", "Director 不能為空!");

}

if
(!ModelState.IsValid)

{

return
View();

}

try

{

//TODO
SaveToDB

Content("OK");

}

catch

{

return
View();

}

}

大家可以下載代碼,運行:當不輸入參數時,提示“Title 不能為空!”和“Director
不能為空!”,一切正常。

4、 原因分析
下面我們來分析下造成這個問題的原因。
(1) 認識一下 System.Web.Mvc.ModelStateDictionary和System.Web.Mvc.ModelState
我們知道,每個Controller都有一個類型為System.Web.Mvc.ModelStateDictionary的ModelState集合(后文中稱為ModelState集合),該集合是一個System.Web.Mvc.ModelState對象的集合(MVC在這里取名存在嚴重的問題,Controller里面的ModelState既然是個集合,應該命名為ModelStates或者ModelStateCollection,以免被誤會)。System.Web.Mvc.ModelState這個對象包含兩個屬性:
l Errors:類型為System.Web.Mvc.ModelErrorCollection的屬性。
l Value:類型為System.Web.Mvc.ValueProviderResult的屬性。
(2)UpdateModel方法與 System.Web.Mvc.ModelStateDictionary和System.Web.Mvc.ModelState的關系
當調用UpdateModel方法時,它至少做了兩件事情。
A、 把提交的數據(FormCollection中的數據)與Movie類實例的屬性匹配並自動更新。(參考:有一天,WebForm 對 MVC 說:能否借你的UpdateModel方法來用用?)
B、 將每個匹配的FormCollection中的數據實例化為System.Web.Mvc.ModelState類,並根據鍵值分別加入ModelState集合中。
通過調試發現,在調用UpdateModel方法前,ModelState集合沒有數據;調用后,集合內是有數據的。
l 調用UpdateModel前 l 調用UpdateModel后


(3)不使用UpdateModel方法,AddModelError與System.Web.Mvc.ModelStateDictionary和System.Web.Mvc.ModelState的關系
當不使用UpdateModel方法,而在驗證不通過時候調用ModelState.AddModelError方法時。通過調試發現,ModelState集合也是有數據的。
也就是說,AddModelError方法同樣實例化了System.Web.Mvc.ModelState類,並根據鍵值將它加入ModelState集合。
通過圖可以看到,集合內有兩個System.Web.Mvc. ModelState對象的實例。
(4)UpdateModel方法與ModelState.AddModelError的PK
既然UpdateModel和ModelState.AddModelError都實例化了System.Web.Mvc.ModelState,並加入了ModelState集合,那有什么區別呢?
l UpdateModel方法:通過調試發現,當使用UpdateModel方法后,ModelState集合內的System.Web.Mvc.ModelState類的實例的Value屬性是不為空的。

l ModelState.AddModelError方法:通過調試發現,當不使用UpdateModel而調用ModelState.AddModelError 方法后,ModelState集合的System.Web.Mvc.ModelState類的實例的Value屬性是空的。
就是說,當傳遞FormCollection參數時,如果不使用UpdateModel方法,而只使用ModelState.AddModelError方法,ModelState集合中System.Web.Mvc.ModelState類的實例的Value屬性並不會被賦值。
(5)不使用UpdateModel方法,手動向ModelState集合的System.Web.Mvc.ModelState實例的Value屬性賦值。
通過上面的分析,我們知道,當傳遞FormCollection參數時,如果不使用UpdateModel方法,當我們調用ModelState.AddModelError方法時,System.Web.Mvc.ModelState對象會被創建,並根據鍵值被加入到ModelState集合中了,但它的Value屬性是空的。那我們就需要手動執行賦值這個操作。通過使用ModelState集合的“Add(string key, ModelState
value)”方法可以搞定。現在,一切OK!(代碼下載)
public ActionResult Create(FormCollection collection)

{

Movie m = new Movie() {

Title =
collection["Title"],

Director = collection["Director"],

Remark =
collection["Remark"] };


//手動添加數據到ModelState集合

{

Value =
collection.ToValueProvider()["Title"] });


ModelState.Add("Director", new ModelState() {

Value =
collection.ToValueProvider()["Director"] });


ModelState.Add("Remark", new ModelState() {

Value =
collection.ToValueProvider()["Remark"] });


if
(m.Title.Trim().Length == 0)

{

ModelState.AddModelError("Title", "Title 不能為空!");

}

if
(m.Director.Trim().Length == 0)

{

ModelState.AddModelError("Director", "Director 不能為空!");

}

if
(!ModelState.IsValid)

{

return
View();

}

try

{

//TODO
SaveToDB

Content("OK");

}

catch

{

return
View();

}

}
現在,讓我們再來分析下異常的原因:
當傳遞FormCollection參數時,不使用UpdateModel方法,但在驗證失敗后調用ModelState.AddModelErro方法時,System.Web.Mvc.ModelState被實例化,並通過某個鍵值(比如“Title”)加入到了ModelState集合中。但是,該System.Web.Mvc.ModelState實例的Value屬性是NULL的。
當在View中使用HtmlHelper.TextBox("Title")進行渲染的時候,HtmlHelper試圖通過鍵值(“Title”)重新將輸入值與控件綁定(例如:TextBox)時,由於ModelState集合的優先級最高,因此HtmlHelper試圖通過這個鍵值(“Title”)從ModelState集合中獲取數據(通過調用GetModelStateValue()方法)。由於AddModelErro方法的“功勞”,HtmlHelper獲取到了這個鍵值(“Title”)對應的System.Web.Mvc.ModelState類的實例,但該實例的Value屬性是Null。因此,出現了開篇的問題:“未將對象應用設置到對象值的實例”。
5、直接傳遞類參數、值參數
如果我們在Post的時候不傳遞FormatCollection,而是直接傳遞類或者值參數。
傳遞類


Create(Movie m) {}
傳遞值參數


Create( string Title, string
Director, string Remark) {}
那不會出現問題。因為當傳遞的是類或者參數時,默認的ModelBinder除了會實例化Movie類並匹配屬性或給參數賦值外,還會根據鍵值填充ModelState集合,就像UpdateModel會幫你做這件事情一樣。
6、 小結
(1) Controller中的ModelState集合是個很重要的東西,它是System.Web.Mvc.ModelState類的集合,System.Web.Mvc.ModelState的實例會負責保存鍵值匹配的輸入值(Value屬性)、以及驗證錯誤信息(Errors屬性)。
(2) Post方式傳遞類參數、值參數時,會通過默認的ModelBinder來填充ModelState集合。
(3) UpdateModel方法也會填充ModelState集合。
(4) 如果使用HtmlHelper,並傳遞FormCollection參數,又需要通過ModelState.AddModelError添加錯誤驗證信息,則需要調用UpdateModel方法或通過ModelState.Add方法來填充ModelState集合。
(5) 使用HtmlHelper渲染View中的控件數據的時候(主要指HtmlHelper.TextBox,其它還未研究),綁定順序為:ModelState集合、指定值、ViewData內的數據、View中強類型Model對象對應屬性的數據。
7、PS:
如果通過Asp.Net MVC 1.0做數據驗證的時候,我們通常不會直接在Controller中的Action里面做,提供幾個開源的工具和幾篇文章:
n FluentValidation
下載地址:http://www.codeplex.com/FluentValidation
文章:http://www.cnblogs.com/wintersun/archive/2009/02/15/1390990.html
n Data Annotation Model Binder
下載地址:http://aspnet.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=24471
文章:http://www.asp.net/learn/mvc/tutorial-39-cs.aspx
或者Google 4 : Asp.Net MVC 數據驗證
8、補充:
根據回復補充:
一、View模型中采用了HtmlHelper("Title",Model.Title)的方式
如果View模型中采用了HtmlHelper("Title",Model.Title)的方式,在第一次進入Create Action的時候,需要給ViewData.Model賦值,如果是Post回的Create Action,如果還需要顯示這個View,也需要給ViewData.Model賦值,否則View模型中的Model為NULL,也會提示未將對象應用設置到對象值的實例”。給ViewData.Model賦值有兩種方法(二選一):
1、在Create Action中給ViewData.Model賦值
ViewData.Model = new Movie() (第一次進入Create Action調用)
ViewData.Model = m(Post回Create Action時候調用,m為手動、自動或者傳遞參數過來的Movie對象實例)
2、返回使用帶TModel參數的重載函數View(TModel)
Return View(new Movie())(說明同上)
Return View(m)(說明同上)
轉載要求及授權協議:
作者: 零零豬(or) Jiessie327(or) JiessieLiang
出處: http://jiessie327.cnblogs.com/
版權:本文版權歸 作者所有
轉載:歡迎轉載,為了尊重作者的勞動成果,在【轉載】時請按作者要求,指明文章【出處】或給出【原文鏈接】,謝謝
================================================================
請遵守 署名-非商業性使用-禁止演繹 2.5 中國大陸 License.

================================================================