The goal is to persist data to a file. You might do this for any number of reasons, including storing away user-preferences or, in this case, storing away data to protect you from a crash.
In this simple application we collect names and display them in a list. If the program crashes after the names are stored to disk, clicking restore will bring them back.
To do this we’re going to create a generic file repository. This is overkill for this simple demo example, but can be a very powerful pattern to use with larger applications.
The Interface
[Begin by creating a standard Xamarin.Forms project]
Because Android and iOS handle files differently, we’ll start by creating an interface, and we’ll use the Dependency Service to invoke the correct implementation at run time.
using System;
namespace FileRepo
{
public interface IFile
{
void SaveText (string filename, string text);
string LoadText (string filename);
void ClearFile (string filename);
bool FileExists (string filename);
}
}
We now turn to the Android implementation of this interface, which we put into a file named File.cs in the Android project.
using System; using System.IO; using FileRepo.Droid; using Xamarin.Forms; [assembly: Dependency (typeof (FileImplementation))] namespace FileRepo.Droid { public class FileImplementation : IFile { public void SaveText (string filename, string text) { var documentsPath = Environment.GetFolderPath (Environment.SpecialFolder.Personal); var filePath = Path.Combine (documentsPath, filename); File.Delete (filePath); File.WriteAllText (filePath, text); } public string LoadText (string filename) { var documentsPath = Environment.GetFolderPath (Environment.SpecialFolder.Personal); var filePath = Path.Combine (documentsPath, filename); return System.IO.File.ReadAllText (filePath); } public void ClearFile (string filename) { var documentsPath = Environment.GetFolderPath (Environment.SpecialFolder.Personal); var filePath = Path.Combine (documentsPath, filename); File.Delete (filePath); } public bool FileExists (string filename) { var documentsPath = Environment.GetFolderPath (Environment.SpecialFolder.Personal); var filePath = Path.Combine (documentsPath, filename); return File.Exists (filePath); } } }
That is pretty much boiler-plate code that you can use every time you create a file repo.
IEntity
Back in the PCL we’ll create an interface that will be implemented by every type that is stored in the repo. It couldn’t be much simpler,
public interface IEntity
{
int Id { get; set; }
}
We are now ready to create the GenericFileRepository which we can use to store any class implementing IEntity.
Generic File Repository
The job of the GenericFileRepository is to provide CRUD operations. We begin by declaring the Repository to hold IEntity objects and the constructor takes the name of the file we’ll be storing to.
public class GenericFileRepository<T> where T : IEntity
{
private string fileName;
public GenericFileRepository (string fileName)
{
this.fileName = fileName;
}
Next we implement the CRUD operations. Let’s start with Get. This comes in two flavors, Get by Id and Get all the Entities,
public T Get (int id)
{
var items = LoadEntities ();
return items.FirstOrDefault (i => i.Id == id);
}
public IEnumerable<T> GetAll ()
{
return LoadEntities ();
}
Notice that both depend on a method LoadEntities which looks like this:
private IEnumerable<T> LoadEntities ()
{
var savedJson = DependencyService.Get<IFile> ().LoadText (fileName);
var deserializedTrayItems = JsonConvert.DeserializeObject<IEnumerable<T>> (savedJson);
return deserializedTrayItems;
}
This takes advantage of the wonderful Newtonsoft.json library which you can get from NuGet
The next methods save, with a single entity or a collection,
public void Save (T entity)
{
List<T> items;
if (DependencyService.Get<IFile> ().FileExists (fileName)) {
items = LoadEntities ().ToList ();
var item = items.FirstOrDefault (i => i.Id == entity.Id);
if (item != null) {
items.Remove (item);
}
} else {
items = new List<T> ();
}
items.Add (entity);
StoreEntities (items);
}
public void Save (IEnumerable<T> entities)
{
StoreEntities (entities);
}
Notice that in the case of a saving a single entity we optimize by locating the entity in the collection and if it is there removing just that one and then adding the new one; This amounts to an update. In the case of the entire collection, we blow away the old collection and replace it with the new.
Both of these operations depend on StoreEntities:
private void StoreEntities (IEnumerable<T> entities)
{
var serializedEntities = JsonConvert.SerializeObject (entities);
DependencyService.Get<IFile> ().SaveText (fileName, serializedEntities);
}
Delete
We round out our repo with the Delete operation,
public void Delete (int id)
{
var items = LoadEntities ().ToList ();
var item = items.FirstOrDefault (i => i.Id == id);
if (item != null) {
items.Remove (item);
}
StoreEntities (items);
}
Person Repository
With the Generic File Repository defined, we can specialize for any type we like. In this demo, we’ll create a Person type:
public class Person : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
Notice that Person implements IEntity and thus is a candidate for storage through the Repo. To facilitate this, we’ll specialize GenericFileRepository with PersonRepository,
public class PersonRepository : GenericFileRepository<Person>
{
public PersonRepository () : base ("PersonFile.json") { }
}
It is PersonRepository that sets the file name for storage.
The Demo
That’s it, we’re ready to rock and roll. All we need now is to create a program that uses this repository. As this is a blog post, and not a complete project, I’ll create a very simple demo that requests names, and then displays them in a list. The key feature, however, is that we’ll store those names to the file, and then restore them in the event of a crash.
The first page just gets the names and has a button to go to the second page. Here’s the XAML:
<?xml version="1.0" encoding="utf-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:FileRepo"
x:Class="FileRepo.FileRepoPage">
<StackLayout>
<StackLayout
Orientation="Horizontal">
<Label
Text="Name: " />
<Entry
x:Name="PersonName"
WidthRequest="150" />
</StackLayout>
<Button
Text="Add"
Clicked="OnAdd" />
<Button
Text="Next Page"
Clicked="OnNextPage" />
</StackLayout>
</ContentPage>
To keep things simple, I put the logic in the code-behind rather than in a viewModel (which is where it belongs). Here’s the code-behind…
public partial class FileRepoPage : ContentPage
{
private List<Person> people = new List<Person> ();
private static int ids = 0;
public FileRepoPage ()
{
InitializeComponent ();
}
public void OnAdd (object o, EventArgs e)
{
var person = new Person ();
person.Id = ids++;
person.Name = PersonName.Text;
PersonName.Text = string.Empty;
people.Add (person);
}
public void OnNextPage (object o, EventArgs e)
{
Navigation.PushAsync (new PeopleListPage (people), true);
}
}
Nothing special here. We just handle the events, and when OnNextPage is called, we invoke the second page, passing in the collection of people we’ve accumulated. Here is PeopleListPage’s XAML:
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="FileRepo.PeopleListPage">
<ContentPage.Content>
<StackLayout>
<Label
Text="People..." />
<ListView
ItemsSource="{Binding People}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ContentView>
<StackLayout Orientation="Horizontal">
<Label
Text="Name: " />
<Label
Text="{Binding Name}" />
</StackLayout>
</ContentView>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button
Text="Store"
Clicked="OnStore" />
<Button
Text="Restore"
Clicked="OnRestore" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
It has a ListView whose ItemsSource is bound to an observableCollection in the code behind. It also has two buttons: one stores the list to a file, the second restores the list from the file. Here is the complete listing for the code behind. Pay particular attention to the event handlers…
public partial class PeopleListPage : ContentPage
{
public ObservableCollection<Person> People { get; set; } = new ObservableCollection<Person> ();
public PeopleListPage (List<Person> people)
{
foreach (Person person in people) {
People.Add (person);
}
InitializeComponent ();
this.BindingContext = this;
}
public void OnStore (object o, EventArgs e)
{
var repo = new PersonRepository ();
repo.Save (People);
}
public void OnRestore (object o, EventArgs e)
{
var repo = new PersonRepository ();
var people = repo.GetAll ();
foreach (Person person in people) {
People.Add (person);
}
}
}
The storage and retrieval code is trivial because we are using the repository. What’s more, this code knows nothing of how the repo implements Save and Get. It may be that the data is saved to a file, or to a database, or to the cloud. There is a very nice separation of concerns.
[Special thanks to Eric Grover]