Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is <Image Source='...'> so slow, and what can I do about it?

Tags:

caching

wpf

Considering the following sample XAML file, which shows the first 1000 people of Facebook, starting with markz as 4th person. Note that this is only a sample. Any Window with 1000 element, no matter how you construct it, is a good demonstration.

<Window x:Class="SO.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:clr="clr-namespace:System;assembly=mscorlib"
        Title="MainWindow" Height="350" Width="525">
    <ListBox ItemsSource="{Binding}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Image Source="{Binding}" />
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Window>

And the code behind:

public partial class MainWindow : Window
{
    public MainWindow() {
        InitializeComponent();
        string[] urls = new string[1000];
        for (int i = 0; i < 1000; ++i) {
            urls[i] = "http://graph.facebook.com/" + i + "/picture";
        }
        this.DataContext = urls;
    }
}

On a very reasonable desktop and high speed connection, the program is extremely slow. Trying to scroll with the ScrollBar ... say to the middle, will take 30 seconds. Hitting the 'Home' and 'End' keys will take significant amount of time.

This isn't a first-time-only-get-images-to-the-cache issue. Going back and forth and seeing pictures already presented is somewhat faster but generally very slow. It appears that nothing is stored in cache, closing the application and restarting it, everything is slow again.

The equivalent HTML code is bleezing fast. Some slowness first time, but then everything is very fast.

What is going on? Does element use any caching at all? Does the list do any pre-fetch of the images not currently presented? Is there anyway to tell it to do? Is it really that my only solution is to manage Bitmap objects myself, along with caching and prefetching logic? If so, any previous work I can incorporate?

EDIT (summary):

  1. @H.B. answer to turn off virtualization will give you best result. The entire listbox is rendered as soon as the Window load, and no image is re-computed
  2. @Phil code works great, and it improves performance, especially when going back and forth.
  3. Without any additional code, WPF will not cache the images between invocation. The WinINET cache is NOT used. Although the request comes with Cache instruction in the HTTP Header, WPF does nothing with it.
like image 843
Uri Avatar asked Feb 25 '12 07:02

Uri


2 Answers

ListBoxes virtualize the items by default, so if you scroll down the items are created on the fly. At first it needs to download the image, then it decodes it. If you have scrolled through all the images they may be cached but the ListBox will still recreate the Image controls and hence the images need to be decoded again every time.

You could turn off virtualization by setting the VirtualizingStackPanel.IsVirtualizing attached property to false on the ListBox then everything will be loaded right away, or you can change the VirtualizationMode to Recycling, then the Images (and containing ListBoxItems) won't be thrown away once created.

like image 177
H.B. Avatar answered Nov 04 '22 07:11

H.B.


An alternative would be to add your own image caching so that images are only download once.

Using my example you would put this in your constructor

this.DataContext = new ViewModel();

The following class would store the url and then download the image when the Image property was first accessed.

public class CachingImage
{
    private readonly Uri _uri;
    public CachingImage(string uriString)
    {
        _uri = new Uri(uriString, UriKind.RelativeOrAbsolute);
    }

    private BitmapImage _image;

    public ImageSource Image
    {
        get
        {
            if (_image == null)
            {
                _image = new BitmapImage(_uri);
                _image.DownloadCompleted += (sender, args) => ((BitmapImage)sender).Freeze();
            }

            return _image;
        }
    }
}

Here's the view model

public class ViewModel
{
    public ViewModel()
    {
        Images = Enumerable.Range(1, 1000).Select(i => new CachingImage("http://graph.facebook.com/" + i + "/picture"));
    }

    public IEnumerable<CachingImage> Images { get; private set; }
    ...

and of course you would need to change your xaml slightly

<ListBox ItemsSource="{Binding}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Image Source="{Binding Image}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>
like image 24
Phil Avatar answered Nov 04 '22 07:11

Phil