Compartilhar via


Offline Data Cache in Windows Phone 7

I’ve been building a Windows Phone 7 application that basically lets a user create records offline, and then pushes those up to a cloud service when network connectivity is available. These offline records are not as offline as I would like though – it’s never that simple is it? The records contain data fields for which the value must be selected from a list of reference data, and that reference data, whilst mostly static, can change over time. Sometimes daily, sometimes annually.

However, note that this reference data never changes on the phone itself, so I don’t need the rich synchronisation logic that you may be thinking of. Rather, the reference data is read-only on the device, and the records themselves are write-only on the phone. I’m going to ignore the write-only data for this post – let’s just think about the read-only reference data.

After thinking this through for a while I identified some interesting requirements and opportunities. For example, my preferred pattern of data usage is exactly the same for all the different reference data lists. The workflow is something like this;

  1. Application starts.
  2. The reference data is loaded into memory from a local store (unless it isn’t found, in which case force step 4).
  3. If the user notices the data is out of date, they hit a refresh button – probably one per reference data type.
  4. The application downloads the new data from a remote service, and replaces the in-memory cache.
  5. The application closes or tombstones.
  6. The reference data is persisted to a local store.

This led me to build a generic LocalCache class that encapsulates most of this logic. Instances of this class can be held within a shared repository of some form.

So let’s start building this class…

    1:      public abstract class LocalCache<ItemType> : INotifyPropertyChanged
    2:      {
    3:          public event PropertyChangedEventHandler PropertyChanged;
    4:          private ObservableCollection<ItemType> _items = null;
    5:   
    6:          public ObservableCollection<ItemType> Items
    7:          {
    8:              get
    9:              {
   10:                  return _items;
   11:              }
   12:              set
   13:              {
   14:                  if (_items == value)
   15:                      return;
   16:   
   17:                  _items = value;
   18:                  OnPropertyChanged("Items");
   19:              }
   20:          }
   21:          
   22:          protected void OnPropertyChanged(string propertyName)
   23:          {
   24:              if (PropertyChanged != null)
   25:                      PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
   26:          }
   27:      }

That’s the bare bones in place. Fundamentally all we’ve got is a class with a generic parameter, that exposes an ObservableCollection of those items – nothing too clever.

Next, we need to be able to load the items from some kind of local cache. I’ve chosen to write the files as XML to Isolated Storage. Because the types I’m going to be using are coming straight from a Web Service, the DataContractSerializer was a perfect hassle-free solution. I also considered the JsonSerializer, as the files would be smaller… but I’ll leave it to you if you want to update it.

    1:      public abstract class LocalCache<ItemType> : INotifyPropertyChanged
    2:      {
    3:          public event PropertyChangedEventHandler PropertyChanged;
    4:          private ObservableCollection<ItemType> _items = null;
    5:   
    6:          public ObservableCollection<ItemType> Items
    7:          {
    8:              get
    9:              {
   10:                  if (_items == null)
   11:                      RefreshFromLocalSource();
   12:                  return _items;
   13:              }
   14:              set
   15:              {
   16:                  if (_items == value)
   17:                      return;
   18:   
   19:                  _items = value;
   20:                  OnPropertyChanged("Items");
   21:              }
   22:          }
   23:          
   24:          protected void OnPropertyChanged(string propertyName)
   25:          {
   26:              if (PropertyChanged != null)
   27:                  PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
   28:          }
   29:   
   30:          protected virtual string GetFilename()
   31:          {
   32:              return String.Format("{0}.xml", typeof(ItemType).FullName);
   33:          }
   34:   
   35:          protected virtual void BeginGetItemsFromLocalSource(Action<ObservableCollection<ItemType>> callback)
   36:          {
   37:              var filename = GetFilename();
   38:              Object deserialized = null;
   39:              ItemType[] result = null;
   40:   
   41:              using (var iso = IsolatedStorageFile.GetUserStoreForApplication())
   42:              {
   43:                  if (iso.FileExists(filename))
   44:                  {
   45:                      using (IsolatedStorageFileStream file = iso.OpenFile(filename, FileMode.Open))
   46:                      {
   47:                          DataContractSerializer serializer = new DataContractSerializer(typeof(ItemType[]));
   48:                          deserialized = serializer.ReadObject(file);
   49:                      }
   50:                  }
   51:              }
   52:   
   53:              result = deserialized as ItemType[];
   54:              if (result == null && deserialized != null)
   55:                  throw new InvalidCastException(String.Format("{0} is not in the right format.", filename));
   56:              if (result == null)
   57:                  callback(new ObservableCollection<ItemType>());
   58:              if (result != null)
   59:                  callback(result.ToObservableCollection<ItemType>());
   60:          }
   61:   
   62:          public void RefreshFromLocalSource()
   63:          {
   64:              BeginGetItemsFromLocalSource(i => this.Items = i);
   65:          }
   66:      }

Here I’ve added methods to load in the cached data… and added a call to read the data in when the Items collection is first accessed. A good practice would be to do this file access on another thread, so as not to block the UI thread, and that’s why my chosen method is named “Begin…”. I’ll leave it to you to add that. Notice that I use a call-back to update the local items collection, which makes it easy to turn this into an asynchronous process and return the response, as well as making it easy to override my behaviour in a subclass if needed.

Of course, there’s still no way to get the data from a remote Web Service, so I’ve added an abstract method for the subclass to implement. I toyed with various approaches here and thought it was the best solution;

    1:      protected abstract void BeginGetItemsFromRemoteSource(Action<ObservableCollection<ItemType>> callback);
    2:   
    3:      public void RefreshFromRemoteSource()
    4:      {
    5:          BeginGetItemsFromRemoteSource(i =>
    6:              {
    7:                  this.Items = i;
    8:              });
    9:      }

The public RefreshFromRemoteSource method wraps the call to the BeginXxx method. The assumption is that this BeginXxx method will do it’s work asynchronously – and that’s easy as we know it’ll be calling a Web Service. Therefore the call-back approach is ideal again.

The final piece of the puzzle is that we want to save the in-memory data back to disk whenever the application shuts down;

    1:      public LocalCache()
    2:      {
    3:          PhoneApplicationService.Current.Closing += 
    4:              new EventHandler<ClosingEventArgs>((sender, e) => SaveToLocalSource());
    5:          PhoneApplicationService.Current.Deactivated += 
    6:              new EventHandler<DeactivatedEventArgs>((sender, e) => SaveToLocalSource());
    7:      }
    8:   
    9:      public void SaveToLocalSource()
   10:      {
   11:          // if we never loaded in the data, don't bother saving it
   12:          if (_items == null)
   13:              return;
   14:   
   15:          // note we might want to do all this work on another thread in a real system
   16:          var filename = GetFilename();
   17:   
   18:          using (var iso = IsolatedStorageFile.GetUserStoreForApplication())
   19:          {
   20:              if (iso.FileExists(filename))
   21:                  iso.DeleteFile(filename);
   22:   
   23:              using (IsolatedStorageFileStream file = iso.CreateFile(filename))
   24:              {
   25:                  DataContractSerializer serializer = new DataContractSerializer(typeof(ItemType[]));
   26:                  serializer.WriteObject(file, _items.ToArray());
   27:              }
   28:          }
   29:      }

The saving of the data just uses Isolated Storage and the DataContractSerializer again – easy. But you’ll notice something interesting here – in the constructor of the class we now sign up to receive events when the application is Tombstoning or Closing. We use the opportunity to  call SaveToLocalSource. This means the management of this data is entirely encapsulated within the LocalCache class (or more accurately the concrete subclass of it).

This means to implement a locally cached list of Person objects, all I need is a very simple subclass;

    1:  public class PersonRepository : LocalCache<Person>
    2:  {
    3:      protected override void BeginGetItemsFromRemoteSource(Action<ObservableCollection<Person>> callback)
    4:      {
    5:          PersonServiceClient client = new PersonServiceClient();
    6:          client.GetPeopleCompleted += 
    7:              new EventHandler<GetPeopleCompletedEventArgs>(GetPeopleCompleted);
    8:          client.GetPeopleAsync(callback);
    9:      }
   10:   
   11:      private void GetPeopleCompleted(object sender, GetPeopleCompletedEventArgs e)
   12:      {
   13:          var client = sender as PersonServiceClient;
   14:          var callback = e.UserState as Action<ObservableCollection<Person>>;
   15:   
   16:          if (e.Cancelled || e.Error != null)
   17:          {
   18:              callback(null);
   19:              return;
   20:          }
   21:   
   22:          Person[] result = null;
   23:          try {
   24:              result = e.Result as Person[];
   25:          } catch {
   26:              // no valid result found
   27:          } finally {
   28:              if (client != null)
   29:                  client.CloseAsync();
   30:          }
   31:   
   32:          if (result != null)
   33:              callback(result.ToObservableCollection());
   34:          else
   35:              callback(null);
   36:      }
   37:  }

This class purely provides the logic to call the Web Service and pass the data to the call-back.

So, any comments or thoughts? The attached sample code should show it in action. There are a few extra bits in the code – such as a “busy” flag to indicate when the data is being loaded, and the use of the Dispatcher class to ensure PropertyChanged events occur on the UI thread, and a little (but not complete) error handling… but not a lot. I’d be interested if you’ve any alternative solution. Note the usual disclaimers apply – this isn’t fully tested yet so your mileage may vary – in particular I’ve been lazy about concurrency at the moment, partly to keep the code simple.

PhoneCache.zip

Comments

  • Anonymous
    October 28, 2010
    I've been working on this problem myself for a while now. I started on the desktop with a rich WPF client, and recently ported the code over to WP7. My work is on Codeplex at correspondence.codeplex.com. I recognized, as you did, that mutable data is hard to synchronize. So I took an approach similar to yours. All objects are immutable. The difference is that I decided to hang new objects on my reference data to represent "changes" to it. These new objects serve not only as storage, but also as a queue. Just push these objects up to the server and it's in sync. Another difference between our approaches is that I'm saving changes as they happen. This not only prevents lost data in the event of an app or phone crash, but it also gets these changes into the queue so they can be uploaded asynchronously. I'm interested to see the write-only side of this equation.