Using an HttpModule to detect page refresh
While reading through the excellent ASP.NET book written by Dino Esposito I've come across a solution to detect page refreshes using an HttpModule. When I read it I couldn't understand a line of the code and it seemed to me that it couldn't work, so I started experimenting with it using the code available to download from MS Press website. I discovered that I had been a good runtime, since the code in fact doesn't work very well. In particular, if you have more than one page opened concurrently you can see that the code behaves strangely, and the same can be said for the other couple of methods I've found out there (by Joel Thoms and Jared Evans), which are mostly complex and unreliable.
I started thinking about it from scratch and the question was: "How do I distinct between requests caused by the F5 or refresh button and normal requests?" That's not an easy question, since the stateless nature of the HTTP protocol doesn't allow to maintain state across requests, and maybe it has no answer, or, if you prefer, "You can't".
So I restricted the problem; do I care about all requests or just the ones which POST something to the server? I think most people care just about POST requests, those which usually perform some kind of action on the server, like working with database data, and the same actions we don't want to see duplicated because of a page refresh.
Therefore the question became: "How do I distinct between POST requests triggered by the interaction with the page and those fired by the F5 button?". Luckily, in this case the DOM comes to help and a simple detail becomes the lifesaver. When you POST something to the server in the canonical way, the onsubmit event of the page form is fired; when you try reposting the same page to the server using F5 that event isn't fired.
As far as I know this mechanism depends on the browser implementation, but I've tested it in IE7, FF 2.0.1 and Opera 9.10 and the all behave the same.
This is the trick:
- On the server we keep a queue of all the Guids we have retrieved from the pages which posted back. Each time a page posts back we retrieve its Guid reading from the hidden field, and if it's already in our queue it means that the page is a refresh.
That's simple, isn't it? Much simpler than all the methods which rely on Session state, Cookies. HttpContext.Items or whatever.
The idea seems good! Let's write it in code. We need to intercept requests, so we need an HttpModule. Each time a request arrives we add dynamically a HiddenField control to the page to keep our Guid value and then retrieve the Guid of the page (if one exists). If the Guid is in our queue then the page is refreshed, otherwise it's a good clean postback. The only place where we can store this boolean value is the Items collection of the current HttpContext, so that it can be retrieved in the page code.
The queue where all the guids are kept resided in memory, so we need a way to define how many elements it can contain at most. It obviously depends on how many visitors the website has. The default value is 100; it means that if I do a postback, wait a bit before pressing F5 and during that time 100 genuine postbacks are fired by other users (or by myself but in another window/tab/browser) then my refresh is considered as a genuine postback because its Guid has already been dequeued (and I don't want this to happen!).
Place the assembly binary in the Bin directory of the website.
Since implemented as an HttpModule it has to be registered in the web.config file:
<add name="RefreshModule" type="BusyBoxDotNet.RefreshModule"/>
You can configure the size of the queue to fit your needs, otherwise the default value will be set to 100:
<section name="refresh" type="BusyBoxDotNet.RefreshSection, RefreshModule" />
The IsRefreshed key of the Context.Items collection is set during the page PreLoad phase, so it can be read starting from the Load phase in the page code, otherwise it will always return false.
To retrieve its value just check the Context.Items["IsRefreshed"] boolean value, or use the base page class I've written for those who love strong typing, which exposes the wrapper boolean property IsRefreshed.
NOTE: if you missed it before, remember that IsRefreshed (or Context.Items["IsRefreshed"]) notifies you when a POST request is refreshed, not a GET request. That is, if you write a url in your browser's address bar and then push F5, IsRefreshed will be false, because they are GET requests. Usually you care about detecting POST requests being refreshed, but in case you need GET requests refresh detection so this method doesn't work for you.
Attached to the post is a demo website which includes the binary.
The source code of this and all my other works is on the subversion repository of my main project: http://sourceforge.net/projects/busybox