Hank
Hank

Reputation: 2616

How to stop DataGridRow from selecting if mouse is clicked inside RowDetails

I have a DataGrid, in each DataGridRow I have row details, which hold a couple of controls.

What I'd like is, if anything is clicked inside the row details to:
- Not select the row, or more accurately,
- Change the existing DataGrid selection.

I was thinking along the lines of handling PreviewMouseDown and MouseDown events in a behavior, somehow making the DataGrid skip over the selection process, however not sure how to proceed.

Eventually I'm going to have a TabControl in the details with more info as well, so I also don't want a TabItem click to change the DataGrid's existing selection.

Would there be a way to start the tunnelling of PreviewMouseDown at the Grid "DetailsContainer" level and stop the bubbling of MouseDown at the Grid "DetailsContainer" level

<DataGrid Name="dgAudit"
          CanUserReorderColumns="False"
          CanUserAddRows="False"
          CanUserDeleteRows="False"
          CanUserResizeColumns="False"
          CanUserResizeRows="False"
          CanUserSortColumns="False"
          IsReadOnly="True"
          ItemsSource="{Binding GEOM_ASSET_OC_LIST}"
          VirtualizingPanel.ScrollUnit="Pixel"
          RowDetailsVisibilityMode="Visible"
          >
    <i:Interaction.Behaviors>
        <behaviors:DataGridBehaviors />
    </i:Interaction.Behaviors>

    <DataGrid.Columns>
        <DataGridTextColumn Header="Asset ID" Binding="{Binding ASSET_ID}" Width="200" />
        <DataGridTextColumn Header="Asset Type" Binding="{Binding ASSET_TYPE}" Width="200" />
        <DataGridTextColumn Header="Last Update By" Binding="{Binding LAST_UPDATE_BY}" Width="150" />
        <DataGridTextColumn Header="Last Update Date" Binding="{Binding LAST_UPDATE_DATETIME, StringFormat=\{0:dd.MM.yy HH:mm:ss tt\}}" Width="150" />
    </DataGrid.Columns>
    <DataGrid.RowDetailsTemplate>
        <DataTemplate>
            <Grid Name="DetailsContainer">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <TextBlock Name Text="{Binding Notes}" Width="400" HorizontalAlignment="Left" TextWrapping="Wrap"/>
                <Button Content="Button" Grid.Column="1" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
            </Grid>
        </DataTemplate>
    </DataGrid.RowDetailsTemplate>
</DataGrid>

Just a quick mock up of an empty behavior

public class DataGridBehaviors : Behavior<DataGrid>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        this.AssociatedObject.MouseDown += DataGrid_MouseDown;
        this.AssociatedObject.PreviewMouseDown += DataGrid_PreviewMouseDown;
    }

    protected override void OnDetaching()
    {
        this.AssociatedObject.PreviewMouseDown -= DataGrid_PreviewMouseDown;
        this.AssociatedObject.MouseDown -= DataGrid_MouseDown;
        base.OnDetaching();
    }

    private void DataGrid_MouseDown(object sender, MouseButtonEventArgs e)
    {
    }

    private void DataGrid_PreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
        DependencyObject obj = (DependencyObject)e.OriginalSource;
        DataGridDetailsPresenter RowsDetails = FindParent<DataGridDetailsPresenter>(obj);
        if (RowsDetails != null)
        {
            //Skip over selection, maybe temporarily removed native selection handler???
        }
    }

    public static T FindParent<T>(DependencyObject child) where T : DependencyObject
    {
        //get parent item
        DependencyObject parentObject = VisualTreeHelper.GetParent(child);

        //we've reached the end of the tree
        if (parentObject == null) return null;

        //check if the parent matches the type we're looking for
        T parent = parentObject as T;
        if (parent != null)
            return parent;
        else
            return FindParent<T>(parentObject);
    }

    private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
    {
        T child = default(T);

        int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < numVisuals; i++)
        {
            Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
            child = v as T;
            if (child == null)
            {
                child = GetVisualChild<T>(v);
            }
            if (child != null)
            {
                break;
            }
        }
        return child;
    }
}

Upvotes: 3

Views: 1991

Answers (2)

Hank
Hank

Reputation: 2616

I found another possible work-around. By creating a small Canvas inside the DataGridRowHeader then in the next child container setting ClipsToBounds to false. It is then free of the header but also can be clicked on without affecting the DataGrids current selection. It is bound to the IsExpanded of the row details and looks the part.

One word of warning is that if the row details contain a significant amount of data, with the code as it is below, may be quite inefficient, loading time and scroll delay. Depending on if virtualization is being used. This data will load at the same time as the row and may be quite inefficient, where as when the controls are in the RowDetailsTemplate they load on demand. Probably best to strictly control the loading of this data through a User Control or dynamically.

Also you'll probably need to listen to the DataGrid LayoutUpdated event to adjust the width of the detail control.

<DataGrid.RowHeaderTemplate>
    <DataTemplate>
        <Grid>
            <Expander Template="{StaticResource StretchyExpanderTemp}"
                      OverridesDefaultStyle="True"
                      Header=""
                      HorizontalAlignment="Right"
                      VerticalAlignment="Top"
                      Expanded="Expander_Expanded" Collapsed="Expander_Collapsed"
                      IsExpanded="{Binding DataContext.IsExpanded, RelativeSource={RelativeSource AncestorType=DataGridRowHeader}}" />

            <Canvas Background="BlueViolet" 
                    Width="5"
                    VerticalAlignment="Top"
                    HorizontalAlignment="Left" 
                    Height="5">
                <TabControl Name="tcAA" 
                            TabStripPlacement="Left" 
                            Margin="20,18,0,0" 
                            Height="185"
                            Width="500"
                            VerticalAlignment="Top"
                            HorizontalAlignment="Left"
                            ItemsSource="{Binding DataContext.AAUDIT, RelativeSource={RelativeSource AncestorType=DataGridRowHeader}}" 
                            SelectedIndex="0" 
                            ClipToBounds="False"
                            Visibility="{Binding DataContext.IsExpanded, RelativeSource={RelativeSource AncestorType=DataGridRowHeader}, Converter={StaticResource bool2VisibilityConverter}}"
                            >
                    ...
                    <TabControl.ItemTemplate>
                        <DataTemplate>
                            <Grid>
                                <TextBlock Text="{Binding DISPLAY_NAME}" />
                            </Grid>
                        </DataTemplate>
                    </TabControl.ItemTemplate>
                    <TabControl.ContentTemplate>
                        <DataTemplate>
                            ...
                        </DataTemplate>
                    </TabControl.ContentTemplate>
                </TabControl>
            </Canvas>
        </Grid>
    </DataTemplate>
</DataGrid.RowHeaderTemplate>

<DataGrid.RowDetailsTemplate>
    <DataTemplate>
        <Grid Height="185" >
        </Grid>
    </DataTemplate>
</DataGrid.RowDetailsTemplate>

As it stands here's a quick example of what I've produced: enter image description here

Upvotes: 1

dymanoid
dymanoid

Reputation: 15197

Unfortunately, with the current WPF DataGrid's implementation, it is not possible to achieve what you want.

The DataGridDetailsPresenter registers a class event handler for the MouseLeftButtonDownEvent routed event using the EventManager API:

EventManager.RegisterClassHandler(
    typeof(DataGridDetailsPresenter),
    MouseLeftButtonDownEvent,
    new MouseButtonEventHandler(OnAnyMouseLeftButtonDownThunk),
    true);

Note that last parameter which is set to true. It is a flag indicating whether or not the listener wants to hear about events that have already been handled. In this case, even if you set the RoutedEventArgs.Handled property to true at any level, the internal DataGridDetailsPresenter's event handler will be called anyway.

This event handler is responsible for focusing the DataRow you click the details view within. And you surely know, that in WPF, the class event handlers are called prior to the instance event handlers. So the first thing will happen on a left click on a details row's presenter is the focusing of the containing row (actually, of that row's first cell).

Please also note that this behavior manages the realizing of the virtual items inside the RowPresenter too, so disabling it might lead to unwanted GUI side-effects.

You could set the IsHitTestVisible property of your container Grid to false, this will disable the automatic row selection behavior. But obviously, you won't be able to handle any clicks inside the row details at all.

Upvotes: 3

Related Questions