Предотвратить двойной щелчок от двойного запуска команды
Учитывая, что у вас есть элемент управления, который запускает команду:
<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());
}