Home > Net >  WPF UI performance impact when loading images
WPF UI performance impact when loading images

Time:12-24

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:

https://social.msdn.microsoft.com/Forums/en-US/7fc238ea-194e-4f29-bcbd-9a3d4bdb2180/async-loading-of-bitmapsource-in-value-converter?forum=wpf

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;
            }
        });
    }
}
  • Related