我有一个DataGrid
,可编辑ObservableCollection
个对象中的一个IEditableObject
。 DataGrid设置为CanUserAddRows="True"
,因此存在用于添加新记录的空白行。一切正常,只有一个例外。
其中包含数据的所有行的默认选项卡行为是,当从当前行的最后一列中跳出时,将移至下一行的第一列,这正是我想要的行为。但是,如果下一行是新行(将包含下一条新记录的行),这不是我得到的行为。该选项卡不会将焦点移到DataGrid中第一行的第一列。
我当前尝试将行为更改为我想要的样子:
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();
}
}
}
即使实际上已调用cell.SetFocus()
,也没有将焦点设置到我想要的位置。
我当前的工作原理是:row.Focusable
返回false
,可能是因为该行还不“足够”存在(我已经知道该行目前还不包含数据) ,因此所需的单元格无法获得焦点,因为该行无法获得焦点。
有什么想法吗?
下面是我最喜欢的MCVE。 WPF相当冗长。请注意,我将see pic of vs code用作自己的INotifyPropertyChanged
实现。
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);
}
}
}
答案 0 :(得分:1)
您是否看到了此处描述的方法: https://peplowdown.wordpress.com/2012/07/19/wpf-datagrid-moves-input-focus-and-selection-to-the-wrong-cell-when-pressing-tab/
以我的经验,一旦开始更改行编辑和制表等行为,就可以在出现小写字母后找到小写字母。 祝你好运。
答案 1 :(得分:0)
这是完整的解决方案。
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;
}
}
}
在DataGrid的XAML中:
<i:Interaction.Behaviors>
<local:NewLineOnTabBehavior />
</i:Interaction.Behaviors>
将以下名称空间添加到顶级XAML属性:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="clr-namespace:MyNamespace"
此解决方案不能与常用的验证技术配合使用,因此我使用RowValidator来验证每一行。
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
实现了INotifyDataErrorInfo
接口。
然后在DataGrid的XAML中:
<DataGrid.RowValidationRules>
<local:RowValidationRule ValidationStep="CommittedValue" />
</DataGrid.RowValidationRules>