为什么 TreeViewItem 的 MouseDoubleClick 事件在双击时会被多次触发?

24

XAML

<TreeView Name="GroupView" ItemsSource="{Binding Documents}">
            <TreeView.ItemContainerStyle>
                <Style TargetType="{x:Type TreeViewItem}">
                    <EventSetter Event="MouseDoubleClick" Handler="OnTreeNodeDoubleClick"/>
                </Style>
            </TreeView.ItemContainerStyle>
            ....
</TreeView>

代码后台

private void OnTreeNodeDoubleClick(object sender, MouseButtonEventArgs mouseEvtArgs)
       {
           Console.WriteLine("{3} MouseDoubleClick Clicks={0} ChangedButton={1} Source={2} Handled={4} ButtonState={5}",
               mouseEvtArgs.ClickCount, mouseEvtArgs.ChangedButton, mouseEvtArgs.OriginalSource,
               mouseEvtArgs.Timestamp, mouseEvtArgs.Handled, mouseEvtArgs.ButtonState);
       }

我发现双击一次时,事件处理程序会被调用多次。 我正试图在对应的树节点上双击以在选项卡中打开文档; 因此,我需要过滤掉额外的调用。

23479156 MouseDoubleClick Clicks=1 ChangedButton=Left Source=System.Windows.Controls.TextBlock Handled=False ButtonState=Pressed
23479156 MouseDoubleClick Clicks=1 ChangedButton=Left Source=System.Windows.Controls.TextBlock Handled=False ButtonState=Pressed

在我稍微复杂的应用程序中,每次双击都会触发4次。在一个简单的重现应用程序中,每次双击会触发2次。此外,所有的事件参数都是相同的,所以我无法区分一组中的最后一个。

有任何想法为什么会这样吗?


你是否在UpdatePanel中使用了treeview? - Kangkan
@Kangkan:不是Web应用程序,只是一个简单的桌面应用程序。 - Gishu
1
我曾经遇到过同样的问题,但从未解决。我在树视图上安装了双击事件处理程序(而不是在树视图项上),并只使用了selecteditem属性... - Oskar Duveborn
9个回答

29

我知道这是一个老问题,但是当我在寻找解决方案时遇到了它,所以以下是我为任何未来的访问者发现的结果!

TreeViewItem 递归地包含在彼此之内。 TreeViewItem 是一个 HeaderedContentControl(请参见msdn),并将子节点作为Content。因此,每个 TreeViewItem 的范围包括其所有子项。可以使用出色的 WPF Inspector通过选择视觉树中的TreeViewItem来验证这一点,这将突出显示TreeViewItem的边界。

在 OP 的示例中,使用样式在每个 TreeViewItem 上注册了 MouseDoubleClick 事件。因此,将为您双击的每个TreeViewItem及其父项单独引发事件。可以通过在双击事件处理程序中设置断点并在事件参数的 Source 属性上放置监视器来在调试器中验证这一点-您会注意到每次调用事件处理程序时它都会更改。顺便说一下,可以预期的是,事件的OriginalSource保持不变。

为了对抗这种意外行为,检查源TreeViewItem是否已选择(正如 Pablo 在他的答案中建议的那样)对我最有用。


22

当双击 TreeViewItem 时,该项将被选择作为控件行为的一部分。根据特定情况,可能会出现以下情况:

...
TreeViewItem tviSender = sender as TreeViewItem;

if (tviSender.IsSelected)
    DoAction();
...

9
我已经进行了一些调试,似乎是WPF中的一个bug。大多数已经给出的答案都是正确的,解决方法是检查树形视图项是否被选中。
@ristogod的答案最接近根本问题-它提到第一次调用处理程序时设置e.Handled = true没有预期效果,事件继续向上冒泡,调用父TreeViewItem的处理程序(其中e.Handled再次为false)。
这个bug似乎在WPF的这段代码中: http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Control.cs,5ed30e0aec6a58b2 它接收MouseLeftButtonDown事件(已由子控件处理),但它没有检查e.Handled是否已经设置为true。然后它继续创建一个新的MouseDoubleClick事件args(带有e.Handled == false),并始终调用该事件。
问题在于第一次将事件设置为已处理后,为什么事件仍然会冒泡?因为在这行代码中,当我们注册处理程序 Control.HandleDoubleClick 时: http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Control.cs,40 我们将 true 作为最后一个参数传递给 RegisterClassHandler: http://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/EventManager.cs,161 即 handledEventsToo。
因此,不幸的行为是两个因素的汇合:
1. 对于所有事件(包括已处理的事件),都会调用 Control.HandleDoubleClick。 2. Control.HandleDoubleClick 没有检查事件是否已经被处理。
我将通知 WPF 团队,但我不确定这个 bug 是否值得修复,因为它可能会破坏现有的应用程序(这些应用程序依赖于当前事件处理程序即使已经将 Handled 设置为 true)。

7
private void TreeView_OnItemMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
    if (e.Source is TreeViewItem
        && (e.Source as TreeViewItem).IsSelected)
    {
        // your code
        e.Handled = true;
   }
}

4

这实际上不是一个冒泡问题。我以前见过这种情况。即使你告诉事件已经处理,它仍然会继续向上冒泡。但我认为它实际上并没有冒泡,而是触发了上面节点自己的双击事件。我可能完全错了。但无论如何,重要的是要知道说:

e.handled = true;

无法阻止这种情况的发生。

防止这种行为的一种方法是要注意,当您双击时,您首先进行的是单击,并且所选事件应该首先触发。因此,虽然您无法阻止双击事件的发生,但您应该能够在处理程序内部检查事件逻辑是否应该运行。以下示例利用了这一点:

TreeViewItem selectedNode;

private void MouseDoubleClickEventHandler(object sender, MouseButtonEventArgs e)
{
    if(selectedNode = e.Source)
    {
        //do event logic
    }
}

private void TreeViewSelectedEventHandler(object sender, RoutedEventArgs e)
{
    selectedNode = (TreeViewItem)e.Source;
}

有时候,您可能会遇到一些情况,节点不是通过TreeView SelectedItemChanged事件选择的,而是由其他Bean选择的。在这种情况下,您可以按照以下方式操作。如果您恰好有一个具有单个声明的顶部节点的TreeView,则可以给该节点指定一个特定的名称,然后执行以下操作:

bool TreeViewItemDoubleClickhandled;

private void MouseDoubleClickEventHandler(object sender, MouseButtonEventArgs e)
{
    if (!TreeViewItemDoubleClickhandled)
    {
        //do logic here

        TreeViewItemDoubleClickhandled = true;
    }

    if (e.Source == tviLoadTreeTop)
    {
        TreeViewItemDoubleClickhandled = false;
    }
    e.Handled = true;
}

无论你使用什么方法,重要的是要注意,对于TreeviewItem的双击事件,无论出于何种原因,你都不能阻止树形结构中的事件触发。至少我还没有找到一种方法。


非常准确!将e.Handled设置为true没有预期的效果是WPF中的一个错误。我会添加自己的答案并提供更多细节。 - Kirill Osenkov

2

我有一个比检查选择或创建标志更加优雅的解决方案:

一个辅助方法:

public static object GetParent(this DependencyObject obj, Type expectedType) {

    var parent = VisualTreeHelper.GetParent(obj);
    while (parent != null && parent.GetType() != expectedType)
        parent = VisualTreeHelper.GetParent(parent);

    return parent;
}

然后是你的处理程序:

public void HandleDoubleClick(object sender, MouseButtonEventArgs e)
{
    if (e.OriginalSource is DependencyObject)
        if (sender == (e.OriginalSource as DependencyObject).GetParent(typeof(TreeViewItem))) 
    {
        // sender is the node, which was directly doubleclicked
    }
}

你应该明确指定OriginalSource是由纯命中测试确定的原始报告源,因此如果事件发送者与父TreeViewItem相同,则我们在直接单击的节点上。 - Maxence

0

这个解决方案存在一些相当大的问题,但如果有人需要在多个地方解决这个问题,并且我找到了一个已接受的解决方案不起作用的情况(双击打开弹出窗口的切换按钮,在处理双击的另一个元素内包含切换按钮),那么它可能会起作用。

public class DoubleClickEventHandlingTool

{ private const string DoubleClickEventHandled = "DoubleClickEventHandled";

{ private const string DoubleClickEventHandled = "处理双击事件";

public static void HandleDoubleClickEvent()
{
    Application.Current.Properties[DoubleClickEventHandled] = DateTime.Now.AddSeconds(1);
}

public static bool IsDoubleClickEventHandled()
{
    var doubleClickWasHandled = Application.Current.Properties[DoubleClickEventHandled] as DateTime?;

    return doubleClickWasHandled.HasValue && !IsDateTimeExpired(doubleClickWasHandled.Value);
}

private static bool IsDateTimeExpired(DateTime value)
{
    return value < DateTime.Now;
}

public static void EnableDoubleClickHandling()
{
    Application.Current.Properties[DoubleClickEventHandled] = null;
}

public static bool IsDoubleClickEventHandledAndEnableHandling()
{
    var handled = IsDoubleClickEventHandled();
    EnableDoubleClickHandling();

    return handled;
}

}

在编程中,使用DoubleClickEventHandlingTool.HandleDoubleClickEvent()函数来处理内部/低级元素的双击事件。
    private void OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{if (e.ClickCount == 2) DoubleClickEventHandlingTool.HandleDoubleClickEvent();}

高级双击事件只有在以下情况下才执行其操作:

if (!DoubleClickEventHandlingTool.IsDoubleClickEventHandledAndEnableHandling())

0

这是事件冒泡的美妙世界。事件会沿着您的TreeView节点层次结构向上冒泡,对于路径中的每个节点,都会调用一次处理程序。

只需使用类似以下的内容

        // ...
        if (sender != this)
        {
            return;
        }
        // Your handler code goes here ...
        args.Handled = true;
        // ...

在你的处理程序代码中。


冒泡会导致调用UI元素层次结构中的不同处理程序以处理事件。它不应该导致对每个事件调用相同的处理程序多次。 - Gishu
关于相同的处理程序参数,还有一件事情。无论您订阅哪个事件,发送者始终是TreeView。真正的发送者隐藏在RoutedEvent.OriginalSource参数中。根据事件,有时它是TreeViewItem,但对于鼠标事件,它是用户单击的TreeViewItem(分层)DataTemplate中定义的控件类型。可能微软没有覆盖FrameworkElement实现。 - banzai

-1
最有可能的原因是双击处理程序被安装了多次,因此每个处理程序实例都会被调用一次。

@John - 在所有文件中查找了一下,唯一提到OnTreeNodeDoubleClick的地方是在样式定义中。 - Gishu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接