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:

  1. Each time a page's form posts back we create on the client side a Guid (using Javascript) which is then assigned to a hidden field, so that when the request reaches the server we can retrieve that value. This means that if a page posts back via F5 it keeps the Guid which was generated during the last submission, since the onsumbit event is not fired again and thus no new Guid is generated.
  2. 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.

Implementation details

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!).

Setting up

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:

<httpModules>
<add name="RefreshModule" type="BusyBoxDotNet.RefreshModule"/>
</httpModules>

You can configure the size of the queue to fit your needs, otherwise the default value will be set to 100:

<configSections>
<section name="refresh" type="BusyBoxDotNet.RefreshSection, RefreshModule" />
</configSections>

<refresh queueSize="50"/>

Using 

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 

kick it on DotNetKicks.com

Published 07 January 2007 04:21 AM by simoneb
Filed under:

Comments

# DotNetKicks.com said on 06 January, 2007 09:23 PM

You've been kicked (a good thing) - Trackback from DotNetKicks.com

# aleksper said on 15 January, 2007 09:50 AM
I can't make it work using MasterPage. Is it possible?
# simoneb said on 15 January, 2007 07:54 PM

I fixed it, it was because of the HiddenField that was registered in the page that got its id changed because of the master page nesting.

I attached the new code and update the source of the svn repository.

# aleksper said on 16 January, 2007 03:52 AM
Yes, it works whith the master pages now. Thanks! But I found another serious problem. The RefrehshModule don't detect the onsubmit event fired by, for example, RadioButton controls, CheckBox controls... (AutoPostBack=true). These controls call the __doPostBack function. Moreover, I use a third party button control that also calls it. Could you fix that too? :)
# simoneb said on 16 January, 2007 04:45 AM

That's odd. So it seems that the submit event of the form is not fired when fired via javascript. I'll give it a shot. Thanks for reporting.

# simoneb said on 17 January, 2007 06:31 PM

It should be working now, check out the updated source on the repository.

# aleksper said on 19 January, 2007 09:34 AM
It's working! Thanks a lot. Now, the solution almost looks perfect. I suggest to introduce some time-out management in order to keep the queue size reasonable. This assumes that the Guid or some additional hidden field shall contain a time-stamp. It should be the perfect solution.
# simoneb said on 19 January, 2007 11:59 AM

Good to know. The queue size can be set via the configuration file, and I think this mechanism would be more reliable than a timeout, but I'll eventually consider it.

# MadManMoon said on 25 January, 2007 10:33 PM
Nice solution. Maybe I am missing something here, but where is the source for the RefreshModule?
# simoneb said on 26 January, 2007 07:39 AM

It's written in the post. Hint: read the bottom line. ;)

# LFN said on 31 January, 2007 08:33 AM
Many thanks. I have re-written it using VB - (because VB is where I started and I dont know any better - he he) . I'm not using it for it's intended purpose - but as an easy access to creating http modules its already proved the basis of an excellent learning opportunity for me. Great code well presented. Every now and again you pass a small milestone. You have provided me with one. Thank you for this - and your part in everything it leads to! :)
# simoneb said on 31 January, 2007 08:57 AM

Thank you LFN, I'm pretty sure I don't deserve such compliments, but thanks anyway!

# Sam said on 02 March, 2007 10:25 PM
Is it possible to have this in .NET 1.1 instead of 2.0? I didn't realise it until I try to add into Visual Studio 2003 application.
# simoneb said on 03 March, 2007 03:35 AM

Yes, of course you can use it with ASP.NET 1.1 too. The only difference is that you'll have to use a different API for the configuration section because ASP.NET 2.0 introduced a new API for that, but you can always rely on the standard AppSettings section instead of creating a custom section.

# Sam said on 04 March, 2007 06:13 AM
thanks....but i couldnt add to the project reference. coz it's build with .net 2.0. i try to find the source and recompile my own but it wasnt there.
# simoneb said on 04 March, 2007 07:29 AM

The source is on the subversion repository of my main project; you'll have to use a svn client to check it out, like TortoiseSVN.

# Sam said on 04 March, 2007 11:32 PM
Thanks. I am noob with this SVN.
# simoneb said on 05 March, 2007 05:35 AM

It's a very common source control system. I've posted a quick guide some time ago on how to use it with one of my projects here: http://dotnetslackers.com/community/blogs/simoneb/archive/2006/08/05/293.aspx

Just look at the section called "QuickStart Guide to SVN" of the post and replace the url specified in the step 5 with the url of the repository containing the RefreshHttpModule project: https://svn.sourceforge.net/svnroot/busybox/trunk

# david said on 08 March, 2007 04:46 PM
Hi, thanks for your great work. I had problem when use the module, it happens on page with inline code, I will get error : The Controls collection cannot be modified because the control contains code blocks (i.e. <% ... %>). It seems you can not insert into control collection of a Form when it has control in it Any idea?
# simoneb said on 08 March, 2007 09:37 PM

Can you send me the code for the page where this happens?

# david said on 10 March, 2007 12:56 AM
very simple page, like <%@ Page Language="C#" EnableViewState="true" EnableEventValidation="false" ViewStateEncryptionMode="Never" %> <% Page.Response.Write("test"); %>
# simoneb said on 10 March, 2007 05:52 AM

I've investigated it a bit David. It looks like it's a well known issue. You simply cannot add controls dynamically to the page when it contains <% %> code blocks.

Take a look a these posts by Rick Strahl for further details:

http://west-wind.com/WebLog/posts/5758.aspx

http://west-wind.com/WebLog/posts/6148.aspx

There are a couple of workarounds to this issue:

- You can use databinding expressions <%# %>, it works fine for simple cases

- You can use controls which render the same stuff you wanted to inject using code blocks, like the LiteralControl class.

Hope it helps.

This site

Search

Go

This Blog

News

Syndication

Sponsors

  • MaximumASP