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

时间:2018-11-20 23:40:13

标签: c# wpf wpfdatagrid

我有一个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);
        }
    }
}

2 个答案:

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