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