【问题标题】:Filtering large data collections in a ListView C#在 ListView C# 中过滤大型数据集合
【发布时间】:2019-03-22 02:54:31
【问题描述】:

我正在尝试使用过滤器字段来过滤存储在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;
        }
    }
}

【问题讨论】:

  • WPF 中的排序、分组、过滤和选择跟踪是通过 CollectionView 完成的。 UI 类将自动创建(甚至共享)单个 CollectionView,但要有效地使用它,您必须控制它的创建。实际上,您应该旨在公开 CollectionView,而不是原始的 ObservableCollection。
  • 如果使用 Hashset&lt;T&gt;. Contains 会是 O(1)。
  • “但我正在处理的数据大小范围可以从 50,000 到 200,000 个条目。”如果您要检索那么多条目,那么您的数据库访问基本设计是完全错误的。您实际上不能向用户显示超过 100 个数据字段并期望他使用它。如果您要按该比例进行过滤,请始终在数据库查询中进行。不要批量检索然后在 UI 中进行过滤。使用分页和 DBMS 支持的任何其他方式来不检索那么多数据。
  • 这可能很有用github.com/lvaleriu/Virtualization/tree/master/…,它只会加载要显示的指定数量的记录。
  • @Christopher 感谢您提供这些信息。我会考虑重新设计它以便数据库处理它

标签: c# wpf large-data


【解决方案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.
        //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;
            };
        }
    }

下一个升级是在 textchanged 事件中添加一个 DispatcherTimer,以便过滤器仅在大约一秒钟内没有输入文本后更新,而不是每个字符。

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-09-06
      • 2021-11-21
      • 2010-12-02
      • 2022-10-01
      • 1970-01-01
      • 2016-10-16
      • 1970-01-01
      相关资源
      最近更新 更多