目录
出人意料的是,从代码中更改某些WPF DataGrid数据后遇到许多挑战,这些代码要求对行进行新的排序并滚动DataGrid以显示最初选择的行。本文重点介绍遇到的问题以及如何解决。最后是完整的示例代码。
介绍
我正在使用WPF编写一个WPF应用程序,该WPF DataGrid应用程序显示具有rank属性的项目,这些项目按rank排序。用户界面应允许用户选择一些行(项目),并通过单击按钮将它们上下移动几行:
单击“向下移动”时,Rank 3-5的Item 3-5下移20行并获得新的Rank 23-25。Item 6-25上升了3 rank,为Item 3-5腾出了空间。网格自动按rank对Item进行排序,滚动并在网格中的新位置显示3个选定的行。
我认为这在“下移按钮”的处理程序中很容易实现:
- 检测选择了哪些行(Item)。
- 循环遍历它们,并将它们的Rank增加20。
- 在需要移开的行上循环并调整它们的行Rank。
- 刷新DataGrid。
不幸的是,刷新DataGrid使DataGrid忘记了选择了哪些行。如果用户需要多次按下“下移”按钮以将选定的行移至正确的位置,则会给用户带来严重的问题。
第一种方法:记住选定的行,刷新DataGrid,再次选择行
听起来很简单,对吧?不幸的是,事实证明选择行并将它们显示在WPF DataGrid中非常复杂,原因是由于虚拟化,只有当前可见的Item才实际分配了DataRow和DataGridCell,但是Item被选中时的信息存储在这些类中。因此,如果某个Item从可见部分消失,则将其重新显示并再次标记为选中状态相当复杂。
幸运的是,我发现了这篇Technet文章WPF:以编程方式选择和聚焦DataGrid中的行或单元格
不幸的是,所需的代码既复杂又缓慢。就像这样(有关代码,请参见上一个链接):
- 循环浏览应选择的每个Item。
- 使用DataGrid.ItemContainerGenerator.ContainerFromIndex(itemIndex)以确定该行是否可见。
- 如果不是,请使用TracksDataGrid.ScrollIntoView(item),然后再次使用ContainerFromIndex(itemIndex)。
- 希望DataRow现在可以找到一个。给它Focus。
现在,如果你想给DataGridRow一个Focus,这DataGridRow是可见的,是很容易的,那你就错了。它涉及以下步骤(有关代码,请参见上一个链接):
- 在保存DataGridCells的DataGridRow中找到DataGridCellsPresenter。如果您认为这是微不足道的,那么您会再次犯错。您需要遍历可视化树以找到DataGridCellsPresenter。
- 如果找不到它,则它不在可视化树中,您必须自己应用DataRow模板,然后再次重复步骤1,这一次成功。
- 使用presenter.ItemContainerGenerator.ContainerFromIndex(0)找到的第一列。如果未找到任何内容,则它不在可视化树中,您必须将dataGrid.ScrollIntoView(rowContainer, dataGrid.Columns[0])列滚动到视图中。
- 现在,只有现在您才能调用DataGridCell.Focus()。
现在继续遍历每一行。
这不仅听起来很复杂,而且代码执行也很慢。在我的顶级工作站上,它花费了将近一秒钟。现在,假设用户单击几次按钮(10次是很容易的,如果他将10次增加1)。但是10秒的延迟根本不可接受。因此,我不得不寻找另一种解决方案。
最终方法:使用OneWay绑定,避免调用Refresh()
由于用户不能在datagrid中直接改变任何数据,我将其设置为只读并使用默认绑定,默认绑定是OneTime,这意味着数据被分配给DataGrid的DataSource时,数据被分写入一次。我将绑定更改为OneWay,每次DataGrid数据更改时,该绑定都会复制新值。为此,我的item必须实现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:将选定的行显示为具有焦点
当用户用鼠标选择一些行时,它们以深蓝色背景显示。但是,一旦单击该Move Down按钮,该按钮将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进行格式化的指南。
使用代码
该示例应用程序不需要太多代码,但是我花了很长时间使它工作,通过研究它,我希望您可以节省一些时间。
<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;
}
}
https://www.codeproject.com/Articles/5294035/WPF-DataGrid-Solving-Sorting-ScrollIntoView-Refres