代码改变世界

UpdatePanel与ASP.NET Page的缓存

2007-02-04 11:18 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

UpdatePanel的功能大家一定都非常熟悉了。无论是官方还是社区里热心推广ASP.NET AJAX的朋友,都会对于UpdatePanel的使用进行大量说明与展示。但是在这些简单的的示例似乎都遗漏了一个非常重要的问题,这个问题会直接导致UpdatePanel无法正确使用。

这个问题就是ASP.NET Page的缓存。

ASP.NET Page是个非常强大的模型,缓存是它的重要特性。一个成熟的ASP.NET应用程序几乎都会使用缓存,它能够显著得提高性能,减少服务器端生成页面或者控件内容的消耗。不过现在出现了UpdatePanel这个“神奇”的控件,如果使用缓存不当,就会让我们的应用程序出现错误。

 

重现问题

我们还是来编写一个使用UpdatePanel的简单示例,如下:

<%@ Page Language="C#" AutoEventWireup="true" %>
<%@ OutputCache Duration="100" VaryByParam="None" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Cached Page</title>
</head>
<body>
    <form id="form1" runat="server">
        <asp:ScriptManager ID="ScriptManager1" runat="server" />
        
        <asp:UpdatePanel ID="UpdatePanel1" runat="server">
            <ContentTemplate>
                <%= DateTime.Now.ToString() %>
                <asp:Button ID="Button1" runat="server" Text="Async PostBack" />
            </ContentTemplate>
        </asp:UpdatePanel>
<asp:Button ID="Button2" runat="server" Text="PostBack" /> </form> </body> </html>

 

我们打开页面,依次做一下操作,并察看页面上显示的时间。

  1. 多次刷新页面,时间不会改变。
  2. 多次点击Async PostBack按钮,页面部分刷新,时间每次都会改变。
  3. 多次点击PostBack按钮,页面完全刷新,时间只会更新一次,然后时间保持不变。
  4. 点击Async PostBack按钮,发生错误。

直到等待时间超过100秒(Cache过期),点击Async PostBack才工作正常,直到用户再次点击过PostBack按钮。

 

分析问题

使用Fiddler等工具察看出现错误时的Response Body,会发现页面返回了整张页面的内容,而不是UpdatePanel熟悉的特殊的字符串。要说明这个问题,需要再了解一下UpdatePanel的工作方式。

  1. 点击Async PostBack按钮之后,客户端采集页面上所有的信息,使用XMLHttpRequest发起一个请求,并且会在请求的Header信息中设置一个x-microsoftajax的值为Delta=true。
  2. 服务器端的Page页面接受到请求之后,并不会得知这是由XMLHttpRequest发出的请求,因此和常规一样进行页面中所有的工作,例如触发控件事件等等。
  3. 页面上的ScriptManager控件在OnInit时,一旦发现Request的Header里有正确的x-microsoftajax信息,将会意识到目前在进行异步的PostBack。
  4. 如果ScriptManager得知现在正在异步PostBack,则在OnPreRender时调用Pate.SetRenderDelegate方法,提供自定义的方法用于输出页面。
  5. 在自定义的输出方法中,ScriptManager采集所有的信息(例如哪些UpdatePanel进行了更新,需要输出的脚本等等),并且输出客户端能够识别的内容。
  6. 客户端收到信息后,对其进行分析,并且更新页面。

在正常情况下,这个流程应该顺利的跑完,但是如果页面内容在服务器端被缓存了呢?将会出现下面的情况:

  1. 点击Async PostBack按钮,客户端采集页面上所有的信息,使用XMLHttpRequest发起一个请求。
  2. Page在接受到请求之后,发现页面中存在缓存信息、并且缓存没有过期,于是直接输出缓存中的页面内容。
  3. 客户端收到了完整的页面内容,无法识别,抛出异常。

但是为什么在出现一个传统的PostBack之前,异步PostBack都能够正常工作呢?我怀疑这是因为ASP.NET的Page对象是在Render时一并处理缓存的,当ScriptManager替换了Page自带的方法后,就不会对输出内容进行缓存了。这导致了接下来的异步更新发生了错误。这里是我根据出现的状况进行的猜测,哪位朋友如果了解ASP.NET Page缓存的确切情况,请不吝指教。:)

 

解决问题

说实话,如果一个页面已经使用了缓存,我还没有想到一个操作简单,但又能保持其缓存特性的解决方案。另外,我甚至想合理利用这种缓存机制来提高异步更新时的性能(也就是尝试着让页面缓存异步更新的内容)。我查阅了ASP.NET Page缓存的资料,作了一些尝试,但是依旧无法得出一个比较好的解决方案。可能的确是ScriptManager替代页面输出方法造成的问题吧,既然无法在那时处理缓存,又如何能够避免页面缓存的问题,甚至利用缓存来提高性能呢?

如果要让UpdatePanel正常工作的话,只能取消页面级别的缓存了。不过控件级别的缓存还是能够使用的,下面的例子就可以看到控件缓存与UpdatePanel同时工作时的状况:

<%@ Control Language="C#" AutoEventWireup="true" %>
<%@ OutputCache Duration="100" VaryByParam="None" %>
<%= DateTime.Now.ToString() %>
<%@ Page Language="C#" AutoEventWireup="true" %>
<%@ Register Src="CachedControl.ascx" TagName="CachedControl" TagPrefix="uc1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Cached Control</title>
</head>
<body>
    <form id="form1" runat="server">
        <asp:ScriptManager ID="ScriptManager1" runat="server">
        </asp:ScriptManager>
        
        <asp:UpdatePanel ID="UpdatePanel1" runat="server">
            <ContentTemplate>
                <uc1:CachedControl ID="CachedControl1" runat="server" />
                <br />
                <%= DateTime.Now.ToString() %>
                <asp:Button ID="Button1" runat="server" Text="Async PostBack" />
            </ContentTemplate>
        </asp:UpdatePanel>
        
        <asp:Button ID="Button2" runat="server" Text="PostBack" />
    </form>
</body>
</html>

 

在这里,我们把缓存加于CachedControl控件上,打开CachedControl.aspx之后并进行多次传统PostBack和异步PostBack都不会出现问题,从页面上两个不同的时间也可以看出控件的缓存生效了。