Caching dynamic controls in ASP.NET – solution 5

In my last post, I asked for help with a problem we’ve been having with caching of dynamic controls – and I got some from Dave Reed. Dave works for the ASP.NET team and if his blog isn’t on your roll yet, add it ASAP – it’s simply brilliant.

Dave explained the problem:


Apparently, the stack trace involved with loading the control is taken into account when deciding how to cache the controls output. So for example, if you load the control twice like so:
LoadControl("foo.ascx"); // once 
LoadControl("foo.ascx"); // twice
Then they will be cached separately. But if you load them like this:
MethodThatLoadsFoo(); // once 
MethodThatLoadsFoo(); // twice
 
Where MethodThatLoadsFoo contains:
LoadControl("foo.ascx"); 
They are cached together because it will appear that they were loaded via the same code line.
 
Since you are loading them dynamically based on data you probably have a single method that loads whatever control it is to be. But depending on the details of your implementation perhaps you can find a way to have a different LoadControl reference load each one?


Ok.. So where does that leave us? Remember, the implementation that calls LoadControl in my last example looks like this:

foreach (ControlInfo controlInfo in controlList)
{
   // Load the control and add it to the page
   HttpContext.Current.Items["uniqueId"] = controlInfo.UniqueControlId;
   Control ctrl = LoadControl(controlInfo.ControlUrl);
   PlaceHolder1.Controls.Add(ctrl);
}

It is the same physical line of code that loads all controls, which makes the caching mechanism believe that it is the very same control that is being loaded. In order for it to cache multiple versions, I’d need to add a new LoadControl line for each control. So I guess I could "unwrap" the foreach-loop and do something like:

if (controlList.Count > 0)
{
   HttpContext.Current.Items["uniqueId"] = controlList[0].UniqueControlId;
   PlaceHolder1.Controls.Add(LoadControl(controlList[0].ControlUrl));
}
if (controlList.Count > 1)
{
   HttpContext.Current.Items["uniqueId"] = controlList[1].UniqueControlId;
   PlaceHolder1.Controls.Add(LoadControl(controlList[1].ControlUrl));
}
if (controlList.Count > 2)
{
   HttpContext.Current.Items["uniqueId"] = controlList[2].UniqueControlId;
   PlaceHolder1.Controls.Add(LoadControl(controlList[2].ControlUrl));
}

Ehh.. Nope.. Not gonna happen…

The only other alternative I could think of was to dynamically generate and compile the code that is used to render a page. This is (a crude version of) what I came up with:

private void CompileAndInitializePage(Page page, List controlList)
{
  Assembly compiledAssembly;

  // Has this page been built before? If so, there should already be an existing assembly
  // compiled for it.
  if (compiledAssemblies.ContainsKey(page.AppRelativeVirtualPath))
  {
    compiledAssembly = compiledAssemblies[page.AppRelativeVirtualPath];
  }
  else
  {
    // If not, compile a new one.
    string mainHeader =
      "using System.Web;\n" +
      "using System.Web.UI;\n" +

      "namespace DynamicCacheTest\n" +
      "{\n" +
      " public class TempPageBuilder\n" +
      " {\n" +
      " public void BuildPage(Page page)\n" +
      " {\n" +
      " Control holder;\n" +
      " holder = page.FindControl(\"PlaceHolder1\");\n";

    string mainFooter =
      " }\n" +
      " }\n" +
      "}\n";
    StringBuilder sb = new StringBuilder(mainHeader);
    foreach (ControlInfo ctrl in controlList)
    {
      sb.AppendFormat("HttpContext.Current.Items[\"uniqueId\"] = {0};\n", ctrl.UniqueControlId);
      sb.AppendFormat("holder.Controls.Add(page.LoadControl(\"{0}\"));\n", ctrl.ControlUrl);
    }
    sb.Append(mainFooter);

    ICodeCompiler compiler = new CSharpCodeProvider().CreateCompiler();

    CompilerParameters parameters = new CompilerParameters(new string[] { "System.dll", "System.Web.dll" });
    CompilerResults res = compiler.CompileAssemblyFromSource(parameters, sb.ToString());
    compiledAssembly = res.CompiledAssembly;
    compiledAssemblies[page.AppRelativeVirtualPath] = compiledAssembly;
  }

  // Build the page builder object
  object builder = Activator.CreateInstance(compiledAssembly.GetType("DynamicCacheTest.TempPageBuilder"));

  // Invoke the BuildPage method
  builder.GetType().InvokeMember("BuildPage", BindingFlags.InvokeMethod, null, builder, new object[] { this });
}

And it works beautifully! I’m not too worried about performance since I’m only compiling the page once, and running the compiled code shouldn’t be slower than the foreach-loop is today. The only thing I’m thinking is that with many pages on a site I’d have a lot of very small assemblies loaded. Probably it would be a better idea to precompile all pages on startup and put them in a single assembly.

Anyway, in the end we’ve decided that we won’t actually put this in production since the problem probably is better worked around by educating our webmasters, but it was nice to finally find a solution.

Update: I’ve attached the updated source code to this post.

Update 2: And now I’m trying out a Live Writer Plugin to format the source code

5 thoughts on “Caching dynamic controls in ASP.NET – solution

  1. Matt Feb 21,2008 14:11

    Hi, I had a look at your solution as i was trying to do the same kind of stuff, but i’m having problems with update panels which are loaded in a dynamic way in some of my user controls ascx. Do you know a way to do that? Cheers

  2. anders Feb 23,2008 16:33

    Hi Matt,

    Can you explain in a bit more detail? I didn’t quite get what you are trying to do I’m afraid.

  3. Stewart Mar 10,2008 17:12

    This seems counter intuitive, Can dynamically generating and compiling the code really be quicker than just using LoadControl ? surely not?

  4. anders Mar 10,2008 20:25

    No, of course not. The only reason why you would want to use this method is in order to overcome the initial problem with caching dynamically loaded controls where each control has different content even though they share the same .ascx (as mentioned in the first post).

    With that said, I don’t expect this method to be very slow since compiling the page only happens the first time it is shown.

  5. Daniel Saidi Jul 15,2008 15:49

    Hi there!

    The following blog discusses the issues of dynamically loading user controls.

    http://daniel-saidi.blogspot.com/2008/07/aspnet-dynamically-loaded-usercontrol.html

    It features a small, downloadable class which, hopefully, will take care of this issue for you and let you focus on other things.

Comments are closed.