使用 DataBinding 的 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);
            }
        }
    }
}
彼得·杜尼霍

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

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

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

最简单的是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>

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

考虑到该技术的简单性,我认为这是首选方法,至少对我而言。用户通常不会关心完全初始化所有数据的总时间,只要程序响应迅速并显示出进展迹象。但是,我确实注意到,如果没有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 的标签,而是将其设置为与文件名相同。

我确实环顾了一下四周,看看是否还有另一个问题可以直接解决您的问题。我没有找到一个我认为是完全重复的,但有一个非常广泛的,使用 WPF 在 C# 中异步加载 BitmapImage,它显示了许多技术,包括与上述类似或相同的技术。

本文收集自互联网,转载请注明来源。

如有侵权,请联系[email protected] 删除。

编辑于
0

我来说两句

0条评论
登录后参与评论

相关文章

来自分类Dev

使用 DataBinding 时在 RadGrid 内显示 ListBox

来自分类Dev

MVVM模式中的WPF DataBinding ListBox

来自分类Dev

MVVM模式中的WPF DataBinding ListBox

来自分类Dev

查询速度极慢 - 使用 google sql cloud

来自分类Dev

使用存储过程填充@ html.listbox

来自分类Dev

Windows Phone-使用Json填充ListBox

来自分类Dev

使用 Gparted 调整分区后,Ubuntu 启动速度极慢

来自分类Dev

在VB.Net上使用参数化查询填充ListBox

来自分类Dev

使用DataBinding设置android:textAppearance

来自分类Dev

从行数组填充ListBox

来自分类Dev

Json填充Dual ListBox

来自分类Dev

Json填充Dual ListBox

来自分类Dev

在jQuery中使用ListBox

来自分类Dev

使用DataBinding访问整数数组

来自分类Dev

如何正确使用DataBinding,InotifyPropertyChanged,ListViewGridView

来自分类Dev

使用DataBinding Xamarin表单旋转标签

来自分类Dev

如何正确使用DataBinding,InotifyPropertyChanged,ListViewGridView

来自分类Dev

使用DataBinding创建自定义控件

来自分类Dev

在 Android 中使用 DataBinding 更新 UI 属性

来自分类Dev

替换na.locf.xts(与多列xts一起使用时速度极慢)

来自分类Dev

填充ListBox DataTemplate的性能更好

来自分类Dev

如何填充backgroundworker中的ListBox?

来自分类Dev

VBA-无法填充ListBox

来自分类Dev

VBA - 从多个 ListObjects 填充 ListBox

来自分类Dev

如何使用mvc 4用Ajax结果填充Html.Listbox

来自分类Dev

如何仅使用一种方法来填充ComBobox和ListBox的项目?

来自分类Dev

用List(T)类填充ListBox。不使用数据源

来自分类Dev

MVC 4 ListBox填充另一个ListBox

来自分类Dev

CSV生成速度极慢