Monday, March 7, 2011

Le tombstoning et MVVM dans une application Windows Phone 7 (sorry this one is in French)

Le développement d’applications Silverlight pour Windows Phone est attractif si on a déjà écrit du code Silverlight ou WPF. On se sent tout de suite en confiance avec le XAML, le data binding, les appels asynchrones qui si familiers à Silverlight, etc.
Certains  aspects spécifiques à WP7, cependant, requièrent une attention particulière, car ils peuvent s’avérer quelque peu déstabilisants pour le nouveau venu, en particulier la navigation et le lifecycle.  J’aborderai les aspects de navigation dans un billet ultérieur, mais pour l’heure, penchons-nous un moment sur le lifecycle, c’est-à-dire le cycle de vie de l’application.
Comme cela a été répété un peu partout, l’OS de WP7 gère le mutitâche d’une manière particulière, il ne permet pas à une application tierce (votre application!) de tourner en tâche de fond. Donc, si une interface d’une application n’est pas visible à l’écran, cela signifie généralement (sauf quelques exceptions) qu’elle a cessé de s’exécuter! Pas d’exécution en background thread pendant qu’une autre appli s’affiche. C’est valable également si on affiche le menu de démarrage, ou qu’un appel entrant vient interrompre le travail.
Pourtant, lorsqu’on utilise une appli sur WP7, si on presse le bouton Windows pour revenir au démarrage, puis on lance une autre appli, puis on revient à la première appli en utilisant le bouton de retour arrière, on dirait bien que la première appli est telle qu’on l’a quittée, à la même page et avec les mêmes données éventuellement saisies...  Tout est fait pour que l’utilisateur ait l’impression que la première appli a continué à tourner en tâche de fond pendant qu’il faisait un saut dans la deuxième appli (et éventuellement une troisième etc).
En réalité, l’appli est désactivée, et (en général) son processus est terminé. L’application est “tombstoned”, c’est-à-dire mis dans la tombe (sinistre métaphore…). Bonne nouvelle cependant, lorsqu’on revient à l’appli après une escapade ailleurs, elle est ressusitée par l’OS. Mais si le processus et l’état mémoire de l’appli sont ressussités, les données qui étaient dans l’appli au moment de la mise en terre, elles, ne le sont pas. C’est à vous, pauvre développeur, de vous charger de cet aspect.
Comment cela fonctionne-t-il ? Le cycle de vie de l’application démarre au lancement depuis la liste des applis ou son icone dans la page de démarrage. L’évènement correspondant levé par l’OS est “Launching”. Lorsque l’appli est ensuite “tombstonée”, par exemple si l’utilisateur presse la touche Windows, l’évenement “Deactivated” est levé. Lorsque l’utilisateur revient dans l’appli apres le tombstoning grâce au bouton Retour (en supposant que l’appli n’ait pas été définitivement tuée, ce qui peut arriver dans certaines situations), l’évènement “Activated” est levé. Enfin, l’évènement “Closing” est généré si l’appli est quittée pour de bon, généralement lorsque l’utilisateur presse le bouton Retour jusqu’à la première page puis une fois supplémentaire pour quitter l’appli. Dans ce cas, l’appli n’est pas désactivée ni tombstonée mais bel et bien terminée.
Tous ces évènements sont exposés par la classe PhoneApplicationService, et des squelettes de gestionnaires sont prêts à l’emploi dans le fichier App.xaml.cs des différents templates Visual Studio pour WP7.  On utilise ces gestionnaires pour enregistrer les données que l’on souhaite au moment d’un tombstoning (évenement Deactivated), et pour les restaurer au retour du tombstoning (évènement “Activated”). Pour les enregistrer, on peut stocker ces données dans un dictionnaire accessible via l’objet PhoneApplicationService.State, qui est maintenu en mémoire pendant la durée du tombstoning, alors que l’appli est arrêtée. Attention, ce n’est pas du stockage permanent mais temporaire, il y a des risques que ces données ne soient jamais restaurées car il n’est pas garanti que l’appli sortira de l’état tombal. Pour les données à persister de manière permanente, on peut utiliser le isolated storage (mais cela fera l’objet de billets séparés).
En avant pour un exemple. Prenons une application basique qui affiche une liste de plats culinaires. Je crée un projet utilisant le template “Windows Phone Databound Application”.  Dans le projet, je crée une classe Plat :
    public class Plat
    {
        public string Nom { get; set; }
        public string Recette { get; set; }
        public byte NoteSurDix { get; set; }
    }
Dans le constructeur de mon MainViewModel, j’initialise la propriété Items avec une ObservableCollection<Plat> :
        public MainViewModel()
        {
            this.Items = new ObservableCollection<Plat>();
        }
et je crée quelques données de runtime :
public void LoadData()
{
// Sample data; replace with real data
this.Items.Add(new Plat() { Nom = "Tétine de vache", Recette="Rien que du bon", NoteSurDix=9 });
this.Items.Add(new Plat() { Nom = "Poitrine de cochon", Recette = "Un peu de cochon, beaucoup de gras", NoteSurDix = 5 });
this.Items.Add(new Plat() { Nom = "Couscous", Recette = "semoule, légumes, viande, tabasco!", NoteSurDix = 7 });
       this.IsDataLoaded = true;
}
J’ai donc mon MainViewModel qui contient une collection de plats, et cette liste est générée au lancement de l’appli.  Au moment ou l’appli est désactivée, je sauvegarde mon MainViewModel tout entier et ses données, grâce au gestionnaire d’évènement dans app.xaml.cs :
        private void Application_Deactivated(object sender, DeactivatedEventArgs e)
        {
            PhoneApplicationService.Current.State["monViewModel"] = viewModel;
        }
A la réactivation de l’appli (si réactivation il y a), je restaure l’état de mon MainViewModel depuis le cache mémoire temporaire :
private void Application_Activated(object sender, ActivatedEventArgs e)
{
if (PhoneApplicationService.Current.State.ContainsKey("monViewModel"))
viewModel = PhoneApplicationService.Current.State["monViewModel"] as MainViewModel;
if (!App.ViewModel.IsDataLoaded)
       {
              App.ViewModel.LoadData();
       }
}

Donc, à la réactivation, comme le viewmodel a été sauvegardé au moment de la désactivation, il est restauré en mémoire dans l’appli et repart comme en 14. S’il a été sauvegardé et restauré correctement, la valeur de sa variable IsDataLoaded (definie dans le template) devrait être true, car les données ont déjà été chargées dans le viewmodel par la vue (MainPage.xaml.cs) au démarrage de l’appli:
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
if (!App.ViewModel.IsDataLoaded)
       {
              App.ViewModel.LoadData();
       }
}
… et on a vu que LoadData donne la valeur false = IsDataLoaded avant de finir.
Si lance l’appli, les données s’affichent :
clip_image002[4]
Jusque là, tout va bien. Ensuite, je presse le bouton Windows pour désactiver l’appli. Dans le debugger, je vois le gestion de Deactivated s’éxécuter.

clip_image004[4]
Puis, je presse le bouton Retour pour revenir à l’appli. Le gestionnaire de Activated s’exécute.  Cependant, si je mets un point d’arrêt dans le gestionnaire, je me rends compte que ls données sont rechargés avec un appel à LoadData(), ce qui n’est pas le résultat escompté :
clip_image006[4]
La raison est que l’état du viewmodel n’a pas été sauvegardé correctement. En cherchant un peu plus, on retrace la raison à la sérialisation de l’objet. En effet, l’OS utilise un DataContractSerializer par défault pour écrire notre MainViewModel et son contenu au format xml, et stocker le fichier de manière temporaire.
Or, quelque chose gêne la sérialisation : il s’agit du mot clé “private” utilisé pour les Set de deux propriétés, la propriété Items et la propriété IsDataLoaded :
public ObservableCollection<Plat> Items { get; private set; }
public bool IsDataLoaded { get; private set; }
Au point d’arrêt si dessus, si on vérifie les valeurs de Items et IsDataLoaded de App.ViewModel, on voit que IsDataLoaded est false, et Items.Count = 0. Si on modifie les Set pour les rendre public (en supprimant le mot private pour les 2 propriétés), tout rendre dans l’ordre :
clip_image008[4]
La ligne LoadData() n’est plus executée à la réactivation, car IsDataLoaded est true telle qu’elle l’était à la désactivation.
Récapitulons : il est relativement facile de persister l’état actuel de l’application au moment de la désactivation, et de le restaurer au moment de la réactivation, si celle-ci a lieu. Il est important de se rappeller que cette persistance est temporaire, car si l’application est fermée, l’état est perdu. Lorsqu’on sauvegarde des données temporaires dans l’object State de PhoneApplicationService ou de Page, l’OS utilise un DataContractSerializer pour stocker les données au format XML. Il est donc nécessaire de s’assurer que les membres des classes contenant les données en question soient toujours publiques et possèdent un type qui est sérialisable. Dans le cas contraire, attendez-vous à des résultats imprévisibles, ou même des exceptions (donc le message est souvent trompeur).
Joyeux codage!

No comments:

Post a Comment