4
votes

I have a WPF DataGrid control with a SelectionUnit of "FullRow" and SelectionMode of "Extended" that I'm programmatically selecting an item in (the first item, usually). The selection works, but for some reason any form of programmatic selection seems to break the shift-select multiselect ability.

If I single click another item in the DataGrid (so the item I just clicked is the only item selected), then shift-select will work. It only seems to break if I've programmatically selected the item. Additionally, control-click works to select multiple items in either case -- it seems to only be shift-select that is broken.

I've tried various forms of programmatically selecting the single item, from as simple as myGrid.SelectedIndex = 0, to using the DataGrid's ItemContainerGenerator to get an instance of the DataGridRow object and setting IsSelected = true on it, but to no avail.

To re-iterate -- programmatic selection of an item works, but it breaks shift-click selection.

Has anyone run into this before? I've tried setting focus on the DataGridRow instance that is programmatically selected, but it doesn't seem to help?

4
Looks like a bug in the control to me. It feels like something to do with SelectedItem vs. SelectedItems but setting SelectedItems programmatically doesn't seem to work. (accidentally added this as answer instead of comment first, not sure if my delete worked)Dana Cartwright

4 Answers

3
votes

I succeeded to work around this problem using reflection:

var method = typeof(DataGrid).GetMethod("HandleSelectionForCellInput", BindingFlags.Instance | BindingFlags.NonPublic);
method.Invoke(MyDataGrid, new object[] { cellToSelect, false, false, false });
1
votes

Remember there is a difference between focus and keyboard focus. When you select the item in code, check to see what control has Keyboard focus / regular focus. I'm guessing that the data grid loses this focus until you click on it with the mouse and then it regains the focus needed to use the ctrl function.

I ran into this issue in a WPF user control we were hosting inside a C++ application.

1
votes

I struggled with this problem for multiple days and tried a lot of things that I found on the internet. In the end, I found the solution that works for me by studying the source code of the DataGrid. In the DataGrid I noticed a member variable called _selectionAnchor and guessed that this must be the starting point for when a user expands the selection in the grid. My solution is to set this member to the first cell of the row that is selected. If a row is selected in code, than this fix makes sure that when expanding the selection it starts at the selected row.

Please note that I used the code from this issue to enable multiselect. Then, in file MainWindow.xaml.cs, I added this code:

private void ExampleDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (ExampleDataGrid.SelectedItems.Count > 0)
    {
        ExampleDataGrid.ScrollIntoView(ExampleDataGrid.SelectedItems[0]);

        // Make sure that when the user starts to make an extended selection, it starts at this one
        foreach (var cellInfo in ExampleDataGrid.SelectedCells)
        {
            if (cellInfo.Column.DisplayIndex == 0)
            {
                var cell = GetDataGridCell(cellInfo);
                cell?.Focus();
                var field = typeof(DataGrid).GetField("_selectionAnchor", BindingFlags.NonPublic | BindingFlags.Instance);
                field?.SetValue(ExampleDataGrid, cellInfo);
                break;
            }
        }
    }
}

public DataGridCell GetDataGridCell(DataGridCellInfo cellInfo)
{
    var cellContent = cellInfo.Column.GetCellContent(cellInfo.Item);
    if (cellContent != null)
    {
        return (DataGridCell)cellContent.Parent;
    }

    return null;
}

In the xaml file:

<vm:CustomDataGrid x:Name="ExampleDataGrid" ItemsSource="{Binding ImportItems}"
    SelectedItemsList="{Binding SelectedImportItems, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    AutoGenerateColumns="False" SelectionMode="Extended" IsReadOnly="True" CanUserAddRows="False"
    SelectionChanged="ExampleDataGrid_SelectionChanged">
0
votes

I just resolved exactly the same problem with the help of @ezolotko's snippet. Because the grid is dynamically generating rows I needed to subscribe to ItemContainerGenerator.StatusChanged event and find the first cell in a row representing this element.

To find the cell I used DataGridHelper class and wrapped it all in an attached behaviour:

using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using Speedwell.WPF.Helpers;

namespace Speedwell.WPF.Behaviors
{
    public static class DataGridSingleRowSelected
    {
        public static readonly DependencyProperty IsSelectionFixEnabledProperty = DependencyProperty.RegisterAttached
        (
            "IsSelectionFixEnabled",
            typeof(bool?),
            typeof(DataGridSingleRowSelected),
            new PropertyMetadata(null, IsSelectionFixEnabledChanged)
        );

        public static bool GetIsSelectionFixEnabled(DataGrid element)
        {
            return (bool)element.GetValue(IsSelectionFixEnabledProperty);
        }

        public static void SetIsSelectionFixEnabled(DataGrid element, bool value)
        {
            element.SetValue(IsSelectionFixEnabledProperty, value);
        }

        private static void IsSelectionFixEnabledChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var dataGrid = sender as DataGrid;
            if(dataGrid != null)
            {
                if(args.OldValue == null)
                {
                    dataGrid.ItemContainerGenerator.StatusChanged += (s, e) => ContainerStatusChanged(dataGrid, ((ItemContainerGenerator)s));
                }
            }
        }

        private static void ContainerStatusChanged(DataGrid dataGrid, ItemContainerGenerator generator)
        {
            if(generator != null && generator.Status == GeneratorStatus.ContainersGenerated && dataGrid.SelectedItems.Count == 1)
            {
                var row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromItem(dataGrid.SelectedItems[0]);
                if(row != null)
                {
                    var cell = dataGrid.GetCell(row, 0);
                    if(cell != null)
                    {
                        SelectCellMethod.Invoke(dataGrid, new object[] { cell, false, false, false });
                    }
                }
            }
        }

        private static readonly MethodInfo SelectCellMethod = typeof(DataGrid).GetMethod("HandleSelectionForCellInput", BindingFlags.Instance | BindingFlags.NonPublic);
    }
}

As you can see the proper selection is only applied when there is a single (1) row selected and this is exactly what I need and it seems it also what @Jordan0Day requested.