Dot.Blog

C#, XAML, Xamarin, UWP/Android/iOS

Silverlight : L’Isolated Storage en pratique

[new:31/05/2011]Je vous ai déjà parlé en détail de l’Isolated Storage de Silverlight dans le billet “Silverlight : Isolated Storage et stockage des préférences utilisateur”, je voudrais vous présenter aujourd’hui quelques classes “helper” pour tirer partie rapidement des nombreuses possibilité de l’IS dans vos applications.

L’Isolated Storage

Je ne reviendrai pas sur les détails déjà exposés dans le billet cité plus haut auquel je renvoie le lecteur désireux d’en savoir plus sur ce “stockage isolé”. Mais en deux mots rappelons que l’Isolated Storage est une zone de stockage locale (côté client) dédié à chaque application Silverlight. L’application n’a pas besoin de droits particuliers pour y accéder. L’I.S. a aussi l’avantage de pouvoir globalisé pour un nom de domaine.

C’est à dire que vous pouvez stocker des données spécifiques à l’application en cours ou bien spécifique à tout un domaine, par exemple www.e-naxos.com. Dans ce cas toutes les applications Silverlight qui proviendront de ce domaine pourront partager (lecture / écriture) les données stockées.

On peut utiliser cet espace “domaine” pour stocker les préférences utilisateur qui seront valable pour toutes les applications du domaine (choix de la langue, préférence de couleurs, etc.).

L’I.S. est limité en taille par défaut, et si une application désire plus d’espace elle doit le demander à l’utilisateur qui peut refuser.

Autre aspect : l’I.S. bien que “caché” dans les profondeur des arborescences de l’hôte est tout à fait accessible, il est donc fortement déconseillé d’y stocker des valeurs de type mot de passe, chaine de connexion ou autres données sensibles (en tout cas sans les crypter).

SI le stockage des préférences de l’utilisateur est une fonction principale de l’I.S. il ne faut pas oublier d’autres utilisations comme le stockage de données en attente de synchronisation avec un serveur par exemple, l’exploitation de l’espace I.S. comme d’un cache pour des données distantes longues à télécharger (images, fichiers de données, voire DLL ou Xap annexes d’une application modulaire...).

La gestion de l’espace

Parmi les fonctions de base indispensable à une bonne gestion de l’I.S. il y a celles qui gèrent l’espace disponible.

La classe helper suivante permet ainsi d’obtenir l’espace libre dans l’I.S. de l’application en cours, l’espace actuellement utilisé et de faire une demande d’extension de taille à l’utilisateur.

// Classe      : IsManager
// Description : Manages Isolated Storage space
// Version     : 1.0
// Dev.        : Olivier DAHAN - www.e-naxos.com
// ----------------------------------------------
 
 
using System.IO.IsolatedStorage;
using GalaSoft.MvvmLight;
 
namespace Utilities
{
    /// <summary>
    /// Simple Isolated Storage Manager.
    /// Mainly to manage IS space.
    /// </summary>
    public static class IsManager
    {
        /// <summary>
        /// Gets the free space.
        /// </summary>
        /// <returns></returns>
        public static long GetFreeSpace()
        {
            if (ViewModelBase.IsInDesignModeStatic) return 0L;
            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                return store.AvailableFreeSpace;
            }
        }
 
        /// <summary>
        /// Gets the used space.
        /// </summary>
        /// <returns></returns>
        public static long GetUsedSpace()
        {
            if (ViewModelBase.IsInDesignModeStatic) return 0L;
            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                return store.UsedSize;
            }
        }
 
        /// <summary>
        /// Extend IS quota.
        /// </summary>
        /// <param name="bytes">The bytes.</param>
        /// <returns></returns>
        public static bool ExtendTo(long bytes)
        {
            if (ViewModelBase.IsInDesignModeStatic) return false;
            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                return store.IncreaseQuotaTo(bytes);
            }
        }
 
    }
}

On notera que la classe proposée est statique, comme ses méthodes qui s’utilisent donc directement. L’unité de toutes les méthodes est l’octet.

Les paramètres utilisateurs

Une fois l’espace géré, l’utilisation la plus fréquente de l’I.S. est le stockage des préférences utilisateurs. Je vous propose ici une petite classe qui implémente la pattern Singleton et autorise la lecture et l’écriture de tout paramètre.

C’est un singleton et non une classe statique pour une raison simple : le binding Xaml ne fonctionne pas sur les statiques... Il faut ainsi une instance pour faire un binding sous Silverlight. La classe créée une instance d’elle-même accessible depuis un point d’entré connu (le singleton) ce qui permet de binder directement les champs d’une Vue sur la classe ISParameter (qui est alors un Modèle. Sous MVVM la liaison directe Vue/Modèle sans passer par un ViewModel – Modèle de Vue – est parfaitement licite).

namespace Utilities
{
    /// <summary>
    /// Isolated Storage Manager for user's parameters 
    /// </summary>
    public class ISParameters
    {
        #region Singleton + init
        private static readonly ISParameters instance = new ISParameters();
        /// <summary>
        /// Gets the instance.
        /// </summary>
        /// <value>The instance.</value>
        public static ISParameters Instance { get { return instance; } }
        private ISParameters()
        {
            // init defaut values
            InitDefaultParameterValues();
        }
 
        /// <summary>
        /// Inits the default parameter values.
        /// </summary>
        public void InitDefaultParameterValues(bool factoryReset=false)
        {
            if (factoryReset)
            {
                if (userSettings.Contains("PARAM1")) userSettings.Remove("PARAM1");
            }
 
            if (!userSettings.Contains("PARAM1"))
                userSettings.Add("PARAM1", TimeSpan.FromSeconds(30));
 
            userSettings.Save();
        }
 
        #endregion
 
        #region fields
        private readonly IsolatedStorageSettings userSettings = IsolatedStorageSettings.ApplicationSettings;
        #endregion
 
        #region public methods
        /// <summary>
        /// Gets the parameter.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name">The name.</param>
        /// <returns></returns>
        public T GetParameter<T>(string name)
        {
            try
            {
                return userSettings.Contains(name)
                           ? (T) userSettings[name]
                           : default(T);
            } catch(Exception e)
            {
                ISLogger.Log(e.Message,LogLevel.Error);
                return default(T);
            }
        }
 
        /// <summary>
        /// Stores the parameter.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="value">The value.</param>
        public void StoreParameter(string name, object value)
        {
            try
            {
                if (userSettings.Contains(name))
                    userSettings[name] = value;
                else userSettings.Add(name, value);
                userSettings.Save();
            }
            catch(Exception e)
            {
                ISLogger.Log(e.Message, LogLevel.Error);
            }
        }
        #endregion
    }
}

Le principe d’utilisation de la classe est le suivant : on utiliser “StoreParameter” pour stocker la valeur d’un paramètre dans l’I.S., on utilise la méthode “GetParameter” pour lire un paramètre. Cette dernière est générique ce qui permet de récupérer le paramètre directement dans son type natif (il faut le connaitre mais c’est généralement le cas).

Lorsqu’on gère des paramètres dans une application il y a forcément des valeurs par défaut. C’est la raison d’être de la méthode “InitDefaultParemeterValues” qui est appelée par le constructeur.

Il est aussi nécessaire de permettre à l’utilisateur de remettre les paramètres à leur valeur par défaut. C’est pourquoi cette même méthode est publique et peut ainsi être invoquée à tout moment pour rétablir l’ensemble des paramètres à leur valeur “usine”. Le paramètre “factoryReset” est là pour forcer la remise à défaut justement (sinon la procédure ne fait qu’écrire les valeurs par défaut lorsqu’elles n’existent pas).

A noter : il faut modifier manuellement cette méthode pour y ajouter tous les paramètres de l’application ainsi que leur valeur par défaut. Le code ne comporte qu’un exemple pour le paramètre “PARAM1” qui serait de type TImeSpan. Il suffit de recopier ce code autant de fois que nécessaire et de l’adapter à chaque paramètre.

Cette contrainte à un avantage : centraliser en un seul point la liste de tous les paramètres...

Enfin, on remarquera que les exceptions sont gérées et font appel à une mystérieuse classe ISLogger. Vous pouvez supprimer cette gestion si vous le désirez, sinon passons à la troisième classe qui se trouve être la fameuse ISLogger...

La gestion des Logs

Gérer des logs est le propre de toute application pour en simplifier le debug. On pourrait intégrer quelque chose comme Log4Net, mais il n’y a pas de version Silverlight. Certes on peut utilise Clog (voir sur Codeplex) pour envoyer les messages au serveur via WCF. Mais si l’application plante, logger des messages en utilisant un mécanisme aussi complexe a peu de chance de fonctionner à tous les coups (par exemple si justement ce sont les connexions réseaux qui ne marchent pas !).

Dans mes applications je résous le problème de deux façons : en utilisant un logger simplifié qui stocke les informations dans l’I.S. et un autre mécanisme qui utilise les connexions de l’applications. Par exemple WCF Ria Service ou un simple Web Service selon les cas. Je créé alors un Logger global qui s’occupe de stocker l’erreur en local en premier puis qui tente de l’envoyer au serveur. Comme cela les erreurs sont reportées au serveur si c’est possible, mais si cela ne l’est pas je dispose d’une trace utilisable sur chaque client. Le fichier de log local peut être présenté à l’écran ou bien sauvegardé ailleurs, mais il ne s’agit plus que de présentation sous Silverlight.

La partie qui nous intéresse ici se concentre sur le Log local stocké dans l’I.S.

Rien ne sert de faire compliqué. On pourra toutefois sophistiquer un peu plus la classe suivante pour lui permettre de gérer les InnerException s’il y en a. Cela peut être pratique. J’ai préféré vous présenter la version simple, à vous de l’améliorer selon vos besoins (idem pour le formatage du fichier de log qui est ici un simple fichier texte, on peut concevoir des variantes en XML par exemple).

Notre petit logger a besoin d’une énumération qui indique la gravité du message :

namespace Utilities
{
    /// <summary>
    /// Log Level
    /// </summary>
    public enum LogLevel
    {
        /// <summary>
        /// Information (lowest level)
        /// </summary>
        Info = 1,
        /// <summary>
        /// Warning (perhaps a problem)
        /// </summary>
        Warning = 2,
        /// <summary>
        /// Error (exceptions not stopping the application)
        /// </summary>
        Error = 3,
        /// <summary>
        /// Fatal errors (exceptions stopping the application, should be log to the server if possible)
        /// </summary>
        Fatal = 4
    }

Le code qui suit n’exploite pas réellement de filtrage sur le niveau de log, à vous de compliquer les choses selon vos besoins !

using System;
using System.Collections.Generic;
using System.IO.IsolatedStorage;
using System.IO;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Messaging;
 
namespace Utilities
{
 
    /// <summary>
    /// Simple logging class using Isolated Storage to keep track of exceptions and important messages.
    /// </summary>
    public static class ISLogger
    {
 
        private const string LOGNAME = "TenorLight.log";
 
        /// <summary>
        /// Logs the specified message.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <param name="logLevel">The log level.</param>
        public static void Log(string message, LogLevel logLevel)
        {
            if (IsManager.GetFreeSpace() < (message.Length * 3))
            {
                Messenger.Default.Send(new NotificationMessage("ISFULL"));
                return;
            }
            if (!ViewModelBase.IsInDesignModeStatic)
                try
                {
                    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
                    {
                        using (Stream stream = new IsolatedStorageFileStream(LOGNAME, FileMode.Append, FileAccess.Write, store))
                        {
                            var writer = new StreamWriter(stream);
                            switch (logLevel)
                            {
                                case LogLevel.Info:
                                    writer.Write(String.Format("{0:u} [INFO] {1}{2}", DateTime.Now, message, Environment.NewLine));
                                    break;
                                case LogLevel.Warning:
                                    writer.Write(String.Format("{0:u} [WARNING] {1}{2}", DateTime.Now, message, Environment.NewLine));
                                    break;
                                case LogLevel.Error:
                                    writer.Write(String.Format("{0:u} [ERROR] {1}{2}", DateTime.Now, message, Environment.NewLine));
                                    break;
                                case LogLevel.Fatal:
                                    writer.Write(String.Format("{0:u} [FATAL] {1}{2}", DateTime.Now, message, Environment.NewLine));
                                    break;
                                default:
                                    break;
                            }
                            writer.Close();
                        }
                    }
                }
                catch (Exception e)
                {
                    Messenger.Default.Send(new NotificationMessage<Exception>(e, "LOGERROR"));
                }
        }
 
        /// <summary>
        /// Gets the log.
        /// </summary>
        /// <returns></returns>
        public static List<string> GetLog()
        {
            var li = new List<string>();
            if (!ViewModelBase.IsInDesignModeStatic)
                try
                {
                    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
                    {
                        using (
                            var stream = new IsolatedStorageFileStream(LOGNAME, FileMode.OpenOrCreate, FileAccess.Read,
                                                                       store))
                        {
                            var reader = new StreamReader(stream);
                            string s;
                            while ((s = reader.ReadLine()) != null) li.Add(s);
                            reader.Close();
                        }
                    }
                }
                catch (Exception e)
                {
                    Messenger.Default.Send(new NotificationMessage<Exception>(e,"LOGERROR"));
                }
            return li;
        }
 
        /// <summary>
        /// Clears the log.
        /// </summary>
        /// <returns></returns>
        public static bool ClearLog()
        {
            if (!ViewModelBase.IsInDesignModeStatic)
                try
                {
                    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
                    {
                        if (store.FileExists(LOGNAME)) store.DeleteFile(LOGNAME);
                        return true;
                    }
                }
                catch
                {
                    return false;
                }
            return false;
        }
 
        /// <summary>
        /// Lines the count.
        /// </summary>
        /// <returns></returns>
        public static int LineCount()
        {
            var log = GetLog();
            return log.Count;
        }
 
    }
}

Le logger s’utilise de la façon la plus simple, c’est une classe statique. Pour logger un message on utilise “ISLogger.Log(monmessage,leniveau)”.

Quelques méthodes utiles complètent la classe : LineCount() qui retourne le nombre de lignes actuellement dans le log, ClearLog() qui efface le log, et GetLog() qui retourne le log sous la forme d’une grande chaine de caractères.

Avec ça on dispose d’une base pratique sur laquelle on peut “broder” pour l’adapter à toutes les situations.

Une première amélioration serait d’ajouter un lock pour éviter les collisions dans une application multi thread.

Vous remarquerez que certaines méthodes utilisent la messagerie MVVM Light en cas d’exception. Comme nous sommes dans la classe Logger la question se pose de savoir qui “logue les logs du loggers” ? !

Dans cet exemple les exceptions gérées par le logger lui-même sont simplement retransmises à l’application via une notification, un message MVVM Light. Les Vues peuvent intercepter ces messages et afficher une boite de dialogue ou ajouter le message à une liste ou autre mécanisme afin que l’utilisateur soit averti.

Si vous n’utilisez pas MVVM Light vous pouvez supprimer les appels à Messenger bien entendu. Il faut alors décider si vous laisserez les bloc try/catch vide (pas toujours très malin de cacher les erreurs) ou bien si vous substituerez les appels à Messenger par un autre mécanisme propre à votre application ou bien encore si vous déciderez de supprimer les blocs try/catch pour laisser les exceptions remonter vers l’application (ce qui permet d’en prendre connaissance).

Le stockage de fichiers

Enfin, l’I.S. peut être utilisé comme un cache local par exemple pour éviter à l’utilisateur d’avoir à supporter les attentes du téléchargement d’éléments externes lorsque ceux-ci ont déjà été chargés une fois (images utilisées par l’application mais non intégrées au XAP par exemple).

La classe statique suivante propose deux méthodes très simples : l’une pour stocker des données dans un fichier, l’autre pour les lire.

L’exemple traite les données sous la forme d’une grande chaine de caractères. Bien entendu c’est un exemple, vous pouvez gérer des arrays de Bytes par exemple pour lire et stocker des fichiers binaires (jpeg, png, wav, mp3 ...).

Il suffit d’adapter les deux méthodes selon vos besoins. Dans la version présentées on peut lire et écrire des fichiers texte simplement (txt, Xml, Xaml...).

using System.IO.IsolatedStorage;
using System.IO;
 
namespace Utilities
{
    public static class ISHelper
    {
        public static void StoreIsolatedFile(string path, string data)
        {
            var iso = IsolatedStorageFile.GetUserStoreForApplication();
            using (var isoStream = new IsolatedStorageFileStream(path, FileMode.Create, iso))
            {
                using (var writer = new StreamWriter(isoStream))
                {
                    writer.Write(data);
                }
            }
        }
 
        public static string ReadIsolatedFile(string path)
        {
            var iso = IsolatedStorageFile.GetUserStoreForApplication();
            using (var isoStream = new IsolatedStorageFileStream(path, System.IO.FileMode.Open, iso))
            {
                using (var reader = new StreamReader(isoStream))
                {
                    return reader.ReadToEnd();
                }
            }
        } 
    }
}

 

Conclusion

Rien de bien savant aujourd’hui, du pratique et de l’efficace, qui ne fait pas grossir inconsidérément la taille de votre application Silverlight !

Il est toujours possible de gérer tout cela plus finement, mais un XAP qui n’en fait pas trop est un XAP qui reste petit, et donc qui se télécharge vite, ce que l’utilisateur apprécie toujours !

Stay Tuned !

blog comments powered by Disqus