Partial caching of custom WebControls

I need to cache the generated content of a custom WebControl. Because the accumulation of the management collection hierarchy is very expensive, simply caching the database results is not enough. Caching the entire page is not possible because there are other dynamic parts on the page.

My question is, is there a best practice for this problem? I have found many solutions caching whole pages or static UserControls, but nothing worked for me. I ended up with my solution, but I highly doubt this is an acceptable approach.

The custom WebControl to be cached might 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));
    }
}

      

An aspx page that contains content controls might look like this:

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 shell controls, one to grab the generated html and one to cache the content. Quite a lot of code for a simple cache function (see below).

Controlling the wrapper to capture the output overwrites the Render function 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 for caching this generated output is 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 });

      

from

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

      

+3


source to share


1 answer


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

  • ClientIdMode of child controls of your custom control to avoid conflicts if those controls are to be displayed in a different context
  • LiteralMode of your Literal: this should be PassThrough
  • the expiration date of your cached element (absoluteExpiration / slideExpiration)
  • disable the ViewState of your CustomControl


Lately, I tend to take a different approach: my shell controls only contain javascript that makes an AJAX GET request on a page containing only my custom control. Caching is done client-side via HTTP headers and servers via the OutputCache directive (if HTTPS, content should be public)

+1


source







All Articles