在看TempData的說明時,有人說用一次就刪除,有人說一個Request就結束,在道聽途說下,有一次我的Code就出了Bug,一直死在TempData,最後看Source Code才發現,我對TempData的認知出了錯誤。
在看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的存留期測試
public ActionResult Index() { this.ViewData["Data"] = "Index"; return View(); } public ActionResult List() { //什麼Data都沒有輸出 return View(); }
<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。
{
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呼叫一直都是使用預設值。
原始碼分析
載入與儲存時機
protected override void ExecuteCore() {
//載入TempData
PossiblyLoadTempData();
try {
//呼叫Action
...........
}
finally {
//儲存TempData
PossiblySaveTempData();
}
TempData的一些操作
//_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結束後就消失,不過專案成員們都覺得太多此一舉了,而作罷。
public class MyControllerBase : Controller { protected override ITempDataProvider CreateTempDataProvider() { return new HttpContextItemsTempDataProvider(); } } //使用 public class HomeControllerBase : MyControllerBase { }
參考資料