This blog post discusses one way to deal with the fact that, by default, a BitmapImage object holds a lock on the source image file. Along the way we will see what inspired this solution.
I have been working away on Podder, my WPF podcast player. Grant Hinkson, my favorite Visual Designer in the world, has spent some of his scarce free time creating a truly phenomenal skin for Podder. His work has revealed shortcomings and oversights in the Podder design, which I have been diligently working on and resolving.
Every podcast has an associated image, which is linked to via the podcast’s RSS feed. Grant’s Podder skin represents a podcast by showing it’s image. When I first added in support for providing the image of a podcast, I simply exposed a property on my Podcast class that returned the URL of the image. This worked OK for my skin, since it only shows one image at a time. For Grant’s skin, however, this did not cut it. His UI shows multiple podcast images at the same time. When the app first loaded with his skin applied, it would take quite a while for those images to be downloaded and displayed. Subsequent loads with his skin, during the same run of the app, would be fast because WPF caches images by default.
I decided to address this issue by saving each podcast’s image to disk, and then always load the image from disk (instead of downloading it every time you run the app). Getting the image saved to disk was easy:
The tricky part was a little more subtle. When the user removes a podcast from Podder’s list of podcasts, I feel morally obligated to delete that podcast’s image file from their disk. I suppose it isn’t truly necessary, but I’m not a fan of apps that lazily leave garbage floating around on my disk, eating up space.
This moral quandary resulted in a call to File.Delete(theImageFile). Unfortunately, this did not work. It turns out that the BitmapImage, which is used by an Image element, holds a lock on the image file that you pass as it’s source. If you try to delete the file, an exception is thrown letting you know that you’re being a very naughty developer.
Naturally, the first thing I did was search the Web for more info about this problem. I came across an excellent solution for the problem here. Ralph’s solution involves a custom value converter that adjusts the BitmapImage’s caching behavior, so that it does not put a lock on the image file after the image has been loaded. That’s a very clean and simple solution, but I didn’t want to use it. One of my goals in designing Podder is that it should be as simple as possible to skin it. I’m actually toying with the idea of hosting a competition where people make their own Podder skin(s), and then submit them as entries to be judged and possibly awarded prizes. So I need to keep the skinning process as simple as possible. Having to use some obscure value converter on Image sources does not jive with that objective.
I was stumped. I needed to be able to delete image files for podcasts that were removed from Podder, but attempting to delete an image file resulted in an exception being thrown. What to do? I stepped away from the computer, sat down at the piano, and improvised. The solution dawned on me after playing this lovely little tune:
Josh Smith Piano Improv [2-21-2008]
In an inspired moment, I ran back to my laptop and modified my PodcastImageCache class a bit, so that instead of attempting to immediately delete an image file when told to, it stored the image file path in a list.
I added the PodcastImageCache object to the application’s object graph that gets serialized by the BinaryFormatter as it shuts down. When the PodcastImageCache object is serialized that list of image file paths gets saved, too. I then used the magical OnDeserialized attribute to specify a callback to be invoked when Podder starts up again, and the PodcastImageCache object is deserialized.
Since the object graph is deserialized before the UI has a chance to come back to life, I am certain that none of the image files will be locked by the UI at the time this code runs. Perhaps this solution is overkill for most apps, but if you need to maintain absolute simplicity in the UI layer then this approach has value.