programming4us
programming4us
WEBSITE

ASP.NET 3.5 : Writing HTTP Modules (part 2) - The Page Refresh Feature

11/22/2012 2:46:57 AM

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
ScenarioAction
Page has no ticket associated:
  • No refresh

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:
  • Page refresh occurs if the ticket associated with the page is lower than the last served ticket

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.

Figure 2. The page doesn’t repeat a sensitive action if the user refreshes the browser’s view.

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();
    }
    ...
}

The IsRefreshed property lets you decide what to do when a postback action is requested. In the preceding code, the AddRecord method is not invoked if the page is refreshing. Needless to say, IsRefreshed is available only with the custom page class presented here. The custom page class doesn’t just add the property, it also adds the hidden field, which is essential for the machinery to work.

Other  
 
 
Video tutorials
- How To Install Windows 8

- How To Install Windows Server 2012

- How To Install Windows Server 2012 On VirtualBox

- How To Disable Windows 8 Metro UI

- How To Install Windows Store Apps From Windows 8 Classic Desktop

- How To Disable Windows Update in Windows 8

- How To Disable Windows 8 Metro UI

- How To Add Widgets To Windows 8 Lock Screen

- How to create your first Swimlane Diagram or Cross-Functional Flowchart Diagram by using Microsoft Visio 2010
programming4us programming4us
programming4us
 
 
programming4us