3

I need to cache the generated content of custom WebControls. Since build up of control collection hierarchy is very expensive, simple caching of database results is not sufficient. Caching the whole page is not feasible, because there are other dynamic parts inside the page.

My Question: Is there a best practice approach for this problem? I found a lot of solutions caching whole pages or static UserControls, but nothing appropriate for me. I ended up with my own solution, but im quite doubtful if this is a feasible approach.

A custom WebControl which should be cached could look like this:

public class ReportControl : WebControl
{
    public string ReportViewModel { get; set; }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        // Fake expensive control hierarchy build up
        System.Threading.Thread.Sleep(10000);

        this.Controls.Add(new LiteralControl(ReportViewModel));
    }
}

The aspx page which includes the content control(s) could look as follows:

public partial class Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        // Fake authenticated UserID
        int userID = 1;

        // Parse ReportID
        int reportID = int.Parse(Request.QueryString["ReportID"]);

        // Validate if current user is allowed to view report
        if (!UserCanAccessReport(userID, reportID))
        {
            form1.Controls.Add(new LiteralControl("You're not allowed to view this report."));
            return;
        }

        // Get ReportContent from Repository
        string reportContent = GetReport(reportID);

        // This controls needs to be cached
        form1.Controls.Add(new ReportControl() { ReportViewModel = reportContent });
    }

    private bool UserCanAccessReport(int userID, int reportID)
    {
        return true;
    }

    protected string GetReport(int reportID)
    {
        return "This is Report #" + reportID;
    }
}

I ended up writing two wrapper controls, one for capturing generated html and a second one for caching the content - Quite a lot of code for simple caching functionality (see below).

The wrapper control for capturing the output overwrites the function Render and looks like this:

public class CaptureOutputControlWrapper : Control
{
    public event EventHandler OutputGenerated = (sender, e) => { };

    public string CapturedOutput { get; set; }

    public Control ControlToWrap { get; set; }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        this.Controls.Add(ControlToWrap);
    }

    protected override void Render(HtmlTextWriter writer)
    {
        StringWriter stringWriter = new StringWriter();
        HtmlTextWriter htmlTextWriter = new HtmlTextWriter(stringWriter);

        base.RenderChildren(htmlTextWriter);

        CapturedOutput = stringWriter.ToString();

        OutputGenerated(this, EventArgs.Empty);

        writer.Write(CapturedOutput);
    }
}

The wrapper control to cache this generated output looks as follows:

public class CachingControlWrapper : WebControl
{
    public CreateControlDelegate CreateControl;

    public string CachingKey { get; set; }

    public delegate Control CreateControlDelegate();

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        string content = HttpRuntime.Cache.Get(CachingKey) as string;

        if (content != null)
        {
            // Content is cached, display
            this.Controls.Add(new LiteralControl(content));
        }
        else
        {
            // Content is not cached, create specified content control and store output in cache
            CaptureOutputControlWrapper wrapper = new CaptureOutputControlWrapper();
            wrapper.ControlToWrap = CreateControl();
            wrapper.OutputGenerated += new EventHandler(WrapperOutputGenerated);

            this.Controls.Add(wrapper);
        }
    }

    protected void WrapperOutputGenerated(object sender, EventArgs e)
    {
        CaptureOutputControlWrapper wrapper = (CaptureOutputControlWrapper)sender;

        HttpRuntime.Cache.Insert(CachingKey, wrapper.CapturedOutput);
    }
}

In my aspx page i replaced

// This controls needs to be cached
form1.Controls.Add(new ReportControl() { ReportViewModel = reportContent });

with

CachingControlWrapper cachingControlWrapper = new CachingControlWrapper();
// CachingKey - Each Report must be cached independently
cachingControlWrapper.CachingKey = "ReportControl_" + reportID;
// Create Control Delegate - Control to cache, generated only if control does not exist in cache
cachingControlWrapper.CreateControl = () => { return new ReportControl() { ReportViewModel = reportContent }; };

form1.Controls.Add(cachingControlWrapper);
Dresel
  • 2,375
  • 1
  • 28
  • 44
  • Set Page directive. `<%@ OutputCache Duration="30000" VaryByParam="ReportID" %>` according to reportID or `<%@ OutputCache Duration="30000" VaryByParam="*" %>` for all Query String Parameters – Pankaj Mar 27 '12 at 13:54
  • I can't cache the whole page, as a wrote above. – Dresel Mar 28 '12 at 05:24

1 Answers1

1

Seems like a good idea, maybe you should pay attention to :

  • the ClientIdMode of the child controls of your custom control to prevent conflicts if these controls are to be displayed in another context
  • the LiteralMode of your Literal : it should be PassThrough
  • the expiration mode of your cached item (absoluteExpiration/slidingExpiration)
  • disable ViewState of your CustomControl

Recently, I tend to have another approach : my wrapper controls only holds some javascript that performs an AJAX GET request on a page containing only my custom control. Caching is performed client side through http headers and serverside through OutputCache directive (unless HTTPS, content has to be public though)

jbl
  • 15,179
  • 3
  • 34
  • 101
  • Good remarks - i will take them into consideration. So you make one page per control to cache - and the page content get called by the client via JS? – Dresel Mar 28 '12 at 05:30
  • Yes, used in the right context, it helps sharing controls (such as navigation controls) across applications, servers, and third-party sites. This reduces bandwith and server overhead. – jbl Mar 28 '12 at 07:08
  • Sounds reasonable for your requirements. Seems a bit circumstantial to adopt for my particular situation. I would have to make the calls serverside and the page with the control to cache not accessible from the client directly. If my UserCanAccessReport is true, i would make the call and could make a LiteralControl with the returned content. Nevertheless thanks for your input. – Dresel Mar 30 '12 at 08:05