I have a DataGrid
that edits an ObservableCollection
of IEditableObject
objects. The DataGrid is set to CanUserAddRows="True"
so that the blank row for adding a new record is present. Everything works perfectly, with one notable exception.
The default tab behavior for all rows that have data in them is to move to the first column in the next row when tabbing out of the last column in the current row, which is exactly the behavior I want. However, this is not the behavior I get if the next row is a new row, the row that will contain the next new record. Instead of moving to the first column in the new row, the tab moves focus to the first column of the first row in the DataGrid.
My current attempt to change the behavior to what I want looks like this:
private void ItemsDataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
if (ItemsDataGrid.SelectedIndex == ItemsDataGrid.Items.Count - 2)
{
DataGridRow row = ItemsDataGrid
.ItemContainerGenerator.ContainerFromItem(CollectionView.NewItemPlaceholder) as DataGridRow;
if (row.Focusable)
row.Focus();
DataGridCell cell = ItemsDataGrid.GetCell(row, 0);
if (cell != null)
{
DataGridCellInfo dataGridCellInfo = new DataGridCellInfo(cell);
if (cell.Focusable)
cell.Focus();
}
}
}
Which doesn't set the focus to where I want, even though cell.SetFocus()
actually gets called.
My current working theory is this: row.Focusable
returns false
, probably because the row doesn't "quite" exist yet (I already know that it doesn't yet contain data at this point), so the desired cell can't get the focus because the row can't get the focus.
Any thoughts?
The closest thing to an MCVE that I could muster is below. WPF is rather verbose. Note that I'm using Fody.PropertyChanged as my INotifyPropertyChanged
implementation.
MainWindow.XAML
<Window
x:Class="WpfApp2.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:local="clr-namespace:WpfApp2"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Grid>
<TabControl>
<TabItem Header="List">
<DataGrid
Name="ItemsDataGrid"
AutoGenerateColumns="False"
CanUserAddRows="True"
ItemsSource="{Binding EditableFilterableItems}"
KeyboardNavigation.TabNavigation="Cycle"
RowEditEnding="ItemsDataGrid_RowEditEnding"
RowHeaderWidth="20"
SelectedItem="{Binding SelectedItem}"
SelectionUnit="FullRow">
<DataGrid.Resources>
<!-- http://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/ -->
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn
x:Name="QuantityColumn"
Width="1*"
Binding="{Binding Quantity}"
Header="Quantity" />
<DataGridComboBoxColumn
x:Name="AssetColumn"
Width="3*"
DisplayMemberPath="Description"
Header="Item"
ItemsSource="{Binding Data.ItemDescriptions, Source={StaticResource proxy}}"
SelectedValueBinding="{Binding ItemDescriptionID}"
SelectedValuePath="ItemDescriptionID" />
<DataGridTextColumn
x:Name="NotesColumn"
Width="7*"
Binding="{Binding Notes}"
Header="Notes" />
</DataGrid.Columns>
</DataGrid>
</TabItem>
</TabControl>
</Grid>
</Window>
MainWindow.xaml.CS
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace WpfApp2
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
MainWindowViewModel _viewModel;
public MainWindow()
{
_viewModel = new MainWindowViewModel();
DataContext = _viewModel;
InitializeComponent();
}
private void ItemsDataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
if (ItemsDataGrid.SelectedIndex == ItemsDataGrid.Items.Count - 2)
{
DataGridRow row = ItemsDataGrid
.ItemContainerGenerator.ContainerFromItem(CollectionView.NewItemPlaceholder) as DataGridRow;
var rowIndex = row.GetIndex();
if (row.Focusable)
row.Focus();
DataGridCell cell = ItemsDataGrid.GetCell(row, 0);
if (cell != null)
{
DataGridCellInfo dataGridCellInfo = new DataGridCellInfo(cell);
if (cell.Focusable)
cell.Focus();
}
}
}
}
}
MainWindowViewModel.CS
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;
using PropertyChanged;
namespace WpfApp2
{
[AddINotifyPropertyChangedInterface]
public class MainWindowViewModel
{
public MainWindowViewModel()
{
Items = new ObservableCollection<Item>(
new List<Item>
{
new Item {ItemDescriptionID=1, Quantity=1, Notes="Little Red Wagon"},
new Item {ItemDescriptionID=2, Quantity=1, Notes="I Want a Pony"},
}
);
FilterableItems = CollectionViewSource.GetDefaultView(Items);
EditableFilterableItems = FilterableItems as IEditableCollectionView;
}
public ObservableCollection<Item> Items { get; set; }
public ICollectionView FilterableItems { get; set; }
public IEditableCollectionView EditableFilterableItems { get; set; }
public Item SelectedItem { get; set; }
public List<ItemDescription> ItemDescriptions => new List<ItemDescription>
{
new ItemDescription { ItemDescriptionID = 1, Description="Wagon" },
new ItemDescription { ItemDescriptionID = 2, Description="Pony" },
new ItemDescription { ItemDescriptionID = 3, Description="Train" },
new ItemDescription { ItemDescriptionID = 4, Description="Dump Truck" },
};
}
}
Item.CS, ItemDescription.CS
public class Item : EditableObject<Item>
{
public int Quantity { get; set; }
public int ItemDescriptionID { get; set; }
public string Notes { get; set; }
}
public class ItemDescription
{
public int ItemDescriptionID { get; set; }
public string Description { get; set; }
}
BindingProxy.CS
using System.Windows;
namespace WpfApp2
{
/// <summary>
/// http://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/
/// </summary>
public class BindingProxy : Freezable
{
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
public object Data
{
get { return GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
}
DataGridHelper.CS
using System;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
namespace WpfApp2
{
public static class DataGridHelper
{
public static T GetVisualChild<T>(Visual parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
public static DataGridCell GetCell(this DataGrid grid, DataGridRow row, int column)
{
if (row != null)
{
DataGridCellsPresenter presenter = GetVisualChild<DataGridCellsPresenter>(row);
if (presenter == null)
{
grid.ScrollIntoView(row, grid.Columns[column]);
presenter = GetVisualChild<DataGridCellsPresenter>(row);
}
DataGridCell cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(column);
return cell;
}
return null;
}
public static DataGridCell GetCell(this DataGrid grid, int row, int column)
{
DataGridRow rowContainer = grid.GetRow(row);
return grid.GetCell(rowContainer, column);
}
}
}
EditableObject.CS
using System;
using System.ComponentModel;
namespace WpfApp2
{
public abstract class EditableObject<T> : IEditableObject
{
private T Cache { get; set; }
private object CurrentModel
{
get { return this; }
}
public RelayCommand CancelEditCommand
{
get { return new RelayCommand(CancelEdit); }
}
#region IEditableObject Members
public void BeginEdit()
{
Cache = Activator.CreateInstance<T>();
//Set Properties of Cache
foreach (var info in CurrentModel.GetType().GetProperties())
{
if (!info.CanRead || !info.CanWrite) continue;
var oldValue = info.GetValue(CurrentModel, null);
Cache.GetType().GetProperty(info.Name).SetValue(Cache, oldValue, null);
}
}
public virtual void EndEdit()
{
Cache = default(T);
}
public void CancelEdit()
{
foreach (var info in CurrentModel.GetType().GetProperties())
{
if (!info.CanRead || !info.CanWrite) continue;
var oldValue = info.GetValue(Cache, null);
CurrentModel.GetType().GetProperty(info.Name).SetValue(CurrentModel, oldValue, null);
}
}
#endregion
}
}
RelayCommand.CS
using System;
using System.Windows.Input;
namespace WpfApp2
{
/// <summary>
/// A command whose sole purpose is to relay its functionality to other objects by invoking delegates.
/// The default return value for the CanExecute method is 'true'.
/// <see cref="RaiseCanExecuteChanged"/> needs to be called whenever
/// <see cref="CanExecute"/> is expected to return a different value.
/// </summary>
public class RelayCommand : ICommand
{
#region Private members
/// <summary>
/// Creates a new command that can always execute.
/// </summary>
private readonly Action execute;
/// <summary>
/// True if command is executing, false otherwise
/// </summary>
private readonly Func<bool> canExecute;
#endregion
/// <summary>
/// Initializes a new instance of <see cref="RelayCommand"/> that can always execute.
/// </summary>
/// <param name="execute">The execution logic.</param>
public RelayCommand(Action execute) : this(execute, canExecute: null) { }
/// <summary>
/// Initializes a new instance of <see cref="RelayCommand"/>.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
public RelayCommand(Action execute, Func<bool> canExecute)
{
this.execute = execute ?? throw new ArgumentNullException("execute");
this.canExecute = canExecute;
}
/// <summary>
/// Raised when RaiseCanExecuteChanged is called.
/// </summary>
public event EventHandler CanExecuteChanged;
/// <summary>
/// Determines whether this <see cref="RelayCommand"/> can execute in its current state.
/// </summary>
/// <param name="parameter">
/// Data used by the command. If the command does not require data to be passed, this object can be set to null.
/// </param>
/// <returns>True if this command can be executed; otherwise, false.</returns>
public bool CanExecute(object parameter) => canExecute == null ? true : canExecute();
/// <summary>
/// Executes the <see cref="RelayCommand"/> on the current command target.
/// </summary>
/// <param name="parameter">
/// Data used by the command. If the command does not require data to be passed, this object can be set to null.
/// </param>
public void Execute(object parameter)
{
execute();
}
/// <summary>
/// Method used to raise the <see cref="CanExecuteChanged"/> event
/// to indicate that the return value of the <see cref="CanExecute"/>
/// method has changed.
/// </summary>
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
}
Here's the full solution.
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
namespace MyNamespace
{
/// <summary>
/// Creates the correct behavior when tabbing out of a new row in a DataGrid.
/// https://peplowdown.wordpress.com/2012/07/19/wpf-datagrid-moves-input-focus-and-selection-to-the-wrong-cell-when-pressing-tab/
/// </summary><remarks>
/// You’d expect that when you hit tab in the last cell the WPF data grid it would create a new row and put your focus in the first cell of that row.
/// It doesn’t; depending on how you have KeboardNavigation.TabNavigation set it’ll jump off somewhere you don’t expect, like the next control
/// or back to the first item in the grid. This behavior class solves that problem.
/// </remarks>
public class NewLineOnTabBehavior : Behavior<DataGrid>
{
private bool _monitorForTab;
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.BeginningEdit += _EditStarting;
AssociatedObject.CellEditEnding += _CellEnitEnding;
AssociatedObject.PreviewKeyDown += _KeyDown;
}
private void _EditStarting(object sender, DataGridBeginningEditEventArgs e)
{
if (e.Column.DisplayIndex == AssociatedObject.Columns.Count - 1)
_monitorForTab = true;
}
private void _CellEnitEnding(object sender, DataGridCellEditEndingEventArgs e)
{
_monitorForTab = false;
}
private void _KeyDown(object sender, KeyEventArgs e)
{
if (_monitorForTab && e.Key == Key.Tab)
{
AssociatedObject.CommitEdit(DataGridEditingUnit.Row, false);
}
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.BeginningEdit -= _EditStarting;
AssociatedObject.CellEditEnding -= _CellEnitEnding;
AssociatedObject.PreviewKeyDown -= _KeyDown;
_monitorForTab = false;
}
}
}
And in the XAML for the DataGrid:
<i:Interaction.Behaviors>
<local:NewLineOnTabBehavior />
</i:Interaction.Behaviors>
Add the following namespaces to the top-level XAML attributes:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="clr-namespace:MyNamespace"
This solution doesn't play nice with the usual validation techniques, so I used a RowValidator to validate each row.
using System.Windows.Controls;
using System.Windows.Data;
using System.Globalization;
namespace MyNamespace
{
public class RowValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
T_Asset item = (value as BindingGroup).Items[0] as T_Asset;
item.ValidateModel();
if (!item.HasErrors) return ValidationResult.ValidResult;
return new ValidationResult(false, item.ErrorString);
}
}
}
T_Asset
implements the INotifyDataErrorInfo
interface.
And then in the XAML for the DataGrid:
<DataGrid.RowValidationRules>
<local:RowValidationRule ValidationStep="CommittedValue" />
</DataGrid.RowValidationRules>