File Persistence in Xamarin.Forms Apps

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]

About Jesse Liberty

Jesse Liberty is an independent consultant and programmer with three decades of experience writing and delivering software projects. He is the author of 2 dozen books and multiple Pluralsight courses, and has been 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 Xamarin Certified Mobile Developer and a Xamarin MVP, Microsoft MVP and Telerik MVP.
This entry was posted in Essentials, Patterns & Skills, Programming, Xamarin. Bookmark the permalink.

Leave a Reply

Your email address will not be published.