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!

2 Answers 2

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;
        }

    }
}
Sign up to request clarification or add additional context in comments.

8 Comments

BitmapImage also has a constructor with Uri parameter, which would save you the BeginInit/EndInit calls.
+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.
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.
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".
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.
|
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;

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.