Search code examples
c#wpfdatagridwpf-controls

Weird event behavior in a Popup inside a DataGridColumnHeader


I'm not using any external libraries here, just plain WPF.

I have a DataGrid with a custom DataGridColumnHeader. This column header contains a ToggleButton to toggle a Popup. Inside the popup there is a TextBox. The problem I'm having is that doubleclicking inside the TextBox raises the MouseDoubleClick event on the DataGrid. Here's a simplified version containing numbered comments I will refer to afterwards

<Window x:Class="PopupsAreWeird.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:PopupsAreWeird" xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <Style TargetType="DataGridColumnHeader">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="DataGridColumnHeader">
                        <!-- 1) This eventhandler is never called -->
                        <Grid Control.MouseDoubleClick="Grid_MouseDoubleClick_1">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="Auto" />
                            </Grid.ColumnDefinitions>

                            <ContentPresenter Grid.Column="0" />
                            <ToggleButton x:Name="openToggle" Grid.Column="1" Content="Open" />

                            <Popup IsOpen="{Binding ElementName=openToggle, Path=IsChecked}" StaysOpen="True">
                                <!-- 2) This eventhandler is always called, and the problem I am having is there regardless of whether I set e.Handled = true in this handler or not -->
                                <TextBox Width="200" MouseDoubleClick="TextBox_MouseDoubleClick" />
                            </Popup>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <StackPanel Orientation="Vertical">
        <!-- 3) This eventhandler is always called, but never should be -->
        <DataGrid MouseDoubleClick="DataGrid_MouseDoubleClick">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Header 1" Width="200" />
            </DataGrid.Columns>
        </DataGrid>
    </StackPanel>
</Window>

1) I do not understand why this handler is never called. This was meant to set e.Handled to true to stop bubbling upwards to the DataGrid. While Grid does not have a definition for MouseDoubleClick, it is my understanding that I can attach eventhandlers for any event to any element (like attaching ButtonBase.Click to a Panel element). Is this not true, or is there any special case here I am missing?

3) I want to avoid the double click event bubbling to here, but even if I set e.Handled to true in eventhandler 2, this eventhandler is called, and in that handler, e.Handled is false. I assume the reason is that the Popup is defined inside of DataGridColumnHeader, and for some weird reason 2 events are being raised, one for the tree of the popup, and one for the tree that contains the Popup element, but that seems a bit nonsensical.

I know Popup is a sort of weird thing in WPF, but this seems like I'm missing something obvious. Is there any way to achieve what I want, i.e. not have events (or at least the MouseDoubleClick event) bubble up to the DataGrid?

Thanks in advance, David


Solution

  • 1) You are right, You can attach a handler for a routed event to any UIElement. But indeed the Popup is a special case. The Popup is a special control that behaves like Window. It can popup everywhere on the screen, always rendered top-most and is not necessarily bound to the application itself. That's why its visual tree is detached from the application's visual tree. Popup.Child will be a separate isolated visual tree. Microsoft Docs: Popup and the Visual Tree.
    Since routed events traverse the visual tree to be handled by any node, it makes sense that bubbling/tunneling routed events inside the Popup will stop/start at the root of this isolated tree. So routed events that are originated in the Popup cannot be handled outside the Popup.

    3) Short version: Control.MouseDoubleClick (and the preview version) is a special event that behaves different. This event is raised on each UIElement on the route when the event traverses the visual tree. So setting Handled to true has no effect.
    To solve your problem, you should either handle UIElement.PreviewMousLeftButtonDown and check if the MouseButtonEventArgs.ClickCount equals 2 to detect a double click and then set Handled = true
    or check if the type of the sender or RoutedEventArgs.Source is not TextBox before handling it (explicit event filtering).

    "Although this routed event seems to follow a bubbling route through an element tree, it actually is a direct routed event that is raised along the element tree by each UIElement. If you set the Handled property to true in a MouseDoubleClick event handler, subsequent MouseDoubleClick events along the route will occur with Handled set to false. This is a higher-level event for control consumers who want to be notified when the user double-clicks the control and to handle the event in an application.

    Control authors who want to handle mouse double clicks should use the MouseLeftButtonDown event when ClickCount is equal to two. This will cause the state of Handled to propagate appropriately in the case where another element in the element tree handles the event.

    The Control class defines the PreviewMouseDoubleClick and MouseDoubleClick events, but not corresponding single-click events. To see if the user has clicked the control once, handle the MouseDown event (or one of its counterparts) and check whether the ClickCount property value is 1."
    Microsoft Docs: Control.MouseDoubleClick