Hello World
Spiga

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

2009-09-22 11:05 by 老赵, 13405 visits

上一篇文章里已经把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吧。

相关文章

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

19 条回复

  1. Rain Shan
    *.*.*.*
    链接

    Rain Shan 2009-09-22 11:12:00

    老赵真快啊,刚刚看完中,下就出来了
    先顶一下,然后再读。

  2. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-22 11:20:00

    老赵啊 用枪逼着 mvc小组把这些加入2.0吧 T_T

  3. laudy
    *.*.*.*
    链接

    laudy 2009-09-22 11:23:00

    我实在太佩服您的速度了,刚还在看中,回到首页,发现下就来了,my god!!

  4. 老赵
    admin
    链接

    老赵 2009-09-22 11:27:00

    @韦恩卑鄙
    等我搞好了MvcPatch会去提的,这样有个案例好给他们参考。

  5. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-22 11:35:00

    @Jeffrey Zhao
    晚了我怕2.0赶不上。。。。

  6. 老赵
    admin
    链接

    老赵 2009-09-22 11:38:00

    @韦恩卑鄙
    应该不会吧……赶不上我就MvcPatch 2.0。

  7. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-09-22 13:03:00

    Jeffrey Zhao:
    @韦恩卑鄙
    应该不会吧……赶不上我就MvcPatch 2.0。


    T_T 每个版本都来个patch那就不爽了

  8. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-22 13:05:00

    @CoolCode
    4242

  9. 老赵
    admin
    链接

    老赵 2009-09-22 13:06:00

    @CoolCode
    我挺喜欢修改代码的,呵呵。
    不喜欢都等微软来搞,最佳实践要靠自己总结啊。
    既然他开源,那么就自己动手丰衣足食。

  10. 寒 刚入门
    *.*.*.*
    链接

    寒 刚入门 2009-09-22 13:13:00

    我爱老赵

  11. 寒 刚入门
    *.*.*.*
    链接

    寒 刚入门 2009-09-22 13:18:00

    的代码

  12. Mien++[未注册用户]
    *.*.*.*
    链接

    Mien++[未注册用户] 2009-09-22 18:37:00

    寒 刚入门:我爱老赵


    完了:)

    寒 刚入门:的代码



  13. 海清
    116.30.30.*
    链接

    海清 2010-05-22 11:41:39

    var cache = htmlHelper.ViewContext.HttpContext.Cache;
    var content = cache.Get(cacheKey) as string;
    var writer = htmlHelper.GetRecordWriter();
    
    if (content == null)
    {
        var recorder = new System.Text.StringBuilder();
        writer.AddRecorder(recorder);
    
        action();
    
        writer.RemoveRecorder(recorder);
        content = recorder.ToString();
        cache.Insert(cacheKey, content, cacheDependencies, absoluteExpiration, slidingExpiration);
    }
    else
    {
        htmlHelper.**Output**.Write(content);
    }
    

    GetRecordWriter是怎么得到的?

  14. 老赵
    admin
    链接

    老赵 2010-05-22 13:22:16

    @海清

    看前后文或代码吧。

  15. 海清
    116.30.30.*
    链接

    海清 2010-05-22 15:01:11

    老赵你好,我觉得这个太酷了,我急着用,你可以给点详细的代码给我吗?谢谢你啦!!我还是MVC菜鸟呢!!谢谢啦!

  16. 老赵
    admin
    链接

    老赵 2010-05-22 15:30:50

    @海清

    自认文章里写的很清楚,恕不接受伸手要代码的请求。

  17. 海清
    116.30.30.*
    链接

    海清 2010-05-22 15:35:25

    哦!!谢谢你,我研究了两天了!都还是搞不懂!!Output这个也不知道怎么得来!!

  18. 提问
    121.9.146.*
    链接

    提问 2010-05-25 22:46:00

    看了你很多文章之后我知道你为什么那么出名,我怎么也不明白你那里有那么多时间学习的呢?我一般下班回到家吃完饭都已经8点多,晚上十二点睡,但是学习的东西远没有你深入。 这难道是天分吗?天啊。。向你致敬

  19. xinqikan
    113.118.48.*
    链接

    xinqikan 2010-11-17 16:10:43

    怎么在后台获取view输出的内容?

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我