在看TempData的說明時,有人說用一次就刪除,有人說一個Request就結束,在道聽途說下,有一次我的Code就出了Bug,一直死在TempData,最後看Source Code才發現,我對TempData的認知出了錯誤。
原理
在ASP.NET MVC中資料傳遞主要有ViewData與TempData,ViewData主要是Controller傳遞Data給View,存留期只有一個Action,要跨Action要使用TempData,而TempData依TempDataProvider的不同,會有不同的存留期,預設的TempDataProvider是SessionStateTempDataProvider,你沒有看錯,預設是用Session來存放TempData,Session不是使用者存放資料,而且存留時間預設在20分鐘的嗎?
所以SessionStateTempDataProvider有做一些手段,Controller起來時,從Session載入TempData,然後刪除Session,所以在Action時是不會看到TempData的Session,在讀取TempData時,會記錄用了那些Key,在Controller結束時,會把沒有過的TempData在存回Session中,所以一直沒有讀取的TempData是會存在到Session消失的。
Note:
ViewData的存留期測試
HomeController.cs 片段 public ActionResult Index() { this.ViewData["Data"] = "Index"; return View(); } public ActionResult List() { //什麼Data都沒有輸出 return View(); }Index.aspx 片段 <div> Partial: <% //ViewData是使用Index,不會執行List的Action Html.RenderPartial("List"); %> </div> <div> Action: <% //ViewData是使用List,會執行List的Action Html.RenderAction("List"); %> </div> List.ascx 片段 <%:this.ViewData["Data"] %>
結果
執行Partial或RanderPartial是在同一個Action中直接呼叫View,共用同一個ViewData。
執行Action或RanderAction會呼叫另一個Action,那一個Action再呼叫View,使用不用的ViewData。
如果要在不同的Action中傳遞資料,要使用TempData。
錯誤重現
下列這段Code,猜猜有什麼Bug。
public ActionResult Index() { this.TempData["UseDefault"] = "true"; return View(); } public ActionResult List() { //在Index的View,會使用RanderAction呼叫List,但那一個區塊是會用Ajax重載 if (this.TempData.ContainsKey("UseDefault")) { //從Index的View,使用RanderAction呼叫,使用預設值 .......... } else { //從Ajax呼叫 ........... } return View(); }
答案是
呼叫this.TempData.ContainsKey("UseDefult")一直都是True,因為ContainsKey不是使用,所以TempData["UseDefult"]會一直保留在Session,直到Session消失前都是true,所以從Ajax呼叫一直都是使用預設值。
原始碼分析
載入與儲存時機
System.Web.Mvc.Controller.cs 片段 protected override void ExecuteCore() { //載入TempData PossiblyLoadTempData(); try { //呼叫Action ........... } finally { //儲存TempData PossiblySaveTempData(); }
TempData的一些操作
System.Web.Mvc.TempDataDictionary.cs 片段 //_data 是放Keys + Values //_initialKeys 是放Keys,使用時移除Key //_retainedKeys 是放有呼叫,Keep的Keys public void Load(ControllerContext controllerContext, ITempDataProvider tempDataProvider) { //載入放在Provider的資料 IDictionary<string, object> providerDictionary = tempDataProvider.LoadTempData(controllerContext); _data = (providerDictionary != null) ? new Dictionary<string, object>(providerDictionary, StringComparer.OrdinalIgnoreCase) : new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); _initialKeys = new HashSet<string>(_data.Keys, StringComparer.OrdinalIgnoreCase); _retainedKeys.Clear(); } public void Save(ControllerContext controllerContext, ITempDataProvider tempDataProvider) { //keysToKeep = _initialKeys + _retainedKeys string[] keysToKeep = _initialKeys.Union(_retainedKeys, StringComparer.OrdinalIgnoreCase).ToArray(); //keysToRemove = _data - keysToKeep string[] keysToRemove = _data.Keys.Except(keysToKeep, StringComparer.OrdinalIgnoreCase).ToArray(); //刪除使用過且不保留的Keys foreach (string key in keysToRemove) { _data.Remove(key); } //將沒有使用的TempData存起來 tempDataProvider.SaveTempData(controllerContext, _data); } public object this[string key] { get { object value; if (TryGetValue(key, out value)) { //讀取時刪除Key,在Save時用來比較 _initialKeys.Remove(key); return value; } return null; } set { _data[key] = value; _initialKeys.Add(key); } } public void Keep(string key) { //保留Key _retainedKeys.Add(key); }
Note:
我曾經想過寫一個Provider,資料是存放在HttpContext.Items,因為我習慣Temp的資料,在一個Request結束後就消失,不過專案成員們都覺得太多此一舉了,而作罷。
//自訂的TempDataProvider,沒辦法用設定改變(至少我沒找到),只能用繼承來覆寫CreateTempDataProvider作到統一使用 public class MyControllerBase : Controller { protected override ITempDataProvider CreateTempDataProvider() { return new HttpContextItemsTempDataProvider(); } } //使用 public class HomeControllerBase : MyControllerBase { }
參考資料