Loading...

28 June, 2009

Preventing Duplicate Record Insertion on Page Refresh

A common concern of ASP.NET developers is, "How do I prevent previously submitted form data from being reinserted into the database when the user presses the browser's Refresh button?" Now you might say "Why on Earth would the user be pressing the Refresh button?" However, if you have been in application development for any length of time, you will know that end users have no limit to their creative approaches to application navigation. And they will indeed press that Refresh button even though there is no logical reason to do so. So you need to program defensively to handle the problem of a Refresh request.

Ideas That Did Not Work

Before we look at solutions that do work, let's look at ideas that did not work. 

Delving into troubleshooting mode, the first idea I had was, "Well, blank out your form elements and you should be good to go'.  But wait, not so fast.  Due to the nature of web pages and posted data, the form values are still held by the browser.  A refresh of the page will post the data again and the data will wind up in your database an additional time.  This has nothing to do with ASP.NET in particular; it is a built-in browser behavior.  The same thing can happen with PHP, ASP, and any other type of web page.

My next idea was to use a hidden field on the form that would be set to an initial value on the first page load. Before the database insertion is performed, that hidden field would be evaluated and if it contains the expected initial value then the insert would be permitted.  Following the insert the hidden field would then be set to a different value.  However, this again does not work because with the refresh the form's previously posted data is resent, and the hidden field still contains that initial value.

Pulling the next trick out of the bag, I changed the above approach to use a Session variable instead of a hidden form field.  And lo and behold this approach worked.  It actually worked so well that once one record was successfully inserted into the database, no more could be added, even intentionally, since once that Session variable was set there was no way to know when it was okay to reset it to the initial value.  This is not a feasible solution because a web application would likely need to write more than one record to the database.

The last approach I tried without success was disallowing the caching of the page on the user’s browser.  This is accomplished by adding the following to the Page.Load handler:

Response.Cache.SetExpires(DateTime.Now.AddDays(-1))
Response.Cache.SetCacheability(HttpCacheability.NoCache)
Response.Cache.SetValidUntilExpires(false)

The intent here was to force the user's browser to call for a fresh version of the page each time.  However, this had no effect on the posted data in the header, and the Refresh button still had the effect of reposting the form data.

Now that we have considered a few ideas, and found that they do not work, let's look at some solutions that do work.

Solution that Work by Clearing the Header

A simple solution is to Response.Redirect back to the same page after the INSERT command is called.  This will call up the page without transmitting any post headers to it.  Using Request.Url.ToString() as the first parameter of Response.Redirect will cause both the URL and the page's querystring to be included in the redirect.  The use of false as the second parameter will suppress the automatic Response.End that may otherwise generate a ThreadAbortedException.  A disadvantage of this approach is that any ViewState that had been built up will be lost.

Sub Button1_Click(sender As Object, e As EventArgs)
    If AddEmployee(firstName.Text, lastName.Text) = 0
        Message.Text = "Success"
        Response.Redirect(Request.Url.ToString(), false) ' will include the querystring
    Else
        Message.Text = "Failure"
    End If
    firstName.Text = ""
    lastName.Text = ""
End Sub

Function AddEmployee(firstName As String, lastName As String) As Integer
    Dim connectionString As String
    connectionString = "server='(local)'; trusted_connection=true; database='Northwind'"
    Dim dbConnection As New SqlConnection(connectionString)
    Dim insertString As String
    insertString = "INSERT INTO Employees (FirstName, LastName) VALUES (@FirstName, @LastName)"
    Dim dbCommand As New SqlCommand
    dbCommand.CommandText = insertString
    dbCommand.Connection = dbConnection
    dbCommand.Parameters.Add(New SqlParameter("@FirstName",SqlDbType.NVarchar,10))
    dbCommand.Parameters("@FirstName").Value = firstName
    dbCommand.Parameters.Add(New SqlParameter("@LastName",SqlDbType.NVarchar,20))
    dbCommand.Parameters("@LastName").Value = lastName
    Dim rowsAffected As Integer = 0
    Dim commandResult As Integer = 1
    Try
        dbConnection.Open
        rowsAffected = dbCommand.ExecuteNonQuery
        If rowsAffected > 0 Then commandResult = 0
    Catch ex AS SqlException
        commandResult = ex.Number
    Finally
        dbConnection.Close
    End Try
    Return commandResult
End Function

As a side note, I thought that Server.Transfer would be a more lightweight alternative to Response.Redirect; however this method does not clear the post data from the header and hence does not solve the refresh problem.

A related approach would be for the form to submit to an intermediate processing page and then Response.Redirect back to the calling page, similar to the classic ASP approach to form processing.  This has the same effect as simply using the Response.Redirect in the Button_Click event so it has the same disadvantages, with the added disadvantage of creating another page for the website developer
to manage.

Solution that Work by Detecting the Browser’s Refresh

The next batch of solutions works by determining whether the user has refreshed the page in the browser instead of pressing the form's submit button.  All of these solutions depend on the ability of the website to use Session variables successfully.  If the website uses cookie-based Sessions, but the user's browser does not permit the use of cookies, these solutions would all fail.  Additionally, should the Session expire these solutions would also fail.

Sub Page_Load (sender As Object, e As EventArgs)
    If Not Page.IsPostBack
        Session("update") = Server.URLEncode(System.DateTime.Now.ToString())
    End If
End Sub

Sub Page_PreRender (sender As Object, e As EventArgs)
    ViewState("update") = Session("update")
End Sub

Sub Button1_Click(sender As Object, e As EventArgs)
    If Session("update").ToString() = ViewState("update").ToString() Then
        If AddEmployee(firstName.Text, lastName.Text) = 0
            Message.Text = "Success"
            Session("update") = Server.URLEncode(System.DateTime.Now.ToString())
        Else
            Message.Text = "Failure"
        End If
    Else
        Message.Text = "Failure - Session"
    End If
    firstName.Text = ""
    lastName.Text = ""
End Sub

A simple way to implement refresh trapping is by the use of a date/time stamp held in a ViewState variable and a date/time stamp held in the user's Session.  When the page is first loaded, a Session variable is populated with the current date/time.  On the page's PreRender event, a ViewState variable is set to the value of the Session variable.  These two values are compared to each other immediately before the database INSERT command is run.  If they are equal, then the command is permitted to execute and the Session variable is updated with the current date/time, otherwise the command is bypassed.  Should the user refresh the page, the ViewState variable will be repopulated from the post header, so then the ViewState and Session variables will no longer hold the same values, and the INSERT command will not run.  Note that ViewState needs to be enabled on the page for this to work; if ViewState is not enabled then a hidden form field may be used instead.

An additional advantage is that the page refresh can be trapped for a variety of scenarios, not just for the prevention of duplicate records which we are concerned with here.

Hope this article will help my developer friends with this browser refresh nightmare. Best of Luck !!

0 comments: