在 ListView C# 中过滤大型数据集合

Filtering large data collections in a ListView C#

我正在尝试使用筛选字段来筛选存储在 ObservableCollection 中的大量数据,具体取决于项目是否包含字符串并在 ListView 中显示结果。

目前我正在使用一个转换器来实现这个。它通过使用简单的不区分大小写的比较方法检查目标字符串是否包含过滤器字符串来工作。

private static bool Contains(string source, string toCheck, StringComparison comp = StringComparison.OrdinalIgnoreCase)
{
    return source?.IndexOf(toCheck, comp) >= 0;
}

这种方法似乎适用于较少数量的条目(几百个)。但是我正在处理的数据大小可以从 5 万到 20 万个条目不等。

在搜索大约 200000 个条目的数据集合时,有没有一种方法可以有效地过滤列表而不会对性能造成很大影响。

下面是MCVE。

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

对于这种即席查询,您必须扫描整个集合以构建筛选项集,因此您无能为力。

为了提高效率,我的建议是不要在每次(单个字符)更改过滤字符串后立即重新进行过滤。相反,对 FilterString 的每次更改(重新)启动一个定时器对象,周期为 1 秒,并且只有在定时器计时后才真正进行过滤。或者您可以使用某种缓冲反应式扩展构造来实现相同的结果。

尝试使用 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.
        //
        //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;
            };
        }
    }

下一次升级是向 textchanged 事件添加一个 DispatcherTimer,这样过滤器仅在文本未输入约一秒后才更新,而不是每个字符都更新。