Have you ever seen one of those web pages that's about to charge your credit card and practically begs you not to click the "submit" button more than once? Even assuming that every user of the page actually reads the text (and many don't), many users have a habit of double-clicking everything, because that's how it works on their desktop. The only reliable way to prevent submitting a web form multiple times is to make it impossible. I recently came upon a situation where I needed to prevent this form happening in one of my web applications, so I decided to create a custom button that would encapsulate this behavior, so that it could be easily shared and re-used amongst the team.
The basic idea is simple enough: when the button is clicked, disable it, and then let ASP.NET post the page. I created a subclass of Button and began hacking. My first attempt looked very much like this:
protected override void OnPreRender( EventArgs e ) { OnClientClick = "this.disabled = true;" + OnClientClick; base.OnPreRender( e ); }
This accomplished disabling the button, but now the button didn't cause a postback! A Google search indicated that I should set UseSubmitBehavior to false. When UseSubmitBehavior is true (the default), ASP.NET uses the browser's built-in submission behavior, and when the property is false, ASP.NET renders client script to perform the postback instead. I guess that this means that Internet Explorer won't submit a form if the source button is disabled. At any rate, this means that my button will need to have a default of false for UseSubmitBehavior. I also decided to disallow changing the value, since the button just won't work otherwise. My code now looked something like this:
public class SubmitButton : Button { public SubmitButton() : base() { base.UseSubmitBehavior = false; } protected override void OnPreRender( EventArgs e ) { OnClientClick = "this.disabled = true;" + OnClientClick; base.OnPreRender( e ); } [DefaultValue( false )] public override bool UseSubmitBehavior { set { } } }
This is all well and good, but what if there is validation on the page, and the validation fails? Now you're stuck with a disabled submit button and no way to post the page!
My next attempt was to create a script for OnClientClick that would duplicate everything that ASP.NET does when it performs a postback. Fortunately, this is not too difficult thanks to methods of ClientScriptManager:
protected override void OnPreRender( EventArgs e ) { OnClientClick = CreateClientClick(); base.OnPreRender( e ); } private string CreateClientClick() { if ( Page == null ) throw new Exception( "Page is null. SubmitButton can't create scripts." ); string postback = Page.ClientScript.GetPostBackEventReference( GetPostBackOptions() ); return "this.disabled = true;" + OnClientClick + postback + "this.disabled = Page_IsValid;"; }
This works pretty well, except that it doesn't prevent ASP.NET from rendering yet another postback call and appending it to the end of all of this. That means validation runs twice, and depending on the browser's script engine, the postback might occur twice!
What we really need is to disable the button, let ASP.NET do all of its stuff including validation, and then re-enable the button if validation failed. This means that we need to be able to have client script that runs before and after whatever client script ASP.NET renders, and that's where things begin to get hairy, because ASP.NET assumes that its postback will be the last thing to get rendered, and it doesn't make it easy at all to change that. Here is a quick summary of the things I tried to get ASP.NET to let me have control over how it renders the onclick attribute:
At this point, I had to give up on trying to control the way that ASP.NET renders the button. ASP.NET just wasn't designed to give that kind of control. Finally, I turned to my last option: solving the problem using JavaScript.
I'm no expert at JavaScript, I know that it's a very flexible and powerful language, and it can execute without interference from ASP.NET's assumptions about what I might want to do in a custom control's lifecycle, so I have high hopes. First I wrote the function that I wanted to execute when the button was clicked:
function SubmitButton_Click() { var src = event.srcElement; src.disabled = true; src.aspnet_onclick(); src.disabled = Page_IsValid; }
Whoops! If there are no validators on the page, Page_IsValid will be undefined, so that last line really needs to go like this:
src.disabled = typeof( Page_IsValid ) != "undefined" ? Page_IsValid : true;
I know that you can add new properties to an element simply by assigning them, so my plan was to use JavaScript to capture the onclick handler that ASP.NET rendered and assign it to a new property called aspnet_onclick, then surround the call to that with my own code. Now I needed to create the function that would accomplish the swap:
function SubmitButton_InitOnClick( id ) { var sb = document.getElementById( id ); sb.aspnet_onclick = sb.onclick; sb.onclick = SubmitButton_Click; }
I saved these scripts in a separate .js file. Now all I need to do is to get my custom button to use the scripts:
protected override void OnPreRender( EventArgs e ) { RegisterClientScript(); RegisterStartupScript(); base.OnPreRender( e ); } private void RegisterClientScript() { if ( Page == null ) throw new Exception( "Page is null. SubmitButton can't register scripts." ); string key = "SubmitButton"; string url = ResolveClientUrl( "SubmitButton.js" ); if ( !Page.ClientScript.IsClientScriptIncludeRegistered( key ) ) Page.ClientScript.RegisterClientScriptInclude( key, url ); } private void RegisterStartupScript() { if ( Page == null ) throw new Exception( "Page is null. SubmitButton can't register scripts." ); string key = UniqueID + "_Startup"; string script = String.Format( "SubmitButton_InitOnClick( '{0}' );", UniqueID ); if ( !Page.ClientScript.IsStartupScriptRegistered( key ) ) Page.ClientScript.RegisterStartupScript( typeof( Page ), key, script, true ); }
The resulting output does exactly what we want: disables the button, performs whatever action the button would have done if it was an ordinary ASP.NET button, including whether the button causes validation, and using the button's validation group, and then finally conditionally re-enables the button.
I've attached the complete source code to this blog post. As far as I know, this is the best single-click button available on the web. Enjoy, and never write "Please only click the button once" ever again!
SubmitButton12.zip (2 KB)
Remember Me
a@href@title, i, strike, u
Copyright © 2003-2008 Falafel Software Inc.
Subscribe to Falafel Blogs
The opinions expressed herein are Falafel's employees own personal opinions and do not represent Falafel Software's view in any way in case they go bananas!