l'application HasCharacter

Windows Store Apps et leurs données : un exemple

l'application HasCharacterDans mon dernier article, j’ai présenté les options dont dispose une application Windows 8 pour sauvegarder ses données. J’ai également abordé les différentes API et les périodes propices à la sauvegarde. Récemment, j’ai eu l’occasion d’utiliser ces API dans une application réelle et complexe. J’en profite alors pour approfondir selon trois axes :

  1. Présenter un exemple concret des API  à utiliser pour chaque type de données
  2. Détailler les différents moments pour sauvegarder en fonction du type de données
  3. Donner des petites astuces qui vous permettront d’utiliser ces API (souvent mal documentées) sans pépins

Le problème

L’application en question est une bibliothèque de personnages de Donjons & Dragons qui présente des feuilles de personnage interactives. Elle permet de télécharger les personnages d’un compte en ligne D&D Insider ou de les charger à partir des fichiers .dnd4e, en les convertissant dans un format plus pratique pour la visualisation. Elle permet aussi une édition basique de certaines informations (argent, points d’expérience et notes).

Même si le sujet semble relativement simple, pour offrir la meilleure expérience utilisateur possible, j’utilise à peu près la moitié des options présentées dans le 1er article !

Personnages téléchargés : Local Storage immédiat

Chaque personnage correspond à un xml source de 200-400Ko, qui est converti par l’application dans un format plus léger. Ceci dit, ce sont des fichiers complexes et leur sérialisation peut consommer du temps et provoquer des erreurs pour lesquelles nous voulons prévenir l’utilisateur immédiatement et lui permettre de réagir.

95% des informations des personnages n’est pas éditable. Le choix est donc simple : séparer la partie éditable de la non-éditable et sauvegarder le premier immédiatement dans le Local Storage.

Astuce : Le Local Storage va probablement recevoir vos structures les plus complexes. Si vous optez pour une sérialisation, faites attention aux subtilités des références : une structure complexe risque d’avoir plusieurs références au même objet. La méthode standard est d’utiliser la propriété IsReference, mais ce n’est pas la plus simple à implémenter. Si vos fichiers sont uniquement consommés par des applications .NET vous pouvez utiliser la propriété PreserveObjectReferences.

C’est à cause de cette gestion de références (et de certaines raisons de sécurité) que JSON n’est pas le meilleur format pour cette tâche.

Données éditables essentielles : Roaming Settings [Priorité Haute]

Dans l’idéal, si l’utilisateur fait n’importe quelle modification, elle devrait apparaître sur tous ses appareils. En pratique, vous devez prioriser les informations pour les découper en fonction de votre volumétrie et des contraintes de taille de Roaming Settings.

Pour les volumétries attendues par cette application, j’ai pu sauvegarder en Priorité Haute une série d’informations numériques importantes (points d’expérience, dégâts, etc) – ce qui permet à un utilisateur de continuer un combat même en changeant d’appareil.

Astuce 1 : Sachant que nous sommes limités à 8Ko de RoamingSettings en priorité haute, vous devez considérer une sérialisation manuelle de vos objets, pour minimiser la mémoire consommée. Vu que je ne pouvais avoir qu’une seule ApplicationDataCompositeValue en priorité haute, j’ai opté pour avoir une liste de clés-valeurs du style {characterIndex, “0;3;78;0”}.  Après j’ai créé une façade RoamingCharacterData qui permet de simplifier la gestion de ces valeurs. Si, par exemple, je voulais connaitre les points d’expérience d’un personnage,  je pourrais faire :


    new RoamingCharacterData(characterIndex).GetPriority(2)

 

Ici 2 est l’index du nombre demandé dans le tableau en question, c’est à dire 78 dans notre cas. A partir de ça on peut imaginer une classe Character qui contient certaines propriétés qui sont sérialisables (pour sauvegarder dans LocalStorage) et d’autres qui proviennent de RoamingCharacterData (pour sauvegarder dans RoamingSettings).

Astuce 2 : Les API de settings ne présentent pas comment gérer la modification d’une valeur. Dans le cas d’une valeur complexe, il est recommandé de la lire, la recréer avec la modification et écraser l’existante :

    ApplicationDataContainer roamingSettings = ApplicationData.Current.RoamingSettings;
    var priorityData = roamingSettings.Values.ContainsKey("HighPriority")?
                        roamingSettings.Values["HighPriority"] as ApplicationDataCompositeValue :
                        new ApplicationDataCompositeValue();
    //<modifiy priorityData here>

    roamingSettings.Values["HighPriority"] = priorityData;

    

Données éditables non-critiques : Roaming Settings

Et que fait-on avec les 92Ko restants ? Le plus possible ! Dans mon cas j’ai pu les utiliser pour les informations éditables textuelles courtes et pour  les choix utilisateur qui concernent l’interface (exemple : ordre de visualisation des pouvoirs d’un personnage). Ce sont des informations qui ne sont pas critiques pour reprendre l’application depuis un autre appareil.

Librairie et Notes : Local Storage à la suspension

Si vous avez lu les commentaires de SuspensionManager, fourni avec les templates d’application Windows 8, vous savez déjà qu’il est censé sauvegarder des valeurs simples. Oui, vous pouvez sérialiser n’importe quel objet à la suspension d’une application, mais :

  • Vous avez peu de marge de réaction en cas d’erreur.
  • Vous risquez de dépasser le temps alloué par le système pour la suspension de votre application.
  • Vous risquez de perdre ces données en cas d’exception inattendue.

C’est pour ces raisons que dans mon cas j’ai opté pour la sauvegarde de seulement 3 types d’informations :

  1. L’état de navigation (fourni en partie par le template de base, mais incluant aussi dans mon cas deux variables qui me permettent de revenir à l’endroit précis où l’utilisateur était).
  2. La bibliothèque des personnages : une liste d’informations basiques pour lister les 2-40 personnages. Pour une structure plus complexe, vous devez considérer une base de données locale.
  3. Les notes (un champ texte par personnage). Cette donnée était trop grande à inclure dans les données en itinérance. Si vous avez une grosse volumétrie de données de ce type, vous devez considérer une synchronisation à la main (ie sans utiliser RoamingSettings).

Astuce : Si vous optez pour sauvegarder une structure à la suspension au lieu de l’avoir dans une base de données, vous devrez probablement implémenter une résistance aux fautes. Dans mon cas, il suffisait de relire les fichiers stockés dans LocalStorage pour récréer la structure de librairie.

Données éditables sur D&D Insider : Web

Comme déjà mentionné, certaines données (XP, Notes) doivent être synchronisées avec le compte D&D Insider de l’utilisateur. Nous avons déjà présenté comment ces données sont sauvegardées et synchronisées entre les appareils de l’utilisateur, mais il faut aussi appeler les web services de D&D Insider pour mettre à jour les personnages en ligne. Il faut alors mettre en place une infrastructure de suivi de modifications (une date de dernière mise à jour et un champ IsDirty) et aussi appeler les services :

  1. En tâche de fond (quand l’utilisateur n’utilise pas l’application) pour chaque personnage avec IsDirty=true.
  2. Juste avant chaque mise à jour de personnage par le web (premièrement nous uploadons les modifications récentes et après nous re-téléchargeons la totale).

Astuce : Si l’appel de web service à l’étape 2 ne marche pas, il faut faire attention de ne pas écraser les modifications locales avec le contenu qui provient du web.

Identifiants utilisateur : Credentials Locker

Si tous les choix étaient aussi faciles !

Astuce : Si vous  faites un RetrieveAll ou FindBy, les Credentials retournés n’auront pas le  mot de passe rempli ! Voici la bonne méthode pour récupérer un credential :

    var credential = passwordVault.RetrieveAll().FirstOrDefault(c => c.Resource == PrimaryAccountKey);
    if (credential != null)
       credential = passwordVault.Retrieve(PrimaryAccountKey, credential.UserName); //this will populate the password
    

Feedback en fichiers : TempStorage

Quand un xml de personnage présente des problèmes de parsing, je génère un fichier en stockage temporaire et je propose à l’utilisateur de le partager avec le support (ie moi :P). Vous pouvez utiliser le TempStorage aussi pour des logs ou d’autres fichiers volatils.

Astuce : N’oubliez pas de faire en sorte que vos besoins de stockage temporaire n’augmentent pas à l’infini. Vous pouvez nettoyer à intervalle régulier ou écraser les anciens fichiers avec les nouveaux.

Monitoring et Analytics : Web (Google Analytics)

Windows Store garde certains logs sur l’utilisation d’une application, mais cette fois j’ai opté pour plus : Google Analytics for WinRT. Extrêmement simple à configurer, il nous permet de voir la fréquence d’utilisation de certaines fonctionnalités et être informé sur la fréquence de certaines erreurs.

Observation : Etant asynchrone, Google Analytics for WinRT est très léger pendant l’exécution, mais au démarrage j’ai constaté un délai supplémentaire d’environ 10ms.

Astuce : Si vous voulez enregistrer une exception inattendue qui provoque la sortie de l’application, il est plus sûr de ne pas l’envoyer au web immédiatement. Elle risque d’être perdue si l’application ferme avant qu’elle ne soit envoyée. Voici un exemple de ce que l’on peut faire :

Dans App.xaml.cs :

void App_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    try
    {
        ApplicationData.Current.LocalSettings.Values[exceptionKey] = e.ToString();
    }
    catch (Exception)
    {
    }
}

private static void ReportLastUnhandledException()
{
    var localSettings = ApplicationData.Current.LocalSettings.Values;
    if (localSettings.ContainsKey(exceptionKey))
         AnalyticsHelper.Track("Exception", "Unhandled", localSettings[exceptionKey].ToString());

    localSettings.Remove(exceptionKey);
}

Et au moment où vous initialisez l’application, aussi dans App.xaml.cs :

    AnalyticsHelper.Setup();
    ReportLastUnhandledException();

    

Epilogue

S’il y a un point qu’il faut retenir de cet article, c’est que vous avez tous les outils pour répondre à un besoin de façon performante. La difficulté est d’identifier ce besoin correctement. Vous devez savoir, pour chaque type de données, quand les données sont modifiées et quel serait l’impact si elles ne sont pas synchronisées assez rapidement. Pour chaque type de données vous pouvez utiliser l’API (Microsoft ou tiers) correspondante.

A vous de jouer !

Nombre de vue : 199

COMMENTAIRES 1 commentaire

  1. Nicolas Kyriazopoulos-Panagiotopoulos dit :

    Et voilà une petite correction (et une raison additionnelle de sauvegarder très peu d’information à la Suspension):
    http://social.msdn.microsoft.com/Forums/en-US/winappswithcsharp/thread/43da2212-9910-461d-816a-94b479ce9885

AJOUTER UN COMMENTAIRE