如何解决WPF ListView SelectedItems性能差的问题?

3
这是我的代码(它在WPF ListView中搜索所有匹配项并选择它们):
            public bool FindAll(LogFilter filter, bool matchCase)
            {
                lastLogFilter = filter;
                lastMatchCase = matchCase;
                MatchSearcher quickSearchSearcher = new MatchSearcher(filter, !matchCase);
                bool foundOnce = false;
                Stopwatch watch = new Stopwatch();
                watch.Start();
                var query = from x in listView.Items.Cast<LogRecord>() where quickSearchSearcher.IsMatch(x, false) select x;
                watch.Stop();
                Console.WriteLine("Elapsed milliseconds to search: {0}.", watch.ElapsedMilliseconds);
                if (query.Count() > 0)
                {
                    foundOnce = true;
                    listView.SelectedItems.Clear();
                    watch.Restart();
                    foreach (LogRecord record in query)
                    {
                        listView.SelectedItems.Add(record);
                    }
                    watch.Stop();
                    Console.WriteLine("Elapsed milliseconds to select: {0}.", watch.ElapsedMilliseconds);
                    listView.ScrollIntoView(query.First());
                }
                return foundOnce;
            }

以下是10,000个ListView项目的结果:

Elapsed milliseconds to search: 0.
Elapsed milliseconds to select: 36385.

所以,很明显我的问题在于循环:

foreach (LogRecord record in query)
{
    listView.SelectedItems.Add(record);
}

我觉得应该有更好的方法来添加到所选项目列表中,或者至少在所有选定项目被设置之前,阻止数据模板更新(或类似的东西)在列表上。在WPF ListView中尝试以编程方式选择多个项时,有没有办法获得更好的性能?


1
好的,让我试着更好地解释一下:你的代码是错的。你需要把它全部删除,重新开始。在WPF中使用数据绑定而不是过程式方法。它可以提高性能,使你的代码更清晰,更易于维护。 - Federico Berasategui
@HighCore 我并不认为是这种情况。我怎样才能知道UI虚拟化是否被破坏了? - Alexandru
1
即使OP以编程方式添加项目,也不意味着他自动添加ListViewItems。 您可以通过调用“Items.Add(new LogRecord(...))”轻松添加数据项(显然此处为“LogRecord”类型)。 - Clemens
@HighCore 嘿,听着,下次不要这么粗鲁和残忍。我希望你能把原始帖子保留在顶部,这样人们就可以看到你有多么刻薄和心胸狭窄。如果我想要切换控件,我只需要将不同的对象重新绑定到我的集合中...但是,哦,等等,我不需要这样做...我决定只在运行一些性能测试后使用ListView。 - Alexandru
1
哈哈,那是一条好的评论。每个人都很热情。不知道为什么我不那么热情 :P - Viv
显示剩余10条评论
3个回答

6

如果不想一个个添加选定的项目到SelectedItems属性中,可以调用SetSelectedItems方法。但是这个方法是受保护的,所以你需要创建一个派生自ListBox的类来公开它:

public class MyListView : ListView
{
    public void SelectItems(IEnumerable items)
    {
        SetSelectedItems(items);
    }
}

谢谢,这将时间降低到了不到一秒钟。 :) - Alexandru
SetSelectedItems() 对于成千上万的元素也非常缓慢。有什么想法吗? - Lumo
1
虽然比逐个添加项目要快得多,但对于大型列表仍然变慢。我可以在调用堆栈中看到它调用了IList.Contains(),所以它可能仍然是O(n^2)。MVVM答案在虚拟化方面无法正常工作,而我尝试解决问题的努力并没有成功,因此在WPF ListView中编程选择许多项目可能根本不实际。(顺便说一下,如果您不想为此创建子类,还可以使用反射来调用SetSelectedItems)。 - fadden

3

好的,您已经接受了这个问题的答案,但我仍然想展示一种不同的方法:

在此输入图片描述

XAML:

<Window x:Class="WpfApplication1.ListViewSearch"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ListViewSearch" Height="300" Width="300">
    <DockPanel>
        <DockPanel DockPanel.Dock="Left" Margin="2">
            <Button DockPanel.Dock="Bottom" Content="Find All" Margin="2" Click="FindAll_Click"/>

            <ListBox ItemsSource="{Binding Filters}"
                     SelectedItem="{Binding SelectedFilter}"
                     DisplayMemberPath="DisplayName"/>
        </DockPanel>

        <ListView ItemsSource="{Binding Items}">
            <ListView.View>
                <GridView>
                    <GridViewColumn DisplayMemberBinding="{Binding FirstName}" Header="First Name"/>
                    <GridViewColumn DisplayMemberBinding="{Binding LastName}" Header="Last Name"/>
                </GridView>
            </ListView.View>

            <ListView.ItemContainerStyle>
                <Style TargetType="ListViewItem">
                    <Setter Property="IsSelected" Value="{Binding IsSelected}"/>
                </Style>
            </ListView.ItemContainerStyle>
        </ListView>

    </DockPanel>
</Window>

代码后台:

public partial class ListViewSearch : Window
{
    private ViewModel ViewModel;

    public ListViewSearch()
    {
        InitializeComponent();

        DataContext = ViewModel = new ViewModel();
    }

    private void FindAll_Click(object sender, RoutedEventArgs e)
    {
        ViewModel.Filter();
    }
}

视图模型:

public class ViewModel
{
    public ViewModel()
    {
        Items = new ObservableCollection<DataItem>(RandomDataSource.GetRandomData());
        Filters = new ObservableCollection<DataFilter>();

        Filters.Add(new DataFilter()
        {
            DisplayName = "First Name starting with A",
            FilterExpression = x => x.FirstName.ToLower().StartsWith("a")
        });

        Filters.Add(new DataFilter()
        {
            DisplayName = "Last Name starting with E",
            FilterExpression = x => x.LastName.ToLower().StartsWith("e")
        });
    }

    public ObservableCollection<DataItem> Items { get; private set; }

    public DataFilter SelectedFilter { get; set; }

    public ObservableCollection<DataFilter> Filters { get; private set; }

    public void Filter()
    {
        if (SelectedFilter == null)
            return;

        foreach (var item in Items)
            item.IsSelected = SelectedFilter.FilterExpression(item);
    }
}

数据项:
public class DataItem : INotifyPropertyChanged
{
    private bool _isSelected;

    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            _isSelected = value;
            OnPropertyChanged("IsSelected");
        }
    }

    public string LastName { get; set; }

    public string FirstName { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

数据过滤器:

public class DataFilter
{
    public Func<DataItem, bool> FilterExpression { get; set; }

    public string DisplayName { get; set; }
}

随机数据源(只是一堆样板文件)

public static class RandomDataSource
{
    private static string TestData = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum";
    private static List<string> words;
    private static int maxword;
    private static Random random;

    public static List<DataItem> GetRandomData()
    {
        random = new Random();
        words = TestData.Split(' ').ToList();
        maxword = words.Count - 1;

        return Enumerable.Range(0, 10000)
                         .Select(x => GetRandomItem())
                         .ToList();
    }

    private static DataItem GetRandomItem()
    {
        return new DataItem()
        {
            LastName = words[random.Next(0, maxword)],
            FirstName = words[random.Next(0, maxword)],
        };
    }
}

此方法相比于传统的代码后台方法有以下优势:
  • 它将UI和逻辑分离开来。你可以针对自己定义的类进行操作,而不是处理(WPF对象模型有时候很深奥难懂的)。
  • 由于你的代码实际上并不依赖于任何特定的UI元素类型,所以你可以将UI更改为“3D旋转粉色大象”,它仍然能够正常工作。它使视图具有更多的可定制性,而不会影响任何代码或逻辑。
  • 它易于重用(你可以创建一个SearchViewModel<T>和一个DataFilter<T>,并在许多不同的实体类型上重用这些)。
  • 它可以进行单元测试。

谢谢,这是一篇很好的文章,你做得也很好。我的UI实际上是通过编程完成的,而不是出于选择。如果要通过MVVM设计模式进行搜索,我唯一需要更改的就是将IsSelected布尔值添加到我的LogRecord类中,并在设置它时触发更改事件,因为我已经将我的样式设置为绑定到项目的IsSelected属性,并且我已经将UI绑定到集合。除此之外,我们的代码实际上看起来非常相似,我想我也可以用这种方式选择所有项目。 - Alexandru
天哪... MVVM 到骨子裡。對於 MVVM,我會說這個... 在這個世界上有兩種人。喜歡抽象層的人和不喜歡的人。我個人討厭它們。MVVM 是介於 UI 和代碼之間的巨大抽象層,如果你喜歡抽象,那就沒問題了。但對我來說,有時候感覺好像在單一層中擁有所有東西,我能看到更大的圖片。我甚至不認為我能舉出一個好的例子來解釋我的意思或優缺點。也許會有其他人來幫助描述我試圖表達的一點點更具實證性的事情。 - Alexandru
我认为这取决于你的大脑功能。过多的抽象会让我在搜索代码时感到头痛,尤其是那些我没有编写过的代码。 - Alexandru
@Alexandru,没错,我部分地同意这个观点,问题在于WPF可视树是一个非常复杂的东西,当你需要处理VisualTreeHelper.Whatever()来改变深埋在ListBoxItemTemplate中的DataTemplate内的TextBlock文本时,它就变得很糟糕了。你明白我的意思吗?MVVM通过简单的属性和INotifyPropertyChanged来解决所有这些问题。 - Federico Berasategui
@Alexandru 抽象化帮助我创建了大量通用的可重用代码。我对几乎所有东西都有抽象化,我的大部分代码都基于通用类。我在调试时并没有看到问题,因为在运行时,您实际上是在使用具体实现而不是“抽象”抽象 :P - Federico Berasategui
显示剩余4条评论

1

评论中有很多信息,所以我要总结一下:

  1. 在多选ListView中更新选择的正确方法是修改SelectedItems属性。在WPF中没有按索引获取或设置的方法。所有操作都是按项进行的,这意味着每个选择操作都需要在列表中查找一个项目。对于大型列表,这可能会很慢。
  2. ListView定义了一个“批量更改”方法SetSelectedItems,可以用于一次调用选择多个项目。它被声明为protected,因此您需要子类化ListView或使用反射调用它。在底层,它仍然必须在列表中查找项目以将其标记为已选择,因此虽然速度更快,但对于大型列表仍然可能非常缓慢。
  3. 另一种方法是将IsSelected值移动到项目本身中,并使用数据绑定。这种方法非常快,但在启用UI虚拟化时会崩溃。由于几乎所有大型列表都希望使用虚拟化,因此这是不可行的。我还没有找到解决问题的方法。

总之,没有一种快速选择 WPF ListView 中大量项目的方法。

任何想要尝试的人都可以使用 DisasmUiTest 项目中的“选择测试”作为起点。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接