I'm building a little app I call "MediaBarrage," which displays rapid-fire images of J.R. "Bob" Dobbs and associated artwork. In the background, behind a bunch of bewildering animation, I have a slide show which displays all the images in a directory one by one. I want this control to load and display images without interrupting other animation in the app.
The answer, of course, is to do all the high-latency work on a background thread. This turns out to be a little trickier than I expected, but with the help of some guys on the WPF team, I got it working.
The SlideShow control is just an Image control with some async capability. The trick is to assign the Source property correctly. I have a BackgroundWorker which sleeps for awhile, then wakes up and loads a BitmapImage from the file system and assigns it to the Source property. However, making this assignment directly from the background thread gives me a thread-access exception.
Thread access exception raised by calling into a UI element from off-thread.
This is by design and enforces safe access to UI elements.
So how to make the assignment correctly? The key is to use the Dispatcher.BeginInvoke method. My first thought was that this method would invoke my delegate on a threadpool thread, but that turns out not to be the case; this method simply schedules a delegate for later execution on the main UI thread.
How do we use BeginInvoke? The good people in the WPF group gave me the magical code, which I jacked into my DoWork event handler.
void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
BackgroundWorker bw = sender as BackgroundWorker;
BitmapImage bi = this.GetNextImage();
this.Source = param as BitmapImage;
The emphasized code is the important bit. The Source assignment happens inside an anonymous delegate, which is executed by the Dispatcher sometime after the BeginInvoke call happens.
Before all this works, however, there's one more little detail. I need to call the Freeze method to make the new BitmapImage read-only and therefore thread-safe. I do this in a static LoadImage method.
private static BitmapImage LoadImage(
int DecodePixelHeight )
BitmapImage bi = new BitmapImage();
bi.DecodePixelWidth = DecodePixelWidth;
bi.DecodePixelHeight = DecodePixelHeight;
bi.UriSource = new Uri( fullPath, UriKind.Absolute );
This is called by the GetNextImage method. With all this in place, the images load and are correctly passed between threads. The SlideShow control doesn't interfere with the other animation in the app, and everything looks smooth and nice.
Thanks to Adam Smith, Atanas Koralski, and the WPF team for setting me straight on this.