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 (VirtualizingPanel.IsVirtualizing="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
Siehe Bild 1
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.
Siehe Bild 2
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.
Siehe Bild 3
Ich hoffe ihr könnt mir helfen, nachfolgend der Code zu meinem Testprojekt.
Formular.xaml
<DataGrid
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>
<Label Content="Filter: " />
<TextBox Width="100" Text="{Binding Path=Filter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</WrapPanel>
Formular.xaml.cs
public partial class Formular : INotifyPropertyChanged
{
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);
}
}
MyListItem.cs
public class MyListItem : INotifyPropertyChanged
{
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;
}
}
Hallo,
funktioniert es denn ohne VirtualizingPanel.IsVirtualizing = true
?
Hast du denn mehrere virtuelle DataGrid
-Elemente, daß du dies überhaupt benötigst (denn auf dem Screenshot kann ich das nicht sehen)?
PS:
Anstatt in der Filterfunktion mehrfach ToUpper()
aufzurufen, ist es performanter direkt einen case-insensitiven Stringvergleich durchzuführen (s. String.Compare(String, String, StringComparison)):
String.Compare(item.Name, Filter, StringComparison.CurrentCultureIgnoreCase) == 0
(was du ja beim Highlighter
bei IndexOf(...)
auch schon nutzt ;- )
Hallo Th69,
danke für den Tipp!
Folgendes geht:
Folgendes geht nicht:
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.
Für ein DataGrid
benötigst du aber keine Panel
Virtualisierung. Hast du ein StackPanel
als äußeren Container?
Falls es dir um die vertikale Scrollbar bei dem DataGrid
geht, dann s. How can I enable scrollbars on the WPF Datagrid?
Und wenn es dir um Virtualisierung für das DataGrid
selbst geht (also um sehr viele Zeilen anzuzeigen, ohne alle im Speicher zu halten), dann s. DataGrid.EnableRowVirtualization (aber der Standardwert dafür ist schon true
).
Und dies wird dann auch der Grund für dein Problem sein, da du auf die DataGrid.Row
zugreifst, anstatt auf die Daten (d.h. über die ItemsSource
).
M.E. wäre es auch besser, wenn du die ganze Highlighter-Logik über das ViewModel (das du übrigens aus deiner Window-Klasse auslagern solltest, s.a. [Artikel] MVVM und DataBinding) lösen solltest - und nicht direkt über die UI-Komponenten, sondern per Control Templates und Binding.
Danke für die Erklärung, dann habe ich mir das wohl falsch abgeschaut.
Also mit EnableRowVirtualization="false" gehts, aber mit true nicht.
Das ist nur eine Testklasse, normalerweise ist mein Code entsprechend ausgelagert.
Die Highlighter Funktionalität soll unabhängig von jeglichen ViewModels verwendet werden können, egal ob DataGrid oder TreeViewCtrl (Eigenes TreeViewControl), egal welche Daten reingeschoben werden. Der Highlighter soll stumpf erkennen, dass dort irgendwo Texte stehen und diese entsprechend markieren.
Oder habe ich das falsch verstanden? Ich habe das jetzt so verstanden, dass ich die ganze Highlighting Geschichte für jedes ViewModel separat machen muss, dass würde aber leider nicht meiner Anforderung von "Überall verwendbar" entsprechen.
Dein Highlighter
funktioniert aber doch bisher auch nur für das DataGrid
, da es explizit DataGridRow
benutzt (welches bei TreeView
nicht existiert).
Das Auslagern in ein ViewModel soll einfach dafür sorgen, daß die Logik eben UI-unabhängig ist. Du kannst die Funktionalität ja trotzdem in eine eigene Highlighter
-Klasse auslagern, welche dann von den ViewModels benutzt wird.
Und diese Highlighter
-Klasse sollte eben nur auf den Daten (Collection
) operieren und ebenfalls nur Daten zurückliefern, welche dann von einem entsprechenden Control/Data-Template in UI-Komponenten aufgelöst wird, s. z.B. WPF ItemsControl Fundamentals - Part 1 (das Beispiel weiter unten mit dem CityViewModel
und den Orten mit Flaggen).
Da TextBlock.Inlines
jedoch kein DependencyObject
ist, kann daran nicht gebunden werden. Evtl. hilft dir aber die Antwort bzgl. TextBlockExtensions
aus Data binding the TextBlock.Inlines?
Vielen Dank für deine schnelle Antwort.
Die Testklasse beinhaltet nur einen gewissen Ausschnitt aus dem Hauptcode um den Fehler zu zeigen, die eigentliche Highlighter
Klasse kann mit den unterschiedlichsten Controls
(z.B. DataGrid
, TreeView
, Alleinstehender TextBlock
, ... ) umgehen. Egal welches Control
, es werden die TextBlöcke
an unterster Ebene ermittelt und der Text entsprechend markiert.
Ich glaube mein Verständnis ist für diese Thematik zu gering, ich hab mir die Links angeschaut aber verstehe nicht, wie ich das für mich nutzen kann.
Wie kann ich sowas so generisch bauen, dass es unabhängig vom ViewModel
funktioniert? Ich bring in meinen Gedanken nichts zusammen. Ich müsste dort dann für jeden sichtbaren TextBlock
Inlines
generieren und dafür sorgen, dass das beim Scrollen auch entsprechend synchronisiert wird.
Zudem muss ich das DataGrid
nutzen, da wir abgeleitete Klassen davon verwenden und die Virtualisierung durch EnableRowVirtualization zwingend erforderlich ist, da die Elemente beim Scrollen entsprechend abgebaut werden.
Hast du evt. noch eine Idee, die mit DataGrid
funktionieren könnte? Evtl. eine eigene Klasse
davon ableiten und darin irgendwas magisches machen, wodurch das funktionieren könnte?
Der Umbau der Highlighter
-Klasse ist nur ein Vorschlag von mir, wie ich es selber machen würde.
Du solltest den Code unterhalb von grid.LoadingRow += GridOnLoadingRow;
// Get DataGridRows
...
entfernen und entsprechend HighlightRow(e.Row)
in die Ereignismethode GridOnLoadingRow
packen, damit sich auch die Inlines
-Texte aktualisieren.
PS: Du fügst bisher jedesmal mit grid.LoadingRow += GridOnLoadingRow;
die Ereignismethode dem Ereignis hinzu (so daß er je Aufruf immer mehrfach ausgeführt wird) - dies sollte nur einmalig passieren!
Guten Morgen,
genau das ist leider das Problem, HighlightRow(e.Row
) verwendet die Texte der Textblöcke
um die Inlines
zu aktualisieren, aber die Texte sind in GridOnLoadingRow
noch nicht aktualisiert, im DataContext
sind zwar die neuen Texte, aber in den Textblöcken
im Text noch nicht.
Und ich habe im Grunde versucht irgendetwas zu machen, damit die Aktualisierung der TextBlock
-Texte anhand dem DataContext
neu angestoßen wird.
Ich habe gerade gesehen, dass in der Methode HighlightTextBlock(TextBlock)
direkt beim Einstieg txtBlock.GetBindingExpression(TextBlock.TextProperty)
die entsprechenden Daten liefert, aber am Ende der Methode, nachdem die Inlines
gesetzt wurden, liefert txtBlock.GetBindingExpression(TextBlock.TextProperty)
null.
Da scheint wohl doch das Binding
kaputt zu gehen.
Du solltest auch in HighlightTextBlock
auf die Daten zugreifen und nicht auf die UI-Komponenten, also den Text aus der ItemsSource
(bzw. DataSource
) auslesen bzw. als Parameter übergeben.
Du solltest datenorientiert vorgehen, daher auch mein Vorschlag bzgl. UI-unabhängiger Highlighter
-Klasse.