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