2017-03-07 17 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