Нажатие клавиши Tab в последнем столбце последней строки в DataGrid должно установить фокус на первый столбец новой строки - PullRequest
0 голосов
/ 21 ноября 2018

У меня есть 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 довольно многословен.Обратите внимание, что я использую Fody.PropertyChanged в качестве 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 голосов
/ 29 ноября 2018

Вот полное решение.

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

И в XAML для DataGrid:

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

<DataGrid.RowValidationRules>
    <local:RowValidationRule ValidationStep="CommittedValue" />
</DataGrid.RowValidationRules>
0 голосов
/ 21 ноября 2018

Видели ли вы подход, описанный здесь: https://peplowdown.wordpress.com/2012/07/19/wpf-datagrid-moves-input-focus-and-selection-to-the-wrong-cell-when-pressing-tab/

По моему опыту, как только вы начнете изменять поведение редактирования строк, табуляции и т. Д., Вы можете найти регистр граней после регистра граней.Удачи.

...