0

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:

Bild 1

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.

Bild 2

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.

Bild 3

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.

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.

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;
    }
}

I don't know what i can do.
I checked the other answers but I have the impression that nothing is suitable for my problem.

4
  • If it's "incremental searching" you're trying to implement, one method is to only show the eligible records (returned); in which case high-lighting is somewhat redundant. Commented Nov 30, 2024 at 0:45
  • FWIW, I do incremental searching on the "backing store"; e.g. "paragraphs" of text. If I find paragraph(s) that match, I do what you do: Create "inlines". But I create the TextBlock also; and load all of them into a StackPanel inside a ScrollViewer. (No "property notification" ... just load the "text blocks" into a viewable container). "Code" is fast (i.e. searching); and only loading the results, makes the UI fast. Commented Nov 30, 2024 at 15:16
  • Yes, I only show the search results, but the searched text needs to be highlighted, that's a requirement. And also using a DataGrid because we have some derived classes that we use everywhere. Commented Dec 2, 2024 at 8:56
  • My suggestion was to "generate" a result set; not "fiddle" with the existing UI. You can have the "source" in one "panel"; and the results in another (panel). Commented Dec 3, 2024 at 18:30

1 Answer 1

0

At first, sorry for my bad english, it's a lot of text so i use google translator.

I have found a solution. It's a bit ugly, but in my case it's the only one I found.

The virtualization uses the old list entries when scrolling and inserts them again at the bottom. The DataContext has updated itself according to the new list element, but the texts in the TextBlock have not, but these are used for highlighting. The solution in my case was that I save certain BindingExpressions in the Tag property of the TextBlock and as soon as the next row is loaded while scrolling (event LoadingRow in DataGrid) I reset the BindingExpressions according to the updated DataContext. But this only works if the GUI is already rendered. If the code is executed too early, the BindingExpressions will be null. That's why it's important that LoadingRow is only executed after rendering and after the actual highlighting action.

Here is an overview of the most important changes

Formular9.xaml

  • Using MyGrid instead of DataGrid (see the corresponding class for details)
  • Specification of virtualization
  • New property 'RaiseHighlight'

.

<local:MyGrid
...
local:Highlighter9.RaiseHighlight="{Binding IsFilterReady, Mode=OneWay}"
EnableRowVirtualization="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"/>

Formular9.xaml.cs

  • New property 'IsFilterReady', which is set to true after the list is filtered by refresh

.

public string Filter
{
    get => this.filter;
    set
    {
        this.filter = value;
        this.RaisePropertyChanged();

        this.DisplayedItems.Refresh();
        this.IsFilterReady = true;
    }
}

public bool IsFilterReady
{
    get => this.isFilterReady;
    set
    {
        this.isFilterReady = value;
        this.RaisePropertyChanged();
    }
}

MyGrid.cs

Own DataGrid that provides required functionalities

public class MyGrid : DataGrid
{
    public MyGrid()
    {
        this.LoadingRow += GridOnLoadingRow;
    }

    public bool IsHighlightingReady { get; set; }

    public bool IsVirtualization => VirtualizingPanel.GetIsVirtualizing(this) || this.EnableColumnVirtualization || this.EnableRowVirtualization;

    public VirtualizationMode VirtualizationMode => VirtualizingPanel.GetVirtualizationMode(this);

    public string Filter { get; set; }

    private void GridOnLoadingRow(object sender, DataGridRowEventArgs e)
    {
        Highlighter9.GridOnLoadingRow((MyGrid)sender, e);
    }
}

Highlighter9.cs

  • New Property 'RaiseHighlight' to start processing
  • LoadingRow Event that is only executed when the actual highlighting has been performed
  • Determine & set the BindingExpressions

.

public static class Highlighter9
{
    // Fires the start of the highlighting process
    public static readonly DependencyProperty RaiseHighlightProperty = DependencyProperty.RegisterAttached(
        "RaiseHighlight",
        typeof(bool),
        typeof(Highlighter9),
        new PropertyMetadata(false, null, CoerceValueCallback));

    public static void SetRaiseHighlight(DependencyObject obj, bool value)
    {
        obj.SetValue(RaiseHighlightProperty, value);
    }

    public static bool GetRaiseHighlight(DependencyObject obj)
    {
        return (bool)obj.GetValue(RaiseHighlightProperty);
    }

    private static object CoerceValueCallback(DependencyObject d, object baseValue)
    {
        // It is important to wait until the list has been filtered before highlighting.
        // If the list elements are not fully rendered, information that is necessary such as BindingExpression is missing.
        if ((bool)baseValue)
        {
            var filter = GetFilter(d);
            if (filter == null || string.IsNullOrWhiteSpace(filter))
            {
                return baseValue;
            }

            var grid = (MyGrid)d;
            grid.IsHighlightingReady = false;

            Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() =>
            {
                DoAction(grid, filter);
            }));
        }

        return baseValue;
    }

    #endregion

    #region Filter Property
    public static readonly DependencyProperty FilterProperty =
        DependencyProperty.RegisterAttached("Filter", typeof(string), typeof(Highlighter9));

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

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

    private static void DoAction(MyGrid grid, string filter)
    {
        // Save current filter data to the grid.
        // If there are several grids in the form, complications arise because the grids attract each other's data.
        // Grid also needs the data so that the correct data is known via the LoadingRow event
        grid.Filter = filter;

        var gridRows = grid.GetDescendants<DataGridRow>().ToList();
        foreach (var row in gridRows)
        {
            HighlightRow(row, grid);
        }

        grid.IsHighlightingReady = true;
    }

    private static void HighlightRow(DataGridRow row, MyGrid grid)
    {
        var txtBlocks = row.GetDescendants<TextBlock>().ToList();
        if (!txtBlocks.Any())
        {
            return;
        }

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

    private static void HighlightTextBlock(TextBlock txtBlock, MyGrid grid)
    {
        // Check whether virtualization is activated
        if (grid.IsVirtualization && grid.VirtualizationMode == VirtualizationMode.Recycling)
        {
            // Determine and save property name from binding
            var exp = txtBlock.GetBindingExpression(TextBlock.TextProperty);
            txtBlock.Tag = exp?.ResolvedSourcePropertyName;
        }

        var text = txtBlock.Text;
        if (string.IsNullOrEmpty(text))
        {
            return;
        }

        // Check whether the text contains the filter text
        var index = text.IndexOf(grid.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, grid.Filter.Length))
                            {
                                Background = Brushes.Yellow,
                            });

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

            // Check whether the remaining text also contains the filter text
            index = text.IndexOf(grid.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);
    }
    
    public static void GridOnLoadingRow(MyGrid grid, DataGridRowEventArgs e)
    {
        // When the filter is entered again, the LoadingRow event is executed before the actual processing of the highlighter is carried out.
        // Ignore code execution until the highlighter finishes processing.
        // Otherwise it will lead to the error that BindingExpression information is not available
        if (!grid.IsHighlightingReady)
        {
            return;
        }

        // Leave method if virtualization is deactivated
        if (!grid.IsVirtualization)
        {
            return;
        }

        if (grid.VirtualizationMode == VirtualizationMode.Recycling)
        {
            // Determine text blocks of the row
            if (e.Row.GetDescendants<TextBlock>().Any())
            {
                HighlightRowWithUpdateBinding(e.Row, grid);
            }
            else
            {
                // If no text blocks are found, Row is loaded but not yet rendered. Delay call.
                Application.Current.Dispatcher.BeginInvoke(new Action(() =>
                {
                    if (e.Row.GetDescendants<TextBlock>().Any())
                    {
                        HighlightRowWithUpdateBinding(e.Row, grid);
                    }
                }), DispatcherPriority.Send);
            }
        }
        else
        {
            // Here the row has not yet been rendered, GetDescendants in HighlightRow does not find any text boxes, delay the call
            Application.Current.Dispatcher.BeginInvoke(new Action(() =>
            {
                HighlightRow(e.Row, grid);
            }), DispatcherPriority.Send);
        }
    }

    private static void HighlightRowWithUpdateBinding(DataGridRow row, MyGrid grid)
    {
        // Reset bindings for the text blocks
        foreach (var txtBlock in row.GetDescendants<TextBlock>())
        {
            // Determine property name from binding or tag property
            var exp = txtBlock.GetBindingExpression(TextBlock.TextProperty);
            var path = exp != null ? exp.ResolvedSourcePropertyName : txtBlock.Tag.ToString();

            // Reset binding
            var binding = new Binding { Source = txtBlock.DataContext, Path = new PropertyPath(path) };
            txtBlock.SetBinding(TextBlock.TextProperty, binding);
        }

        HighlightRow(row, grid);
    }
}

MyListItem.cs

  • Unchanged, see above
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.