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.
Th开发者_运维问答is 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.
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.
You need to do two things:
- Freeze your BitmapImage so it can be moved to the UI thread, then
- 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:
- You loose the ability to use your view model independently of a constructed Application object.
- 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));
In the background thread work with Streams.
For example, in the background thread:
var artUri = new Uri("MyProject;component/../Images/artwork.placeholder.png", UriKind.Relative);
StreamResourceInfo albumArtPlaceholder = Application.GetResourceStream(artUri);
var _defaultArtPlaceholderStream = albumArtPlaceholder.Stream;
SendStreamToDispatcher(_defaultArtPlaceholderStream);
In the UI thread:
void SendStreamToDispatcher(Stream imgStream)
{
dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
{
var imageToDisplay = new BitmapImage();
imageToDisplay.SetSource(imgStream);
//Use you bitmap image obtained from a background thread as you wish!
}));
}
精彩评论