Surprisingly, many challenges were encountered when changing some WPF DataGrid data from code behind which required a new sorting of the rows and scrolling the DataGrid to show the initially selected rows. The article focuses on describing the problems encountered and how to solve it. At the end is the complete sample code.
Introduction
I was writing a WPF application using a WPF DataGrid
displaying items with a rank property, the items sorted by rank. The user interface should allow the user to select some rows (items) and move them up or down by some rows by clicking on a button:

When clicking Move Down, the Item 3-5 with Rank 3-5 get moved down by 20 rows and get new ranks 23-25. Items 6-25 get moved up by 3 ranks to make space for items 3-5. The grid automatically sorts the items by rank, scrolls and shows the 3 selected rows at their new place in the grid.
I thought this was quite easy to implement in the handler of the Move Down Button:
- Detect which rows (items) are selected.
- Loop over them and increase their
Rank
by 20
. - Loop over the rows which need to be moved away and adjust their
Rank
. - Refresh
DataGrid
.
Unfortunately, refreshing the DataGrid
made the DataGrid
forget which rows were selected. This gives the user a serious problem, if he needs to press the Move Down Button several times to move the selected rows into the right place.
First Approach: Remember Selected Rows, Refresh DataGrid, Select Rows Again
Sounds simple enough, right? Unfortunately, it turns out selecting rows and bringing them into view is VERY complicated with a WPF DataGrid
, the reason being that because of virtualisation, only the currently visible items have actually DataRow
s and DataGridCell
s assigned to them, but the information if an item is selected is stored in those classes. So if an item disappears from the visible part, it is rather complicated to bring it back into view and to mark it as selected again.
Luckily, I found this Technet article WPF: Programmatically Selecting and Focusing a Row or Cell in a DataGrid
Unfortunately, the required code is complicated and slow. It goes like this (for code see previous link):
- Loop through every item that should get selected.
- Use
DataGrid.ItemContainerGenerator.ContainerFromIndex(itemIndex)
to determine if the row is in view. - If not, use
TracksDataGrid.ScrollIntoView(item)
and then again ContainerFromIndex(itemIndex)
. - Hopefully, a
DataRow
is now found. Give it the Focus
.
Now, if you think giving a Focus
to a DataGridRow
, which is in view, is easy, you are mistaken. It involves the following steps (for code see previous link):
- Find the
DataGridCellsPresenter
in the DataGridRow
which holds the DataGridCells
. If you think that is trivial, you are mistaken again. You need to loop through the visual tree to find the DataGridCellsPresenter
. - If you can't find it, it is not in the visual tree and you have to apply the
DataRow
template yourself and then repeat step 1 again, this time successfully. - Use
presenter.ItemContainerGenerator.ContainerFromIndex(0)
to find the first column. If nothing is found, it is not in the visual tree and you have to scroll the column into view: dataGrid.ScrollIntoView(rowContainer, dataGrid.Columns[0])
. - Now, and only now can you call
DataGridCell.Focus()
.
Now continue with the loop through every row.
This not only sounds complicated, the code executes also slowly. On my top notch workstation, it takes nearly a second. Now imagine the user clicking several times the button (10 times is easily possible, if he increases the rank 10 times only by 1). But a 10 second delay is simply not acceptable. So I had to find another solution.
Final Approach: Use OneWay Binding and Avoid Calling Refresh()
Since the user cannot change any data directly in the datagrid
, I made it readonly and used the default binding, which is OneTime
, meaning the data gets written once when the data gets assigned to the DataGrid
's DataSource
. I changed the binding to OneWay
, which copies the new value every time the data changes to the DataGrid
. For this to work, my item had to implement 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;
}
Each time the Rank
changes, the PropertyChanged
event gets called, to which the DataGrid
subscribes.
The DataGrid
did now display the rows with the new Rank
values, but did not sort. After some Googling, I found that live sorting needs to be activated like this:
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));
With this change, the click on Move Down button executed reasonably fast and the DataGrid
sorted properly, BUT: the selected rows got out of view and could no longer be seen. That should be easy enough to solve by adding DataGrid.ScrollIntoView(DataGrid.SelectedItem)
. Alas, nothing happened, the DataGrid
didn't scroll.
Improvement 1: Getting ScrollIntoView() to Work
After some more Googling, I came to the conclusion that ScrollIntoView()
simply did nothing when I called it in the Move Down button click event, because at that time the DataGrid
was not sorted yet. So I had to delay the call of ScrollIntoView()
, but how? I first considered the use of a timer, but then I found a better solution: using the DataGrid.LayoutUpdated
event:
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]);
}
}
And presto, the click on Move Down button executed reasonably fast, the DataGrid
sorted properly and the DataGrid
scrolled to the selected rows.
Improvement 2: Showing the Selected Rows as if they have a Focus
When the user selects some rows with the mouse, they are shown with a dark blue background. But once the Move Down
button was clicked, the BackGround
became gray and difficult to see on my monitor. As explained in the First Approach, it is possible to give rows the focus from code behind, but it is way too complicated and slow. Luckily, there is a much simpler solution:
<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>
The trick here is just to make the row look the same when it is just selected (InactiveSelectionHighlightBrush
) and when it is selected and has the focus (HighlightBrush
).
Deep Dive into DataGrid Formatting
If you have read until here, it's safe to assume that you are genuinely interested in the DataGrid
. In this case, I would like to recommend you also to read my article over DataGrid
formatting, a dark art on itself: Guide to WPF DataGrid Formatting Using Bindings.
Using the Code
The sample application didn't need much code, but I spent a long time to make it work, By studying it, I hope you can save some time.
<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;
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;
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);
}
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;
}
}
History
- 5th February, 2021: Initial version