Windows 8: occhio a cio’ che fate nei Background Task
Sviluppando una mia app Windows 8 mi è capitata una cosa strana riguardante i Background Task. Se volete capire cosa sono, a cosa servono e come si sviluppano i Background Task vi rimando a questi due post (in inglese) dell’amico Matteo Pagani: post 1 e post 2.
Quello che mi è accaduto è che il debugging effettuato tramite Visual Studio 2012 non è veramente la stessa cosa che accade quando invece il codice gira realmente tramite l’app deployata. Ora vi spiego.
Immaginate di sviluppare un’app che vi fa il countdown relativamente ad un certo evento. Oggi è il 14 Gennaio. Supponiamo di voler impostare un countdown per San Valentino. Il countdown appare sulla tile dell’app stessa. Quindi oggi apparirebbe una cosa tipo “mancano 30 giorni a san valentino”, domani vedreste “mancano 29 giorni a san valentino”, e così via.
In pratica, il Background Task della mia app, che gira ogni 15 minuti, non fa altro che aggiornare la tile riportando il tempo rimanente. Le informazioni sull’evento sono banalmente scritte in un file .txt, che viene creato nel momento in cui scegliete l’evento. Quindi, per riassumere, ecco il codice contenuto nel metodo Run del Background Task.
public async void Run(IBackgroundTaskInstance taskInstance) { BackgroundTaskDeferral deferral = taskInstance.GetDeferral(); try { var container = Windows.Storage.ApplicationData.Current.LocalFolder; var file = await container.GetFileAsync("File.txt"); var lines = await Windows.Storage.FileIO.ReadLinesAsync(file); var header = lines[0]; var time = DateTime.Parse(lines[1]); if (time > DateTime.Now) { var ts = time.Subtract(DateTime.Now); string content = ts.TimeSpanDescription(" all'evento scelto. Non mancare!"); TileHelper.UpdateTileWidePeekImage01("Assets/WideLogo.reminder.png", header, content); } else { TileHelper.UpdateTileWideImage("Assets/WideLogo.scale-100.png"); } } catch (Exception) { } deferral.Complete(); }
Questo è il codice corretto, che gira senza problemi, per cui sa volete prendere ispirazione, fatelo pure. Occhio però a come usate la keyword await, necessaria per eseguire i metodi asincroni. Stupidamente, io in una prima versione del codice non la usavo, ed usavo invece il metodo GetResults(). Per spiegarmi meglio, ecco il pezzetto di codice incriminato:
var lines = Windows.Storage.FileIO.ReadLinesAsync(file).GetResults();
Ripeto il concetto: invece di usare await, ho chiamato GetResults(), convinto – chissà perchè – che facessero la stessa cosa. Purtroppo questa cosa è stata ereditata da un pezzetto di codice scritto mesi fa, quando avevo ancora a che fare con le versioni beta di Windows 8 e Visual Studio 2012 11. La cosa bella è proprio questa: questo codice qui sopra quando eseguito da Visual Studio gira senza problemi (dentro l’oggetto lines avete la List<string> delle righe contenute nel file di testo appena letto), mentre quando gira “in produzione” si schianta. Ci ho litigato per un weekend prima di raggiungere la soluzione, proprio perchè dovevo aspettare realmente 15 minuti prima di verificare il comportamento.
Anche in questo caso, tutto è bene ciò che finisce bene.
Ciao Igor, si schianta perché se il dispatcher resta sospeso per più di 3 secondi, l’app viene automaticamente terminata.
Quando chiami GetResult il thread principale viene bloccato in attesa dei risultati. Se il file non è stato letto precedentemente, è normale che impieghi più tempo e magari su macchine con disco meccanico un po’ datato il tempo può superare i 3 secondi.
Quando invece usi await, il compilatore genera una macchina a stati e un paio di metodi. La chiamata asincrona viene eseguita in un altro thread e tutto il codice sotto la await viene eseguito in una callback. La comodità di await è che la callback viene eseguita nel contesto del thread principale, eliminando la necessità di usare Dispatcher.RunAsync (analogo del vecchio Control.Invoke).
Hai appena scoperto perché hanno reso tutte le chiamte asincrone 🙂