2

I've encountered a problem with displaying an image from the web in my WPF app:

I used a BitmapImage for this task. My first attempt was to execute it in the UI thread but I quickly understood that's a no-no since the application became unresponsive until the image was completely loaded. My second attempt was to use a BackgroudWorker:

var worker = new BackgroundWorker();
worker.DoWork += worker_LoadImage;
worker.RunWorkerCompleted+=worker_RunWorkerCompleted;
worker.RunWorkerAsync(someURI);

and the worker functions:

    private void worker_LoadImage(object sender, DoWorkEventArgs e)
    {
        var image = new BitmapImage(); 
        image.BeginInit();
        image.CacheOption = BitmapCacheOption.OnDemand;
        image.UriSource = e.Argument as Uri;
        image.DownloadFailed += new EventHandler<ExceptionEventArgs>(image_DownloadFailed);
        image.EndInit();
        e.Result = image;
    }

    void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        //if I understand correctly, this code runs in the UI thread so the 
        //access to the component image1 is valid.
        image1.Source = e.Result as BitmapImage;
    }

after that, I still got an InvalidOperationException: "The calling thread cannot access this object because a different thread owns it." I've researched a bit and found out that since BitmapImage is Freezable, I have to call Freeze before accessing the object from another thread. So I've tried to replace the last row in worker_LoadImage with:

        image.Freeze();
        e.Result = image;

But then I got an exception that my image cannot be frozen, I found out that it's probably because the image wasn't done being downloaded when I tried to invoke Freeze(). So I added the following code to the image creation:

        image.DownloadCompleted += image_DownloadCompleted;

where:

 void image_DownloadCompleted(object sender, EventArgs e)
 {
     BitmapImage img = (BitmapImage)sender;
     img.Freeze();
 }

So now we get to the real question: How do I make the background worker to wait until the image is completely downloaded and the event is fired?

I've tried many things: looping while the image's isDownloading is true, Thread.Sleep, Thread.Yield, Semaphores, Event wait handles and more. I dont know how the image downloading actually works behind the scenes but what happens when I try one of the methods above is that the image never finishes to download (isDownloading is stuck on True)

Is there a better, simpler way to achieve the rather simple task im trying to accomplish?

Some things to notice:

  1. this answer actually works, but only once: when I try to load another image it says the dispatcher is closed. Even after reading a bit about Dispatchers, I don't really understand how the OP achieved that or if it's possible to extend the solution for more than one image.

  2. When I put a message box before the worker exits his DoWork function, I click OK and the image apears which means the download continued while the message box was opened and finished before I clicked OK.

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
Block
  • 23
  • 4
  • BitmapImage already loads the image asynchronously. All you need to do is `image1.Source = new BitmapImage(someURI);`. For performing the entire image loading in a separate thread, see [this answer](http://stackoverflow.com/a/16041810/1136211). – Clemens Dec 17 '14 at 07:13

1 Answers1

1

Given that you're using the bitmap's ability to asynchronously load the image you don't need a BackgroundWorker in the first place. Rather than creating a BGW to start an asynchronous operation and wait for it to finish, just use the asynchronous operation directly.

All you have to do is update the UI from your image_DownloadCompleted handler (after freezing the image) and you no longer need a BGW anymore:

private void FetchImage(Uri uri)
{
    var context = SynchronizationContext.Current;
    var image = new BitmapImage();
    image.BeginInit();
    image.CacheOption = BitmapCacheOption.OnDemand;
    image.UriSource = uri;
    image.DownloadFailed += image_DownloadFailed;
    image.DownloadCompleted += (s, args) =>
    {
        image.Freeze();
        context.Post(_ => image1.Source = image, null);
    };
    image.EndInit();
}
Servy
  • 202,030
  • 26
  • 332
  • 449
  • I feel so stupid.. Thanks! I went back and checked: the first BitmapImage is blocking and makes the whole screen freeze for a couple of seconds, but the other images indeed load smoothly and asynchronously. Is there a reason why such thing would happen? is there any way to prevent it? – Block Dec 16 '14 at 21:21
  • @Block Where is it blocking? – Servy Dec 16 '14 at 21:24
  • I'm not 100% sure since I'm using message boxes to see where's the freezing, but I think it happens in the: image.EndInit(); line – Block Dec 16 '14 at 21:33
  • @Block I'm not sure what would cause that, but you would be able to wrap everything after `var context ...` in a call to `Task.Run` and run the task-creation code in another thread, if it's blocking the UI for some reason. It's a bit of a hack, but if that's the problem then it'd work just fine. – Servy Dec 16 '14 at 21:35
  • I'm not sure I fully understand what you mean (sorry im quite new at this) I tried putting everything after the context declaration in Task.Run, but obviously this doesn't work since the new thread and the image declaration within it are destroyed before the download can be completed. Can you please explain what you mean? – Block Dec 16 '14 at 22:02
  • @Block They're not destroyed. They go out of scope, but they won't be collected because the created thread is rooted, even if it's not accessible from the UI thread. – Servy Dec 16 '14 at 22:12
  • Then why is the DownloadCompleted event never fired this way? – Block Dec 16 '14 at 22:15