ASP.NET MVC TempData使用心得

在看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"] %>

 

結果

 image

執行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
{     
}


 

參考資料