WPF DataGrid:解决排序、滚动到视图、刷新和焦点问题





5.00/5 (4投票s)
让用户在 DataGrid 中上下移动某些行本应很容易实现,但实际上却是一场噩梦。
引言
我正在开发一个 WPF 应用程序,使用 WPF DataGrid
显示具有排名属性的项,并按排名对这些项进行排序。用户界面应该允许用户选择一些行(项),然后通过单击按钮将它们向上或向下移动几行。
当单击“下移”时,排名为 3-5 的第 3-5 项向下移动 20 行,获得新的排名 23-25。第 6-25 项向上移动 3 个排名,为第 3-5 项腾出空间。网格会自动按排名对项进行排序,滚动并显示其在新位置的 3 行选定项。
我认为在“下移”按钮的处理程序中实现这一点非常简单
- 检测到哪些行(项)被选中。
- 遍历它们并将它们的
Rank
增加20
。 - 遍历需要移开的行并调整它们的
Rank
。 - 刷新
DataGrid
。
不幸的是,刷新 DataGrid
会导致 DataGrid
忘记哪些行被选中。这给用户带来了严重的问题,如果他需要多次单击“下移”按钮才能将选定的行移到正确的位置。
第一种方法:记住选定的行,刷新 DataGrid,重新选择行
听起来足够简单,对吧?不幸的是,事实证明,使用 WPF DataGrid
选择行并将其带入视图是非常复杂的,原因是由于虚拟化,只有当前可见的项才分配了实际的 DataRow
和 DataGridCell
,但项是否被选中的信息存储在这些类中。因此,当一个项从可见部分消失时,将其带回视图并再次标记为选中状态就相当复杂了。
幸运的是,我找到了这篇 Technet 文章 WPF:以编程方式选择和聚焦 DataGrid 中的行或单元格
不幸的是,所需的代码很复杂且速度很慢。它如下所示(代码请参见之前的链接)
- 遍历应该被选中的每一项。
- 使用
DataGrid.ItemContainerGenerator.ContainerFromIndex(itemIndex)
来确定该行是否在视图中。 - 如果不在,则使用
TracksDataGrid.ScrollIntoView(item)
,然后再次使用ContainerFromIndex(itemIndex)
。 - 希望现在能找到一个
DataRow
。给它Focus
。
现在,如果您认为给视图中的 DataGridRow
设置 Focus
很容易,那就错了。它涉及以下步骤(代码请参见之前的链接)
- 在
DataGridRow
中找到保存DataGridCells
的DataGridCellsPresenter
。如果您认为这无关紧要,那您又错了。您需要遍历视觉树来查找DataGridCellsPresenter
。 - 如果找不到,则它不在视觉树中,您必须自己应用
DataRow
模板,然后再次重复步骤 1,这次会成功。 - 使用
presenter.ItemContainerGenerator.ContainerFromIndex(0)
查找第一列。如果找不到任何内容,则它不在视觉树中,您必须将列滚动到视图中:dataGrid.ScrollIntoView(rowContainer, dataGrid.Columns[0])
。 - 现在,并且只有现在,您才能调用
DataGridCell.Focus()
。
然后继续遍历每一行。
这不仅听起来很复杂,而且代码执行速度也很慢。在我顶级的性能工作站上,它几乎需要一秒钟。现在想象一下用户多次单击按钮(如果他每次只增加 1 个排名,那么 10 次很容易)。但 10 秒的延迟是完全不可接受的。所以我不得不寻找另一个解决方案。
最终方法:使用 OneWay 绑定并避免调用 Refresh()
由于用户无法直接在 datagrid
中更改任何数据,因此我将其设为只读,并使用了默认绑定,即 OneTime
,这意味着数据在分配给 DataGrid
的 DataSource
时只写入一次。我将其绑定更改为 OneWay
,每次数据更改时都会将新值复制到 DataGrid
。要实现此目的,我的项必须实现 INotifyPropertyChanged
public class Item: INotifyPropertyChanged {
public string Name { get; set; }
public int Rank {
get {
return rank;
}
set {
if (rank!=value) {
rank = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Rank)));
}
}
}
int rank;
public event PropertyChangedEventHandler? PropertyChanged;
}
每次 Rank
更改时,都会调用 PropertyChanged
事件,DataGrid
会订阅该事件。
DataGrid
现在显示了带有新 Rank
值的行,但没有排序。经过一番谷歌搜索,我发现需要像这样激活实时排序
var itemsViewSource = ((CollectionViewSource)this.FindResource("ItemsViewSource"));
itemsViewSource.Source = items;
itemsViewSource.IsLiveSortingRequested = true;
ItemsDataGrid.Columns[0].SortDirection = ListSortDirection.Ascending;
itemsViewSource.View.SortDescriptions.Add
(new SortDescription("Rank", ListSortDirection.Ascending));
通过此更改,单击“下移”按钮的执行速度大大加快,并且 DataGrid
进行了正确的排序,但是:选定的行移出了视图,无法再看到。通过添加 DataGrid.ScrollIntoView(DataGrid.SelectedItem)
应该很容易解决。唉,什么也没发生,DataGrid
没有滚动。
改进 1:让 ScrollIntoView() 生效
经过更多的谷歌搜索,我得出结论,当我在“下移”按钮的点击事件中调用 ScrollIntoView()
时,它根本不起作用,因为那时 DataGrid
还没有排序。所以我不得不延迟调用 ScrollIntoView()
,但是如何做到呢?我首先考虑使用计时器,但后来我找到了一个更好的解决方案:使用 DataGrid.LayoutUpdated
事件
bool isMoveDownNeeded;
bool isMoveUpNeeded;
private void ItemsDataGrid_LayoutUpdated(object? sender, EventArgs e) {
if (isMoveUpNeeded) {
isMoveUpNeeded = false;
ItemsDataGrid.ScrollIntoView(ItemsDataGrid.SelectedItem);
}
if (isMoveDownNeeded) {
isMoveDownNeeded = false;
ItemsDataGrid.ScrollIntoView
(ItemsDataGrid.SelectedItems[ItemsDataGrid.SelectedItems.Count-1]);
}
}
然后,单击“下移”按钮的执行速度大大加快,DataGrid
进行了正确的排序,并且 DataGrid
滚动到了选定的行。
改进 2:显示选定的行,就像它们具有焦点一样
当用户用鼠标选择一些行时,它们会显示为深蓝色背景。但是一旦单击了“下移”按钮,BackGround
就变成了灰色,在我的显示器上很难看清。如第一种方法所述,可以从代码隐藏给行设置焦点,但这太复杂而且速度太慢。幸运的是,有一个更简单的解决方案
<DataGrid.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Blue"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}"
Color="Blue"/>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="White"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}"
Color="White"/>
</DataGrid.Resources>
这里的技巧就是让行在刚被选中时(InactiveSelectionHighlightBrush
)和被选中并具有焦点时(HighlightBrush
)的外观相同。
深入了解 DataGrid 格式设置
如果您读到这里,那么可以肯定地说,您对 DataGrid
真正感兴趣。在这种情况下,我想向您推荐我的另一篇文章,关于 DataGrid
格式设置,这本身就是一门晦涩的艺术:使用绑定进行 WPF DataGrid 格式设置指南。
Using the Code
示例应用程序并不需要太多代码,但我花了很长时间才使其正常工作。通过研究它,我希望您可以节省一些时间。
<Window x:Class="TryDataGridScrollIntoView.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TryDataGridScrollIntoView"
mc:Ignorable="d"
Title="Move" Height="450" Width="400">
<Window.Resources>
<CollectionViewSource x:Key="ItemsViewSource" CollectionViewType="ListCollectionView"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<DataGrid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" x:Name="ItemsDataGrid"
DataContext="{StaticResource ItemsViewSource}"
ItemsSource="{Binding}" AutoGenerateColumns="False"
EnableRowVirtualization="True" RowDetailsVisibilityMode="Collapsed"
EnableColumnVirtualization="False"
AllowDrop="False" CanUserAddRows="False" CanUserDeleteRows="False"
CanUserResizeRows="False">
<DataGrid.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Blue"/>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}"
Color="Blue"/>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="White"/>
<SolidColorBrush
x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="White"/>
<!--<SolidColorBrush
x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}"
Color="{DynamicResource {x:Static SystemColors.HighlightColor}}"/>-->
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=Rank, StringFormat=N0, Mode=OneWay}"
Header="Rank"
IsReadOnly="True" Width="45"/>
<DataGridTextColumn Binding="{Binding Path=Name}" Header="Name" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="1" Grid.Column="0" x:Name="MoveDownButton" Content="Move _Down"/>
<Button Grid.Row="1" Grid.Column="1" x:Name="MoveUpButton" Content="Move Up"/>
</Grid>
</Window>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
namespace TryDataGridScrollIntoView {
public partial class MainWindow : Window{
public MainWindow(){
InitializeComponent();
MoveDownButton.Click += MoveDownButton_Click;
MoveUpButton.Click += MoveUpButton_Click;
ItemsDataGrid.LayoutUpdated += ItemsDataGrid_LayoutUpdated;
var items = new List<Item>();
for (int i = 0; i < 100; i++) {
items.Add(new Item { Name = $"Item {i}", Rank = i });
}
var itemsViewSource = ((CollectionViewSource)this.FindResource("ItemsViewSource"));
itemsViewSource.Source = items;
itemsViewSource.IsLiveSortingRequested = true;
ItemsDataGrid.Columns[0].SortDirection = ListSortDirection.Ascending;
itemsViewSource.View.SortDescriptions.Add(new SortDescription
("Rank", ListSortDirection.Ascending));
}
const int rowsPerPage = 20;
private void MoveUpButton_Click(object sender, RoutedEventArgs e) {
var firstSelectedTrack = ItemsDataGrid.SelectedIndex;
if (firstSelectedTrack<=0) return;//cannot move up any further
var selectedTracksCount = ItemsDataGrid.SelectedItems.Count;
int firstMoveTrack;
int moveTracksCount;
firstMoveTrack = Math.Max(0, firstSelectedTrack - rowsPerPage);
moveTracksCount = Math.Min(rowsPerPage, firstSelectedTrack - firstMoveTrack);
isMoveUpNeeded = true;
moveTracksDown(firstMoveTrack, moveTracksCount, selectedTracksCount);
moveTracksUp(firstSelectedTrack, selectedTracksCount, moveTracksCount);
}
private void MoveDownButton_Click(object sender, RoutedEventArgs e) {
var firstSelectedTrack = ItemsDataGrid.SelectedIndex;
var selectedTracksCount = ItemsDataGrid.SelectedItems.Count;
var lastSelectedTrack = firstSelectedTrack + selectedTracksCount - 1;
if (lastSelectedTrack + 1 >=
ItemsDataGrid.Items.Count) return;//cannot move down any further
int lastMoveTrack;
int moveTracksCount;
lastMoveTrack = Math.Min(ItemsDataGrid.Items.Count-1, lastSelectedTrack + rowsPerPage);
moveTracksCount = Math.Min(rowsPerPage, lastMoveTrack - lastSelectedTrack);
isMoveDownNeeded = true;
moveTracksUp(lastMoveTrack - moveTracksCount + 1, moveTracksCount, selectedTracksCount);
moveTracksDown(firstSelectedTrack, selectedTracksCount, moveTracksCount);
ItemsDataGrid.ScrollIntoView(ItemsDataGrid.SelectedItem); //doesn't work :-(
}
private void moveTracksDown(int firstTrack, int tracksCount, int offset) {
for (int itemIndex = firstTrack; itemIndex<firstTrack+tracksCount; itemIndex++) {
Item item = (Item)ItemsDataGrid.Items[itemIndex]!;
item.Rank += offset;
}
}
private void moveTracksUp(int firstTrack, int tracksCount, int offset) {
for (int itemIndex = firstTrack; itemIndex<firstTrack+tracksCount; itemIndex++) {
Item item = (Item)ItemsDataGrid.Items[itemIndex]!;
item.Rank -= offset;
}
}
bool isMoveDownNeeded;
bool isMoveUpNeeded;
private void ItemsDataGrid_LayoutUpdated(object? sender, EventArgs e) {
if (isMoveUpNeeded) {
isMoveUpNeeded = false;
ItemsDataGrid.ScrollIntoView(ItemsDataGrid.SelectedItem);
}
if (isMoveDownNeeded) {
isMoveDownNeeded = false;
ItemsDataGrid.ScrollIntoView(ItemsDataGrid.SelectedItems
[ItemsDataGrid.SelectedItems.Count-1]);
}
}
}
public class Item: INotifyPropertyChanged {
public string Name { get; set; }
public int Rank {
get {
return rank;
}
set {
if (rank!=value) {
rank = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Rank)));
}
}
}
int rank;
public event PropertyChangedEventHandler? PropertyChanged;
}
}
历史
- 2021 年 2 月 5 日:初始版本