30 Days of Zumo.v2 (Azure Mobile Apps): Day 16 – Offline Sync .NET Edition

I introduced the concept of offline sync and discussed some of the key concepts and terminology of offline sync in my last article. However, implementation was a little lacking. I’m going to fix that today. Today, I’m going to adjust my Universal Windows Task List to use Offline Sync. You can get the starting point for this application here. You will note that I have both a backend and a frontend in the repository – they go together. You will need to configure an AAD web application to use them – you can find the instructions in an earlier post – and adjust the Client.UWP project for your backend. Once done, you should be able to deploy the backend and use the Client.UWP project to connect to your backend. Make sure you can do this before continuing. If you have problems, here are some of the blog posts you should cover again to see what is going on:

Preparing the Project

Azure Mobile Apps uses SQLite to store data on the client device. In order to enable Offline Sync, I need to add SQLite to my Visual Studio installation, then add the appropriate libraries to the project. The SQLite libraries themselves are available through Tools > Extensions and Updates. Just search for SQLite online:

day-16-p1

I an doing a UWP app, so SQLite for Universal Windows Platform is appropriate. I had to restart Visual Studio after installing the extension. Once that is done, I can go on to the project itself. Right-click on the Client.UWP project and select Manage NuGet Packages…, click on Browse and then enter “Azure Mobile” in the search box:

day-16-p2

Add the Microsoft.Azure.Mobile.Client.SQLiteStore v2.0.1 or later to your project. Note that the version should match the version of the Microsoft.Azure.Mobile.Client you are using. They are released in tandem, so update or install them together. Finally, add a reference to SQLite into your project. This is done by right-clicking on the References… node and selecting Add Reference…. Expand the Universal Windows node and click on Extensions. SQLite for Universal Windows Platform will be in the list:

day-16-p3

Check the box next to SQLite for Universal Windows Platform and then click on OK.

Initialize the Offline Sync Store

There is a SQLite store on the client device. You need to set it up. My code is in Services/AzureCloudProvider.cs and here is how I did it:

        /// <summary>
        /// Initialize the Offline Sync Store
        /// </summary>
        /// <returns>async task</returns>
        public async Task InitializeOfflineSync()
        {
            var store = new MobileServiceSQLiteStore("localstore.db");

            store.DefineTable<TodoItem>();
            syncTables.Add(typeof(TodoItem).Name);

            await client.SyncContext.InitializeAsync(store);
        }

You can call your store whatever you want. It’s a local database store and there is one per app. You define the tables you want to have offline sync, and then you run the database initialization code. If the store is not defined, it will create the tables for you. If you have more than one table that you need to be offline-enabled then just add more DefineTable calls. I put this into an extra method in the AzureCloudProvider class so that I can await on it.

It’s important to note here that not every table needs to be offline-enabled. Let’s say you have a table that is changed frequently, but referenced infrequently. You may consider that the overhead of maintaining the offline sync store – most notably storage, but also network bandwidth – isn’t worth it. You may just want a table that you can query for values as needed. The process I’m layout out here puts you in control – you get to decide (as the developer) which tables are available for offline sync and which ones you have to be online to access. I add my sync tables as they are set up to a list that can be queried by my table provider.

Where you add the local offline sync cache initialization is up to you. I like to put it in the OnLaunched method of the App.xaml.cs file:

        protected override void OnLaunched(LaunchActivatedEventArgs e)
        {
            Frame rootFrame = Window.Current.Content as Frame;

            // Do not repeat app initialization when the Window already has content,
            // just ensure that the window is active
            if (rootFrame == null)
            {
                rootFrame = new Frame();
                rootFrame.NavigationFailed += OnNavigationFailed;

                if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
                {
                    //TODO: Load state from previously suspended application
                }

                Window.Current.Content = rootFrame;
            }

            // Initialize offline-sync and wait for it to complete
            Debug.WriteLine($"[App.xaml.cs#OnLaunched] Initializing offline sync");
            AzureCloudProvider.Instance.InitializeOfflineSync();
            Debug.WriteLine($"[App.xaml.cs#OnLaunched] Finished initializing offline sync");

            if (e.PrelaunchActivated == false)
            {
                if (rootFrame.Content == null)
                {
                    rootFrame.Navigate(typeof(EntryPage), e.Arguments);
                }
                Window.Current.Activate();
            }
        }

However, there are other places to put it. You should definitely call it well in advance of creating a connection to the table as there is an initialization process and it takes time.

Let’s stop there for a moment and take a look at the SyncContext in the debugger:

day-16-p4

Note that there is an IsInitialized property on the SyncContext that shows if the SQLite store is properly initialized. Ideally, I’d use that to ensure that initialization is done prior to creating a sync table. Talking of which, let’s take a look at the AzureDataTable class to show off how to create a sync table:

    public class AzureDataTable<T> where T: EntityData
    {
        private AzureCloudProvider provider;
        private bool isSyncTable = false;

        private IMobileServiceSyncTable<T> syncTable;
        private IMobileServiceTable<T> dataTable;

        private ObservableCollection<T> dataView;


        public AzureDataTable(AzureCloudProvider provider)
        {
            this.provider = provider;
            this.isSyncTable = provider.IsSyncTable(typeof(T).Name);

            if (isSyncTable)
            {
                this.syncTable = provider.Client.GetSyncTable<T>();
            }
            else
            {
                this.dataTable = provider.Client.GetTable<T>();
            }
        }

Probably not the most efficient code. I have one variable for a sync table and the other for a regular table. The point is that I can use the same basic class for both offline sync and online tables with the same API. Taking a look at my new RefreshAsync() method:

        public async Task<ObservableCollection<T>> RefreshAsync(bool syncItems = true)
        {
            Debug.WriteLine($"[AzureDataTable$RefreshAsync] Entry");
            try
            {
                if (syncItems && isSyncTable)
                {
                    Debug.WriteLine($"[AzureDataTable$RefreshAsync] Updating Offline Sync Cache");
                    await this.SyncOfflineCacheAsync();
                }
                Debug.WriteLine($"[AzureDataTable$RefreshAsync] Requesting Items");
                if (isSyncTable)
                {
                    List<T> items = await syncTable.OrderBy(item => item.UpdatedAt).ToListAsync();
                    dataView = new ObservableCollection<T>(items);
                }
                else
                {
                    List<T> items = await dataTable.OrderBy(item => item.UpdatedAt).ToListAsync();
                    dataView = new ObservableCollection<T>(items);
                }
                Debug.WriteLine($"[AzureDataTable$RefreshAsync] {dataView.Count} items available");
                return dataView;
            }
            catch (MobileServiceInvalidOperationException exception)
            {
                throw new CloudTableOperationFailed(exception.Message, exception);
            }
        }

The only addition here is the fact that I want to synchronize the offline cache if I am synchronizing. I’ve made this optional and I’ll bind that to the “network is available” indicator. Let’s take a look at the SaveAsync() and DeleteAsync() methods:

        public async Task SaveAsync(T item)
        {
            try
            {
                if (item.Id == null)
                {
                    if (isSyncTable)
                        await syncTable.InsertAsync(item);
                    else
                        await dataTable.InsertAsync(item);
                    dataView.Add(item);
                }
                else
                {
                    if (isSyncTable)
                        await syncTable.UpdateAsync(item);
                    else
                        await dataTable.UpdateAsync(item);
                    dataView.Remove(item);
                    dataView.Add(item);
                }
            }
            catch (MobileServiceInvalidOperationException exception)
            {
                throw new CloudTableOperationFailed(exception.Message, exception);
            }
        }

        public async Task DeleteAsync(T item)
        {
            try
            {
                if (isSyncTable)
                    await syncTable.DeleteAsync(item);
                else
                    await dataTable.DeleteAsync(item);
                dataView.Remove(item);
            }
            catch (MobileServiceInvalidOperationException exception)
            {
                throw new CloudTableOperationFailed(exception.Message, exception);
            }
        }

Note that the only difference between the offline-sync version and the online version is the type of table I am using. The actual API is identical. Now, back to the RefreshAsync() method. I introduced a new method call there – SyncOfflineCacheAsync(). This method does the synchronization between the cloud and the offline sync store. From my prior post, I have to do a Push and then a Pull, handling conflicts during the Push section. The Client SDK does the rest. Here is the code:

        public async Task SyncOfflineCacheAsync()
        {
            string queryName = $"incremental_sync_{typeof(T).Name}";
            try
            {
                await provider.Client.SyncContext.PushAsync();
                await syncTable.PullAsync(queryName, syncTable.CreateQuery());
            }
            catch (MobileServicePushFailedException exception)
            {
                if (exception.PushResult != null)
                {
                    foreach (var error in exception.PushResult.Errors)
                    {
                        await ResolveConflictAsync(error);
                    }
                }
            }
        }

        private async Task ResolveConflictAsync(MobileServiceTableOperationError error)
        {
            Debug.WriteLine($"Resolve Conflict for Item: {error.Item}");

            var serverItem = error.Result.ToObject<T>();
            var localItem = error.Item.ToObject<T>();

            if (serverItem.Equals(localItem))
            {
                // Items are the same, so ignore the conflict
                await error.CancelAndDiscardItemAsync();
            }
            else
            {
                // Always take the client
                localItem.Version = serverItem.Version;
                await error.UpdateOperationAsync(JObject.FromObject(localItem));
            }
        }

In this case, I’m doing an implicit conflict resolution. I’m always accepting that the local item is correct and overwriting the server item. You could just as easily put a UI here that allows the end-user to select which one to use. You could also see which UpdateAt field is later and take that. There are lots of options for conflict resolution.

Next Steps

Offline Sync is available across all the Client SDKs, so if you have an iOS Native or Android Native mobile client, you’ll be able to take the same concepts. In the next post, I’m going to switch my attention back to the backend and look at the ASP.NET backend – why would you use it and why would you not use it.

In the mean time, find the code for the server and client on my GitHub Repository.

One thought

  1. Pingback: Dew Drop – May 5, 2015 (#2244) | Morning Dew

Comments are closed.