One-time Cached Images in Windows Phone 7

By | July 19, 2010

It’s trivially simple to put an Image control onto a page in Windows Phone 7 and point it at a url. Handily, the Image control will also cache that URL, and not request it again for the life of your application. But what if you’re like me and really, really want to save data traffic? I want that image to download once, ever. The only reason that Image control should generate data traffic would be if I reinstall my application.

Presenting the ImageCacher (say it like Terminator, in a James Earle Jones voice). This is little class takes a URL, and returns a BitmapImage, and is guaranteed to hit your URL once and only once. Example code is available at the end of this post. Note, throughout these examples I use a helper class around the IsolatedStorage methods.

First things first, whip up an ImageCacheConverter that we can apply to any Image bindings and make them call the cacher:

[code lang="csharp"]
public class ImageCacheConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return ImageCacher.GetCacheImage(value.ToString());
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
[/code]

Add that value converter as a handy static reference in our App.xaml:
[code lang="xml"]
<Application.Resources>
<ImageCacherDemo:ImageCacheConverter x:Key="imageCacheConverter" />
</Application.Resources>
[/code]

And then use the value converter wherever we bind an image URL to our Image controls:
[code lang="xml"]
<Image Source="{Binding ElementName=ImageSource, Path=Text, Converter={StaticResource imageCacheConverter}}" Width="200" />
[/code]

So now the ImageCacher can go to work. GetCacheImage is where the magic starts. The first thing we do is convert our URL to a local filename, then check if that file exists. If it does, it means we’ve already cached the image, so just return it as is:
[code lang="csharp"]
if ( IsoStore.FileExists( cacheFile ) )
result.SetSource( IsoStore.StreamFileFromIsoStore( cacheFile ) );
else
CacheImageAsync(url, result);
[/code]

CacheImageAsync does what it says: it caches the image on a background thread. This way we can replicate the behaviour of the standard Image control. The UI doesn’t wait for the image to load: the image is blank when we first see it, then populates with the actual image from the web server once it has downloaded.

So we create an object to pass the image url and BitmapImage to the background thread (I really should use a dedicated class here, but KeyValuePair does the trick), then kick it off.
[code lang="csharp"]
public static void CacheImageAsync( string url, BitmapImage image )
{
var items = new KeyValuePair<string, BitmapImage>(url, image);
var t = new Thread(GetImageSource);
t.Start(items);
}
[/code]

Inside CacheImage there’s a number of things that happen, shown in the snippet below. We create a WaitHandle that we can trigger when the image download completes; we pass this and the destination filename through to the web callback when we trigger the download; then we wait for up to 5 seconds. Once the time is up (or the image is downloaded), we check for the cache file and set our image source.

[code lang="csharp"]
var waitHandle = new AutoResetEvent( false );
var fileNameAndWaitHandle = new KeyValuePair<string, AutoResetEvent>( cacheFile, waitHandle );

var wc = new WebClient();
wc.OpenReadCompleted += OpenReadCompleted;
// start the caching call (web async)
wc.OpenReadAsync( new Uri( url ), fileNameAndWaitHandle );

// wait for the file to be saved, or timeout after 5 seconds
waitHandle.WaitOne( 5000 );
if ( IsoStore.FileExists(cacheFile) )
{
// ok, our file now exists! set the image source on the UI thread
Deployment.Current.Dispatcher.BeginInvoke(() => image.SetSource(IsoStore.StreamFileFromIsoStore(cacheFile)));
}
[/code]

So in the web callback, it’s simply a matter of saving the streamed image download, then triggering the WaitHandle to let the CacheImage method know we’re done:
[code lang="csharp"]
var state = (KeyValuePair<string, AutoResetEvent>)e.UserState;
IsoStore.SaveToIsoStore( state.Key, e.Result );
state.Value.Set();
[/code]

It’s a fair bit of work, but the result is exactly what I wanted. If you start up NetMon, you’ll see one and only one request, even if you stop and restart the application. You’ll also notice that the image is significantly faster to load on the second and subsequent application loads.

There’s a few things to tidy up. Like if the cache file doesn’t exist after the 5 second timeout, we could display a “broken” image, or a default local image. We could also implement some sort of cache timeout so that the image is re-downloaded every week or month. I’ll leave those as an exercise for the reader.

Anyway, here’s the code.

9 thoughts on “One-time Cached Images in Windows Phone 7

  1. Pingback: Windows Phone 7 – Starting out « Mike Hole

  2. Hao

    Nice post! But I just noticed that the image control will cache the icon, if i run the app again, there is no data traffic generated!

    Reply
  3. Roger

    I am running into this:

    System.IO.IsolatedStorage.IsolatedStorageException was unhandled
    Message=Operation not permitted on IsolatedStorageFileStream.
    StackTrace:
    at System.IO.IsolatedStorage.IsolatedStorageFileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, IsolatedStorageFile isf)
    …..

    Reply
  4. Pingback: One-time Cached Images in Windows Phone 7 » Saintchubs.com

  5. Pingback: WPDev Sin: Gluttony « Windows Phone Love & .NET Ramblings

  6. Joel Santos

    Everything seems to work fine except the part where I add the converter to the app.xaml..I’ve changed the namespace but no sucess

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *