Ecriture dans l’isolated Storage d’un fichier image
Il est parfois fort utile de sauvegarder les images provenant d’un serveur HTTP dans un cache afin d’éviter de les charger à chaque fois.
En implémentant ce système de cache en asynchrone, je me suis rendu compte de plusieurs problèmes causant systématiquement un UnauthorizedAccessException: Access is denied (0x80070005).
Le problème rencontré intervient lorsque plusieurs images d’une liste essaient d’enregistrer des éléments dans l’Isolated Storage en même temps.
Pourtant le code me paraissait plutôt correct ?
public async Task<string> SaveImageInStorageAsyncBad(string urlImage, object datas) { if (!String.IsNullOrEmpty(urlImage) && datas is byte[]) { string id = urlImage; var localFolder = Windows.Storage.ApplicationData.Current.LocalFolder; var folder = await localFolder.CreateFolderAsync("DataCache", Windows.Storage.CreationCollisionOption.OpenIfExists); var file = await folder.CreateFileAsync(id, Windows.Storage.CreationCollisionOption.ReplaceExisting); var stream = await file.OpenStreamForWriteAsync(); using (var outStream = stream) { var dataWriter = new Windows.Storage.Streams.DataWriter(outStream.AsOutputStream()); dataWriter.WriteBytes(datas as byte[]); await dataWriter.StoreAsync(); return id; } } else return String.Empty; }
FlushAsync()
La première chose qu’il manque est un await dataWriter.FlushAsync() juste après le StoreAsync().
En effet, nous utilisons un Stream pour écrire l’image.
Le Flush permet de s’assurer que tout le stream va être écrit correctement avant de continuer.
Verrou – Lock ?
Mais le Flush ne suffit pas l’erreur est encore rencontrée. Il va falloir protéger le code en évitant une écriture parallèle des images.
En mode Synchrone un Lock aurait pû être utilisé mais nous somme dans un traitement asynchrone.
L’utilisation du Lock est interdite car empêche le thread de continuer. Le compilateur l’indique clairement.
Pour assurer une écriture à la fois, nous allons utiliser l’objet SemaphoreSlim.
Il faut initialiser cet objet de la façon suivante :
private SemaphoreSlim _syncLock = new SemaphoreSlim(1);
Le 1 en paramètre indique que nous ne souhaitons q’un seul accès concurent.
Pour blocker les accès concurent on appelle la méthode :
await _syncLock.WaitAsync();
Pour rendre le block
_syncLock.Release();
Le code final qui fonctionne sans erreur ressemble maintenant à ceci :
A noter : l’utilisation du try … finally pour assurer le déverrouillage même en cas d’anomalies.
public async Task<string> SaveImageInStorageAsync(string urlImage, object datas) { if (!String.IsNullOrEmpty(urlImage) && datas is byte[]) { string id = GetIdStorage(urlImage, savedName); await _syncLock.WaitAsync(); try { var localFolder = Windows.Storage.ApplicationData.Current.LocalFolder; var folder = await localFolder.CreateFolderAsync("DataCache", Windows.Storage.CreationCollisionOption.OpenIfExists); var file = await folder.CreateFileAsync(id, Windows.Storage.CreationCollisionOption.ReplaceExisting); var stream = await file.OpenStreamForWriteAsync(); using (var outStream = stream) { var dataWriter = new Windows.Storage.Streams.DataWriter(outStream.AsOutputStream()); dataWriter.WriteBytes(datas as byte[]); await dataWriter.StoreAsync(); await dataWriter.FlushAsync(); outStream.Dispose(); return id; } } finally { _syncLock.Release(); } } else return String.Empty; }
Voici donc un mécanisme fort utile pour améliorer la réactivité de vos applications Windows et Windows Phone