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
- Why such a seemingly small xaml change makes the app choke
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:
- Keep the BitMapImage Decode dimensions and Image dimensions to 200
- Begin the loading by adding a default image for each item. The intended path is stored in the Images tag.
- 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;
}