代码之家  ›  专栏  ›  技术社区  ›  Robert Harvey

在DataGrid最后一行的最后一列中按Tab键应将焦点设置为新行的第一列。

  •  0
  • Robert Harvey  · 技术社区  · 7 年前

    我有一个 DataGrid 编辑一个 ObservableCollection 属于 IEditableObject 物体。数据报设置为 CanUserAddRows="True" 以便出现添加新记录的空白行。每件事都很完美,但有一个显著的例外。

    所有行中都有数据的默认选项卡行为是,在当前行的最后一列中制表时移动到下一行的第一列,这正是我想要的行为。但是,这不是我得到的行为如果下一行是新行,将包含下一条新记录的行。选项卡不会移动到新行中的第一列,而是将焦点移动到数据报中第一行的第一列。

    我当前试图将行为更改为我想要的行为,如下所示:

    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 ,可能是因为行尚未“完全”存在(我已经知道此时它还不包含数据),所以所需的单元格无法获得焦点,因为行无法获得焦点。

    有什么想法吗?


    我能收集到的最接近MCV的东西在下面。WPF相当冗长。注意我正在使用 Fody.PropertyChanged 作为我的 INotifyPropertyChanged 实施。

    主窗口

    <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>
    

    主窗口.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();
                    }
                }
            }
        }
    }
    

    主窗口视图模型.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,项描述.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; }
    }
    

    绑定程序

    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));
        }
    }
    

    数据报帮助程序.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);
            }
        }
    }
    

    可编辑对象.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
        }
    }
    

    中继命令

    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);
            }
        }
    }
    
    2 回复  |  直到 7 年前
        1
  •  1
  •   Andy    7 年前

    您看到这里描述的方法了吗: https://peplowdown.wordpress.com/2012/07/19/wpf-datagrid-moves-input-focus-and-selection-to-the-wrong-cell-when-pressing-tab/

    根据我的经验,一旦你开始改变roweditending和tabbing等的行为,你可以找到一个接一个的边缘案例。 祝你好运。

        2
  •  0
  •   Robert Harvey    7 年前

    这是完整的解决方案。

    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 接口。

    然后在XAML中查找数据报:

    <DataGrid.RowValidationRules>
        <local:RowValidationRule ValidationStep="CommittedValue" />
    </DataGrid.RowValidationRules>