Dot.Blog

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

WinRT : RoamingSettings, quota et Sérialisation

[new:30/09/2013]Dernièrement je suis tombé sur un billet d’un évangéliste Microsoft qui mettait l’accent sur l’importance du stockage itinérant de WinRT. C’est, il est vrai, une feature encore mal exploitée. Je me suis dit qu’en parler cinq minutes ne ferait de mal à personne!

Le stockage itinérant (Roaming)

C’est un des grands avantages de la plateforme unifiée WinRT et pourtant il est peu utilisé par les applications. Parfois on rencontre même des développeurs qui se compliquent la tâche en créant un service Web (ce qui réclame serveur et application) pour qu’un utilisateur puisse retrouver facilement certaines données privées, des réglages de l’application etc.

Or, WinRT comme les autres plateformes possède un mécanisme de stockage centralisé qui est synchronisé automatiquement sur le compte de l’utilisateur lui permettant de retrouver ces fameux réglages d’application ou quelques données privées (en quantité limitée certes) lorsqu’il change de machine. L’espace de cette nature étant compté, pour sauvegarder et synchroniser des données volumineuses il faudra se tourner vers DropBox, Box, Google Drive, SkyDrive ou autres services de Cloud. Mais pour les réglages de base d’une application le système de Roaming est gratuit, facile à utiliser et entièrement automatique.

La bonne utilisation de ce mécanisme rend une application immédiatement plus agréable. Cela créé aussi une surprise favorable chez l’utilisateur, il se logue sur une machine différentes et comme par magie votre logiciel s’ouvre avec les bons réglages ! Je dis bien “magie” car pour un utilisateur de base même assez malin les mécanismes de synchronisation des données itinérantes est quelque chose qui lui échappera techniquement. Et si j’en parle aujourd’hui c’est que cela échappe même souvent aux développeurs !

Proposer une meilleure UX c’est essentiel.

Mais à quel prix ?

Et c’est là que c’est rageant : se servir du stockage itinérant n’est pas plus compliqué qu’autre chose…

Le problème de la sérialisation des données génériques

C’est un problème annexe qui n’a rien à voir directement avec les données itinérantes mais qui se pose souvent aussi dans ce contexte. Autant le régler.

Bref ce problème apparait dans de nombreux cas et donc aussi dans celui de l’utilisation du Roaming car souvent une application doit conserver des éléments de configuration plus complexes qu’un simple integer ou une string.

Stocker par exemple une “List<MonType>” n’est pas directement possible car le mécanisme de sérialisation derrière le Roaming ne sait rien sur la classe “MonType”. Cela fonctionne bien avec des données simples, des types de base, mais pas avec des types complexes.

Une solution évidente : transformer ce qui est complexe en quelque chose de simple !

Du complexe vers le simple

“MonType” est trop complexe pour la sérialisation du RoamingSettings ? Qu’à cela ne tienne ! Transformons le complexe en simple, c’est à dire une string…

Une simple méthode outil comme la suivante fera le travail :

public static string SerializeToString(object obj)
{
   XmlSerializer serializer = new XmlSerializer(obj.GetType());
   using (StringWriter writer = new StringWriter())
   {
      serializer.Serialize(writer, obj);
      return writer.ToString();
   }
} 

 

Tout objet qui lui est proposé sera sérialisé en XML et retourné sur la forme d’une string. Le complexe devient simple.

Du simple vers le complexe

Bien entendu il faut être capable de récupérer les valeurs ainsi “simplifiées” pour réhydrater des instances de classes plus complexes…

La méthode ci-dessous fait l’inverse de la première :

public static T DeserializeFromString<T>(string xml)
{
   XmlSerializer deserializer = new XmlSerializer(typeof(T));
   using (StringReader reader = new StringReader(xml))
   {
      return (T)deserializer.Deserialize(reader);
   }
}

 

A partir d’une chaine contenant la sérialisation d’un objet en XML elle retourne une instance réhydratée. Du simple nous retrouvons le complexe.

Sauvegarder et relire les settings

Armez que nous sommes de ces deux méthodes il est donc possible de sauvegarder un type générique comme “List<MonType>” dans les Settings de l’application (et de les relire bien entendu) :

if (settingsRoaming == null)
   settingsRoaming = ApplicationData.Current.RoamingSettings;
settingsRoaming.Values["myData"] = SerializeToString(myData);

 

La relecture s’écrit alors :

if (settingsRoaming == null)
   settingsRoaming = ApplicationData.Current.RoamingSettings;
if (settingsRoaming.Values["myData"] != null)
   myData = (List<DataTypes.MyDataType>) 
DeserializeFromString<List<DataTypes.MyDataType>>
(settingsRoaming.Values["myData"].ToString()); else myData = new List<DataTypes.MyDataType>(); // init si rien

 

Cette méthode est parfaite, simple, et permet de sauvegarder des données complexes dans le RoamingSettings… mais d’autres aspects sont à prendre en compte.

Les contraintes des données d’itinérance

Lorsqu’on utilise des données d’itinérance il faut garder à l’esprit un certain nombre de choses… La première est que les données peuvent avoir changé après le lancement de l’application, par exemple parce qu’elles n’avaient pas pu être synchronisées avant. Ou même parce que l’application est utilisée ailleurs en même temps.

Pour éviter ce genre de désagrément (qui soulève toutefois d’autres questions) il est possible d’écouter l’évènement DataChanged de ApplicationData. Les questions soulevées peuvent se résumer à la principale “qu’est ce que je fais des nouvelles données s’il y en a qui arrivent après le lancement de l’application ?”.

La réponse est “ça dépend” Sourire

Selon la nature des réglages sauvegardés on pourra tout aussi bien s’en servir immédiatement sans que l’utilisateur ne le voit, parfois (et même assez souvent) ces changements seront visibles et pourront déstabiliser l’utilisateur si on ne le prévient pas. Il faudra donc par exemple afficher un message lui expliquant l’arrivée de nouveaux réglages et lui demander s’il veut les appliquer tout de suite ou non.

Ce qui cache d’autres questions comme “comment l’utilisateur peut-il prendre une telle décision s’il ne sait rien des données sauvegardées, de leur nature, de leur effet sur son travail ?”. Epineux… Une boîte de dialogue ce n’est pas une formation sur le Roaming… ni un catalogue détaillé du fonctionnement de votre application. Parfois prévenir l’utilisateur ne sert à rien car il ne pourra pas comprendre les effets de son choix.

C’est une des bases d’une bonne UX : ne jamais demander à l’utilisateur quelque chose qu’il ne peut raisonnablement pas comprendre facilement… Dans un tel cas, c’est au développeur de faire le moins mauvais choix à la place de l’utilisateur (souvent il n’y a pas de “meilleur” choix, juste un “moins mauvais”…).

Le quota

Tout cela est essentiel mais il ne faudrait pas oublier la plus grande des contraintes du RoamingSettings : c’est la taille autorisée des données qui y sont stockées !

Microsoft gère cette espace par des copies locales mais aussi par un stockage dans le Cloud ce qui permet justement de synchroniser différentes machines. Et la générosité de ce stockage gratuit dans les nuages à une limite, un chiffre résume cette générosité : 100 Ko.

Bien sur c’est peu. Voire ridicule.

Mais d’une part cela est fait pour stocker des paramètres d’application (généralement des entiers, des dates, des chemins de fichiers…) qui ne prennent pas tant de place que cela, et d’autre part, il faut s’imaginer que si des centaines d’applications s’autorisaient plus, de grands débats sur la consommation de la bande passante et le “scandale” d’une telle consommation “à l’insu du plein gréé” de l’utilisateur ne manqueraient d’apparaitre ici et là …

En limitant l’espace à 100Ko Microsoft a bien plus voulu protéger l’utilisateur contre une telle consommation qu’essayer de jouer les radins.

On peut connaître le quota attribué à l’application comme cela :

var quota = ApplicationData.Current.RoamingStorageQuota;

Ne vous laissez pas abuser par cette API dont le résultat est un “ulong” ! Cela retournera 100Ko ni plus ni moins. Mais peut-être qu’un jour cela évoluera-t-il ? …

Quota dépassé ?

Que se passe-t-il quand le quota est dépassé ?

C’est tout simple : les données qui ne “passent pas” sont uniquement stockées en local et ne sont pas synchronisées.

C’est un bug qui peut être très difficile à découvrir !

Surtout que sur une machine de développement et en debug, le quota sera retourné à zéro (pour éviter de synchroniser des logiciels en cours de développement ce qui serait inutile). Il faudra donc anticiper une valeur de 100 Ko de toute façon. Elle peut même être codée en dur dans votre application, si cela change il sera toujours possible d’en tirer partie et de justifier une mise à jour aux utilisateurs ce qui démontrera votre capacité à vous adapter… (et vous fera un peu de pub. Pousser des mises à jours fréquentes et souvent inutiles est une pratique utilisée par de nombreux logiciels pour se “rappeler aux bons souvenirs” de l’utilisateur dans les environnements mobiles… pas très éthique mais ça existe…).

Prendre en compte le quota

Comme il est difficile de prévoir la taille exacte des données sauvegardées et comme la synchronisation partielle par dépassement de quota est un bug sournois mieux vaut prévenir que guérir…

En s’inspirant du code montré plus haut dans ce billet on peut écrire :

private static string SerializeToString(object obj)
{
    XmlSerializer serializer = new XmlSerializer(obj.GetType());
    using (StringWriter writer = new StringWriter())
    {
        serializer.Serialize(writer, obj);
        return writer.ToString();
    }
}
 
public void Save()
{
    ApplicationDataContainer settingsRoaming =
        ApplicationData.Current.RoamingSettings;
    this.LastModified = DateTime.Now;
    string serializedData = SerializeToString(this);
 
    if ((ulong)UnicodeEncoding.Unicode.GetByteCount(serializedData) <=
        ApplicationData.Current.RoamingStorageQuota*1024)
    {
        settingsRoaming.Values["appSettings"] = serializedData;
    }
    else
    {
        //handle the situation
    }
}

Les limites dans la limite…

Mais les choses ne sont pas si simples hélas. Il ne faut pas oublier que dans la limite des 100Ko se cache d’autres limites…

Notamment un paramètre dans le RoamingSettings ne peut dépasser 8Ko… Fâcheux si on s’appuie sur une sérialisation XML dont on ne sait rien de la taille finale à l’avance (comme la fameuse “List<MonType>” discutée plus haut).

Heureusement il existe les settings “composites”. Leur taille peut atteindre 64 Ko, ce qui de toute façon se rapproche de la limite fatidique des 100ko pour la totalité des paramètres.

Leur utilisation complique un peu les choses, pas tant techniquement (le code ci-dessous n’est pas complexe) que conceptuellement (comment construire un composite depuis l’ensemble des paramètres à sauvegarder, quels paramètres regrouper, etc ?).

// Composite setting
Windows.Storage.ApplicationDataCompositeValue composite = 
   new Windows.Storage.ApplicationDataCompositeValue();
composite["intVal"] = 1;
composite["strVal"] = "string";

roamingSettings.Values["exampleCompositeSetting"] = composite;

 

Conclusion

Il est difficile, voire impossible de prévoir la taille exacte des données itinérantes. Une date est stockée sur 8 octets mais dans le Roaming est-elle sérialisée par WinRT et sous quelle forme ? Les textes sont encodés en unicode avec 2 octets par charactères ou par une autre forme d’encodage ? Bien des questions se posent alors même que la limite des 100Ko est un couperet. Il ne faut pas oublier non plus que le stockage itinérant peut contenir des fichiers et des sous-répertoires … comment cela est-il comptabilisé dans les 100Ko ?

La conclusions a en tirer c’est qu’aux extrêmes on sait mais pas au milieu… je veux dire que si on stocke trois entiers et une chaîne de caractères on sait qu’on sera largement dans les limites de 100Ko, de même si on veut stocker une liste d’images on sait qu’on serait en dehors des clous immédiatement. Mais dans beaucoup de cas on se trouve entre les deux… Comment décider, comment savoir pour éviter le bug ? Il faudra tester à fond l’application comme d’habitude et intégrer dans vos tests celui du dépassement de quota du RoamingSettings…

Certains tenteront d’utiliser JSON au lieu de XML pour sérialiser “à l’économie”. Dans la pratique je n’ai jamais vu de différence énorme (sauf dans les exemples “pro” JSON, mais je parle de pratique pas de propagande). D’autres seront tentés de zipper le résultat, mais pour le stocker en chaîne il faudra l’encoder en Base64 ce qui fait perdre beaucoup de place… Bref on peut tourner le problème comme on veut être certain de ne pas dépasser la limite de 100 Ko est très difficile à faire dans certains cas.

Mais que cela ne vous décourage pas d’utiliser les données itinérantes de WinRT, cela peut grandement participer à l’attrait que suscitera votre application…

Stay Tuned !

blog comments powered by Disqus