2

I'm trying to add items (images) to a ListBox, preferably with a WrapPanel type layout. Pretty simple.

The issue I have is that when I define the ListBox.ItemsPanel template to use a wrap panel, the time spent loading the images becomes unbearable. I am adding about 70 images currently, but I'd like this to support thousands.

If I don't customize the listbox, things load pretty darn fast. I've mentioned below in the XAML exactly what part I am changing.

What I don't understand is

  1. Why such a seemingly small xaml change makes the app choke
  2. How I can still get wrappanel type layout with decent perf.

    <Grid>
    <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
    </Grid.RowDefinitions>
    
    <Button Grid.Row=0 Name="ImageButton" Click="ImageButton_Click">GET IMAGES</Button> 
    
    <ListBox Grid.Row="1" Name="ImageCollection" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
        <!-- Items added to list panel using Wrap Panel Template. --> 
        <!-- By removing the below, things go pretty fast --> 
        <!-- When the ListBox.ItemsPanel uses a wrappanel as below, loading up the images is SLOW. If I delete the below, loading is FAST, but isn't what I want.  -->
    
        <ListBox.ItemsPanel>                
            <ItemsPanelTemplate>
                <WrapPanel/>
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
    </ListBox>
    </Grid>
    

My Code behind looks like this. I don't change anything here, but just including it as informational.

    public AsyncGet()
    {
        InitializeComponent();                        
    }

    private List<String> images = new List<string>(
        Directory.GetFiles(@"C:\Users\me\Pictures\wallpapers")
    );

    private void ImageButton_Click(object sender, RoutedEventArgs e)
    {
        foreach (string s in images)
        { 
            Image image = new Image();
            BitmapImage bi = new BitmapImage();
            bi.BeginInit();
            bi.UriSource = new Uri(s);
            bi.EndInit();
            image.Source = bi;
            image.Height = 200;
            image.Width = 200;
            ImageCollection.Items.Add(image);
        }
    }

EDIT

After trying out a number of VirtualWrapPanel implementations that I was not really happy with, I decided to stick with WrapPanel. I did a few things that seem to keep the main thread responsive:

  1. Keep the BitMapImage Decode dimensions and Image dimensions to 200
  2. Begin the loading by adding a default image for each item. The intended path is stored in the Images tag.
  3. Go through each item update the source, using async/await to generate the bitmapImage.

I don't know I'm using async/await the way it was intended. But it seems to allow the UI to remain responsive. The images are cycled over and eventually all defaults are replaced, it may take a little longer than otherwise but I prefer the ability to still take user input.

I wish I could find a way to databind this, I may try it later.

private List<String> images = new List<string>(
    Directory.GetFiles(@"C:\Users\me\Pictures\wallpapers")
);

private Image defaultImage;

public AsyncGet()
{
    InitializeComponent();
    DataContext = this;
    defaultImage = new Image();
    defaultImage.Source = MakeBitmapImage( @"C:\Users\me\Pictures\MeowShawn_Lynch.jpg");
    defaultImage.Height = 200;
    defaultImage.Width = 200;
}

private async void ImageButton_Click(object sender, RoutedEventArgs e)
{
    ImageCollection.Items.Clear();

    foreach(string s in images)
    {
        Image image = new Image();
        image.Source = defaultImage.Source;
        image.Height = defaultImage.Height;
        image.Width = defaultImage.Width;
        ImageCollection.Items.Add(image);
        image.Tag = s;
    }

    foreach (Image image in ImageCollection.Items)
    {
        string path = image.Tag.ToString();
        Task<BitmapImage> task = new Task<BitmapImage>(() => MakeBitmapImage(path, true));
        task.Start();

        image.Source = await task;
    }         
}

    private BitmapImage MakeBitmapImage(string path, Boolean freeze = false)
    {
        BitmapImage bi = new BitmapImage();

        bi.CacheOption = BitmapCacheOption.OnLoad;            
        bi.BeginInit();
        bi.UriSource = new Uri(path);
        bi.DecodePixelHeight = 200;

        bi.EndInit();
        if (freeze)
        {
            bi.Freeze();
        }
        return bi;
    }
Sperry
  • 317
  • 1
  • 3
  • 16

3 Answers3

3

By default ListBox will use VirtualizingStackPanel which does not render elements that aren't in the view at the moment.

The word "virtualize" refers to a technique by which a subset of user interface (UI) elements are generated from a larger number of data items based on which items are visible on-screen. Generating many UI elements when only a few elements might be on the screen can adversely affect the performance of your application. The VirtualizingStackPanel calculates the number of visible items and works with the ItemContainerGenerator from an ItemsControl (such as ListBox or ListView) to create UI elements only for visible items.

WrapPanel does not have that functionality and all items are treated as if they are visible. You can try using this Virtualizing WrapPanel control.

For normal ListBox, with default ItemsPanel, you can control virtualization by setting VirtualizingStackPanel.IsVirtualizing attached property:

<ListBox VirtualizingStackPanel.IsVirtualizing="True" 
dkozl
  • 32,814
  • 8
  • 87
  • 89
  • 2
    If you can get away with it then I'd also recommend setting VirtualizingPanel.VirtualizationMode to "Recycling" so that any items that get created get reused as the user scrolls through the list. – Mark Feldman Feb 02 '14 at 22:40
  • Thanks. I'm still playing around with some ideas here and not really satisfied with the results, but I think this is the best answer as to 'why the wrap panel is slow'. I tried the VirtualizingWrapPanel but it seems a little ...clunky. Scrolling is choppy and weird things happen at the end of the list. – Sperry Feb 04 '14 at 15:24
  • You are right. I've used it on few occasions and it is far from perfect. There is no easy answer to your problem as there is no option to switch on/off. You can play with threads. If you would use bindings you could use [`Binding.IsAsync`](http://msdn.microsoft.com/en-us/library/system.windows.data.binding.isasync(v=vs.110).aspx). If it will load all images at least don't block UI when it's loading. – dkozl Feb 04 '14 at 15:36
  • Also to optimize memory usage, if original image is large, you can use `BitmapImage.DecodePixelWidth`/`BitmapImage.DecodePixelHeight` to limit resolution to which image is decoded in memory – dkozl Feb 04 '14 at 15:40
  • Binding.IsAsync is a tricky thing. I think this is a good example where it helps minimally but is no silver bullet. I get the feeling the I/O portion (where IsAsync helps) is actually pretty fast and the CPU cost is higher than I had estimated. Makes sense, processing those pixels must be expensive. And since this is UI stuff, we have to do this on main thread and get a UI hang. The BEST I've managed is an async/await while creating the bitMapImage on another thread. DecodePixelHeight helps too. Magnitudes better than what I started with. I'll update shortly with final results. – Sperry Feb 04 '14 at 19:41
  • You can load bitmaps on another thread as long as you call `Freeze()` on it before you send it to UI thread with `Dispatcher` – dkozl Feb 04 '14 at 19:54
0

Suggestion: Instead of populating the images collection using the UIElement's name (ImageCollection), implement INotifyPropertyChanged and expose the List of Images as property of the Form, than use Binding (that is the WPF - Way of doing such things) and last but not least check if enabling/diabling virtualization has any impact on your issue...

user1608721
  • 136
  • 1
  • 9
0

If you want to support thousands then consider a class that only loads the BitmapImage on demand
This example only loads the image when Virtualization asks for it

The ListBox has Image with itemsource bound to BitmapImage

Showing items as images in a WPF ListView

public class ImageDefer
{
   private string source;
   private BitmapImage bi;
   public BitmapImage BI 
   {
      get 
      {
         if (bi is null)
         {
            bi = new BitmapImage();
            bi.BeginInit();
            bi.UriSource = new Uri(source);
            bi.EndInit();
         }
         return bi;
      }
    }
    public  ImageDefer (string Source) { source = Source; }   
}
Community
  • 1
  • 1
paparazzo
  • 44,497
  • 23
  • 105
  • 176