navigation
 Tuesday, November 06, 2007

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.

First Steps

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!

Supporting Validation

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:

  1. Override AddAttributesToRender. This is where Button adds the onclick attribute to the HtmlTextWriter that will ultimately write the control to the output stream. Unfortunately, no amount of tweaking the Button's properties before invoking the base method will allow script to be appended to the end of the onclick handler after the ASP.NET postback script, and after the base method has been called, the script has been input into the HtmlTextWriter, where it may not be modified or even retrieved.
  2. Replace AddAttributesToRender. This approach could work; I could copy the disassembled code from the Button class and paste it into my subclass with the modifications I want, but this offends my delicate sensibilities. :) It's inelegant, the resulting code won't be very human-readable, and it won't inherit any changes in ASP.NET that might occur as the result of a patch or service pack.
  3. Next, I considered writing my own HtmlTextWriter that would detect when an onclick attribute was added, and inject the code I wanted there. ASP.NET does allow this kind of custom rendering, but unfortunately, it's intended as a way to adapt ASP.NET output to different devices and browsers, and it affects all content, rather than one specific control, so this approach would be incompatible with adaptive rendering for alternative devices.
  4. Lastly, I looked at creating a WebControlAdapter, which is specific to a particular control, but unfortunately, it did not offer control that was fine-grained enough. I needed to touch code smack dab in the middle of a 60-line method that rendered other attributes as well, but WebControlAdapters only offer control at the method level.

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.

The Solution

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)

Tuesday, November 06, 2007 10:53:09 PM UTC
Awesome blog! I will be using this technique a lot.
John Waters
Thursday, November 08, 2007 11:30:39 PM UTC
P.S. If you add your own client script to the button, it's important to realize that using the return statement will cause the button not to generate a postback, even if you return true. If you want to return values from the event, assign the return value to event.returnValue instead.
Adam Anderson
Saturday, November 17, 2007 4:53:13 PM UTC
Update: fixed missing opening angle bracket in the ToolboxData attribute
Adam Anderson
Wednesday, November 28, 2007 9:13:47 PM UTC
You almost got it!

event.srcElement doesn't work on firefox, so I changed the code to:
function SubmitButton_Click(e) {
var src = (event) ? event.srcElement : e.target;

Also, if UseSubmitBehaviour is false and Javascript is disabled on the browser then the form won't submit, because the input type is button not submit...
Thursday, November 29, 2007 3:38:59 AM UTC
Try this code at the end of the page.

if (theForm) { var oldonsubmit = theForm.onsubmit; if (typeof oldonsubmit != 'function') {
theForm.onsubmit = theForm_submit; } else { theForm.onsubmit = function(e) { if (oldonsubmit) {
var retVal = oldonsubmit(); if (!retVal) return false; } theForm_submit((e ? e : window.event)); return true; }; } }

Write inside a function called 'theForm_submit(e)' the code you wish to use. I recommend instead of disabling the button just hiding it and displaying a 'Please wait..' message.
Tuesday, December 04, 2007 12:22:53 AM UTC
I am a new to this and i do need this function.
Please add the actual web page with a button on it and the link to your code in your download so I can see the finished product. I dont know how to actualy get this to work.
thanks
mikekapl
Tuesday, December 04, 2007 5:55:55 PM UTC
Maxtoroq: Thanks for the Firefox support.
Mikekapl: I've uploaded a new zip file containing an example of how to use the button in a page.
Adam Anderson
Wednesday, December 05, 2007 9:20:47 PM UTC
About the correction I made, you should qualify the event property, or still get problems in Firefox.

var src = (window.event) ? window.event.srcElement : e.target;
Thursday, December 06, 2007 4:35:33 PM UTC
Maxtoroq: OK, thanks.

BTW, the syntax (undefined-element) ? undefined-element : alternate-value doesn't work in IE when undefined-element is undefined; you get a script error. That's why I've stuck with using typeof to test existence.
Adam Anderson
Thursday, December 13, 2007 5:40:12 PM UTC
When I use the submitbutton within a multiview on the second view the startupscript doesn't execute. It seems that, after a postback, the startup-script isn't registered correctly.

I switch from view 1 to view 2 after a postback.

My multiview is wrapped with an updatepanel..

Am I the only one with this problem, or does it also exists with somebody else???

Any help is appreciated.

thanks in advance.

Bas
Bas
Monday, January 07, 2008 3:04:51 AM UTC
this is exactly what i am looking for.

one question: becase UseSubmitBehavior is set to false, that the browser no longer recognises a default button. This means that you can't use the enter key to submit the form e.g. on a login page, type username, password, hit enter and you're in.

i have tried setting the default button on the form, and it does submit the form when the enter key is pressed, but does not post the value of the control that has focus e.g. using my login example: type username, type password, hit enter and the username field posts back, but not the password field.

any ideas on how to use the SubmitButton but maintain the default button functionality?
Peter
Wednesday, January 09, 2008 8:37:46 PM UTC
I still can't get my page working in Firefox with a post back that was fired by a OnSelectedIndexChanged (or OnTextChange) in a drop down...

I've searched every where for something to disable a submit button after it's been clicked. I found many samples and this one seemed real promising so I tried it. I've even written my own code that does basically the same thing as this (in a different manner, though).

So, good work Adam, but you're not finished, yet. That is, if you want to solve a problem that a lot more developers are running in to.

Why can't they just make the button a one click event within the browser code? Save us all a lot of headache.
Chris
Wednesday, January 09, 2008 11:03:47 PM UTC
Peter:

If you're using ASP.NET 2.0+, try putting the SubmitButton in a Panel and set the Panel's DefaultButton property to the SubmitButton.

Chris:

I'm not clear on what your problem is. If you're firing a postback from other JavaScript, the button won't disable itself; that's not what it was designed to do. You could try calling the button's click method in JavaScript; that should fire the onclick event, both disabling the button and performing the postback.
Thursday, January 10, 2008 4:37:35 PM UTC
I figured it out and posting here because I'm sure someone else will need this fix. I beleive it is a .NET bug. What was happening was my drop down list was triggering the validation and within the validation .NET code there is a JavaScript error that causes Firefox to break. This only occurs if you have the AutoPostBack="true" in your asp:DropDownList.

This javascript is needed in order to not break the page in Firefox:

function DisableValidators()
{
Page_ValidationActive = false;
}
function EnableValidators()
{
Page_ValidationActive = true;
}

Add some attributes to trigger the JS above

YOURDROPDOWNID.Attributes.Add("onchange", "DisableValidators();");
YOURSUBMITBUTTONID.Attributes.Add("onfocus", "EnableValidators();");

Now to disable the submit button when clicked. Much cleaner and easier.

YOURSUBMITBUTTONID.Attributes.Add("onclick", " this.disabled = true; this.value = 'Processing...'; " +
ClientScript.GetPostBackEventReference(YOURSUBMITBUTTONID, null) + ";");
Chris
Tuesday, January 15, 2008 10:50:45 PM UTC
Holy crap, this is the best blog entry in the history of the internet. Thank you, thank you, a thousand times thank you for this code! It's so freakin' easy to implement, and the functionality is more useful than a machine that prints money.
Jason
Wednesday, January 30, 2008 3:11:11 PM UTC
I'm an idiot, so please can I have an idiot's guide to using this?

I've looked at the sample Default.aspx, copied the Register line into my own page, and added SubmitButton.cs and .js into the project.

But, I get Element 'SubmitButton' is not a known element...

What else do I have to do? Do I need to build it into a separate assembly first?

Thanks!
Simon
Thursday, February 28, 2008 7:03:06 PM UTC
I don't know if anyone else has found this problem but this button doesn't work if it's inside of an UpdatePanel. I have script that fixes this but haven't figured out how to get the script embedded in the button code.

Anyone who knows more about this want to try this out?

var pbControl = null;
var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_beginRequest(BeginRequestHandler);
prm.add_endRequest(EndRequestHandler);

function BeginRequestHandler(sender, args) {
pbControl = args.get_postBackElement(); //the control causing the postback
pbControl.disabled = true;
}

function EndRequestHandler(sender, args) {
pbControl.disabled = false;
pbControl = null;
}
Aaron Prohaska
Wednesday, March 26, 2008 2:00:52 PM UTC
Really this is what iam looking for a long but..

I get Element 'SubmitButton' is not a known element...

What should i do to make it work....

i think some thing is missing.. in the zip file

Thanks in advance! Please Help!!!!

Aboobacker
Aboobacker
Wednesday, April 30, 2008 12:56:52 PM UTC
how would I be able to change the text on the button to say something like 'processing...please wait' after the user clicks on the button? and I would like the text to revert afterwards.
Daren
Monday, June 30, 2008 7:38:17 PM UTC
Wouldn't it be a little easier to just put any javascript code you want to run when a button is clicked in an asp custom validator's client script? Just set a window timeout to check if the client side validation succeeded after about half a second. If it succeeded, it means the page is being posted and the button should be disabled.
Karcirate
Name
E-mail
Home page

Comment (Some html is allowed: a@href@title, i, strike, u) where the @ means "attribute." For example, you can use <a href="" title=""> or <blockquote cite="Scott">.  

Enter the code shown (prevents robots):

Live Comment Preview