Home > front end >  How to shift focus from an edited cell in a datagrid to a text box in WPF
How to shift focus from an edited cell in a datagrid to a text box in WPF

Time:12-12

I'm attempting to have a user input textbox take an ID string and fetch data to fill rows in a datagrid, then trigger editing a specific row and cell in the datagrid based on the value of the user input and then put focus back on the textbox when the return/enter key is pressed after the user enters a value in the datagrid cell.

I have most of this working correctly except for the last step, the enter key has to be pressed twice in order to return to the text box, I would like it to return on first press.

If the edited cell is selected by the user directly using the mouse then it works correctly, a single enter press sends focus back to the textbox.

The automatic cell editing is triggered with an attached property and a helper class that listens for the OnCurrentCellChanged event and triggers the datagrid BeginEdit method. I wonder if this needs to be closed off/ended before the focus can change properly? (Helper class implemented based on answer here, with great thanks to @Orace)

How can I achieve this? I have tried commiting the edit, cancelling the edit in the xaml code behind PreviewKeyDown method but no luck. So possibly I have to change the OnCurrentCellChanged event method somehow?

my xaml:

<Window x:Class="TestChangingEditedCellFocus.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestChangingEditedCellFocus"  d:DataContext="{d:DesignInstance Type=local:ViewModel}"
        mc:Ignorable="d"
        FocusManager.FocusedElement="{Binding ElementName=idfield}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <ResourceDictionary>
            <CollectionViewSource x:Key="srModels" Source="{Binding Models}"/>
        </ResourceDictionary>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="40"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <TextBox x:Name="idfield" Width="100" Height="30" Text="{Binding Path=ModelId, UpdateSourceTrigger=PropertyChanged}"/>
            <Button IsDefault="True" Command="{Binding Path=Command}" Content="Get Row" Height="30"/>
        </StackPanel>
        <DataGrid Grid.Row="1"
                  x:Name="modelgrid"                  
                  local:DataGridAutoEdit.AutoEditColumn="2"
                  ItemsSource="{Binding Source={StaticResource srModels}}"
                  SelectedItem="{Binding CurrentModel, UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}"
                  AutoGenerateColumns="False" 
                  SelectionMode="Single"
                  SelectionUnit="FullRow"
                  PreviewKeyDown="modelgrid_PreviewKeyDown"
                  >
            <DataGrid.Columns>
                <DataGridTemplateColumn Header="ID" Width="Auto" IsReadOnly="True">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Label Content="{Binding Id}" HorizontalContentAlignment="Right"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
                <DataGridTemplateColumn Header="Name" Width="Auto" IsReadOnly="True">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Label Content="{Binding Name}" HorizontalContentAlignment="Right"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
                <DataGridTemplateColumn Header="Value" Width="Auto">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBox Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" BorderThickness="0" 
                                     VerticalContentAlignment="Center" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" HorizontalContentAlignment="Right"
                                     FocusManager.FocusedElement="{Binding RelativeSource={RelativeSource Self}}"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

Xaml code behind:

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new ViewModel();
        }

        private void modelgrid_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            if ((e.Key == Key.Enter) || (e.Key == Key.Return))
            {
                //DataGrid grid = sender as DataGrid;
                //grid.CancelEdit();
                idfield.Focus();
                idfield.SelectAll();                
            }
        }
    }

The VeiwModel:

public class ViewModel : ObservableObject
    {
        private ObservableCollection<model> _models;
        private model _currentModel;
        private string _modelId;
        private ICommand _command;
        
        public ObservableCollection<model> Models
        {
            get
            {
                if (_models is null)
                {
                    _models = new ObservableCollection<model>();
                    OnPropertyChanged("Models");
                }
                return _models;
            }
        }
        public model CurrentModel
        {
            get
            {
                return _currentModel;
            }
            set
            {
                _currentModel = value;
                OnPropertyChanged("CurrentModel");
            }
        }
        public string ModelId
        {
            get
            {
                return _modelId;
            }
            set
            {
                _modelId = value;
                OnPropertyChanged("ModelId");
            }
        }
        public ICommand Command
        {
            get
            {
                if (_command == null)
                {
                    _command = new RelayCommand(param=>ExcecuteCommand(), pred=> ModelId is not null);
                }
                return _command;
            }
        }

        public void ExcecuteCommand()
        {
            Models.Clear();
            if (Models.Count == 0)
            {
                Models.Add(new model() { Id = 1, Name = "name1" });
                Models.Add(new model() { Id = 2, Name = "name2" });
                Models.Add(new model() { Id = 3, Name = "name3" });
                Models.Add(new model() { Id = 4, Name = "name4" });
            }

            foreach (model model in Models)
            {
                System.Diagnostics.Debug.WriteLine("Model: "   model.Name);
                int id = int.Parse(ModelId);
                if(id == model.Id)
                {
                    CurrentModel = model;
                }
            }
            ModelId = null;
        }
    }

The DataGridAutoEdit helper class to trigger edit mode:

class DataGridAutoEdit
    {
        public static readonly DependencyProperty AutoEditColumnProperty = DependencyProperty.RegisterAttached("AutoEditColumn", typeof(int), typeof(DataGridAutoEdit), new PropertyMetadata(default(int), AutoEditColumnChangedCallback));

        public static void SetAutoEditColumn(DependencyObject element, int value)
        {
            element.SetValue(AutoEditColumnProperty, value);
        }

        public static int GetAutoEditColumn(DependencyObject element)
        {
            return (int)element.GetValue(AutoEditColumnProperty);
        }

        private static void AutoEditColumnChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is not DataGrid dataGrid)
                return;

            GetAutoEditColumnHelper(dataGrid)?.Dispose();

            if (e.NewValue is int columnIndex)
            {
                SetAutoEditColumnHelper(d, new AutoEditColumnHelper(dataGrid, columnIndex));
            }
            else
            {
                d.ClearValue(AutoEditColumnHelperProperty);
            }
        }


        private static readonly DependencyProperty AutoEditColumnHelperProperty = DependencyProperty.RegisterAttached("AutoEditColumnHelper", typeof(AutoEditColumnHelper), typeof(DataGridAutoEdit), new PropertyMetadata(default(AutoEditColumnHelper)));

        private static void SetAutoEditColumnHelper(DependencyObject element, AutoEditColumnHelper value)
        {
            element.SetValue(AutoEditColumnHelperProperty, value);
        }

        private static AutoEditColumnHelper? GetAutoEditColumnHelper(DependencyObject element)
        {
            return element.GetValue(AutoEditColumnHelperProperty) as AutoEditColumnHelper;
        }



        //add private class to get datagrid auto cel edit function working, move to helpers if this works
        private class AutoEditColumnHelper : IDisposable
        {
            private readonly DataGrid _dataGrid;
            private readonly int _columnIndex;

            private object? _lastItem;

            public AutoEditColumnHelper(DataGrid dataGrid, int columnIndex)
            {
                _dataGrid = dataGrid;
                _columnIndex = columnIndex;

                _dataGrid.CurrentCellChanged  = OnCurrentCellChanged;
            }

            public void Dispose()
            {
                _dataGrid.CurrentCellChanged -= OnCurrentCellChanged;
            }

            private void OnCurrentCellChanged(object? sender, EventArgs e)
            {
                DataGridCellInfo currentCell = _dataGrid.CurrentCell;
                
                if (!currentCell.IsValid || Equals(currentCell.Item, _lastItem))
                {
                    return;
                }

                DataGridColumn autoEditColumn = GetAutoEditColumn();
                if (autoEditColumn is null)
                {
                    return;
                }

                _dataGrid.Dispatcher.BeginInvoke(() =>
                {
                    _lastItem = _dataGrid.SelectedItem;
                    _dataGrid.CurrentCell = new DataGridCellInfo(_lastItem, autoEditColumn);
                    _dataGrid.BeginEdit();
                });
            }

            private DataGridColumn? GetAutoEditColumn()
            {
                return _columnIndex < 0 || _columnIndex > _dataGrid.Columns.Count ? null : _dataGrid.Columns[_columnIndex];
            }
        }
    }

A working simplified project is here.

CodePudding user response:

After some trial and error I found editing my PreviewKeyDowan method in code behind to both Commit and Cancel the edit AND using SendKeys to send the Enter command again worked to send the cursor back to the textbox feild as I wanted:

private void modelgrid_PreviewKeyDown(object sender, KeyEventArgs e)
{
    if ((e.Key == Key.Enter) || (e.Key == Key.Return))
    {
        DataGrid grid = sender as DataGrid;
        grid.CommitEdit();
        grid.CancelEdit();
        SendKeys.SendWait("{ENTER}");
        idfield.Focus();              
    }
}

but this seems pretty hacky and I'll be happy for any other suggestions.

  • Related