Search code examples
.netwpfdatagridviewdatagrid

WPF DataGridView keyboard focus from command/viewmodel


The net is full of similar questions and I searched SO as well (closest: [1], [2], [3]).

So far I cannot believe that this problem is that ugly/non-trivial.

I have a DataGridView. I have other controls (in this case: In the DataGridView's column headers). I want to support a quick 'jump to grid' command. This command needs to set the keyboard focus to the grid so that the user can use arrow keys to navigate between rows.

Trivial testcase to play along at home below. It's insanely easy to select an element in the datagrid, but there doesn't seem to be a way to give it keyboard focus. Except in code-behind actions and even then it seems like you have to jump through hoops (see [2], juggling to get the cell container to set the keyboard focus, because .. grid and row doesn't seem to work from all I can tell).

It seems simple enough?

Some trivial model:

public class Item
{
    public int DayOfMonth { get; set; }
    public string Weekday { get; set; }
}

Matching trivial viewmodel (assumes that you have a RelayCommand implementation, see JumpToGrid for the ~meat~):

public class MainViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private IList<Item> m_items;
    public IList<Item> SampleItems
    {
        get { return m_items; }
        set { SetField(ref m_items, value, () => SampleItems); }
    }

    private Item m_currentItem;
    public Item CurrentItem
    {
        get { return m_currentItem; }
        set { SetField(ref m_currentItem, value, () => CurrentItem); }
    }

    public ICommand JumpToGridCommand { get; private set; }

    public MainViewModel()
    {
        JumpToGridCommand = new RelayCommand(p => JumpToGrid());

        var items = new List<Item>();
        var today = DateTime.Now;
        for (int i = 1; i <= DateTime.DaysInMonth(today.Year, today.Month); i++ )
        {
            items.Add(new Item { DayOfMonth = i, Weekday = new DateTime(today.Year, today.Month, i).DayOfWeek.ToString() });
        }
        SampleItems = items;
    }

    private void JumpToGrid()
    {
        // I can change the selection just fine
        CurrentItem = SampleItems[0];

        // But the keyboard focus is broken, up/down doesn't work as expected
    }

    protected bool SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression)
    {
        if (selectorExpression == null) throw new ArgumentNullException("selectorExpression");
        MemberExpression body = selectorExpression.Body as MemberExpression;
        if (body == null) throw new ArgumentException("The body must be a member expression");
        var fieldName = body.Member.Name;

        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(fieldName);
        return true;
    }

    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Simplistic view (code-behind is empty except for a "DataContext = new MainViewModel()" in the loaded event):

<Window x:Class="DataGridKeyboardFocus.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataGridKeyboardFocus"
        Loaded="Window_Loaded"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DataGrid
                ColumnWidth="*" 
                AutoGenerateColumns="False"
                Margin="1"
                ItemsSource="{Binding SampleItems}" 
                SelectedItem="{Binding CurrentItem, Mode=TwoWay}"
                SelectionUnit="FullRow"
                IsReadOnly="True">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding DayOfMonth}">
                    <DataGridTextColumn.HeaderTemplate>
                        <DataTemplate>
                        <StackPanel Orientation="Vertical">
                            <TextBlock Text="Some label" />
                            <TextBox>
                                <TextBox.InputBindings>
                                    <KeyBinding Modifiers="Control" Key="Tab" Command="{Binding DataContext.JumpToGridCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" />
                                </TextBox.InputBindings>
                            </TextBox>
                        </StackPanel>
                        </DataTemplate>
                    </DataGridTextColumn.HeaderTemplate>
                </DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding Weekday}">
                    <DataGridTextColumn.HeaderTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Vertical">
                                <TextBlock Text="Another label" />
                                <TextBox>
                                    <TextBox.InputBindings>
                                        <KeyBinding Modifiers="Control" Key="Tab" Command="{Binding DataContext.JumpToGridCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" />
                                    </TextBox.InputBindings>
                                </TextBox>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTextColumn.HeaderTemplate>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

1: Keyboard focus vs logical focus in WPF

2: Keyboard focus to DataGrid

3: WPF: Can't control keyboard focus


Solution

  • I have added selection changed event for the grid and trying to select a cell. Refer the below code.

     <DataGrid
                ColumnWidth="*" 
                AutoGenerateColumns="False"
                Margin="1"
                ItemsSource="{Binding SampleItems}" 
                SelectedItem="{Binding CurrentItem, Mode=TwoWay}"
                SelectionUnit="FullRow"
                IsReadOnly="True"
            SelectionChanged="DataGrid_SelectionChanged">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding DayOfMonth}">
                    <DataGridTextColumn.HeaderTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Vertical">
                                <TextBlock Text="Some label" />
                                <TextBox>
                                    <TextBox.InputBindings>
                                        <KeyBinding Modifiers="Control" Key="T" Command="{Binding DataContext.JumpToGridCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
                                    </TextBox.InputBindings>
                                </TextBox>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTextColumn.HeaderTemplate>
                </DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding Weekday}">
                    <DataGridTextColumn.HeaderTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Vertical">
                                <TextBlock Text="Another label" />
                                <TextBox>
                                    <TextBox.InputBindings>
                                        <KeyBinding Modifiers="Control" Key="T" Command="{Binding DataContext.JumpToGridCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
                                    </TextBox.InputBindings>
                                </TextBox>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTextColumn.HeaderTemplate>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
    

    Code behind.

    private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            DataGrid dg = sender as DataGrid;
            SelectRowByIndex(dg, dg.SelectedIndex);
        }
        public static DataGridCell GetCell(DataGrid dataGrid, DataGridRow rowContainer, int column)
        {
            if (rowContainer != null)
            {
                DataGridCellsPresenter presenter = FindVisualChild<DataGridCellsPresenter>(rowContainer);
                if (presenter == null)
                {
                    /* if the row has been virtualized away, call its ApplyTemplate() method
                     * to build its visual tree in order for the DataGridCellsPresenter
                     * and the DataGridCells to be created */
                    rowContainer.ApplyTemplate();
                    presenter = FindVisualChild<DataGridCellsPresenter>(rowContainer);
                }
                if (presenter != null)
                {
                    DataGridCell cell = presenter.ItemContainerGenerator.ContainerFromIndex(column) as DataGridCell;
                    if (cell == null)
                    {
                        /* bring the column into view
                         * in case it has been virtualized away */
                        dataGrid.ScrollIntoView(rowContainer, dataGrid.Columns[column]);
                        cell = presenter.ItemContainerGenerator.ContainerFromIndex(column) as DataGridCell;
                    }
                    return cell;
                }
            }
            return null;
        }
        public static T FindVisualChild<T>(DependencyObject obj) where T : DependencyObject
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(obj, i);
                if (child != null && child is T)
                    return (T)child;
                else
                {
                    T childOfChild = FindVisualChild<T>(child);
                    if (childOfChild != null)
                        return childOfChild;
                }
            }
            return null;
        }
        public static void SelectRowByIndex(DataGrid dataGrid, int rowIndex)
        {
            DataGridRow row = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) as DataGridRow;
            if (row != null)
            {
                DataGridCell cell = GetCell(dataGrid, row, 0);
                if (cell != null)
                    cell.Focus();
            }
        }
    

    Refer the link. http://social.technet.microsoft.com/wiki/contents/articles/21202.wpf-programmatically-selecting-and-focusing-a-row-or-cell-in-a-datagrid.aspx