Page output caching for dynamic web applications
- by Mike Ellis
I am currently working on a web application where the user steps (forward or back) through a series of pages with "Next" and "Previous" buttons, entering data until they reach a page with the "Finish" button. Until finished, all data is stored in Session state, then sent to the mainframe database via web services at the end of the process.
Some of the pages display data from previous pages in order to collect additional information. These pages can never be cached because they are different for every user. For pages that don't display this dynamic data, they can be cached, but only the first time they load. After that, the data that was previously entered needs to be displayed. This requires Page_Load to fire, which means the page can't be cached at that point.
A couple of weeks ago, I knew almost nothing about implementing page caching. Now I still don't know much, but I know a little bit, and here is the solution that I developed with the help of others on my team and a lot of reading and trial-and-error.
We have a base page class defined from which all pages inherit. In this class I have defined a method that sets the caching settings programmatically. For pages that can be cached, they call this base page method in their Page_Load event within a if(!IsPostBack) block, which ensures that only the page itself gets cached, not the data on the page.
if(!IsPostBack)
{
...
SetCacheSettings();
...
}
protected void SetCacheSettings()
{
Response.Cache.AddValidationCallback(new HttpCacheValidateHandler(Validate), null);
Response.Cache.SetExpires(DateTime.Now.AddHours(1));
Response.Cache.SetSlidingExpiration(true);
Response.Cache.SetValidUntilExpires(true);
Response.Cache.SetCacheability(HttpCacheability.ServerAndNoCache);
}
The AddValidationCallback sets up an HttpCacheValidateHandler method called Validate which runs logic when a cached page is requested. The Validate method signature is standard for this method type.
public static void Validate(HttpContext context, Object data, ref HttpValidationStatus status)
{
string visited = context.Request.QueryString["v"];
if (visited != null && "1".Equals(visited))
{
status = HttpValidationStatus.IgnoreThisRequest; //force a page load
}
else
{
status = HttpValidationStatus.Valid; //load from cache
}
}
I am using the HttpValidationStatus values IgnoreThisRequest or Valid which forces the Page_Load event method to run or allows the page to load from cache, respectively. Which one is set depends on the value in the querystring. The value in the querystring is set up on each page in the "Next" and "Previous" button click event methods based on whether the page that the button click is taking the user to has any data on it or not.
bool hasData = HasPageBeenVisited(url);
if (hasData)
{
url += VISITED;
}
Response.Redirect(url);
The HasPageBeenVisited method determines whether the destination page has any data on it by checking one of its required data fields. (I won't include it here because it is very system-dependent.) VISITED is a string constant containing "?v=1" and gets appended to the url if the destination page has been visited.
The reason this logic is within the "Next" and "Previous" button click event methods is because 1) the Validate method is static which doesn't allow it to access non-static data such as the data fields for a particular page, and 2) at the time at which the Validate method runs, either the data has not yet been deserialized from Session state or is not available (different AppDomain?) because anytime I accessed the Session state information from the Validate method, it was always empty.