The Page Refresh Feature
Let’s examine a
practical situation in which the ability to filter the request before it
gets processed by an HTTP handler helps to implement a feature that
would otherwise be impossible. The postback mechanism has a nasty
drawback—if the user refreshes the currently displayed page,
the last action taken on the server is blindly repeated. If a new
record was added as a result of a previous posting, for example, the
application would attempt to insert an identical record upon another
postback. Of course, this results in the insertion of identical records
and should result in an exception. This snag has existed since the dawn
of Web programming and was certainly not introduced by ASP.NET. To
implement nonrepeatable actions, some countermeasures are required to
essentially transform any critical server-side operation into an idempotency. In algebra, an operation is said to be idempotent
if the result doesn’t change regardless of how many times you execute
it. For example, take a look at the following SQL command:
DELETE FROM employees WHERE employeeid=9
You can execute the
command 1000 consecutive times, but only one record at most will ever be
deleted—the one that satisfies the criteria set in the WHERE clause.
Consider this command, instead:
INSERT INTO employees VALUES (...)
Each time you execute
the command, a new record might be added to the table. This is
especially true if you have auto-number key columns or nonunique
columns. If the table design requires that the key be unique and
specified explicitly, the second time you run the command a SQL
exception would be thrown.
Although the
particular scenario we considered is typically resolved in the data
access layer (DAL), the underlying pattern represents a common issue for
most Web applications. So the open question is, how can we detect
whether the page is being posted as the result of an explicit user
action or because the user simply hit F5 or the page refresh toolbar button?
The Rationale Behind Page Refresh Operations
The page refresh action
is a sort of internal browser operation for which the browser doesn’t
provide any external notification in terms of events or callbacks.
Technically speaking, the page refresh consists of the “simple”
reiteration of the latest request. The browser caches the latest request
it served and reissues it when the user hits the page refresh key or
button. No browsers that I’m aware of provide any kind of notification
for the page refresh event—and if there are any that do, it’s certainly
not a recognized standard.
In light of this, there’s
no way the server-side code (for example, ASP.NET, classic ASP, or ISAPI
DLLs) can distinguish a refresh request from an ordinary submit or
postback request. To help ASP.NET detect and handle page refreshes, you
need to build surrounding machinery that makes two otherwise identical
requests look different. All known browsers implement the refresh by
resending the last HTTP payload sent; to make the copy look different
from the original, any extra service we write must add more parameters
and the ASP.NET page must be capable of catching them.
I considered some
additional requirements. The solution should not rely on session state
and should not tax the server memory too much. It should be relatively
easy to deploy and as unobtrusive as possible.
Outline of the Solution
The solution is based
on the idea that each request will be assigned a ticket number and the
HTTP module will track the last-served ticket for each distinct page it
processes. If the number carried by the page is lower than the
last-served ticket for the page, it can only mean that the same
request has been served already—namely, a page refresh. The solution
consists of a couple of building blocks: an HTTP module to make
preliminary checks on the ticket numbers, and a custom page class that
automatically adds a progressive ticket number to each served page.
Making the feature work is a two-step procedure: first, register the
HTTP module; second, change the base code-behind class of each page in
the relevant application to detect browser refreshes.
The HTTP module sits in
the middle of the HTTP runtime environment and checks in every request
for a resource in the application. The first time the page is requested
(when not posting back), there will be no ticket assigned. The HTTP
module will generate a new ticket number and store it in the Items collection of the HttpContext
object. In addition, the module initializes the internal counter of the
last-served ticket to 0. Each successive time the page is requested,
the module compares the last-served ticket with the page ticket. If the
page ticket is newer, the request is considered a regular postback;
otherwise, it will be flagged as a page refresh. Table 2 summarizes the scenarios and related actions.
Table 2. Scenarios and Actions
Scenario | Action |
---|
Page has no ticket associated:
| Counter of the last ticket served is set to 0.
The ticket to use for the next request of the current page is generated and stored in Items. |
Page has a ticket associated:
| Counter of the last ticket served is set with the ticket associated with the page.
The ticket to use for the next request of the current page is generated and stored in Items. |
Some help from the page
class is required to ensure that each request—except the first—comes
with a proper ticket number. That’s why you need to set the code-behind
class of each page that intends to support this feature to a particular
class—a process that we’ll discuss in a moment. The page class will
receive two distinct pieces of information from the HTTP module—the next
ticket to store in a hidden field that travels with the page, and
whether or not the request is a page refresh. As an added service to
developers, the code-behind class will expose an extra Boolean property—IsRefreshed—to let developers know whether or not the request is a page refresh or a regular postback.
Important
The Items collection on the HttpContext
class is a cargo collection purposely created to let HTTP modules pass
information down to pages and HTTP handlers in charge of physically
serving the request. The HTTP module we employ here sets two entries in
the Items
collection. One is to let the page know whether the request is a page
refresh; another is to let the page know what the next ticket number is.
Having the module pass the page the next ticket number serves the
purpose of keeping the page class behavior as simple and linear as
possible, moving most of the implementation and execution burden on to
the HTTP module. |
Implementation of the Solution
There are a few open
points with the solution I just outlined. First, some state is required.
Where do you keep it? Second, an HTTP module will be called for each
incoming request. How do you distinguish requests for the same page? How
do you pass information to the page? How intelligent do you expect the
page to be?
It’s clear that each
of these points might be designed and implemented in a different way
than shown here. All design choices made to reach a working solution
here should be considered arbitrary, and they can possibly be replaced
with equivalent strategies if you want to rework the code to better suit
your own purposes. Let me also add this disclaimer: I’m not aware of
commercial products and libraries that fix this reposting problem. In
the past couple of years, I’ve been writing articles on the subject of
reposting and speaking at various user groups. The version of the code
presented in this next example incorporates the most valuable
suggestions I’ve collected along the way. One of these suggestions is to
move as much code as possible into the HTTP module, as mentioned in the
previous note.
The following code shows the implementation of the HTTP module:
public class RefreshModule : IHttpModule
{
public void Init(HttpApplication app) {
app.BeginRequest += new EventHandler(OnAcquireRequestState);
}
public void Dispose() {
}
void OnAcquireRequestState(object sender, EventArgs e) {
HttpApplication app = (HttpApplication) sender;
HttpContext ctx = app.Context;
RefreshAction.Check(ctx);
return;
}
}
The module listens to the BeginRequest event and ends up calling the Check method on the helper RefreshAction class:
public class RefreshAction
{
static Hashtable requestHistory = null;
// Other string constants defined here
...
public static void Check(HttpContext ctx) {
// Initialize the ticket slot
EnsureRefreshTicket(ctx);
// Read the last ticket served in the session (from Session)
int lastTicket = GetLastRefreshTicket(ctx);
// Read the ticket of the current request (from a hidden field)
int thisTicket = GetCurrentRefreshTicket(ctx, lastTicket);
// Compare tickets
if (thisTicket > lastTicket ||
(thisTicket==lastTicket && thisTicket==0)) {
UpdateLastRefreshTicket(ctx, thisTicket);
ctx.Items[PageRefreshEntry] = false;
}
else
ctx.Items[PageRefreshEntry] = true;
}
// Initialize the internal data store
static void EnsureRefreshTicket(HttpContext ctx)
{
if (requestHistory == null)
requestHistory = new Hashtable();
}
// Return the last-served ticket for the URL
static int GetLastRefreshTicket(HttpContext ctx)
{
// Extract and return the last ticket
if (!requestHistory.ContainsKey(ctx.Request.Path))
return 0;
else
return (int) requestHistory[ctx.Request.Path];
}
// Return the ticket associated with the page
static int GetCurrentRefreshTicket(HttpContext ctx, int lastTicket)
{
int ticket;
object o = ctx.Request[CurrentRefreshTicketEntry];
if (o == null)
ticket = lastTicket;
else
ticket = Convert.ToInt32(o);
ctx.Items[RefreshAction.NextPageTicketEntry] = ticket + 1;
return ticket;
}
// Store the last-served ticket for the URL
static void UpdateLastRefreshTicket(HttpContext ctx, int ticket)
{
requestHistory[ctx.Request.Path] = ticket;
}
}
The Check
method performs the following actions. It compares the last-served
ticket with the ticket (if any) provided by the page. The page stores
the ticket number in a hidden field that is read through the Request
object interface. The HTTP module maintains a hashtable with an entry
for each distinct URL served. The value in the hashtable stores the
last-served ticket for that URL.
Note
The Item indexer property is used to set the last-served ticket instead of the Add method because Item overwrites existing items. The Add method just returns if the item already exists. |
In addition to creating
the HTTP module, you also need to arrange a page class to use as the
base for pages wanting to detect browser refreshes. Here’s the code:
// Assume to be in a custom namespace
public class Page : System.Web.UI.Page
{
public bool IsRefreshed {
get {
HttpContext ctx = HttpContext.Current;
object o = ctx.Items[RefreshAction.PageRefreshEntry];
if (o == null)
return false;
return (bool) o;
}
}
// Handle the PreRenderComplete event
protected override void OnPreRenderComplete(EventArgs e) {
base.OnPreRenderComplete(e);
SaveRefreshState();
}
// Create the hidden field to store the current request ticket
private void SaveRefreshState() {
HttpContext ctx = HttpContext.Current;
int ticket = (int) ctx.Items[RefreshAction.NextPageTicketEntry];
ClientScript.RegisterHiddenField(
RefreshAction.CurrentRefreshTicketEntry,
ticket.ToString());
}
}
The sample page defines a new public Boolean property IsRefreshed that you can use in code in the same way you would use IsPostBack or IsCallback. It overrides OnPreRenderComplete
to add the hidden field with the page ticket. As mentioned, the page
ticket is received from the HTTP module through an ad hoc (and
arbitrarily named) entry in the Items collection.
Figure 2 shows a sample page in action. Let’s take a look at the source code of the page.
public partial class TestRefresh : Core35.Components.Page
{
protected void AddContactButton_Click(object sender, EventArgs e)
{
Msg.InnerText = "Added";
if (!this.IsRefreshed)
AddRecord(FName.Text, LName.Text);
else
Msg.InnerText = "Page refreshed";
BindData();
}
...
}