Skip to main content

[探索 10 分鐘] 寫點有關 ASP.NET MVC ViewModel, ViewData, ViewBag, TempData 的代碼

將 ASP.NET MVC 的 Controller 資料要傳給頁面, 或是頁面再轉只給其他頁, 有很多方式, Data Passing Mechanism in MVC Architecture 對於不同的資料拋轉方式整理出非常明確的結論, 在開始介紹代碼之前我們先有基本認知 :
在 ASP.NET MVC 中, 沒有 view state, 沒有 code behind, 沒有 server controls。我們仍需在 MVC 架構中傳遞數據, 有一些機制來傳遞數據如下, 如ViewData, ViewBag, TempData, Session。

ViewData

  • ViewData 是 ControllerBase 類的 property
  • ViewData 用於 Controller 將資料傳遞給對應的 view (Controller to View)
  • ViewData 生命週期只存在於當下的請求, 導頁後就被清掉了 (null)
  • ViewData 是 Dictionary 類別, 繼承 ViewDataDictionary 纇, 要注意字典資料轉型跟 null 問題
  • Example - ViewData["Key"] = "Value"

ViewBag

  • ViewBag 也是 ControllerBase 類的 property
  • ViewData 操作方法須對每個資料使用中括弧, ViewBag 簡化為點運算式
  • ViewBag 基本上是 ViewData 的包裝類別, 也是用於 Controller 將資料傳遞給對應的View (Controller to View)
  • ViewBag 是 C# 4.0的 dynamic 型別, 可在執行期再判斷真正型別 (C# reflection)
  • ViewBag 生命週期也只存在於當下的請求, 導頁後就被清掉了 (null)
  • Example - ViewBag.Key = "Value"

TempData

  • TempData 也是 ControllerBase 類的 property
  • TempData 生命週期除了當下請求, 導頁後仍可續存 (如action to action, controller to action), 或想像為暫時性的 Session
  • TempData也是 Dictionary 類別, 繼承 TempDataDictionary 纇, 要注意字典資料轉型跟Null問題
  • Example - TempData["Order1"] = order, 注意 value 若為類別物件建議加上 [Serializable]

Session

  • Session 是 HttpContext 類的 property
  • Session 生命週期可跨所有請求, 導頁後可續存
  • Session 也是全站獨立等級資料, 有 InProc, StateServer, SqlServer, 自訂等狀態儲存種類  (參考 MSDN)
  • Session 繼承 HttpSessionState 纇, ICollection 的介面操作方式, 也要注意字典資料轉型跟 null 問題
  • Example - C#: Session["Profile1"] = profile, 注意 value 若為類別物件建議加上 [Serializable] 屬性
接下來, 我們舉一個最常看到的例子, 登入頁。任何人用 Visual Studio 建立一個 MVC 的 Web Application, 應該會看到登入 page、會員 Controller、登入 Action、登入 View, 登入 Model。

ViewModel

以下列舉3段簡化後的程式碼 :
  • LoginViewModel 的宣告
  • AccountController 針對 Login Action 的處理
  • Login.csthml 操作 LoginViewModel
用於登入的 ViewModel 宣告如下 :

AccountViewModels.cs

public class LoginViewModel
{
    [Required]
    [Display(Name = "Email")]
    [EmailAddress]
    public string Email { get; set; }
    
    [Required]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }
}
就是 Email, Password 的輸入框, 並且運用 System.ComponentModel.DataAnnotations 的各種屬性標上 [Required]、[EmailAddress] 做資料欄位驗證, 把他封裝為一個 LoginViewModel 類。從名稱來看也很直覺, 他是一個 Model, 用於 View, 行為是 Login (強烈建議 ViewModel 這名稱要出來, 或有人會簡稱為 vm, VM)。

AccountController.cs


public ActionResult Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
     return View(model);
    }
    var isValid = IsValid(model.Email, model.Password, model.RememberMe, out var msg);
    if (isValid)
    {
     return RedirectToLocal(returnUrl);
    }
    
    ModelState.AddModelError("", "登入失敗: " + msg);
    return View(model);
}
Controller 邏輯很簡單, 針對 post 過來的 LoginViewModel 資料模型做 ModelState.IsValid 驗證 (從宣告中可以知道 Email 與 Password 為 [Required] 必填), 以及登入服務處理: 成功就 Redirect導頁, 否則就告知登入失敗訊息。

Login.cshtml

@using BlogWebApplication.Models
@model LoginViewModel
@{
    ViewBag.Title = "Log in";
}

<h2>@ViewBag.Title.</h2>
<section id="loginForm">
 @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
 {
  <h4>Use a local account to log in.</h4><hr />    
  @Html.ValidationSummary(true, "", new { @class = "text-danger" })  
  <div class="form-group">
   @Html.LabelFor(m => m.Email)
   @Html.TextBox("Email", Model.Email, new { @class = "form-control" })
   @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" })
  </div>
  <div class="form-group">
   @Html.LabelFor(m => m.Password)
   @Html.PasswordFor(m => m.Password)
   @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" })
  </div>
  <div>
   <input type="submit" value="Log in" />
  </div>
 }
</section>
View 用到非常多 Razor 語法, 但請先聚焦黃色螢光處

@model LoginViewModel是一個 View 頁面的模型宣告, 一個 View 可以有一個 Model, 注意是小寫的 model
@Html.ValidationSummary可取得 Controller 透過 ModelState.AddModelError() 傳遞來的錯誤訊息, 做互動或除錯很好用
@Html.TextBox("Email", Model.Email)可產生 <input id="Name" name="Name" type="text" value="@Model.Email"> 的 HTML 腳本, 但這寫法有點囉嗦, 也隱含 Model 為 null 時的錯誤。注意是大寫的 Model
@Html.TextBoxFor(m > m.Email)同樣產生 HTML 語法, 但能自動取得 property name, 是不是優雅多了, 推薦這種 For 結尾的 Lambda 用法

頁面呈現如下, 如果忘記輸入 [Required] 欄位時, 前端就會阻擋並顯示對應訊息; 如果順利 Submit 到後端, 是一個不合法的用戶, 前端也順利顯示登入失敗訊息。



登入失敗情況, 在 @Html.ValidationSummary 代碼的位置, 順便截一下除錯畫面看會產生什麼 HTML 腳本 :

以上說明, 大致順了一次 ViewModel 在 View 與 Controler 之間的交互方法。但有沒有可能 View 需要的只是一個字串、陣列或字典 ? 有沒有更便宜行事簡便的作法 ? 請繼續看。

ViewData / ViewBag / TempData

可以考慮使用 ViewData 跟 ViewBag, 而這兩類其實是兄弟, 操作起來略有不同, 先說操作的部分 :

HomeController.cs

public ActionResult ViewModel()
{
    ViewData["Message"] = "ViewData";
    ViewBag.Message = "ViewBag";
    TempData["Message"] = "TempData";
    
    return View();
}

ViewModel.csthml

@{
    ViewBag.Title = "View";
}
<h2>@ViewBag.Title.</h2>

<h3>Message</h3>
ViewData["Message"] : @ViewData["Message"] <br />
ViewBag.Message : @ViewBag.Message<br />
TempData["Message"] : @TempData["Message"]<br />
你覺得網址輸入 ~/home/viewmodel 會顯示出什麼結果 ?
跟你想的一樣嗎 ? 要不要再仔細看一次, 其實 ViewData 內容被蓋掉了 !
ViewData內容被蓋掉了 !
ViewData內容被蓋掉了 !
ViewData內容被蓋掉了 !
Why ?

賣個關子, 等等來探討。這些操作行為先掌握好, ViewData 與 TempData 都是字典, 用中括號來操作; 而 ViewBag 是一個 dynamic 物件, 用「點」運算式來操作。效能的部分略有差異,  dynamic 還是會稍微慢一些, 來試試 Controller set 100萬次, 跟 View get 100萬次, 這些類別的效能差異 :

好用的 ViewBag 效率比 ViewData 慢 30% 左右, 但因為好用, 要不要用、怎麼用還是見仁見智。但可確定 TempData 速度明顯慢很多。

到了深入討論的時刻了。

ViewBag vs ViewData

Short, the ViewBag is a dynamic object where you can store data to pass from controller to the view.
原來, 他們操作同一個 ViewDataDictionary 物件。ViewBag 雖然依賴 DynamicViewDataDictionary, 但 DynamicViewDataDictionary 創建時需要一個 ViewDataDictionary 依賴, ControllerBase.cs 的原始碼就是丟 ViewData 給他 :

public dynamic ViewBag
{
    get
    {
        if (_dynamicViewDataDictionary == null)
        {
            _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData);
        }
        return _dynamicViewDataDictionary;
    }
}
這也解釋了為何相同的 key 值前提下, ViewBag 或 ViewData 的後者都會把前者的值覆蓋。至於 ViewBag 比較慢, 簡單來說, 就是直接跟間接操作的關係 :
  • ViewData 是一個 ViewDataDictionary 類, 繼承 IDictionary<string, object>, 操作起來跟「直接」操作字典一樣單純 
  • ViewBag 是 DynamicViewDataDictionary 類, 繼承 DynamicObject, 創建時(建構子)需要傳入一個 ViewDataDictionary 變數, 然後「間接」來操作他, 就是間接操作 ViewData
有興趣的同學可以查看 ViewDataDictionary.cs 跟 DynamicViewDataDictionary.cs 的原始碼, 看他們的實作。

TempData

文章一開始有提到 TempData 可以跨請求如 Controller to Action or Action to Action, 再看一次 ControllerBase.cs 原始碼有關 TempData 的部分原來是這麼寫 :

public TempDataDictionary TempData
{
    get
    {
        if (ControllerContext != null && ControllerContext.IsChildAction)
        {
            return ControllerContext.ParentActionViewContext.TempData;
        }
        if (_tempDataDictionary == null)
        {
            _tempDataDictionary = new TempDataDictionary();
        }
        return _tempDataDictionary;
    }
    set { _tempDataDictionary = value; }
}
他是優先找 parent 的 TempData, 找到就回傳; 不僅如此, TempData 的類別 TempDataDictionary 內部還有許多操作, 資料保留期較 ViewBag / ViewData 長, 呼叫 Keep() 還可續命, 導致他的效能比 ViewBag 更慢。

程式筆記

筆記一下TempData的一種有趣的實驗, 看代碼之前先看一下流程圖 :
Add caption

也就是只操作 TempData["Action"] 變數, 比較前端導頁與後端導頁之後的結果, 有些情況 TempData 資料就被清掉了, 有時候卻還能 Keep()。Temp_3() 直接於Action 內呼叫 Redirect(), 所以等等不特別提及對應的 View 檔。

HomeController.cs

//TempData is missing While redirect via Javascript
public ActionResult Temp_1()
{
    if (TempData["Action"] == null)
        TempData["Action"] = "Temp_1";
    return View();
}
//Always call TempData.Keep()
public ActionResult Temp_2()
{
    return View();
}
//Redirect to Temp_2
public ActionResult Temp_3()
{
    if (TempData["Action"] == null)
        TempData["Action"] = "Temp_3";
    return RedirectToAction("Temp_2"); // or Redirect("~/Home/Temp_2");
}

Temp_1.csthml

//TempData is missing While redirect via Javascriptpublic ActionResult Temp_1()
{
    if (TempData["Action"] == null)
        TempData["Action"] = "Temp_1";
    return View();
}
//Always call TempData.Keep()
public ActionResult Temp_2()
{
    return View();
}
//Redirect to Temp_2
public ActionResult Temp_3()
{
    if (TempData["Action"] == null)
        TempData["Action"] = "Temp_3";
    return RedirectToAction("Temp_2"); // or Redirect("~/Home/Temp_2");
}

Temp_2.csthml

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
    ViewBag.Title = "Temp_2";
    TempData.Keep();
}
<h2>@ViewBag.Title.</h2>

<h3>Message</h3>
TempData["Action"] : @TempData["Action"] <br />

<input type="button" id="btnRedirect" value="Redirect" />

@section scripts {
    <script type="text/javascript">
        $(function () {
            $("#btnRedirect").click(function () {
                window.location.href = "@Url.Action("Temp_1", "Home")";
            });
        });
    </script>
}
TempData 飛來飛去的好療癒 (誤)。歡迎留言討論程式 !

參考資料

  • https://msdn.microsoft.com/zh-tw/library/system.dynamic.dynamicobject.aspx
  • https://msdn.microsoft.com/zh-tw/library/ms178586.aspx
  • https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/HtmlHelper.cs
  • https://github.com/mono/aspnetwebstack/blob/6248bfd24c31356e75a31c1b1030d4d96f669a6a/src/System.Web.Mvc/ViewDataDictionary.cs
  • https://github.com/mono/aspnetwebstack/blob/6248bfd24c31356e75a31c1b1030d4d96f669a6a/src/System.Web.Mvc/DynamicViewDataDictionary.cs
  • https://github.com/mono/aspnetwebstack/blob/6248bfd24c31356e75a31c1b1030d4d96f669a6a/src/System.Web.Mvc/TempDataDictionary.cs
  • https://github.com/Microsoft/referencesource/blob/4fe4349175f4c5091d972a7e56ea12012f1e7170/System.Web/State/SessionState.cs
  • https://github.com/mono/aspnetwebstack/blob/master/src/System.Web.Mvc/ControllerBase.cs
  • http://www.mytecbits.com/microsoft/dot-net/viewmodel-viewdata-viewbag-tempdata-mvc
  • https://www.codeproject.com/Tips/827059/Data-Passing-Mechanism-in-MVC-Architecture
  • https://stackoverflow.com/questions/6735423/where-can-i-find-the-official-documentation-for-dynamicviewdatadictionary
  • https://stackoverflow.com/questions/5908523/html-textbox-vs-html-textboxfor
  • http://www.dotnet-stuff.com/tutorials/aspnet-mvc/how-to-persist-data-with-tempdata

Comments