在 Partial View 與 View 間使用 ViewBag 傳送資料
2 |
在 ASP.NET MVC View 引用伺服器端傳來的資料,正統做法是定義 View Model 類別,Action return View(viewModelObject),在 CSHTML 宣告 @model 定義強型別並使用 Razor 語法存取 Model 變數。(延伸閱讀:mrkt 的程式學習筆記: ASP.NET MVC 的ViewModel - 基礎篇 )
但如果是要傳遞簡單的數字或字串(像是啟用特定功能的旗標、頁面標題... 等等,建議只用於少量、單純與核心商業邏輯較無關的變數,否則應回歸 View Model),為此定義 View Model 類別有點小題大作,此時 ViewData 跟 ViewBag 是不錯的輕巧選擇。(不建議使用 TempData、Session,延伸閱讀:[探索 10 分鐘] 寫點有關 ASP.NET MVC ViewModel, ViewData, ViewBag, TempData 的代碼)
前幾天學到在 Partial View 使用 ViewBag 的一則眉角,寫成筆記備忘。
我在 ViewBagTest.cshtml 裡宣告 ViewBag.Num = 123:
@{
Layout = null;
ViewBag.Num = 123;
}
<!DOCTYPE html>
<html>
<head>
<title>ViewBag Text</title>
</head>
<body>
<div>
<div>V1: Num=@ViewBag.Num</div>
@Html.Partial("_PartialView")
<div>V2: Num=@ViewBag.Num</div>
<div>V2: Text=@ViewBag.Text</div>
</div>
</body>
</html>
ViewBagTest.cshtml 使用 Html.Partial("_PartialView") 引用 _PartialView.cshtml,在其中將 ViewBag.Num 改成 456,並另外指定 ViewBag.Text = "Test":
<div style="background-color: #ddd; padding: 6px;">
<div>P1: Num=@ViewBag.Num</div>
@{
ViewBag.Num = 456;
ViewBag.Text = "Test";
}
<div>P2: Num=@ViewBag.Num</div>
<div>P2: Text=@ViewBag.Text</div>
</div>
請問,ViewBagTest.cshtml 在執行 Html.Partial("_PartialView") 之後,讀取到的 ViewBag.Num 與 ViewBag.Title 為何?
答案是 ViewBag.Num == 123,ViewBag.Text == null。
依此實驗推論,在 View 設定的 ViewBag 屬性會傳入 Partial View,但 Partial View 對 ViewBag 所做的修改則不會傳回 View 端。如此就有定論了嗎?
且慢,再看一個實驗,我們改設 ViewBag.List = new List<string>():
@{
Layout = null;
ViewBag.List = new List<string>()
{
"View"
};
}
<!DOCTYPE html>
<html>
<head>
<title>ViewBag Text</title>
</head>
<body>
<div>
<div>V1: List=@string.Join(",", ViewBag.List.ToArray())</div>
@Html.Partial("_PartialView")
<div>V2: List=@string.Join(",", ViewBag.List.ToArray())</div>
</div>
</body
在 _PartialView.cshtml 新增字串到 ViewBag.List:
<div style="background-color: #ddd; padding: 6px;">
@{
ViewBag.List.Add("Partial");
}
</div>
測試結果如下。這次 View 端成功讀到 Partial View 塞入 ViewBag.List 的內容:
以上兩個現象,說穿了是 Value Type 與 Reference Type 特性使然。(延伸閱讀:Self Test - Value Type vs Reference Type ) 而背後的原因是 - ASP.NET MVC 會為 Partial View 複製一顆專屬的 ViewBag,但因為 Reference Type 特性,複製版 ViewBag 與原始 ViewBag 的 Reference Type 屬性是同一個物件個體。
由 Github 上的 ASP.NET MVC 原始碼可驗證這點:(是的,ASP.NET MVC 的原始碼已經完全在 Github 公開了,Open Source 萬歲!)
internal virtual void RenderPartialInternal(string partialViewName, ViewDataDictionary viewData,
object model, TextWriter writer, ViewEngineCollection viewEngineCollection)
{
if (String.IsNullOrEmpty(partialViewName))
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "partialViewName");
}
ViewDataDictionary newViewData = null;
if (model == null)
{
if (viewData == null)
{
newViewData = new ViewDataDictionary(ViewData);
}
else
{
newViewData = new ViewDataDictionary(viewData);
}
}
else
{
if (viewData == null)
{
newViewData = new ViewDataDictionary(model);
}
else
{
newViewData = new ViewDataDictionary(viewData) { Model = model };
}
}
ViewContext newViewContext = new ViewContext(ViewContext, ViewContext.View,
newViewData, ViewContext.TempData, writer);
IView view = FindPartialView(newViewContext, partialViewName, viewEngineCollection);
view.Render(newViewContext, writer);
}
HtmlHelper 在 RenderPartial() 時會以 new ViewDataDictionary(原來ViewData) 方式另建一個 newViewData 供 PartialView 使用,這就是為什麼在 Partial View 修改數字及字串不會反映回 View 端,但 List<string> 會。
好,如果我們希望在 Partial View 裡能修改 ViewBag 的所有屬性並傳回 View 端該怎麼做?
以下是我找到最簡潔的做法,在 View 端來個 ViewBag.ParentViewBag = ViewBag,把自己當成 Reference Type 屬性傳進去:
@{
Layout = null;
ViewBag.Num = 123;
ViewBag.ParentViewBag = ViewBag;
}
<!DOCTYPE html>
<html>
<head>
<title>ViewBag Text</title>
</head>
<body>
<div>
<div>V1: Num=@ViewBag.Num</div>
@Html.Partial("_PartialView")
<div>V2: Num=@ViewBag.Num</div>
</div>
</body>
</html>
在 Partial View 改用 ViewBag.ParentViewBag,所有的修改就會忠實反映回 View 端囉!
<div style="background-color: #ddd; padding: 6px;">
@{ var vb = ViewBag.ParentViewBag;}
<div>P1: Num=@vb.Num</div>
@{
vb.Num = 456;
}
<div>P2: Num=@vb.Num</div>
</div>
測試 OK,打完收工,我們下次再會。(揮手下降)
You cannot change ViewBag's property to pass information from partial view to parent view due to by value type characteristic. A workaround is to assign ViewBag's own instance to its property.
Comments
# by weskerjax
不建議直接使用 Parent View 的 ViewBag,這樣以後在維護上會很不直覺,無法從 Parent View 知道 Partial 需要的參數,以及 Partial 也無法知道具有的參數,使用 ViewDataDictionary 或 ViewModel 還是比較好的。
# by Jeffrey
to weskerjax, 同意。ViewBag 是一種方便但不嚴謹的選項,尤其它還是 dynamic,打錯字 Runtime 還不會主動報錯只會傳回空值,使用時要留意這些缺陷。不過有少數場合它還是可考量的選擇,像是預設模板使用 ViewBag.Title 傳遞頁面標題即屬經典應用。