I am trying to use a filter field that filters a large list of data, stored in an ObservableCollection
, based on whether an item contains a string and display the results in a ListView
.
Currently I am using a converter to achieve this. It works by checking if a target string contains the filter string by using a simple case insensitive compare method.
private static bool Contains(string source, string toCheck, StringComparison comp = StringComparison.OrdinalIgnoreCase)
{
return source?.IndexOf(toCheck, comp) >= 0;
}
This approach seems to work fine for a smaller number of entries (a few hundred). But the data size I am working with can range from 50 thousand to 200 thousand entries.
Is there a way to efficiently filter the list without large performance hits when searching data collections of roughly 200000 entries.
MCVE below.
XAML
<Window x:Class="FastFilter.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:FastFilter"
mc:Ignorable="d"
Title="Fast Filter" Height="450" Width="800">
<Window.Resources>
<local:FilterConverter x:Key="FilterConverter"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBox Text="{Binding Path=FilterString, UpdateSourceTrigger=PropertyChanged}"/>
<ListView Grid.Row="1"
ItemsSource="{Binding Path=Infos}">
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="Visibility">
<Setter.Value>
<MultiBinding Converter="{StaticResource FilterConverter}">
<Binding Path="DataContext.FilterString" RelativeSource="{RelativeSource AncestorType=ListView}"/>
<Binding Path="Text"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<!-- List Box Item Layout -->
<StackPanel Orientation="Horizontal">
<Label Content="Text:"/>
<Label Content="{Binding Text}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Window>
CS
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Data;
namespace FastFilter
{
public partial class MainWindow : INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
for (int i = 0; i < 200000; i++)
{
Infos.Add(new ObjectInfo(Guid.NewGuid().ToString()));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string filterString = string.Empty;
public string FilterString
{
get => filterString;
set
{
filterString = value;
OnPropertyChanged();
}
}
private ObservableCollection<ObjectInfo> infos = new ObservableCollection<ObjectInfo>();
public ObservableCollection<ObjectInfo> Infos {
get => infos;
set {
infos = value;
OnPropertyChanged();
}
}
}
public class ObjectInfo
{
public ObjectInfo(string text)
{
Text = text;
}
public string Text { get; }
}
public class FilterConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
string filter = (string)values[0];
string checkStringContains = (string)values[1];
return !(string.IsNullOrWhiteSpace(checkStringContains) || string.IsNullOrWhiteSpace(filter))
? Contains(checkStringContains, filter) ? Visibility.Visible : Visibility.Collapsed
: Visibility.Visible;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
private static bool Contains(string source, string toCheck, StringComparison comp = StringComparison.OrdinalIgnoreCase)
{
return source?.IndexOf(toCheck, comp) >= 0;
}
}
}
Try using ICollectionView.
xaml
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBox TextChanged="FilterTextChanged" Text="{Binding Path=FilterString, UpdateSourceTrigger=PropertyChanged}"/>
<ListView
x:Name="InfosListView"
Grid.Row="1"
ItemsSource="{Binding Path=Infos}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<!-- List Box Item Layout -->
<StackPanel Orientation="Horizontal">
<Label Content="Text:"/>
<Label Content="{Binding Text}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
CS
private void FilterTextChanged(object sender, TextChangedEventArgs e)
{
UpdateFilter();
}
private void UpdateFilter()
{
//NOTE: bellow comment only applies to DataGrids.
//Calling commit or cancel edit twice resolves exceptions when trying to filter the DataGrid.
//https://stackoverflow.com/questions/20204592/wpf-datagrid-refresh-is-not-allowed-during-an-addnew-or-edititem-transaction-m
//CommitEdit();
//CommitEdit();
ICollectionView view = CollectionViewSource.GetDefaultView(Infos);
if (view != null)
{
view.Filter = delegate (object item)
{
if (item is ObjectInfo objectInfo)
{
return objectInfo.Text.Contains(FilterString);
}
return false;
};
}
}
Next upgrade would be to add a DispatcherTimer to the textchanged event so that the filter only updates after text has not been enter for about a second, instead of for each character.