4

I have a webusercontrol with a public int property SelectedCatID. I use this control on other pages and in other controls as follows:

<NewStore:LeftMenuLinks runat="server" SelectedCatID="<%#CatIDToSelect%>" />

How do I output cache this control based on SelectedCatID? Everything I've tried fails.

The closest I've gotten is getting it to cache, but it doesn't vary on SelectedCatID leaving the same menu item selected until the cache expires. Without caching, the control works as expected.

Tom Gullen
  • 61,249
  • 84
  • 283
  • 456
  • The `VaryByControl` attribute is supposed to match an ID of the user control to cache, not just an arbitrary property of the control. If you check that the control exists in the code behind and then set the variable to be used by your `NewStore:LeftMenuLinks` user control, does it work? – Justin Aug 29 '14 at 16:20
  • Do you have the SelectedCatID="<%#SelectedCatID %>" version in a databound control, or is it just bound to a property of the page class ? What's the relation between SelectedCatID and SelectedMenu ? Or is that just a typo ? – Menno van den Heuvel Sep 02 '14 at 12:07
  • @menno typo, fixing now! – Tom Gullen Sep 02 '14 at 12:54
  • All I'm trying to do is output cache based on the controls public properties, and I just can't figure out how to do it. Question not getting much attention so I'll edit it to make it clearer. – Tom Gullen Sep 02 '14 at 12:54

3 Answers3

3

I figured out why the VaryByControls approach you used initially does not work. Sadly you edited it out of your question, so my research for this will just have to go into a blog post. Update: the blog post in question: http://tabeokatech.blogspot.be/2014/09/outputcache-on-user-controls.html .

The long and short of it though is that VaryByControls is kinda shorthand for VaryByParams, and does nothing for properties: it only looks at POST values. The fact that it ever worked for properties with a static value appears to be a bug - any string whatsoever in the VaryByControls would have made that part work. The accepted answer to this question is wrong: Vary by control properties using PartialCaching in ASP.NET .

There is no built-in way to vary by control property values.

That wouldn't make sense anyway, because user controls need to be created to have property values, and you want to avoid creating them, instead caching their rendered markup - cached user controls fields are null in code-behind if cached markup is served for them. This works by injecting a PartialCachingControl into the page instead of the actual user control. This PartialCachingControl checks the cache, and only creates the control if no cached version exists.

As for making it work, I see two options:

  1. If you only have 1 usercontrol per page, you could use the VaryByCustom approach. To make things easy you could write an interface that returns your property value for that page, and implement it on every page that hosts the user control, e.g.:

    interface INumberProvider
    {
        int GetNumber();
    }
    
    // and the page:
    public partial class _Default : Page, INumberProvider
    {
        public int GetNumber()
        {
            return this.SomeNumberPropertyOrWhatever;
        }
    ...
    

    In your Global.asax you cast the current handler to INumberProvider and get the number:

        public override string GetVaryByCustomString(HttpContext context, string custom)
        {
            if (custom == "INumberProvider")
            {
                var page = context.CurrentHandler as INumberProvider;
    
                if (page != null)
                {
                    return page.GetNumber().ToString();
                }
            }
            return base.GetVaryByCustomString(context, custom);
        }
    

    And in your control you obviously add:

    OutputCache Duration="180" VaryByCustom="INumberProvider" VaryByParam="None" Shared="true"

    That's if you only have one user control per page, and should be pretty straightforward. If you need more than one user control per page you're out of luck:

  2. Build your own wrapper around your user control by writing a custom WebControl. Add the properties you need, capture the output of the rendered user control, and insert it into HttpContext.Current.Cache with a key that includes the SelectedCatID. Basically write your own custom PartialCachingControl. There's also option 3:
  3. Decide caching is not that important after all
Community
  • 1
  • 1
Menno van den Heuvel
  • 1,851
  • 13
  • 24
  • `That's if you only have one user control per page, and should be pretty straightforward. If you need more than one user control per page you're out of luck:` Incorrect .You just add the directive to the various usercontrols and not the page itself. caching an individual user control is possible. – Hillboy Sep 02 '14 at 18:56
  • The above does relate to caching of user controls specifically. What I'm saying is: based on the information available in the Global.asax override, there is no way to infer the property value of the user control the cache is supposed to create a variation for, unless there is only one of the user controls per page. – Menno van den Heuvel Sep 03 '14 at 08:38
  • Great answer thank you, exactly the sort of information I was after. Please link to your blog post here when you've done it as well! – Tom Gullen Sep 03 '14 at 11:12
0
<%@ OutputCache Duration="60" VaryByParam="SelectedCatID" %>

Now store youre <%#CatIDToSelect%> as an a parameter ex ?SelectedCatID=12 Now you're Page or UserControl depending on what you want to cache will output the cache depending on what the Request.Param["SelectedCatID"] is equal to.

You can also do something like this (although not the easiest way)

This goes on the page/usercontrol you want cached:

<%@ OutputCache duration="120" varybyparam="None" varybycustom="SelectedCatID" %>

This goes into the Gloabal.asax file:

public override string GetVaryByCustomString(HttpContext context, string custom)
{
    if(custom == "SelectedCatID")
    {
        return CatIDToSelect;
    }
    return String.Empty;
}
Hillboy
  • 472
  • 6
  • 19
  • Take a look at this pastebin: http://pastebin.com/NLmZkfP9 It's throwing the error `CS0115: 'Controls_Blog_LatestEntries.GetVaryByCustomString(System.Web.HttpContext, string)': no suitable method found to override`. Cache defined as `<%@ OutputCache duration="120" varybyparam="None" varybycustom="SelectedBlogID" %>` I understand from using Google this is meant to go in global.asax, but that's not helping me. I need a way to cache a control based on properties passed to iut – Tom Gullen Sep 02 '14 at 15:30
  • Okay so I tested it seems you dont need the ([OutputCache(CacheProfile = "CacheProfile")]. And works once you refresh the page. I tested with a page that shows exact time (including seconds) after second refresh seconds no longer change a time is permanent until duration expires. And just follow the changes i made to the post – Hillboy Sep 02 '14 at 16:59
  • my test code for your viewing ( 4 files): http://pastebin.com/6v10bSgg http://pastebin.com/QFNXE7Z1 http://pastebin.com/Jbfuwywf http://pastebin.com/AXC2vT95 – Hillboy Sep 02 '14 at 17:10
0

I'm late to the party here what with an accepted answer and a 500 point bounty awarded. Still wanted to give my few cents on how this could be achieved.

It can be made to work in the control itself. You can have the control store it's own output in the cache and use the cached version in the Render method if found. I have made a really simple UserControl to test with. The markup looks like this:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="TestUC.ascx.cs" 
    Inherits="Webforms_Test.UserControls.TestUC" %>
<div>
    <asp:Label ID="curTime" runat="server"></asp:Label>
</div>

It just contains a label that is set to DateTime.Now when it is initialized. The code behind looks like this:

public partial class TestUC : System.Web.UI.UserControl
{
    private string cachedOutput = null;
    public bool RenderFromCache = true; // set to false in containing page if this control needs to be re-rendered

    protected void Page_Load(object sender, EventArgs e)
    {
        cachedOutput = HttpContext.Current.Cache["key"] as string;
        if (cachedOutput == null)
        {
            // not found in cache, do the heavy lifting here to setup the control
            curTime.Text = "UC:" + DateTime.Now.ToString("yy-MM-dd hh:mm:ss");
        }
    }
    protected void Page_PreRender(object sender, EventArgs e)
    {
        if (cachedOutput == null || !RenderFromCache)
        {
            RenderFromCache = false;
            StringBuilder b = new StringBuilder();
            HtmlTextWriter h = new HtmlTextWriter(new StringWriter(b));
            this.RenderControl(h);
            cachedOutput = b.ToString();
            HttpContext.Current.Cache.Insert("key", cachedOutput, null, DateTime.UtcNow.AddSeconds(10), TimeSpan.Zero);
            RenderFromCache = true;
        }
    }
    protected override void Render(HtmlTextWriter writer)
    {
        if (!RenderFromCache)
            base.Render(writer);
        else
            writer.Write(cachedOutput);
    }
}

In this sample, the control itself checks if its output is found in the cache, and if so the Render method will just write the cached output. If it is not found in the cache, the PreRender method will run the Render method normally and capture the output and store it in the cache.

In your case you would of course need a bit more logic which would check the relevant property on the control and use that to check if a cached version exists.

Disclaimer: This is an extremely simple test control. I have not tried to figure out how to make all of this work with controls that contain event handlers etc. So take it for what it's worth...

user1429080
  • 9,086
  • 4
  • 31
  • 54
  • I see what you're saying. I'm not convinced it would be nearly as effective though as a wrapper class. Your control would still build the whole control tree, in order to throw it all away, and serve the cached html. The PartialCachingControl overrides the InitRecursive method, but it's marked internal so we can't use it. Maybe OnInit would work. – Menno van den Heuvel Sep 05 '14 at 20:17