在 ASP.NET MVC View 引用伺服器端傳來的資料,正統做法是定義 View Model 類別,Action return View(viewModelObject),在 CSHTML 宣告 @model 定義強型別並使用 Razor 語法存取 Model 變數。(延伸閱讀:mrkt 的程式學習筆記: ASP.NET MVC 的ViewModel - 基礎篇 )
但如果是要傳遞簡單的數字或字串(像是啟用特定功能的旗標、頁面標題... 等等),為此定義 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,打完收工,我們下次再會。(揮手下降)