將 ASP.NET MVC 的 Controller 資料要傳給頁面, 或是頁面再轉只給其他頁, 有很多方式, Data Passing Mechanism in MVC Architecture 對於不同的資料拋轉方式整理出非常明確的結論, 在開始介紹代碼之前我們先有基本認知 :
頁面呈現如下, 如果忘記輸入 [Required] 欄位時, 前端就會阻擋並顯示對應訊息; 如果順利 Submit 到後端, 是一個不合法的用戶, 前端也順利顯示登入失敗訊息。
登入失敗情況, 在 @Html.ValidationSummary 代碼的位置, 順便截一下除錯畫面看會產生什麼 HTML 腳本 :
你覺得網址輸入 ~/home/viewmodel 會顯示出什麼結果 ?
跟你想的一樣嗎 ? 要不要再仔細看一次, 其實 ViewData 內容被蓋掉了 !
賣個關子, 等等來探討。這些操作行為先掌握好, ViewData 與 TempData 都是字典, 用中括號來操作; 而 ViewBag 是一個 dynamic 物件, 用「點」運算式來操作。效能的部分略有差異, dynamic 還是會稍微慢一些, 來試試 Controller set 100萬次, 跟 View get 100萬次, 這些類別的效能差異 :
好用的 ViewBag 效率比 ViewData 慢 30% 左右, 但因為好用, 要不要用、怎麼用還是見仁見智。但可確定 TempData 速度明顯慢很多。
到了深入討論的時刻了。
他是優先找 parent 的 TempData, 找到就回傳; 不僅如此, TempData 的類別 TempDataDictionary 內部還有許多操作, 資料保留期較 ViewBag / ViewData 長, 呼叫 Keep() 還可續命, 導致他的效能比 ViewBag 更慢。
在 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] 屬性
ViewModel
以下列舉3段簡化後的程式碼 :- LoginViewModel 的宣告
- AccountController 針對 Login Action 的處理
- Login.csthml 操作 LoginViewModel
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 />
跟你想的一樣嗎 ? 要不要再仔細看一次, 其實 ViewData 內容被蓋掉了 !
ViewData內容被蓋掉了 !
ViewData內容被蓋掉了 !
ViewData內容被蓋掉了 !Why ?
賣個關子, 等等來探討。這些操作行為先掌握好, ViewData 與 TempData 都是字典, 用中括號來操作; 而 ViewBag 是一個 dynamic 物件, 用「點」運算式來操作。效能的部分略有差異, dynamic 還是會稍微慢一些, 來試試 Controller set 100萬次, 跟 View get 100萬次, 這些類別的效能差異 :
到了深入討論的時刻了。
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; }
}
程式筆記
筆記一下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
Post a Comment