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:
LoadControl("foo.ascx"); // once LoadControl("foo.ascx"); // twice
MethodThatLoadsFoo(); // once MethodThatLoadsFoo(); // twice
LoadControl("foo.ascx");
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
Leave a Reply