使用 DataBinding 的 ListBox 填充速度极慢

Posted

技术标签:

【中文标题】使用 DataBinding 的 ListBox 填充速度极慢【英文标题】:Extremely slow population of ListBox using DataBinding 【发布时间】:2017-07-27 03:24:48 【问题描述】:

我以前使用后面的代码手动将项目添加到我的 ListBox,但速度非常慢。我听说通过 XAML 进行数据绑定在性能方面是可行的方法。

所以我设法让数据绑定工作(绑定的新手),但令我沮丧的是,性能并不比我以前的非数据绑定方法好。

我的想法是我的 ListBox 包含一个带有名称的图像。我做了一些基准测试,54 个项目需要整整 8 秒才能显示。这对于用户来说自然是等待太久了。

源图像最大:2100x1535px,范围为 400kb>4mb/文件。

可以在此处找到重现此问题所需的图像:链接已删除,因为问题已得到解答,并且我的服务器没有太多的带宽余量。其他图片来源:https://imgur.com/a/jmbv6

我为以下问题制作了一个可重现的示例。我做错了什么让这变得如此缓慢?

谢谢。

XAML:

<Window x:Class="WpfApplication1.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:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="600" Width="800" WindowState="Maximized">
    <Grid>
        <ListBox x:Name="listBoxItems" ItemsSource="Binding ItemsCollection"
                    ScrollViewer.HorizontalScrollBarVisibility="Disabled">

            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel IsItemsHost="True" />
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>

            <ListBox.ItemTemplate>
                <DataTemplate>
                    <VirtualizingStackPanel>
                        <Image Width="278" Height="178">
                            <Image.Source>
                                <BitmapImage DecodePixelWidth="278" UriSource="Binding ImagePath" CreateOptions="IgnoreColorProfile" />
                            </Image.Source>
                        </Image>
                        <TextBlock Text="Binding Name" FontSize="16" VerticalAlignment="Bottom" HorizontalAlignment="Center" />
                    </VirtualizingStackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

背后的代码:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Threading;

namespace WpfApplication1

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    
        internal class Item : INotifyPropertyChanged
        
            public Item(string name = null)
            
                this.Name = name;
            

            public string Name  get; set; 
            public string ImagePath  get; set; 

            public event PropertyChangedEventHandler PropertyChanged;
            private void NotifyPropertyChanged(String propertyName)
            
                if (PropertyChanged != null)
                
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                
            
        

        ObservableCollection<Item> ItemsCollection;
        List<Item> data;

        public MainWindow()
        
            InitializeComponent();

            this.data = new List<Item>();
            this.ItemsCollection = new ObservableCollection<Item>();
            this.listBoxItems.ItemsSource = this.ItemsCollection;

            for (int i = 0; i < 49; i ++)
            
                Item newItem = new Item
                
                    ImagePath = String.Format(@"Images/0.jpg", i + 1),
                    Name = "Item: " + i
                ;

                this.data.Add(newItem);
            

            foreach (var item in this.data.Select((value, i) => new  i, value ))
            
                Dispatcher.Invoke(new Action(() =>
                
                    this.ItemsCollection.Add(item.value);
                ), DispatcherPriority.Background);
            
        
    

【问题讨论】:

刚刚用 50 张 300-900kb 范围内的图像对其进行了测试,它几乎立即显示出来......但是,我不得不复制一些图像并重命名它们,没有足够的可用测试材料。 小图像可以,是的。正是大而详细的图像让它爬行 @PeterDuniho Aha,看起来 imgur 正在压缩它们。这是全部下载的链接:s.imgur.com/a/jmbv6/zip - 我也会将其添加到 OP @PeterDuniho 是的,我也在这里验证了自己。好吧,这简直是疯了。我比较了两张图片(我的源图片和来自 zip 的一张),文件大小有 3mb 的差异)。看起来图像在上传到 Imgur 时被压缩到了一定程度;/ @PeterDuniho 我将不得不将图像上传到我的站点并提供直接下载,以防止图像完整性。我现在就这样做 【参考方案1】:this.listBoxItems.ItemsSource = this.ItemsCollection; 行移动到方法的末尾应该会有所帮助。 这里发生的情况是,每次执行this.data.Add(newItem) 时,列表都会尝试更新其内容,这涉及大量 I/O(读取磁盘文件并解码相当大的图像)。运行分析器应该可以确认这一点。 更好的方法是从 smaller thumbnail cache 加载(这将需要更少的 I/O),如果这对您的要求可行的话 启用VirtualizingStackPanel.IsVirtualizing 将有助于降低内存需求

Here 是关于此主题的一次讨论,我认为您可能会感兴趣。

【讨论】:

我认为 VirtualizingStackPanel.IsVirtualizing="True" 无论如何都是隐式设置的?移动 itemsource 行没有明显区别。我将查看缩略图缓存。最让我困惑的部分是,您经常看到这种代码在使用中,但是当他们处理数千个项目时,您会听到人们使用更复杂的解决方案。我们在这里只讨论 50 项,所以在我的示例代码中肯定有一些险恶的东西,不是吗? 当然,我想我也可以调整图像的大小并将它们保存为缩略图。我尝试使用批量图像转换器调整图像大小,然后使用这些小图像,它几乎是即时的。以编程方式调整大小和保存到磁盘会需要很长时间吗?同样,使用 50 张图像作为示例数据池。谢谢 @PersuitOfPerfect:我认为延迟可能不是由图像数量或ListBox 的人口造成的,而是简单地读取和解码图像数据。如果不使文件和图像本身更小,就无法加快速度。您可能在异步加载文件方面取得了一些成功,因此至少 UI 保持响应。这是一篇您可能会觉得有用的帖子:***.com/questions/9317460/…。那里没有万无一失的答案,但值得深思。【参考方案2】:

ObservableCollectionList 保留相同的对象时,您不需要它们。删除data 字段。

您没有正确使用VirtualizingStackPanel。默认情况下,ListBox 将其项目可视化。我不明白您为什么将 WrapPanel 用作 ItemsPanel,因为您将 Horizo​​ntalScrollBar 设置为禁用。从最小的变化开始。我的意思是,首先删除VirtualizingStackPanelItemsPanel,看看性能如何变化。您可以稍后更改 ItemsPanel 等。

我不明白您为什么使用Dispatcher.Invoke 来填充ObservableCollection。您已经在当前线程中创建了它。没必要。虚拟化将负责加载图像。

如果有问题请告诉我。

【讨论】:

即使禁用了水平滚动条,这些项目也不会并排显示,除非在包装面板中使用(来自我的测试)。没有它,它们将出现在垂直列表中。每行一项。 目前,您在错误的地方使用了VirtualizingStackPanel。我要说的是从通常的ListBox开始,看看你是否有性能问题。 (我想不是)。然后您可以通过谷歌搜索或提出新问题来思考使用 WrapPanel 或水平 StackPanel 等的正确方法。【参考方案3】:

现在我可以看到您正在使用的图像,我可以确认这里的主要问题只是加载大图像的基本成本。使用这些图像文件根本无法改善那个时间。

您可以做的是异步加载图像,以便在用户等待所有图像加载时至少程序的其余部分是响应的,或者减小图像的大小,以便它们加载快点。如果可能,我强烈推荐后者。

如果出于某种原因需要以原始的大尺寸格式部署和加载图像,那么您至少应该异步加载它们。有很多不同的方法可以做到这一点。

最简单的就是在Image.Source绑定上设置Binding.IsAsync

<ListBox.ItemTemplate>
  <DataTemplate>
    <StackPanel>
      <Image Width="278" Height="178" Source="Binding ImagePath, IsAsync=True"/>
      <TextBlock Text="Binding Name" FontSize="16"
                 VerticalAlignment="Bottom" HorizontalAlignment="Center" />
    </StackPanel>
  </DataTemplate>
</ListBox.ItemTemplate>

这种方法的主要缺点是在使用这种方法时不能设置DecoderPixelWidthImage 控件正在为您处理从路径到实际位图的转换,并且没有设置各种选项的机制。

鉴于该技术的简单性,我认为这是首选方法,至少对我而言。用户通常不会关心完全初始化所有数据的总时间,只要程序有响应并显示出进展的迹象。但是,我确实注意到,在这种情况下,如果不设置 DecoderPixelWidth,加载所有图像所需的时间几乎是原来的两倍(大约 7.5 秒对大约 14 秒)。因此,您可能对自己异步加载图像感兴趣。

这样做需要您可能已经熟悉的常规异步编程技术。主要的“陷阱”是 WPF 位图处理类默认情况下会将位图的实际加载推迟到实际需要时。异步创建位图没有帮助,除非您可以强制立即加载数据。

幸运的是,你可以。只需将CacheOption 属性设置为BitmapCacheOption.OnLoad

我冒昧地清理了您的原始示例,创建了正确的视图模型数据结构,并实现了图像的异步加载。通过这种方式,我获得了不到 8 秒的加载时间,但 UI 在加载过程中保持响应。我包括了几个计时器:一个显示程序启动后经过的时间,主要用于说明 UI 的响应性,另一个显示实际加载位图图像所花费的时间。

XAML:

<Window x:Class="TestSO42639506PopulateListBoxImages.MainWindow"
        x:ClassModifier="internal"
        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:l="clr-namespace:TestSO42639506PopulateListBoxImages"
        mc:Ignorable="d"
        WindowState="Maximized"
        Title="MainWindow" Height="350" Width="525">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition/>
    </Grid.RowDefinitions>
    <StackPanel>
      <TextBlock Text="Binding TotalSeconds, StringFormat=Total seconds: 0:0"/>
      <TextBlock Text="Binding LoadSeconds, StringFormat=Load seconds: 0:0.000"/>
    </StackPanel>

    <ListBox x:Name="listBoxItems" ItemsSource="Binding Data"
             Grid.Row="1"
             ScrollViewer.HorizontalScrollBarVisibility="Disabled">

      <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
          <WrapPanel IsItemsHost="True" />
        </ItemsPanelTemplate>
      </ListBox.ItemsPanel>

      <ListBox.ItemTemplate>
        <DataTemplate>
          <StackPanel>
            <Image Width="278" Height="178" Source="Binding Bitmap"/>
            <TextBlock Text="Binding Name" FontSize="16"
                       VerticalAlignment="Bottom" HorizontalAlignment="Center" />
          </StackPanel>
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </Grid>
</Window>

C#:

class NotifyPropertyChangedBase : INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    protected void _UpdatePropertyField<T>(
        ref T field, T value, [CallerMemberName] string propertyName = null)
    
        if (EqualityComparer<T>.Default.Equals(field, value))
        
            return;
        

        field = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    


class Item : NotifyPropertyChangedBase

    private string _name;
    private string _imagePath;
    private BitmapSource _bitmap;

    public string Name
    
        get  return _name; 
        set  _UpdatePropertyField(ref _name, value); 
    

    public string ImagePath
    
        get  return _imagePath; 
        set  _UpdatePropertyField(ref _imagePath, value); 
    

    public BitmapSource Bitmap
    
        get  return _bitmap; 
        set  _UpdatePropertyField(ref _bitmap, value); 
    


class MainWindowModel : NotifyPropertyChangedBase

    public MainWindowModel()
    
        _RunTimer();
    

    private async void _RunTimer()
    
        Stopwatch sw = Stopwatch.StartNew();
        while (true)
        
            await Task.Delay(1000);
            TotalSeconds = sw.Elapsed.TotalSeconds;
        
    

    private ObservableCollection<Item> _data = new ObservableCollection<Item>();
    public ObservableCollection<Item> Data
    
        get  return _data; 
    

    private double _totalSeconds;
    public double TotalSeconds
    
        get  return _totalSeconds; 
        set  _UpdatePropertyField(ref _totalSeconds, value); 
    

    private double _loadSeconds;
    public double LoadSeconds
    
        get  return _loadSeconds; 
        set  _UpdatePropertyField(ref _loadSeconds, value); 
    


/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
partial class MainWindow : Window

    private readonly MainWindowModel _model = new MainWindowModel();

    public MainWindow()
    
        DataContext = _model;
        InitializeComponent();

        _LoadItems();
    

    private async void _LoadItems()
    
        foreach (Item item in _GetItems())
        
            _model.Data.Add(item);
        

        foreach (Item item in _model.Data)
        
            BitmapSource itemBitmap = await Task.Run(() =>
            
                Stopwatch sw = Stopwatch.StartNew();
                BitmapImage bitmap = new BitmapImage();

                bitmap.BeginInit();
                // forces immediate load on EndInit() call
                bitmap.CacheOption = BitmapCacheOption.OnLoad;
                bitmap.UriSource = new Uri(item.ImagePath, UriKind.Relative);
                bitmap.DecodePixelWidth = 278;
                bitmap.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
                bitmap.EndInit();
                bitmap.Freeze();

                sw.Stop();
                _model.LoadSeconds += sw.Elapsed.TotalSeconds;
                return bitmap;
            );
            item.Bitmap = itemBitmap;
        
    

    private static IEnumerable<Item> _GetItems()
    
        for (int i = 1; i <= 60; i++)
        
            Item newItem = new Item
            
                ImagePath = String.Format(@"Images/0.jpg", i),
                Name = "Item: " + i
            ;

            yield return newItem;
        
    

由于我只是将文件直接从您的 .zip 复制到我的项目目录中,因此我将图像路径循环更改为与那里的实际文件名相对应,即 1-60,而不是原始示例中的 1-49 .我也没有理会基于 0 的标签,而是将其与文件名相同。

我确实环顾四周,看看是否还有其他问题可以直接解决您的问题。我没有找到一个我认为是完全相同的副本,但是有一个非常广泛的,asynchronously loading a BitmapImage in C# using WPF,它展示了许多技术,包括与上述相似或相同的技术。

【讨论】:

精彩的答案。非常感谢您的示例和解释。我也感谢您抽出时间来测试我的示例并在此处提供答案。感谢您对我的坚持,并帮助我在未来形成结构上更好的问题。

以上是关于使用 DataBinding 的 ListBox 填充速度极慢的主要内容,如果未能解决你的问题,请参考以下文章

DataBinding:DataBinding的基本用法

DataBinding学习记录

DataBinding学习记录

DataBinding学习记录

使用 DataBinding 库绑定事件

Offline DataBinding = enable导致错误:找不到databinding-runtime.jar