WPF datagrid and the tab key
Asked Answered
M

5

12

Another datagrid keybindings question

I have a datagrid. It has selection mode set to FullRow and KeyboardNavigation.TabNavigation="Once" which I was hoping would get my desired result but it doesn't.

When the tab key is pressed when the datagrid has focus it will tab over each column in the grid one by one. So if I tab into the grid which has 4 columns, I will have to press tab 4 times to go to the next tabindex.

What I want is for the tab key to tab right out of the datagrid on first press and give focus to the next tabindex... if that makes sense.

I have tried overriding the tab key in the keydown event handler like so.

class BetterDataGrid : DataGrid
{
  ..............
  protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)
  {
    ..............
    if (e.Key == Key.Tab)
    {
        Console.WriteLine("TAB");
        MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
    }
    .........
  }

It does write "TAB" to the console but the tab still keeps it's default behavior. Not sure if this is the right way to go the next tabindex, but then this should make the tab key do nothing but write to the console or cause an exception.
Makes me think it's impossible to override the tab key behavior.

Hoping for some helpful input.
As always, thanks in advance.

Migraine answered 13/4, 2011 at 20:17 Comment(0)
S
11

I wanted this for my line-of-business software, and the only way I have found to solve it is by codebehind, using the PreviewKeyDown, GotKeyboardFocus and LostKeyboardFocus events of the datagrid. I have put these eventhandlers in a WPF decorator, to avoid repeating it for every single DataGrid. It would probably be possible to subclass the DataGrid, but I haven't tried that.

The code for the handlers are as follows (DataGrid is x:Name="grid" for this sample code):

        private IInputElement lastDataGridFocus = null;
    private int selectedcolumnindex = 0;

    void grid_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
    {
        if (grid.Items.Count > 0 && (e.NewFocus is DataGrid || (e.NewFocus is DataGridCell && !(e.OldFocus is DataGridCell))))
        {
            DataGridCell cell = null;

            if (lastDataGridFocus != null)
            {
                FocusManager.SetFocusedElement(grid, lastDataGridFocus);
                lastDataGridFocus = null;
                e.Handled = true;
                return;
            }

            if (grid.SelectedCells.Count == 0)
            {
                DataGridRow rowContainer = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(0);
                if (rowContainer != null)
                {
                    DataGridCellsPresenter presenter = GetVisualChild<DataGridCellsPresenter>(rowContainer);
                    cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex((selectedcolumnindex < 0) ? 0 : selectedcolumnindex);
                }
            }
            else
            {
                DataGridCellInfo selectedDataGridCellInfo = (grid.SelectedCells[0] as DataGridCellInfo?).Value;
                DataGridRow rowContainer = (DataGridRow)grid.ItemContainerGenerator.ContainerFromItem(selectedDataGridCellInfo.Item);
                if (rowContainer != null)
                {
                    DataGridCellsPresenter presenter = GetVisualChild<DataGridCellsPresenter>(rowContainer);
                    cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex((selectedcolumnindex < 0) ? 0 : selectedcolumnindex);
                }
            }
            if (null != cell)
            {
                FocusManager.SetFocusedElement(grid, cell as IInputElement);
                e.Handled = true;
            }
        }
    }

    void grid_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
    {
        if (!(e.NewFocus is DataGridCell))
        {
            if (grid.CurrentCell != null)
            {
                selectedcolumnindex = grid.Columns.IndexOf(grid.CurrentCell.Column);
            }
        }
    }

    void grid_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        if (Keyboard.Modifiers == ModifierKeys.Shift && e.Key == Key.Tab)
        {
            lastDataGridFocus = Keyboard.FocusedElement;
            grid.MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous));
            e.Handled = true;
        }
        else if (Keyboard.Modifiers == ModifierKeys.None && e.Key == Key.Tab)
        {
            lastDataGridFocus = Keyboard.FocusedElement;
            grid.MoveFocus(new TraversalRequest(FocusNavigationDirection.Last));
            (Keyboard.FocusedElement as FrameworkElement).MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
            e.Handled = true;
        }
    }

With this code you can navigate inside the grid using the cursor keys, and the tab key and shift-tab key gets you out of the datagrid. If you tab out of the grid and come back to the grid, you also get to the same cell that you left. This is what my users and I want, and this is IMHO what the DataGrid control should provide as default behaviour.

Streamline answered 14/4, 2011 at 5:56 Comment(2)
Sorry for the late acceptance, didn't get a chance to test it out, but this is good. ThanksMigraine
Using FocusManager.SetFocusedElement causes two issues: 1. it will re-submit '\t' as input to selected cell 2. it would occassionally fail to set keyboard focus and/or select next cell. Using Dispatcher.BeginInvoke(new Action(() => cell.Focus())); should fix both problems.Umpire
B
0

I was also looking for this behaviour. While the solution proposed by Guge was a good start, I did not like how it saves the previously stored element nor it's overall complexity. Worst of all, I simply couldn't get it to consistently work as expected no matter how much I tweaked it. Eventually, I decided to write my own solution from scratch. By thinking outside the box (literally) I've come up with a different, simpler solution.

In the XAML file, create an empty control before and after your DataGrid like so:

<DockPanel>
  <Control IsTabStop="False" x:Name="PreControl" />
  <DataGrid PreviewKeyDown="DataGrid_PreviewKeyDown">...</DataGrid>
  <Control IsTabStop="False" x:Name="PostControl" />
</DockPanel>

Then in the code-behind, add a function for the DataGrid's PreviewKeyDown event like so:

private void DataGrid_PreviewKeyDown(object sender, KeyEventArgs e)
{
    if (Keyboard.Modifiers == ModifierKeys.Shift && e.Key == Key.Tab)
    {
        PreControl.MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous));
        e.Handled = true;
    }
    else if (Keyboard.Modifiers == ModifierKeys.None && e.Key == Key.Tab)
    {
        PostControl.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
        e.Handled = true;
    }
    else if (new[] { Key.Up, Key.Down, Key.Left, Key.Right }.Contains(e.Key))
    {
        var grid = (DataGrid)sender;
        grid.CurrentCell = new DataGridCellInfo(grid.SelectedItem, grid.CurrentColumn);
    }
}

The first if and else-if override the default tab behavior by tabbing from the empty controls instead of from the datagrid. The next else-if statement updates the current cell before moving with the arrow keys. Sometimes the current cell becomes out of sync with the selected cell when switching focus in and out of the grid. This is an issue with the previously proposed solution as well as this one and I have not found a way to fix it, but by doing this, I can make sure that when navigating using the arrow keys, it navigates relative to the selected cell, rather than the current cell.

Some caveats to this approach:

  • It won't work well when allowing multiple rows to be selected
  • As previously mentioned, sometimes the current cell can be different than the selected cell. This can cause visual issues but does not affect the selected item or navigation.
  • I have only thoroughly tested this with a data grid that has full row selection. The selected column may not be properly preserved or may have navigation issues.
Bonnee answered 13/1, 2023 at 20:48 Comment(0)
R
0

This might help someone... I wanted to move to the next row using the Tab key, but also select or pass through my custom cell which contains two buttons. The solution was to extend the DataGrid column CellStyle and set Focusable to False for each column/cell. Here is my code:

<DataGrid Grid.Row="1" Name="TutorialsDataGrid" AutoGenerateColumns="False" CanUserAddRows="False" IsReadOnly="True"
        EnableRowVirtualization="True" SelectionUnit="FullRow">
<DataGrid.Columns>
    <DataGridTextColumn Header="Title" Width="0.5*" MinWidth="380" Binding="{Binding Title}">
        <DataGridTextColumn.CellStyle>
            <Style TargetType="DataGridCell" BasedOn="{StaticResource MetroDataGridCell}">
                <Setter Property="Margin" Value="5,0"/>
                <!-- SOLUTION -->
                <Setter Property="Focusable" Value="False"/>
            </Style>
        </DataGridTextColumn.CellStyle>
    </DataGridTextColumn>
    <DataGridTemplateColumn Header="Format" Width="*" MinWidth="200">
        <DataGridTemplateColumn.CellStyle>
            <Style TargetType="DataGridCell" BasedOn="{StaticResource MetroDataGridCell}">
                <Setter Property="Margin" Value="5,0"/>
                <!-- SOLUTION -->
                <Setter Property="Focusable" Value="False"/>
            </Style>
        </DataGridTemplateColumn.CellStyle>
        <DataGridTemplateColumn.CellTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal" >
                    <Button Name="OpenVideo" ToolTip="Open Video" Margin="5,3" Width="80" Height="28" Tag="{Binding VideoPath}" Click="OpenVideo_Click">
                        <Path Height="16" Stretch="Uniform" Data="{StaticResource UniconsCirclePlay}" Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
                    </Button>
                    <Button Name="OpenPDF" ToolTip="Open PDF" Margin="5,3" Width="80" Height="28" Tag="{Binding PdfPath}" Click="OpenPDF_Click">
                        <Path Height="16" Stretch="Uniform" Data="{StaticResource FilePdf}" Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
                    </Button>
                </StackPanel>
            </DataTemplate>
        </DataGridTemplateColumn.CellTemplate>
    </DataGridTemplateColumn>
</DataGrid.Columns>
Rumble answered 24/7, 2023 at 11:54 Comment(0)
S
0

I finally got the best answer to this problem that I have been struggling with for a long time. At some point, I confirmed that pressing Ctrl+Tab in the DataGrid would result in the desired behavior. From this, I thought I could just swap the values of KeyboardNavigation.TabNavigation attached property and KeyboardNavigation.ControlTabNavigation attached property in the DataGrid. However, it did not work. After checking the code of the DataGrid's child elements with ReferenceSource, further, I thought that I should swap the values of KeyboardNavigation.TabNavigation attached property and KeyboardNavigation.ControlTabNavigation attached property in DataGridCellsPanel. I ran it and got the ideal behavior. What's more, and this is great, the current cell after focus is reentered is the current cell when the focus is lost.

<DataGrid KeyboardNavigation.TabNavigation="Once"
          KeyboardNavigation.ControlTabNavigation="Continue">
    <DataGrid.RowStyle>
        <Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource {x:Type DataGridRow}}">
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <DataGridCellsPanel KeyboardNavigation.TabNavigation="Continue"
                                            KeyboardNavigation.ControlTabNavigation="Local"/>
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </DataGrid.RowStyle>
</DataGrid>

It can also be realized with the following attached properties, referring to the StandardTab property of DataGridView in WinForms.

For VB.net

Public Class DataGridHelper

    Public Shared Function GetStandardTab(element As DataGrid) As Boolean
        If element Is Nothing Then Throw New ArgumentNullException(NameOf(element))
        Return CBool(element.GetValue(StandardTabProperty))
    End Function

    Public Shared Sub SetStandardTab(element As DataGrid, value As Boolean)
        If element Is Nothing Then Throw New ArgumentNullException(NameOf(element))
        element.SetValue(StandardTabProperty, value)
    End Sub

    Public Shared ReadOnly StandardTabProperty As DependencyProperty =
                           DependencyProperty.RegisterAttached("StandardTab",
                           GetType(Boolean), GetType(DataGridHelper),
                           New FrameworkPropertyMetadata(False, FrameworkPropertyMetadataOptions.Inherits, New PropertyChangedCallback(AddressOf OnStandardTabPropertyChanged)))

    Private Shared Sub OnStandardTabPropertyChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
        If TypeOf d Is DataGrid OrElse TypeOf d Is DataGridCellsPanel Then
            If CBool(e.NewValue) Then
                If TypeOf d Is DataGridCellsPanel Then
                    d.SetValue(KeyboardNavigation.TabNavigationProperty, KeyboardNavigationMode.Continue)
                    d.SetValue(KeyboardNavigation.ControlTabNavigationProperty, KeyboardNavigationMode.Local)
                Else
                    d.SetValue(KeyboardNavigation.TabNavigationProperty, KeyboardNavigationMode.Once)
                    d.SetValue(KeyboardNavigation.ControlTabNavigationProperty, KeyboardNavigationMode.Continue)
                End If
            Else
                d.ClearValue(KeyboardNavigation.TabNavigationProperty)
                d.ClearValue(KeyboardNavigation.ControlTabNavigationProperty)
            End If
        End If
    End Sub

End Class

For C# (not tested)

public class DataGridHelper
{
    public static bool GetStandardTab(DataGrid element)
    {
        if (element == null)
        {
            throw new ArgumentNullException(nameof(element));
        }
        return (bool)element.GetValue(StandardTabProperty);
    }

    public static void SetStandardTab(DataGrid element, bool value)
    {
        if (element == null)
        {
            throw new ArgumentNullException(nameof(element));
        }
        element.SetValue(StandardTabProperty, value);
    }

    public static readonly DependencyProperty StandardTabProperty = 
                                              DependencyProperty.RegisterAttached("StandardTab", 
                                              typeof(bool), typeof(DataGridHelper), 
                                              new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Inherits, new PropertyChangedCallback(OnStandardTabPropertyChanged)));

    private static void OnStandardTabPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is DataGrid || d is DataGridCellsPanel)
        {
            if ((bool) e.NewValue)
            {
                if (d is DataGridCellsPanel)
                {
                    d.SetValue(KeyboardNavigation.TabNavigationProperty, KeyboardNavigationMode.Continue);
                    d.SetValue(KeyboardNavigation.ControlTabNavigationProperty, KeyboardNavigationMode.Local);
                }
                else
                {
                    d.SetValue(KeyboardNavigation.TabNavigationProperty, KeyboardNavigationMode.Once);
                    d.SetValue(KeyboardNavigation.ControlTabNavigationProperty, KeyboardNavigationMode.Continue);
                }
            }
            else
            {
                d.ClearValue(KeyboardNavigation.TabNavigationProperty);
                d.ClearValue(KeyboardNavigation.ControlTabNavigationProperty);
            }
        }
    }
}
Stockholder answered 30/8, 2023 at 17:16 Comment(0)
Z
-2

What you are trying to achive is not the right behavior, everyone expect Excel like navigation when pressing the Tab-Key while the DataGrid is focused. It's better to prevent tab stops on the DataGrid by setting IsTabStop="False" on the DataGrid if you don't want the user to navigate through the DataGrid.

Zingale answered 13/4, 2011 at 20:58 Comment(8)
Why for the down vote?! it's just my opinion from UX point of view!!Zingale
Yeah I agree with you Mr. Fadil, it's better to set the IsTabStop="False"Official
Sorry for the downvote but your answer didn't really help at all. IsTabStop=false takes away the tabbing into the grid, which I'd like to keep and does not result in the desired behaviour when it's focused. Sure in many cases the Excel-style navigation works well, but in this case it's just redundant, the grid is read only and has fullrow selection.Migraine
If it's a read-only DataGrid, I'm struggling to see how a tab stop makes sense...Tedder
@JohnB. I think it makes sense. I have users that don't want to be forced to use a mouse, they like to tab their way through a form. The better question is why every cell is a tabstop.Streamline
@Guge, What is the meaning of navigating with the tab-key just to the first row of a read-only DataGrid, and when you press tab-key again you navigate to another control other than the DataGrid?!! for me this is weird!!Official
@Hohinhime. You tab into the DataGrid, then you navigate around inside the DataGrid using the arrow keys, and when you want to leave the DataGrid you use the tab key again. I think this is the best way. And it is essential that if you tab out of a DataGrid and back into it you should come back to the same cell you were in the last time. That cell would have logical focus for the DataGrid. If you have a DataGrid with a couple of hundred cells, you wouldn't want to have to press tab once for each cell to leave the DataGrid.Streamline
@Hohinhime. And by the way: Did you even bother to read Steini's question? This behaviour is what Steini wants!Streamline

© 2022 - 2024 — McMap. All rights reserved.