Export (0) Print
Expand All

Using Isolated Storage on the Phone

You Will Learn

  • How to encrypt user passwords before storing them in isolated storage.
  • How to save model data in isolated storage as serialized objects.

The mobile client application must be able to operate when there is no available network connection. Therefore, it must be able to store survey definitions and survey answers locally on the device, together with any other data or settings that the application requires to operate.

Isolated storage is the only mechanism available to persist application data on the Windows Phone platform. When an application uses a local database, it resides in the application’s isolated storage container.

The application must behave as a "good citizen" on the device. It is not possible to access another application's isolated storage or access the file system directly on the Windows Phone platform, so the application cannot compromise another application by reading or modifying its data. However, there is a limited amount of storage available on the phone, so Tailspin tries to minimize the amount of data that the mobile client application stores locally and be proactive about removing unused data.

Hh821020.note(en-us,PandP.10).gifChristine Says:
Christine To be a "good citizen" on the phone, your application should minimize the amount of isolated storage it uses. There's nothing on the phone that enforces any quotas, it's your responsibility.

The Tailspin mobile client application must adopt a robust storage solution and minimize the risk of losing any stored data.

Overview of the Solution

The Windows Phone platform offers isolated storage functionality. Isolated storage provides a dictionary-based model for storing application settings, enables the application to create folders and files that are private to the application, and can store relational data in a local database by using LINQ to SQL.

Hh821020.note(en-us,PandP.10).gifJana Says:
Jana It is the application's responsibility to manage the amount of storage it uses by deleting temporary or unused data because the operating system does not enforce any quota limits. Tailspin removes completed surveys from isolated storage after they are synchronized to the Tailspin Surveys service.

Figure 2 shows how each application on a Windows Phone device only has access to its own private storage on the device, and cannot access the storage belonging to other applications.

Hh821020.6273A24312A3258A3360FCA10C4FA615(en-us,PandP.10).png

Figure 2

Isolated storage on the Windows Phone platform

Security

Tailspin does not encrypt the survey data that the mobile client application stores in isolated storage because it does not consider that data to be confidential. However, Tailspin does encrypt user passwords before storing them in isolated storage.

Hh821020.note(en-us,PandP.10).gifChristine Says:
Christine Isolated storage provides one level of data security—isolation from other applications on the device—but you should consider whether this is adequate for the security needs of your application.


Hh821020.note(en-us,PandP.10).gifNote:
The sample application stores user passwords as encrypted text in isolated storage.

There are two scenarios to think about when you are implementing security for data stored on the phone. The first is whether other applications on the phone could potentially access your application's data and then transmit it to someone else. The isolated storage model used on Windows Phone that limits applications to their own storage makes this a very unlikely scenario. However, security best practices suggest that you should guard against even unlikely scenarios, so you may want to consider encrypting the data in your application's isolated storage.

The second scenario to consider is what happens if an unauthorized user gains access to the device. If you want to protect your data in this scenario, you must encrypt the data while it is stored on the device.

The Windows Phone API provides access to the Data Protection API (DPAPI). DPAPI generates and stores a cryptographic key by using the user and phone credentials to encrypt and decrypt data. The ProtectedData class provides access to DPAPI through Protect and Unprotect methods. On a Windows Phone device, every application gets its own decryption key, which is created when an application is run for the first time. Calls to Protect and Unprotect will implicitly use the decryption key and make sure that all the data remains private to the application. In addition, the decryption key persists across application updates. For more information, see "Security for Windows Phone," on the MSDN® developer program website.

Storage Format

Tailspin stores the application's settings as key/value pairs and uses serializable model classes to store the survey data as files in isolated storage. The latest release of the Windows Phone platform includes the ability to store data in a local database, and the developers at Tailspin are considering this option for the future. They have implemented the storage service in the application in a way that makes it easy to replace the storage classes with alternative implementations if they decide to use a different storage mechanism in the future. The current implementation also makes it easy to test the storage functionality in the application.

Inside the Implementation

Now is a good time to walk through the code that implements isolated storage in the Tailspin mobile client application in more detail. As you go through this section, you may want to download the Windows Phone Tailspin Surveys from the Microsoft Download Center.

You can find the code that implements isolated storage access in the Tailspin mobile client application in the Services/Stores folder in the TailSpin.PhoneServices project.

Application Settings

The user enters application settings data on the AppSettingsView page in the Surveys application. The ISettingsStore interface defines the data items that the application saves as settings. The following code example shows this interface.

public interface ISettingsStore
{
  string Password { get; set; }
  string UserName { get; set; }
  bool SubscribeToPushNotifications { get; set; }
  bool LocationServiceAllowed { get; set; }
  bool BackgroundTasksAllowed { get; set; }
  event EventHandler UserChanged;
}

The following code example shows how the application implements this interface to save the value of the Password property in the ApplicationSettings dictionary in the application's isolated storage.

public class SettingsStore : ISettingsStore
{
  private readonly IProtectData protectDataAdapter;
  private const string PasswordSettingDefault = "";
  private const string PasswordSettingKeyName = "PasswordSetting";
  private readonly IsolatedStorageSettings isolatedStore;
  private UTF8Encoding encoding;
  ...  

  public SettingsStore(IProtectData protectDataAdapter)
  {
    this.protectDataAdapter = protectDataAdapter;
    isolatedStore = IsolatedStorageSettings.ApplicationSettings;
    encoding = new UTF8Encoding();
  }

  public string Password
  {
    get
    {
      return PasswordByteArray.Length == 0 ? PasswordSettingDefault : 
        encoding.GetString(PasswordByteArray, 0, PasswordByteArray.Length);
    }
    set
    {
      PasswordByteArray = encoding.GetBytes(value);
    }
  }

  private byte[] PasswordByteArray
  {
    get
    {
      byte[] encryptedValue = GetValueOrDefault(PasswordSettingKeyName, 
        new byte[0]);
      if (encryptedValue.Length == 0)
        return new byte[0];
      return protectDataAdapter.Unprotect(encryptedValue, null);
    }
    set
    {
      byte[] encryptedValue = protectDataAdapter.Protect(value, null);
      AddOrUpdateValue(PasswordSettingKeyName, encryptedValue);
    }
  }

  ...

  private void AddOrUpdateValue(string key, object value)
  {
    bool valueChanged = false;

    try
    {
      // If the new value is different, set the new value.
      if (this.isolatedStore[key] != value)
      {
        this.isolatedStore[key] = value;
        valueChanged = true;
      }
    }
    catch (KeyNotFoundException)
    {
      this.isolatedStore.Add(key, value);
      valueChanged = true;
    }
    catch (ArgumentException)
    {
      this.isolatedStore.Add(key, value);
      valueChanged = true;
    }

    if (valueChanged)
    {
      Save();
    }
  }

  private T GetValueOrDefault<T>(string key, T defaultValue)
  {
    T value;

    try
    {
      value = (T)this.isolatedStore[key];
    }
    catch (KeyNotFoundException)
    {
      value = defaultValue;
    }
    catch (ArgumentException)
    {
      value = defaultValue;
    }

    return value;
  }

  private void Save()
  {
    isolatedStore.Save();
  }
}

The SettingsStore constructor accepts a parameter of IProtectData, which specifies the class that will provide access to the encryption functionality.

The IProtectData interface, in the TailSpin.Phone.Adapters project, defines method signatures for encrypting and decrypting data. The following code example shows this interface.

public interface IProtectData
{
  byte[] Protect(byte[] userData, byte[] optionalEntropy);
  byte[] Unprotect(byte[] encryptedData, byte[] optionalEntropy);
}

The IProtectData interface is implemented by the ProtectDataAdapter class, which adapts the ProtectedData class that provides access to the Data Protection API. The purpose of adapting the ProtectedData class with a class that implements IProtectData is to create a loosely coupled class that is testable. The following code example shows the class.

public class ProtectDataAdapter : IProtectData
{
  public byte[] Protect(byte[] userData, byte[] optionalEntropy)
  {
    return ProtectedData.Protect(userData, optionalEntropy);
  }

  public byte[] Unprotect(byte[] encryptedData, byte[] optionalEntropy)
  {
    return ProtectedData.Unprotect(encryptedData, optionalEntropy);
  }
}

Therefore, rather than calling the ProtectedData class methods directly, the PasswordByteArray property in the SettingsStore class calls the methods in the ProtectDataAdapter class to perform data encryption and decryption.

An adapter is a design pattern that translates an interface for a class into a compatible interface. The adapter translates calls to its interface into calls to the original interface. This approach enables the writing of loosely coupled code that is testable.

Survey Data

The application saves the local survey data in isolated storage as serialized SurveyTemplate and SurveyAnswer objects. The following code example shows the definition of the SurveysList object that uses the model classes SurveyTemplate and SurveyAnswer. The SurveyTemplate class is a model class that defines a survey and includes the question types, question text, possible answers, and survey metadata. The SurveyAnswer class is a model class that defines the responses collected by the surveyor. For more information about these model classes, see the section, "The Model Classes," earlier in this chapter.

Hh821020.note(en-us,PandP.10).gifJana Says:
Jana Tailspin uses JavaScript Object Notation (JSON) serialization to reduce CPU usage and storage space requirements.

public class SurveysList
{
  public SurveysList()
  {
    this.LastSyncDate = string.Empty;
  }

  public List<SurveyTemplate> Templates { get; set; }

  public List<SurveyAnswer> Answers { get; set; }

  public string LastSyncDate { get; set; }

  public int UnopenedCount { get; set; }
}

The synchronization tasks run by the phone can be performed in both the background and the foreground. When synchronization occurs in the background, it is performed by a background agent. The background agent never runs its tasks while the Tailspin mobile client application is in the foreground. Therefore, the mobile client application and the background agent will never be concurrently attempting to update the survey data in isolated storage. For more information about background agents, see the section, "Synchronizing Data between the Phone and the Cloud,"later in this chapter.

Hh821020.note(en-us,PandP.10).gifJana Says:
Jana A background agent allows code to run in the background, even when the application is not running in the foreground. A background agent can be registered as a PeriodicTask, a ResourceIntensiveTask, or both.

The following code example shows how the SurveyStore class (that implements the ISurveyStore interface) performs the serialization and deserialization of the properties of the SurveysList instance to and from isolated storage.

private readonly IsolatedStorageSettings isolatedStore;
private const string lastSyncDateKey = "lastSyncDateKey";
private const string unopenedCountKey = "unopenedCountKey";
private readonly string storeName;

public SurveyStore(string storeName)
{
  this.storeName = storeName;
  isolatedStore = IsolatedStorageSettings.ApplicationSettings;
  Initialize();
}

public SurveysList AllSurveys { get; set; }

...

private void SaveLastSyncDate()
{
  if (isolatedStore.Contains(lastSyncDateKey))
  {
    isolatedStore[lastSyncDateKey] = AllSurveys.LastSyncDate;
  }
  else
  {
    isolatedStore.Add(lastSyncDateKey, AllSurveys.LastSyncDate);
  }
  isolatedStore.Save();
}

...

private void SaveTemplates()
{
  using (var filesystem = IsolatedStorageFile.GetUserStoreForApplication())
  {
    using (var fs = new IsolatedStorageFileStream(
      storeName + ".templates", FileMode.Create, filesystem))
    {
      var serializer = new System.Runtime.Serialization
        .Json.DataContractJsonSerializer(typeof(List<SurveyTemplate>));
      serializer.WriteObject(fs, AllSurveys.Templates);
    }
  }
}

private void SaveAnswers()
{
  using (var filesystem = IsolatedStorageFile.GetUserStoreForApplication())
  {
    using (var fs = new IsolatedStorageFileStream(
      storeName + ".answers", FileMode.Create, filesystem))
    {
      var serializer = new System.Runtime.Serialization
        .Json.DataContractJsonSerializer(typeof(List<SurveyAnswer>));
      serializer.WriteObject(fs, AllSurveys.Answers);
    }
  }
}

public void SaveStore()
{
  SaveLastSyncDate();
  SaveTemplates();
  SaveAnswers();
}

...

private void Initialize()
{
  AllSurveys = new SurveysList();

  if (isolatedStore.Contains(lastSyncDateKey))
  {
    AllSurveys.LastSyncDate = (string)isolatedStore[lastSyncDateKey];
  }

  if (isolatedStore.Contains(unopenedCountKey))
  {
    AllSurveys.UnopenedCount = (int)isolatedStore[unopenedCountKey];
  }

  using (var filesystem = IsolatedStorageFile.GetUserStoreForApplication())
  {
    if (!filesystem.FileExists(storeName + ".templates"))
    {
      AllSurveys.Templates = new List<SurveyTemplate>();
    }
    else
    {
      using (var fs = new IsolatedStorageFileStream(
        storeName + ".templates", FileMode.Open, filesystem))
      {
        var serializer = new System.Runtime.Serialization
          .Json.DataContractJsonSerializer(typeof(
          List<SurveyTemplate>));
        AllSurveys.Templates = serializer.ReadObject(fs) as 
          List<SurveyTemplate>;
      }
    }

    if (!filesystem.FileExists(storeName + ".answers"))
    {
      AllSurveys.Answers = new List<SurveyAnswer>();
    }
    else
    {
      using (var fs = new IsolatedStorageFileStream(
        storeName + ".answers", FileMode.Open, filesystem))
      {
        var serializer = new System.Runtime.Serialization
          .Json.DataContractJsonSerializer(typeof(List<SurveyAnswer>));
        AllSurveys.Answers = serializer.ReadObject(fs) as 
          List<SurveyAnswer>;
      }
    }
  }
}
Hh821020.note(en-us,PandP.10).gifMarkus Says:
Markus Survey templates and survey answers are stored in separate files to enable future development of the synchronization service.

The following code example shows the ISurveyStore interface in the TailSpin.PhoneServices project that defines the operations that the application can perform on the survey store. The SurveyStore class implements this interface.

public interface ISurveyStore
{
  string LastSyncDate { get; set; }
  SurveysList AllSurveys { get; set; }
  IEnumerable<SurveyTemplate> GetSurveyTemplates();
  IEnumerable<SurveyAnswer> GetCompleteSurveyAnswers();
  void SaveSurveyTemplates(IEnumerable<SurveyTemplate> surveys);
  void SaveSurveyAnswer(SurveyAnswer answer);
  SurveyAnswer GetCurrentAnswerForTemplate(SurveyTemplate template);
  void DeleteSurveyAnswers(IEnumerable<SurveyAnswer> surveyAnswers);
  void SaveStore();
  void SaveUnopenedCount();
  void MarkSurveyTemplateRead(SurveyTemplate template);

  event EventHandler SurveyAnswerSaved;
}

The SurveyListViewModel class calls the GetSurveyTemplates method to retrieve a list of surveys to display to the user. The SurveyListViewModel class creates filtered lists of surveys (new surveys, favorite surveys, and so on) after it has added the surveys to CollectionViewSource objects.

The SurveysSynchronizationService class uses the GetCompleteSurveyAnswers, SaveSurveyTemplates, and DeleteSurveyAnswers methods to manage the survey data stored on the device.

The TakeSurveyViewModel class uses the GetCurrentAnswerForTemplate and SaveSurveyAnswer methods to retrieve and save survey responses on the device. In addition, the TakeSurveyViewModel class uses the GetSurveyTemplates method to support pinned surveys.

Last built: May 25, 2012

Show:
© 2014 Microsoft