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

http://www.cnblogs.com/errorif/archive/2012/02/13/2349902.htmljavascript

在Asp.Net MVC 
1.0編程中,咱們常常碰見這樣的場景,在新建一個對象時候,經過HtmlHelper的方式在View模型中渲染Html控件,當填寫完相關內容後,經過Form把須要新建的內容Post回View對應Controller的Action(例如:Create),指定的Action能夠經過接受FormCollection參數、值參數或者某個類的實例參數(好比:Movie類),完成新建的操做。(主要指HtmlHelper.TextBox)html

當咱們經過傳遞FormCollection參數進行操做時,若是不使用UpdateModel方法,而利用ModelState.IsValid及ModelState.AddModelError實現錯誤校驗提示等操做。這個時候,當心陷阱。
【注:本文章源代碼經過VS2008建立】
java



1
、View模型中HtmlHelper綁定數據的順序編程

開始前,讓咱們先了解下View模型中HtmlHelper綁定數據的順序(主要指HtmlHelper.TextBox,其它還未研究)mvc

咱們知道,當View使用了HtmlHelper進行控件渲染的時候,HtmlHelper會經過鍵值嘗試填充咱們曾經填寫過的數據,以防止用戶從頭填寫。(好比:咱們填寫表單,提交,當出現驗證錯誤的時候,咱們但願表單刷新後曾經填寫的內容依然存在,而不是所有要從新填寫。而HtmlHelper就是這樣幫助咱們的)。HtmlHelper填充數據的順序以下:asp.net

(1) 經過鍵值調用ModelState集合對應的System.Web.Mvc.ModelState實例的Value屬性獲取ide

(2) 經過HtmlHelper指定的值填充(Html.TextBox("Title",指定值))函數

(3) 經過鍵值獲取ViewData內的對應數據工具

(4) 經過鍵值獲取View中強類型的Model對象對應屬性的數據spa

(5) 不填充

二、 傳遞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 
不能爲空!」,一切正常。

 

四、 緣由分析

下面咱們來分析下形成這個問題的緣由。

(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這個對象包含兩個屬性:

Errors類型爲System.Web.Mvc.ModelErrorCollection的屬性。

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集合,那有什麼區別呢?

UpdateModel方法經過調試發現,當使用UpdateModel方法後,ModelState集合內的System.Web.Mvc.ModelState類的實例的Value屬性是不爲空的。

 

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。所以,出現了開篇的問題:「未將對象應用設置到對象值的實例」。

 

五、直接傳遞類參數、值參數

若是咱們在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會幫你作這件事情同樣。

六、 小結

(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對象對應屬性的數據。

 

七、PS:

若是經過Asp.Net MVC 1.0作數據驗證的時候,咱們一般不會直接在Controller中的Action裏面作,提供幾個開源的工具和幾篇文章:

FluentValidation

下載地址:http://www.codeplex.com/FluentValidation

文章:http://www.cnblogs.com/wintersun/archive/2009/02/15/1390990.html

 

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 數據驗證



八、補充:
 

根據回覆補充:
1、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賦值有兩種方法(二選一):一、在Create Action中給ViewData.Model賦值    ViewData.Model = new Movie() (第一次進入Create Action調用)    ViewData.Model = m(Post回Create Action時候調用,m爲手動、自動或者傳遞參數過來的Movie對象實例)二、返回使用帶TModel參數的重載函數View(TModel)    Return View(new Movie())(說明同上)    Return View(m)(說明同上)

相關文章
相關標籤/搜索