ASP.NET MVC Preview 2 - RedirectToAction

[ 2008-03-14 11:54:31 | 作者: yuhen ]
字号: | |
其实 RedirectToAction 本身并没有多少可说的,无非是用 "Response.Redirect" 让客户端进行跳转而已。
protected virtual void RedirectToAction(RouteValueDictionary values)
{
    VirtualPathData virtualPath = this.RouteCollection.GetVirtualPath(this.ControllerContext, values);
    string url = null;
    if (virtualPath != null)
    {
        url = virtualPath.VirtualPath;
    }

    this.HttpContext.Response.Redirect(url);
}

我们关注的焦点是在 RedirectToAction 调用时,如何将相关数据传递给下一个 Action 方法。MVC Controller 提供了一个称之为 TempData 的字典属性作为 "转场"。
public class Controller : IController
{
    protected internal virtual void Execute(ControllerContext controllerContext)
    {
        // ...

        this.ControllerContext = controllerContext;
        this.TempData = new TempDataDictionary(controllerContext.HttpContext);

        // ...
    }

    public TempDataDictionary TempData { get; set; }
}

如果我们看代码认真一些,会发现一个问题:RedirectToAction 调用 Response.Redirect(url) 必然会导致新的 Controller 实例被创建,而这个 TempData 同样也会是一个在 Controller.Execute() 中诞生的 "新人"。那么 "老" 从何而来?对于会话(Session)级别的数据共享,我们会想到谁?HttpContext.Session ?也许是吧,既然 Controller.Execute() 里面没有线索,我们不妨深入到 TempDataDictionary 里面看看。
public class TempDataDictionary : ...
{
    public TempDataDictionary(HttpContextBase httpContext)
    {
        // ...

        this.HttpContext = httpContext;
        this.EnsureReadData();
    }

    private void EnsureReadData()
    {
        if (this._sessionData == null)
        {
            if (this.HttpContext.Session != null)
            {
                this._sessionData = this.HttpContext.Session[TempDataSessionStateKey] as 
                    Pair<Dictionary<string, object>, HashSet<string>>;
            }

            if (this._sessionData != null)
            {
                this.HttpContext.Session.Remove(TempDataSessionStateKey);
                HashSet<string> set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

                foreach (string str in this._sessionData.First.Keys)
                {
                    if (!this._sessionData.Second.Contains(str))
                    {
                        set.Add(str);
                    }
                }

                foreach (string str2 in set)
                {
                    this._sessionData.First.Remove(str2);
                }

                this._sessionData.Second.Clear();
            }
            else
            {
                this._sessionData = new Pair<Dictionary<string, object>, HashSet<string>>(
                    new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase), 
                    new HashSet<string>(StringComparer.OrdinalIgnoreCase));
            }
        }
    }
}

在 EnsureReadData() 中,MVC 首先使用一个固定的 key —— TempDataSessionStateKey 从 Session 中获取 _sessionData,如果这个 _sessionData 不为空,那么就对其做些 "处理",并将其引用从 Session 中删除。当然,没找到的话,自然是创建一个新的了。这应该就是 "老数据" 得以传递的秘密了。

等等…… 就算 TempData 从 Session 中获取了 "老数据",那么我们在 Controller.RedirectToAction() 中并没有看到有任何代码将这个 TempData 写入 Session …… 嗯,这是个大问题,没有写,又如何读呢?问题的奥秘还是在 TempDataDictionary 中。
public class TempDataDictionary : ...
{
    public void Add(string key, object value)
    {
        this.EnsureWriteData(key);
        this._sessionData.First.Add(key, value);
    }

    public object this[string key]
    {
        get
        {
            object obj2;
            if (this._sessionData.First.TryGetValue(key, out obj2))
            {
                return obj2;
            }
            return null;
        }
        set
        {
            this.EnsureWriteData(key);
            this._sessionData.First[key] = value;
        }
    }
}

无论是 Add 方法还是索引器,只要我们视图 "修改" 这个字典时都会调用一个名为 "EnsureWriteData(key)" 的方法。
private void EnsureWriteData(string key)
{
    HttpSessionStateBase session = this.HttpContext.Session;
    if (session != null)
    {
        session[TempDataSessionStateKey] = this._sessionData;
    }

    if (key == null)
    {
        this._sessionData.Second.Clear();
    }
    else if (!this._sessionData.Second.Contains(key))
    {
        this._sessionData.Second.Add(key);
    }
}

而这个 "EnsureWriteData(string key)" 第一要做的就是将 _sessionData 加到 HttpContext.Session 里面。好了,找到读,也找到了写,似乎是结束了。但还有一个问题,第一个 Action 将数据写入 TempData,然后调用 RedirectToAction() 跳转到第二个 Action,这第二个 Action 除了读取 "老数据" 外,还做了一份额外的工作,就是将 "老数据" 从 HttpContext.Session 中删除了。也就是说,"老数据" 的生命周期也就到此为止了,是不可能再往后传递的。
public void Action1()
{
    this.TempData["a"] = "Hello...";
    this.RedirectToAction("action2");
}

public void Action2()
{
    Debug.WriteLine(this.TempData["a"], "Action2");
    this.RedirectToAction("action3");
}

public void Action3()
{
    Debug.WriteLine(this.TempData["a"], "Action3");
    Response.Write("Action3...");
}

输出:
Action2: Hello...
Action3:

问题似乎没说清楚…… 假如我们 "修改" Action2 TempData,那么这个 _sessionData 又会再次写入 HttpContext.Session,这样是否可以将 "Action1 老数据" 继续带到 Action3 呢?试验一下。
public void Action1()
{
    this.TempData["a"] = "Hello...";
    this.RedirectToAction("action2");
}

public void Action2()
{
    Debug.WriteLine(this.TempData["a"], "Action2");
    this.TempData["b"] = "xxx";
    this.RedirectToAction("action3");
}

public void Action3()
{
    Debug.WriteLine(this.TempData["a"], "Action3");
    Response.Write("Action3...");
}

输出:
Action2: Hello...
Action3:

试验结果证明不行。为什么会这样?问题还是出在 TempDataDictionary 身上。TempDataDictionary._sessionData 使用一种看起来很奇怪的存储方式,First 是一个 Dictionary,用来存储我们添加的数据,Second 是一个 HashSet 用来存储 First.Key,这看起来有些莫名奇妙。别着急,当 "老数据" 在 Action2 中被提取时,它做了些附加工作。
internal sealed class Pair<TFirst, TSecond>
{
    public TFirst First { get; }
    public TSecond Second { get; }
}

public class TempDataDictionary : ...
{
    private Pair<Dictionary<string, object>, HashSet<string>> _sessionData;

    private void EnsureReadData()
    {
        if (this._sessionData == null)
        {
            if (this.HttpContext.Session != null)
            {
                this._sessionData = this.HttpContext.Session[TempDataSessionStateKey] as 
                    Pair<Dictionary<string, object>, HashSet<string>>;
            }

            if (this._sessionData != null)
            {
                this.HttpContext.Session.Remove(TempDataSessionStateKey);
                HashSet<string> set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

                // 1. 循环获取 "老数据" 字典(First) 的键(Key)
                foreach (string str in this._sessionData.First.Keys)
                {
                    // 如果这个键不包含在 Second 中,则添加到一个哈希列表中。
                    if (!this._sessionData.Second.Contains(str))
                    {
                        set.Add(str);
                    }
                }

                // 2. 将那些没有在 Second 登记的数据从 First 字典中删除
                foreach (string str2 in set)
                {
                    this._sessionData.First.Remove(str2);
                }

                // 3. 清空 Second。
                this._sessionData.Second.Clear();
            }
            else
            {
                this._sessionData = new Pair<Dictionary<string, object>, HashSet<string>>(
                    new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase), 
                    new HashSet<string>(StringComparer.OrdinalIgnoreCase));
            }
        }
    }
}

我们先看 "注释3",它将老数据的 Key 登记从 Second 中全部清除了,也就是说以后登记的都只是 Action2 中新 "修改" 的数据键。在 Action2 时,"注释1" 和 "注释2" 处的代码并没有起到作用,可一旦执行流程到了 Action3 时,它们就积极行动起来了。在 "注释1" 处,所有保留在 First 中的 "Action1 老数据" 必然无法从 Second 中找到登记记录,所以通通被加入到待删除列表 set 中。"注释2" 则是个 "杀手",它砍死了所有的 "Action1 老数据",留下的只有 "Action2 老数据" 了。唉~~~~ 只有新人笑,不见旧人哭啊。

好了,老来老去的,但愿你没犯迷糊。 [sad]
[最后修改由 yuhen, 于 2008-03-14 11:58:01]
评论Feed 评论Feed: http://www.rainsts.net/feed.asp?q=comment&id=671

这篇日志没有评论。

发表评论
表情图标
[smile] [confused] [cool] [cry]
[eek] [angry] [wink] [sweat]
[lol] [stun] [razz] [redface]
[rolleyes] [sad] [yes] [no]
[heart] [star] [music] [idea]
UBB代码
转换链接
表情图标
悄悄话
用户名:   密码:  
验证码 * 请输入验证码