Preventing double form submit in ASP.NET (2.0)
By George Mihaescu
Summary: Sometimes pages take long to process on the
server side and it is critical that the user does not submit the form multiple
times by repeatedly clicking the submit button while waiting for the form to
process. This document presents a number of solutions for this, with special
emphasis (and examples in C#) on disabling the submit button while the form is
processing.
The solutions were tested on .Net 2.0 across IE (6 & 7)
and Firefox (1.5 and 2) in fairly complex usage scenarios (master pages, user
controls, multiple groups of controls in different validation groups, etc).
They were not tested on .Net 1.x (they probably will not work).
The best solution I have found so far is the last one listed (disable the
form’s submit button while the form is being processed).
The problem
Some forms may take long to process on the server side;
typical examples are forms that interact with 3rd part systems, such
as on-line order processing systems (e.g. credit card payment forms) or email
systems (e.g. user registration forms, where the user needs to be emailed his
registration info). There is nothing you can do to control the duration of the
form processing in such instances, and you don’t want the user to re-submit the
form by clicking the submit button again without realizing that the form is
already submitted (or just being frustrated by the wait).
Obviously many other forms may exhibit the same problem, but
depending on the specifics of your application, it may not be critical that the
form is not re-submitted. It’s up to you to decide where you want to apply the
solutions listed below (and which solution, depending on your circumstances).
The solutions
I found a number of solutions to this problem; I will list
them all, for completeness, although I will be focusing on the ones that deal
with disabling the submit button in the form as I find those to be the best in
terms of usability (user feedback) and complexity of implementation.
1.
Server-side solution: issue a token
Simply stated, this solution consists of creating a token
(such as DateTime.Now.Ticks) and placing that into a session variable and
into a hidden field of the form when first presenting the form to the user. On
the server, when the form is submitted, check the value of the hidden field and
compare it with the one stored in the session; if they are the same, this is
the first submit, so remove the token from the session and start processing the
form. Subsequent submissions will not find the token in the session anymore,
and at that point you can decide how to handle this: simply ignore the
submission, or show the user a page saying “your form is already being
processed”, etc.
The disadvantage of this is that the user has no feedback in
the browser that his form is actually being processed upon his first click
(well, he actually does because browsers do show a progress bar, an animated
icon of some sort, or similar, but those may be too subtle sometimes).
2.
Server-side solution: asynchronous processing
What I'm referring to here is not the standard asynchronous
ASP.Net page processing – this will not help in our problem as the request does
not return until the processing has completed (just like in normal, synchronous
processing) and therefore the user will still be able to click the submit
button again.
What I am referring to is a scenario in which in the
server-side click handler of your submit button start the asynchronous form
processing (i.e. on a worker thread) and return the user a page with the
message “Your form is being processed, please wait” (through a
Response.Redirect () or Server.Transfer (), etc) – this page will have to
periodically poll the server to determine the result of the asynchronous
processing.
This solution is not the best in terms of performance and
reliability (due to the need to poll the server), and adds substantial
complexity to the whole process. In addition, there is also the usability issue,
which may be critical here: the user can actually navigate to a different page
after seeing this message because he feels that the submission was ok (as
opposed to still seeing the original form, where he would feel compelled to
wait for a message). So consider the client-side solutions below and you’ll see
that there’s no need to delve into those complexities.
3.
Client-side solution: change the form submission code (limited)
I’ll start by stating that this solution (unlike the one
below) is limited because it will not work if you page contains input controls
that belong to different validation groups. For instance, you may have a
side-bar that contains a search text box + its button, and the login controls
(user text box, password text box + the login button), plus you actual page
controls and their submit button. As you want each group of such functionally
related controls to work independently (as if they were in different forms) you
will place them in different validation groups. In this scenario, the solution
below will not work, as it will always validate all the controls on the
page, regardless of their validation group.
The solution consists in adding code in your Page_Load to
modify the client-side java script called when the form is submitted. The
default code generated by ASP.Net (2.0) for this is:
<script type="text/javascript">
<!--
function WebForm_OnSubmit()
{
if (typeof(ValidatorOnSubmit) == "function" &&
ValidatorOnSubmit() == false)
return false;
return true;
}
// -->
</script>
Now, if you could insert somehow some javascript code just
above returning true (i.e. after form validation was passed ok), to disable the
submit button, the problem is solved. The answer is that you cannot insert
javascript right before the return true statement, but you can right at the
beginning of the function, which is good enough, because you can always return
true (and render the existing code dead). To do this, use the
ClientManager.RegisterOnSubmitStatement method, as shown below (note that I
have not used the StringBuilder, I just concatenated on a String, which is not
a good idea):
protected void Page_Load(object sender, EventArgs
e)
{
//SOLUTION
1: CHANGE THE FORM SUBMIT CODE TO VALIDATE THE PAGE, THEN DISABLE //THE BUTTON
BEFORE SUBMITTING
//DOES NOT
WORK IF PAGE CONTAINS MULTIPLE CONTROLS IN DIFFERENT VALIDATION //GROUPS
BECAUSE IT VALIDATES ALL CONTROLS
String jsname = "OnSubmitScript";
Type jstype = this.GetType();
// Check to see if the OnSubmit
statement is already registered.
if (!Page.ClientScript.IsOnSubmitStatementRegistered(jstype,
jsname))
{
String jstext = "if (typeof(ValidatorOnSubmit) == 'function' &&
ValidatorOnSubmit() == false)return false;\n";
jstext += "else\n";
jstext += "{\n";
jstext += "\t var button =
document.getElementById('" + this.buttonSubmit.ClientID + "');\n";
jstext += "\t button.value =
'Processing...';\n";
jstext += "\t
button.disabled = true;\n";
jstext += "}\n";
jstext += "return
true;\n";
Page.ClientScript.RegisterOnSubmitStatement(jstype, jsname,
jstext);
}
}
Note that you have to execute this code on all Page_Load
calls (i.e. don’t do it only if
(!IsPostback)) because you may have server-side validation code
that will re-render the page on an invalid submission. This will produce the
following javascript in your page (I colored the new code in green):
<script type="text/javascript">
<!--
function WebForm_OnSubmit()
{
if (typeof(ValidatorOnSubmit) ==
'function' && ValidatorOnSubmit() == false)return false;
else
{
var button = document.getElementById('ctl00_ContentPlaceHolder1_buttonSubmit');
button.value =
'Processing...';
button.disabled = true;
}
return true;
if (typeof(ValidatorOnSubmit) == "function" &&
ValidatorOnSubmit() == false) return false;
return true;
}
Essentially this renders the existing code dead (by doing a
return true just above it) and replaces it with code that first does the same
validation (using ValidatorOnSubmit) and if it finds the form to be valid,
changes the text on the submit button to “Processing…” then disables it.
Note that in the C# code (in Page_Load) I used
buttonSubmit.ClientID to get the client-side ID of the button, as the button
may be in a user-control, in a child page of a master page, etc. and its ID
will be generated dynamically by ASP.Net – so don’t hard-code it).
The problem with this solution is that ValidatorOnSubmit (at
least as called above) will validate ALL controls on the page, regardless of
their validation group. So if you do have controls in different validation
groups you will find that your form is not submitted because it does not pass
client-side validation. Maybe a solution exists to this issue, but I did not
dig deep as I have below a solution that works in all contexts I’ve tried.
4.
Client-side solution: change the button submission code (best and
recommended)
This is the best solution I have found so far. It consists
of modifying the client-side javascript of the submit button to perform
client-side validation only on the validation group the button belongs to,
and if validation is passed, to submit the form.
The standard code generated by ASP.Net 2.0 for a submit
button is:
onclick="javascript:WebForm_DoPostBackWithOptions(new
WebForm_PostBackOptions('buttonSubmit', '', true, '';, '';, false,
false))"
Now, we could think of doing something similar to the above
solution: insert javascript code before the call to
WebForm_DoPostBackWithOptions to validate the form, and if found valid, disable
the submit button and continue with calling WebForm_DoPostBackWithOptions.
However, there is a small issue with this – the paragraph below explains it:
WebForm_DoPostBackWithOptions
is new to ASP.Net 2.0. It was added to replace the _doPostback function of 1.x
and provide better functionality (client-side validation preserving scroll position,
etc) before submitting the form. However, one new behavior is that it will not
submit the form if the control (e.g. button) that called it is not visible and
enabled). What this means for us is that we cannot simply disable the submit
button before calling WebForm_DoPostBackWithOptions on it, as the call will not
submit because we’ve disabled the button.
So we’ll do this: add javascript code right at the beginning
of the onclick handler to validate the form controls in the same validation
group with the submit button. If the controls are valid, then disable the
button and post the form using the old __doPostback call (so that we force
posting the form, circumventing the checks in the new
WebForm_DoPostBackWithOptions).
You will need to add the following code to your Page_Load:
protected void
Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
//SOLUTION 2:
CHANGE THE BUTTON SUBMIT JS CODE SO THAT IT
//DISABLES WHEN
THE FORM IS VALID, PRIOR TO SUBMISSION
//build the JS
StringBuilder
sb = new StringBuilder();
sb.Append("if
(typeof(Page_ClientValidate) == 'function') { ");
//if client-side
does not validate, stop (this supports validation groups)
//BUGFIX: must
save, then restore the page validation / submission state, otherwise
//when the
validation fails, it prevents the FIRST autopostback from other controls
sb.Append("var
oldPage_IsValid = Page_IsValid; var oldPage_BlockSubmit =
Page_BlockSubmit;");
sb.Append("if
(Page_ClientValidate('" + buttonSubmit.ValidationGroup + "') == false) {");
sb.Append("
Page_IsValid = oldPage_IsValid; Page_BlockSubmit = oldPage_BlockSubmit; return
false; }} ");
//change button
text and disable it
sb.Append("this.value
= 'Processing...';");
sb.Append("this.disabled
= true;");
//insert the call
to the framework JS code that does the postback of the form in the client
//The default code
generated by ASP (WebForm_DoPostbackWithOptions) will not
//submit because
the button is disabled (this is new in 2.0)
sb.Append(ClientScript.GetPostBackEventReference(buttonSubmit, null) + ";");
//BUGFIX: MUST
RETURN AFTER THIS, OTHERWISE IF THE BUTTON HAS UseSubmitBehavior=false
//THEN ONE CLICK
WILL IN FACT CAUSE 2 SUBMITS, DEFEATING THE WHOLE PURPOSE
sb.Append("return
true;");
string
submit_button_onclick_js = sb.ToString();
buttonSubmit.Attributes.Add("onclick", submit_button_onclick_js);
}
}
This actually inserts calls to Page_ClientValidate
client-side javascript (ASP.Net function) which can take a validation group
name as a parameter. Note that I’ve used the button’s ValidationGroup property
to insert the name of the validation group as a parameter to this function.
Then, if validation is passed on that validation group, the button label is
changed, the button is disabled and __doPostback is called to do the posting of
the form (the name of the __doPostback function is obtained using the
ClientManager.GetPostBackEventReference method).
Note that you can do the above on if (!IsPostback) in your Page_Load,
unlike the solution before. The result is the following onclick javascript code
inserted for the submit button:
onclick="if (typeof(Page_ClientValidate) == 'function') {
var oldPage_IsValid = Page_IsValid; var oldPage_BlockSubmit =
Page_BlockSubmit;if (Page_ClientValidate('') == false) { Page_IsValid =
oldPage_IsValid; Page_BlockSubmit = oldPage_BlockSubmit; return false; }}
this.value = 'Processing...';this.disabled =
true;__doPostBack('buttonSubmit','');return
true;WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions('buttonSubmit', '',
true, '', '', false, true))"
In my case, the button was in the default (un-named)
validation group and that’s why Page_ClientValidate is called with an empty
string parameter. The call to WebForm_DoPostbackWithOption remains (we cannot
remove it) but will do nothing as we don't reach it due to the return statement
we insert just above it.
This solution has worked when used in a child page of a
master page, or in a user control, with multiple logical control groups
belonging to different validation groups. This seems to be the best solution
because it is low complexity, robust and has the best usability: the user is
kept on the same page (therefore he’s expecting a result and does not feel free
to navigate to another page) but he’s unable to perform multiple submissions. This
was tested on IE 6 & 7 and Firefox 1.5 and 2.0 and worked flawlessly.