在 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 傳遞頁面標題即屬經典應用。

Post a comment