In a previous post, I explained how to store data to a “known location” such as the My Documents folder. Often, however, 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.
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,
publicclassCustomer{publicintId {get;set; }publicstringEmail {get;set; }publicstringFirstName {get;set; }publicstringLastName {get;set; }publicstringTitle {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,
publicinterfaceIDataRepository{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,
publicclassFileRepository : IDataRepository{StorageFolder folder = ApplicationData.Current.LocalFolder;stringfileName ="customers.json";ObservableCollection<Customer> customers;
Our FileRepository class implements IDataRepository, and has three member variables:
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.
publicFileRepository(){Initialize();}privatevoidInitialize(){}
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,
publicTask Add(Customer customer){customers.Add(customer);returnWriteToFile();}
Write to file is a helper method,
privateTask WriteToFile(){returnTask.Run(async () =>{stringJSON = 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,
privateasync Task<StorageFile> OpenFileAsync(){returnawait 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,
publicTask Remove(Customer customer){customers.Remove(customer);returnWriteToFile();}
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,
publicTask Update(Customer customer){var oldCustomer = customers.FirstOrDefault(c => c.Id == customer.Id);if(oldCustomer ==null){thrownewSystem.ArgumentException("Customer not found.");}customers.Remove(oldCustomer);customers.Add(customer);returnWriteToFile();}
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.
publicasync Task<ObservableCollection<Customer>> Load(){var file = await folder.CreateFileAsync(fileName, CreationCollisionOption.OpenIfExists);stringfileContents =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)??newList<Customer>();customers =newObservableCollection<Customer>(customersFromJSON);returncustomers;}
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.
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,
publicViewModel(IDataRepository data){_data = data;}
In the view model initialize, we tell the repository to load its data,
asyncpublicvoidInitialize(){Customers = await _data.Load();}
There are two public properties in the VM:
privateCustomer selectedItem;publicCustomer SelectedItem{get{returnthis.selectedItem; }set{if(value != selectedItem){selectedItem = value;RaisePropertyChanged();}}}privateObservableCollection<Customer> customers;publicObservableCollection<Customer> Customers{get{returncustomers; }set{customers = value;RaisePropertyChanged();}}
With these, we are ready to implement the CRUD operations, delegating the work to the repository,
internalvoidAddCustomer(Customer cust){_data.Add(cust);RaisePropertyChanged();}internalvoidDeleteCustomer(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><StyleTargetType="TextBlock"><SetterProperty="FontSize"Value="20"/><SetterProperty="Margin"Value="5"/><SetterProperty="HorizontalAlignment"Value="Right"/><SetterProperty="Grid.Column"Value="0"/><SetterProperty="Width"Value="100"/><SetterProperty="VerticalAlignment"Value="Center"/></Style><StyleTargetType="TextBox"><SetterProperty="Margin"Value="5"/><SetterProperty="HorizontalAlignment"Value="Left"/><SetterProperty="Grid.Column"Value="1"/></Style></Page.Resources>
It then adds an AppBar for saving data
<Page.BottomAppBar><AppBarx:Name="BottomAppBar1"Padding="10,0,10,0"AutomationProperties.Name="Bottom App Bar"><Grid><Grid.ColumnDefinitions><ColumnDefinitionWidth="50*"/><ColumnDefinitionWidth="50*"/></Grid.ColumnDefinitions><StackPanelx:Name="LeftPanel"Orientation="Horizontal"Grid.Column="0"HorizontalAlignment="Left"><Buttonx:Name="Save"Style="{StaticResource SaveAppBarButtonStyle}"Tag="Save"Click="Save_Click"/><Buttonx: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,
<StackPanelMargin="150"><StackPanelOrientation="Horizontal"><TextBlockText="Email"Margin="5"/><TextBoxWidth="200"Height="40"Name="Email"Margin="5"/></StackPanel><StackPanelOrientation="Horizontal"><TextBlockText="First Name"Margin="5"/><TextBoxWidth="200"Height="40"Name="FirstName"Margin="5"/></StackPanel><StackPanelOrientation="Horizontal"><TextBlockText="Last Name"Margin="5"/><TextBoxWidth="200"Height="40"Name="LastName"Margin="5"/></StackPanel><StackPanelOrientation="Horizontal"><TextBlockText="Title"Margin="5"/><TextBoxWidth="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><ListViewName="xCustomers"ItemsSource="{Binding Customers}"SelectedItem="{Binding SelectedItem, Mode=TwoWay}"Height="400"><ListView.ItemTemplate><DataTemplate><StackPanel><TextBlockText="{Binding FirstName}"/><TextBlockText="{Binding LastName}"/><TextBlockText="{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,
publicsealedpartialclassMainPage : Page{privateIDataRepository data =newFileRepository();privateViewModel _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,
publicMainPage(){this.InitializeComponent();_vm =newViewModel(data);_vm.Initialize();DataContext = _vm;}
All that is left is to implement the two event handlers
privatevoidSave_Click(objectsender, RoutedEventArgs e){Customer cust =newCustomer{Email = Email.Text,FirstName = FirstName.Text,LastName = LastName.Text,Title = Title.Text};_vm.AddCustomer(cust);}privatevoidDelete_Click(objectsender, 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,
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"}]
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,
Hey! Presto! Without any further work, your application data is now available on any Windows 8 computer you sign into.
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.
Jesse Liberty 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 or follow him on twitter