DataGrid Virtualisierung - Komischer Fehler beim scrollen

AnnaBauer21

Grünschnabel
Hallo ihr Lieben,

ich stehe vor einem Problem und komme nicht mehr weiter.
Ich habe die Hoffnung, dass ihr mir helfen könnt.

Kurze Erklärung:
Ich habe ein DataGrid (EnableRowVirtualization=true) & eine Suchleiste. Gebe ich in der Suchleiste etwas ein wird die Liste entsprechend gefiltert & über ein AttachedProperty der entsprechende Text markiert. Das markieren erfolgt, indem der Text aus einem TextBlock in mehrere Inlines zerlegt wird. In den Inlines, die den Suchtext beinhalten ist dann eine entsprechende Background-Farbe gesetzt.
--> Funktioniert wunderbar
1732863675936.png

Problem:
Sobald ich runter und wieder rauf scrolle, enthalten die Elemente, die den sichtbaren Bereich verlassen haben, plötzlich den gleichen Text wie das 2. Element. Gleiches passiert auch am Ende der Liste. Das komische ist, wenn ich erneut runter und wieder rauf scrolle ist der Text teilweise wieder richtig.
1732863974130.png
Ich habe mich am Grid auf das Event LoadingRow gehangen und gesehen, dass die Row, die dann in den sichtbaren Bereich kommt, zwar im DataContext die richtigen Daten enthält, aber die Texte in den TextBlöcken sich nicht aktualisiert haben.
Mein Gedanke war, dass evtl. durch das Manipulieren der Inlines das Binding kaputt gegangen ist, aber das scheint nicht der Fall zu sein.
1732865421363.png

Wenn EnableRowVirtualization auf false gesetzt wird, funktioniert es, aber leider ist die Virtualisierung zwingend nötig denn die Liste kann im Grunde n Einträge besitzen, aktuell ist die Einschätzung bis zu 5000.

Update 1:-----
In der Methode HighlightTextBlock(TextBlock) direkt beim Einstieg liefert txtBlock.GetBindingExpression(TextBlock.TextProperty) die entsprechenden Daten, aber am Ende der Methode, nachdem die Inlines gesetzt wurden, liefert txtBlock.GetBindingExpression(TextBlock.TextProperty) null.
Ist das Binding dadurch irgendwie kaputt gegangen? Das würde erklären, warum in GridOnLoadingRow der DataContext die neuen Daten enthält, die TextBlöcke aber noch die alten.
-----

Ich hoffe ihr könnt mir helfen, nachfolgend der Code zu meinem Testprojekt.
Formular.xaml
XML:
<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
C#:
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
C#:
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;
        }

        // Check filter
        var check = CheckFilter(txtBlocks);
        if (check)
        {
            foreach (var txtBlock in txtBlocks)
            {
                HighlightTextBlock(txtBlock);
            }
        }
    }

    private static bool CheckFilter(List<TextBlock> txtBlocks)
    {
        if (string.IsNullOrWhiteSpace(filter))
        {
            return false;
        }

        // Check whether one of the text blocks in the row contains the filter text
        var contains = false;
        foreach (var txtBlock in txtBlocks)
        {
            contains |= txtBlock.Text.ToLower().Contains(filter.ToLower());
        }

        return contains;
    }

    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
C#:
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;
    }
}
 
Zuletzt bearbeitet:
Hallo Spyke,
das hat funktioniert, aber laut Doku ist es mit Standard langsamer, deshalb würde ich gerne Recycle verwenden.
Schau dir bitte mal Update 1 an, könnte mein Problem daran liegen?
 
Wäre denkbar das dadurch die Bindung zerstört wird.

Falls du in deinem DataGrid immer gleichen Zeilenhöhen hast (keine dynamik brauchst) könnte es für die Performance helfen eine feste Zeilenhöhe zu definieren.

im folgendem Beispiel mit Bindung und/oder festem Wert.
Code:
        <DataGrid>
            <DataGrid.RowStyle>
                <Style TargetType="{x:Type DataGridRow}">
                    <Setter Property="Height" Value="{Binding MyRowHeight}"/>
                </Style>
            </DataGrid.RowStyle>
        </DataGrid>
       
        <DataGrid>
            <DataGrid.RowStyle>
                <Style TargetType="{x:Type DataGridRow}">
                    <Setter Property="Height" Value="50"/>
                </Style>
            </DataGrid.RowStyle>
        </DataGrid>

Edit:
sehe gerade du setzt ja RowHeight direkt im DataGrid.

Was noch helfen könnte AutoGenerateColumns auf false und die Spalten selbst definieren mit festen Spaltenbreiten.
 
Zuletzt bearbeitet:
Hallo ihr Lieben,

ich habe eine Lösung gefunden. Ist etwas unschön, aber in meinem Fall die einzige die ich gefunden habe.

Die Virtualisierung nutzt beim Scrollen die alten Listelenemte und fügt sie unten wieder an.
Der DataContext hat sich entsprechend dem neuen Listelement aktualisiert, aber die Texte im TextBlock nicht, diese werden aber für das Highlighting verwendet.
Die Lösung war in meinem Fall, dass ich mit im Tag-Property des TextBlocks gewisse BindingExpressions merke und sobald beim Scrollen die nächste Row geladen ist (Event LoadingRow im DataGrid), ich die BindingExpressions entsprechend dem aktualisierten DataContext neu setze.
Das funktioniert aber nur , wenn die GUI bereits gerendert ist. Wird der Code zu früh ausgeführt, sind die BindingExpressions null.
Deshalb ist es wichtig, dass LoadingRow erst nach dem Rendern und nach der eigentlichen Highlighting-Aktion ausgeführt wird.

Wichtigste Änderungen im Überblick

Formular9.xaml
  • Verwendung von MyGrid statt DataGrid
  • Angabe der Virtualisierung
  • Neues Property 'RaiseHighlight'
  • Ganzer Code Formular9.xaml
XML:
<local:MyGrid
    ...
    local:Highlighter9.RaiseHighlight="{Binding IsFilterReady, Mode=OneWay}"
    EnableRowVirtualization="True"
    VirtualizingStackPanel.VirtualizationMode="Recycling"/>

Formular9.xaml.cs
  • Neues Property IsFilterReady, das auf true gesetzt wird, nachdem die Liste durch Refresh gefildert wurde
  • Ganzer Code Formular9.xaml.cs
C#:
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
  • Eigenes DataGrid, das benötigte Funktionalitäten bereitstellt
C#:
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
  • Neues Property 'RaiseHighlight' zum Einstieg der Verarbeitung
  • LoadingRow Event, das nur ausgeführt wird, wenn das eigentliche Highlighten durchgeführt wurde
  • Ermitteln & setzen der BindingExpressions
  • Ganzer Code Highlighter9.cs
C#:
public static class Highlighter9
{
    // Feuert den Start des Highlighting Prozesses
    public static readonly DependencyProperty RaiseHighlightProperty = DependencyProperty.RegisterAttached(
        ...
        new PropertyMetadata(false, null, CoerceValueCallback));

    ...

    private static object CoerceValueCallback(DependencyObject d, object baseValue)
    {
        // Es ist wichtig zu warten, bis die Liste gefildert wurde, bevor das Highlighting durchgeführt wird.
        // Wenn die Listenelemente nicht vollständig gerendert sind fehlen Informationen, die nötig sind wie z.B. BindingExpression.
        if ((bool)baseValue)
        {
            ...

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

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

        return baseValue;
    }

    ...

    private static void DoAction(MyGrid grid, string filter)
    {
        // Aktuelle Daten im Grid speichern, damit Grid nach verzögertem Aufruf die richtigen Daten verwendet.
        // Bei mehreren Grids im Formular kommt es sonst zu Komplikationen, dass die Grids die Daten des anderen anziehen, da der Behavior statisch ist.
        // Zudem benötigt Grid die Daten damit im Event LoadingRow die richtigen Daten bekannt sind
        grid.Filter = filter;

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

        grid.IsHighlightingReady = true;
    }

    ...

    private static void HighlightTextBlock(TextBlock txtBlock, MyGrid grid)
    {
        // Prüfen, ob Virtualisierung aktiviert ist
        if (grid.IsVirtualization && grid.VirtualizationMode == VirtualizationMode.Recycling)
        {
            // Property Name aus Binding ermitteln & merken
            var exp = txtBlock.GetBindingExpression(TextBlock.TextProperty);
            txtBlock.Tag = exp?.ResolvedSourcePropertyName;
        }

        ...
    }

    public static void GridOnLoadingRow(MyGrid grid, DataGridRowEventArgs e)
    {
        // Bei erneuter Filtereingabe wird das Event LoadingRow durchgeführt, bevor die eigentliche Verarbeitung des Highlighters durchgeführt wird.
        // Die Ausführung des Codes so lange ignorieren, bis der Highlighter seine Verarbeitung abgeschlossen hat.
        // Führt sonst zum Fehler, dass BindingExpression Informationen nicht vorhanden sind
        if (!grid.IsHighlightingReady)
        {
            return;
        }

        // Bei Deaktivierter Virtualisierung Methode verlassen
        if (!grid.IsVirtualization)
        {
            return;
        }

        if (grid.VirtualizationMode == VirtualizationMode.Recycling)
        {
            // Textblöcke der Row ermitteln
            if (e.Row.GetDescendants<TextBlock>().Any())
            {
                HighlightRowWithUpdateBinding(e.Row, grid);
            }
            else
            {
                // Werden keine Textblöcke gefunden, ist Row zwar geladen, aber noch nicht gerendert. Aufruf verzögern.
                Application.Current.Dispatcher.BeginInvoke(new Action(() =>
                {
                    if (e.Row.GetDescendants<TextBlock>().Any())
                    {
                        HighlightRowWithUpdateBinding(e.Row, grid);
                    }
                }), DispatcherPriority.Send);
            }
        }
        else
        {
            // Hier ist die Row noch nicht gerendert, GetDescendants in HighlightRow findet keine Textboxen, Aufruf verzögern
            Application.Current.Dispatcher.BeginInvoke(new Action(() =>
            {
                HighlightRow(e.Row, grid);
            }), DispatcherPriority.Send);
        }
    }

    private static void HighlightRowWithUpdateBinding(DataGridRow row, MyGrid grid)
    {
        // Bindings für die TextBlöcke neu setzen
        foreach (var txtBlock in row.GetDescendants<TextBlock>())
        {
            // Property Name aus Binding oder Tag-Property ermitteln
            var exp = txtBlock.GetBindingExpression(TextBlock.TextProperty);
            var path = exp != null ? exp.ResolvedSourcePropertyName : txtBlock.Tag.ToString();

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

        HighlightRow(row, grid);
    }

MyListItem.cs
  • Unverändert, siehe oben
 

Anhänge

Zurück