Muds
Muds

Reputation: 4116

How do i stop drag select on wpf data grid

Hi all this might sound trivial but I want to stop drag select on WPF DataGrid

I have a simple grid like

<DataGrid ItemsSource="{Binding Items}" SelectionMode="Extended">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding .}"/>
            </DataGrid.Columns>
</DataGrid>

how can I stop multiple selection on click drag but and have multiple selection by Shift and Ctrl.

thanks

Upvotes: 2

Views: 2611

Answers (4)

Vlad i Slav
Vlad i Slav

Reputation: 61

That works for me:

public partial class DataGridView : DataGrid
{
    // ...
    
    protected override void OnPreviewMouseMove(MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed && SelectionMode == DataGridSelectionMode.Extended)
        {
            // Do some stuff which you need for dragging
        }

        base.OnPreviewMouseMove(e);
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        //base.OnMouseMove(e);
    }
    
    // ...
}

Upvotes: 0

Paul Skibitzke
Paul Skibitzke

Reputation: 96

Mikolaytis's answer is incomplete. With that solution, clicking an unselected row selects all rows between it and the first selected row above it, for as long as the mouse button is down. This is a side effect of DataGrid._isDraggingSelection still being true, which is evaluated in other mouse-event-driven operations.

Il Vic's answer, while I did not try it, is far more complex than necessary.

Instead, in the override for DataGrid.OnMouseMove() (as was done in Mikolaytis's answer), call private method DataGrid.EndDragging(), via reflection:

public class MyDataGrid : DataGrid
{
    private static readonly FieldInfo s_isDraggingSelectionField = 
        typeof(DataGrid).GetField("_isDraggingSelection", BindingFlags.Instance | BindingFlags.NonPublic);

    private static readonly MethodInfo s_endDraggingMethod =
        typeof(DataGrid).GetMethod("EndDragging", BindingFlags.Instance | BindingFlags.NonPublic);

    // DataGrid.OnMouseMove() serves no other purpose than to execute click-drag-selection.
    // Bypass that, and stop 'is dragging selection' mode for DataGrid
    protected override void OnMouseMove(MouseEventArgs e)
    {
        if ((bool)(s_isDraggingSelectionField?.GetValue(this) ?? false))
            s_endDraggingMethod.Invoke(this, new object[0]);
    }
}

Put simply, after drag-selection is started, the next mouse move event ends it.

Upvotes: 4

Il Vic
Il Vic

Reputation: 5666

The DataGrid control is not designed for customizing the selection gestures, anyway some hacks can be performed to reach your target. First of all we need an helper class:

public static class ReflectionHelper
{
    public static T GetPropertyValue<T>(object owner, string propertyName) where T : class
    {
        Type ownerType = owner.GetType();
        PropertyInfo propertyInfo = ownerType.GetProperty(propertyName,
            BindingFlags.Instance | BindingFlags.NonPublic);

        while (propertyInfo == null)
        {
            ownerType = ownerType.BaseType;
            propertyInfo = ownerType.GetProperty(propertyName,
                BindingFlags.Instance | BindingFlags.NonPublic);
        }

        return propertyInfo.GetValue(owner, null) as T;
    }

    public static void Execute(object owner, string methodName, params object[] parameters)
    {
        Type ownerType = owner.GetType();
        MethodInfo methodInfo = ownerType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);

        while (methodInfo == null)
        {
            ownerType = ownerType.BaseType;
            methodInfo = ownerType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);
        }
        methodInfo.Invoke(owner, parameters);
    }
}

I do not like it, but reflection is needed. First of all we need to create our own DataGridRowHeader in order to change its OnClick code:

public class DataGridRowHeader : System.Windows.Controls.Primitives.DataGridRowHeader
{
    protected override void OnClick()
    {
        if (Mouse.Captured == this)
        {
            base.ReleaseMouseCapture();
        }
        DataGrid dataGridOwner = ReflectionHelper.GetPropertyValue<DataGrid>(this, "DataGridOwner");
        DataGridRow parentRow = ReflectionHelper.GetPropertyValue<DataGridRow>(this, "ParentRow");

        if (dataGridOwner != null && parentRow != null)
        {
            ReflectionHelper.Execute(dataGridOwner, "HandleSelectionForRowHeaderAndDetailsInput", parentRow, false);
        }
    }        
}

All we want to do is to pass false (instead of true) as the second parameter of the method HandleSelectionForRowHeaderAndDetailsInput.

We need to handle the DataGrid's cells by creating our own DataGridCell:

public class DataGridCell : System.Windows.Controls.DataGridCell
{
    static DataGridCell()
    {
        EventManager.RegisterClassHandler(typeof(DataGridCell), 
            UIElement.MouseLeftButtonDownEvent, 
            new MouseButtonEventHandler(DataGridCell.OnAnyMouseLeftButtonDownThunk), true);
    }

    private static void OnAnyMouseLeftButtonDownThunk(object sender, MouseButtonEventArgs e)
    {
        ((DataGridCell)sender).OnAnyMouseLeftButtonDown(e);
    }

    private void OnAnyMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        bool isKeyboardFocusWithin = base.IsKeyboardFocusWithin;
        bool flag = (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;
        DataGrid dataGridOwner = ReflectionHelper.GetPropertyValue<DataGrid>(this, "DataGridOwner");
        if (isKeyboardFocusWithin && !flag && !e.Handled && this.IsSelected)
        {
            if (dataGridOwner != null)
            {
                ReflectionHelper.Execute(dataGridOwner, "HandleSelectionForCellInput",
                    this, false, true, false);

                if (!this.IsEditing && !this.IsReadOnly)
                {
                    dataGridOwner.BeginEdit(e);
                    e.Handled = true;
                    return;
                }
            }
        }
        else if (!isKeyboardFocusWithin || !this.IsSelected || flag)
        {
            if (!isKeyboardFocusWithin)
            {
                base.Focus();
            }
            if (dataGridOwner != null)
            {
                ReflectionHelper.Execute(dataGridOwner, "HandleSelectionForCellInput",
                    this, Mouse.Captured == null && flag, true, false);
            }
            e.Handled = true;
        }
    }
}

A simple DataGridCellsPresenter is also needed:

public class DataGridCellsPresenter : System.Windows.Controls.Primitives.DataGridCellsPresenter
{
    protected override DependencyObject GetContainerForItemOverride()
    {
        return new DataGridCell(); /* the one in our namespace */
    }
}

It will say to the DataGrid to use our DataGridCell. Now, of course, we have to create a default style (it should be placed in the Window resources) for forcing the DataGrid to use our stuff:

<Style x:Key="{x:Type DataGridRow}" TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource {x:Type DataGridRow}}">
    <Setter Property="Control.Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type DataGridRow}">
                <Border Name="DGR_Border" Background="{TemplateBinding Control.Background}" BorderBrush="{TemplateBinding Control.BorderBrush}" BorderThickness="{TemplateBinding Control.BorderThickness}" SnapsToDevicePixels="True">
                    <SelectiveScrollingGrid>
                        <SelectiveScrollingGrid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition Width="*" />
                        </SelectiveScrollingGrid.ColumnDefinitions>
                        <SelectiveScrollingGrid.RowDefinitions>
                            <RowDefinition Height="*" />
                            <RowDefinition Height="Auto" />
                        </SelectiveScrollingGrid.RowDefinitions>
                        <DataGridCellsPresenter Grid.Column="1" ItemsPanel="{TemplateBinding DataGridRow.ItemsPanel}" SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
                        <local:DataGridDetailsPresenter Grid.Column="1" Grid.Row="1" Visibility="{TemplateBinding DataGridRow.DetailsVisibility}" SelectiveScrollingGrid.SelectiveScrollingOrientation="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=AreRowDetailsFrozen, Converter={x:Static DataGrid.RowDetailsScrollingConverter}, ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical}}" />
                        <local:DataGridRowHeader SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical" Grid.RowSpan="2" Visibility="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=HeadersVisibility, Converter={x:Static DataGrid.HeadersVisibilityConverter}, ConverterParameter={x:Static DataGridHeadersVisibility.Row}}" />
                    </SelectiveScrollingGrid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

I hope it can help.

Upvotes: 0

Mikolaytis
Mikolaytis

Reputation: 962

Try that hack: create a class that inherits DataGrid and override OnMouseMove without calling base.OnMouseMove

Upvotes: 1

Related Questions