ASP.net如何在控件公共属性上使用输出缓存来缓存WebUserControl

5
我有一个Web用户控件,它执行一些可能会产生大量数据计算的任务,我希望对其进行输出缓存,以便每个页面视图不会重新计算数据。它位于非常频繁访问的页面上,所以让它正确运行非常重要!
为了提供背景信息,它是我们游戏中心使用的: http://www.scirra.com/arcade/action/93/8-bits-runner 单击“统计信息”,图表和统计数据的数据来自此Web用户控件。
该控件的开头如下:
public partial class Controls_Arcade_Data_ArcadeChartData : System.Web.UI.UserControl
{
    public int GameID { get; set; }
    public Arcade.ChartDataType.ChartType Type { get; set; }

    protected void Page_Load(object sender, EventArgs e)
    {

现在我遇到的困难是输出缓存需要依赖于GamID和ChartType两个因素。
这个控件被多次重复使用,每次使用都有不同的GameID和Type组合,我需要为每个组合创建一个缓存,但我很难找到如何实现。
例如,一个街机游戏可能传递GameID = 93Type = GraphData,另一个游戏可能是GameID = 41Type = TotalPlaysData,还有一个可能是GameID = 93Type = TotalPlaysData。它们应该返回不同的数据并有不同的输出缓存。
该控件在游戏页面上使用,类似于以下方式(实际参数设置在代码后端)。
<div>Total Plays:</div>
<div class="count"><Scirra:ArcadeChartData runat="server" GameID="93" Type="TotalPlays" /></div>
<br /><br />
<div>Total Guest Plays:</div>
<div class="count"><Scirra:ArcadeChartData runat="server" GameID="93" Type="TotalGuestPlays" /></div>

etc.

任何帮助都将不胜感激!我花了一段时间在网上寻找答案,但仍无法解决这个问题。
编辑:
编辑:我尝试将此行添加到我的控件中: <%@ OutputCache Duration="20" VaryByControl="GameID;Type" %>
但是它只是在使用控件的ASPX页面上第一次设置 GameID 时,在该行上抛出错误对象引用未设置为对象的实例。
8个回答

10
当从输出缓存中检索控件时,它不会被实例化为可以操作的实例;你只能获取控件生成的输出,而不能获取控件本身。例如,你无法在代码后端设置缓存控件的属性,就像你在问题中所说的一样。应该使用声明性的vary-by属性(使用ExpressionBuilder也可以,尽管我没有尝试过)。要在代码后端查看控件是否已从输出缓存中检索,请检查是否为空值:
if (this.YourControlID != null) // true if not from cache
{
    // do stuff
}

即使需要注意这一点,控制输出缓存仍然有点古怪。
尝试这个:
<%@ OutputCache Duration="20" VaryByControl="GameID;Type" Shared="true" %>

控件的输出通过与特定键相关联的方式存储在输出缓存中。使用Shared="true",缓存键是所有指定属性的值和控件ID的组合。没有使用Shared="true",缓存键还包括页面类型,因此输出将根据页面而变化--这似乎不是您想要的。
如果您在多个页面上使用该控件,请确保在每个页面上使用相同的ID(如果可能),因为ID作为输出缓存的键的一部分被包括在内。如果不能或不使用不同的ID,则对于每个唯一的ID,您将在缓存中获得该控件输出的新副本。如果具有不同ID的控件始终具有不同的属性值,那可能不是一个问题。
作为OutputCache指令的替代方案,您可以在类声明上设置一个属性。
[PartialCaching(20, null, "GameID;Type", null, true)]
public partial class Controls_Arcade_Data_ArcadeChartData : UserControl

由于他想在同一页中多次使用控件,因此这个解决方案是无效的。不能在同一页中为多个控件使用相同的ID。 - Chamika Sandamal
2
在同一页上使用不同的ID是可以的;只需记住,ID是缓存键的一部分,因此可能会获得多个相同输出的副本。如果类型始终基于页面上控件的位置设置,就像问题中一样,那可能不是问题。 - RickNZ
这似乎按照我的意愿工作!我会再进行一些测试并回报结果。 - Tom Gullen
它的行为很不寻常,但基本上有效!感谢您的回答! - Tom Gullen

3
你需要执行以下步骤:

1)向页面中添加以下输出缓存指令:

<%@ OutputCache Duration="21600" VaryByParam="None" VaryByCustom="FullURL" %>

2) 将以下内容添加到global.asax中:

    public override string GetVaryByCustomString(HttpContext context, string arg)
    {
        if (arg.Equals("FullURL", StringComparison.InvariantCultureIgnoreCase)
        {
            // Retrieves the page
            Page oPage = context.Handler as Page;

            int gameId;

            // If the GameID is not in the page, you can use the Controls 
            // collection of the page to find the specific usercontrol and 
            // extract the GameID from that.

            // Otherwise, get the GameID from the page

            // You could also cast above
            gameId = (MyGamePage)oPage.GameID;

            // Generate a unique cache string based on the GameID
            return "GameID" + gameId.ToString();
        }
        else
        {
            return string.Empty;
        }
    }

您可以从MSDN获取有关GetVaryByCustomString方法的更多信息,并查看其他缓存选项此处


我这样想是正确的吗,根据页面的URL它将被缓存6小时?数据将出现在许多URL上,我只想基于“GameID”进行缓存。 - Tom Gullen
@TomGullen:持续时间以秒为单位,因此我在其中设置的值仅适用于10分钟;我已将其调整为正确的值(21600)。该方法的设计方式是,在完整的URL上进行缓存,但如果您有其他查询字符串参数,则可以根据需要在GetVaryByCustomString方法中调整此行为。 - competent_tech
我没有查询字符串参数,但我有一个控件,它有一个公共属性,我想要缓存。 - Tom Gullen
不用担心,感谢你迄今为止的回答,我很感激你的时间!当您在ASPX页面上查看游戏时,将分配GameID。这是我们的街机http://www.scirra.com/arcade,如果您单击游戏,然后单击游戏下面的“统计信息”,它会显示总播放次数。只需要缓存它,目前每次都要重新计算。 - Tom Gullen
我已经在这上面悬赏了500美元,你知道怎么做吗? - Tom Gullen
显示剩余2条评论

2
在代码中创建一个缓存对象。
HttpRuntime.Cache.Insert("ArcadeChartData" + GameID + Type, <object to cache>, null, System.Web.Caching.Cache.NoAbsoluteExpiration,new TimeSpan(0, 0, secondsToCache),CacheItemPriority.Normal, null);

上面的缓存项目已经足够你的工作,但如果你真的想要使用输出缓存,可以尝试在代码后台使用以下代码:

Response.AddCacheItemDependency("ArcadeChartData" + GameID + Type);
Response.Cache.SetExpires(DateTime.Now.AddSeconds(60));
Response.Cache.SetCacheability(HttpCacheability.Public);
Response.Cache.SetValidUntilExpires(true);

设置页面输出缓存的值与通过 Cache 属性操作 SetExpires 和 SetCacheability 方法相同。

我肯定想使用输出缓存,因为我不希望每个新访问者都重新渲染对象,如果我们有一个在一天内有10,000次游戏播放的热门游戏,这可能会对服务器造成很大压力。因此,我认为输出缓存是最好的选择。 - Tom Gullen
你可以将它放在页面加载中。 - Chamika Sandamal
这似乎不起作用。我将代码添加到控件的Page_Load中,但没有效果。 - Tom Gullen
@TomGullen:记住,输出缓存将缓存用户控件的整个输出。在上面的示例中,当您使用不同的GameID或Type时,它将使输出缓存失效,但不会使缓存对象失效。因此,您无需再次处理逻辑。现在,您有基于GameID和Type的多个缓存对象。但是输出缓存只有一个版本。它不能保留多个版本。因此,当您放置不同的GameID或Type时,它将使用上述缓存对象重新创建输出缓存(而无需处理整个过程)。 - Chamika Sandamal
我所说的“不起作用”是指没有效果,我刷新页面后计数器会增加,但我希望计数器被缓存。你提供的方法似乎没有效果,计数器/输出仍然一次递增一个。 - Tom Gullen
显示剩余5条评论

1

我知道我的解决方案可能看起来非常简单,甚至有些奇怪,但我已经尝试过它并且它有效。你只需要在你的用户控件中添加这一行代码。

<%@ OutputCache Duration="10" VaryByParam="none" %>

注意:我只测试了Framework 4.0。如果您需要更改UserControl中属性的值(例如此示例中的MyInt,MyString),请在Page_Init事件中执行。
以下是我的所有代码:
页面:
<%@ Page Title="Home Page" Language="vb" MasterPageFile="~/Site.Master" AutoEventWireup="false" CodeBehind="Default.aspx.vb" Inherits="MyWebApp._Default" %>
<%@ Register Src="~/UserControl/MyUserControl.ascx" TagPrefix="uc" TagName="MyUserControl" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>

<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <uc:MyUserControl ID="uc1" MyInt="1" MyString="Test"  runat="server" />
    <hr />
    <uc:MyUserControl ID="uc2" MyInt="3" MyString="Test"  runat="server" />
    <hr />      
    <uc:MyUserControl ID="uc3" MyInt="1" MyString="Testing" runat="server" />
</asp:Content>

用户控件:

<%@ Control Language="vb" AutoEventWireup="false" CodeBehind="MyUserControl.ascx.vb" Inherits="MyWebApp.MyUserControl" %>
<%@ OutputCache Duration="10" VaryByParam="none" %>

<div style="background-color:Red;">
    Test<br />
    <asp:Label ID="lblTime" ForeColor="White" runat="server" />
</div>

用户控制代码:

Public Class MyUserControl
    Inherits System.Web.UI.UserControl

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        Debug.Write("Page_Load of {0}", Me.ID)
        Dim oStrBldr As New StringBuilder()
        For i As Integer = 1 To Me.MyInt
            oStrBldr.AppendFormat("{0}: {1} - {2} at {3}<br />{4}", Me.ID, i, Me.MyString, Date.Now.ToLongTimeString(), System.Environment.NewLine)
        Next
        Me.lblTime.Text = oStrBldr.ToString()
    End Sub


    Public Property MyInt As Integer
    Public Property MyString As String

End Class

请随时告知我最新情况,如果您需要其他解决方案,我还有一些更复杂的。我也可以使用C#进行发布。

当我尝试在包含控件的ASPX页面中设置GameID = TheGame.ID;时,它无法正常工作并且会在尝试加载缓存时抛出Object reference not set to an instance of an object.错误。 - Tom Gullen
这实际上是有效的。看起来 asp.net 在缓存键中使用了标记中定义的属性(在本例中为 <uc:MyUserControl)。 - Richard

0

如果我理解的没错,缓存没有正确地工作是因为您将属性提供值给控件的方式,这可能与正在进行的计算有关。

您可以创建一个 HttpHandlerFactory,接收请求,如果它们不在缓存中则进行计算(之后插入到缓存中),处理值的过期,然后将请求传递给页面。 这不会是特定于控件的。 这样,您可以在任何控件或页面中使用这些计算出的值,并且不必实现关注自己的计算的缓存策略。


0
一个简单的技巧是将所有图形放在一个新页面中,接收GameId和Type作为查询字符串参数,使用开箱即用的查询字符串参数输出缓存,并在您的页面中放置一个iframe。此外,您还可以利用浏览器缓存,在一段时间内不需要服务器访问。

0

好的,那么这个情况中为什么很难让OutputCache起作用的原因是因为它不是设计来与属性一起使用的,但它与QueryString参数非常搭配。所以,我的下一个解决方案可能不是最专业的,也可能不是最好的,但它肯定是最快的,并且需要更少的代码更改。

由于它最适合QueryString,我建议您将您的UserControl 放在一个空白页面中,每当您想要使用您的UserControl 时,就创建一个链接到带有QueryString的页面的iframe,其中包含您的UserControl

您想要使用您的UserControl的地方:

<iframe src="/MyArcadeChartData.aspx?GameID=93&Type=TotalPlays"></iframe>

完整的页面标记,MyArcadeChartData.aspx

<%@ Page ... %>
<%@ OutputCache Duration="20" VaryByParam="GameID;Type" %>
<Scirra:ArcadeChartData ID="MyUserControlID" runat="server />

完整页面代码,MyArcadeChartData.aspx.cs

protected void Page_Load(object sender, EventArgs e)
{
    //TODO: Put validation here
    MyUserControlID.GameID = (int)Request.QueryString["GameID"];
    MyUserControlID.Type = (YourEnum)Request.QueryString["Type"];
}

请注意,查询字符串中的值可以被用户看到,请勿放置敏感数据。
此外,我知道这不是最专业的解决方案,但它是最容易实现的。
祝节日快乐!
致敬

我注意到ivowiblo发布了与我相同的答案,只是我加入了更多的解释和细节。 - plauriola

0
如果这不是数据密集型的话,您是否考虑将其存储在会话中而不是缓存中?只是一个想法...
Arcade.ChartDataType.ChartType Type;
string GameKey = GameId + Type.toString();
storedData = callCalculation(GameId,Type);
Session[GameKey] = storedData;

我知道这个不在缓存中,我只是想要建设性地尝试一下。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接