Предотвратить двойной щелчок от двойного запуска команды

Учитывая, что у вас есть элемент управления, который запускает команду:

<Button Command="New"/>

Есть ли способ предотвратить двойной запуск команды, если пользователь дважды щелкнет по команде?

РЕДАКТИРОВАТЬ: Что важно в этом случае, это то, что я использую модель командования в WPF.

Похоже, что при каждом нажатии кнопки команда выполняется. Я не вижу способа предотвратить это, кроме отключения или скрытия кнопки.

17 ответов

Решение

Возможно, кнопка должна быть отключена после первого нажатия и до тех пор, пока обработка не будет завершена?

Проверенный ответ на этот вопрос, представленный vidalsasoon, является неправильным и неправильным для всех различных способов, которыми задавался этот же вопрос.

Возможно, что любой обработчик событий, содержащий код, требующий значительного времени обработки, может привести к задержке отключения рассматриваемой кнопки; независимо от того, где отключающая строка кода вызывается в обработчике.

Попробуйте приведенные ниже доказательства, и вы увидите, что отключение / включение не имеет отношения к регистрации событий. Событие нажатия кнопки все еще зарегистрировано и все еще обрабатывается.

Доказательство от противоречия 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;

}

Доказательство по условию 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();
}

Простой и эффективный для блокировки двойных, тройных и четырехкратных кликов

<Button PreviewMouseDown="Button_PreviewMouseDown"/>

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

У меня была та же проблема, и это сработало для меня:

<Button>
    <Button.InputBindings>
            <MouseBinding Gesture="LeftClick" Command="New" />
    </Button.InputBindings>
</Button>

Мы решили это следующим образом... с помощью async мы не смогли найти другого способа эффективно блокировать дополнительные щелчки на кнопке, которая вызывает этот 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;
        }
    }
}

Предполагая, что WPF Commanding не дает вам достаточного контроля, чтобы связываться с обработчиком щелчков, не могли бы вы поместить некоторый код в обработчик команд, который запоминает последний раз, когда команда была выполнена, и завершается, если она запрашивается в течение заданного периода времени? (пример кода ниже)

Идея состоит в том, что если это двойной щелчок, вы получите событие дважды в течение миллисекунд, поэтому игнорируйте второе событие.

Что-то вроде: (внутри команды)


// 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
}

(примечание: если бы это был просто старый обработчик кликов, я бы сказал, что следуйте этому совету: http://blogs.msdn.com/oldnewthing/archive/2009/04/29/9574643.aspx)

Вы думаете, что это будет так же просто, как с помощью Command и делая CanExecute() вернуть false во время выполнения команды. Ты был бы неправ. Даже если вы поднимете CanExecuteChanged в явном виде:

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;
}

Я подозреваю, что если эта команда имела ссылку на окно Dispatcherи использовал Invoke когда это называется OnCanExecuteChanged, это будет работать.

Я могу придумать пару способов решить эту проблему. Подход JMarsch: просто отслеживать, когда Execute называется, и выручить, ничего не делая, если это было вызвано в последние несколько сотен миллисекунд.

Более надежным способом может быть Execute метод начать BackgroundWorker чтобы сделать фактическую обработку, есть CanExecute вернуть (!BackgroundWorker.IsBusy)и поднять CanExecuteChanged когда задача завершена. Кнопка должна запросить CanExecute() как только Execute() возвращается, что он сделает мгновенно.

Вы можете использовать EventToCommand класс в MVVMLightToolkit, чтобы предотвратить это.

Обработайте событие Click и отправьте его через EventToCommand от вашего взгляда до вашего viewmodel (вы можете использовать EventTrigger сделать это).
Задавать MustToggleIsEnabled="True" на ваш взгляд и реализовать CanExecute() Метод в вашей модели представления.
Задавать CanExecute() возвращать значение false, когда команда начинает выполняться, и возвращать значение true, когда команда выполнена.

Это отключит кнопку на время обработки команды.

Простое и элегантное решение - создать реакцию отключения поведения при втором щелчке в сценарии двойного щелчка. Это довольно легко использовать:

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

Поведение (подробнее о поведении - 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();
    }
}

Вы можете установить флаг

bool boolClicked = false;
button_OnClick
{
    if(!boolClicked)
    {
        boolClicked = true;
        //do something
        boolClicked = false;
    }
}

Была та же проблема, решена с помощью прикрепленного поведения.

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;
        }

    }
}

а затем просто установите для свойства значение TRUE либо непосредственно в XAML, объявив стиль, чтобы оно могло влиять на все ваши кнопки одновременно. (не забудьте объявление пространства имен XAML)

<Style x:Key="styleBoutonPuff" TargetType="{x:Type Button}">
    <Setter Property="VLEvaControls:ButtonBehavior.IgnoreDoubleClick" Value="True" />
    <Setter Property="Cursor" Value="Hand" />
</Style>

Я использую Xamarin и MVVMCross, хотя это не WPF. Я думаю, что применимо следующее решение, я создал решение, которое зависит от модели представления (не имеет отношения к пользовательскому интерфейсу платформы), которое я считаю очень удобным, используя вспомогательный или базовый класс. для модели представления создайте список, который отслеживает команды, что-то вроде этого:

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();
        }

Добавьте команду в действии следующим образом:

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);
                    }
                });
            }
        }

Try/finally просто гарантирует, что команда всегда завершена.

Протестировал это, установив асинхронное действие и сделав задержку, первое нажатие работает, второе возвращается в состояние.

Оберните код в блок try-catch-finally или try-finally. Оператор finally всегда вызывается независимо от ошибки, возникающей при попытке.

пример

    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;
       }
    }

ПРИМЕЧАНИЕ. CursorType - это свойство, с которым связан UserControl или Window.

<Window 
Cursor = {Binding Path=CursorType}>

Моя кнопка привязана к функции делегата, которая может запускать 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;
    }
}

Единственное реальное решение здесь - создать одноэлементный класс CommandHandler, который использует ConcurrentQueue команд. Обработчику команд потребуется собственный цикл обработки, который запускается после первого нажатия кнопки и завершается, когда очередь пуста, это нужно будет запускать в собственном потоке.

Затем каждый обработчик кликов помещает команду в эту очередь, которая затем выполняет команду. Если одна и та же команда появляется в очереди дважды, вы можете просто проигнорировать ее обработку (или сделать что-то еще).

Все остальное в этом вопросе, которое я видел, не будет работать, поскольку они используют неатомарные операции, чтобы проверить, была ли кнопка нажата дважды в быстрой последовательности. Это может потерпеть неудачу, поскольку вы можете получить двойной вход до того, как будут установлены логическое значение / таймер / семафор.

Если ваш элемент управления происходит от System.Windows.Forms.Control, вы можете использовать событие двойного щелчка.

Если это не происходит от System.Windows.Forms.Control, вместо этого подключите mousedown и подтвердите количество кликов == 2:

private void Button_MouseDown(object sender, MouseButtonEventArgs e)
{
    if (e.ClickCount == 2)
    {
       //Do stuff
    }
 }

Это проверяет, прошла ли валидация, а затем отключает кнопку.

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());
    }
Другие вопросы по тегам