1. Introduction▲
Windows Phone 7 (WP7) est la nouvelle plateforme de développement de Microsoft destinée aux smartphones.
Si vous découvrez la programmation WP7, n'hésitez pas à consulter l'article précédent : tutoriel d'introduction au développement d'applications Windows Phone 7.
Dans ce deuxième tutoriel, vous découvrirez la puissance du contrôle ListBox WP7 et comment il s'adapte au développement sur téléphone.
Vous verrez aussi comment traiter des événements simples, comme un clic sur un bouton par exemple.
Vous verrez enfin comment faire persister de l'information entre les différents démarrages du téléphone et lorsque l'application est suspendue.
2. Utiliser la ListBox WP7▲
Je vous ai indiqué dans le tutoriel précédent que les contrôles standards ont été réécrits pour tirer parti des fonctionnalités du téléphone.
C'est le cas de la ListBox qui prend en charge automatiquement un effet classique dans les téléphones : le défilement vertical.
Vous allez voir que le rendu est plutôt élégant et qu'il n'y a rien à faire…
Commençons par créer une nouvelle application comme on l'a déjà fait dans le tutoriel précédent : Fichier -> Nouveau projet -> Application Windows Phone. J'appelle cette application : ListBoxDemo.
Nous allons développer une application toute simple de « todo list ».
Dans le Xaml, il s'agit de rajouter un TextBox pour saisir une valeur et un bouton pour ajouter la valeur saisie.
Ensuite, nous pouvons ajouter une ListBox qui contiendra un TextBlock pour afficher les valeurs de la liste de TODO :
<Grid
x
:
Name
=
"LayoutRoot"
Background
=
"Transparent"
>
<Grid.RowDefinitions>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"*"
/>
</Grid.RowDefinitions>
<StackPanel
x
:
Name
=
"TitlePanel"
Grid.
Row
=
"0"
Margin
=
"12,17,0,28"
>
<TextBlock
x
:
Name
=
"ApplicationTitle"
Text
=
"Demo ListBox"
Style
=
"{StaticResource PhoneTextNormalStyle}"
/>
</StackPanel>
<Grid
x
:
Name
=
"ContentPanel"
Grid.
Row
=
"1"
Margin
=
"12,0,12,0"
>
<!--<TextBlock Text="Nom : " Margin="0 20 43 0" FontSize="35" />-->
<Grid>
<TextBlock
Text
=
"TODO : "
Margin
=
"0 20"
FontSize
=
"45"
/>
<TextBox
Margin
=
"150 15 0 0"
Height
=
"80"
VerticalAlignment
=
"Top"
FontSize
=
"30"
Name
=
"todoTb"
Width
=
"320"
HorizontalAlignment
=
"Left"
/>
<Button
Height
=
"70"
Width
=
"250"
VerticalAlignment
=
"Top"
Margin
=
"0 100 0 0"
Click
=
"Button_Click"
Content
=
"Ajouter"
/>
<ListBox
Margin
=
"0 190 0 0"
Name
=
"listbox"
>
<ListBox.ItemTemplate>
<DataTemplate>
<Border
Background
=
"#FFDEDEDE"
CornerRadius
=
"10"
Margin
=
"20"
>
<StackPanel
Width
=
"400"
>
<TextBlock
HorizontalAlignment
=
"Center"
Text
=
"{Binding Todo}"
FontSize
=
"45"
Foreground
=
"Blue"
Margin
=
"20"
/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Grid>
</Grid>
Si vous démarrez l'application, vous pourrez noter que la TextBox prend automatiquement en charge l'affichage d'un clavier virtuel et qu'il n'y a rien à faire de particulier.
En regardant ce Xaml, on observe notamment le TextBox « todoTb » ainsi qu'un bouton qui possède une propriété Click : Click=« Button_Click ».
On peut ainsi constater que l'appui sur un bouton (le « tap ») peut être géré par le même handler que pour une application Silverlight pilotée à la souris.
Enfin, nous pouvons voir la ListBox « listbox » qui possède un TextBlock dans sa propriété ItemTemplate avec une valeur liée à la propriété Todo : Text=« {Binding Todo} ».
Pour assurer le binding, créons une classe TodoElement qui implémente INotifyPropertyChanged :
public
class
TodoElement :
INotifyPropertyChanged
{
private
string
todoElement;
public
event
PropertyChangedEventHandler PropertyChanged;
public
void
NotitfyPropertyChanged
(
string
propertyName)
{
if
(
PropertyChanged !=
null
)
{
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
propertyName));
}
}
public
string
Todo
{
get
{
return
todoElement;
}
set
{
todoElement =
value
;
NotitfyPropertyChanged
(
"Todo"
);
}
}
}
Et assurons ce binding dans le code behind via la propriété ItemsSource de la ListBox grâce à une ObservableCollection de TodoElement :
public
partial
class
MainPage :
PhoneApplicationPage
{
ObservableCollection<
TodoElement>
listTodo =
new
ObservableCollection<
TodoElement>(
);
public
MainPage
(
)
{
InitializeComponent
(
);
listbox.
ItemsSource =
listTodo;
}
private
void
Button_Click
(
object
sender,
RoutedEventArgs e)
{
listTodo.
Add
(
new
TodoElement {
Todo =
todoTb.
Text }
);
}
}
3. Gestion de l'événement d'appui sur le bouton▲
Nous voyons également la méthode Button_Click qui permettra de gérer l'événement d'appui sur le bouton. Ici, nous rajouterons un élément à la liste.
On peut voir que la gestion de l'événement d'appui sur une touche se passe de la même façon que la gestion du clic sur une application Silverlight classique. On verra lors de prochains tutoriels qu'il est possible de gérer les événements utilisateurs d'une autre façon, pour reconnaitre par exemple les « glisser » (swipe) ou les gestes avec une structure plus complexe.
Pour des applications simples de gestion, comme ici, on aura tout à fait intérêt à utiliser cette méthode, en utilisant les handler de clic sur un bouton par exemple.
4. Démarrage de l'application dans l'émulateur▲
Démarrons l'application dans l'émulateur (F5) et ajoutons quelques éléments…
Ce qui donne :
Et si on fait défiler vers le bas en maintenant la pression sur la souris et en faisant glisser vers le haut, on voit le défilement attendu ; que vous pouvez retrouver sur la vidéo ci-dessous.
Vous conviendrez que l'effet est plutôt réussi. Et tout ça sans code supplémentaire. La Listbox WP7 intègre par défaut ce comportement, ce qui est bien pratique.
Cliquons sur « back », qui a pour effet de terminer l'application, et relançons-la.
Horreur ! Tout a disparu. Ceci est plutôt dérangeant pour une liste de TODO. Si on doit se rappeler des tâches à faire et les ressaisir à chaque fois, l'application perd grandement de son intérêt…
Forcément me diriez-vous, nous n'avons rien sauvegardé. Voyons comme faire.
5. Persistance dans l'Isolated Storage▲
Pour faire persister de l'information, nous avons à notre disposition l'Isolated Storage. Le principe sera de sérialiser notre liste de TODO dans l'Isolated Storage au moment où l'on souhaite la sauvegarder. De même, nous pourrons la désérialiser quand l'on souhaitera recharger les informations.
Pour ce faire, on va utiliser une classe helper qui est fournie dans le training kit de WP7 qui permet de sérialiser et désérialiser en JSON.
Nous aurons besoin pour ce faire de rajouter une référence à System.Servicemodel.Web.
On ajoute la classe IsolatedStorageHelper dont le code ci-dessous est assez simple à comprendre :
// ----------------------------------------------------------------------------------
// Microsoft Developer & Platform Evangelism
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
// ----------------------------------------------------------------------------------
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious. No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
// ----------------------------------------------------------------------------------
using
System.
IO;
using
System.
IO.
IsolatedStorage;
using
System.
Runtime.
Serialization.
Json;
using
System.
Text;
namespace
ListBoxDemo
{
public
static
class
IsolatedStorageHelper
{
public
static
T GetObject<
T>(
string
key)
{
if
(
IsolatedStorageSettings.
ApplicationSettings.
Contains
(
key))
{
string
serializedObject =
IsolatedStorageSettings.
ApplicationSettings[
key].
ToString
(
);
return
Deserialize<
T>(
serializedObject);
}
return
default
(
T);
}
public
static
void
SaveObject<
T>(
string
key,
T objectToSave)
{
string
serializedObject =
Serialize
(
objectToSave);
IsolatedStorageSettings.
ApplicationSettings[
key]
=
serializedObject;
}
public
static
void
DeleteObject
(
string
key)
{
IsolatedStorageSettings.
ApplicationSettings.
Remove
(
key);
}
private
static
string
Serialize
(
object
objectToSerialize)
{
using
(
MemoryStream ms =
new
MemoryStream
(
))
{
DataContractJsonSerializer serializer =
new
DataContractJsonSerializer
(
objectToSerialize.
GetType
(
));
serializer.
WriteObject
(
ms,
objectToSerialize);
ms.
Position =
0
;
using
(
StreamReader reader =
new
StreamReader
(
ms))
{
return
reader.
ReadToEnd
(
);
}
}
}
private
static
T Deserialize<
T>(
string
jsonString)
{
using
(
MemoryStream ms =
new
MemoryStream
(
Encoding.
Unicode.
GetBytes
(
jsonString)))
{
DataContractJsonSerializer serializer =
new
DataContractJsonSerializer
(
typeof
(
T));
return
(
T)serializer.
ReadObject
(
ms);
}
}
}
}
J'ai choisi de sauvegarder les informations après chaque ajout à la liste de TODO, mais il pourrait tout à fait être envisageable de faire les sauvegardes lors de l'événement de fermeture de l'application.
Ce qui nous donne :
private
void
Button_Click
(
object
sender,
RoutedEventArgs e)
{
listTodo.
Add
(
new
TodoElement {
Todo =
todoTb.
Text }
);
IsolatedStorageHelper.
SaveObject
(
"ListeTodo"
,
listTodo);
}
Pour recharger les informations, on peut modifier le constructeur de MainPage pour avoir :
List<
TodoElement>
todoElementList;
ObservableCollection<
TodoElement>
listTodo =
new
ObservableCollection<
TodoElement>(
);
public
MainPage
(
)
{
InitializeComponent
(
);
todoElementList =
IsolatedStorageHelper.
GetObject<
List<
TodoElement>>(
"ListeTodo"
);
if
(
todoElementList ==
null
)
todoElementList =
new
List<
TodoElement>(
);
foreach
(
TodoElement todoElement in
todoElementList)
{
listTodo.
Add
(
todoElement);
}
listbox.
ItemsSource =
listTodo;
}
Redémarrons l'application et saisissons quelques valeurs.
Cliquons sur back, le débogueur se ferme. Relançons l'application ainsi que le débogueur (F5) et ô miracle, notre Listbox est remplie avec les informations déjà saisies.
Merci la sérialisation.
Il ne manquera plus à notre application qu'à savoir supprimer des éléments de la liste pour qu'elle soit vraiment fonctionnelle.
Ceci est plutôt simple à faire, il faudrait rajouter un bouton dans l'ItemTemplate de la Listbox, brancher un événement clic dessus et supprimer de la liste le todo sélectionné.
Comme ce n'est pas l'objet du tutoriel, nous n'allons pas le faire. Je vous encourage chaudement à essayer afin de vous entrainer.
Juste pour le plaisir, n'hésitez pas à passer en debug dans l'IsolatedStorageHelper, vous pourrez voir notamment comment est sérialisée notre liste de Todo. Par exemple :
[{
"Todo"
:
"aaa"
},{
"Todo"
:
"bbb"
}]
est la représentation JSON d'une liste contenant deux todo : « aaa » et « bbb ».
6. Réactivation d'une application▲
Disons que notre application est fonctionnelle et que je suis en train de noter un élément à faire et que je reçois un appel. Ou alors, j'ai besoin d'aller chercher une information sur Internet pour compléter ma saisie. Ce qui peut se résumer par le scénario suivant :
Je lance mon application. Je commence à saisir. J'appuie sur start pour aller dans le navigateur, je fais ma recherche, je ferme la recherche et je reviens sur mon application.
Malheur, ce que j'étais en train de saisir a disparu.
C'est le principe de ce qu'on appelle le Tombstone, qui correspond au passage en mode « désactivé » de l'application.
Ce qui se passe en fait c'est que lorsque l'application passe en mode désactivé puis repasse en mode réactivé, alors l'application est relancée depuis le début (ce qui explique que l'on perde nos données saisies) mais à la différence près que l'on passe dans un événement particulier, signe de la réactivation de l'application.
Le modèle de développement de Windows Phone 7 nous offre l'opportunité de stocker des informations temporaires pendant le temps où l'application est désactivée et où elle se réactive.
Pour bien comprendre comment cela fonctionne, ouvrons le fichier App.Xaml.cs et mettons un point d'arrêt dans les méthodes suivantes :
// Code à exécuter lorsque l'application démarre (par exemple, à partir de Démarrer)
// Ce code ne s'exécute pas lorsque l'application est réactivée
private
void
Application_Launching
(
object
sender,
LaunchingEventArgs e)
{
}
// Code à exécuter lorsque l'application est activée (affichée au premier plan)
// Ce code ne s'exécute pas lorsque l'application est démarrée pour la première fois
private
void
Application_Activated
(
object
sender,
ActivatedEventArgs e)
{
}
// Code à exécuter lorsque l'application est désactivée (envoyée à l'arrière-plan)
// Ce code ne s'exécute pas lors de la fermeture de l'application
private
void
Application_Deactivated
(
object
sender,
DeactivatedEventArgs e)
{
}
// Code à exécuter lors de la fermeture de l'application (par exemple, lorsque l'utilisateur clique sur Précédent)
// Ce code ne s'exécute pas lorsque l'application est désactivée
private
void
Application_Closing
(
object
sender,
ClosingEventArgs e)
{
}
Relancez l'application en debug. On passe dans un premier temps dans l'événement Application_Launching.
Cliquons sur Start et l'événement Application_Deactivated est levé. Cliquons sur back et nous pouvons observer l'affichage d'un texte « reprise » dans l'émulateur :
Puis nous passons dans l'événement Application_Activated.
Cliquons sur back et nous passons dans l'événement Application_Closing.
Pour faire persister temporairement la valeur de la textbox, nous pourrons utiliser le dictionnaire State du PhoneApplicationService.
Pour ce faire, rajoutons un événement de changement de valeur pour la textbox :
<TextBox
Name
=
"todoTb"
[...]
TextChanged
=
"todoTb_TextChanged"
/>
et dans le code behind :
private
void
todoTb_TextChanged
(
object
sender,
TextChangedEventArgs e)
{
PhoneApplicationService.
Current.
State[
"CurrentTodo"
]
=
todoTb.
Text;
}
Cela nous permet de stocker les informations dans le dictionnaire d'états temporaires.
Pour récupérer les informations, on peut le faire dans le constructeur de la page par exemple :
public
MainPage
(
)
{
InitializeComponent
(
);
if
(
PhoneApplicationService.
Current.
State.
ContainsKey
(
"CurrentTodo"
))
todoTb.
Text =
(
string
)PhoneApplicationService.
Current.
State[
"CurrentTodo"
];
...
}
Recommençons l'opération.
On lance l'application. On clique sur Start, on clique sur Back et on peut constater que la valeur de la TextBox est bien restaurée.
Parfait et cela évitera à nos utilisateurs d'être frustrés si jamais ils ont saisi un long texte…
Notez que nous avons utilisé la persistance au niveau de l'application. Il est également possible d'utiliser la persistance au niveau de la page ; en effet, la PhoneApplicationPage dispose aussi d'un dictionnaire State. Celui-ci sera plutôt utilisé via les événements de navigation (OnNavigatedFrom et OnNavigatedTo) pour faire persister des informations d'affichage (focus, etc.) lors de la navigation.
7. Remarques▲
La ListBox est un contrôle très puissant, mais elle peut souffrir de lenteur, surtout si elle est très volumineuse.
On pourra avantageusement tirer parti du contrôle écrit par David Anson, le DeferredLoadListBox qui pallie ces problèmes de lenteur en se basant sur des StackPanel.
Accéder à l'article.
8. Téléchargements▲
Vous pouvez télécharger ici les sources du projet : version rar (75 Ko) , version zip (81 Ko).
9. Conclusion▲
Ce tutoriel a donc présenté comment la ListBox WP7 fonctionnait sur les téléphones. Nous avons également vu comment réagir à un appui sur un bouton. Enfin, nous avons eu un aperçu de comment faire persister des informations entre les lancements d'applications et lorsque celles-ci passent en mode désactivé.
J'espère que ce tutoriel a pu vous être utile et vous a donné envie de vous lancer dans la création d'applications sur Windows Phone 7.
Remerciements▲
Je remercie les membres de l'équipe Dotnet pour leurs relectures attentives du document et leurs remarques ainsi que Mahefasoa et jacques_jean pour leurs relectures orthographiques.
Contact▲
Si vous constatez une erreur dans le tutoriel, dans le code source, dans la programmation ou pour toute information, n'hésitez pas à me contacter par le forum.