7

I would like to load Gravatar-Images and set them from code behind to a WPF Image-Control. So the code looks like

imgGravatar.Source = GetGravatarImage(email);

Where GetGravatarImage looks like:

BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.UriSource = new Uri( GravatarImage.GetURL( "http://www.gravatar.com/avatar.php?gravatar_id=" + email) , UriKind.Absolute );
bi.EndInit();
return bi;

Unfortunately this locks the GUI when the network connection is slow. Is there a way to assign the image-source and let it load the image in the background without blocking the UI?

Thanks!

zpete
  • 1,725
  • 2
  • 18
  • 31

2 Answers2

26

I suggest you to use a Binding on your imgGravatar from XAML. Set IsAsync=true on it and WPF will automatically utilize a thread from the thread pool to pull your image. You could encapsulate the resolving logic into an IValueConverter and simply bind the email as Source

in XAML:

<Window.Resouces>
    <local:ImgConverter x:Key="imgConverter" />
</Window.Resource>

...


<Image x:Name="imgGravatar" 
       Source="{Binding Path=Email, 
                        Converter={StaticResource imgConverter}, 
                        IsAsync=true}" />

in Code:

public class ImgConverter : IValueConverter
{
    public override object Convert(object value, ...)
    {
        if (value != null)
        {
             BitmapImage bi = new BitmapImage();
             bi.BeginInit();
             bi.UriSource = new Uri( 
                 GravatarImage.GetURL(
                     "http://www.gravatar.com/avatar.php?gravatar_id=" + 
                      value.ToString()) , UriKind.Absolute 
                 );
             bi.EndInit();
             return bi;                
        }
        else
        {
            return null;
        }

    }
}
JanW
  • 1,799
  • 13
  • 23
  • 2
    BitmapImage also has a [constructor with Uri parameter](http://msdn.microsoft.com/en-us/library/ms602473.aspx), which would save you the BeginInit/EndInit calls. – Clemens Apr 16 '13 at 19:58
  • +1 did not know about it, nice hint. I just copied the code from the author and did not think on how to optimize the actual downloading, instead I focused on resolving the UI block issue in an elegant way (and of course it blocks, if the code is not async). I think for repetitive usage, the converter could also get the complete url from the binding to be more reusable, instead of the email. I dislike using `ThreadPool` because you have to manage the dispatching all the time. This makes it not elegant enough to me for a pure MVVM application. – JanW Apr 17 '13 at 09:37
  • Can you give an explanation why creating a BitmapImage would block when not called asynchronously. Why then does it have the `IsDownloading` property and `DownloadCompleted` event. If you simply set an Image control's `Source` property in XAML to a URL that references a large image on the web, the UI won't block. Instead WPF downloads the image in the background and shows it as soon as the download has finished. – Clemens Apr 17 '13 at 15:48
  • And I'm pretty sure that this is performed by BitmapImage, as (from MSDN) "BitmapImage primarily exists to support Extensible Application Markup Language (XAML) syntax and introduces additional properties for bitmap loading that are not defined by BitmapSource". – Clemens Apr 17 '13 at 15:51
  • I testet around a bit with a huge image (14MB) and you are absolutely right. Downloading is automatically performed by `BitmapImage` on a background thread. The UI only hangs in the moment of loading the image data into the UI. Sorry for the confusion. The downloading starts after the `BitmapImage` is assigned as `Source` and not with `BeginInit()`. So basically the original code from the author should work. So maybe it has something todo with the amount of images he uses on his UI. – JanW Apr 18 '13 at 09:39
  • Just came along here again, and (from current experience) I have to add that although you set `IsAsync` to true on the binding, this will only affect how the property getter is called, but not the binding converter. The converter is still called on the UI thread. It does simply not block the UI thread because `BitmapImage` doesn't. – Clemens Apr 21 '14 at 19:36
  • This is awesome! Is there also a LoadEvenFaster=True setting that's not set to true by default? – Matt Becker Mar 27 '17 at 12:59
  • I don't really agree, I have a `ListView` to display a collection of images and I use `IsAsync="true"` which causes all the Images to flickering when I add or remove list elements. When I use `IsAsync="false"`, the UI does not freeze as a result, I believe the `Image` control comes with an asynchronous loading method. The official documentation may be ambiguous. – CodingNinja Aug 19 '22 at 14:02
10

I can't see why your code would block the UI, as BitmapImage supports downloading image data in the background. That's why it has an IsDownloading property and a DownloadCompleted event.

Anyway, the following code shows a straightforward way to download and create the image entirely in a separate thread (from the ThreadPool). It uses a WebClient instance to download the whole image buffer, before creating a BitmapImage from that buffer. After the BitmapImage is created it calls Freeze to make it accessible from the UI thread. Finally it assigns the Image control's Source property in the UI thread by means of a Dispatcher.BeginInvoke call.

ThreadPool.QueueUserWorkItem(
    o =>
    {
        var url = GravatarImage.GetURL(
           "http://www.gravatar.com/avatar.php?gravatar_id=" + email);
        var webClient = new WebClient();
        var buffer = webClient.DownloadData(url);
        var bitmapImage = new BitmapImage();

        using (var stream = new MemoryStream(buffer))
        {
            bitmapImage.BeginInit();
            bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
            bitmapImage.StreamSource = stream;
            bitmapImage.EndInit();
            bitmapImage.Freeze();
        }

        Dispatcher.BeginInvoke((Action)(() => image.Source = bitmapImage));
    });

EDIT: today you would just use async methods:

var url = GravatarImage.GetURL(
    "http://www.gravatar.com/avatar.php?gravatar_id=" + email);
var httpClient = new HttpClient();
var responseStream = await httpClient.GetStreamAsync(url);
var bitmapImage = new BitmapImage();

using (var memoryStream = new MemoryStream())
{
    await responseStream.CopyToAsync(memoryStream);

    bitmapImage.BeginInit();
    bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
    bitmapImage.StreamSource = memoryStream;
    bitmapImage.EndInit();
    bitmapImage.Freeze();
}

image.Source = bitmapImage;
Clemens
  • 123,504
  • 12
  • 155
  • 268