I have been looking into ways to minimize the performance impact on binding/loading images to a list.
I have tried a few different options that people suggest online, such as:
or using a separate thread to load the list of Images into ImageSource then bind to the list view
and/or priority binding with the main source marked as async (i didn't see any improvement when I use this approach alone, tbh)
they all have some level of improvement and I ended up with something like the following: a custom Image control with async loading
public class AsyncImage : Image
{
private static ImageSource _blank;
private static Random random;
private static ImageSource Blank
{
get
{
if(_blank == null)
{
var bi = new BitmapImage();
bi.BeginInit();
Stream imgStream = File.OpenRead("D:\\...\\blankLoading.png");
bi.StreamSource = imgStream;
bi.EndInit();
bi.Freeze();
_blank = bi;
}
return _blank;
}
}
public string ImageUrl
{
get { return GetValue(ImageUrlProperty).ToString(); }
set
{
SetValue(ImageUrlProperty, value);
LoadImageAsync(value);
}
}
public static readonly DependencyProperty ImageUrlProperty =
DependencyProperty.Register("ImageUrl", typeof(string), typeof(AsyncImage), new UIPropertyMetadata(string.Empty, new PropertyChangedCallback(OnImageUrlChanged)));
private static void OnImageUrlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
AsyncImage control = (AsyncImage)d;
control.ImageUrl = e.NewValue.ToString();
}
private void LoadImageAsync(string url)
{
Image image = new Image();
base.Source = Blank;
ThreadPool.QueueUserWorkItem((r) =>
{
BitmapImage bi = null;
bi = new BitmapImage();
bi.BeginInit();
var bytes = File.ReadAllBytes(url); //i want to make sure data is loaded before assignment
bi.StreamSource = new MemoryStream(bytes);
bi.EndInit();
bi.Freeze(); //makes sure your image can be passed across threads
Console.Write(url);
image.Dispatcher.Invoke(DispatcherPriority.Normal,
(ThreadStart)delegate
{
base.Source = bi; //this is where it actually comes back to UI thread
});
});
}
}
with this approach, I can just bind a list of URLs or file paths to a list view and the data binding will perform a lot smoother... HOWEVER, it always causes a little freeze on the UI when the actual image is been shown (at the last line). the freeze is more noticeable when you have a good list of last images.
Is there a way to solve this issue? all I want is that the UI will still be responsive while images show up...
CodePudding user response:
There are a few things wrong with your code. You must not call LoadImageAsync()
in the setter of the ImageUrl
property. The method must instead be called in OnImageUrlChanged
.
In LoadImageAsync you are creating an Image element only to use its Dispatcher, which is totally pointless. Use the Dispatcher of your custom control, like this.Dispatcher
. Also write this.Source
instead of base.Source
.
You must also close the stream from wich a BitmapImage is loaded. Set bi.CacheOption = BitmapCacheOption.OnLoad
and dispose of the MemoryStream by a using
block.
That said, consider using a more modern implementation without ThreadPool.QueueUserWorkItem
and Dispatcher.Invoke
.
Use Task.Run
instead:
public class AsyncImage : Image
{
public static readonly DependencyProperty ImagePathProperty =
DependencyProperty.Register(
nameof(ImagePath), typeof(string), typeof(AsyncImage),
new PropertyMetadata(async (o, e) =>
await ((AsyncImage)o).LoadImageAsync((string)e.NewValue)));
public string ImagePath
{
get { return (string)GetValue(ImagePathProperty); }
set { SetValue(ImagePathProperty, value); }
}
private async Task LoadImageAsync(string imagePath)
{
Source = await Task.Run(() =>
{
using (var stream = File.OpenRead(imagePath))
{
var bi = new BitmapImage();
bi.BeginInit();
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.StreamSource = stream;
bi.EndInit();
bi.Freeze();
return bi;
}
});
}
}