Saving data to the file
system is a necessity for most real applications. On Windows Phone 7,
each application can access an isolated file system to read and write
data that is only accessible to that application. This means that
applications cannot share data with each other via the file system. It
also means that one application cannot access or overwrite data from
another application in the file system.
There are several
namespaces related to data persistence that are available on Windows
Phone 7. These namespaces include the following:
System.IO: Provides access to base Stream, StreamWriter, and StreamReader classes.
System.IO.IsolatedStorage: Provides access to Isolated Storage.
System.Runtime.Serialization: Must add a reference to System.Runtime.Serialization assembly.
System.Xml: Provides access to XMLReader stream class, as well as other core XML related classes.
System.Xml.Linq: Must add a reference to System .Xml.Linq assembly. Provides access to XDocument class for XML manipulation as well as LINQ language constructs.
System.Xml.Serialization: Provides access to XML serialization attributes that you can apply to .NET classes.
If you are a .NET developer, most of
these look familiar, as they are the standard .NET classes related to
serializing .NET objects, as well as reading and writing data to the
file system. The one exception is possibly System.IO.IsolatedStorage,
which is available via Silverlight on the desktop and also on Windows
Phone 7. Isolated Storage represents the physical file system made
available via the IsolatedStorage classes.
Unlike with Silverlight for the
Web, there isn't an Isolated Storage quota on Windows Phone 7; however,
it is recommended to not exceed two Gigabytes of data for an
application if you are to be a good citizen and not abuse available
space. Also, applications should make users aware of estimated file
space requirements and try to give an option to delete data if needed.
Keeping the user informed and in control is paramount.
The System.IO.IsolatedStorage
namespace provides two possible methods to access the file system. The
first is a key-value pair of type string-object where the key is a
string and the value is of type object available in the static class IsolatedStorageSettings via its single property named ApplicationSettings. The Dictionary class stores object values, so essentially any class that can be serialized can be stored in the IsolatedStorageSettings.ApplicationSettings object. It is not limited to just simple values.
The other method to access the file system is via file and folder management using the IsolatedStorageFilestatic class. The method that provides access to the application's file system area is the GetUserStoreForApplication() method. With a reference to the user store in hand, you can create files to serialize objects to the file system.
The next two sections cover
the sample code details for this section. The first example covers
basic isolated storage operations, and the second example covers object
persistence and serialization.
1. Basic File IO
Figure 1 has the UI.
The UI has several controls that store fake settings and data values. The sample uses both the ApplicationSettings object and an IsolatedStorageFileStream object to store and retrieve the values.
NOTE
All of the data could persist within the ApplicationSettings object, but the sample shows how to work with the IsolatedStorageFileStream to prepare for more complex scenarios.
The code is straightforward Dictionary object access and file IO. See Listing 1.
Example 1. BasicIsoStorage.xaml.cs Code File
using System;
using System.IO;
using System.IO.IsolatedStorage;
using Microsoft.Phone.Controls;
namespace DataPersistence.pages
{
public partial class BasicIsoStorage : PhoneApplicationPage
{
private const string fileName = "notes.dat";
public BasicIsoStorage()
{
InitializeComponent();
}
private void saveAppBarIcon_Click(object sender, EventArgs e)
{
SaveData();
}
private void loadAppBarIcon_Click(object sender, EventArgs e)
{
LoadData();
}
private void LoadData()
{
//Load "settings"
if (IsolatedStorageSettings.ApplicationSettings.Contains("EnablePush"))
enableNotifications.IsChecked =
(bool)IsolatedStorageSettings.ApplicationSettings["EnablePush"];
if (IsolatedStorageSettings.ApplicationSettings.Contains("FavColor"))
colorListBox.SelectedIndex =
(int)IsolatedStorageSettings.ApplicationSettings["FavColor"];
if (IsolatedStorageSettings.ApplicationSettings.Contains("NickName"))
nicknameTextBox.Text =
(string)IsolatedStorageSettings.ApplicationSettings["NickName"];
//Load "notes" text to file
using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
{
if (isf.FileExists(fileName))
{
using (IsolatedStorageFileStream fs =
isf.OpenFile(fileName, System.IO.FileMode.Open))
{
using (StreamReader reader = new StreamReader(fs))
{
notesTextBox.Text = reader.ReadToEnd();
reader.Close();
}
}
}
}
}
private void SaveData()
{
//Save "settings"
IsolatedStorageSettings.ApplicationSettings["EnablePush"] =
enableNotifications.IsChecked;
IsolatedStorageSettings.ApplicationSettings["FavColor"] =
colorListBox.SelectedIndex;
IsolatedStorageSettings.ApplicationSettings["NickName"] =
nicknameTextBox.Text;
//Save "notes" text to file
using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
{
using (IsolatedStorageFileStream fs =
isf.OpenFile(fileName, System.IO.FileMode.Create))
{
using (StreamWriter writer = new StreamWriter(fs))
{
writer.Write(notesTextBox.Text);
writer.Flush();
writer.Close();
}
}
}
}
}
}
|
There are two application bar icons to load and save data. Loading data is a matter of checking that a key exists using ApplicationSettings.Contains,
and then accessing the value via the key. The ApplicationSettings class
is of type Dictionary that takes a key value of type String and stores
the passed-in class as a type of Object. This permits you to pass any
class into a Dictionary object, since all classes inherit from type
Object. You must, however, type convert the retrieved object to the
original type.Otherwise a runtime error will occur.
Loading the "Notes" TextBox data checks that the file exists and just reads the data using a StreamReader object. You may be tempted to put the filename into the StreamReader
constructor directly. This will result in a code security access
violation if you do. The only valid constructor parameter for an
instance of StreamReader or StreamWriter is an object of type IsolatedStorageFile.
Saving the data is very similar code to loading the data. Note the use of the using construct for all objects that have a Dispose method. The following is the pseudocode:
Using (TextReader textReader = new StreamReader(streamResourceInfo.Stream) // create
isntance
{
//...code that leverages the instance....
}//Disposed called automatically when the block is exited
NOTE
Only .NET objects that have handles to non-managed objects like a physical file handle have Dispose methods in .NET.
The using clause ensures that Dispose
is called and that unmanaged resources are released. Otherwise, memory
will not be freed immediately, and the runtime memory load will slowly
increase until either Garbage Collection occurs or until the memory
threshold limit is exceeded, and the app fails or until the application
exits. Now that we've covered the basic isolated storage operations, we
move on next to object persistence.
2. Object Persistence
In the previous
section, we demonstrated how to store and retrieve individual values. In
this section, we add some realism by serializing objects instead of
individual values. The ObjectSerialization.xaml sample starts with almost the same UI and values as the BasicIsoStorage sample, but this time the controls data bind to a sample class named AppClass, as shown in Listing 2.
Example 2. AppClass.cs Code File
using System.Xml.Serialization;
namespace DataPersistence
{
[XmlRootAttribute("AppClass")]
public class AppClass
{
public AppClass()
{
FavoriteColor = −1;
}
//Settings
[XmlElement]
public bool EnablePushNotifications { get; set; }
[XmlElement]
public int FavoriteColor { get; set; }
[XmlElement]
public string NickName { get; set; }
//Data
[XmlElement]
public string Notes { get; set; }
}
}
|
The sample AppClass class that we serialize in this example includes attributes from the System.Xml.Serialization
namespace to provide support for serializing the object to the file
system. The MSDN documentation covers all of the possible XML
serialization attribute values:
http://msdn.microsoft.com/en-us/library/83y7df3e(v=VS.100).aspx
Configuring the data binding with Blend using sample data based on the AppClass is straightforward. A sample data source is added to the project that is based on the .NET AppClass class. This facilitates design-time data binding without causing issues at run-time.
In order to save and load the data, the code-behind for ObjectSerialization.xaml has modified Save and Load methods that serialize and deserialize an instance of the AppClass object, and configures it as the DataContext for the LayoutRoot Grid object. The following are the modified methods:
private void LoadData()
{
using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
{
if (isf.FileExists(fileName))
{
using (IsolatedStorageFileStream fs =
isf.OpenFile(fileName, System.IO.FileMode.Open))
{
XmlSerializer serializer = new XmlSerializer(typeof(AppClass));
LayoutRoot.DataContext = (AppClass)serializer.Deserialize(fs);
}
}
}
}
private void SaveData()
{
using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
{
using (IsolatedStorageFileStream fs =
isf.OpenFile(fileName, System.IO.FileMode.Create))
{
XmlSerializer xs = new XmlSerializer(typeof(AppClass));
xs.Serialize(fs, ((AppClass)LayoutRoot.DataContext));
}
}
}
The serialization attributes attached to the AppClass object tell the XmlSerializer object how to read and write the class in Xml format. From there, the rest of the code in SaveData and LoadData methods is boilerplated, isolated storage file I/O, as in the previous sample.
The samples in this
section exposed data management issues when you run the sample
application to test the samples. If you navigate away from the sample,
such as going to the Start screen, and then navigate back, the UI resets
to what it is at startup. The data that was displayed is not preserved.
Serializing out UI objects and settings is a critically important
requirement for Windows Phone 7 applications. The concepts covered in
this section lay the groundwork for the section titled Application Life
Cycle Management.
In
that section, application and data architecture is discussed, such that
the data can be made to appear to be preserved, and to the user it will
appear that the application was waiting in the background for the user
to return to it. Before jumping there, we cover launchers and choosers
first in the next section.