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.