DataGrid Virtualization – Strange error after scrolling

AnnaBauer21

New member
Joined
Nov 29, 2024
Messages
3
Programming Experience
3-5
Hello,

I'm facing a problem and can't get any further. I hope you can help me.

Short explanation:
I have a DataGrid (VirtualizingPanel.IsVirtualizing="true") and a search bar. When I type something into the search bar, the list is filtered accordingly and the search text is highlighted using an AttachedProperty.
The highlighting is done by splitting the TextBlock text into several inlines. A corresponding background color is then set in the inlines that contain the search text.
--> Works fine
1732873887403.png


Problem:
As soon as I scroll down and up again, the elements that have left the visible area suddenly contain the same text as the 2nd element. The same thing happens at the end of the list. The strange thing is, when I scroll down and up again, some of the text is correct again.
1732873894419.png


I checked the Grid's LoadingRow event and saw that the Row that comes into the visible area contains the correct data in the DataContext, but the texts in the TextBlocks have not updated. My thought was that perhaps the binding was broken by manipulating the inlines, but that doesn't seem to be the problem.
1732873911250.png

If EnableRowVirtualization is set to false, it works, but unfortunately virtualization is absolutely necessary because the list can basically have n entries, the current estimate is up to 5000.

I hope you can help me, below is the code for my test project.

Formular.xaml:
<DataGrid
          Grid.Row="0"
          local:Highlighter.Filter="{Binding Filter, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
          AutoGenerateColumns="true"
          ColumnWidth="100"
          ItemsSource="{Binding Path=DisplayedItems, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
          RowHeight="30"
          SelectionMode="Single" />

<WrapPanel Grid.Row="1">
    <Label Content="Filter: " />
    <TextBox Width="100" Text="{Binding Path=Filter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</WrapPanel>

Formular.xaml.cs:
public partial class Formular : INotifyPropertyChanged
{
    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged = (sender, args) => { };
 
    public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
    {
        this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
 
    public ICollectionView DisplayedItems { get; set; }

    private string filter;
    public string Filter
    {
        get => this.filter;
        set
        {
            this.filter = value;

            this.DisplayedItems.Refresh();
            this.RaisePropertyChanged();
        }
    }

    public Formular()
    {
        InitializeComponent();

        this.DataContext = this;

        var listItems = new ObservableCollection<MyListItem>()
        {
            new MyListItem("Alpha", "Mission1"),
            new MyListItem("Beta1", "Mission1"),
            new MyListItem("Beta1", "Mission2"),
            new MyListItem("Beta1", "Mission3"),
            new MyListItem("Beta1", "Mission4"),
            new MyListItem("Beta1", "Mission5"),
            new MyListItem("Beta1", "Mission6"),
            new MyListItem("Beta1", "Mission7"),
            new MyListItem("Beta1", "Mission8"),
            new MyListItem("Beta1", "Mission9"),
            new MyListItem("Beta2", "Mission2"),
        };

        this.DisplayedItems = CollectionViewSource.GetDefaultView(listItems);
        this.DisplayedItems.Filter = this.FilterCallback;
    }
 
    public bool FilterCallback(object obj)
    {
        var item = (MyListItem) obj;

        return string.IsNullOrEmpty(this.Filter)
               || item.Name.ToUpper().Contains(Filter.ToUpper())
               || item.MissionName.ToUpper().Contains(Filter.ToUpper());
    }
}

Highlighter.cs:
public static class Highlighter
{
    private static string filter;

    static Highlighter(){}

    #region Filter
    public static readonly DependencyProperty FilterProperty =
        DependencyProperty.RegisterAttached("Filter", typeof(string), typeof(Highlighter), new PropertyMetadata("", PropertyChangedCallback));

    public static void SetFilter(DependencyObject obj, string value)
    {
        obj.SetValue(FilterProperty, value);
    }

    public static string GetFilter(DependencyObject obj)
    {
        return (string)obj?.GetValue(FilterProperty);
    }

    private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() => DoAction(d)));
    }
    #endregion

    private static void DoAction(DependencyObject d)
    {
        filter = GetFilter(d);
        if (filter == null)
        {
            return;
        }

        var grid = (DataGrid)d;
        grid.LoadingRow += GridOnLoadingRow;

        // Get DataGridRows
        var gridRows = grid.GetDescendants<DataGridRow>().ToList();
        foreach (var row in gridRows)
        {
            HighlightRow(row);
        }
    }
 
    private static void HighlightRow(DataGridRow row)
    {
        // Get TextBlocks
        var txtBlocks = row.GetDescendants<TextBlock>().ToList();
        if (!txtBlocks.Any())
        {
            return;
        }

        foreach (var txtBlock in txtBlocks)
        {
            HighlightTextBlock(txtBlock);
        }
    }

    private static void HighlightTextBlock(TextBlock txtBlock)
    {
        var text = txtBlock.Text;
        if (string.IsNullOrEmpty(text))
        {
            return;
        }

        // Check whether the text contains the filter text
        var index = text.IndexOf(filter, StringComparison.CurrentCultureIgnoreCase);
        if (index < 0)
        {
            // Filter text not found
            return;
        }

        // Generate Inlines with highlighting information
        var inlines = new List<Inline>();
        while (true)
        {
            // Text from beginning to filter text
            inlines.Add(new Run(text.Substring(0, index)));

            // Text that corresponds to the filter text
            inlines.Add(new Run(text.Substring(index, filter.Length))
            {
                Background = Brushes.Yellow
            });

            // Text from filter text to ending
            text = text.Substring(index + filter.Length);

            // Check whether the remaining text also contains the filter text
            index = text.IndexOf(filter, StringComparison.CurrentCultureIgnoreCase);
            if (index < 0)
            {
                // If not, add remaining text and exit loop
                inlines.Add(new Run(text));
                break;
            }
        }

        // Replace Inlines
        txtBlock.Inlines.Clear();
        txtBlock.Inlines.AddRange(inlines);
    }

    private static void GridOnLoadingRow(object sender, DataGridRowEventArgs e)
    {
        var dataContext = (MyListItem) e.Row.DataContext;

        var newData = $"{dataContext.Name}_{dataContext.MissionName}";
        var oldData = string.Join("_", e.Row.GetDescendants<TextBlock>().Select(t => t.Text).ToList());
    }
}

MyListItem.cs:
public class MyListItem : INotifyPropertyChanged
{
    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged = (sender, args) => { };
 
    public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
    {
        this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion

 
    public string name;
    public string Name
    {
        get => name;
        set
        {
            this.name = value;
            this.RaisePropertyChanged();
        }
    }

    public string missionName;
    public string MissionName
    {
        get => missionName;
        set
        {
            this.missionName = value;
            this.RaisePropertyChanged();
        }
    }

    public MyListItem(string name, string missionName)
    {
        this.Name = name;
        this.MissionName = missionName;
    }
}
 
Last edited:
Do you see the same thing happen without the highlighting?
 
At the beginning of the method HighlightTextBlock(TextBlock) the call txtBlock.GetBindingExpression(TextBlock.TextProperty) returns the corresponding data but at the end of the method after the Inlines are set, txtBlock.GetBindingExpression(TextBlock.TextProperty) returns null.

Did this break the binding? That would explain why in GridOnLoadingRow the DataContext contains the new data but the TextBlocks still contains the old.
 
That is so weird. Right now, the only thing that comes to mind is that you are hitting a timing issue.
 
I wonder if the words "reuse" and "recycle" in the documentation is more literal than my first reading of it means:
To improve performance, the EnableRowVirtualization property is set to true by default. When the EnableRowVirtualization property is set to true, the DataGrid does not instantiate a DataGridRow object for each data item in the bound data source. Instead, the DataGrid creates DataGridRow objects only when they are needed, and reuses them as much as it can. For example, the DataGrid creates a DataGridRow object for each data item that is currently in view and recycles the row when it scrolls out of view.
 
Back
Top Bottom