Using the Documents Library as a database in Windows 8 Store apps

Many Windows 8 Store apps require some amount of structured storage on the client. But in WinRT, local and roaming storage is limited in size, and a client-side relational database does not come out of the box yet. This article describes how to use a folder in the user’s Documents library to store serialized business objects. This gives the app an almost unlimited amount of client-side structured storage. I created a small but representative sample project (code is attached at the end of the article) to illustrate this. Its purpose is to maintain a list of Trappist beers. Here’s how the app looks like:


By default, a Windows 8 Store app runs in the smallest possible sandbox: it does not have the right to directly access the user’s Documents. If you need this capability, you have to register it through the app manifest:


Even with the capability declared, the app’s rights on the documents folder are limited to the registered file types. More on that later.
The sample app uses a specific folder to store its Trappist beers in. Store apps don’t not come with an installer à la .msi, so the app should create the working folder at run time:

private static string AppFolderName = "Trappist Beers";

public static async Task<StorageFolder> AppFolder()
{
    return await KnownFolders.DocumentsLibrary.GetFolderAsync(AppFolderName);
}

public static async Task EnsureFolderExists()
{
    StorageFolder folder = null;

    try
    {
        folder = await KnownFolders.DocumentsLibrary.GetFolderAsync(AppFolderName);
    }
    catch (Exception)
    {
        // Folder does not exist, but we cannot await in a catch block, so we can not create it here.
    }

    if (folder == null)
    {
        folder = await KnownFolders.DocumentsLibrary.CreateFolderAsync(AppFolderName, CreationCollisionOption.OpenIfExists);
    }
}
protected async override void OnLaunched(LaunchActivatedEventArgs args)
{
    await AppModel.EnsureFolderExists();

    // Rest of the app initialization
    // ...
}


We now have a container to store the objects, let’s generate some content. The WinRT framework comes with a whole range of serializers that can serialize business objects to a string that can be persisted in a file and deserialize it back to an object when needed. You can choose between plain XML as explained by Iris Classon, WCF DataContract XML as explained by Sara Silva, and  JSON as explained by Bart Lannoeye. I’m using the XmlSerializer in the sample app, it focuses on the public properties of the class. Here’s how to use this one to serialize and deserialize a Trappist:

// Serialize
XmlSerializer serializer = new XmlSerializer(typeof(Trappist));
using (StringWriter writer = new StringWriter())
{
    serializer.Serialize(writer, trappist);
    String xmlString = writer.ToString();
}

// Deserialize
string xml = await FileIO.ReadTextAsync(file);
XmlSerializer serializer = new XmlSerializer(typeof(Trappist));
trappist = serializer.Deserialize(new StringReader(xml)) as Trappist;


We have a folder now, and we know how to serialize and deserialize its content. Let’s see how our app can access the folder. There are actually two ways to achieve this:
• Through the classic File and Folder API. This requires asynchronous calls.
• Through the MostRecentlyUsed API. This requires less programmatic maintenance, and uses synchronous calls.

For the sample app, I’m assuming that the user will work most of the times with one of the 25 most recent objects that he manipulated. In the case of Trappist beers, that is a fair assumption: there are only 8 authentic Trappist breweries in the world, and together they have not even created 25 Trappist beers. By the way: they’re excellent, I tasted all of them (except the two from Engelszell which are hard to get outside Austria). Anyway, a scenario in which a user mainly works with his most recent business objects, is common.

Each Windows 8 Store app can maintain its own Most Recently Used file list. When registering a file in the MRU list, you are allowed to store some metadata with it. The file itself is indexed through a token (not unsurprisingly based on a GUID). The sample app stores the name of the Trappist beer in the metadata. That name is also used as the physical file name. So I’m actually treating the name of the beer as primary key. Here’s how to add a file to the MRU list:

StorageApplicationPermissions.MostRecentlyUsedList.Add(file, trappist.Name);


We execute this registration when saving a new file, and when opening an existing file. Here’s how to fetch the list of most recently used Trappist names, the most recently accessed file always comes first:

public static ObservableCollection<string> MostRecentTrappists
{
    get
    {
        var result = new ObservableCollection<string>();

        foreach (var item in StorageApplicationPermissions.MostRecentlyUsedList.Entries)
        {
            result.Add(item.Metadata);
        }

        return result;
    }
}


Observe that the call is synchronous. We’re actually not accessing the folder or files themselves.

Of course, the user may add, delete, move, and rename these files with other apps, or directly with the File Explorer. After all: it is *his* Documents folder. So the app’s GUI should also expose that real situation from disk. Here’s how to get the alphabetical list of Trappist names in the app’s working folder:

public async static Task<ObservableCollection<string>> FolderTrappists()
{
    var result = new ObservableCollection<string>();

    var folder = await AppFolder();
    var files = await folder.GetFilesAsync();
    var names = from file in files
                where file.FileType == AppFileExtension
                orderby file.DisplayName
                select file.DisplayName;

    foreach (var name in names)
    {
        result.Add(name);
    }

    return result;
}


This is an asynchronous method because in WinRT we cannot access the file system synchronously.

I use a toggle switch to change between ‘MRU’ and ‘Folder’ mode. In a production app the control would end up in the app bar or the settings panel:


To save memory, the app only displays the list of Trappist names, not the list of full business objects. When saving, opening or deleting a Trappist, we need to make the link with the corresponding physical file. Therefor we first need to fetch the identification token from the MRU list. Here’s how to do this:

private static string GetMruToken(string trappistName)
{
    var token = (from e in StorageApplicationPermissions.MostRecentlyUsedList.Entries
                    where e.Metadata == trappistName
                    select e.Token).FirstOrDefault();
    return token;
}


Using the token, we can get the file, like this:

var token = GetMruToken(trappistName);

if (!String.IsNullOrEmpty(token))
{
    try
    {
        // Get file through MRU list
        file = await StorageApplicationPermissions.MostRecentlyUsedList.GetFileAsync(token);
        return file;
    }
    catch (Exception)
    {
        // File was probably deleted, so continue with app folder.
    }
}


If something went wrong (e.g. the Trappist is not in the MRU anymore), then you should go for the physical file directly:

try
{
    var folder = await AppFolder();
    file = await folder.GetFileAsync(trappistName + AppFileExtension);
}
catch (Exception)
{
    // Ignore
}

return file;


Now you can save, open, and delete the corresponding file for a Trappist name in the list.

So far for the Model and the Data Access Layer, let’s now take a look to the ViewModel. It has the classic CRUD commands to open, create, rename, and delete a Trappist. I’m using the RelayCommand from MVVM Light for this. To get the name for a newly created or renamed Trappist, I’m using an InputDialog control from the WinRTXamlToolkit. Here’s how to work with it:

var dialog = new InputDialog();
var result = await dialog.ShowAsync(
    "Add new Trappist",
    "Please enter the name:",
    "Create",
    "Cancel");

if (result == "Create" && !String.IsNullOrEmpty(dialog.InputText))
{
    // ...
}


This is how it looks like by default:


Since some of the interactions require asynchronous calls under the hood, I decorated the ViewModel with some internal events and local event handlers to bridge the period between setting a property and notifying its change:

private event EventHandler TrappistNamesChanged;

public MainPageViewModel()
{
    // Initialization
    // ...
    this.TrappistNamesChanged += this.MainPageViewModel_TrappistNamesChanged;
}

public ObservableCollection<string> TrappistNames
{
    get
    {
        return this.trappistNames;
    }
}

private async void MainPageViewModel_TrappistNamesChanged(object sender, EventArgs e)
{
    if (this.ShowAll)
    {
        this.trappistNames = await AppModel.FolderTrappists();
    }
    else
    {
        this.trappistNames = AppModel.MostRecentTrappists;
    }

    this.OnPropertyChanged("TrappistNames");
}


I’m not evangelizing this as a best practice, but it seems to do the trick.

I already revealed one advantage of using the file system over a local database: the user can easily copy, move, rename, and delete objects through the File Explorer (admitted: that’s not always an advantage). Let’s take this one step further. Through the app manifest you can register your app as the default app to open files with the corresponding extension(s), and at the same time define a default icon for the files. The declaration of these extensions is mandatory, since it defines the files to which the app has access:


When the user is now browsing through his Documents, he’ll easily recognize the files:


You can let the app open on the selected business object when the user double clicks (or taps) the file. Here’s the code to do that (in app.xaml.cs):

protected override void OnFileActivated(FileActivatedEventArgs args)
{
    if (args.Files.Count > 0)
    {
        var file = args.Files[0];

        Frame rootFrame = Window.Current.Content as Frame;

        if (rootFrame == null)
        {
            rootFrame = new Frame();
            Window.Current.Content = rootFrame;
        }

        // MRU list
        var name = Path.GetFileNameWithoutExtension(file.Name);
        StorageApplicationPermissions.MostRecentlyUsedList.Add(file, name);

        if (!rootFrame.Navigate(typeof(MainPage), name))
        {
            throw new Exception("Failed to create initial page");
        }

        Window.Current.Activate();
    }
}


That’s it, I tried to structure the code to make it as reusable as possible. I have at least three apps waiting for this pattern.

Here’s the code: U2UConsult.Win8.documentLibrarySample.zip (643.43 kb)

Enjoy!
Diederik