29 September 2010

Extension methods for tomb stoning the Windows Phone 7 model

As I was (and still am) playing with my MVVM-driven Windows Phone 7 map viewer, I started thinking about tomb stoning and/or serializing stuff into the Windows Phone 7 application state and/or isolated storage. I came up with four extension methods than can be used to store/serialize the complete model in both. Maybe not ideal in all cases, but it was a simple solution for me.

Getting started

I created an assembly LocalJoost.Utilities, in which I first defined a generic interface for the model, which I called – with a flash of inspiration – IModel ;-). It’s implementation is not interesting for the extension methods.

Save to phone application state

using System;
using System.IO;
using System.IO.IsolatedStorage;
using System.Runtime.Serialization;
using System.Windows;
using Microsoft.Phone.Shell;

namespace LocalJoost.Utilities
{
  /// <summary>
  /// Some extensions method that allow serializing and deserializing
  /// a model to and from phone state and/or isolated storage
  /// </summary>
  public static class ApplicationExtensions
  {
    private const string ModelKey = "model";
 
    public static IModel RetrieveFromPhoneState(this Application app) 
    {
      if (PhoneApplicationService.Current.State.ContainsKey(ModelKey))
      {
        return PhoneApplicationService.Current.State[ModelKey] as IModel;
      }
      return null;
    }

    public static void SaveToPhoneState(this Application app, IModel model)
    {
      if (PhoneApplicationService.Current.State.ContainsKey(ModelKey))
      {
        PhoneApplicationService.Current.State.Remove(ModelKey);
      }
      PhoneApplicationService.Current.State.Add(ModelKey, model);
    }
  }
}

Creating a Locator

I wanted my model to be bindable by XAML and be usable from code as well, so I created the following ‘Locator’:

using LocalJoost.Utilities;

namespace LocalJoost.Models
{
  public class Locator
  {
    private static IModel _model;
    private static Locator _locator;

    public IModel Model
    {
      get { return _model; }
      set { _model = value; }
    }

    public static Locator Instance
    {
      get { return _locator ?? (_locator = new Locator()); }
    }
  }
}

Using Locator and Model from XAML

First, add the namespace to the XAML

xmlns:Utilities="clr-namespace:LocalJoost.Utilities;assembly=LocalJoost.Utilities"

Then bind it to your Layout root or any other element of choice

 <Grid x:Name="LayoutRoot"
  DataContext="{Binding Source={StaticResource Locator}, Path=Model}">

Instantiating your model

Interesting caveat – the Locator is instantiated, not your model. When the application runs for the very first time, there will be no model to retrieve. Thus, in the constructor of App.Xaml.cs you need to add:

Locator.Instance.Model = new YourModel();

in which you replace "YourModel" for your own model type.

Storing in / retrieving from phone application state

Using the extension methods is pretty simple then: open your App.Xaml.cs, find two methods called “Application_Activated” and “Application_Deactivated” and modify them as follows:

// Code to execute when the application is activated (brought to foreground)
// This code will not execute when the application is first launched
private void Application_Activated(object sender, ActivatedEventArgs e)
{
  Locator.Instance.Model = this.RetrieveFromPhoneState();
}

// Code to execute when the application is deactivated (sent to background)
// This code will not execute when the application is closing
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
  this.SaveToPhoneState(Locator.Instance.Model);
}

And that’s it. Your model now persists to the phone state and survives, for instance, a hit on the search button

Storing in Isolated Storage

This proves to be marginally more difficult. I expanded the class ApplicationExtensions with two more methods and a const:

private const string DataFile = "model.dat";

public static T RetrieveFromIsolatedStorage<T>(this Application app) 
  where T : class
{
  using (var appStorage = IsolatedStorageFile.GetUserStoreForApplication())
  {
    if (appStorage.FileExists(DataFile))
    {
      using (var iss = appStorage.OpenFile(DataFile, FileMode.Open))
      {
        try
        {
          var serializer = new DataContractSerializer(typeof(T));
          return serializer.ReadObject(iss) as T;
        }
        catch (Exception e)
        {
          System.Diagnostics.Debug.WriteLine(e);
        }
      }
    }
  }
  return null;
}

public static void SaveToIsolatedStorage(this Application app, IModel model)
{
  using (var appStorage = IsolatedStorageFile.GetUserStoreForApplication())
  {
    if (appStorage.FileExists(DataFile))
    {
      appStorage.DeleteFile(DataFile);
    }
    var serializer = new DataContractSerializer(model.GetType());
    using (var iss = appStorage.CreateFile(DataFile))
    {
      serializer.WriteObject(iss, model);
    }
  }
}

You now have to find Application_Launching and Application_Closing in App.Xaml.cs and modify them in a similar way:

// Code to execute when the application is launching (eg, from Start)
// This code will not execute when the application is reactivated
private void Application_Launching(object sender, LaunchingEventArgs e)
{
    Locator.Instance.Model = 
       this.RetrieveFromIsolatedStorage<YourModel>();
}

// Code to execute when the application is closing (eg, user hit Back)
// This code will not execute when the application is deactivated
private void Application_Closing(object sender, ClosingEventArgs e)
{
    this.SaveToIsolatedStorage(Locator.Instance.Model);
}

Some notes

  • In order to make this work, your need to mark your model class with the [DataContract] attribute, and every member you want to be serialized with [DataMember]
  • If you are using MVVMLight (which of course you are ;-) ) and if you are just making simple apps that don’t use separate models but just view models, be aware that your model cannot inherit from ViewModelBase, for the simple reason that this class is not serializable. Oops ;-)
  • If your model communicates with the view using attached dependency properties you may run into some timing difficulties. I will soon blog about a work-around for that.

No comments: