In Part 1 of this series, we built a basic photo-extras application that allows a user to load photos from the phone’s Pictures library and convert the photos to grayscale. We also learned that in the absence of tombstoning support, the app doesn’t work very well. Specifically, if the user clicks the Start button to tombstone the app, followed by the Back button to reactivate it, the app loses its state. Any photo that has been loaded simply goes away.
We can fix that by adding tombstoning support. We’ll follow Microsoft’s guidance and use the page’s OnNavigatedFrom and OnNavigatedTo methods to house our tombstoning code. We’ll begin by adding an OnNavigatedFrom override that saves the current state of the app before tombstoning occurs. And we’ll modify the existing OnNavigatedTo method to restore that state when the app is reactivated. On the surface, it seems pretty simple.
One question we have to address is where will the tombstoned state be stored? You have three options here:
- Store it in page state, which is a transient repository for page-scoped data accessed through the PhoneApplicationPage.State property
- Store it in application state, which is similar to page state except that it’s provided for application-scoped data and accessed through the PhoneApplicationService.State property
- Store it in isolated storage, which is independent of tombstoning and permits data to be stored permanently, just like data written to a hard disk
John Garland has written a series of articles on these persistence mechanisms, so I won’t rehash the details here. However, since the data we need to persist is all page-scoped, we’ll use page state to tombstone our data – at least we’ll try.
The first thing we need to do is think about what needs to be persisted. If our sample app is tombstoned (terminated) and then reactivated, what values do we need to persist so the user thinks he or she has simply reconnected to a running application? In our sample, two items need persisting:
- The WriteableBitmap that holds the photo loaded from the Pictures library (assuming a photo has been loaded)
- The state of the application bar’s Save button, which is either enabled or disabled at various times as the user interacts with the app
The second one is easy: we’ll just write the button’s IsEnabled property to page state in OnNavigatedFrom, and read it back (and apply it to the button) in OnNavigatedTo. The first one is a little harder. WriteableBitmaps aren’t serializable, so you can’t just do this to serialize an entire bitmap to page state:
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
if (_bitmap != null)
State["Bitmap"] = _bitmap;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (State.ContainsKey("Bitmap"))
_bitmap = (WriteableBitmap)State["Bitmap"];
}
The code will compile, but it’ll cause the run-time to throw an exception shortly after OnNavigatedFrom returns:
A first chance exception of type ‘System.Runtime.Serialization.InvalidDataContractException’ occurred in System.Runtime.Serialization.dll
A first chance exception of type ‘System.Reflection.TargetInvocationException’ occurred in mscorlib.dll
A first chance exception of type ‘System.Runtime.Serialization.InvalidDataContractException’ occurred in System.Runtime.Serialization.dll
WriteableBitmap may not be serializable, but its Pixels property, which is of type int[], is. So you might be tempted to try something along these lines:
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
if (_bitmap != null)
{
State["Width"] = _bitmap.PixelWidth;
State["Height"] = _bitmap.PixelHeight;
State["Pixels"] = _bitmap.Pixels;
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (State.ContainsKey("Width") && State.ContainsKey("Height"))
{
int width = (int)State["Width"];
int height = (int)State["Height"];
if (State.ContainsKey("Bitmap"))
{
_bitmap = new WriteableBitmap(width, height);
int[] pixels = (int[])State["Pixels"];
// TODO: Restore _bitmap.Pixels
}
}
}
But if you do, you’ll be met with an even nastier exception after OnNavigatedFrom returns:
A first chance exception of type ‘System.Runtime.InteropServices.COMException’ occurred in Microsoft.Phone.Interop.dll
The problem here is that the operating system limits the amount of data an app can write to page state. The limit is 2 MB per page, or 4 MB per app. WriteableBitmap uses 4 bytes per pixel, so a 1600 x 1200 image consumes more than 7 MB of storage. The documentation doesn’t mention a similar limit on application state, but the limit is there, and it’s around 1.5 MB. The upshot is that neither page state nor application state can be used to persist WriteableBitmaps.
That leaves us with isolated storage, which imposes no artificial limits on volumes of data. Standard practice is to use page state and application state to tombstone data and isolated storage to persist data permanently, but when you need to tombstone a lot of data, isolated storage is your friend. But even there, you need to be careful. Applications are given 10 seconds to tombstone data, and if they’re not finished when the 10 seconds are up, they’re terminated. If you’re going to tombstone a bitmap in isolated storage, don’t get caught in the trap of doing something like this:
using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
{
using (IsolatedStorageFileStream stream =
new IsolatedStorageFileStream("Bitmap.dat", FileMode.Create, store))
{
using (BinaryWriter writer = new BinaryWriter(stream))
{
for (int i = 0; i < _bitmap.Pixels.Length; i++)
writer.Write(_bitmap.Pixels[i]);
}
}
}
That for loop is slow and can easily require more than 10 seconds for a large bitmap. Performance is key when it comes to tombstoning and untombstoning data. Even if you slip in just under the 10-second limit, users will look unfavorably upon apps that take too long to activate and deactivate.
Here’s the solution I arrived at to save and restore the WriteableBitmap and the state of the Save button, too:
private const string _save = "Save";
private const string _width = "Width";
private const string _height = "Height";
private const string _file = "Bitmap.dat";
.
.
.
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
Debug.WriteLine("OnNavigatedFrom");
// Store the state of the Save button in page state
State[_save] = (ApplicationBar.Buttons[1] as ApplicationBarIconButton).IsEnabled;
if (_bitmap != null)
{
// Store the bitmap’s width and height in page state
State[_width] = _bitmap.PixelWidth;
State[_height] = _bitmap.PixelHeight;
// Store the bitmap bits in isolated storage
using (IsolatedStorageFile store =
IsolatedStorageFile.GetUserStoreForApplication())
{
using (IsolatedStorageFileStream stream =
new IsolatedStorageFileStream(_file, FileMode.Create, store))
{
using (BinaryWriter writer = new BinaryWriter(stream))
{
int count = _bitmap.Pixels.Length * sizeof(int);
byte[] pixels = new byte[count];
Buffer.BlockCopy(_bitmap.Pixels, 0, pixels, 0, count);
writer.Write(pixels, 0, pixels.Length);
}
}
}
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
Debug.WriteLine("OnNavigatedTo");
if (NavigationContext.QueryString.ContainsKey("token"))
{
// If the app was invoked through the Extras menu, grab the photo and display it
MediaLibrary library = new MediaLibrary();
Picture picture =
library.GetPictureFromToken(NavigationContext.QueryString["token"]);
BitmapImage bitmap = new BitmapImage();
bitmap.SetSource(picture.GetImage());
_bitmap = new WriteableBitmap(bitmap);
Photo.Source = _bitmap;
// Disable the application bar’s Save button
(ApplicationBar.Buttons[1] as ApplicationBarIconButton).IsEnabled = false;
}
else
{
// Restore the state of the Save button
if (State.ContainsKey(_save))
(ApplicationBar.Buttons[1] as ApplicationBarIconButton).IsEnabled =
(bool)State[_save];
// Restore the WriteableBitmap
if (State.ContainsKey(_width) && State.ContainsKey(_height))
{
int width = (int)State[_width];
int height = (int)State[_height];
// Create a new WriteableBitmap
_bitmap = new WriteableBitmap(width, height);
// Get the bitmap bits from isolated storage
using (IsolatedStorageFile store =
IsolatedStorageFile.GetUserStoreForApplication())
{
if (store.FileExists(_file))
{
using (IsolatedStorageFileStream stream =
new IsolatedStorageFileStream(_file, FileMode.Open, store))
{
using (BinaryReader reader = new BinaryReader(stream))
{
int count = _bitmap.Pixels.Length * sizeof(int);
byte[] pixels = new byte[count];
reader.Read(pixels, 0, count);
Buffer.BlockCopy(pixels, 0, _bitmap.Pixels, 0, count);
}
}
// Clean up by deleting the file
store.DeleteFile(_file);
}
}
// Show the bitmap to the user
Photo.Source = _bitmap;
}
}
}
Notice the use Buffer.BlockCopy to block-copy pixels to and from the bitmap, and the use of block reads and writes with the IsolatedStorageFile. This makes saving and restoring the WriteableBitmap about as fast as it can be without resorting to unsafe code, which isn’t permitted in phone applications, anyway. Also note that OnNavigatedTo cleans up by deleting the isolated storage file containing the bitmap’s pixels. Files written to isolated storage don’t get deleted unless somebody deletes them, so it’s good form to free up the storage if the file is no longer needed. Which brings up another point: if you use isolated storage to tombstone data, what happens if the app doesn’t get untombstoned? What if, for example, the user clicks the Start button, causing the app to be tombstoned, but never clicks the Back button to untombstone the app?
For that reason, I added a few lines of the code to the Application_Closing method in App.xaml.cs:
private const string _file = "Bitmap.dat";
.
.
.
private void Application_Closing(object sender, ClosingEventArgs e)
{
Debug.WriteLine("Closing");
// Delete the file containing the WriteableBitmap (if it exists)
using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
{
if (store.FileExists(_file))
store.DeleteFile(_file);
}
}
Now if the the app gets tombstoned but doesn’t get untombstoned, it’ll clean up after itself when it’s shut down once and for all.
Now let’s try it out. Take the finished application from Part 1, which you can download from here, and paste in the new OnNavigatedFrom and OnNavigatedTo methods, as well as the const fields and the modified Application_Launching method. (Alternatively, you can download the finished application for Part 2 from here.) Then repeat the experiment you tried in Part 1 to see if tombstoning works:
- Start the app in the Windows phone emulator.
- Click the Open button and select a picture.
- Click the Open button again, but this time, don’t select a picture. Instead, click the Back button. The picture you chose a moment ago should still be displayed. No surprise there, because we learned in Part 1 that the app wasn’t tombstoned at this point.
- Click the Start button to return to the phone’s main screen.
- Now click the Back button to return to your app.
Voila! The last time you tried this, the picture disappeared when you returned to the app. This time, it’s still there. It looks like we’re getting the hang of this tombstoning thing. It took some code to make it happen, but at least it’s working. Or is it?
Sadly, we’re not out of the woods yet. (Or should I say not out of the grave yet?) To see what I mean, try this:
- Start the app again in the emulator.
- Click the Open button and select a picture.
- Click the Open button and select another picture. Be sure to select a different one than you selected in the previous step.
Interestingly, the app seems to ignore the second selection; the first picture you selected is still displayed. Try it again and again and again and you’ll get the same results. (Who was it that said the definition of insanity is repeating the same action over and over again and expecting a different result? Maybe he – or she – was a phone developer.)
It might not be obvious to you what’s wrong here. It wasn’t obvious to me the first time I encountered it. The problem has to do with the ordering of events and method calls, and it’s not unusual to run into this when you’re writing tombstoning code – especially if the app uses launchers and choosers, as this one does. Clearly we have work left to do. And we’ll commence that work in Part 3.