Azure Mobile Services with Offline Sync to SQLite using Xamarin Forms

This post:

http://blog.alectucker.com/post/2014/05/02/cross-platform-azure-mobile-services-with-xamarin-and-mvvmcross.aspx

detailed how to set up and use Azure Mobile Services from a portable class library (PCL) in a Xamarin project with the help of MvvmCross. Since I wrote that two major updates have hit the scene. The first is Xamarin Forms, which allows us to write cross platform xaml and bring the UI into the shared code while still rendering natively on each platform and, critically, still allowing or platform specific customisations. The second is the addition of offline sync capability to Azure Mobile Services using SQLite.

This post covers both. Note that version 1.3 of the Azure Mobile Services components is still pre-release. If you're looking to use Xamarin Forms but want to stick with the current stable release then you can still work through this post - I'll highlight where the differences are. You just won't get the offline sync ability. (The code is almost identical.)

As was the case with that previous post, I'm not attempting to walk through how to create the Azure Mobile Service. If you need a hand with this there's a link in the other post.

The last difference from the previous post is that this is now all being done without the help of MvvmCross.

So, first let's create a Xamarin Forms app. We'll use a PCL again.

Azure1

Those already familiar with Xamarin Forms PCL solutions will recognise the structure generated:

Azure2

First let's add the references we need. Right click on the solution node and select 'Manage NuGet Packages for Solution' - this way we can do this once and have the correct references added to all constituent projects.

Azure3

To get the offline sync capability you'll need to change the dropdown at the top from 'Stable' to 'Include Prerelease' and ensure that both the select packages are added. If you're sticking with the 'Stable' release then leave the dropdown list on 'Stable' and you'll only have the option of installing the 'Windows Azure Mobile Services' package.

At present (and indeed for quite some time now) the NuGet packages for the Xamarin.iOS and Xamarin.Android projects require a small tidy up before they'll work. If you build the projects now you'll get some duplicate reference errors, complaining about:

  • System.IO
  • System.Runtime
  • System.Threading.Tasks

You'll need to manually remove these from these two projects in order to get them to compile.

Next we have to add some initialisation code to prevent the references we've just added from being stripped out at compile time. This is the only bit of platform specific code which I'm putting in this example.

In the iOS project, an AppDelegate.cs you'll need to add two lines to FinishedLaunching, after the Forms.Init() line:

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    Forms.Init();
    Microsoft.WindowsAzure.MobileServices.CurrentPlatform.Init();
    SQLitePCL.CurrentPlatform.Init();
 
    window = new UIWindow(UIScreen.MainScreen.Bounds);
...

These two lines perform the necessary initialisation for Azure Mobile Sevices and SQLite respectively.

In the Android project we'll add the line to initialise azure Mobile Services into MainActivity.cs:

protected override void OnCreate(Bundle bundle)
{
    base.OnCreate(bundle);
 
    Xamarin.Forms.Forms.Init(this, bundle);
    Microsoft.WindowsAzure.MobileServices.CurrentPlatform.Init();
 
    SetPage(App.GetMainPage());
}

To get all of this to work on Windows Phone 8 you'll need to install SQLite for Windows Phone. In Visual Studio select Extensions and Updates from the Tools menu, find "SQLite for Windows Phone" and install it.

Next we'll set up folders for using MVVM, so add the following folders to the Portable project:

  • Services
  • Models
  • ViewModels
  • Views

That's right - the Views folder now sits in the PCL, thanks to Xamarin Forms. Inside the Models folder create a Contact class as follows:

namespace AzureOfflineSyncXF.Models
{
    public class Contact
    {
        public string Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public string PhoneNumber { get; set; }
        public string ImageUrl { get; set; }
        public string Version { get; set; }
    }
}

This should look familiar if you went through my previous post. The one change to highlight is the addition of the Version property. This is required by the sync handler.

The next thing to do is add a DataService class in the Services folder. In there we'll need to define the mobile service like this:

private readonly MobileServiceClient MobileService = new MobileServiceClient("https://[yourservice].azure-mobile.net/", "[yourkey]");

Next we'll add a generic GetAll method which will just return the entire contents of the Contacts table.

public async Task<ObservableCollection<T>> GetAll<T>()
{
    ObservableCollection<T> theCollection = new ObservableCollection<T>();

    try
    {
        //var theTable = MobileService.GetTable<T>();
        var theTable = MobileService.GetSyncTable<T>();
        theCollection = await theTable.ToCollectionAsync<T>();
    }
    catch (Exception ex)
    {
        theCollection = null;
        ReportError(ex);
    }
 
    return theCollection;
}

We'll also put in a generic Save method.

public async Task Save<T>(T pEntity)
{
    try
    {
        //var theTable = MobileService.GetTable<T>();
        var theTable = MobileService.GetSyncTable<T>();
        await theTable.InsertAsync(pEntity);
    }
    catch (Exception ex)
    {
        ReportError(ex);
    }
}

Note the commented out line calling MobileService.GetTable() in both these methods. This is what you will need to use if you're using a version of the Azure Mobile Services components which doesn't have offline sync capability (i.e. pre 1.3). The line beneath uses GetSyncTable() instead.

Next we'll add a generic Pull method (you won't need this if you're using < version 1.3).

public async Task Pull<T>()
{
    try
    {
        var theTable = MobileService.GetSyncTable<T>();
        await theTable.PullAsync();
    }
    catch(Exception ex)
    {
        ReportError(ex);
    }
}

As an example of using this we'll put code into the GetAll method to pull the data down from the mobile service into the local DB before reading it from the local DB and returning it. In practice you'd probably want to get a bit cleverer than this. Add this line to the GetAll method:

await Pull<AzureOfflineSyncXF.Models.Contact>();

Now we need to do some initialisation in the constructor of the DataService class.

public DataService()
{
    Initialise();
}

private async void Initialise()
{
    if (!MobileService.SyncContext.IsInitialized)
    {
        var store = new MobileServiceSQLiteStore("local.db");
        store.DefineTable<AMS.Offline.XF.Models.Contact>();
        await MobileService.SyncContext.InitializeAsync(store, new MobileServiceSyncHandler());
    }
}

You'll need the following using statements to resolve this code:

using Microsoft.WindowsAzure.MobileServices;

using Microsoft.WindowsAzure.MobileServices.SQLiteStore;
using Microsoft.WindowsAzure.MobileServices.Sync;

The last thing to do in here is add a method which will initially populate the Azure table for us.

public async Task LoadContacts()
{
    Collection<Contact> theList = new Collection<Contact>();
 
    theList.Add(new Contact() { FirstName = "George", LastName = "Washington", Email = "George@fake.com", PhoneNumber = "02 555 1111" });
    theList.Add(new Contact() { FirstName = "John", LastName = "Adams", Email = "John@fake.com", PhoneNumber = "02 555 1112" });
    theList.Add(new Contact() { FirstName = "Thomas", LastName = "Jefferson", Email = "Thomas@fake.com", PhoneNumber = "02 555 1113" });
    theList.Add(new Contact() { FirstName = "James", LastName = "Madison", Email = "James@fake.com", PhoneNumber = "02 555 1114" });
    theList.Add(new Contact() { FirstName = "James", LastName = "Monroe", Email = "James@fake.com", PhoneNumber = "02 555 1115" });
    theList.Add(new Contact() { FirstName = "John Quincy", LastName = "Adams", Email = "John@fake.com", PhoneNumber = "02 555 1116" });
    theList.Add(new Contact() { FirstName = "Andrew", LastName = "Jackson", Email = "Andrew@fake.com", PhoneNumber = "02 555 1117" });
    theList.Add(new Contact() { FirstName = "Martin", LastName = "van Buren", Email = "Martin@fake.com", PhoneNumber = "02 555 1118" });
    theList.Add(new Contact() { FirstName = "William Henry", LastName = "Harrison", Email = "William@fake.com", PhoneNumber = "02 555 1119" });
    theList.Add(new Contact() { FirstName = "John", LastName = "Tyler", Email = "John@fake.com", PhoneNumber = "02 555 1120" });
    theList.Add(new Contact() { FirstName = "James K.", LastName = "Polk", Email = "James@fake.com", PhoneNumber = "02 555 1121" });
    theList.Add(new Contact() { FirstName = "Zachary", LastName = "Taylor", Email = "Zachary@fake.com", PhoneNumber = "02 555 1122" });
    theList.Add(new Contact() { FirstName = "Millard", LastName = "Filmore", Email = "Millard@fake.com", PhoneNumber = "02 555 1123" });
    theList.Add(new Contact() { FirstName = "Franklin", LastName = "Pierce", Email = "Franklin@fake.com", PhoneNumber = "02 555 1124" });
    theList.Add(new Contact() { FirstName = "James", LastName = "Buchanan", Email = "James@fake.com", PhoneNumber = "02 555 1125" });
    theList.Add(new Contact() { FirstName = "Abraham", LastName = "Lincoln", Email = "Abraham@fake.com", PhoneNumber = "02 555 1126" });
    theList.Add(new Contact() { FirstName = "Andrew", LastName = "Johnson", Email = "Andrew@fake.com", PhoneNumber = "02 555 1127" });
    theList.Add(new Contact() { FirstName = "Ulysees S.", LastName = "Grant", Email = "Ulysees@fake.com", PhoneNumber = "02 555 1128" });
    theList.Add(new Contact() { FirstName = "Rutherford B.", LastName = "Hayes", Email = "Rutherford@fake.com", PhoneNumber = "02 555 1129" });
    theList.Add(new Contact() { FirstName = "James A.", LastName = "Garield", Email = "James@fake.com", PhoneNumber = "02 555 1130" });
    theList.Add(new Contact() { FirstName = "Chester A.", LastName = "Arthur", Email = "Chester@fake.com", PhoneNumber = "02 555 1131" });
    theList.Add(new Contact() { FirstName = "Grover", LastName = "Cleveland", Email = "Grover@fake.com", PhoneNumber = "02 555 1132" });
    theList.Add(new Contact() { FirstName = "Benjamin", LastName = "Harrison", Email = "Benjamin@fake.com", PhoneNumber = "02 555 1133" });
    theList.Add(new Contact() { FirstName = "Grover", LastName = "Cleveland", Email = "Grover@fake.com", PhoneNumber = "02 555 1134" });
    theList.Add(new Contact() { FirstName = "William", LastName = "McKinley", Email = "William@fake.com", PhoneNumber = "02 555 1135" });
    theList.Add(new Contact() { FirstName = "Theodore", LastName = "Roosevelt", Email = "Theodore@fake.com", PhoneNumber = "02 555 1136" });
    theList.Add(new Contact() { FirstName = "William Howard", LastName = "Taft", Email = "William@fake.com", PhoneNumber = "02 555 1137" });
    theList.Add(new Contact() { FirstName = "Woodrow", LastName = "Wilson", Email = "Woodrow@fake.com", PhoneNumber = "02 555 1138" });
    theList.Add(new Contact() { FirstName = "Warren G.", LastName = "Harding", Email = "Warren@fake.com", PhoneNumber = "02 555 1139" });
    theList.Add(new Contact() { FirstName = "Calvin", LastName = "Coolidge", Email = "Calvin@fake.com", PhoneNumber = "02 555 1140" });
    theList.Add(new Contact() { FirstName = "Herbert", LastName = "Hoover", Email = "Herbert@fake.com", PhoneNumber = "02 555 1141" });
    theList.Add(new Contact() { FirstName = "Franklin D.", LastName = "Roosevelt", Email = "Franklin@fake.com", PhoneNumber = "02 555 1142" });
    theList.Add(new Contact() { FirstName = "Calvin", LastName = "Coolidge", Email = "Calvin@fake.com", PhoneNumber = "02 555 1143" });
    theList.Add(new Contact() { FirstName = "Harry S.", LastName = "Truman", Email = "Harry@fake.com", PhoneNumber = "02 555 1144" });
    theList.Add(new Contact() { FirstName = "Dwight D.", LastName = "Eisenhower", Email = "Dwight@fake.com", PhoneNumber = "02 555 1145" });
    theList.Add(new Contact() { FirstName = "John F.", LastName = "Kennedy", Email = "John@fake.com", PhoneNumber = "02 555 1146" });
    theList.Add(new Contact() { FirstName = "Lyndon B.", LastName = "Johnson", Email = "Lyndon@fake.com", PhoneNumber = "02 555 1147" });
    theList.Add(new Contact() { FirstName = "Richard", LastName = "Nixon", Email = "Richard@fake.com", PhoneNumber = "02 555 1148" });
    theList.Add(new Contact() { FirstName = "Gerald", LastName = "Ford", Email = "Gerald@fake.com", PhoneNumber = "02 555 1149" });
    theList.Add(new Contact() { FirstName = "Jimmy", LastName = "Carter", Email = "Jimmy@fake.com", PhoneNumber = "02 555 1150" });
    theList.Add(new Contact() { FirstName = "Ronald", LastName = "Reagan", Email = "Ronald@fake.com", PhoneNumber = "02 555 1151" });
    theList.Add(new Contact() { FirstName = "George H.W.", LastName = "Bush", Email = "George@fake.com", PhoneNumber = "02 555 1152" });
    theList.Add(new Contact() { FirstName = "Bill", LastName = "Clinton", Email = "Bill@fake.com", PhoneNumber = "02 555 1153" });
    theList.Add(new Contact() { FirstName = "George W.", LastName = "Bush", Email = "George@fake.com", PhoneNumber = "02 555 1154" });
    theList.Add(new Contact() { FirstName = "Barack", LastName = "Obama", Email = "Barack@fake.com", PhoneNumber = "02 555 1155" });
 
    foreach(Contact one in theList)
    {
        await this.Save<Contact>(one);
    }
}

Now that the DataService has been written let's extract an interface by right clicking on 'DataService' in the class declaration and selecting 'Refactor', then 'Extract Interface'.

Azure4

Make the resulting interface public, and save everything.

namespace AzureOfflineSyncXF.Services
{
    public interface IDataService
    {        System.Threading.Tasks.Task<System.Collections.ObjectModel.ObservableCollection<T>> GetAll<T>();
        System.Threading.Tasks.Task Pull<T>();
        System.Threading.Tasks.Task Save<T>(T pEntity);
    }
}

The last thing we'll do in the Services folder is to add a simple ServiceLocator class like this:

namespace AzureOfflineSyncXF.Services
{
    public static class ServiceLocator
    {
        public static IDataService DataService { get; set; }
    }
}

Next, add a similar ViewModelLocator into the ViewModels folder.

namespace AzureOfflineSyncXF.ViewModels
{
    public static class ViewModelLocator
    {
        public static  ContactListViewModel ContactListViewModel { get; set; }
    }
}

Now we can move on to the viewmodel. First, add a BaseViewModel class to the ViewModels folder.

namespace AzureOfflineSyncXF.ViewModels
{

    public class BaseViewModel : INotifyPropertyChanged
    {
        #region Property change notification stuff
        public event PropertyChangedEventHandler PropertyChanged;
        protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
        {
            RaisePropertyChangedExplicit(propertyName);
        }
        protected void RaisePropertyChanged<TProperty>(Expression<Func<TProperty>> projection)
        {
            var memberExpression = (MemberExpression)projection.Body;
            RaisePropertyChangedExplicit(memberExpression.Member.Name);
        }
        void RaisePropertyChangedExplicit(string propertyName)
        {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
            {
                var e = new PropertyChangedEventArgs(propertyName);
                handler(this, e);
            }
        }
        #endregion
    }
}

You'll see I like to use regions (yes - I'm the one!), but sparingly. Now we can add the ContactListViewModel to the ViewModels folder.

namespace AzureOfflineSyncXF.ViewModels
{
    public class ContactListViewModel : BaseViewModel
    {
        private readonly IDataService _dataService;
 
        public ContactListViewModel()
        {
            _dataService = ServiceLocator.DataService;
            _constructor();
        }
        public ContactListViewModel(IDataService pDataService)
        {
            // Shown to demo injection support
            _dataService = pDataService;
            _constructor();
        }
        private async void _constructor()
        {
            this.Contacts = await LoadContacts();
        }
 
        private ObservableCollection<Contact> _contacts;
        public ObservableCollection<Contact> Contacts
        {
            get { return _contacts; }
            set { if (_contacts != value) { _contacts = value; RaisePropertyChanged(() => Contacts); } }
        }
 
        private async Task<ObservableCollection<Contact>> LoadContacts()
        {
            ObservableCollection<Contact> theCollection = new ObservableCollection<Contact>();
 
            try
            {
                theCollection = await _dataService.GetAll<Contact>();
            }
            catch(Exception ex)
            {
                theCollection = null;
 
                // Do something with the exception
            }
 
            return theCollection;
        }
    }
}

I've put in a parameterless constructor, which is what we'll be using. This will make use of the static ServiceLocator class to find the data service. However, I've also added a constructor which takes the service as a parameter, in case I get a bit cleverer one day and set up some dependency injection. Other than this it's a very straightforward viewmodel - there's one ObservableCollection property which we'll bind to a list on screen, and there's a method to populate it by making an asynchronous call to the data service.

Now for the Xamarin Forms magic. Add a Forms Xaml Page to the Views folder and call it ContactsView.

Azure5

This will give you ContactsView.xaml and ContactsView.xaml.cs, just like you'd expect if you've done Windows Phone dev (or WPF or Silverlight).

Open up the xaml and change it to look like this:

<?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="AzureOfflineSyncXF.Views.ContactListView">
 
  <StackLayout>
    <Label Text="contacts" />

    <ListView ItemsSource="{Binding Contacts}" x:Name="listCategories">
      <ListView.ItemTemplate>
        <DataTemplate>
          <TextCell Text="{Binding FirstName}" Detail="{Binding LastName}" />
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </StackLayout>
 
</ContentPage>

This binds the list to the Contacts property in the view model, and defines a template to use for each cell in the list with bindings on two of the contact's properties.

Now we need to put some code behind this. Remember we're not using MvvmCross this time around (or any other Mvvm framework) so we need to wire up the viewmodel manually.

namespace AzureOfflineSyncXF.Views
{
    public partial class ContactListView
    {
        public ContactListView()
        {
            InitializeComponent();
            this.BindingContext = ViewModelLocator.ContactListViewModel;
        }
    }
}

That's the view defined. Now to App.cs to indicate that this view is to be displayed on startup.

namespace AzureOfflineSyncXF
{
    public class App
    {
        public static Page GetMainPage()
        {
            // Initial, once off data load
            LoadData();
 
            NavigationPage theNavigationPage = null;
            ContentPage theContentPage = null;
 
            try
            {
                theContentPage = new ContactListView();
                theNavigationPage = new NavigationPage(theContentPage);
            }
            catch(Exception ex)
            {
                // Do something with the exception
            }
 
            return theNavigationPage;
        }
 
        private static async void LoadData()
        {
            await ServiceLocator.DataService.LoadContacts();
        }
    }
}

There's one last gotcha in the Windows Phone project. This may well be fixed by the time you read this, but I'm currently getting the following exception when building:

The 'ProductID' attribute is invalid

This looks like a bug in the Xamarin Forms project template for Windows Phone. It's caused by the WMAppManifest.xml file. You'll need to open this and fix it up. Double click that file in Visual Studio and you'll get this:

Azure6

This is caused by the same problem. Click the link to open it up in the XML editor, and find this line:

  <App xmlns="" ProductID="aaaaaaaa-1111-aaaa-1111-aaaa1111aaaa" Title="AzureOfflineSyncXF.WinPhone" RuntimeType="Silverlight" Version="1.0.0.0" Genre="apps.normal"  Author="AzureOfflineSyncXF.WinPhone author" Description="Sample description" Publisher="AzureOfflineSyncXF.WinPhone" PublisherID="bbbbbbbb-2222-bbbb-2222-bbbb2222bbbb">

The ProductID and the PublisherID should both contain strings between {curly parentheses}, so change it to look like this:

  <App xmlns="" ProductID="{aaaaaaaa-1111-aaaa-1111-aaaa1111aaaa}" Title="AzureOfflineSyncXF.WinPhone" RuntimeType="Silverlight" Version="1.0.0.0" Genre="apps.normal"  Author="AzureOfflineSyncXF.WinPhone author" Description="Sample description" Publisher="AzureOfflineSyncXF.WinPhone" PublisherID="{bbbbbbbb-2222-bbbb-2222-bbbb2222bbbb}">

Save and close the file. You should now be able to double click it to open it, and you should be able to build and run successfully.

Further reading:

For details on how to handle sync conflicts take a look here:

http://azure.microsoft.com/en-us/documentation/articles/mobile-services-windows-store-dotnet-handling-conflicts-offline-data/

This page is also worth a look, and it has some useful links too:

http://msopentech.com/blog/2014/06/09/azure-mobile-services-offline-adds-xamarin-support-using-sqlitepcl/

Feedback:

Please feel free to comment / criticise / question. I'll commit to deleting anything which isn't complimentary :)