Caching large HTML elements in the Browser document cache
Sometimes we have to do things that we know are wrong.
For example, in my current project we have to present a list of customers so that the user can select one or more of them. I work for a large company that has been around for many many years, and the list of customers can exceed 15,000 for some countries.
But I can't decide which ones should be shown, and which ones should not be shown. Instead I need to show the complete list to the user. What is painful is that it can take a bit of time for this list to be sent down to the browser. It would be much better would be if the customer list could be cached on the browser's document cache. This is what this blog post is about.
Note that in order for this to work, the list is presented as a simple HTML SELECT, and the selected items need to be processed correspondingly on the server using the Request.Form[SelectName] property.
In the ASPX page, I define my customer list like this:
<div id="ListDiv"> <select id="List" multiple="multiple" size="15" disabled="disabled"> <option>Loading List ...</option> <option>This may take a moment the first time...</option> </select> </div>
Note that the SELECT is surrounded by a DIV, so that I can replace the DIV contents with a new list.
Then I have some JavaScript that fires when the page loads, and asynchronously fetches the list:
<script language="javascript" type="text/javascript"> // This is called by the ASP.NET AJAX Framework automatically function pageLoad() { var wRequest = new Sys.Net.WebRequest(); wRequest.set_url('ListHandler.ashx?Version=<%=ListVersion.Value%>)'); wRequest.set_httpVerb("GET"); // GETs can be cached, POSTs can not wRequest.add_completed(OnFetchListCompleted); wRequest.invoke(); }
The pageLoad function gets invoked automatically. It simply fires off an HTTP GET request to a handler, passing as a parameter the value of the ListVersion hidden field. By changing the value of this field, the server-side code can control when the browser-cached contents are expired, and a new version is fetched (since changing the URL will mean there is no browser-cached version).
When the response comes back, it replaces the contents of the DIV with the value that is returned from the handler:
function OnFetchListCompleted(executor, eventArgs) { var listDiv = $get('ListDiv'); if(executor.get_responseAvailable()) { var list = executor.get_responseData(); if(list && list.startsWith('<select')) { listDiv.innerHTML = list; } } } </script>
The handler simply generates the appropriate content:
<%@ WebHandler Language="C#" Class="ListHandler" %> using System; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.Collections.Generic; public class ListHandler : IHttpHandler { public void ProcessRequest (HttpContext context) { context.Response.Clear(); context.Response.ContentType = "text/html"; context.Response.Cache.SetCacheability(HttpCacheability.Public); context.Response.Cache.SetExpires(DateTime.Now.AddMonths(1)); context.Response.Cache.SetSlidingExpiration(true); HtmlTextWriter htmlTextWriter = new HtmlTextWriter( context.Response.Output); ListBox listBox = GetListBox(); listBox.RenderControl(htmlTextWriter); htmlTextWriter.Flush(); context.Response.End(); } private static ListBox GetListBox() { ListBox listBox = new ListBox();
...
return listBox;
}
...
Note the bolded directives to enforce the browser-side caching of the response.
I have a complete example here.
You might be wondering why I've used the Sys.Net.WebRequest mechanism with the HttpHandler. Why not simply use a Web Service? That would indeed be simpler, however there is no easy way to set the appropriate cache expiration headers, although it is possible if you are willing to use reflection -- the PageFlakes guys use it to speed up their pages.