【问题标题】:Prevent double-click from double firing a command防止双击双重触发命令
【发布时间】:2010-10-24 21:20:01
【问题描述】:

假设您有一个可以触发命令的控件:

<Button Command="New"/>

如果用户双击命令,有没有办法防止命令被触发两次?

编辑:在这种情况下重要的是我在 WPF 中使用Commanding 模型。

似乎只要按下按钮,就会执行命令。除了禁用或隐藏按钮之外,我没有看到任何防止这种情况的方法。

【问题讨论】:

  • 似乎没有调整/覆盖 WPF 对点击事件的内部处理?
  • 注意:其中许多答案会阻止点击处理程序同时执行两次,但不要阻止它快速连续执行两次我>。您可以通过实现一个不执行任何操作的单击处理程序来测试每个处理程序(使用您选择的答案中的机制除外)。另外打印一条调试行到控制台。然后尝试连续两次快速单击。几次,每次的延迟略有不同。要查看 UI 处理代码的实际行为,请在正在运行的 UI 上手动执行此操作。不是像stackoverflow.com/a/16407514/199364 这样的代码测试。

标签: c# .net wpf double-click


【解决方案1】:

任何包含需要大量处理时间的代码的事件处理程序都可能导致相关按钮的禁用延迟;无论在处理程序中的何处调用禁用代码行。

尝试下面的证明,您会发现禁用/启用与事件注册无关。按钮点击事件仍被注册并仍在处理中。

矛盾证明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();
}

【讨论】:

  • OP 正在使用 MVVM(绑定)和 WPF 命令。您正在使用此处不适用的 WinForms“事件”方法。
  • @vidalsasoon 你是对的,我使用 Winforms 作为演示。然而,它们的关键点是事件注册与其他控件/按钮的启用或禁用属性隔离,并且禁用按钮不会阻止订阅的事件处理程序执行代码。这适用于 WPF 和 Winforms。我只是想指出这篇文章中的检查答案不正确。无意冒犯。我自己犯了错误。
  • @sotn 是的。我的唯一目的是阐明这里的情况。直到今天,这个帖子上的检查答案还是不正确的。我只是想说明这个问题。
  • 我相信这个问题的唯一解决方案是命令队列,它按顺序而不是异步处理命令。然后它可以做出决定,例如连续两次看到相同的命令并忽略它。
  • 此代码不是有效的测试。原因:它的编写方式不允许在btnTest.Enabled = false; 行执行后发生事件。这就是按钮似乎没有被禁用的原因。要解决此问题,请在访问 btnTest 的每一行之后添加 Application.DoEvents();。 OTOH,所讨论的症状也可能是由认为它已禁用按钮但具有相同缺陷的处理程序引起的 - 不是DoEvents,因此尚未发生禁用。
【解决方案2】:

简单而有效地阻止两次、三次和四次点击

<Button PreviewMouseDown="Button_PreviewMouseDown"/>

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

【讨论】:

    【解决方案3】:

    我遇到了同样的问题,这对我有用:

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

    【讨论】:

    • 哇。如果这行得通,那就太棒了。有时间我一定要试试。
    • 我可以确认这对我也有效,谢谢!
    • 说真的,你是怎么找到这个的?
    • MouseGesture 检查 ClickCount
    • 但是行为发生了变化:该命令在MouseDown 上触发,而不是在MouseUp 上触发。所以你没有经常点击。
    【解决方案4】:

    假设 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)

    【讨论】:

    • 良好的链接。您所指的去抖动技术正是我所说的。如果您双击原本打算单独单击的按钮,我们的 WPF 程序有时会出现错误。
    【解决方案5】:

    我们是这样解决的……使用异步我们找不到任何其他方法来有效阻止对调用此 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;
            }
        }
    }
    

    【讨论】:

    • 在尝试了本次讨论和其他讨论中提到的所有方法后,这是唯一对我的 MVVM 项目有效的方法
    【解决方案6】:

    您可以使用MVVMLightToolkit 中的EventToCommand 类来防止这种情况。

    处理 Click 事件并将其通过 EventToCommand 从您的视图发送到您的视图模型(您可以使用 EventTrigger 来执行此操作)。
    在您的视图中设置MustToggleIsEnabled="True" 并在您的视图模型中实现CanExecute() 方法。
    CanExecute() 设置为在命令开始执行时返回false,并在命令完成时返回true。

    这将在处理命令期间禁用按钮。

    【讨论】:

    • 你说的重点:如果按钮绑定到命令,禁用命令(CanExecute() = false)直到命令处理完毕。
    • 如果处理命令足够快,这不会阻止第二次点击的发生 - 如果在命令已经将 CanExecute 恢复到 true 之后发生第二次点击。
    【解决方案7】:

    您会认为它就像使用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,并且在调用OnCanExecuteChanged 时使用了Invoke,它会起作用。

    我可以想出几种方法来解决这个问题。一个 JMarsch 的方法:只需跟踪何时调用 Execute,如果在最后几百毫秒内调用它,则无需执行任何操作即可退出。

    更强大的方法可能是让Execute 方法启动BackgroundWorker 来进行实际处理,让CanExecute 返回(!BackgroundWorker.IsBusy),并在任务完成时引发CanExecuteChanged。该按钮应在Execute() 返回后立即重新查询CanExecute(),它会立即执行此操作。

    【讨论】:

    • 我认为您在测试时没有尝试多次点击(足够快)。它不起作用...
    【解决方案8】:

    你可以设置一个标志

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

    【讨论】:

    • 我会添加一个 Try{ boolClicked = true;做一点事(); } 最后{ boolClicked = false;} 确保重置标志。干杯。
    • @nathan koop 没有尝试在真正的 Xamarin Forms 应用程序上测试您的代码。
    • 在 boolClicked 设置为检查和设置指令不是原子之前,从技术上讲,处理程序可以进入两次。
    • @rollsch - 我从未见过这种情况。在我使用过这种技术的任何不同平台上。有充分的理由:OnClick 处理程序在主线程上运行。只涉及一个这样的线程。 (这是一个保护自身的单个对象。它与单个上下文相关联。)不需要针对多线程访问的保护。线程处理程序是否有理论上的可能性可以中断更高概率的线程,然后返回不继续已经运行的 UI 任务?可能,但系统设计不佳。
    • 这个方案保证了两次点击不会同时尝试处理;但它不能确保不会快速连续发生两次点击。如果在完成之前未处理第二次点击,则不会停止第二次点击。请参阅明确“消除”点击事件的解决方案。如stackoverflow.com/a/841668/199364
    【解决方案9】:

    一个简单而优雅的解决方案是在双击场景中创建第二次单击时的行为禁用反应。这很容易使用:

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

    【讨论】:

      【解决方案10】:

      遇到了同样的问题,通过附加行为解决了。

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

      然后直接在 XAML 中通过声明样式将属性设置为 TRUE,这样它就可以立即影响所有按钮。 (不要忘记 XAML 命名空间声明)

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

      【讨论】:

      • 显然 ClickCount 是不可靠的,并不总是正确的。
      【解决方案11】:

      我正在使用 Xamarin 和 MVVMCross,虽然不是 WPF,但我认为以下解决方案适用,我创建了一个特定于视图模型的解决方案(不处理特定于平台的 UI),我认为这非常方便,使用帮助器或视图模型的基类创建一个跟踪命令的列表,如下所示:

      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 只是确保命令始终完成。

      通过设置动作异步和延迟进行测试,第一次点击有效,第二次在条件下返回。

      【讨论】:

      • 这是唯一适用于所有场景的解决方案。
      • @rollsch - 它之所以有效,是因为长时间的延迟确保在调用 FinishCommand 之前第二次点击被“吞下”。将延迟更改为await Task.Delay(1);,您会看到,如果您以正确的速度单击两次,两次单击都会通过。因此,这不是问题的“通用”解决方案。任何通用解决方案都将包括“去抖时间”——在应用程序代码中或通过执行这种去抖的系统调用。例如,stackoverflow.com/a/841668/199364
      • 我不相信(但我还没有测试到 100%)。我的想法是 StartCommand 在 UI 线程上运行,因此您可以保证在 UI 可以再次进入函数之前 Commands 将包含该命令。 UI 线程将只能在 3000 毫秒(或 1 毫秒)窗口期间处理进一步的点击。如果 IsCommandRunning 在 UI 线程以外的线程上执行,那么是的,我同意你的观点,但是它应该在 UI 线程上运行,有效地阻止(而不是吞下)UI 点击,直到它线程切换(在 await task.delay 期间)。
      • 当您说“两次点击都通过”时,我假设您的意思是并行/同时执行。如果您的意思是他们都执行(但一个接一个),那么是的,我同意。我认为这个问题的目的是避免代码并行执行,但是如果纯粹是为了防止双击,那么你是对的,它不会在很短的延迟内做到这一点。但是,您可以简单地添加 Task.Delay 来创建 debounce thoug。例如,您的实际代码执行需要多长时间,然后延迟剩余的去抖动时间。
      【解决方案12】:

      如果您的控件派生自 System.Windows.Forms.Control,则可以使用double click event

      如果它不是从 System.Windows.Forms.Control 派生的,则改为 wire up mousedown 并确认点击次数 == 2:

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

      【讨论】:

      • 这是一个 WPF 控件...它派生自 System.Windows.Controls.Control
      • 那你可以用这个:msdn.microsoft.com/en-us/library/… 还是我误会了?
      • 目标不是处理双击事件;我的意图是压制它。
      【解决方案13】:

      将代码包装在 try-catch-finally 或 try-finally 块中。无论 try 中是否发生任何错误,都会始终调用 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}>
      

      【讨论】:

      • 这不是问题的答案,它是关于防止两次快速点击执行两次点击处理程序。
      【解决方案14】:

      这里唯一真正的解决方案是创建一个使用 ConcurrentQueue 命令的 CommandHandler 单例类。命令处理程序需要它自己的处理循环,该循环在第一次按下按钮后启动并在队列为空时结束,这需要在它自己的线程中运行。

      然后每个点击处理程序将命令推送到该队列,然后该队列执行该命令。如果同一命令在队列中出现两次,您可以简单地忽略处理它(或执行其他操作)

      我看到的这个问题中的其他所有内容都不起作用,因为他们使用非原子操作来检查按钮是否已连续快速按下两次。这可能会失败,因为您可以在设置布尔值/计时器/信号量之前获得双重入口。

      【讨论】:

        【解决方案15】:

        我的按钮绑定到一个可以触发 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;
            }
        }
        

        【讨论】:

          【解决方案16】:

          这会检查验证是否通过,如果通过则禁用按钮。

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

          【讨论】:

          • 哇。不确定这里发生了什么,但看起来你正在假设 ASP.NET 或其他东西。没有 Page,没有 ClientScript,也没有回发。
          • 是的,发错地方了。这适用于标准的 asp.net,而不是 wpf。
          猜你喜欢
          • 1970-01-01
          • 2015-06-11
          • 1970-01-01
          • 2019-02-05
          • 2011-09-11
          • 1970-01-01
          • 2011-10-30
          • 2012-06-09
          • 2016-12-07
          相关资源
          最近更新 更多