Condividi tramite


Persisting Data to and from Isolated Storage

patterns & practices Developer Center

You Will Learn

  • How to adapt the IsolatedStorageFileStream SDK class and consume the adapted class.
  • How to adapt the IsolatedStorageFile SDK class and consume the adapted class.
  • How to create a facade over the adapted IsolatedStorageFileStream and adapted IsolatedStorageFile SDK classes and consume the facade.
  • How to adapt the MessageBox SDK class and consume the adapted class.
  • How to create a mock of the IsolatedStorageFileStream class and write unit tests that test view model business logic by consuming the mock class.
  • How to create a mock of the facade over the adapted IsolatedStorageFileStream and adapted IsolatedStorageFile classes and write unit tests that test view model business logic by consuming the mock class.
  • How to create a mock of the MessageBox class and write unit tests that test view model business logic by consuming the mock class.

Applies To

  • Silverlight for Windows Phone OS 7.1
On this page: Download:
The Application | Adapting the IsolatedStorageFile SDK Class | Adapting the IsolatedStorageFileStream SDK Class | Creating a Facade over the IsolatedStorageFileStreamAdapter and IsolatedStorageFileAdapter Classes | Adapting the MessageBox SDK Class | Consuming the Adapters | Unit Testing the Application | Summary Download code samples

The purpose of this sample application is to demonstrate how to build a testable Windows Phone application that persists data to and from isolated storage.

The Application

This sample application saves user-supplied text to isolated storage, and loads the content back from isolated storage and displays it in a message box. You can download the code for this sample application at the following link:

Hh830879.CABCB44D03D1B3D9C8B19DBE980FC99E(en-us,PandP.10).png

When the application is launched, the page in the following illustration is shown. Clicking the save button after entering content in the textbox causes the content to be saved to isolated storage before a message box is displayed informing the user that the content has been saved and the content in the textbox is cleared. Clicking the load button causes the saved content to be loaded from isolated storage and displayed in a message box.

Follow link to expand image

Adapting the IsolatedStorageFile SDK Class

The IsolatedStorageFile class represents an isolated storage area containing files and directories. However, this class is not easily testable. To create a testable version of this class, you should adapt the class, and then create a mock for it.

The interface for the IsolatedStorageFile class is not available in the Windows Phone 7.1 SDK. Therefore, the IIsolatedStorageFile interface was generated from the IsolatedStorageFile SDK class. The following code example shows the IIsolatedStorageFile interface, which contains some of the properties and method signatures implemented by the IsolatedStorageFile SDK class.

public interface IIsolatedStorageFile : IDisposable
{
  long Quota { [SecuritySafeCritical] get; }

  long AvailableFreeSpace { [SecuritySafeCritical] get; }

  [SecuritySafeCritical]
  void Remove();
  …
}

The IIsolatedStorageFile interface is implemented by the IsolatedStorageFileAdapter class. The IsolatedStorageFileAdapter class adapts the IsolatedStorageFile class from the Windows Phone 7.1 SDK. Adapter classes pass parameters and return values to and from the underlying Windows Phone 7.1 SDK class. Any classes that want to consume the IsolatedStorageFile class to access an isolated storage area containing files and directories should consume the IsolatedStorageFileAdapter class instead. The following code example shows the IsolatedStorageFileAdapter class.

public class IsolatedStorageFileAdapter : IIsolatedStorageFile
{
  private IsolatedStorageFile WrappedSubject;

  public IsolatedStorageFileAdapter()
  {
    WrappedSubject = IsolatedStorageFile.GetUserStoreForApplication();
  }

  public void Dispose()
  {
    WrappedSubject.Dispose();
  }

  public long Quota
  {
    get { return WrappedSubject.Quota; }
  }

  public long AvailableFreeSpace
  {
    get { return WrappedSubject.AvailableFreeSpace; }
  }

  public void Remove()
  {
    WrappedSubject.Remove();
  }
  
  …
}

The IsolatedStorageFileAdapter class constructor initializes the WrappedSubject property to be the isolated storage file that corresponds to the isolated storage scope for the application. IsolatedStorageFileAdapter properties and methods then simply get and invoke the IsolatedStorageFile class properties and methods on the WrappedSubject property.

Adapting the IsolatedStorageFileStream SDK Class

The IsolatedStorageFileStream class exposes a file within isolated storage. However, this class is not easily testable. To create a testable version of this class you should adapt the class, and then create a mock for it.

The interface for the IsolatedStorageFileStream class is not available in the Windows Phone 7.1 SDK. Therefore, the IIsolatedStorageFileStream interface was generated from the IsolatedStorageFileStream SDK class. The following code example shows the IIsolatedStorageFileStream interface, which contains some of the properties and method signatures implemented by the IsolatedStorageFileStream SDK class.

public interface IIsolatedStorageFileStream : IDisposable
{
  bool CanRead { get; }
  bool CanWrite { get; }
  bool CanSeek { get; }
  long Length { get; }
  long Position { get; set; }

  …
 
  int Read(byte[] buffer, int offset, int count);
  int ReadByte();

  …

  [SecuritySafeCritical]
  void Write(byte[] buffer, int offset, int count);

  [SecuritySafeCritical]
  void WriteByte(byte value);
  
  …
}

The IIsolatedStorageFileStream interface is implemented by the IsolatedStorageFileStreamAdapter class. The IsolatedStorageFileStreamAdapter class adapts the IsolatedStorageFileStream class from the Windows Phone 7.1 SDK. The IsolatedStorageFileStreamAdapter class inherits from the IsolatedStorageFileStream class and provides no additional functionality. Any classes that want to consume the IsolatedStorageFileStream class to expose a file within isolated storage should consume the IsolatedStorageFileStreamAdapter class instead. The following code example shows the IsolatedStorageFileStreamAdapter class.

public class IsolatedStorageFileStreamAdapter : IsolatedStorageFileStream, 
  IIsolatedStorageFileStream
{
  public IsolatedStorageFileStreamAdapter(string path, FileMode mode, 
    IsolatedStorageFile isf)
    : base(path, mode, isf)
  {
  }

  public IsolatedStorageFileStreamAdapter(string path, FileMode mode, 
    FileAccess access, IsolatedStorageFile isf)
    : base(path, mode, access, isf)
  {
  }

  public IsolatedStorageFileStreamAdapter(string path, FileMode mode, 
    FileAccess access, FileShare share, IsolatedStorageFile isf)
    : base(path, mode, access, share, isf)
  {
  }
}

The IsolatedStorageFileStreamAdapter class constructor simply calls the constructor of the base IsolatedStorageFileStream SDK class.

Creating a Facade over the IsolatedStorageFileStreamAdapter and IsolatedStorageFileAdapter Classes

The IsolatedStorageFileAdapter class provides access to an isolated storage area containing files and directories. The IsolatedStorageFileStreamAdapter class exposes a file within isolated storage. However, both classes expose a lot of complexity. A simplified interface to the classes can be provided by creating a facade over the classes. The following code example shows the IIsolatedStorageFacade interface.

public interface IIsolatedStorageFacade
{
  IIsolatedStorageFileStream CreateStream(string path, FileMode mode);
}

The IIsolatedStorageFacade class is implemented by the IsolatedStorageFacade class. The IsolatedStorageFacade class provides a facade over the IsolatedStorageFileAdapter and IsolatedStorageFileStreamAdapter classes. A facade class provides a simplified interface to the underlying classes. Any classes that want to consume the IsolatedStorageFileAdapter and IsolatedStorageFileStreamAdapter classes should consume the IsolatedStorageFacade class instead. The following code example shows the IsolatedStorageFacade class.

public class IsolatedStorageFacade : IIsolatedStorageFacade
{
  public IIsolatedStorageFileStream CreateStream(string path, FileMode mode)
  {
    var store = new IsolatedStorageFileAdapter();
    return new IsolatedStorageFileStreamAdapter(path, mode, 
      store.IsolatedStorageFile);
  } 
}

The CreateStream method uses the IsolatedStorageFileAdapter and IsolatedStorageFileStreamAdapter classes to return access to the isolated storage file that corresponds to the isolated storage scope based on the identity of the application.

Adapting the MessageBox SDK Class

The MessageBox class displays a message to the user and optionally prompts for a response. However, this class is not easily testable because it contains some static members. To create a testable version of this class, you should adapt the class, and then create a mock for it.

The interface for the MessageBox class is not available in the Windows Phone 7.1 SDK. Therefore, the IMessageBox interface was generated from the MessageBox SDK class. The following code example shows the IMessageBox interface, which contains two method signatures.

public interface IMessageBox
{
  MessageBoxResult Show(string messageBoxText);
  MessageBoxResult Show(string messageBoxText, string caption, 
    MessageBoxButton button);
}

The IMessageBox interface is implemented by the MessageBoxAdapter class. The MessageBoxAdapter class adapts the MessageBox class from the Windows Phone 7.1 SDK. Adapter classes pass parameters and return values to and from the underlying Windows Phone 7.1 SDK class. Any classes that want to consume the MessageBox class to display a message to the user should consume the MessageBoxAdapter class instead. The following code example shows the MessageBoxAdapter class.

public class MessageBoxAdapter : IMessageBox
{
  public MessageBoxResult Show(string message)
  {
    return MessageBox.Show(message);
  }

  public MessageBoxResult Show(string messageBoxText, string caption, 
    MessageBoxButton button)
  {
    return MessageBox.Show(messageBoxText, caption, button);
  }
}

The MessageBoxAdapter class implements two Show methods. The first Show method displays a message box that contains the specified text and an OK button. The second Show method displays a message box that contains the specified text, title bar caption, and response buttons.

Consuming the Adapters

The following code example shows the MainPage view, which comprises a series of TextBlocks and two Buttons.

<Grid x:Name="LayoutRoot" Background="Transparent">
  …
  <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
    <TextBlock x:Name="ApplicationTitle" Text="MY APPLICATION" 
      Style="{StaticResource PhoneTextNormalStyle}"/>
    <TextBlock x:Name="PageTitle" Text="page name" Margin="9,-7,0,0" 
      Style="{StaticResource PhoneTextTitle1Style}"/>
  </StackPanel>

  <!--ContentPanel - place additional content here-->
  <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <StackPanel>
      <TextBox Text="{Binding TextToSave, Mode=TwoWay}" />
      <Button Content="Save Content" Click="SaveButton_Click" />
      <Button Content="Load Content" Click="LoadButton_Click" />
    </StackPanel>
  </Grid>
</Grid>

Both Buttons call event handlers when their Click event is raised. Each event handler simply calls methods in the MainPageViewModel class and displays a message box to the user. The MainPage view also contains a TextBox that is bound to the TextToSave property of the MainPageViewModel class.

The ViewModelLocator class connects the view to the view model by settings the view's DataContext to the value of the MainPageViewModel property in the ViewModelLocator class. The following code example shows the MainPageViewModel property.

public MainPageViewModel MainPageViewModel 
{ 
  get { return new MainPageViewModel(
    new IsolatedStorageFacade(),
    new MessageBoxAdapter());
}

The MainPageViewModel property returns a new instance of the MainPageViewModel class, and passes new instances of the IsolatedStorageFacade and MessageBoxAdapter classes into the MainPageViewModel constructor.

The MainPageViewModel class uses the IIsolatedStorageFacade interface to persist data to and from isolated storage, and an instance of the IMessageBox interface to display a message box to the user. The following code example shows how the constructor receives the IIsolatedStorageFacade and IMessageBox instances from the ViewModelLocator class.

private IIsolatedStorageFacade isolatedStorageFacade;
private IMessageBox messageBox;
…

public MainPageViewModel(IIsolatedStorageFacade isolatedStorageFacade, 
  IMessageBox messageBox)
{
  this.isolatedStorageFacade = isolatedStorageFacade;
  this.messageBox = messageBox;
}

The constructor sets the isolatedStorageFacade field to the instance of IIsolatedStorageFacade it receives from the ViewModelLocator class; in this case an instance of the IsolatedStorageFacade class. It also sets the messageBox field to the instance of the IMessageBox it receives from the ViewModelLocator class; in this case an instance of the MessageBoxAdapter class. Initializing the class in this way enables view model testability, as mock implementations of the IsolatedStorageFacade and MessageBoxAdapter classes can be passed into the constructor.

The following code example shows the SaveToFile and LoadFromFile methods that are invoked from the event handler methods in the view code-behind.

string fileName = "filename.dat";
…

public void SaveToFile(string content)
{
  try
  {
    using (var stream = isolatedStorageFacade
      .CreateStream(fileName, FileMode.Create))
    using (var writer = new StreamWriter((Stream)stream))
    {
      writer.Write(content);
    }
  }
  catch (Exception ex)
  {
    messageBox.Show(ex.Message);
  }
}

public string LoadFromFile()
{
  try
  {
    using (var stream = isolatedStorageFacade
      .CreateStream(fileName, FileMode.Open))
    using (var reader = new StreamReader((Stream)stream))
    {
      return reader.ReadToEnd();
    }
  }
  catch (Exception e)
  {
    return String.Empty;
  }
}

The SaveToFile method attempts to write the value of the content parameter to a file in isolated storage named filename.dat. In this case, the value of the content parameter is the value of the TextToSave property. It does this by using the StreamWriter class to write to the stream returned by the CreateStream method of the IsolatedStorageFacade class. If an exception occurs, this is caught and displayed to the user in the instance of the MessageBoxAdapter that was passed into the MainPageViewModel constructor.

The LoadFromFile method attempts to read the data from the filename.dat file in isolated storage. It does this by using the StreamReader class to read from the stream returned by the CreateStream method of the IsolatedStorageFacade class. If an exception occurs, it is caught and an empty string is returned.

The MainPageViewModel implements the INotifyPropertyChanged interface, thus using change notification to notify the view when a property in the view model changes.

Unit Testing the Application

The Microsoft.Practices.Phone.Testing project contains mock implementations of the Windows Phone 7.1 SDK adapter and facade classes contained in the Microsoft.Practices.Phone.Adapters project. These mocks were developed to be general purpose with many of them having properties that accept delegates. Delegate accepting properties enable the execution of any desired behavior necessary for the unit test.

The following code example shows the MockIsolatedStorageFileStream class.

public class MockIsolatedStorageFileStream : Stream, IIsolatedStorageFileStream
{
  public BufferOffsetCountParameters WriteCalledWithParameters { get; set; }
  public Action<byte[], int, int> WriteTestCallback { get; set; }

  public BufferOffsetCountParameters ReadCalledWithParameters { get; set; }
  public Func<byte[], int, int, byte[]> ReadTestCallback { get; set; }

  public MockIsolatedStorageFileStream(bool canRead, bool canWrite, 
    bool canSeek, long length)
  {
    …
    WriteTestCallback = (b, o, c) => { };
    ReadTestCallback = (b, o, c) => null;
  }

  …

  public override int Read(byte[] buffer, int offset, int count)
  {
    ReadCalledWithParameters = new BufferOffsetCountParameters
    {
      buffer = buffer,
      offset = offset,
      count = count
    };
    var contentBytes = ReadTestCallback(buffer, offset, count);
    for (int index = 0; index < contentBytes.Length; index++)
    {
      buffer[index] = contentBytes[index];
    }
    return contentBytes.Length;
  }

  …

  public override void Write(byte[] buffer, int offset, int count)
  {
    WriteCalledWithParameters = new BufferOffsetCountParameters
    {
      buffer = buffer,
      offset = offset,
      count = count
    };
    WriteTestCallback(buffer, offset, count);
  }

  …
}

The MockIsolatedStorageFileStream class implements the IIsolatedStorageFileStream interface and is an example of how a general-purpose mock can be given behavior using delegates. The WriteTestCallback and ReadTestCallback properties can be set with a delegate or a lambda expression so that calls to the Read and Write methods can execute the delegate. By initializing the mock in this way, unit tests have unlimited test control of the mock.

Unit tests can then be written that use the mock to test aspects of the view model business logic. The following code example shows the LoadFromFileGetsContent test method, which demonstrates how to initialize a mock by passing a delegate to it, and which follows the standard arrange, act, assert pattern. The unit test validates that the MainPageViewModel class can load content from a file in isolated storage.

[TestMethod]
public void LoadFromFileGetsContent()
{
  var isolatedStorageFacade = new MockIsolatedStorageFacade();
  var isolatedStorageFileStream = new MockIsolatedStorageFileStream(
    true, true, true, 10);
  isolatedStorageFacade.CreateStreamReturnValue = isolatedStorageFileStream;
  UTF8Encoding encoding = new UTF8Encoding();
  bool isEndOfStream = false;
  isolatedStorageFileStream.ReadTestCallback = (b, o, c) =>
  {
    if (!isEndOfStream)
    {
      var contentBytes = encoding.GetBytes("content");
      isEndOfStream = true;
      return contentBytes;
    }
    return new byte[0]; //End of Stream
  };
            
  var target = new MainPageViewModel(isolatedStorageFacade, 
    new MockMessageBox());

  var fileContents = target.LoadFromFile();

  Assert.AreEqual("content", fileContents);
}

The test method creates instances of the MockIsolatedStorageFacade and MockIsolatedStorageFileStream classes, and then sets the CreateStreamReturnValue property of the instance of the MockIsolatedStorageFacade class to the instance of the MockIsolatedStorageFileStream class. The test method then provides behavior to the ReadTestCallback property in the instance of the MockIsolatedStorageFileStream class, which will be executed when the Read method is called. In this case, the behavior is to encode a string into a sequence of bytes, and return that sequence of bytes. An instance of the MainPageViewModel class is then created, passing the instance of the MockIsolatedStorageFacade class and a new instance of the MockMessageBox class into the MainPageViewModel constructor. The LoadFromFile method of the MainPageViewModel class is then called, which attempts to read the data from isolated storage. This causes the Read method of the MockIsolatedStorageFileStream class to be called, which in turns executes the behavior passed into the ReadTestCallback property, which is to return a sequence of bytes representing a string. The LoadFromFile method reads the sequence of bytes and returns the string, which is stored in the fileContents field. Finally, the test method validates that the contents of the fileContents field is equal to the string originally passed into the ReadTestCallback property.

The following code example shows the MockIsolatedStorageFacade class.

public class MockIsolatedStorageFacade : IIsolatedStorageFacade
{
  public IIsolatedStorageFileStream CreateStreamReturnValue { get; set; }

  public IIsolatedStorageFileStream CreateStream(string path, FileMode mode)
  {
    return CreateStreamReturnValue;
  }
}

The MockIsolatedStorageFacade class implements the IIsolatedStorageFacade interface, with the CreateStream method simply returning the CreateStreamReturnValue property.

Unit tests can then be written that use the mock to test aspects of the view model business logic. The following code example shows the SaveToFilePersistsContent test method, which follows the standard arrange, act, assert pattern. The unit test validates that the MainPageViewModel class can save content to a file in isolated storage.

[TestMethod]
public void SaveToFilePersistsContent()
{
  var isolatedStorageFacade = new MockIsolatedStorageFacade();
  var isolatedStorageFileStream = new MockIsolatedStorageFileStream(
    true, true, true, 10);
  isolatedStorageFacade.CreateStreamReturnValue = isolatedStorageFileStream;
  var valueWritten = string.Empty;
  var target = new MainPageViewModel(isolatedStorageFacade, 
    new MockMessageBox());

  target.SaveToFile("content");

  UTF8Encoding encoding = new UTF8Encoding();
  valueWritten = encoding.GetString(
    isolatedStorageFileStream.WriteCalledWithParameters.buffer,
    isolatedStorageFileStream.WriteCalledWithParameters.offset,
    isolatedStorageFileStream.WriteCalledWithParameters.count);
  Assert.AreEqual("content", valueWritten);
}

The test method creates instances of the MockIsolatedStorageFacade and MockIsolatedStorageFileStream classes, and then sets the CreateStreamReturnValue property of the instance of the MockIsolatedStorageFacade class to the instance of the MockIsolatedStorageFileStream class. An instance of the MainPageViewModel class is then created, passing the instance of the MockIsolatedStorageFacade class and a new instance of the MockMessageBox class into the MainPageViewModel constructor. The SaveToFile method of the MainPageViewModel class is then called, which attempts to write the value of the data passed into the method to a file. The GetString method from the UTF8Encoding class then attempts to read the string back from isolatedStorageFileStream.WriteCalledWithParameters, storing the result in the valueWritten field. Finally, the test method validates that the content of the valueWritten field is equal to the string originally written to isolated storage.

The following code example shows the MockMessageBox class.

public class MockMessageBox : IMessageBox
{
  public string MessageBoxText { get; set; }
  public MessageBoxResult MessageBoxResult { get; set; }
  public bool ShowCalled { get; set; }

  public MessageBoxResult Show(string messageBoxText)
  {
    ShowCalled = true;
    MessageBoxText = messageBoxText;
   return MessageBoxResult;
  }

  public MessageBoxResult Show(string messageBoxText, string caption, 
    MessageBoxButton button)
  {
    ShowCalled = true;
    MessageBoxText = messageBoxText;
    return MessageBoxResult;
  }
}

The MockMessageBox class implements the IMessageBox interface, with the pattern being that the Show methods set the ShowCalled property to true to indicate that a Show method has been called. The ShowCalled property can then be queried from outside the mock.

Unit tests can then be written that use the mock to test aspects of the view model business logic. The following code example shows the SaveShowsMessageBoxWhenIsoStoreThrows test method, which demonstrates how to initialize a mock by passing a delegate to it, and which follows the standard arrange, act, assert pattern. The unit test validates that a message box will be displayed to the user when the SaveToFile method of the MainPageViewModel class is called.

[TestMethod]
public void SaveShowsMessageBoxWhenIsoStoreThrows()
{
  var isolatedStorageFacade = new MockIsolatedStorageFacade();
  var isolatedStorageFileStream = new MockIsolatedStorageFileStream(true, 
    true, true, 10);
  isolatedStorageFacade.CreateStreamReturnValue = isolatedStorageFileStream;
  isolatedStorageFileStream.WriteTestCallback = (b, o, c) => 
  { 
    throw new IOException("error message"); 
  };
  var messageBox = new MockMessageBox();
  var target = new MainPageViewModel(isolatedStorageFacade, messageBox);

  target.SaveToFile("content");

  Assert.IsTrue(messageBox.ShowCalled);
  Assert.AreEqual("error message", messageBox.MessageBoxText);
}

The test method creates instances of the MockIsolatedStorageFacade and MockIsolatedStorageFileStream, and then sets the CreateStreamReturnValue property of the instance of the MockIsolatedStorageFacade class to the instance of the MockIsolatedStorageFileStream class. The test method then provides behavior to the WriteTestCallback property in the instance of the MockIsolatedStorageFileStream class, which will be executed when the Write method is called. In this case, the behavior is to throw an IOException. An instance of the MockMessageBox class it then created, followed by an instance of the MainPageViewModel class. The SaveToFile method of the MainPageViewModel class is then called, which attempts to write the value of the data passed into the method to a file. This causes the Write method of the MockIsolatedStorageFileStream class to be called, which in turn executes the behavior passed into the WriteTestCallback property, which is to throw an IOException. The SaveToFile method catches this exception and calls the Show method of the instance of the MockMessageBox class, which sets the ShowCalled property to true. Finally, the test method validates that the ShowCalled property of the instance of the MockMessageBox class is true, and that the IOException error message is identical to the MessageBoxText property of the instance of the MockMessageBox class.

Summary

The sample application has demonstrated how to build a testable Windows Phone application that persists data to and from isolated storage. The IsolatedStorageFileStreamAdapter class adapts the IsolatedStorageFileStream SDK class, the IsolatedStorageFileAdapter class adapts the IsolatedStorageFile SDK class, and the IsolatedStorageFacade class provides a facade over the two adapted classes. In addition, the MessageBoxAdapter class adapts the MessageBox SDK class. The application then consumes the adapter and facade classes. The MockIsolatedStorageFileStream, MockIsolatedStorageFacade, and MockMessageBox classes are used by unit tests to test business logic contained in the view model class.

Next Topic | Previous Topic | Home

Last built: February 10, 2012