[轉]Asp.Net MVC使用HtmlHelper渲染,並傳遞FormCollection參數的陷阱


在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) 不填充

2、 傳遞FormCollection參數,不使用UpdateModel引起的異常

先看一個簡單的例子源代碼下載)。View通過Post傳遞FormCollection參數到對應Controller的Create
Action,Create
Action檢驗參數是否合法。如果合法,暫時什么都不做;如果不合法,則通過ModelState的AddModelError添加錯誤信息,並通過ModelState.
IsValid判斷,如果無效,重新返回該View。

(1) View代碼(沒有任何特殊的地方,HtmlHelper使用Html.TextBox("Title")的方式):

<% @ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<ValidationTest.Models.Movie>" %>
< asp:Content
ID ="Content1"
ContentPlaceHolderID
="TitleContent"
runat
="server" >
   
Create
</ asp:Content >
< asp:Content
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 >
</ asp:Content >

 (2) Controller中Create Action代碼

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

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection collection)
       
{
          
//手動實例化
            Movie m = new Movie()
{
               
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

return
Content(
"OK");
           
}

           
catch
           
{
               
return
View();
           
}



 (3) 運行結果

大家可以下載代碼運行,結果如下:不輸入參數,提交表單時,我們希望這個時候能夠提示“Title 不能為空!”和“Director
不能為空!”。但是,很不幸,報錯了。

 
3、傳遞FormCollection,使用UpdateModel

現在,View的代碼不變,我們在Create
Action中使用UpdateModel方法,代碼如下源代碼下載

[AcceptVerbs(HttpVerbs.Post)]     
public ActionResult Create(FormCollection collection)
       
{
          
Movie m
= new Movie();
          
//使用UpdateModel方法
            UpdateModel<Movie>(m);

           
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

return
Content(
"OK");
           
}

           
catch
           
{
               
return
View();
           
}

       
}



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

 

4、 原因分析

下面我們來分析下造成這個問題的原因。

(1) 認識一下 System.Web.Mvc.ModelStateDictionarySystem.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.ModelStateDictionarySystem.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方法,AddModelErrorSystem.Web.Mvc.ModelStateDictionarySystem.Web.Mvc.ModelState的關系

當不使用UpdateModel方法,而在驗證不通過時候調用ModelState.AddModelError法時。通過調試發現,ModelState集合也是有數據的。

也就是說,AddModelError方法同樣實例化了System.Web.Mvc.ModelState類,並根據鍵值將它加入ModelState集合。


           通過圖可以看到,集合內有兩個System.Web.Mvc. ModelState對象的實例。

(4)UpdateModel方法與ModelState.AddModelError的PK

既然UpdateModelModelState.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!(
代碼下載

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection collection)
       
{
           
Movie m
= new Movie() {
               
Title
=
collection[
"Title"],
               
Director
= collection["Director"],
               
Remark
=
collection[
"Remark"] }
;

           
//手動添加數據到ModelState集合
            ModelState.Add("Title", new 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

return
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,而是直接傳遞類或者值參數。

 

傳遞類

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult
Create(Movie m)
{}

 

傳遞值參數

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult
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.


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM