views:

108

answers:

3

I have a background thread that generates a series of BitmapImage objects. Each time the background thread finishes generating a bitmap, I would like to show this bitmap to the user. The problem is figuring out how to pass the BitmapImage from the background thread to the UI thread.

This is an MVVM project, so my view has an Image element:

<Image Source="{Binding GeneratedImage}" />

My view-model has a property GeneratedImage:

private BitmapImage _generatedImage;

public BitmapImage GeneratedImage
{
    get { return _generatedImage; }
    set
    {
        if (value == _generatedImage) return;
        _generatedImage= value;
        RaisePropertyChanged("GeneratedImage");
    }
}

My view-model also has the code that creates the background thread:

public void InitiateGenerateImages(List<Coordinate> coordinates)
{
    ThreadStart generatorThreadStarter = delegate { GenerateImages(coordinates); };
    var generatorThread = new Thread(generatorThreadStarter);
    generatorThread.ApartmentState = ApartmentState.STA;
    generatorThread.IsBackground = true;
    generatorThread.Start();
}

private void GenerateImages(List<Coordinate> coordinates)
{
    foreach (var coordinate in coordinates)
    {
        var backgroundThreadImage = GenerateImage(coordinate);
        // I'm stuck here...how do I pass this to the UI thread?
    }
}

I'd like to somehow pass backgroundThreadImage to the UI thread, where it will become uiThreadImage, then set GeneratedImage = uiThreadImage so the view can update. I've looked at some examples dealing with the WPF Dispatcher, but I can't seem to come up with an example that addresses this issue. Please advise.

+4  A: 

The following uses the dispatcher to execute an Action delegate on the UI thread. This uses a synchronous model, the alternate Dispatcher.BeginInvoke will execute the delegate asynchronously.

var backgroundThreadImage = GenerateImage(coordinate);

GeneratedImage.Dispatcher.Invoke(
        DispatcherPriority.Normal,
        new Action(() =>
            {
                GeneratedImage = backgroundThreadImage;
            }));

UPDATE As discussed in the comments, the above alone will not work as the BitmapImage is not being created on the UI thread. If you have no intention of modifying the image once you have created it you can freeze it using Freezable.Freeze and then assign to GeneratedImage in the dispatcher delegate (the BitmapImage becomes read-only and thus threadsafe as a result of the Freeze). The other option would be to load the image into a MemoryStream on the background thread and then create the BitmapImage on the UI thread in the dispatcher delegate with that stream and the StreamSource property of BitmapImage.

Simon Fox
This is helpful, Simon, thanks. But how do I get around the fact that, when this process begins, `GeneratedImage` is not set to an instance of an object?
DanM
@DanM of course I didn't think about that :) You can also get hold of a Dispatcher via `Application.Current.Dispatcher`
Simon Fox
I'm afraid this doesn't do it either. I get "The calling thread cannot access this object because a different thread owns it."
DanM
Looks like you will have to create the BitmapImage on the UI thread...move the call to GenerateImage into the delegate the dispatcher is executing on the UI thread.
Simon Fox
in order to do as much of the work as possible in the background thread you could read the image into a memory stream and then in the dispatcher delegate create the BitmapImage using the stream and BitmapImage.StreamSource
Simon Fox
Simon, just putting most of the code back into the UI thread was the key. My goal in this case really wasn't to make the UI more responsive, it was just to get the UI to update after each image was processed. (Without a background thread, it just waited until the last image to update.)
DanM
+3  A: 

You need to do two things:

  1. Freeze your BitmapImage so it can be moved to the UI thread, then
  2. Use the Dispatcher to transition to the UI thread to set the GeneratedImage

You'll need to access the UI thread's Dispatcher from the generator thread. The most flexible way to do this is to capturing the the value Dispatcher.CurrentDispatcher in the main thread and passing it into the generator thread:

public void InitiateGenerateImages(List<Coordinate> coordinates)   
{
  var dispatcher = Dispatcher.CurrentDispatcher;

  var generatorThreadStarter = new ThreadStart(() =>
     GenerateImages(coordinates, dispatcher));

  ...

If you know you will only use this within a running Application and that the application will only have one UI thread, you can just call Application.Current.Dispatcher to get the current dispatcher. The disadvantages are:

  1. You loose the ability to use your view model independently of a constructed Application object.
  2. You can only have one UI thread in your application.

In the generator thread, add a call to Freeze after the image is generated, then use the Dispatcher to transition to the UI thread to set the image:

var backgroundThreadImage = GenerateImage(coordinate);

backgroundThreadImage.Freeze();

dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
{
   GeneratedImage = backgroundThreadImage;
}));

Clarification

In the above code it is critical that Dispatcher.CurrentDispatcher be accessed from the UI thread, not from the generator thread. Every thread has its own Dispatcher. If you call Dispatcher.CurrentDispatcher from the generator thread you will get its Dispatcher instead of the one you want.

In other words, you must do this:

  var dispatcher = Dispatcher.CurrentDispatcher;

  var generatorThreadStarter = new ThreadStart(() =>
     GenerateImages(coordinates, dispatcher));

and not this:

  var generatorThreadStarter = new ThreadStart(() =>
     GenerateImages(coordinates, Dispatcher.CurrentDispatcher));
Ray Burns
+1. Thanks, Ray. I like your tip to pass in the dispatcher. The Freeze() concept is also critical, something I know I'll need in the future. (As I commented to Simon, freeing up the UI thread wasn't really my goal in this case anyway--I was just trying to get it to update (render) each image rather than waiting until all images were processed to update.)
DanM
Ray, I just tried your idea of passing in the dispatcher, but for some reason, `Dispatcher.CurrentDispatcher` is not equivalent to `App.Current.Dispatcher`. If I use `App.Current.Dispatcher`, my code works perfectly, but if I pass in `Dispatcher.CurrentDispatcher`, I get "The calling thread cannot access this object because a different thread owns it." Any idea why this might be?
DanM
Yes, you are calling Dispatcher.CurrentDispatcher from the generator thread not the UI thread. I've added a clarification to the answer to explain how to fix it.
Ray Burns
That was it. Thanks!
DanM
A: 

Maybe somebody could post a little sample App? I don't get through...

Thomas Spranger