/// <summary> /// Loads the TodoItems property asynchronously because properties don't support async /// </summary> /// <returns>Returns all Todo items not marked as Done</returns> /// <remarks> /// We use a method-based fetcher to fill the collection with data in the mobile app /// table because c# properties can't be async. This method is typically called in the /// Appearing (Xamarin.Forms) or Loaded (Windows Xaml) methods of the View that is going /// to show the collection. /// /// Notice, for performace reasons, the collection is only fetched once for the lifespan /// of the application which means that you must keep the collection up to date as items /// are added, removed or updated in the backing mobile service table. In other words, /// when you add, remove, or update the backing mobile service table you must do the /// same thing the collection fetched by this method. /// </remarks> async public Task GetTodoItems() { if (_TodoItems == null) { _TodoItems = new ObservableCollection <TodoItemViewModel>(); foreach (TodoItem todoItem in await TodoItemTable.Where(todoItem => !todoItem.Done).ToListAsync()) { _TodoItems.Add(new TodoItemViewModel() { TodoItem = todoItem }); } } }
// ToDo: Turn this into a generic method on the order of: // Task SyncViewModel<T, U>(string queryId, IMobileServiceSyncTable<T> table, IMobileServiceTableQuery<U> query) async public Task SyncTodoItemsViewModel() { // Update the local database from the server. Note: For sync tables, the SDK saves // an updatedAt timestamp with each queryId ('ACTIVE_ITEMS' in this case) and uses // that timestamp to pull only new records from the server (i.e. records who's // UpdateAt >= updatedAt assocated with queryId). So this PullAsync() only pulls // newly added or updated records. Note that we aren't using a filtering query // (we passed a null instead) otherwise we would miss items that were updated by // another user if that update caused it to match the filter. In other words, we // don't want to filter out a possible update just because it now meets the filtering // criteria otherwise we won't be able to sync that change with the ViewModel. // ToDo: Get this version of the PullAsync to work since it is more efficient //await TodoItemTable.PullAsync(ACTIVE_ITEMS + Locator.Instance.MobileService.CurrentUser.UserId, null); await TodoItemTable.PullAsync(ACTIVE_ITEMS, null); List <TodoItem> tblTodoItems = await TodoItemTable.ToListAsync(); // This foreach loops handles updating or adding ViewModel items by iterating // over todo items fetched from the server (held in tblTodoItems) and add and // updating corresponding todo items is the backing ViewModel (held in TodoItems) foreach (TodoItem tblTodoItem in tblTodoItems) { bool itemIsInViewModel = false; // Can't use Linq on an ObservableCollection so we have to manually search for it foreach (TodoItemViewModel vmTodoItem in TodoItems) { // If table item is in ViewModel then check to see if it needs to be updated if (tblTodoItem.Id == vmTodoItem.TodoItem.Id) { itemIsInViewModel = true; // If ViewModel item needs to be updated if (vmTodoItem.TodoItem.Version != tblTodoItem.Version) { System.Diagnostics.Debug.WriteLine("Updating item: {0}, \nOld Name: {1}, Old Done: {2}, Old Version: {3}\nNew Name: {4}, New Done: {5}, New Version: {6}", vmTodoItem.TodoItem.Id, vmTodoItem.Name, vmTodoItem.Done, vmTodoItem.TodoItem.Version, tblTodoItem.Name, tblTodoItem.Done, tblTodoItem.Version); // Because TodoItems are data bound to the UI we have to update it on the UI thread Device.BeginInvokeOnMainThread(() => { // Assign these through the ViewModel properties so that INPC event fires vmTodoItem.Name = tblTodoItem.Name; vmTodoItem.Done = tblTodoItem.Done; // Assign these through the Model properties since they don't have // corresponding ViewModel propeties to assign through vmTodoItem.TodoItem.Version = tblTodoItem.Version; }); // Because the update is occuring async we need to block until the update // completes otherwise UI binding updates might not show up properly bool updateComplete = false; while (!updateComplete) { System.Diagnostics.Debug.WriteLine("Waiting for updates to complete"); updateComplete = vmTodoItem.Name == tblTodoItem.Name && vmTodoItem.Done == tblTodoItem.Done && vmTodoItem.TodoItem.Version == tblTodoItem.Version; // Free up execution while we wait await Task.Delay(50); } } } } // If table item is not in ViewModel but it should be then add it if (!itemIsInViewModel && !tblTodoItem.Done) { System.Diagnostics.Debug.WriteLine("Adding item: {0}, Name: {1}", tblTodoItem.Id, tblTodoItem.Name); int beforeCount = TodoItems.Count; bool addComplete = false; // Because TodoItems is data bound to the UI we have to add it on the UI thread Device.BeginInvokeOnMainThread(() => { TodoItems.Add(new TodoItemViewModel() { TodoItem = tblTodoItem }); }); // Because add is occuring async we need to block until it completes // otherwise some adds might not be added to the backing ViewModel and // thus the UI binding won't update correctly either while (addComplete) { System.Diagnostics.Debug.WriteLine("Waiting for adds to complete"); addComplete = TodoItems.Count > beforeCount; // Free up execution while we wait await Task.Delay(50); } } } List <TodoItemViewModel> vmItemToDelete = new List <TodoItemViewModel>(); // This foreach loop delets todo items in backing ViewModel (held in // TodoItems) that no longer exist in the server todo items (held in // tblTodoItems). Since you can't remove items from a collection while // you are iterating over it, we need to create a separate list of // items that need to be removed foreach (TodoItemViewModel vmTodoItem in TodoItems) { // If ViewModel item does not exist in synched table then it was deleted // so we need to delete it from the ViewModel as well if (!tblTodoItems.Exists(tblItem => tblItem.Id == vmTodoItem.TodoItem.Id)) { vmItemToDelete.Add(vmTodoItem); } // Todo items that were updated in such a way that they no longer meet // the filtering criteria are removed here. If you don't have filtering // criteria in your app then comment or remove this if statement if (vmTodoItem.Done) { vmItemToDelete.Add(vmTodoItem); } } int newCount = TodoItems.Count - vmItemToDelete.Count; // Remove delete items from the ViewModel foreach (TodoItemViewModel vmTodoItem in vmItemToDelete) { System.Diagnostics.Debug.WriteLine("Deleting item: {0}, Name: {1}", vmTodoItem.TodoItem.Id, vmTodoItem.Name); // Because TodoItems is data bound to the UI we have to remove it on the UI thread Device.BeginInvokeOnMainThread(() => { TodoItems.Remove(vmTodoItem); }); } // ToDo: I wrote this loop to make sure UI updated and presumably I tested // this to make sure it was needed AND it worked. What I have found is that // there are times where this loops forever while waiting for count to catch // up. So for now I have it commented out. //// Because deletes are occuring async we need to block until they //// complete otherwise UI binding might not update properly //while (TodoItems.Count > newCount) //{ // System.Diagnostics.Debug.WriteLine("Waiting for deletes to complete"); // // Free up execution while we wait // await Task.Delay(50); //} // Clean up local table by removing items marked as Done otherwise those records will // just stay in the local database causing it to continually grow with items that arn't // needed any more but hang around hidden because of the pull filter being used. // See https://azure.microsoft.com/en-us/documentation/articles/app-service-mobile-offline-data-sync/ // for a fuller explanation. await TodoItemTable.PurgeAsync(TodoItemTable.Where(todoItem => todoItem.Done)); }