13

What's the best way to asynchronously load an BitmapImage in C# using WPF?

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
user3837
  • 2,236
  • 2
  • 15
  • 6

6 Answers6

7

I was just looking into this and had to throw in my two cents, though a few years after the original post (just in case any one else comes looking for this same thing I was looking into).

I have an Image control that needs to have it's image loaded in the background using a Stream, and then displayed.

The problem that I kept running into is that the BitmapSource, it's Stream source and the Image control all had to be on the same thread.

In this case, using a Binding and setting it's IsAsynch = true will throw a cross thread exception.

A BackgroundWorker is great for WinForms, and you can use this in WPF, but I prefer to avoid using the WinForm assemblies in WPF (bloating of a project is not recommended, and it's a good rule of thumb too). This should throw an invalid cross reference exception in this case too, but I didn't test it.

Turns out that one line of code will make any of these work:

//Create the image control
Image img = new Image {HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, VerticalAlignment = System.Windows.VerticalAlignment.Stretch};

//Create a seperate thread to load the image
ThreadStart thread = delegate
     {
         //Load the image in a seperate thread
         BitmapImage bmpImage = new BitmapImage();
         MemoryStream ms = new MemoryStream();

         //A custom class that reads the bytes of off the HD and shoves them into the MemoryStream. You could just replace the MemoryStream with something like this: FileStream fs = File.Open(@"C:\ImageFileName.jpg", FileMode.Open);
         MediaCoder.MediaDecoder.DecodeMediaWithStream(ImageItem, true, ms);

         bmpImage.BeginInit();
         bmpImage.StreamSource = ms;
         bmpImage.EndInit();

         //**THIS LINE locks the BitmapImage so that it can be transported across threads!! 
         bmpImage.Freeze();

         //Call the UI thread using the Dispatcher to update the Image control
         Dispatcher.BeginInvoke(new ThreadStart(delegate
                 {
                         img.Source = bmpImage;
                         img.Unloaded += delegate 
                                 {
                                         ms.Close();
                                         ms.Dispose();
                                 };

                          grdImageContainer.Children.Add(img);
                  }));

     };

//Start previously mentioned thread...
new Thread(thread).Start();
Brian
  • 71
  • 1
  • 1
  • 7
    BackgroundWorker is defined in System.ComponentModel namespace of the System.dll, not in Windows Forms, so it's fine to use it for WPF. – Samuel Jack Jan 10 '11 at 14:39
  • Using BackgroundWorker or ThreadStart uses too much code. Just throw a `ThreadPool.QueueUserWorkItem(_ => { ... })` and there is your thread; – SandRock Jan 28 '13 at 14:18
3

Assuming you're using data binding, setting Binding.IsAsync property to True seems to be a standard way to achieve this. If you're loading the bitmap in the code-behind file using background thread + Dispatcher object is a common way to update UI asynchronous

aku
  • 122,288
  • 32
  • 173
  • 203
  • 4
    the bitmapImage must be created on the UI Thread: this will throw an exception... – Jonathan ANTOINE Aug 18 '11 at 09:45
  • 1
    @Jmix90: it's not clear what part of this answer you're referring to. However: using `Binding.IsAsync` allows a Uri/path to be specified in an `Image.Source` and have the image loaded asynchronously automatically. WPF takes care of any cross-thread issues in this scenario. If one is creating bitmaps in a background thread, one simple needs to call `Freeze()` on the bitmap object before handing it off to the UI thread. It only throws an exception if you do it wrong. – Peter Duniho Mar 08 '17 at 01:14
3

This will allow you to create the BitmapImage on the UI thread by using the HttpClient to do the async downloading:

private async Task<BitmapImage> LoadImage(string url)
{
    HttpClient client = new HttpClient();

    try
    {
        BitmapImage img = new BitmapImage();
        img.CacheOption = BitmapCacheOption.OnLoad;
        img.BeginInit();
        img.StreamSource = await client.GetStreamAsync(url);
        img.EndInit();
        return img;
    }
    catch (HttpRequestException)
    {
        // the download failed, log error
        return null;
    }
}
David
  • 429
  • 3
  • 7
2
 BitmapCacheOption.OnLoad

var bmp = await System.Threading.Tasks.Task.Run(() => 
{ 
BitmapImage img = new BitmapImage(); 
img.BeginInit(); 
img.CacheOption = BitmapCacheOption.OnLoad; 
img.UriSource = new Uri(path); 
img.EndInit(); 
ImageBrush brush = new ImageBrush(img); 

}
Cornel Marian
  • 2,443
  • 23
  • 28
2

To elaborate onto aku's answer, here is a small example as to where to set the IsAsync:

ItemsSource="{Binding IsAsync=True,Source={StaticResource ACollection},Path=AnObjectInCollection}"

That's what you would do in XAML.

kevindaub
  • 3,293
  • 6
  • 35
  • 46
  • I dont get why this answer got a -1, since it was just what I was looking for! Couldn't figure out how to do conversion async. Turns out you _just_ have to put in IsAsync=True in your binding. Thank you very much! – Rasive Apr 13 '15 at 13:06
0

Use or extend System.ComponentModel.BackgroundWorker:
http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx

Personally, I find this to be the easiest way to perform asynchronous operations in client apps. (I've used this in WinForms, but not WPF. I'm assuming this will work in WPF as well.)

I usually extend Backgroundworker, but you dont' have to.

public class ResizeFolderBackgroundWorker : BackgroundWorker
{

    public ResizeFolderBackgroundWorker(string sourceFolder, int resizeTo)
    {
        this.sourceFolder = sourceFolder;
        this.destinationFolder = destinationFolder;
        this.resizeTo = resizeTo;

        this.WorkerReportsProgress = true;
        this.DoWork += new DoWorkEventHandler(ResizeFolderBackgroundWorker_DoWork);
    }

    void ResizeFolderBackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        DirectoryInfo dirInfo = new DirectoryInfo(sourceFolder);
        FileInfo[] files = dirInfo.GetFiles("*.jpg");


        foreach (FileInfo fileInfo in files)
        {
            /* iterate over each file and resizing it */
        }
    }
}

This is how you would use it in your form:

    //handle a button click to start lengthy operation
    private void resizeImageButtonClick(object sender, EventArgs e)
    {
        string sourceFolder = getSourceFolderSomehow();
        resizer = new ResizeFolderBackgroundWorker(sourceFolder,290);
        resizer.ProgressChanged += new progressChangedEventHandler(genericProgressChanged);
        resizer.RunWorkerCompleted += new RunWorkerCompletedEventHandler(genericRunWorkerCompleted);

        progressBar1.Value = 0;
        progressBar1.Visible = true;

        resizer.RunWorkerAsync();
    }

    void genericRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        progressBar1.Visible = false;
        //signal to user that operation has completed
    }

    void genericProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        progressBar1.Value = e.ProgressPercentage;
        //I just update a progress bar
    }
bentford
  • 33,038
  • 7
  • 61
  • 57