Gli esempi visti sinora funzionano correttamente: l'unica condizione è che il dispositivo in fase di salvataggio e lettura dei dati abbia a disposizione connettività per potersi connettere al backend.

Inoltre i dati vengono scaricati vengono mantenuti nlla memoria volatile del client: chiudere l'applicativo significa perdere irrimediabilmente le informazioni e la necessità di connettività per riscaricarli.

Inoltre ogni modifica eseguita verrà sottoposta subito al backend per la persistenza su Sql Server.

Detto in altri termini non esiste alcuna persistenza locale, ma solo quella remota su Sql Server, che avviene con l'ausilio del backend ospitato sui servizi Mobile App di Azure.

Per avere la possibilità di salvare il record anche senza connettività, nonchè poter fare delle query sugli stessi dati, occorre introdurre sua maestà l'offline-sync.

Con questo strumento è possibile interagire con un database locale, SqLite, che sarà sincronizzato con il database Sql Server sempre usando i servizi Mobile App Service. Quind tutte le interrogazioni e modifiche avranno come destinatario questo il database locale, e non si avrà necessità di alcuna connessione.

Solo in fase di sincronizzazione del database locale con quello remoto si avrà necessità di connettività per far interagire il client  con i servizi di backend.

Anche per introdurre l'offlie-sync è necessario agire solo sul codice della app (cioè lato client), e la modiifca più notevole è che questa volta si userà l'oggetto che implementa l’interfaccia IMobileServiceSyncTable (prima si usava IMobileServiceTable).

public class AzureMobileClient
{
	MobileServiceClient client;

	public AzureMobileClient()
	{
		client = new MobileServiceClient("<URL del backend Azure>");
	}

	public async Task Initialize()
	{
		if (client?.SyncContext?.IsInitialized ?? false)
			return;

		string DbPath = Path.Combine(MobileServiceClient.DefaultDatabasePath, "focac-book.db");

		var store = new MobileServiceSQLiteStore(DbPath);

		//definisco la tabella
		store.DefineTable();

		//Initialize SyncContext
		//Crea tutte le tabelle di supporto che servono per l'offline sync
		//e che non saranno mai visibili all'utilizzatore
		//ma saranno usate in fase di push e pull
		await client.SyncContext.InitializeAsync(store);
	}

	public async Task<ICollection> ReadAllItemsAsync()
	{
		try
		{
			IMobileServiceSyncTable Focacciatable = client.GetSyncTable();
			return await Focacciatable.ToListAsync();
		}
		catch (Exception e)
		{
			throw e;
		}
	}


	public async Task SyncFocacceDB()
	{
		try
		{

			IMobileServiceSyncTable Focacciatable = client.GetSyncTable();

			await Focacciatable.PullAsync("allFocacce", Focacciatable.CreateQuery());
		}
		catch (Exception ex)
		{
			Debug.WriteLine("Unable to sync - Errore: " + ex);
		}

	}

	public async Task PushModifiche()
	{
		try
		{
			await client.SyncContext.PushAsync();
		}

		catch (Exception ex)
		{
			Debug.WriteLine("Unable to sync, that is alright as we have offline capabilities: " + ex);
		}

	}

	public async Task AddUpdateItemAsync(FocaccePost focaccePost)
	{

		try
		{
			IMobileServiceSyncTable Focacciatable = client.GetSyncTable();
			if (string.IsNullOrEmpty(focaccePost.Id))
			{
				//e' un inserimento
				await Focacciatable.InsertAsync(focaccePost);
			}
			else
			{
				//è una modifica
				await Focacciatable.UpdateAsync(focaccePost);
			}
		}

		catch (Exception e)
		{
			throw;
		}
	}
	public async Task DeleteItemAsync(FocaccePost focaccePost)
	{

		try
		{
			IMobileServiceSyncTable Focacciatable = client.GetSyncTable();
			await Focacciatable.DeleteAsync(focaccePost);
		}
		catch (Exception e)
		{
			throw;
		}
	}
}

Per implementare la sincronizzazione offline occorre usare l'oggetto sync table, che sono proposte dall’interfacciaIMobileServiceSyncTable.

Questa interfaccia propone come in precedenza i metodi per implementare le operazioni CRUD Create, Read, Update, Delete.

Comunque prima di inziare a utilizzare questo oggetto, pena il verificarsi di errori, occorre eseguire alcune operazioni che creano e inizializzano il database locale, creando non solo le tabelle che conterranno i dati nella struttura proposta dai modelli, ma anche diverse altre tabelle di ausilio: queste ultime saranno completamente nascoste e trasparenti all'utilizzatore ma saranno di supporto a tutte le operatività coinvolte.

Queste operazioni di inizializzazione sono contenute nel metodo Intialize (nome orginale, non trovate ?), che sarà richiamato ad ogni avvio della app. La prima volta questo metodo creerà il database con le tabelle: richiami successivi verificheranno che la struttura delle tabelle seguano quelle dei modelli, e nel caso vi siano delle differenze in modo automagico ne modificherà la struttura.

Fatto questo l'altra operazione da fare è fare una prima sincronizzazione che riempie le tabelle locali con quanto scaricato dal backend (cioè con i dati di Sql Server): questa operazione prende il nome di pull (....a proposito di nomi originali....) ed è svolta nel metodo SyncFocacceDB.

Solo al termine di queste due operazioni sarà possibile interagire con le tabelle locali in modo perfettamente analogo a quanto visto nel caso dell'online-sync, solo che questa volta si usa un altro oggetto (che comunque dispone di metodi simili) e, cosa più importante, le modifiche e le interrogazioni impegneranno il database locale e pertanto non è necessaria alcuna connettività per la corretta esecuzione.

Prima o poi, però, si vorranno ribaltare le modiifche CUD eseguite localmente sul database remoto: in questo caso l'operazione è chiamata in modo incredibilmente originale e singolare push !

Eseguendo un'operazione di push le modifiche locali tutte in una volta saranno replicate sull’instanza di Sql Server: questa operazione è implementata dal metodo PushModifiche.

Oss.: Le operazioni eseguite sul database locale e non ancora replicate su Sql Server prendono il nome, pensa te l'originalità dei nome dell'SDK, di pending operation.

Ora una osservazione interessante: per preservare l’integrità della base dati (relazioni tra tabelle, semantica relazione tra le tabelle) le modifiche CUD devono essere sottoposte al backend, e quindi al database, nello stesso ordine con cui sono state fatte sulle relative tabelle locali.

Va da sè che anche in questo caso l'SDK ci viene in aiuto: infatti basta richiamare semplicemente PushAsync e le varie operazioni saranno svolte come necessario per nostro conto richiamando i vari metodi dei controller coinvolti.

Più in dettaglio eseguendo l’operazione di push l’SDK richiamerà nel backend le relative chiamata Post, per gli inserimenti, e Patch per le modifiche nonché Delete per le cancellazioni, ma nell’ordine corretto con cui queste sono avvenute sul database locale. 

Le varie tabelle che l'SDK crea a supporto delle operazioni (InizializaAsync) servono tra le altre cose a questo: cioè a memorizzare l'ordine delle varie operazioni CUD per poter essere replicate nell'ordine corretto nell'ambito del push.

I più attenti di Voi potrebbero anche osservare che se si modificasse una tabella con una operazione CUD e prima di richiamare il Push si eseguisse un Pull, questo potrebbe portare a situazioni di inconsistenza sulle basi dati (o a veri e prorpi casini......).

Per tale motivo prima di eseguire un pull l’SDK verifica preventivamente se per caso non ci siano delle modifiche sulle tabelle coinvolte che ancora non sono state riportate al backend: in caso positivo prima di eseguire il pull autonomamente il sistema eseguirà un push, chiamato Implicit Push.

Tutto questo ci viene servito gratuitamente (....si fa per dire ....nel senso senza dover scrivere codice per implementarlo) dall'utilizzo dell'SDK Microsoft.Azure.Mobile.Client.

Ancora qualche altra figata che ci dà senza sforzo l'SDK.

Le operazioni di pull possono essere molto lunghe, sopratutto a fronte di un grosso numero di record coinvolti: per tale motivo sono state messe in atto tutta una serie di ausili per ottimizzare al massimo l’operazione.

IO sono riuscito a far funzionare la baracca con circa 500.000 record coinvolti, senza grosse latenze, per cui regolatevi Voi......

Per iniziare grazie all’uso dell’SDK in automatico lo scaricamento dei dati è automaticamente paginato, di default 50 item alla volta: questo con l’evidente fine di ottimizzare il trasferimento dei dati.

Inoltre il trasferimento avviene sempre in modo incrementale: questo sempre con l’intento di rendere più efficace l'operazione ma anchedi risparmiare il più possibile banda.

Iniziamo con il valutare il secondo parametro dell’istruzione pull: qui è possibile specificare una query Linq che indica quali dati saranno salvati scaricati e salvati sul database locale.

Va da sè che gli altri dati che NON rispettano la query qui specificata saranno semplicemente ignorati.

Il primo parametro dell’istruzione pull invece serve per raffinare gli aggiornamento incrementali.

Se tale parametro è null allora l’operazione di pull NON sarà incrementale: semplicemente tutte le volte riverranno scaricati tutti i dati dal backend.

Oss.: Questo non significa che la base dati locali corre il rischio di avere dati doppi: semplicemente se il record ricevuto dal backend NON esiste allora viene creato, se già esistente se necessario viene aggiornato, ma nulla più.

Pertanto in condizioni normali porre questo parametro a null è inefficiente. Per aumentare l’efficenza dell’operazione usualmente va assegnata una stringa univoca che identifica la sincronizzazione. Quresta stringa prendei l nome di QueryID.

La prima volta che viene eseguita un’operazione di pull viene rilevato il più alto valore del campo UpdateAt, e quindi viene inserito una riga in una tabella di sistema che avrà una struttura simile alla seguente.

 

Nome Tabella
Query ID
Max valore assunto da UpdateAt

 

Alla operazione successiva operazione di pull il client consulterà la tabella sopra e invierà il massimo valore ottenuto per il campo di UpdateAt, e quindi il backend sarà in grado di inviere le sole righe con UpdateAt maggiore del valore ricevuto: questo aumenta ovviamente l’efficenze dell’operazione.

E' bene ripeterlo: anche in questo caso il controller non deve essere in alcun modo modificato per ottenere le funzionalità ora descritte, e lato client è sufficiente usare semplicemente il metodo PullAsync.

Inutile dire che tutto funziona alla grande se si tiene il valore UpdateAt aggiornato: ogni volta che la riga viene modificata il relativo valore deve essere aggiornato con la data e ora corrente. Cosa analoga per le righe in inserimenti.

Se la riga viene inserita o modificata dal backend Azure che usa l'SDK Microsoft.Azure.Mobile.Server, che è quello in uso, questo non rappresenta affatto un problema: il valore viene tenuto aggiornato automagicamente e non occorre fare altro.

Nel caso, invece, i dati siano inseriti o aggiornati da programmi esterni (vedi per esempio procedure di sincronizzazione con database locali o interazione con altri software) occorre provvedere da noi a tenere questo valore aggiornato. Il problema, in realtà, si risolve brillantemente nel 99 % dei casi con un semplice trigger come quello che Vi riporto per completezza nel seguito.

CREATE TRIGGER [dbo].[TR_dbo_FocaccePost_InsertUpdateDelete] ON [dbo].[FocaccePost] 
AFTER INSERT, UPDATE, DELETE AS BEGIN UPDATE [dbo].[FocaccePost] 
SET [dbo].[FocaccePost].[UpdatedAt] = CONVERT(DATETIMEOFFSET, SYSUTCDATETIME()) FROM INSERTED WHERE inserted.[Id] = [dbo].[FocaccePost].[Id] 
END

Osservo che il trigger potrebbe non andare bene in casi di alta concorrenza, ma in ogni caso esisotno svariate possibilità per tenere questo campo aggiornato e ritengo non rappresenti un grsso problema.

Lo stesso campo l'ho aggiunto nel modello della app: non è necessario affinchè tutto funzioni come esposto, ma in questo modo questo campo diventa disponibile localmente per ogni record e quindi posso mostrare all'utilizzatore la data in cui un post è tato modificato l'ultima volta (nel codice in esposzione non l'ho fatto comunque).

Stessa cosa per il campo CreatedAt che l'ho incluso nel modello ma non è funzionalmente indispensabile.

In casi normali su ogni tabella sarà associato un solo identificatvo di query (Query Id): però è possibile anche gestire casi più complessi.

Immaginiamo che gli utilizzatori della app possano essere suddivisi in gruppi, e che nella struttura del modello del post sia incluso un campo gruppo che riporta il gruppo di appartenenza dell'utente che ha inserito il post.

Sarebbe possibile avere la tabella Focacce post aggiornata molto di frequente per i post delle persone facenti parte del mio medesimo gruppo, mentre magari per i post degli altri gruppi potrei creare delle procedure di sincronizzazione diverse perché magari impegnano più tempo. Ecco che quindi è possibile fare una cosa del genere.

public async Task SyncFocacceDBMioGruppo()
 {
     try
     {
  IMobileServiceSyncTable Focacciatable = client.GetSyncTable();
  await Focacciatable.PullAsync("allFocacceMioGruppo", x=>x.Gruppo==GruppoAppartenenza);
     }
     catch (Exception ex)
     {
        Debug.WriteLine("Unable to sync focacce, that is alright as we have offline capabilities: " + ex);
     }
 }

public async Task SyncFocacceDBTuttiIGruppi()
 {
     try
     {
  IMobileServiceSyncTable Focacciatable = client.GetSyncTable();
  await Focacciatable.PullAsync("allFocacceMioGruppo", x=>x.Gruppo!=GruppoAppartenenza);
     }
     catch (Exception ex)
     {
       Debug.WriteLine("Unable to sync focacce, that is alright as we have offline capabilities: " + ex);
     }
 }

Quindi si avranno due tipi di sync: uno più corto per i post creati da appartenenti a utilizzatori del mio stesso gruppo, e una ovviamente più lunga per tutti gli altri.

Per semplicità il codice sopra non è corredato di un controllo preventivo prima di iniziare le operazioni di push e pull per verificare se la connettività è attiva: nella pratica è buona norma, prima di eseguire queste operazioni eseguire sempre dei test per verificare che la connessione all’URL dell’Azure Mobile App sia attiva e funzionante e proseguire con le operazioni solo in caso affermativo.

Come sempre in lokografia trovate il link allo spazio github dove sono riportati i codici di qui parlo.

Linkografia

Git Page: Focac-Book in Xamarin