Pressing Tab Key in last column of last row in DataGrid should set focus to first column of new row
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);
}
}
}
c# wpf wpfdatagrid
add a comment |
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);
}
}
}
c# wpf wpfdatagrid
Not what you are asking. BUT. It's rarely a good idea to let users edit directly in a datagrid. It's only suitable for very simple scenarios where you don't need validation. I would usually make datagrids read only and edit in a separate panel where I can ensure the data is valid before they can possibly commit it.
– Andy
Nov 21 at 9:46
@Andy: A fair point. But this particular use case requires putting in a significant amount of data in a very short time, and validation is not critical (it's essentially a list of simple items). It really does need to work like I described.
– Robert Harvey♦
Nov 21 at 15:48
add a comment |
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);
}
}
}
c# wpf wpfdatagrid
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);
}
}
}
c# wpf wpfdatagrid
c# wpf wpfdatagrid
edited Nov 21 at 0:13
asked Nov 20 at 23:40
Robert Harvey♦
147k33271415
147k33271415
Not what you are asking. BUT. It's rarely a good idea to let users edit directly in a datagrid. It's only suitable for very simple scenarios where you don't need validation. I would usually make datagrids read only and edit in a separate panel where I can ensure the data is valid before they can possibly commit it.
– Andy
Nov 21 at 9:46
@Andy: A fair point. But this particular use case requires putting in a significant amount of data in a very short time, and validation is not critical (it's essentially a list of simple items). It really does need to work like I described.
– Robert Harvey♦
Nov 21 at 15:48
add a comment |
Not what you are asking. BUT. It's rarely a good idea to let users edit directly in a datagrid. It's only suitable for very simple scenarios where you don't need validation. I would usually make datagrids read only and edit in a separate panel where I can ensure the data is valid before they can possibly commit it.
– Andy
Nov 21 at 9:46
@Andy: A fair point. But this particular use case requires putting in a significant amount of data in a very short time, and validation is not critical (it's essentially a list of simple items). It really does need to work like I described.
– Robert Harvey♦
Nov 21 at 15:48
Not what you are asking. BUT. It's rarely a good idea to let users edit directly in a datagrid. It's only suitable for very simple scenarios where you don't need validation. I would usually make datagrids read only and edit in a separate panel where I can ensure the data is valid before they can possibly commit it.
– Andy
Nov 21 at 9:46
Not what you are asking. BUT. It's rarely a good idea to let users edit directly in a datagrid. It's only suitable for very simple scenarios where you don't need validation. I would usually make datagrids read only and edit in a separate panel where I can ensure the data is valid before they can possibly commit it.
– Andy
Nov 21 at 9:46
@Andy: A fair point. But this particular use case requires putting in a significant amount of data in a very short time, and validation is not critical (it's essentially a list of simple items). It really does need to work like I described.
– Robert Harvey♦
Nov 21 at 15:48
@Andy: A fair point. But this particular use case requires putting in a significant amount of data in a very short time, and validation is not critical (it's essentially a list of simple items). It really does need to work like I described.
– Robert Harvey♦
Nov 21 at 15:48
add a comment |
2 Answers
2
active
oldest
votes
Have you seen the approach described here:
https://peplowdown.wordpress.com/2012/07/19/wpf-datagrid-moves-input-focus-and-selection-to-the-wrong-cell-when-pressing-tab/
In my experience, once you start changing the behaviour of roweditending and tabbing etc you can find edge case after edge case.
Good luck.
How did you find this? I have very good Google-fu, but I googled for ages and never found this.
– Robert Harvey♦
Nov 21 at 15:48
I think I just searched on wpf tab to new row. But I already knew this was likely to be a challenge.
– Andy
Nov 21 at 16:14
Fantastic, it works! Do you want me to edit the full solution into your answer, or post a new answer?
– Robert Harvey♦
Nov 21 at 17:16
add a comment |
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>
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53403251%2fpressing-tab-key-in-last-column-of-last-row-in-datagrid-should-set-focus-to-firs%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
Have you seen the approach described here:
https://peplowdown.wordpress.com/2012/07/19/wpf-datagrid-moves-input-focus-and-selection-to-the-wrong-cell-when-pressing-tab/
In my experience, once you start changing the behaviour of roweditending and tabbing etc you can find edge case after edge case.
Good luck.
How did you find this? I have very good Google-fu, but I googled for ages and never found this.
– Robert Harvey♦
Nov 21 at 15:48
I think I just searched on wpf tab to new row. But I already knew this was likely to be a challenge.
– Andy
Nov 21 at 16:14
Fantastic, it works! Do you want me to edit the full solution into your answer, or post a new answer?
– Robert Harvey♦
Nov 21 at 17:16
add a comment |
Have you seen the approach described here:
https://peplowdown.wordpress.com/2012/07/19/wpf-datagrid-moves-input-focus-and-selection-to-the-wrong-cell-when-pressing-tab/
In my experience, once you start changing the behaviour of roweditending and tabbing etc you can find edge case after edge case.
Good luck.
How did you find this? I have very good Google-fu, but I googled for ages and never found this.
– Robert Harvey♦
Nov 21 at 15:48
I think I just searched on wpf tab to new row. But I already knew this was likely to be a challenge.
– Andy
Nov 21 at 16:14
Fantastic, it works! Do you want me to edit the full solution into your answer, or post a new answer?
– Robert Harvey♦
Nov 21 at 17:16
add a comment |
Have you seen the approach described here:
https://peplowdown.wordpress.com/2012/07/19/wpf-datagrid-moves-input-focus-and-selection-to-the-wrong-cell-when-pressing-tab/
In my experience, once you start changing the behaviour of roweditending and tabbing etc you can find edge case after edge case.
Good luck.
Have you seen the approach described here:
https://peplowdown.wordpress.com/2012/07/19/wpf-datagrid-moves-input-focus-and-selection-to-the-wrong-cell-when-pressing-tab/
In my experience, once you start changing the behaviour of roweditending and tabbing etc you can find edge case after edge case.
Good luck.
answered Nov 21 at 9:48
Andy
2,9051106
2,9051106
How did you find this? I have very good Google-fu, but I googled for ages and never found this.
– Robert Harvey♦
Nov 21 at 15:48
I think I just searched on wpf tab to new row. But I already knew this was likely to be a challenge.
– Andy
Nov 21 at 16:14
Fantastic, it works! Do you want me to edit the full solution into your answer, or post a new answer?
– Robert Harvey♦
Nov 21 at 17:16
add a comment |
How did you find this? I have very good Google-fu, but I googled for ages and never found this.
– Robert Harvey♦
Nov 21 at 15:48
I think I just searched on wpf tab to new row. But I already knew this was likely to be a challenge.
– Andy
Nov 21 at 16:14
Fantastic, it works! Do you want me to edit the full solution into your answer, or post a new answer?
– Robert Harvey♦
Nov 21 at 17:16
How did you find this? I have very good Google-fu, but I googled for ages and never found this.
– Robert Harvey♦
Nov 21 at 15:48
How did you find this? I have very good Google-fu, but I googled for ages and never found this.
– Robert Harvey♦
Nov 21 at 15:48
I think I just searched on wpf tab to new row. But I already knew this was likely to be a challenge.
– Andy
Nov 21 at 16:14
I think I just searched on wpf tab to new row. But I already knew this was likely to be a challenge.
– Andy
Nov 21 at 16:14
Fantastic, it works! Do you want me to edit the full solution into your answer, or post a new answer?
– Robert Harvey♦
Nov 21 at 17:16
Fantastic, it works! Do you want me to edit the full solution into your answer, or post a new answer?
– Robert Harvey♦
Nov 21 at 17:16
add a comment |
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>
add a comment |
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>
add a comment |
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>
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>
answered Nov 28 at 21:53
Robert Harvey♦
147k33271415
147k33271415
add a comment |
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53403251%2fpressing-tab-key-in-last-column-of-last-row-in-datagrid-should-set-focus-to-firs%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Not what you are asking. BUT. It's rarely a good idea to let users edit directly in a datagrid. It's only suitable for very simple scenarios where you don't need validation. I would usually make datagrids read only and edit in a separate panel where I can ensure the data is valid before they can possibly commit it.
– Andy
Nov 21 at 9:46
@Andy: A fair point. But this particular use case requires putting in a significant amount of data in a very short time, and validation is not critical (it's essentially a list of simple items). It really does need to work like I described.
– Robert Harvey♦
Nov 21 at 15:48