Search code examples
c#xamlwpfdatagridvi

Vi move through DataGrid


I'm using a DataGrid to display a bunch of data. I have SelectionMode="Extended" and SelectionUnit="FullRow".

What I would like to be able to do is to press J to move the focus down in the grid, press K to move up in the grid, and press x to add/remove the focused row to/from the list of SelectedItems(basically just like gmail with keyboard shortcuts on)

I'm pretty handy with wpf, but I have yet to be able to accomplish this. I'm not sure that the row focus is separate from the selected items, but I figure what the hell, maybe someone here has done something similar.

Here's what I've tried so far

case Key.X:
{
    resultsGrid.SelectedItems.Add(resultsGrid.SelectedItem);
    e.Handled = true;
    break;
}
case Key.J:
{
    //down
    var currow = (DataGridRow) resultsGrid.ItemContainerGenerator.ContainerFromItem(resultsGrid.SelectedItem);
    currow.MoveFocus(new TraversalRequest(FocusNavigationDirection.Down));
    //if (resultsGrid.SelectedIndex + 1 >= resultsGrid.Items.Count)
    //    resultsGrid.SelectedIndex = 0;
    //else
    //    resultsGrid.SelectedIndex++;
    break;
}
case Key.K:
{
    //up
    var currow =
        (DataGridRow) resultsGrid.ItemContainerGenerator.ContainerFromItem(resultsGrid.SelectedItem);
    currow.MoveFocus(new TraversalRequest(FocusNavigationDirection.Up));
    //if (resultsGrid.SelectedIndex - 1 <= 0)
    //    resultsGrid.SelectedIndex = resultsGrid.Items.Count - 1;
    //else
    //    resultsGrid.SelectedIndex--;
    break;
}

Currently the current row doesn't move up or down. I've also tried FocusNavigationDirection.Previous and Next and those don't move the focus either. If I go by index it moves, but pressing X doesn't add to the list of selected items. It seems multi select doesn't want to kick in until you use shift and up/down or shift mouse click

edit

Ok so I've figured out how to navigate using the j and k key, but selecting still isn't working. If I move up or down it clears the selection, also pressing x doesn't do anything, visually at least.

case Key.X:
    resultsGrid.SelectedItems.Add(resultsGrid.SelectedItem);
    e.Handled = true;
    break;
case Key.J:
    {
        //down
        InputManager.Current.ProcessInput(new KeyEventArgs(Keyboard.PrimaryDevice, Keyboard.PrimaryDevice.ActiveSource, 0, Key.Down)
        {
            RoutedEvent = Keyboard.KeyDownEvent
        });
        resultsGrid.ScrollIntoView(resultsGrid.SelectedItem);
        e.Handled = true;
        break;
    }
case Key.K:
    {
        //up
        InputManager.Current.ProcessInput(new KeyEventArgs(Keyboard.PrimaryDevice, Keyboard.PrimaryDevice.ActiveSource, 0, Key.Up)
        {
            RoutedEvent = Keyboard.KeyDownEvent
        });
        resultsGrid.ScrollIntoView(resultsGrid.SelectedItem);
        e.Handled = true;
        break;
    }

Solution

  • If I understand the issue correctly - you have a focus and selection per row. You want to move focus via k/j keys and to toggle selection via x key.

    I like to use behaviors in these situations - it requires reference to System.Windows.Interactivity.dll from blen SDK but it also makes for a cleaner and modular code.

    [edit: this is a quick POC that I did. It will probably require some more null reference protection and handling of fringe/edge cases]

    The behavior is:

    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Controls.Primitives;
    using System.Windows.Input;
    using System.Windows.Interactivity;
    
    namespace GridNavigationTest
    {
        public class GridNavigationBehavior : Behavior<DataGrid>
        {
            #region Overrides of Behavior
            /// <summary>
            /// Called after the behavior is attached to an AssociatedObject.
            /// </summary>
            /// <remarks>
            /// Override this to hook up functionality to the AssociatedObject.
            /// </remarks>
            protected override void OnAttached()
            {
                AssociatedObject.PreviewKeyDown += AssociatedObject_KeyDown;
            }
    
            /// <summary>
            /// Called when the behavior is being detached from its AssociatedObject, but before it has actually occurred.
            /// </summary>
            /// <remarks>
            /// Override this to unhook functionality from the AssociatedObject.
            /// </remarks>
            protected override void OnDetaching()
            {
                AssociatedObject.KeyDown -= AssociatedObject_KeyDown;
            }
            #endregion
    
            #region Event handlers
            void AssociatedObject_KeyDown(object sender, KeyEventArgs e)
            {
                switch (e.Key)
                {
                    case Key.J:
                        NavigateGridFocus(FocusNavigationDirection.Up);
                        break;
                    case Key.K:
                        NavigateGridFocus(FocusNavigationDirection.Down);
                        break;
                    case Key.X:
                        ToggleRowSelection();
                        break;
                }
            } 
            #endregion
    
            #region Methods
            private void ToggleRowSelection()
            {
                var currentlyFocusedRow = FindCurrentlyFocusedRow();
                if (currentlyFocusedRow == null)
                {
                    return;
                }
    
                var generator = AssociatedObject.ItemContainerGenerator;
                var rowItem = generator.ItemFromContainer(currentlyFocusedRow);
                if (AssociatedObject.SelectionMode == DataGridSelectionMode.Extended)
                {
                    if (AssociatedObject.SelectedItems.Contains(rowItem))
                    {
                        AssociatedObject.SelectedItems.Remove(rowItem);
                    }
                    else
                    {
                        AssociatedObject.SelectedItems.Add(rowItem);
                    }
                }
                else
                {
                    AssociatedObject.SelectedItem = AssociatedObject.SelectedItem == rowItem ? null : rowItem;
                }
            }
    
            private void NavigateGridFocus(FocusNavigationDirection direction)
            {
                var currentlyFocusedRow = FindCurrentlyFocusedRow();
                if (currentlyFocusedRow == null)
                {
                    return;
                }
    
                var traversalRequest = new TraversalRequest(direction);
                var currentlyFocusedElement = Keyboard.FocusedElement as UIElement;
                if (currentlyFocusedElement != null) currentlyFocusedElement.MoveFocus(traversalRequest);
            }
    
            private DataGridRow FindCurrentlyFocusedRow()
            {
                var generator = AssociatedObject.ItemContainerGenerator;
                if (generator.Status != GeneratorStatus.ContainersGenerated)
                {
                    return null;
                }
    
                for (var index = 0; index < generator.Items.Count - 1; index++)
                {
                    var row = generator.ContainerFromIndex(index) as DataGridRow;
                    if (row != null && row.IsKeyboardFocusWithin)
                    {
                        return row;
                    }
                }
                return null;
            }
            #endregion
        }
    } 
    

    And the usage is:

    <Window x:Class="GridNavigationTest.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:gridNavigationTest="clr-namespace:GridNavigationTest"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
            Title="MainWindow"
            Height="350"
            Width="525">
        <Grid>
            <DataGrid ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type gridNavigationTest:MainWindow}}, Path=People}">
                <i:Interaction.Behaviors>
                    <gridNavigationTest:GridNavigationBehavior/>
                </i:Interaction.Behaviors>
                <!--This is here just for testing of focus movement-->
                <DataGrid.ItemContainerStyle>
                    <Style TargetType="{x:Type DataGridRow}">
                        <Style.Triggers>
                            <Trigger Property="IsKeyboardFocusWithin"
                                     Value="True">
                                <Setter Property="Background"
                                        Value="HotPink" />
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </DataGrid.ItemContainerStyle>
            </DataGrid>
        </Grid>
    </Window>
    

    This requires that one of the rows would have IsKeyboardFocuedWithin set to true. You can bake the logic for initial selection into the behavior (or, in a new behavior).