18

Given that you have a control that fires a command:

<Button Command="New"/>

Is there a way to prevent the command from being fired twice if the user double clicks on the command?

EDIT: What is significant in this case is that I am using the Commanding model in WPF.

It appears that whenever the button is pressed, the command is executed. I do not see any way of preventing this besides disabling or hiding the button.

Josh G
  • 14,068
  • 7
  • 62
  • 74
  • There doesn't appear to be anyway to adjust/override WPF's internal handling of the click event? – Josh G May 08 '09 at 19:27
  • CAUTION: Many of these answers prevent the click handler from executing twice *at the same time*, but do NOT prevent it from executing two times *in quick succession*. You can test each, by implementing a click handler that does nothing (except use the mechanism from the answer you choose). Plus print a debug line to console. Then try two quick clicks in a row. Several times, with a slightly different delay each time. To see how UI handling code actually behaves, do this manually, on your running UI. NOT a code test like https://stackoverflow.com/a/16407514/199364. – ToolmakerSteve Oct 24 '21 at 04:31

16 Answers16

18

It is possible that any event handler that contains code that requires a significant process time, can result in a delay to the disabling of the button at question; regardless to where the disabling line of code is called within the handler.

Try the proofs below and you will see that disable/enable has no correlation to the registration of events. The button click event is still registered and is still handled.

Proof by Contradiction 1

private int _count = 0;
    
private void btnStart_Click(object sender, EventArgs e)
{
    btnStart.Enabled = false;
    
    _count++;
    label1.Text = _count.ToString();

    while (_count < 10)
    {            
        btnStart_Click(sender, e);            
    }           

    btnStart.Enabled = true;
}

Proof by Contradition 2

private void form1_load(object sender, EventArgs e)
{
    btnTest.Enabled = false;
}

private void btnStart_Click(object sender, EventArgs e)
{
    btnTest.Enabled = false;
    btnTest_click(sender, e);
    btnTest_click(sender, e);
    btnTest_click(sender, e);
    btnTest.Enabled = true;
}

private int _count = 0;

private void btnTest_click(object sender, EventArgs e)
{
    _count++;
    label1.Text = _count.ToString();
}
LarsTech
  • 80,625
  • 14
  • 153
  • 225
JDennis
  • 672
  • 6
  • 15
  • 1
    The OP is using MVVM (binding) and Commands for WPF. You're using the WinForms "events" approach which doesn't apply here. – vidalsasoon Aug 28 '14 at 12:51
  • 3
    @vidalsasoon You are correct that I used Winforms as a demonstration. They key point, however, is that event registration is isolated from other controls/buttons enable or disable property and disabling a button will not prevent an subscribed event handler from executing code. This is true for WPF and Winforms. I was simply trying to point out that the checked answer on this post is not correct. No offense intended. I have made the mistake myself. – JDennis Aug 28 '14 at 16:45
  • 1
    @sotn True. My sole intent was to shed some light on the situation here. Still to this day, the checked answer on this post is incorrect. I was simply trying to illuminate that problem. – JDennis Jun 19 '18 at 23:22
  • I believe the only solution to this problem is a command queue which processes commands sequentially in order and not asynchronously. it can then make decisions such as seeing the same command twice in a row and ignoring it. – rollsch Apr 07 '21 at 05:21
  • This code is not a valid test. Reason: it is written in a way that does not allow events to occur after `btnTest.Enabled = false;` line executes. THAT is the reason that the button does not appear to be disabled. To fix this, add `Application.DoEvents();` after each line that accesses btnTest. OTOH, it is possible that the symptom discussed is likewise caused by a handler that believes it has disabled button, but has the same flaw -- doesn't `DoEvents`, so the disabling has not yet occurred. – ToolmakerSteve Oct 24 '21 at 03:48
15

Simple & Effective for blocking double, triple, and quadruple clicks

<Button PreviewMouseDown="Button_PreviewMouseDown"/>

private void Button_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
    if (e.ClickCount >= 2)
    {
        e.Handled = true;
    }
}
Jon Dosmann
  • 667
  • 7
  • 20
14

I had the same issue and this worked for me:

<Button>
    <Button.InputBindings>
            <MouseBinding Gesture="LeftClick" Command="New" />
    </Button.InputBindings>
</Button>
Olek
  • 676
  • 7
  • 11
7

We solved it like this... with async we couldn't find any other way to effectively block extra clicks on the button which invokes this Click:

private SemaphoreSlim _lockMoveButton = new SemaphoreSlim(1);
private async void btnMove_Click(object sender, RoutedEventArgs e)
{
    var button = sender as Button;
    if (_lockMoveButton.Wait(0) && button != null)
    {
        try
        {                    
            button.IsEnabled = false;
        }
        finally
        {
            _lockMoveButton.Release();
            button.IsEnabled = true;
        }
    }
}
John Fairbanks
  • 1,342
  • 12
  • 16
7

Assuming that WPF Commanding doesn't give you enough control to mess with the click handler, could you put some code in the command handler that remembers the last time the command was executed and exits if it is requested within a given time period? (code example below)

The idea is that if it's a double-click, you'll receive the event twice within milliseconds, so ignore the second event.

Something like: (inside of the Command)


// warning:  I haven't tried compiling this, but it should be pretty close
DateTime LastInvoked = DateTime.MinDate;
Timespan InvokeDelay = Timespan.FromMilliseconds(100);
{
  if(DateTime.Now - LastInvoked <= InvokeDelay)
     return;

  // do your work
}

(note: if it were just a plain old click handler, I'd say follow this advice: http://blogs.msdn.com/oldnewthing/archive/2009/04/29/9574643.aspx )

JMarsch
  • 21,484
  • 15
  • 77
  • 125
  • 1
    Good link. The debouncing technique that your refer to is precisely what I am talking about. Turns out our WPF program misperforms sometimes if you double click on buttons that were intended to be singly clicked. – Josh G May 08 '09 at 20:42
5

You can use the EventToCommand class in the MVVMLightToolkit to prevent this.

Handle the Click event and send it through EventToCommand from your view to your viewmodel (you can use EventTrigger to do this).
Set MustToggleIsEnabled="True" in your view and implement a CanExecute() method in your viewmodel.
Set CanExecute() to return false when the command starts to execute and back to true when the command is done.

This will disable the button for the duration of processing the command.

stombeur
  • 2,704
  • 22
  • 45
  • 2
    The meat of what you are saying: If the button is bound to a command, disable the command (CanExecute() = false) until the command is done processing. – Josh G Mar 23 '11 at 13:59
  • If processing the command is quick enough, this will NOT prevent a second click from happening - if that click happens after the command has already restored CanExecute to `true`. – ToolmakerSteve Oct 24 '21 at 04:16
5

You'd think that it would be as simple as using a Command and making CanExecute() return false while the command is running. You would be wrong. Even if you raise CanExecuteChanged explicitly:

public class TestCommand : ICommand
{
    public void Execute(object parameter)
    {
        _CanExecute = false;
        OnCanExecuteChanged();
        Thread.Sleep(1000);
        Console.WriteLine("Executed TestCommand.");
        _CanExecute = true;
        OnCanExecuteChanged();
    }

    private bool _CanExecute = true;

    public bool CanExecute(object parameter)
    {
        return _CanExecute;
    }

    private void OnCanExecuteChanged()
    {
        EventHandler h = CanExecuteChanged;
        if (h != null)
        {
            h(this, EventArgs.Empty);
        }
    }

    public event EventHandler CanExecuteChanged;
}

I suspect that if this command had a reference to the window's Dispatcher, and used Invoke when it called OnCanExecuteChanged, it would work.

I can think of a couple of ways to solve this problem. One's JMarsch's approach: simply track when Execute is called, and bail out without doing anything if it was called in the last few hundred milliseconds.

A more robust way might be to have the Execute method start a BackgroundWorker to do the actual processing, have CanExecute return (!BackgroundWorker.IsBusy), and raise CanExecuteChanged in when the task is complete. The button should requery CanExecute() as soon as Execute() returns, which it'll do instantly.

Robert Rossney
  • 94,622
  • 24
  • 146
  • 218
4

A simple and elegant solution is to create a Behavior disabling reaction on second click in double-click scenario. That's pretty easy to use:

  <Button Command="New">
          <i:Interaction.Behaviors>
            <behaviors:DisableDoubleClickBehavior />
          </i:Interaction.Behaviors>
  </Button>

Behavior (more about behaviors - https://www.jayway.com/2013/03/20/behaviors-in-wpf-introduction/)

using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;

public class DisableDoubleClickBehavior : Behavior<Button>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.PreviewMouseDoubleClick += AssociatedObjectOnPreviewMouseDoubleClick;
    }

    private void AssociatedObjectOnPreviewMouseDoubleClick(object sender, MouseButtonEventArgs mouseButtonEventArgs)
    {
        mouseButtonEventArgs.Handled = true;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.PreviewMouseDoubleClick -= AssociatedObjectOnPreviewMouseDoubleClick;
        base.OnDetaching();
    }
}
mikka
  • 146
  • 5
4

You could set a flag

bool boolClicked = false;
button_OnClick
{
    if(!boolClicked)
    {
        boolClicked = true;
        //do something
        boolClicked = false;
    }
}
Nathan Koop
  • 24,803
  • 25
  • 90
  • 125
  • 3
    I would add a Try{ boolClicked = true; DoSomething(); } Finally{ boolClicked = false;} to make sure to reset the flag. Cheers. – Sylvain Rodrigue Dec 03 '13 at 11:53
  • @nathan koop didn't try to test your code on real Xamarin Forms app. – Nuri YILMAZ Jul 28 '17 at 13:18
  • The handler could technically enter twice before boolClicked is set as checking and setting the instruction is not atomic. – rollsch Apr 07 '21 at 05:16
  • @rollsch - I've never seen that happen. In any of the different platforms I've used this technique on. For good reason: OnClick handlers RUN ON MAIN THREAD. There is only one such thread involved. (This is a single object, protecting itself. It is associated with a single context.) Don't need protection against multi-threaded access. Is there a theoretical possibility that thread handler could interrupt for a higher probability thread, then on return NOT continue with the already running UI task? Possible, but would be a poor system design. – ToolmakerSteve Oct 24 '21 at 04:01
  • This solution ensures that two clicks don't attempt to process at the same time; but it does NOT ensure that two clicks don't happen in quick succession. If the second click is not processed until this is finished, it won't stop that second click. See solutions that explicitly "debounce" the click events. Such as https://stackoverflow.com/a/841668/199364. – ToolmakerSteve Oct 24 '21 at 04:17
1

Had the same problem, solved it by using attached behavior.

namespace VLEva.Core.Controls
{
    /// <summary></summary>
    public static class ButtonBehavior
    {
        /// <summary></summary>
        public static readonly DependencyProperty IgnoreDoubleClickProperty = DependencyProperty.RegisterAttached("IgnoreDoubleClick",
                                                                                                                  typeof(bool),
                                                                                                                  typeof(ButtonBehavior),
                                                                                                                  new UIPropertyMetadata(false, OnIgnoreDoubleClickChanged));

        /// <summary></summary>
        public static bool GetIgnoreDoubleClick(Button p_btnButton)
        {
            return (bool)p_btnButton.GetValue(IgnoreDoubleClickProperty);
        }

        /// <summary></summary>
        public static void SetIgnoreDoubleClick(Button p_btnButton, bool value)
        {
            p_btnButton.SetValue(IgnoreDoubleClickProperty, value);
        }

        static void OnIgnoreDoubleClickChanged(DependencyObject p_doDependencyObject, DependencyPropertyChangedEventArgs e)
        {
            Button btnButton = p_doDependencyObject as Button;
            if (btnButton == null)
                return;

            if (e.NewValue is bool == false)
                return;

            if ((bool)e.NewValue)
                btnButton.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(btnButton_PreviewMouseLeftButtonDown);
            else
                btnButton.PreviewMouseLeftButtonDown -= btnButton_PreviewMouseLeftButtonDown;
        }

        static void btnButton_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (e.ClickCount >= 2)
                e.Handled = true;
        }

    }
}

and then just set the property to TRUE either directly in XAML by declaring a style so it can affects all your buttons at once. (don't forget the XAML namespace declaration)

<Style x:Key="styleBoutonPuff" TargetType="{x:Type Button}">
    <Setter Property="VLEvaControls:ButtonBehavior.IgnoreDoubleClick" Value="True" />
    <Setter Property="Cursor" Value="Hand" />
</Style>
Sam L.
  • 207
  • 2
  • 3
1

I'm using Xamarin and MVVMCross, although is not WPF I think the following solution applies, I created a solution which is viewmodel specific (doesn't deal with platform specific UI) which I think is very handy, using a helper or base class for the viewmodel create a List that keeps track of the commands, something like this:

private readonly List<string> Commands = new List<string>();

        public bool IsCommandRunning(string command)
        {
            return Commands.Any(c => c == command);
        }

        public void StartCommand(string command)
        {
            if (!Commands.Any(c => c == command)) Commands.Add(command);
        }

        public void FinishCommand(string command)
        {
            if (Commands.Any(c => c == command))  Commands.Remove(command);
        }

        public void RemoveAllCommands()
        {
            Commands.Clear();
        }

Add the command in the action like this:

public IMvxCommand MyCommand
        {
            get
            {
                return new MvxCommand(async() =>
                {
                    var command = nameof(MyCommand);
                    if (IsCommandRunning(command)) return;

                    try
                    {
                        StartCommand(command);

                        await Task.Delay(3000);
                       //click the button several times while delay
                    }
                    finally
                    {
                        FinishCommand(command);
                    }
                });
            }
        }

The try/finally just ensures the command is always finished.

Tested it by setting the action async and doing a delay, first tap works second one returns in the condition.

JpSiller
  • 73
  • 7
  • The is the only solution that would work properly in all scenarios. – rollsch Apr 07 '21 at 05:20
  • 2
    @rollsch - it only works because the long delay ensures that a second click gets "swallowed", before `FinishCommand` is called. Change that delay to `await Task.Delay(1);`, and you will see that if you click twice at the correct speed, both clicks get through. Therefore, this is not a "general" solution to the problem. Any general solution will include a "debounce time" - either in app code or via a system call that performs such a debounce. For example, https://stackoverflow.com/a/841668/199364. – ToolmakerSteve Oct 24 '21 at 04:20
  • I'm not convinced (but I haven't tested to be 100%). My thoughts are StartCommand is run on the UI thread, so you can be guaranteed that Commands will contain the command before the UI can enter the function again. The UI thread will only be able to process further clicks during the 3000ms (or 1ms) window. If IsCommandRunning executed on a thread other than the UI Thread, then yes I would agree with you however it should run on the UI thread effectively blocking (not swallowing) the UI clicks until it thread switches (during the await task.delay). – rollsch Oct 25 '21 at 01:12
  • When you say "both clicks get through" I was assuming you meant execute in parallel/simultaneously. If you mean they both execute (but one after the other) then yes I agree. I thought the purpose of this question was to avoid the code executing in parallel, however if it is purely to prevent double clicks then you are correct, it would not do that with a short delay. You could however simply add the Task.Delay to create the debounce thoug. Eg time how long your actual code takes to execute, then delay for the remainder of the debounce time. – rollsch Oct 25 '21 at 01:16
0

Wrap the code in a try-catch-finally or try-finally block. The finally statement will always be called regardless of any error occurring in the try.

Example

    private Cursor _CursorType;
    // Property to set and get the cursor type
    public Cursor CursorType
    {
      get {return _CursorType; }
      set
      {
        _CursorType = value;
        OnPropertyChanged("CursorType");
      }
    }


    private void ExecutedMethodOnButtonPress()
    {
       try
       {
         CursorType = Cursors.Wait;
         // Run all other code here
       }
       finally
       {
         CursorType = Cursors.Arrow;
       }
    }

NOTE: the CursorType is a property that the UserControl or Window is bound to

<Window 
Cursor = {Binding Path=CursorType}>
javacoder123
  • 193
  • 3
  • 17
0

The only real solution here is to create a CommandHandler singleton class which uses a ConcurrentQueue of commands. The command handler would need it's own processing loop that is started after the first button press and ended when the queue is empty, this would need to run in it's own thread.

Each click handler then pushes the command to this queue which then executes the command. If the same command appears twice in the queue you can simply ignore processing it (or do something else)

Everything else in this question I have seen will not work as they use non atomic operations to check if the button has been pressed twice in quick succession. This can fail as you can get double entrance before the boolean/timer/semaphore has been set.

rollsch
  • 2,518
  • 4
  • 39
  • 65
0

I have my button tied to a delegate function which can fire Run():

private const int BUTTON_EVENT_DELAY_MS = 1000; //1 second. Setting this time too quick may allow double and triple clicking if the user is quick.
private bool runIsRunning = false;

private void Run()
{
    try
    {
        if (runIsRunning) //Prevent Double and Triple Clicking etc. We just want to Run run once until its done!
        {
            return;
        }
        runIsRunning = true;

        EventAggregator.GetEvent<MyMsgEvent>().Publish("my string");

        Thread.Sleep(BUTTON_EVENT_DELAY_MS);  
        runIsRunning = false;
    }
    catch  //catch all to reset runIsRunning- this should never happen.
    {
        runIsRunning = false;
    }
}
John Foll
  • 121
  • 1
  • 10
0

If your control derives from System.Windows.Forms.Control, you can use the double click event.

If it doesn't derive from System.Windows.Forms.Control, then wire up mousedown instead and confirm the click count == 2 :

private void Button_MouseDown(object sender, MouseButtonEventArgs e)
{
    if (e.ClickCount == 2)
    {
       //Do stuff
    }
 }
Noel Kennedy
  • 12,128
  • 3
  • 40
  • 57
  • This is a WPF control... It derives from System.Windows.Controls.Control – Josh G May 08 '09 at 19:26
  • Then you can use this: http://msdn.microsoft.com/en-us/library/system.windows.input.mousebuttoneventargs.clickcount.aspx Or have I missunderstood? – Noel Kennedy May 08 '09 at 19:42
  • The goal was not to handle the double click event; my intention is to suppress it. – Josh G Dec 16 '09 at 14:48
-3

This checks if validation has passed and if it does then disables the button.

private void checkButtonDoubleClick(Button button)
    {
        System.Text.StringBuilder sbValid = new System.Text.StringBuilder();
        sbValid.Append("if (typeof(Page_ClientValidate) == 'function') { ");
        sbValid.Append("if (Page_ClientValidate() == false) { return false; }} ");
        sbValid.Append("this.value = 'Please wait...';");
        sbValid.Append("this.disabled = true;");
        sbValid.Append(this.Page.ClientScript.GetPostBackEventReference(button, ""));
        sbValid.Append(";");
        button.Attributes.Add("onclick", sbValid.ToString());
    }
ta4ka
  • 92
  • 6
  • Wow. Not sure what's going on here, but looks like you are assuming ASP.NET or something. There is no Page, no ClientScript, and no post back. – Josh G Dec 16 '09 at 14:50
  • Yes, wrong place to post this. This is for standard asp.net, not wpf. – ta4ka Dec 31 '09 at 09:55