I know it has been kinda quiet around here lately, and tonight I finally got some time to post a solution to a legendary web application problem (I also rolled out a few updates to my blog, as you may have already noticed). The issue at-hand probably gets most of its exposure through the use of the ASP.NET framework. On a web form, it is often a requirement to prevent multiple form submissions to maintain efficiency and avoid complications on the server-side. The most visually appealing way to accomplish this is by disabling the submit button as soon as the form submits, essentially stopping the user from going on a clicking spree. Seems like such a trivial task, but unfortunately doing it the normal way will cause problems.
The Problem
When an HTML form gets submitted, an HTTP POST is automatically generated and shipped off to the form tags "action" value. This POST data is obviously comes from the form elements, and this is how data gets from your computer to the server. For example, imagine I have a form with two textboxes and a submit button…
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Test Page</title> </head> <body> <form id="frmTest" runat="server"> <asp:TextBox ID="txtBox1" runat="server" Text="Test1"></asp:TextBox> <asp:TextBox ID="txtBox2" runat="server" Text="Test2"></asp:TextBox> <asp:Button ID="btnSubmit" runat="server" Text="Submit" /> </form> </body> </html>
When that form gets submitted, the following POST is generated (note that I removed the ViewState and EventValidation data, as well as a couple irrelevant HTTP headers from the following POST examples for brevity)…
POST /Sandbox/default.aspx HTTP/1.1
Connection: Keep-Alive
Content-Length: 44
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)
txtBox1=Test1&txtBox2=Test2&btnSubmit=Submit
In case you are not familiar with POST, the bottom line is the actual data. You can see it is a simple collection of name/value pairs. When a control within the form is disabled, it no longer becomes a part of the POST. A disabled control is meant to be read-only, so it saves space by not including them with the POST. And there inlies the problem. Here is the same HTTP post outlined above, except this time I disable the submit button with Javascript once it gets clicked…
POST /Sandbox/default.aspx HTTP/1.1
Connection: Keep-Alive
Content-Length: 44
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)
txtBox1=Test1&txtBox2=Test2
You will notice on the bottom line of the POST that the btnSubmit parameter is no longer being passed. You wouldn't think that this would cause a problem, since all of the data inputted by the user still made it to the server. However, ASP.NET takes this button value much more seriously. Typically, you would have some code-behind for the button click event to process the form…
Protected Sub btnSubmit_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnSubmit.Click 'Form processing logic is here End Sub
ASP.NET will only invoke this click event when the submit button's value is included in the POST. I believe the EventArgument value also has something to do with it, but that's another story for another day. Without that value in the POST, your Page_Load will still get fired, but the code in your button's click event handler will never execute. Granted, you could put some crap code in your Page_Load to look for the existence of certain parameters in the request and react to that accordingly. I think it's safe to say that is nothing close to a reasonable solution. There are a few solutions I have stumbled across, most of which I saw in an ASP.NET forums. None of them were very appealing to me because they required tedious server-side logic that would obviously be a bitch to maintain. I hate wasting time doing maintenace (who doesn't?), so I couldn't settle for that. I wanted a solution that had nothing to do with server-side code; a task this trivial shouldn't! I wanted something that could be implemented on existing forms painlessly. And lastly, I wanted something that could easily be extended and configured to accomodate specific scenarios.
The Basic Idea of My Solution
It's pretty simple, actually. Instead of disabling the submit button itself, I just hide it with Javascript. This is only modifying the style of the control, so the value still comes through with the POST. Then I programatically create a disabled <button> tag with the message of my choice and insert it into the DOM, right before the actual submit button. It will happens so quickly that it will appear that the submit button had been disabled, and it's value changed. Works like a charm!
I have setup a really simple example of this, nothing but pure HTML with all the basic Javascript directly in the markup so you can see the general idea of how it works. That example is not dynamic at all, and it's fairly obtrusive. With that said, I have a much cleaner solution that I'd like to share with you.
Doing Things the Right Way
I finally got some time to do what I wanted with this idea. I created an unobtrusive object model that automatically does everything for you. Once the page loads, it loops through every form and attaches another onsubmit handler to the existing one. If no onsubmit handler exists, or if the existing handler returns true, it will loop through each input element within that form (starting with the last one) and disable each submit button (using the method described above). More specifically, it will disable each <input type="submit"> and <input type="image">. It is designed to stop the loop when it encounters the first textbox for the sake of efficiency, but I have set up a configuration option to toggle that. There is also an option to hide all other buttons on the form when it gets submitted, if they exist (<input type="reset"> and <input type="button">). I put one more option in there to change the document cursor to the "Waiting" icon once the button becomes disabled. I figured that would be one final visual indicator for the user. I put some comments in there explaining the configuration options. The options themselves are directly at the top of the object. Here is the code…
function addLoadEvent(func) { if(typeof window.onload != 'function') window.onload = func; else { var oldLoad = window.onload; window.onload = function() { if(oldLoad) oldLoad(); func(); } } } /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Button Disabler * * Version 1.0 * * Written by Josh Stodola * * January 29, 2008 * * * * * * * * * * * * *CONFIGURATION OPTIONS* * * * * * * * * * * * * * * IsTesting (Boolean, defaults to false) * * When this is set to true, the form will never submit. * * Use this to confirm that the script is working. * * * * DisabledButtonValue (String, defaults to 'Please Wait') * * This is the value to show on the button once disabled. * * * * HideNonSubmitButtons (Boolean, defaults to true) * * When true, the script will also hide any reset buttons * * or Javascript buttons it encounters. * * * * ShowHourglassCursor (Boolean, defaults to true) * * When true, the script will change the cursor to the * * OS-defined "waiting" symbol to indicate loading. * * * * StopLoopAtFirstTextbox (Boolean, defaults to false) * * When true, the script will stop looping through input * * elements once it encounters the first textbox. The loop * * begins at the bottom of the form. This can be set to * * true to make the script more efficient. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ var ButtonDisabler = { IsTesting: true, DisabledButtonValue: 'Please Wait', HideNonSubmitButtons: true, ShowHourglassCursor: true, StopLoopAtFirstTextbox: false, IsCapable: (document.getElementById && document.createElement), AddSubmitEvent: function(frm, func) { if(typeof frm.onsubmit != 'function') frm.onsubmit = func; else { var oldSub = frm.onsubmit; frm.onsubmit = function() { if(oldSub) { if(oldSub()) return func(); else return false; } else return func(); } } }, LoadEventHandlers: function() { if(ButtonDisabler.IsCapable) { for(var i = 0; i < document.forms.length; i++) { var frm = document.forms[i]; ButtonDisabler.AddSubmitEvent(frm, function() { ButtonDisabler.DisableForm(frm); return !ButtonDisabler.IsTesting; }); } } }, DisableForm: function(frm) { if(!frm) return; var inputs = frm.getElementsByTagName('INPUT'); for(var j = inputs.length - 1; j >= 0; j--) { var elem = inputs[j]; if(elem.type == 'submit' || elem.type == 'image') { var btn = document.createElement('button'); btn.disabled = true; btn.innerHTML = ButtonDisabler.DisabledButtonValue; elem.parentNode.insertBefore(btn, elem); elem.style.display = 'none'; } if(elem.type == 'reset' || elem.type == 'button') { if(ButtonDisabler.HideNonSubmitButtons) elem.style.display = 'none'; } if(elem.type == 'text' && ButtonDisabler.StopLoopAtFirstTextbox) break; } if(ButtonDisabler.ShowHourglassCursor) document.body.style.cursor = 'wait'; frm.onsubmit = function() { return false; } } }; addLoadEvent(ButtonDisabler.LoadEventHandlers);
How to Use the Script
This best part of this script is its ease-of-use. All you have to do is setup the options however you prefer, and import the Javascript into your web page markup (usually in the <head> section) with <script> tags and the script will do the rest. I have tested it in all modern browsers and it works fantastic. I have also tested it with a series of the ASP.NET validator controls and several of my custom form validation methods, all without any problems. There are a few other things you should know about this script…
- Web Pages with Multiple Forms
- This script is designed to handle pages with multiple forms without any problems. Submit buttons will be disabled on a per-form basis. So, if you submit FormA, FormB will still have a visible, enabled, clickable submit button.
- Forms with Multiple Submit Buttons
- The script is also designed to handle forms with multiple submit buttons. Once submitted, each and every submit button within the form will become disabled. It does this because the overall purpose is to prevent multiple form submissions. When a form has multiple submit buttons, each of them will submit the same form when clicked, so we must disable them all to achieve the desired effect.
That's it! I have probably read dozens of threads on the ASP.NET forums regarding this problem, and now I am glad that I have solution in place to point people to. Most scripts are not perfect, so if you encounter any kind of oddity or bug that is worth questioning, please feel free to let me know about it through the comments form below.
'Til next time…
Which shouldn't be a problem because it can't be clicked because it has been disabled.
So what did I miss ?
So, without the value in the POST, the server-side click event will not fire.
I use Google's personalized home page to watch for updates on blogs (including this one) and such. When I clicked on the link for this entry, I was directed to "blog.josh420.com/archives/2008/02/how-to-disable-the-submit-button-of-web-form.aspx", which lead me to see "That post was not found!".
I noticed that the actual URL for this page is "blog.josh420.com/archives/2008/02/how-to-disable-the-submit-button-ofweb-form.aspx". The difference lies between the word "of" and "web". Google's link has a dash, the real URL does not.
The weird part is that I used Google's link to view this earlier, or so I thought.
Did you change the page name or URL recently?
PART 2
That's a neat trick that you outline above. I'll keep it in mind for future projects where multiple submits maybe an issue.
Yes, there was a URL rewriting discrepency that caused the post to be inaccessible from my RSS feed, Digg, or DotNetKicks. The problem was with the way it interprets the "a" in the blog title. Because of a silly code error (lack of sleep), it did not strip it out appropriately. I fixed my blog and re-deployed it to the server. Then when I went to update the blog post (I noticed a typo), it corrected the permalink! I never intended to EVER update the permalink field of any blog post after it gets published, so I dont know when I put that code in there or why.
The post was still on the front page of my blog, but all external links were incorrect. I'm embarassed to say that it was like this for about 8 hours. I just changed the subject URL back to what it was before, even though it's not how it really should be. But, I guess permalink means permanent, right?
You can actually do this easier, using the UseSubmitBehavior property of the Button control (in ASP.NET 2.0+). It basically causes the button to __doPostBack instead of relying on the form submission behavior of the browser, leaving you free to disable it on the client side and not interfere with the postback.
http://encosia.com/2007/04/17/disable-a-button-control-during-postback/
The problem I have with any kind of server-side approach to this is that it requires the _doPostback function. I think with the new MVC framework, developers are trying to get rid of this postback/viewstate stuff and are going back to the classic, proven method(s). It also does not seem right for the server-side to have anything to do with a subtle visual enhancement such as this. And maybe this is me being picky, but I don't want inline Javascript at all. I like to keep things as unobtrusive as possible; behavioral separation has numerous benefits. The only inline script you will find on this blog is for the CAPTCHA and the comments with displayed email addresses.
Thanks again for the comment, and have a great day!
I have been looking every where for an "all included solution" like yours.
Sadly i tried it on my site and found that
while using the ASP.NET validation controls with a
validation summary the "disabled button" gets stuck and doesn't let the user fix his input and resubmit.
is there a way that i can check client page validation before invoking your function. Something like the Page.IsValid in C#?
Thanks Ilan
See bug report here.
http://webbugtrack.blogspot.com/2007/10/bug-341-button-element-submits-wrong.html
Regardless, who cares about the garbage that IE sends? It's irrelevant, correct? The server does not give a shit about what this new disabled element will generate. As long as the normal submit button makes its way through, everything will get processed perfectly.
Please tell me if I am wrong. Best regards...
My form is AJAX enabled and uses an updatepanel. This way, the window.onload function only actually fires once on the first load. At this point the 'Please wait' functionality works perfectly. Problem is every other time I click the button it doesn't work. I had a quick whizz around t'Internet and found a little message board post suggesting using Sys.Application.add_load() instead of window.onload so that the js runs on every postback from the updatepanel but I can't figure out how to modify your script.
Are you able to help at all?
Many thanks in advance
Tom.
Remove the bottom line from my script: addLoadEvent(ButtonDisabler.LoadEventHandlers);
In your code-behind, try something like:
Dim Script as String = "Sys.Application.add_load(ButtonDisabler.LoadEventHandlers);" ClientScript.RegisterStartupScript(Me.GetType(), "load", Script, True)
Please let me know if that works, and I will update the post accordingly. I also have an update to make to the post regarding validation summaries (much thanks to Ilan).
Well I gave it another couple of tries but to no avail.
I popped the above code into the page_load and the updatepanel_load
code behind but it wouldn't play.
After this failed I also tried Sys . UI . DOMEvent . AddHandler to catch the updatepanel load event and pop your script but this didn't work either and kept telling me that the DOM it was trying to catch was NULL or not yet set... I tried this code in a few different places including the 2 methods above and the click event of the button itself but always the same error.
I am all out of ideas (and time for now) though so it is either get rid of the updatepanel or trust the user (LOL!!!!)
Thanks again anyway.
Tom.
Thanks
Tom.
http://tech.kruelintent.com/player.aspx?vid=1
Thanks
Tom.
Ta
Current script does not work if I have scriptManager tag in my form.
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
Could you send me the updated script for the same.
I have assessment test page once student checks answerchoice and hit on submit button, i was to hide the continue button.
Works great on IE. On fire fox when i hit the continue button it wont let me go to next question same question repeats again.
var inputs = frm.getElementsByTagName('INPUT');To this...
var inputs = new Array();
inputs[0] = document.getElementById('YOUR_BUTTON_ID');You may need to view the source of your rendered page to get the correct button ID. Hope this helps! I'm not sure what your other problem could be in Firefox.
here is the another code i used. I am able to take the test but not able www.codeproject.com/…/OneTimeClickableButton.aspx hide the button but style.visibility property. Thanks alot.
I dont have any text boxes in the form. can I eliminate the following code? How do I break the loop? please let me know.
if(elem.type == 'text' && ButtonDisabler.StopLoopAtFirstTextbox)
break;
Please suggest an alternative way to do so.
An alternative approach to the problem :)
window.onload = function() {
//Your body onload code here
}Thanks for pointing this out, and I am sorry for the delayed response.
Sound fair?
Mark, please let me know if this has resolved your issue.
I have just noticed that the video URL has changed so I have updated it and now the video works just fine.
In a nutshell, it was all about catching the beginning and end of the request using sys instead of just the pageload. It is all explained in the video.
Cheers
Tom
http://tech.kruelintent.com
Dim sb As System.Text.StringBuilder = New System.Text.StringBuilder
'This forces the page validation, if any, to execute.
sb.Append("if (typeof(Page_ClientValidate) == 'function') { ")
sb.Append("if (Page_ClientValidate() == false) { return false; }} ")
'Changes the text of the button. Gives the user a processing message.
sb.Append("this.value = 'Processing...';")
'Prevent the button from being pressed a second time.
sb.Append("this.disabled = true;")
'Forces the page to postback.
sb.Append(Me.ClientScript.GetPostBackEventReference(Me.btnAttachDoc, btnAttachDoc.ID))
sb.Append(";")
Me.btnAttachDoc.Attributes.Add("onclick", sb.ToString())
Cheers Mark
Is there an easy fix for this?
Good job, Thanks for the information
i need some data from a website to display on my program, i could fill in all text to a textbox of that webpage but i cant send to button validate with click event. Inthe source code of the page i found the name and the id of the button, its type is "submitbutton". im newbie please helip, many thanks.
I am trying to use this solution on my jsp page that contains Multiple Forms. Each form have own validation javascript. I have 2 issues:
1. it can't break the loop with following code
if(elem.type == 'text' && ButtonDisabler.StopLoopAtFirstTextbox)
break;
so I used this instead:
if(elem.type != 'submit')
break;
Am I doing it right?
2. The page stuck after the submit and disable the button, it doesn't goes to server side. How should I modify the onsubmit?
frm.onsubmit = function() {
return false;
}
Thanks!!
StopLoopAtFirstTextbox: true,
If your native onsubmit function returns false, then of course the form will not submit to the server-side. Returning false literally cancels the submission.
With this approach call the AddDisableEventToSubmitButtons() function in <body onLoad>. That function finds all the submit elements on the form and adds an onClick event to call the DisableSubmitButtons() method when a button is clicked. When a submit button is clicked, DisableSubmitButtons() iterates through the submit elements and disables all of them EXCEPT for the button that was clicked.
The clicked button doesn't get disabled. Rather, its click event gets sabotaged by making the button return false whenever it is selected. Once that button is clicked, it doesn't matter how long the postback takes because that button's click event is effectively disabled.
<script type="text/javascript" language="javascript">
function AddDisableEventToSubmitButtons() {
// Call this from <body onLoad>
DocForm = document.forms[0];
for (var i=0; i < DocForm.elements.length; i++) {
el = DocForm.elements[i];
if (el.type.toLowerCase() == "submit")
{
// To prevent a clicking spree during slow postbacks.
el.onclick = function() { DisableSubmitButtons(this.id) };
}
}
}
function DisableSubmitButtons(ButtonID) {
DocForm = document.forms[0];
// Decremental loop since submit buttons typically appear at the end of a form.
for (var i = DocForm.elements.length-1; i > -1; i--) {
el = DocForm.elements[i];
if (el.type.toLowerCase() == "submit")
{
if (el.id == ButtonID) {
// Don't disable the submitting button or it will be removed from the HTTP POST header that ASP.Net depends upon for button event detection.
// Instead, sabotage the click event of the submitting button to sap its power.
el.onclick = function() { return false };
} else {
el.disabled = true;
}
}
}
DocForm.submit();
}
</script>
I have an onSubmit handler which performs a form validation that requires a lot of processing time. I want to display a "please wait" message of some sort while the handler runs, but no matter what changes are made to the DOM, the display is not updated until the handler returns. So your script doesn't solve my problem. Let me know if you get any other ideas.
Guess What?
There are a few basic guidelines you should be aware of before leaving a comment…
- If you choose to display your email address, it will not be detected by spam bots
- Comments are limited to 3,000 characters; so far you have used none of them
- HTML will be encoded; links and line breaks will be converted automatically
- Comments containing five or more links will be subject to moderation
