Win 8 – Conference Buddy. Storing to Local or Roaming Files

In a previous post, I explained how to store data to a “known location” such as the My Documents folder. Often, SaveCustomerhowever, you will want to store local data to a subfolder of AppData on the user’s disk. This is even easier to do, because you need no special permissions or settings to access either local or roaming data files.

  In this blog post, based in part on work done for my upcoming book Pro Windows 8 With C# and XAML by Jesse Liberty and Jon Galloway, we’re going to explore storing local data, but doing so in an extensible, reusable, well-factored way. In a later posting, I’ll use the same structure to store data using Sqlite.

The actual storage of the file is pretty straight forward, but we’re going to build out a fully reusable Repository model so that we can reapply it to other storage approaches later. We begin with the data file itself. To keep things simple, I’ll stay with the idea of storing and retrieving customer data.

 

Creating the Application

To begin, create a new Windows 8 Store Application using the blank application template, and call it LocalFolderSample. Add a folder named DataModel and in DataModel add a Customer class,


public class Customer
{
    public int Id { get; set; }
    public string Email { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Title { get; set; }
}

Notice that this is a POCO (Plain Old CLR Object) class, nothing special about it. Next, we want to build a file repository, but to do that we’ll start by defining a DataRepository interface. Create a new file, IDataRepository,

public interface IDataRepository
{
    Task Add(Customer customer);
    Task<ObservableCollection<Customer>> Load();
    Task Remove(Customer customer);
    Task Update(Customer customer);
}

This interface has four methods. The only unusual one is Load, which returns a Task of ObservableCollection of Customer. This is so that Load can be run asynchronously, as we’ll see later in this posting.

With this interface, we can build our implementation, in a file named FileRepository.cs,

public class FileRepository : IDataRepository
{
    StorageFolder folder = ApplicationData.Current.LocalFolder;
    string fileName = "customers.json";
    ObservableCollection<Customer> customers;

Our FileRepository class implements IDataRepository, and has three member variables:

  • A StorageFolder representing a local folder under application data
  • A string for the file name of our specific storage file
  • An observable collection of Customer

The constructor calls the Initialize method, which in this case does nothing. The initialize method will be more important when we cover SQLite, which we will in an upcoming blog post.

public FileRepository()
 {
     Initialize();
 }

 private void Initialize()
 {
 }

The interface requires that we implement four methods. The Add method adds a customer (passed in as a parameter) to the customers collection and then calls WriteToFile,

public Task Add(Customer customer)
 {
     customers.Add(customer);
     return WriteToFile();
 }

Write to file is a helper method,

private Task WriteToFile()
{
    return Task.Run(async () => 
    {
        string JSON = JsonConvert.SerializeObject(customers);
        var file = await OpenFileAsync();
        await FileIO.WriteTextAsync(file, JSON);
    });
}

Notice that WriteToFile converts the customers collection to JSON and then opens the file to write to asynchronously and then writes to that file, again asynchronously. To open the file, we add the helper method OpenFileAsync,

private async Task<StorageFile> OpenFileAsync()
 {
     return await folder.CreateFileAsync(fileName, 
          CreationCollisionOption.OpenIfExists);
 }

Notice that when opening the file we handle CreationCollisions by saying that we want to open the file if it already exists.

The second of the four methods we must implement is Remove, which is pretty much the inverse of Add,

public Task Remove(Customer customer)
 {
     customers.Remove(customer);
     return WriteToFile();
 }

The third interface method is Update. Here we have slightly more work to do: we must find the record we want to update and if it is not null then we remove the old version and save the new,

public Task Update(Customer customer)
{
    var oldCustomer = customers.FirstOrDefault(c => c.Id == customer.Id);
    if (oldCustomer == null)
    {
        throw new System.ArgumentException("Customer not found.");
    }
    customers.Remove(oldCustomer);
    customers.Add(customer);
    return WriteToFile();
}

Finally, we come to Load. Here we create our file asynchronously and if it is not null, we read the contents of the file into a string.

public async Task<ObservableCollection<Customer>> Load()
 {
     var file = await folder.CreateFileAsync(fileName, CreationCollisionOption.OpenIfExists);

     string fileContents = string.Empty;
     if (file != null)
     {
         fileContents = await FileIO.ReadTextAsync(file);
     }

We then Deserialize the customer from that string of JSON into a IList of customer, and create an ObservableCollection of customer from that IList,

IList<Customer> customersFromJSON =
          JsonConvert.DeserializeObject<List<Customer>>(fileContents)
              ?? new List<Customer>();

      customers = new ObservableCollection<Customer>(customersFromJSON);

      return customers;
  }

Note that this JSON manipulation requires that we add the JSON.NET library which you can obtain through NuGet or CodePlex. The easiest route is through NuGet as explained in this posting.

Creating the ViewModel

The final file in the DataModel folder is ViewModel.cs. This will act as the data context for the view. It begins by declaring a member variable of type IDataRepository,

IDataRepository _data;

The constructor takes an IDataRepository and initializes the member variable,

public ViewModel(IDataRepository data)
{
    _data = data;
}

In the view model initialize, we tell the repository to load its data,

async public void Initialize()
 {
     Customers = await _data.Load();
 }

There are two public properties in the VM:

private Customer selectedItem;
 public Customer SelectedItem
 {
     get { return this.selectedItem; }
     set
     {
         if (value != selectedItem)
         {
             selectedItem = value;
             RaisePropertyChanged();
         }
     }
 }

 private ObservableCollection<Customer> customers;
 public ObservableCollection<Customer> Customers
 {
     get { return customers; }
     set
     {
         customers = value;
         RaisePropertyChanged();
     }
 }

With these, we are ready to implement the CRUD operations, delegating the work to the repository,

internal void AddCustomer(Customer cust)
 {
     _data.Add(cust);
     RaisePropertyChanged();
 }

 internal void DeleteCustomer(Customer cust)
 {
     _data.Remove(cust);
     RaisePropertyChanged();
 }

Don’t forget to have the VM implement INotifyPropertyChanged,

 

public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(
    [CallerMemberName] string caller = "")
{
    if (PropertyChanged != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(caller));
    }
}

That’s it for the VM. By using a repository, we keep the VM simple, clean and reusable with different storage approaches.

The View begins with a couple styles,

<Page.Resources>
    <Style TargetType="TextBlock">
        <Setter Property="FontSize"
                Value="20" />
        <Setter Property="Margin"
                Value="5" />
        <Setter Property="HorizontalAlignment"
                Value="Right" />
        <Setter Property="Grid.Column"
                Value="0" />
        <Setter Property="Width"
                Value="100" />
        <Setter Property="VerticalAlignment"
                Value="Center" />
    </Style>
    <Style TargetType="TextBox">
        <Setter Property="Margin"
                Value="5" />
        <Setter Property="HorizontalAlignment"
                Value="Left" />
        <Setter Property="Grid.Column"
                Value="1" />
    </Style>
</Page.Resources>

It then adds an AppBar for saving data

<Page.BottomAppBar>
    <AppBar x:Name="BottomAppBar1"
            Padding="10,0,10,0"
            AutomationProperties.Name="Bottom App Bar">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="50*" />
                <ColumnDefinition Width="50*" />
            </Grid.ColumnDefinitions>
            <StackPanel x:Name="LeftPanel"
                        Orientation="Horizontal"
                        Grid.Column="0"
                        HorizontalAlignment="Left">
                <Button x:Name="Save"
                        Style="{StaticResource SaveAppBarButtonStyle}"
                        Tag="Save"
                        Click="Save_Click" />
                <Button x:Name="Delete"
                        Style="{StaticResource DeleteAppBarButtonStyle}"
                        Tag="Delete"
                        Click="Delete_Click" />
            </StackPanel>
        </Grid>
    </AppBar>
</Page.BottomAppBar>

It then has a set of stack panels to gather the data,

<StackPanel Margin="150">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Email"
                   Margin="5" />
        <TextBox Width="200"
                 Height="40"
                 Name="Email"
                 Margin="5" />
    </StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="First Name"
                   Margin="5" />
        <TextBox Width="200"
                 Height="40"
                 Name="FirstName"
                 Margin="5" />
    </StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Last Name"
                   Margin="5" />
        <TextBox Width="200"
                 Height="40"
                 Name="LastName"
                 Margin="5" />
    </StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Title"
                   Margin="5" />
        <TextBox Width="200"
                 Height="40"
                 Name="Title"
                 Margin="5" />
    </StackPanel>

Finally, we add a ListView to display the customers we had on disk, and now have in memory

<ScrollViewer>
    <ListView Name="xCustomers"
              ItemsSource="{Binding Customers}"
              SelectedItem="{Binding SelectedItem, Mode=TwoWay}" 
              Height="400">
        <ListView.ItemTemplate>
            <DataTemplate>
                <StackPanel>
                    <TextBlock Text="{Binding FirstName}" />
                    <TextBlock Text="{Binding LastName}" />
                    <TextBlock Text="{Binding Title}" />
                </StackPanel>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ScrollViewer>

Notice the binding both for the ItemsSource and the SelectedItem.

The code behind is very straightforward. The first thing we do is instantiate an IDataRepository and declare the viewmodel,

public sealed partial class MainPage : Page
{
    private IDataRepository data = new FileRepository();
    private ViewModel _vm;

In the constructor, we create the ViewModel passing in the repository, then call initialize on the VM and finally set the VM as the DataContext for the page,

public MainPage()
 {
     this.InitializeComponent();

     _vm = new ViewModel(data);
     _vm.Initialize();
     DataContext = _vm;
 }

All that is left is to implement the two event handlers

private void Save_Click(object sender, RoutedEventArgs e)
 {
     Customer cust = new Customer
     {
         Email = Email.Text,
         FirstName = FirstName.Text,
         LastName = LastName.Text,
         Title = Title.Text
     };
     _vm.AddCustomer(cust);
 }

 private void Delete_Click(object sender, RoutedEventArgs e)
 {
     if (_vm.SelectedItem != null)
     {
         _vm.DeleteCustomer(_vm.SelectedItem);
     }
 }

When you run the application, you are presented with the form shown at the top of this post. Fill in an entry and save it, and it immediately appears in the list box.

More important, your file, Customers.json, has been saved in application data. You can find it by searching for it under Application Data on your C drive,

Explorer

Double click on that file and see the JSON you’ve saved:

[{“Id”:0,”Email”:”jesse.liberty@telerik.com”,”FirstName”:”Jesse”,”LastName”:”Liberty”,”Title”:”Evangelist”}]

Roaming

To change from storing this in local storage to roaming storage, you must change one line of code. Back in FileRepository.cs change the LocalFolder to a RoamingFolder,

RoamingFolder

Hey! Presto! Without any further work, your application data is now available on any Windows 8 computer you sign into.

Summary

We’ve seen in an earlier article how to write to known file locations such as My Documents, and in this posting how to write to Local or Roaming. In an upcoming posting I’ll demonstrate how to take this same program as shown here, but use it to write to Sqllite.

Download the source code for this example.

Win8_Download (2)

About the author

Jesse Liberty

Jesse Liberty

Jesse Liberty is a Technical Evangelist for Telerik and has three decades of experience writing and delivering software projects. He is the author of 2 dozen books and has been a Distinguished Software Engineer for AT&T and a VP for Information Services for Citibank and a Software Architect for PBS. You can read more on his personal blog and his Telerik blog or follow him on twitter

About Jesse Liberty

Jesse Liberty has three decades of experience writing and delivering software projects and is the author of 2 dozen books and a couple dozen online courses. His latest book, Building APIs with .NET will be released early in 2025. Liberty is a Senior SW Engineer for CNH and he was a Senior Technical Evangelist for Microsoft, a Distinguished Software Engineer for AT&T, a VP for Information Services for Citibank and a Software Architect for PBS. He is a Microsoft MVP.
This entry was posted in Data, Mini-Tutorial, Windows 8 and tagged . Bookmark the permalink.

14 Responses to Win 8 – Conference Buddy. Storing to Local or Roaming Files

Comments are closed.