2017-03-07 19 views
2

我以前使用后面的代码手动添加项目到我的ListBox,但它非常缓慢。就性能而言,我听说通过XAML进行数据绑定是最好的选择。使用DataBinding的ListBox的极端缓慢人口

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

这个想法是,我的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); 
      } 
     } 
    } 
} 
+0

刚刚用范围为300-900kb的50幅图像对其进行了测试,并且它几乎立即显示...但是,我不得不复制一些图像并重命名它们,没有足够的测试材料可用。 – grek40

+0

小图像的确如此。这是一个庞大而详细的图像,让它爬行 – PersuitOfPerfection

+0

@PeterDuniho阿哈,看起来像imgur正在压缩他们然后什么的。这里是链接全部下载:http://s.imgur.com/a/jmbv6/zip - 我还会将其添加到OP – PersuitOfPerfection

回答

1

现在我能够看到您正在使用的图像,我可以确认这里的主要问题仅仅是加载大图像的基本成本。使用这些图像文件时,根本没有办法改善。

你可以做的是加载图像异步,以便至少其余的程序是响应,而用户等待所有的图像加载,或减少图像的大小,使他们加载更快。如果可能的话,我强烈建议后者。

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

最简单的是设置Binding.IsAsyncImage.Source绑定:

<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,显示了一些技术,包括与上述相似或相同的技术。

+0

精彩的回答。非常感谢这个例子和解释。我也很高兴你花了一天的时间来测试我的例子,并在这里提供答案。感谢你坚持和我一起,并帮助我在未来形成结构更好的问题。 – PersuitOfPerfection

0
  • 移动线this.listBoxItems.ItemsSource = this.ItemsCollection;的方法到底应该一点点帮助。
  • 这里发生的情况是,每次执行this.data.Add(newItem)时,该列表都试图更新其内容,这涉及到大量的I/O(读取磁盘文件并解码相当大的图像)。运行一个分析器应该证实这一点。
  • 更好的方式是从smaller thumbnail cache加载(这将需要更少的I/O),如果这是你的要求
  • 启用VirtualizingStackPanel.IsVirtualizing将有助于保持内存的要求低

Here可行的是在一个讨论这个话题我想你可能会觉得有趣。

+0

我认为VirtualizingStackPanel.IsVirtualizing =“True”是隐式设置的,不管?移动项目源代码行没有明显的区别。我会研究缩略图缓存。让我感到困惑的部分是你经常看到这种类型的代码,但是当你听到有数千个项目的人使用更复杂的解决方案时,你会听到这样的声音。我们在这里只讨论了50个项目,所以在我的示例代码中肯定存在一些阴险的问题,不是吗? – PersuitOfPerfection

+0

当然,我想我也可以调整图像大小并将它们保存为缩略图。我尝试过使用批量图像转换器调整图像大小,然后使用这些小图像,这几乎是瞬间的。以编程方式调整大小并保存到磁盘需要很长时间?再次使用50个图像作为示例数据池。谢谢 – PersuitOfPerfection

+0

@PersuitOfPerfection:我认为延迟不是由图像数量和'ListBox'的数量造成的,而是简单地读取和解码图像数据。如果不将文件和图像缩小,您无法做到这一点。您可能会成功加载文件,以便至少UI保持响应。以下是您可能会觉得有用的帖子:http://stackoverflow.com/questions/9317460/bitmap-performance-optimization-patterns。在那里没有确定的答案,但需要思考。 –

0
  • 你并不需要一个ObservableCollectionList,当两个保持相同的对象。删除data字段。

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

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

让我知道是否有问题。

+0

即使水平滚动条被禁用,项目也不会并排显示,除非在包装面板中使用(从我的测试中)。没有它,它们出现在一个垂直列表中。每行一个项目。 – PersuitOfPerfection

+0

目前,您在错误的地方使用“VirtualizingStackPanel”。我想说的是从通常的ListBox开始,看看你是否有性能问题。 (我想不是)。然后,您可以通过搜索或提出新问题来思考使用WrapPanel或水平StackPanel等的正确方法。 – Ron