5

I have a web user control, it serves some potentially intensive data calculations and I would like it to be output cached so that each page view doesn't recalculate the data. It resides on very frequently viewed pages so it's quite important I get it working right!

For context, it's used on our arcade: http://www.scirra.com/arcade/action/93/8-bits-runner

Click on stats, the data for the graphs and stats are generated from this webusercontrol.

The start of the control is as follows:

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)
    {

Now the difficulty I'm having is the output cache needs to be dependant on both the GamID and the ChartType.

This control is re-used with many different combinations of GameID's and Types, I need it to create a cache for each of these but am struggling to find out how to do this.

For example, one arcade game might pass in GameID = 93 and Type = GraphData, another might be GameID = 41 and Type = TotalPlaysData and another might be GameID = 93 but Type = TotalPlaysData. These should all return different data and have different output caches.

The control is used on the games page sort of like this (the parameters are actually set in the codebehind)

<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.

Any help appreciated! I've spent a while looking online and it's kept coming up as something I need to solve but can't figure this one out.

Edit

Edit: I've tried adding this line to my control: <%@ OutputCache Duration="20" VaryByControl="GameID;Type" %>

But it just throws the error Object reference not set to an instance of an object. on the line where GameID is being set for the first time on the ASPX page using the control.

Tom Gullen
  • 61,249
  • 84
  • 283
  • 456

8 Answers8

10

When a Control is retrieved from the output cache, it's not instantiated as an instance that you can manipulate; you just get the output the Control generated, not the Control itself. For example, you can't set properties on a cached Control from code behind, as you said in your question. The vary-by properties should be set declaratively (using an ExpressionBuilder might also work, though I haven't tried it).

To see in code behind whether a control has been retrieved from the output cache, check for null:

if (this.YourControlID != null) // true if not from cache
{
    // do stuff
}

Even with that caveat, Control output caching is a bit quirky.

Try this:

<%@ OutputCache Duration="20" VaryByControl="GameID;Type" Shared="true" %>

The output of the Control is stored in the output cache by associating it with a certain key. With Shared="true", the cache key is the value of all specified properties, together with the Control's ID. Without Shared="true", the cache key also includes the Page type, so the output would vary by Page -- which doesn't sound like what you want.

If you use the Control on more than one page, be sure to use the same ID on each page if you can, since the ID is included as part of the key for the output cache. If you can't or don't use different IDs, you will get a new copy of the Control's output in the cache for each unique ID. If the Controls with different IDs always have different property values anyway, that may not be an issue.

As an alternative to the OutputCache directive, you can set an attribute on the class declaration:

[PartialCaching(20, null, "GameID;Type", null, true)]
public partial class Controls_Arcade_Data_ArcadeChartData : UserControl
RickNZ
  • 18,448
  • 3
  • 51
  • 66
  • since he want to use control multiple times in the same page, this solution is not valid. cannot have same ID for multiple controls within same page. – Chamika Sandamal Dec 25 '11 at 04:58
  • 2
    It's OK to use different IDs on the same page; just keep in mind that the ID is part of the cache key, so it's possible to get multiple copies of the same output. If Type is always set based on the position of the control on the page, as in the question, that may not be an issue. – RickNZ Dec 25 '11 at 05:10
  • This appears to work as I want it to! Will test a bit more and report back. – Tom Gullen Dec 25 '11 at 08:42
  • It's behaviour is unusual, but this basically works! Thanks for the answer! – Tom Gullen Dec 29 '11 at 16:30
3

You need to take the following steps:

1) Add the following output cache directive to the page:

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

2) Add the following to 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;
        }
    }

You can get more information on the GetVaryByCustomString method from MSDN and also review some of the other caching options here.

competent_tech
  • 44,465
  • 11
  • 90
  • 113
  • Am I correct in thinking this will cache it for 6 hours based on the URL of the page? The data will be on many URL's, I just want it to cache based on `GameID`. – Tom Gullen Dec 20 '11 at 01:22
  • @TomGullen: The duration is in seconds, so the value I had in there would only be good for 10 minutes; I have adjusted to the correct value (21600). The way the method is designed, it will cache on the full URL, but you can adjust this behavior as needed in the GetVaryByCustomString method if you have additional querystring params. – competent_tech Dec 20 '11 at 01:29
  • I don't have querystring params though, I have a control which has a public property I want to cache on. – Tom Gullen Dec 20 '11 at 01:34
  • Ah, I understand now, sorry. How is that GameID assigned to the usercontrol? Is it in the session, cookie, global class? Any of these could be used in the global.asax code, it doesn't have to be an actual URL. – competent_tech Dec 20 '11 at 01:44
  • No worries thanks for your answers so far though I do appreciate your time! The GameID is assigned when you are viewing a game on an ASPX page. It's for our arcade http://www.scirra.com/arcade , if you click on a game then click 'Stats' under the game it shows the total plays. Just need it to cache this, at the moment it's working it out each time. – Tom Gullen Dec 20 '11 at 01:54
  • I've put a 500 bounty on this, do you know how to do it? – Tom Gullen Dec 22 '11 at 14:45
  • @TomGullen: Ok, finally had a chance to play with this a bit. I have updated the answer slightly to show how I think you should be able to accomplish this. Two changes: 1) remove shared=true from the outputcache directive; 2) use the context.handler to get the page that the user control is on, then either get the gameid from the page or search through the page's controls to find the user control and extract the gameid from that. – competent_tech Dec 24 '11 at 20:42
2

create a cache object in the code

HttpRuntime.Cache.Insert("ArcadeChartData" + GameID + Type, <object to cache>, null, System.Web.Caching.Cache.NoAbsoluteExpiration,new TimeSpan(0, 0, secondsToCache),CacheItemPriority.Normal, null);

above cache item will be enough to your work, but if you really want to use output cache as well try following code in the code behind,

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

Setting values for the page output cache is the same as manipulating the SetExpires and SetCacheability methods through the Cache property.

Chamika Sandamal
  • 23,565
  • 5
  • 63
  • 86
  • Thanks, am I correct in saying both these bits of code are intended for the webuser control? And also what is the difference between the two? – Tom Gullen Dec 22 '11 at 13:58
  • output cache is caching the final rendered html in the IIS level and it won't let you to go to the code. but if you use `HttpRuntime.Cache` it will cache the object you specified and request will hit the actual page but not the calculation in the cached object. if you really want to use output cache, you can use object dependency with the cached object. – Chamika Sandamal Dec 22 '11 at 14:14
  • I definitely want to use output cache as I don't want each new visitor having the object re-rendered, if we get a popular game with say 10,000 plays in 1 day it could be hard going on the server. So I think output cache is the best option. – Tom Gullen Dec 22 '11 at 14:34
  • I'm a bit confused where I put the second block of code to create the output cache? – Tom Gullen Dec 22 '11 at 14:39
  • you can put it in the page load. – Chamika Sandamal Dec 22 '11 at 16:36
  • this doesn't seem to work. I add the code in the Page_Load for the control and it has no effect – Tom Gullen Dec 24 '11 at 02:12
  • @TomGullen: what you mean by not working? can you post the code you tried.remember, you have to check whether object is already in cache or not, before inserting to the cache. – Chamika Sandamal Dec 24 '11 at 06:07
  • @TomGullen: remember, output cache will cache entire output of the usercontrol. in above sample when you use different GameID or Type, it will invalidate the output cache but will not invalidate the cache object. so you dont need to process your logic again. now you have multiple cache objects based on the GameID and Type. but output cache only have one version. it cannot keep multiple versions. so when you put different GameID or Type, it will recreate output cache using above cache objects(without processing the whole). – Chamika Sandamal Dec 24 '11 at 10:51
  • By not work I mean have no effect, I refresh the page and the counters go up, but I expect the counters to cache. This method you have given seems to have no effect, the counters/output still goes up one at a time. – Tom Gullen Dec 24 '11 at 23:02
  • @TomGullen: read my last comment.**when you use different GameID or Type, it will invalidate the output cache but will not invalidate the cache object**. for that you need to check `Response.cache["ArcadeChartData" + GameID + Type]==null` before executing `HttpRuntime.Cache.Insert("ArcadeChartData" + GameID + Type, , null, System.Web.Caching.Cache.NoAbsoluteExpiration,new TimeSpan(0, 0, secondsToCache),CacheItemPriority.Normal, null);` – Chamika Sandamal Dec 25 '11 at 04:50
1

I know that my solution may look very simple and possibly weird but I tried it and it works. You simply have to add this line in your UserControl.

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

Note : I have only tested the Framework 4.0. Also if ever you have to change the value of the property in the UserControl (MyInt, My String in this example) do it in the Page_Init event.

Here is all my code :

Page :

<%@ 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>

User Control:

<%@ 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>

User Control Code:

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

Please keep me posted, I have other solutions if ever you wish but they are more complex. I may also post with C#

plauriola
  • 178
  • 3
  • This doesn't work, when I try and set `GameID = TheGame.ID;` in the ASPX page that holds the control it throws an `Object reference not set to an instance of an object.` error when trying to load the cache – Tom Gullen Dec 24 '11 at 02:10
  • This actually works. It looks like asp.net is using the properties defined in the markup tag (in this case – Richard Mar 12 '13 at 16:11
0

One easy trick is to put all the graphics in a new page receiving GameId and Type as querystring parameters, use the out-of-the-box output cache for querystring parameters and the put an iframe in your page. Also you can make use of the browser's cache and never get the server hit for a while.

Ivo
  • 8,172
  • 5
  • 27
  • 42
0

Ok, well the reason why this so hard to make OutputCache work in this case is because it wasn’t design to be use with Property’s, however it works very well with QueryString parameters. So my next solution isn’t the most professional and probably not the best, but it is definitely the fastest and the one that requires less code changing.

Since it works best QueryString, I recommend you putting your UserControl in one blank page, and wend ever you want to use your UserControl make an iframe that links to your page with the UserControl with QueryString.

Where you want to use your UserControl:

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

Full page markup, MyArcadeChartData.aspx

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

Full page code, 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"];
}

Please not that values in the QueryString can be seen by the user, please do not put sensitive data.

Also I’m aware that this isn’t the most professional solution, but it is the easiest to implement, from what I know.

Regards and happy holidays

plauriola
  • 178
  • 3
0

If I understand right, the caching isn't working correctly because of the way you have the properties supplying the values to the control, which probably has to do, in part, with the calculations that are being done.

You could create an HttpHandlerFactory that takes the request, does your calculations if they're not in the cache (inserting into the cache afterwards), handles the expiration of values, and then passes the request on to the page. It wouldn't be control-specific at all. That way you could use these calculated values in any control or page, and wouldn't have to implement caching policies that worry about their own calculations.

Random
  • 1,896
  • 3
  • 21
  • 33
0

If this isn't data intensive, have you considered storing it in the Session as apposed to caching it? Just a thought...

Arcade.ChartDataType.ChartType Type;
string GameKey = GameId + Type.toString();
storedData = callCalculation(GameId,Type);
Session[GameKey] = storedData;

I realize this isn't in the cache, I am just trying to be constructive.

Travis J
  • 81,153
  • 41
  • 202
  • 273