65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2021年2月5日

CPOL

5分钟阅读

viewsIcon

21390

让用户在 DataGrid 中上下移动某些行本应很容易实现,但实际上却是一场噩梦。

引言

我正在开发一个 WPF 应用程序,使用 WPF DataGrid 显示具有排名属性的项,并按排名对这些项进行排序。用户界面应该允许用户选择一些行(项),然后通过单击按钮将它们向上或向下移动几行。

当单击“下移”时,排名为 3-5 的第 3-5 项向下移动 20 行,获得新的排名 23-25。第 6-25 项向上移动 3 个排名,为第 3-5 项腾出空间。网格会自动按排名对项进行排序,滚动并显示其在新位置的 3 行选定项。

我认为在“下移”按钮的处理程序中实现这一点非常简单

  1. 检测到哪些行(项)被选中。
  2. 遍历它们并将它们的 Rank 增加 20
  3. 遍历需要移开的行并调整它们的 Rank
  4. 刷新 DataGrid

不幸的是,刷新 DataGrid 会导致 DataGrid 忘记哪些行被选中。这给用户带来了严重的问题,如果他需要多次单击“下移”按钮才能将选定的行移到正确的位置。

第一种方法:记住选定的行,刷新 DataGrid,重新选择行

听起来足够简单,对吧?不幸的是,事实证明,使用 WPF DataGrid 选择行并将其带入视图是非常复杂的,原因是由于虚拟化,只有当前可见的项才分配了实际的 DataRowDataGridCell,但项是否被选中的信息存储在这些类中。因此,当一个项从可见部分消失时,将其带回视图并再次标记为选中状态就相当复杂了。

幸运的是,我找到了这篇 Technet 文章 WPF:以编程方式选择和聚焦 DataGrid 中的行或单元格

不幸的是,所需的代码很复杂且速度很慢。它如下所示(代码请参见之前的链接)

  1. 遍历应该被选中的每一项。
  2. 使用 DataGrid.ItemContainerGenerator.ContainerFromIndex(itemIndex) 来确定该行是否在视图中。
  3. 如果不在,则使用 TracksDataGrid.ScrollIntoView(item),然后再次使用 ContainerFromIndex(itemIndex)
  4. 希望现在能找到一个 DataRow。给它 Focus

现在,如果您认为给视图中的 DataGridRow 设置 Focus 很容易,那就错了。它涉及以下步骤(代码请参见之前的链接)

  1. DataGridRow 中找到保存 DataGridCellsDataGridCellsPresenter。如果您认为这无关紧要,那您又错了。您需要遍历视觉树来查找 DataGridCellsPresenter
  2. 如果找不到,则它不在视觉树中,您必须自己应用 DataRow 模板,然后再次重复步骤 1,这次会成功。
  3. 使用 presenter.ItemContainerGenerator.ContainerFromIndex(0) 查找第一列。如果找不到任何内容,则它不在视觉树中,您必须将列滚动到视图中:dataGrid.ScrollIntoView(rowContainer, dataGrid.Columns[0])
  4. 现在,并且只有现在,您才能调用 DataGridCell.Focus()

然后继续遍历每一行。

这不仅听起来很复杂,而且代码执行速度也很慢。在我顶级的性能工作站上,它几乎需要一秒钟。现在想象一下用户多次单击按钮(如果他每次只增加 1 个排名,那么 10 次很容易)。但 10 秒的延迟是完全不可接受的。所以我不得不寻找另一个解决方案。

最终方法:使用 OneWay 绑定并避免调用 Refresh()

由于用户无法直接在 datagrid 中更改任何数据,因此我将其设为只读,并使用了默认绑定,即 OneTime,这意味着数据在分配给 DataGridDataSource 时只写入一次。我将其绑定更改为 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 日:初始版本
© . All rights reserved.