代码改变世界

适合ASP.NET MVC的视图片断缓存方式(下):页面输出原则

2009-09-22 11:05 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

上一篇文章里已经把Html.Cache打造成了非常具有可用性的API,需要缓存时我们只需在页面上做一个标记即可:

<% Html.Cache("cache_key", DateTime.Now.AddSeconds(10), () => { %>
 
    <% foreach (var article in Model.Articles) { %> 
        <p><%= article.Body %></p>
    <% } %>
    
<% }); %>

标记内部的写法和普通视图的写法相同,您可以for/foreach/if,也可以<%= %>,或者使用RenderPartial等其他辅助方法输出内容,都会被一并缓存下来。只可惜,上次文章末尾我提到有些效果是有前提的。

这个前提就是:某些RenderPartial和其他一些辅助方法的实现需要进行修改。好吧,再说的直接一些:如果您使用标准的ASP.NET MVC,就无法使用RenderPartial的功能。我认为造成这种问题的原因是ASP.NET MVC框架在实现时没有遵守页面内容输出的准则。所以我建议您使用MvcPatch项目进行ASP.NET MVC开发。

不过现在,我们还是来讨论一下准则吧。下面有些内容涉及到ASP.NET WebForm页面的输出方式,如果您遇到了不理解的地方,可以去看一下这篇文章,它是我为“页面片段缓存”原理介绍而写的“铺垫”。

在普通情况下,一个ASP.NET页面输出时是向一个封装了Response.Output的HtmlTextWriter中写入内容的:

而我们的片段缓存实现为了“捕获”某个缓存块输出的内容,则在HtmlTextWriter与Response.Output之间又插入了一个RecordWriter:

那么,在缓存命中的时候,我们的Cache方法把缓存中的内容写到什么地方去了呢?

public static void Cache(
    this HtmlHelper htmlHelper,
    ...)
{
    var content = ...

    if (content == null)
    {
        ...
    }
    else
    {
        htmlHelper.Output.Write(content);
    }
}

Output是什么?如果您观察ASP.NET MVC的源代码,您会发现HtmlHelper并没有这个属性。这是我在MvcPatch中暴露出来的一个TextWriter,它便是当前正用于页面输出的HtmlTextWriter对象。因此,我这里提出一个原则:如果您是在向页面输出内容,请务必将所有内容通过页面的Writer输出

在原来的ASP.NET MVC实现中,由于无法从HtmlHelper中获得页面的Writer,因此如果需要输出内容,则只能通过Response.Write方法,或由Response.Output输出内容了。根据上图可知,如果我们直接从Response.Output输出,那么这部分内容是无法被RecordWriter捕获的。这意味着什么呢?这意味着,如果我们上面不是通过HtmlHelper.Output,而是直接向Response.Output输出,在Html.Cache嵌套的情况下,内层缓存块的输出无法被外层缓存块捕获到。因此,如果内层缓存命中,而外层重新生成内容,则会发现内层缓存块的内容被没有被外层记录下来。

我们可以想的再远一些。我们这种TextWriter的嵌套其实是一种什么模式呢?应该算是装饰器模式吧。装饰器模式要求我们所有的输出都从链条的顶部输入,这样所有的“装饰”作用才会生效。如果我们获取了其中的某一个环节,直接从这个环节输入参数,那么自然是失败的。这意味着……假如又有另外一个组件在“行使”它的扩展权力呢?如果又有另一个组件,它在我们的RecordWriter外层又进行了包装呢?我们的片断缓存解决方案是一种扩展,作为扩展方案,不应该破坏其他组件正常扩展的能力。因此,我们需要从页面的Writer中输出内容。

一个很好的反例就是ASP.NET MVC框架,您看RenderPartial方法的输出目标是什么:Response.Output。还有FormExtensions及MvcForm对象的输出目标是什么:还是Response.Output。这意味着,ASP.NET MVC框架的做法直接破坏了视图的扩展能力。也直接放倒了我们的片断缓存实现。因此,我最终构建了MvcPatch项目,因为在这一点上(以及其他一些方面,之前也有所提及)使用扩展的方式实在是无法进行修补的。

所以国外社区有种调侃称,微软产品是好的,但是他们自己不知道该如何用好自己的产品。例如我一直说的WebForms的滥用,还有这里ASP.NET MVC实现。前者更像是一种商业策略,而后者可能……就令人摸不着头脑了。

我没有说“微软的确不知道如何用好自己的产品”。因为从ASP.NET MVC的代码中可以发现,好像他们并非不知道我刚提出的页面输出原则。证据在于,他们已经在ViewPage中留有一个“入口”了:

public class ViewPage : Page, IViewDataContainer
    ...

    public HtmlTextWriter Writer
    {
        get;
        private set;
    }

    protected override void Render(HtmlTextWriter writer)
    {
        Writer = writer;
        try
        {
            base.Render(writer);
        }
        finally
        {
            Writer = null;
        }
    }
}

看看这段代码在做什么?这段代码重写了Render方法,将外部传入的HtmlTextWriter对象保留了起来!这意味着ViewPage.Writer属性获得的便是当前正在输出的HtmlTextWriter对象!也就是说,ASP.NET MVC似乎在建议您说,如果您非要在页面上使用Response.Output输出的话,现在就改成Writer的输出吧:

<% Response.Write("Hello World"); %>
<% Writer.Write("Hello World"); %>

不知道是可惜还是可笑,如果您在代码中对Writer属性使用Find All References,您会发现除了在ViewMasterPage或ViewUserControl中继续暴露Writer属性之外,就再也没有使用过了……那么RenderPartial在做什么?FormExtensions在做什么?谁知道……我同样不知道的是,如果微软自己没有这个“意识”,那么又为什么要主动保留Render时的Writer呢?

不管这些了。我们最后总结一下:

  1. 如果您在使用WebForm模型,请像ViewPage那样保留当前Writer,并且向Writer内输出,不要搞Response.Write/Output。
  2. 如果您在编写视图的辅助方法,请向HtmlHelper.Output输出,而不是Reponse.Write/Output。
  3. 如果您发现其他项目在使用Response.Write/Output,请将它修改成页面的Writer输出。
  4. ……

嗯?您说HtmlHelper没有Output属性?没关系,下载代码以后自己修改编译一下,或直接使用MvcPatch吧。

相关文章