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
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;
}
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).