[MVC]适合ASP.NET MVC的视图片断缓存方式(中):更实用的API

转载:http://www.cnblogs.com/JeffreyZhao/archive/2009/09/21/aspnet-mvc-fragment-cache-2-more-friendly-api.html

  上一篇文章中我 们提出了了片断缓存的基本方式,也就是构建HtmlHelper的扩展方法Cache,接受一个用于生成字符串的委托对象。在缓存命中时,则直接返回缓存 中的字符串片断,否则则使用委托生成的内容。因此,缓存命中时委托的开销便节省了下来。不过这个方法并不实用,如果您要缓存大片的HTML,还需要准备一 个Partial View,再用它来生成网页片段:

<%= Html.Cache(..., () => Html.Partial("MyPartialViewToCache")) %>

  但是在实际开发过程中,我们最乐于看到的使用方法,应该只是使用某个标记来“围绕”一段现有的代码。也就是说,我们希望的API使用方式可能是这样的:

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

<% }); %>

  我们可以从这种“表现形式”上推断出这个Cache方法的签名:

public static void Cache(
this HtmlHelper htmlHelper,
string cacheKey,
CacheDependency cacheDependencies,
DateTime absoluteExpiration,
TimeSpan slidingExpiration,
Action action)
{
...
}

  与前一个扩展相比,最后一个委托参数变成了Action,而不是Func<string>。这是因为ASP.NET页面在编译时,会将页面Cache块中的代码,编译为内容的输出方式——这点在之前的文章中已经有过比较详细的描述。不过有一点还是与之前相同的,我们要省下的是action委托的开销。也就是说,如果缓存命中,则不执行action。缓存没有命中,则执行action,获得action生成的字符串,加入缓存并输出。

  看似比较简单,但这里有个问题:如之前的Func<string>参数,我们执行后自然可以获得一个字符串作为结果。但是现在是个action,执行后它又把内容输出到什么地方去,我们又该如何得到这里生成的字符串呢?根据页面输出行为,我们可以推断出页面上的内容是被写入一个HtmlTextWriter中的。那么,这个HtmlTextWriter又是如何生成的呢?

  它是根据Page类型的CreateHtmlTextWriter方法生成的:

protected virtual HtmlTextWriter CreateHtmlTextWriter(System.IO.TextWriter tw) { ... }

  在页面准备生成内容之前,Page会调用其CreateHtmlTextWriter来包装一个TextWriter,这个 TextWriter一般即是由Response.Output暴露出来的HttpWriter对象。CreateHtmlTextWriter方法生成 的HtmlTextWriter,便会交给Page的Render方法用于输出页面内容了。这便是我们的入手点,我们可以趁此机会在 HtmlTextWriter和CreateHtmlTextWriter之间“插入”一个组件。这个组件除了将外部传入的数据传入内部的 TextWriter以外,还有着“纪录”内容的功能:

internal class RecordWriter : TextWriter
{
public RecordWriter(TextWriter innerWriter)
{
this.m_innerWriter = innerWriter;
}
private TextWriter m_innerWriter;
private List<StringBuilder> m_recorders = new List<StringBuilder>();
public override Encoding Encoding
{
get { return this.m_innerWriter.Encoding; }
}
public override void Write(char value) { ... }
public override void Write(string value)
{
if (value != null)
{
this.m_innerWriter.Write(value);
if (this.m_recorders.Count > 0)
{
foreach (var recorder in this.m_recorders)
{
recorder.Append(value);
}
}
}
}
public override void Write(char[] buffer, int index, int count) { ... }
public void AddRecorder(StringBuilder recorder)
{
this.m_recorders.Add(recorder);
}
public void RemoveRecorder(StringBuilder recorder)
{
this.m_recorders.Remove(recorder);
}
}

  一个TextWriter有数十个可以覆盖的成员,但是一般情况下我们只需覆盖其中三个Write方法就 可以了。以上代码用Write(string)作为示例,可以看出,如果RecordWriter中添加了Recorder之后,便会将外界写入的内容再 交给Recorder一次。换句话说,如果我们希望纪录页面上写入Writer的内容,只要在RecordWriter里添加Recorder就可以了。 当然,在此之前我们需要为视图页面“开启”缓存功能:

// 定义在CacheExtensions中
public static TextWriter CreateCacheWriter(this HtmlHelper htmlHelper, TextWriter writer)
{
var recordWriter = new RecordWriter(writer);
htmlHelper.SetRecordWriter(recordWriter);
return recordWriter;
}
// 定义在视图页面(aspx)中
<script runat="server">
protected override HtmlTextWriter CreateHtmlTextWriter(System.IO.TextWriter tw)
{
return base.CreateHtmlTextWriter(Html.CreateCacheWriter(tw));
}
</script>

  当然,在实际开发过程中不会再aspx中重写CreateHtmlTextWriter方法,我们往往会将其放在视图页面的共同基类中。例如在 我的项目中,我就为所有的视图“开启”了这种纪录功能。由于在没有缓存的情况下这层薄薄的封装只是在做一个“转发”功能,因此不会带来性能问题。

  此时,新的Cache方法便非常直观了:

public static void Cache(
this HtmlHelper htmlHelper,
string cacheKey,
CacheDependency cacheDependencies,
DateTime absoluteExpiration,
TimeSpan slidingExpiration,
Action action)
{
var cache = htmlHelper.ViewContext.HttpContext.Cache;
var content = cache.Get(cacheKey) as string;
var writer = htmlHelper.GetRecordWriter();
if (content == null)
{
var recorder = new StringBuilder();
writer.AddRecorder(recorder);
action();
writer.RemoveRecorder(recorder);
content = recorder.ToString();
cache.Insert(cacheKey, content, cacheDependencies, absoluteExpiration, slidingExpiration);
}
else
{
htmlHelper.Output.Write(content);
}
}

  如果缓存没有命中,则我们会向RecordWriter中添加一个Recorder,然后再执行action委托,这样action中的所有内容便会被纪录下来。action执行完毕后,我们再摘除Recorder即可。现在Cache方法已经可用了,例如:

<%= DateTime.Now %>
<br />
<% Html.Cache("now", DateTime.Now.AddSeconds(5), () => { %>
    <%= DateTime.Now %>

<% }); %>

  那么,Html.Cache能否嵌套呢?答案也是肯定的。

<%= DateTime.Now %>
<br />
<% Html.Cache("now", DateTime.Now.AddSeconds(5), () => { %>
    <%= DateTime.Now %>
    <br />
<% Html.Cache("inner_now", DateTime.Now.AddSeconds(10), () => { %>

<% Html.RenderPartial("CurrentTime"); %>

<% }); %>

<% }); %>

  外层缓存块5秒后过期,内存缓存块10秒钟过期,因此在某一时刻(如第一次刷新后7秒后),您会发现页面上会出现这样的结果:

2009/9/21 15:36:10
2009/9/21 15:36:08
2009/9/21 15:36:03

  我们的RecordWriter支持同时拥有多个recorder,您可以根据上面得出的结果来理解内外层循环是以何种顺序向RecordWriter添加Recorder的,这并不困难。

  从代码中我们也可以发现,Cache块内部也可以直接使用Html.RenderPartial。您也可以在Cache块内部使用各种辅助方法,它们的结果会被一并缓存下来。

  不过它们还是有“前提”的,至于这个前提是什么,我们下次在讨论吧。如果您想先睹为快,可以关注MvcPatch项目。

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏