I leave for the PDC tomorrow morning, but there’s time for one more cool Silverlight trick before I leave.
One of the features added to Silverlight 2 late in the development cycle was the CompositionTarget.Rendering event. CompositionTarget.Rendering is essentially a per-frame rendering callback that lets you build high-performance animation loops driven by logic executed in each frame. I’ve been looking for an excuse to use it but hadn’t come across a genuine need for it. Not, that is, until today, when I suddenly found that I needed it very badly—and for something that had nothing to do with animations, but everything to do with how rendering works in Silverlight.
Consider the following code, which uses OpenFileDialog to initialize a collection of XAML image objects with image files selected by the user:
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = “JPEG Files (*.jpg;*.jpeg)|*.jpg;*.jpeg|PNG Files (*.png)|*.png|All Files (*.*)|*.*”;
ofd.FilterIndex = 1;
ofd.Multiselect = true;
if ((bool)ofd.ShowDialog())
{
foreach (FileInfo fi in ofd.Files)
{
using (Stream stream = fi.OpenRead())
{
BitmapImage bi = new BitmapImage();
bi.SetSource(stream);
GetNextImage().Source = bi;
}
}
}
It looks reasonable, and it works reasonably well, too—until the user selects 40 or 50 large image files. The problem? All this code executes on the application’s UI thread, and though the images are coming from the local file system, it can take several seconds to load them all. On the surface, you might think that the images would “pop” onto the page one by one following each assignment to Image.Source. In reality, because we’re hogging the UI thread and not giving it a chance to do any rendering, the user will see nothing until all the images have been loaded. (Incidentally, stepping a ProgressBar control as the images are loaded wouldn’t work, either. Since we’re blocking Silverlight’s ability to render, it couldn’t render the new values you assign to the ProgressBar!)
There isn’t a perfect solution to this problem in Silverlight, but there are work-arounds. If we weren’t dealing with images or other XAML objects, we could do the work on a background thread and free the UI thread to render. But since we’re dealing with images, that’s not an option.
Enter CompositionTarget.Rendering. Here’s how I restructured the code to fix the problem:
private Queue<FileInfo> _files = new Queue<FileInfo>();
…
public Page()
{
InitializeComponent();
// Register a handler for Rendering events
CompositionTarget.Rendering +=
new EventHandler(CompositionTarget_Rendering);
}
…
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = “JPEG Files (*.jpg;*.jpeg)|*.jpg;*.jpeg|PNG Files (*.png)|*.png|All Files (*.*)|*.*”;
ofd.FilterIndex = 1;
ofd.Multiselect = true;
if ((bool)ofd.ShowDialog())
{
// Reset the queue
_files.Clear();
// Place each FileInfo in a queue
foreach (FileInfo fi in ofd.Files)
{
_files.Enqueue(fi);
}
}
…
private void CompositionTarget_Rendering(Object sender, EventArgs e)
{
if (_files.Count != 0)
{
FileInfo fi = _files.Dequeue();
using (Stream stream = fi.OpenRead())
{
BitmapImage bi = new BitmapImage();
bi.SetSource(stream);
GetNextImage().Source = bi;
}
}
}
Now each time Silverlight’s ready to rerender the UI, the application loads one image. References to the images are stored in a Queue<>, and each time the Rendering event handler executes, it retrieves one more image from the queue. Now the images pop onto the screen as they’re loaded, and the user isn’t left waiting and wondering what’s happening.