Skip to content

Instantly share code, notes, and snippets.

@ericbrunner
Created March 22, 2017 15:42
Show Gist options
  • Save ericbrunner/44fe9d0aeb82227ebc38d2b3b53a32c6 to your computer and use it in GitHub Desktop.
Save ericbrunner/44fe9d0aeb82227ebc38d2b3b53a32c6 to your computer and use it in GitHub Desktop.
OfflineSyncManager
// To add offline sync support: add the NuGet package Microsoft.Azure.Mobile.Client.SQLiteStore
// to all projects in the solution and uncomment the symbol definition OFFLINE_SYNC_ENABLED
// For Xamarin.iOS, also edit AppDelegate.cs and uncomment the call to SQLitePCL.CurrentPlatform.Init()
// For more information, see: http://go.microsoft.com/fwlink/?LinkId=620342
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Acr.UserDialogs;
using Microsoft.WindowsAzure.MobileServices;
using Microsoft.WindowsAzure.MobileServices.Sync;
using Plugin.Connectivity;
using trucker_rolsped.Helper;
using trucker_rolsped.Models;
using Microsoft.WindowsAzure.MobileServices.SQLiteStore;
using Newtonsoft.Json.Linq;
using Plugin.Connectivity.Abstractions;
using Polly;
using Polly.Wrap;
using trucker_rolsped.Auth;
using trucker_rolsped.Exceptions.OfflineSyncManager;
using trucker_rolsped.Helper.HockeyApp;
using trucker_rolsped.Interfaces.Authentication;
using trucker_rolsped.Interfaces.Utils;
using trucker_rolsped.Pages;
using trucker_rolsped.Workflow;
using Xamarin.Forms;
namespace trucker_rolsped.StoreManager
{
public sealed class OfflineSyncStoreManager
{
public readonly MobileServiceClient MobileAppClient;
public static readonly OfflineSyncStoreManager Instance = new OfflineSyncStoreManager();
public static bool IsSyncStoreInitialized => Instance.MobileAppClient.SyncContext.IsInitialized;
public bool IsOfflineEnabled => true;
public IMobileServiceSyncTable<TagSprache> TagSpracheTable;
public IMobileServiceSyncTable<TruckFahrer> TruckFahrerTable;
public IMobileServiceSyncTable<TruckAuftrag> TruckAuftragTable;
public IMobileServiceSyncTable<TruckAuftragLauf> TruckAuftragLaufTable;
public IMobileServiceSyncTable<TruckAuftragWorkFlow> TruckAuftragWorkFlowTable;
public IMobileServiceSyncTable<TruckWorkFlowStamm> TruckWorkFlowStammTable;
public IMobileServiceSyncTable<TruckProblemStamm> TruckProblemStammTable;
public IMobileServiceSyncTable<TruckAuftragProblem> TruckAuftragProblemTable;
public readonly TagSpracheStore TagSpracheStore;
public readonly TruckFahrerStore TruckFahrerStore;
public readonly TruckAuftragStore TruckAuftragStore;
public readonly TruckAuftragLaufStore TruckAuftragLaufStore;
public readonly TruckAuftragOverviewStore TruckAuftragOverviewStore;
public readonly TruckAuftragHistoryStore TruckAuftragHistoryStore;
public readonly TruckAuftragWorkFlowStore TruckAuftragWorkFlowStore;
public readonly TruckWorkFlowStammStore TruckWorkFlowStammStore;
public readonly TruckProblemStammStore TruckProblemStammStore;
public readonly TruckAuftragProblemStore TruckAuftragProblemStore;
public static readonly string TagSpracheQueryAll = "allTagSpracheItems";
public static readonly string TruckFahrerQueryAll = "allTruckFahrerItems";
public static readonly string TruckAuftragQueryAll = "allTruckAuftragItems";
public static readonly string TruckAuftragLaufQueryAll = "allTruckAuftragLaufItems";
public static readonly string TruckAuftragWorkFlowQueryAll = "allTruckAuftragWorkFlowItems";
public static readonly string TruckWorkFlowStammQueryAll = "allTruckWorkFlowStammItems";
public static readonly string TruckProblemStammQueryAll = "allTruckProblemStammItems";
private IMobileServiceTableQuery<TagSprache> _tagSpracheTableQuery;
private IMobileServiceTableQuery<TruckFahrer> _truckfahrerTableQuery;
private IMobileServiceTableQuery<TruckAuftrag> _truckAuftragTableQuery;
private IMobileServiceTableQuery<TruckAuftragLauf> _truckAuftragLaufTableQuery;
private IMobileServiceTableQuery<TruckAuftragWorkFlow> _truckAuftragWorkflowTableQuery;
private IMobileServiceTableQuery<TruckWorkFlowStamm> _truckWorkflowStammTableQuery;
private IMobileServiceTableQuery<TruckProblemStamm> _truckProblemStammTableQuery;
private OfflineSyncStoreManager()
{
MobileAppClient = new MobileServiceClient(Constants.AppServiceUrl, new AuthenticationDelegatingHandler());
if (Constants.AlternateLoginHost != null)
MobileAppClient.AlternateLoginHost = new Uri(Constants.AlternateLoginHost);
// CRUD StoreManager (local store)
TagSpracheStore = new TagSpracheStore();
TruckFahrerStore = new TruckFahrerStore();
TruckAuftragStore = new TruckAuftragStore();
TruckAuftragLaufStore = new TruckAuftragLaufStore();
TruckAuftragOverviewStore = new TruckAuftragOverviewStore();
TruckAuftragHistoryStore = new TruckAuftragHistoryStore();
TruckAuftragWorkFlowStore = new TruckAuftragWorkFlowStore();
TruckWorkFlowStammStore = new TruckWorkFlowStammStore();
TruckProblemStammStore = new TruckProblemStammStore();
TruckAuftragProblemStore = new TruckAuftragProblemStore();
}
public async Task<bool> WaitForSyncStoreToBeInitialized()
{
var isInitialized = false;
try
{
// Retry a specified number of times, using a function to
// calculate the duration to wait between retries based on
// the current retry attempt (allows for exponential backoff)
// In this case will wait for
// 2 ^ 1 = 2 seconds then
// 2 ^ 2 = 4 seconds then
// 2 ^ 3 = 8 seconds then
// 2 ^ 4 = 16 seconds then
// 2 ^ 5 = 32 seconds
const int maxAttempts = 5;
var waitAndRetryPolicy =
Policy.
Handle<Exception>()
.WaitAndRetryAsync(
retryCount: maxAttempts,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetryAsync: async (exception, timespan) => { await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Info-SyncStore", exception.Message); }
);
var fallBackPolicy = Policy<bool>
.Handle<Exception>()
.FallbackAsync(
fallbackAction: async (cancelToken) => await Task.FromResult(false),
onFallbackAsync: async (result) =>
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Error-SyncStore",
$"After {maxAttempts} attempts stopped a to try to initialize the SyncStore. Message: {result?.Exception?.Message}"));
PolicyWrap<bool> policyWrap = fallBackPolicy.WrapAsync(waitAndRetryPolicy);
isInitialized = await policyWrap.ExecuteAsync(action: InitializeSyncStore);
}
catch (Exception e)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
return isInitialized;
}
private async Task<bool> InitializeSyncStore()
{
bool isInitialized = OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.IsInitialized;
if (isInitialized)
return true;
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Info-InitSyncStore", "Waiting for SyncStore to be initialized ...");
var store = new MobileServiceSQLiteStore(Constants.SqliteStorePath);
// Real TruckerApp DataModels
store.DefineTable<TagSprache>();
store.DefineTable<TruckFahrer>();
store.DefineTable<TruckAuftrag>();
store.DefineTable<TruckAuftragLauf>();
store.DefineTable<TruckAuftragWorkFlow>();
store.DefineTable<TruckWorkFlowStamm>();
store.DefineTable<TruckProblemStamm>();
store.DefineTable<TruckAuftragProblem>();
//Initializes the SyncContext using the default IMobileServiceSyncHandler.
await MobileAppClient.SyncContext.InitializeAsync(store);
// Rolsped TruckerApp Tables (local store)
TagSpracheTable = MobileAppClient.GetSyncTable<TagSprache>();
TruckFahrerTable = MobileAppClient.GetSyncTable<TruckFahrer>();
TruckAuftragTable = MobileAppClient.GetSyncTable<TruckAuftrag>();
TruckAuftragLaufTable = MobileAppClient.GetSyncTable<TruckAuftragLauf>();
TruckAuftragWorkFlowTable = MobileAppClient.GetSyncTable<TruckAuftragWorkFlow>();
TruckWorkFlowStammTable = MobileAppClient.GetSyncTable<TruckWorkFlowStamm>();
TruckProblemStammTable = MobileAppClient.GetSyncTable<TruckProblemStamm>();
TruckAuftragProblemTable = MobileAppClient.GetSyncTable<TruckAuftragProblem>();
// Create Queries for remote-db to local-db data (from backend SQL store) pull request
_tagSpracheTableQuery = TagSpracheTable.CreateQuery().IncludeTotalCount();
_truckfahrerTableQuery = TruckFahrerTable.CreateQuery().IncludeTotalCount();
_truckAuftragTableQuery = TruckAuftragTable.CreateQuery().IncludeTotalCount();
_truckAuftragLaufTableQuery = TruckAuftragLaufTable.CreateQuery().IncludeTotalCount();
_truckAuftragWorkflowTableQuery = TruckAuftragWorkFlowTable.CreateQuery().IncludeTotalCount();
_truckWorkflowStammTableQuery = TruckWorkFlowStammTable.CreateQuery().IncludeTotalCount();
_truckProblemStammTableQuery = TruckProblemStammTable.CreateQuery().IncludeTotalCount();
isInitialized = OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.IsInitialized;
if (!isInitialized)
throw new SyncStoreInitializationException("SyncStore not initialized!.Maybe with next attempt...");
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Info-InitSyncStore", "Awaited SyncStore Initialization.");
return true;
}
public async Task<bool> IsRemoteBackendReachable()
{
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
try
{
var isConnected = CrossConnectivity.Current.IsConnected;
var isReachable = await CrossConnectivity.Current.IsRemoteReachable(Constants.AppServiceUrl, port: 443);
var isRemoteBackendReachable = isConnected && isReachable;
tcs.SetResult(isRemoteBackendReachable);
}
catch (Exception e)
{
tcs.SetResult(false);
}
return await tcs.Task;
}
private TaskCompletionSource<object> _phoneConnectivityChangedSource;
private bool _isPhoneConnectivityChangedRunning;
public Task WaitForPhoneConnectivityChangedToComplete()
{
return _phoneConnectivityChangedSource == null ? Task.FromResult<object>(null) : _phoneConnectivityChangedSource.Task;
}
private void SetPhoneConnectivityChangedRunning(bool running)
{
_isPhoneConnectivityChangedRunning = running;
if (_isPhoneConnectivityChangedRunning)
{
_phoneConnectivityChangedSource = new TaskCompletionSource<object>();
}
else
{
_phoneConnectivityChangedSource.SetResult(null);
}
}
private readonly SemaphoreSlim _phoneConnectivityChangedMutex = new SemaphoreSlim(1, 1);
public async void PhoneConnectivityChanged(object sender, ConnectivityChangedEventArgs e)
{
if (_phoneConnectivityChangedMutex.CurrentCount == 0)
{
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Info-SyncLock-PhoneConnectivityChanged", "No further threads can enter critical section. Indicator for high frequency connection lost / connection estabished.");
}
//Asynchronously wait to enter the Semaphore. If no-one has been granted access to the Semaphore,
//code execution will proceed, otherwise this thread waits here until the semaphore is released
await _phoneConnectivityChangedMutex.WaitAsync();
SetPhoneConnectivityChangedRunning(running: true);
try
{
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("PhoneConnectivityChanged", $"Connected: {(CrossConnectivity.Current.IsConnected ? "TRUE" : "FALSE")}");
if (!CrossConnectivity.Current.IsConnected)
{
SetPhoneConnectivityChangedRunning(running: false);
_phoneConnectivityChangedMutex.Release();
return;
}
if (!await IsRemoteBackendReachable())
{
SetPhoneConnectivityChangedRunning(running: false);
_phoneConnectivityChangedMutex.Release();
return;
}
if (!App.TruckDriver.Authenticated)
{
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("PhoneConnectivityChanged", "SignIn");
App.TruckDriver = await DependencyService.Get<IAuthenticate>().SignIn();
}
if (!App.TruckDriver.Authenticated)
{
SetPhoneConnectivityChangedRunning(running: false);
_phoneConnectivityChangedMutex.Release();
return;
}
if (!OfflineSyncStoreManager.IsSyncStoreInitialized)
{
if (!await OfflineSyncStoreManager.Instance.WaitForSyncStoreToBeInitialized())
{
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Error-SyncStore-PhoneConnectivityChanged", "SyncStore couldn't be initialized! App must be terminated.");
DependencyService.Get<IApplicationClose>().CloseApp();
return;
}
}
// Wait for local Pending Tasks / Operations to complete
await WaitForPendingTasksToComplete();
if (App.IsApplicationVisible)
{
try
{
UserDialogs.Instance.ShowLoading("Daten werden synchronisiert...");
await OfflineSyncStoreManager.Instance.PushPurgePullOfAllTablesInLocalStoreAsync();
UserDialogs.Instance.HideLoading();
}
catch (Exception exception)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(exception);
}
finally
{
SetPhoneConnectivityChangedRunning(running: false);
_phoneConnectivityChangedMutex.Release();
await PopToRootView();
}
}
else
{
try
{
await OfflineSyncStoreManager.Instance.PushPurgePullOfAllTablesInLocalStoreAsync();
}
catch (Exception exception)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(exception);
}
finally
{
SetPhoneConnectivityChangedRunning(running: false);
_phoneConnectivityChangedMutex.Release();
await PopToRootView();
}
}
}
catch (Exception ex)
{
SetPhoneConnectivityChangedRunning(running: false);
_phoneConnectivityChangedMutex.Release();
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(ex);
}
}
public static async Task PopToRootView()
{
// Show TruckAuftragOverview. OnAppearing => Authenticate + RefreshItemsAsync triggered.
MainPageCS masterDetailPage = Application.Current.MainPage as MainPageCS;
if (masterDetailPage != null)
{
if (masterDetailPage.IsPresented)
{
masterDetailPage.TruckerAppMasterPage.ListView.SelectedItem = null;
masterDetailPage.IsPresented = false;
}
masterDetailPage.Detail = masterDetailPage.TruckAuftragOverviewNavigation;
// If not on the TruckAuftragOverview page
if (masterDetailPage.Detail.Navigation.NavigationStack.Count > 1)
{
await masterDetailPage.Detail.Navigation.PopToRootAsync(animated: false);
// Note: Authenticate, Refresh triggered implicitly when popped OnAppearing event is triggered.
}
Page uncastedOverviewPage = masterDetailPage.TruckAuftragOverviewNavigation.Navigation.NavigationStack.FirstOrDefault();
var overviewPage = uncastedOverviewPage as TruckAuftragOverviewPage;
if (overviewPage != null)
{
await overviewPage.Authenticate();
}
}
}
private static async Task WaitForPendingTasksToComplete()
{
System.Diagnostics.Debug.WriteLine("PhoneConnectivityChanged: Waiting for local user active WorkflowTask to complete...");
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("PhoneConnectivityChanged", "Waiting for local user active WorkflowTask to complete...");
// Check if a Workflow Task is active
await WorkflowTaskCommandFactory.Instance.WaitForWorkflowTaskToComplete();
System.Diagnostics.Debug.WriteLine("PhoneConnectivityChanged: Awaited local user active WorkflowTask.");
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("PhoneConnectivityChanged", "Awaited local user active WorkflowTask.");
}
private async Task HandleConflictResolutionOfLocalTablePushAttemptAsync(ReadOnlyCollection<MobileServiceTableOperationError> syncErrors)
{
/*
* * Handling Conflict Resolution
* * see :https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/#handling-conflict-resolution
* * */
// Simple error/conflict handling. A real application would handle the various errors like network conditions,
// server conflicts and others via the IMobileServiceSyncHandler.
if (syncErrors != null)
{
foreach (var error in syncErrors)
{
var localItem = error.Item;
var serverItem = error.Result;
if (error.OperationKind == MobileServiceTableOperationKind.Update && error.Result != null)
{
var localVersion = localItem?[MobileServiceSystemColumns.Version];
var serverVersion = serverItem?[MobileServiceSystemColumns.Version];
if (localVersion == null)
{
if (await IsRemoteBackendReachable())
{
//Update failed, reverting to server's copy.
await error.CancelAndUpdateItemAsync(serverItem);
await MetricsManagerHelper.Instance
.SendErrorToApplicationInsightsAsync($"Operation: {error.OperationKind} LocalVersion == NULL." +
"Action: Reverting to remote-db server's copy\r\n" +
$"Table: {error.TableName} Record Id: {localItem?[MobileServiceSystemColumns.Id]}\r\n" +
$"Local: {localItem}\r\n" +
$"Server: {serverItem}");
}
continue;
}
if (serverVersion == null)
{
if (await IsRemoteBackendReachable())
{
//Update failed, reverting to server's copy.
await error.CancelAndUpdateItemAsync(serverItem);
await MetricsManagerHelper.Instance
.SendErrorToApplicationInsightsAsync($"Operation: {error.OperationKind} ServerVersion == NULL.\r\n" +
"Action: Reverting to remote-db server's copy\r\n" +
$"Table: {error.TableName} Record Id: {localItem?[MobileServiceSystemColumns.Id]}\r\n" +
$"Local: {localItem}\r\n" +
$"Server: {serverItem}");
}
continue;
}
// Set Local version to Server Version an Update again. PendingOpertation is set to new updated localItem and in outer loop a PushAsync is attempted again.
localItem[MobileServiceSystemColumns.Version] = serverItem[MobileServiceSystemColumns.Version];
if (await IsRemoteBackendReachable())
{
await error.UpdateOperationAsync(JObject.FromObject(localItem));
await MetricsManagerHelper.Instance.SendInfoToApplicationInsightsAsync($"Operation: {error.OperationKind} LocalVersion older than (<) ServerVersion. LocalItem updated in local.db.");
}
}
else
{
if (await IsRemoteBackendReachable())
{
// Discard local change.
await error.CancelAndDiscardItemAsync();
await MetricsManagerHelper.Instance
.SendGenericMessageToApplicationInsightsAsync("Storno", $"Operation: {error.OperationKind} PUSH failed.\r\n" +
"Action: Discard local-db change. TruckerFahrer propably removed from that TruckAuftrag in RCSmobile*\r\n" +
$"Table: {error.TableName} Record Id: {localItem?[MobileServiceSystemColumns.Id]}\r\n" +
$"HttpStatus: {error.Status}\r\n" +
$"Local: {localItem}\r\n" +
$"Server: {serverItem}");
}
}
}
}
}
#region LocalStore methods
public async Task PullAsync()
{
bool isConnected = await IsRemoteBackendReachable();
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db
// When it is online then a sync from the local-db with the remote-db is possible.
if (!isConnected)
{
return;
}
try
{
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/dataconcepts/#offline-synchronization
// Implict PUSH local-db changes to remote-db. Then PULL the remote-db data into local-db tables
await SyncTagSpracheTableInLocalStoreAsync();
await SyncTruckFahrerTableInLocalStoreAsync();
await SyncTruckAuftragTableInLocalStoreAsync();
await SyncTruckAuftragLaufTableInLocalStoreAsync();
await SyncTruckAuftragWorkflowTableInLocalStoreAsync();
await SyncTruckWorkflowStammTableInLocalStoreAsync();
await SyncTruckProblemStammTableInLocalStoreAsync();
}
catch (Exception e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
}
}
public async Task SyncTruckProblemStammTableInLocalStoreAsync()
{
bool isConnected = await IsRemoteBackendReachable();
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db
// When it is online then a sync from the local-db with the remote-db is possible.
if (!isConnected)
{
return;
}
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null;
try
{
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/dataconcepts/#offline-synchronization
// Implict PUSH local-db changes to remote-db. Then PULL the remote-db data into local-db tables
await OfflineSyncStoreManager.Instance.TruckProblemStammTable.PullAsync(TruckProblemStammQueryAll, _truckProblemStammTableQuery);
}
catch (MobileServicePushFailedException e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
if (e.PushResult != null)
{
syncErrors = e.PushResult.Errors;
}
}
}
catch (Exception e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
}
if (await IsRemoteBackendReachable() && syncErrors != null)
{
try
{
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors);
}
catch (Exception e)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
}
}
public async Task SyncTruckWorkflowStammTableInLocalStoreAsync()
{
bool isConnected = await IsRemoteBackendReachable();
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db
// When it is online then a sync from the local-db with the remote-db is possible.
if (!isConnected)
{
return;
}
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null;
try
{
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/dataconcepts/#offline-synchronization
// Implict PUSH local-db changes to remote-db. Then PULL the remote-db data into local-db tables
await OfflineSyncStoreManager.Instance.TruckWorkFlowStammTable.PullAsync(TruckWorkFlowStammQueryAll, _truckWorkflowStammTableQuery);
}
catch (MobileServicePushFailedException e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
if (e.PushResult != null)
{
syncErrors = e.PushResult.Errors;
}
}
}
catch (Exception e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
}
if (await IsRemoteBackendReachable() && syncErrors != null)
{
try
{
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors);
}
catch (Exception e)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
}
}
public async Task SyncTruckAuftragWorkflowTableInLocalStoreAsync()
{
bool isConnected = await IsRemoteBackendReachable();
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db
// When it is online then a sync from the local-db with the remote-db is possible.
if (!isConnected)
{
return;
}
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null;
try
{
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/dataconcepts/#offline-synchronization
// Implict PUSH local-db changes to remote-db. Then PULL the remote-db data into local-db tables
await OfflineSyncStoreManager.Instance.TruckAuftragWorkFlowTable.PullAsync(TruckAuftragWorkFlowQueryAll, _truckAuftragWorkflowTableQuery);
}
catch (MobileServicePushFailedException e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
if (e.PushResult != null)
{
syncErrors = e.PushResult.Errors;
}
}
}
catch (Exception e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
}
if (await IsRemoteBackendReachable() && syncErrors != null)
{
try
{
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors);
}
catch (Exception e)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e).ConfigureAwait(false);
}
}
}
public async Task SyncTruckAuftragTableInLocalStoreAsync()
{
bool isConnected = await IsRemoteBackendReachable();
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db
// When it is online then a sync from the local-db with the remote-db is possible.
if (!isConnected)
{
return;
}
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null;
try
{
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/dataconcepts/#offline-synchronization
// Implict PUSH local-db changes to remote-db. Then PULL the remote-db data into local-db tables
await OfflineSyncStoreManager.Instance.TruckAuftragTable.PullAsync(TruckAuftragQueryAll, _truckAuftragTableQuery);
}
catch (MobileServicePushFailedException e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
if (e.PushResult != null)
{
syncErrors = e.PushResult.Errors;
}
}
}
catch (Exception e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
}
if (await IsRemoteBackendReachable() && syncErrors != null)
{
try
{
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors);
}
catch (Exception e)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
}
}
public async Task SyncTruckAuftragLaufTableInLocalStoreAsync()
{
bool isConnected = await IsRemoteBackendReachable();
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db
// When it is online then a sync from the local-db with the remote-db is possible.
if (!isConnected)
{
return;
}
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null;
try
{
await OfflineSyncStoreManager.Instance.TruckAuftragLaufTable.PullAsync(TruckAuftragLaufQueryAll, _truckAuftragLaufTableQuery);
}
catch (MobileServicePushFailedException e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
if (e.PushResult != null)
{
syncErrors = e.PushResult.Errors;
}
}
}
catch (Exception e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
}
if (await IsRemoteBackendReachable() && syncErrors != null)
{
try
{
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors);
}
catch (Exception e)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
}
}
public async Task SyncTagSpracheTableInLocalStoreAsync()
{
bool isConnected = await IsRemoteBackendReachable();
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db
// When it is online then a sync from the local-db with the remote-db is possible.
if (!isConnected)
{
return;
}
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null;
try
{
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/dataconcepts/#offline-synchronization
// Implict PUSH local-db changes to remote-db. Then PULL the remote-db data into local-db tables
await OfflineSyncStoreManager.Instance.TagSpracheTable.PullAsync(TagSpracheQueryAll, _tagSpracheTableQuery);
await OfflineSyncStoreManager.Instance.TagSpracheStore.InitLocalStoreCacheAsync();
}
catch (MobileServicePushFailedException e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
if (e.PushResult != null)
{
syncErrors = e.PushResult.Errors;
}
}
}
catch (Exception e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
}
if (await IsRemoteBackendReachable() && syncErrors != null)
{
try
{
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors);
}
catch (Exception e)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
}
}
public async Task SyncTruckFahrerTableInLocalStoreAsync()
{
bool isConnected = await IsRemoteBackendReachable();
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db
// When it is online then a sync from the local-db with the remote-db is possible.
if (!isConnected)
{
return;
}
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null;
try
{
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/dataconcepts/#offline-synchronization
// Implict PUSH local-db changes to remote-db. Then PULL the remote-db data into local-db tables
await OfflineSyncStoreManager.Instance.TruckFahrerTable.PullAsync(TruckFahrerQueryAll, _truckfahrerTableQuery);
}
catch (MobileServicePushFailedException e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
if (e.PushResult != null)
{
syncErrors = e.PushResult.Errors;
}
}
}
catch (Exception e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
}
if (await IsRemoteBackendReachable() && syncErrors != null)
{
try
{
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors);
}
catch (Exception e)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
}
}
//Instantiate a Singleton of the Semaphore with a value of 1. This means that only 1 thread can be granted access at a time.
private readonly SemaphoreSlim _pushAsyncMutex = new SemaphoreSlim(1, 1);
public async Task PushAsync(string contextName)
{
if (_pushAsyncMutex.CurrentCount == 0)
{
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Info-SyncLock-PushAsync", "No further threads can enter critical section.");
}
//Asynchronously wait to enter the Semaphore. If no-one has been granted access to the Semaphore,
//code execution will proceed, otherwise this thread waits here until the semaphore is released
await _pushAsyncMutex.WaitAsync();
try
{
bool isConnected = await IsRemoteBackendReachable();
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db
// When it is online then a sync from the local-db with the remote-db is possible.
if (!isConnected)
{
return;
}
const int maxAttempts = 5;
int currentAttempts = 0;
MobileServicePushFailedException mobileServicePushFailedException = null;
while (OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PendingOperations > 0 && (currentAttempts < maxAttempts))
{
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null;
try
{
System.Diagnostics.Debug.WriteLine($"Pending Ops before PUSH: {OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PendingOperations}");
// PUSH all local.db changes to the remote.db (changes of all tables are pushed to achieve referential integrity)
await OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PushAsync();
System.Diagnostics.Debug.WriteLine($"Pending Ops after PUSH: {OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PendingOperations}");
}
catch (MobileServicePushFailedException e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
mobileServicePushFailedException = e;
if (e.PushResult != null)
{
syncErrors = e.PushResult.Errors;
}
if (syncErrors != null)
{
System.Diagnostics.Debug.WriteLine($"Pending Ops before CONFLICT HANDLER: {OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PendingOperations}");
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors);
System.Diagnostics.Debug.WriteLine($"Pending Ops after CONFLICT HANDLER: {OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PendingOperations}");
}
currentAttempts++;
}
else
{
await MetricsManagerHelper.Instance.SendInfoToApplicationInsightsAsync("Internet Connection lost while data sync. Data sync not done.");
break;
}
}
catch (Exception e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
else
{
await MetricsManagerHelper.Instance.SendInfoToApplicationInsightsAsync("Internet Connection lost while data sync. Data sync not done.");
}
}
}
if (isConnected && OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PendingOperations > 0)
{
if (mobileServicePushFailedException != null)
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(mobileServicePushFailedException);
await MetricsManagerHelper.Instance.SendInfoToApplicationInsightsAsync("Internet Connection lost while data sync. Data sync not done.");
}
}
catch (Exception e)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
}
finally
{
// When the task is ready, release the semaphore.It is vital to ALWAYS release the semaphore when we are ready, or else we will end up with a Semaphore that is forever locked.
//This is why it is important to do the Release within a try...finally clause; program execution may crash or take a different path, this way you are guaranteed execution
_pushAsyncMutex.Release();
}
}
public async Task PurgeTagSpracheTableAsync()
{
try
{
// PURGE TagSprachTabel in local.db
await Instance.TagSpracheTable.PurgeAsync(TagSpracheQueryAll, null, true, CancellationToken.None);
}
catch (Exception e)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
if (e.InnerException != null)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e.InnerException);
}
}
}
public async Task PurgeLocalStoreAsync()
{
try
{
// PURGE All local-db tables and force to delete pending changes
await Instance.TruckFahrerTable.PurgeAsync(TruckFahrerQueryAll, null, true, CancellationToken.None);
await Instance.TagSpracheTable.PurgeAsync(TagSpracheQueryAll, null, true, CancellationToken.None);
await Instance.TruckAuftragTable.PurgeAsync(TruckAuftragQueryAll, null, true, CancellationToken.None);
await Instance.TruckAuftragLaufTable.PurgeAsync(TruckAuftragLaufQueryAll, null, true, CancellationToken.None);
await Instance.TruckAuftragWorkFlowTable.PurgeAsync(TruckAuftragWorkFlowQueryAll, null, true, CancellationToken.None);
await Instance.TruckWorkFlowStammTable.PurgeAsync(TruckWorkFlowStammQueryAll, null, true, CancellationToken.None);
await Instance.TruckProblemStammTable.PurgeAsync(TruckProblemStammQueryAll, null, true, CancellationToken.None);
}
catch (Exception e)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
if (e.InnerException != null)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e.InnerException);
}
}
}
private readonly SemaphoreSlim _pushPurgePullMutex = new SemaphoreSlim(1, 1);
public async Task PushPurgePullOfAllTablesInLocalStoreAsync()
{
if (_pushPurgePullMutex.CurrentCount == 0)
{
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Info-SyncLock-PushPurgePullOfAllTablesInLocalStoreAsync", "No further threads can enter critical section.");
}
//Asynchronously wait to enter the Semaphore. If no-one has been granted access to the Semaphore,
//code execution will proceed, otherwise this thread waits here until the semaphore is released
await _pushPurgePullMutex.WaitAsync();
bool isConnected;
try
{
isConnected = await IsRemoteBackendReachable();
// Purge only makes sense when we have connectivity
if (!isConnected)
return;
// PUSH any pending changes to the remote.db backend
await PushAsync("ALLTABLES");
isConnected = await IsRemoteBackendReachable();
if (!isConnected)
{
await MetricsManagerHelper.Instance.SendErrorToApplicationInsightsAsync("Connection lost while trying to Push, Purge, Pull data...");
return;
}
/*
* IMPORTANT Design Notes:
* ========================================================================================================================================================================
* The previous call to PushAsync tries 5 times to push pending ops to the remote.db. To prevent that any pending ops in local.db are
* purged (deleted), that if statement check is done.
*
* When a TA Storno happens it could happen, that the "cancelled" TA remains in local.db when the PushAsync fails for any reason. That cancelled TA
* is no longer be assigned to the current LKW TruckFahrer. So any subsequent PullAsync will not fetch it any more, but it is as "stale" data in local.db.
*
* Ok, not good.
*
* If a LKW User would dare to start the workflow of that "stale" TA it would be rejected fron remote backend (Http NotFound exception thrown, see TruckAuftragController)
*
* If that TA was already started it could happen that PATCH operation to TruckAuftragLauf and TruckAuftragWorkflow goes through if the LKW User e.g. clicks on
* "Bin angekommen" Task.
*
* Well that is possible, but on the other hand it gets logged in Application Insights!
*
* PATCH could be prevented in remote backend on the TAL and TAW tables , too. But then if we throw a Http NotFound exception , we MUST ensure that that exception is
* never thrown when a Task31 TA is completed, where PATCH ops are done on TA, TAL, TAW where the TA.FahrerId is set to -1.
*
*
* Added such code in TruckAuftragLaufController and TruckAuftragWorkflowController, to prevent that a LKW User could successfully update a TA,TAL,TAW in remote.db
* that is already "cancelled" to him ("STORNO")!!!
*
* So far so good.
*
* So we prevented that a "cancelled" TA can be updated in remote.db from a LKW User that is no longer that assigned User.
*
* I can't prevent that that "stale" TA is displayed in the TruckerApp list for a while until the "PushPurgePullOfAllTablesInLocalStoreAsync" is invoked again.
* PushPurgePullOfAllTablesInLocalStoreAsync is invoked with next Push Command and/or PhoneConnectivityChanged event or when LKW User manually clicks on "Sync" button.
*
* */
if (OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PendingOperations == 0)
{
// PURGE All local-db tables, but KEEP Pending Changes!!! Only get a "Fresh" copy of the data from remote.db
await Instance.TruckFahrerTable.PurgeAsync(TruckFahrerQueryAll, _truckfahrerTableQuery, CancellationToken.None);
await Instance.TagSpracheTable.PurgeAsync(TagSpracheQueryAll, _tagSpracheTableQuery, CancellationToken.None);
await Instance.TruckAuftragTable.PurgeAsync(TruckAuftragQueryAll, _truckAuftragTableQuery, CancellationToken.None);
await Instance.TruckAuftragLaufTable.PurgeAsync(TruckAuftragLaufQueryAll, _truckAuftragLaufTableQuery, CancellationToken.None);
await Instance.TruckAuftragWorkFlowTable.PurgeAsync(TruckAuftragWorkFlowQueryAll, _truckAuftragWorkflowTableQuery, CancellationToken.None);
await Instance.TruckWorkFlowStammTable.PurgeAsync(TruckWorkFlowStammQueryAll, _truckWorkflowStammTableQuery, CancellationToken.None);
await Instance.TruckProblemStammTable.PurgeAsync(TruckProblemStammQueryAll, _truckProblemStammTableQuery, CancellationToken.None);
}
//PULL all data from the remote-db sql server into the local-db truckerapp store
await Instance.PullAsync();
}
catch (Exception e)
{
isConnected = await IsRemoteBackendReachable();
if (isConnected)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e);
if (e.InnerException != null)
{
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e.InnerException);
}
}
}
finally
{
// When the task is ready, release the semaphore.It is vital to ALWAYS release the semaphore when we are ready, or else we will end up with a Semaphore that is forever locked.
//This is why it is important to do the Release within a try...finally clause; program execution may crash or take a different path, this way you are guaranteed execution
_pushPurgePullMutex.Release();
}
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment