Contrôles personnalisés et templates en Silverlight

Lorsque l’on débute en Silverlight, on a tendance à utiliser des UserControl (littéralement contrôles personnalisés) pour personnaliser ses contrôles ; c’est vrai que le nom UserControl laisse à penser que c’est le cas d’utilisation parfait. Cependant, la classe UserControl permet de créer un regroupement de contrôles et pas n’est pas forcément idéale pour un contrôle générique, réutilisable et pouvant être stylisé.

En revanche, il existe une technique pour créer un contrôle qui possède tous ces attributs : le mécanisme de Template de Silverlight et le fichier generic.xaml. Dans cet article je vous propose de voir en quelques étapes comment créer un tel contrôle, en prenant l’exemple du contrôle HeaderedContentControl, que l’on peut trouver dans le Silverlight Toolkit, mais qui est absent de la version Windows Phone.

L’exemple

Le contrôle HeaderedContentControl est un contrôle qui sert de conteneur à un contenu (propriété Content) et qui est caractérisé par un entête (Header). Bien que l’implémentation visuelle du HeaderedContentControl pourrait être faite de nombreuses manières différentes, j’ai fait le choix de m’inspirer des écrans de paramétrage de Windows Phone OS, dans lesquels on voit pour chaque option un titre. Comme on le verra également, redéfinir l’aspect visuel d’un contrôle templaté est une tâche particulièrement aisée et succincte.

Etape 1 : choisir la classe de base

Lors de la création d’un contrôle templaté, il est possible de partir de n’importe quelle classe non sellée (sealed) du Framework et de l’étendre. Pour un contrôle qui n’aurait rien en commun avec les autres contrôles, on pourrait partir de la classe Control.

Néanmoins, certaines classes sont là pour encapsuler une partie du comportement désiré par notre contrôle étendu – pas besoin de réinventer la roue donc ; ici on a besoin d’afficher du contenu ; il existe justement une classe nommée ContentControl, qui prévoit justement l’utilisation d’un contenu. Si on avait voulu afficher des éléments, on serait parti de la classe ItemsControl, et ainsi de suite.

Pour commencer il suffit donc de créer une nouvelle classe dans votre projet Silverlight / Windows Phone, puis de la faire dériver de la classe de base qui va bien, ici ContentControl.

Etape 2 : ajouter des propriétés

Une fois la classe créée, on peut à présent ajouter des Dependency Properties, qui vont constituer la valeur ajoutée par votre contrôle. Une Dependency Property est une propriété qui en plus d’être définie dans la classe, nécessite d’être enregistrée afin de pouvoir être utilisée depuis du Binding.

Dans notre cas, on veut ajouter l’entête du conteneur. On pourrait créer cette propriété sous forme de chaîne de caractères, mais ce qui paraît encore plus intéressant est de la définir en tant qu’Object ; ainsi il sera possible de mettre n’importe quel objet en tant qu’entête et ainsi de l’afficher par le biais d’un convertisseur ou du HeaderTemplate, une autre propriété de type DataTemplate que nous allons exposer pour personnaliser l’affichage de l’entête.

Il est également possible d’ajouter des évènements ou des méthodes, et de s’inscrire aux évènements déjà présents dans la classe ContentControl, afin d’ajouter du comportement à notre classe, mais nous n’exploreront pas cette possibilité dans cet article.

Etape 3 : définir le template par défaut

Maintenant que toutes les propriétés sont prêtes à être utilisées, il nous faut encore décrire le template par défaut, utilisé quand aucun template personnalisé n’est là pour le surcharger. C’est logiquement du code XAML que nous allons écrire pour définir ce template car il s’agit de la description de l’interface. Le code XAML sera mis dans un fichier nommé generic.xaml situé dans le répertoire Themes. Attention, l’emplacement exact du fichier est important, car c’est un fichier spécial, utilisé uniquement dans le cas de contrôles templatés.

Pour définir le contrôle, nous allons tout d’abord ajouter le namespace XML dans lequel il est défini :
xmlns:controls=”clr-namespace:Controls”

Nous pouvons ensuite créer un style dont le type ciblé est notre classe HeaderedContentControl. A l’intérieur de ce style, nous allons définir les valeurs par défaut de propriétés telles que Background, ForeGround, Horizontal/VerticalContentAlignment, etc. qui seront utilisé si ces propriétés ne sont pas changées explicitement.

Enfin nous allons définir, par le même biais, la propriété ControlTemplate qui représente l’aspect visuel du contrôle. Ici cette partie est extrêmement simple, une Grid comportement 2 lignes, l’une pour l’entête et l’autre pour le contenu, mais il pourrait bien plus complexe, avec par exemple l’utilisation du VisualStateManager, qui permet de faire des contrôle templatés à états (par exemple l’état sélectionné ou non d’une CheckBox) et animés.

Ce qu’il faut tout de même remarquer, est l’utilisation du TemplateBinding pour faire la liaison avec le contenu, l’entête et leur ContentTemplate respectifs. Le TemplateBinding est en fait un raccourci permettant de faire du Binding sur le contrôle templaté ; c’est l’équivalent de :
Content=”{Binding Header, RelativeSource={RelativeSource TemplatedParent}}”

Etape 4 : changer la DefaultStyleKey

Il ne reste plus qu’à préciser dans le constructeur de notre contrôle que le style par défaut est celui défini dans generic.xaml ; pour cela Silverlight passe par la propriété DefaultStyleKey. En lui donnant le type du contrôle comme clé, il va retrouver la bonne ressource et associé le code C# au code XAML.

Utilisation et surcharge du template

Il suffit maintenant pour utiliser notre contrôle flambant neuf d’ajouter le namespace XML et de remplir sa propriété Header et Content. C’est d’ailleurs ce même contrôle que j’avais déjà utilisé dans mon article précédent : Ajouter des éléments spéciaux dans une collection liée via binding

L’avantage de cette technique est que le Template est du coup très facile à changer, en particulier à l’aide de Microsoft Expression Blend : certain parlent par conséquent du caractère « blendable » du contrôle.

Dans Blend, en faisant un clic-droit sur le contrôle en question et en choisissant « Edit Template » puis « Edit a Copy… », l’outil crée une copie du Template que l’on peut éditer.

Une fois le Template créé, on peut facilement reporter la modification sur toutes les occurrences du contrôle, en utilisant la commande « Apply Resource ». On peut donc en quelque clics obtenir le résultat suivant :

Pour terminer on peut donc dire que la création d’un contrôle templaté, à priori moins immédiat que celle d’un UserControl, apporte plus de finesse dans le contrôle de l’apparence et est donc à prescrire pour les contrôles pensés pour être génériques et réutilisables.

Ajouter des éléments spéciaux dans une collection liée via Binding

Le Binding des technologies basées sur XAML (Silverlight / WPF) a constitué une grande avancée pour simplifier le découplage des données et de l’interface en apportant un contrôle poussé des données affichées dans la vue ainsi qu’une grande simplicité. Cependant certains rares cas de figures étaient plus faciles à implémenter avec les mécanismes de Binding pré-XAML.

Cet article présente l’un de ces cas, et une astuce pour le contourner, transparente pour l’utilisateur. La méthode exposée en est assurément une parmi tant d’autre mais je n’ai que trouvé peu de solutions sur la toile (sans doute me manquait-il les mots-clés magiques). Je vous présente donc la mienne, possiblement améliorable, et si vous avez une autre technique à ce même problème, n’hésitez pas à réagir via les commentaires !

Le problème

Nous sommes dans une application de test de gestion d’une bibliothèque (vous savez ces endroits où l’on peut trouver des ensembles de feuilles de papiers attachées les unes aux autres appelés « Livres »… Qu’importe c’est un exemple théorique). La vue permettant d’ajouter un nouveau livre comporte trois champs, dont une catégorie qu’il faut sélectionner dans une liste de choix.

Cette liste de choix, est dans cet exemple implémenté avec le ListPicker du Silverlight Toolkit pour Windows Phone. On a lié sa propriété ItemsSource à une collection Categories contenant les objets représentant les catégories, possiblement chargée elle-même depuis une base de données.

<toolkit:ListPicker ItemsSource=”{Binding Categories}” />

Seulement voilà, vous voulez gérer le cas où un livre n’a pas la catégorie proposée ! Quelles sont alors vos possibilités ?

  • Ajouter un enregistrement dans la base nommé « Aucune » est une solution peu intelligente car on serait en train de mélanger des catégories avec des non-catégories !
  • Mettre le SelectedIndex du ListPicker à -1 ; quand on ouvre la vue, il n’y a donc pas de catégorie de sélectionner. Cela fonctionne, mais si l’utilisateur choisi une catégorie, il ne pourra plus se rétracter, et sera condamné à fermer la vue et à la rouvrir ! Pas très ergonomique…
  • Ma solution : utiliser une classe pour étendre la classe utilisée dans la collection pour fournir des items spéciaux qui représentent des valeurs non liées. Tout un programme !

La solution

Un enum qui représente les types d’éléments spéciaux

Pour symboliser les éléments spéciaux nous allons créer tout d’abord un enum qui correspond à leurs types. Par exemple ici, j’ai mis les types suivants : aucun, tous et autre. Vous êtes libre d’en ajouter ou d’en enlever, ma solution est plus un pattern qu’un framework, et il vous faudra sans doute l’adapter à vos besoins.

La classe pour étendre les entités

Cette classe implémente le Design Pattern Decorator qui consiste à encapsuler un objet dans un autre objet pour lui ajouter des fonctionnalités. Ici on rajoute un attribut de type SpecialItemType qui est l’enum que nous avons déclaré plus tôt (c’est un Nullable pour pouvoir gérer le cas où l’élément n’a pas de type spécial).

A noter également que ma classe dérive de NotificationObject qui est apportée par Prism v4 pour Windows Phone (sur lequel je ferai un article plus complet plus tard). Globalement il sert uniquement à implémenter l’interface INotifyPropertyChanged et définit une méthode RaisePropertyChanged que l’on peut utiliser dans les setters de ses classes. Nous pouvons à présent utiliser cette classe dans la collection de catégories située dans notre ViewModel.

L’affichage des catégories et des non-catégories

Pour l’affichage nous sommes confrontés à un dernier problème : un élément à afficher de deux façons différentes selon le type d’objet. Heureusement ce problème-là, je l’avais déjà traité dans mon post : https://sebastienmornas.wordpress.com/2010/10/07/utiliser-un-datatemplateselector-en-silverlight/

Il faut créer un DataTemplateSelector qui choisit son template en fonction du caractère spécial de l’élément ou non. Pas de surprise dans mon implémentation, si ce n’est que cette fois j’utilise la classe DataTemplateSelector définie par Prism v4 (aucune différence avec la version que j’avais présentée dans mon article, mais au moins il n’y a plus besoin de la réécrire dans chaque projet !)

Enfin dans notre XAML, il ne nous reste plus qu’à utiliser ce DataTemplateSelector, comme ceci :

Et voilà : visuellement, on a bien ce qu’on s’attend à voir, et sous le capot, on voit que la catégorie est à null ce qui représente bien la notion de « aucune catégorie ». Certes la solution n’est pas immédiate à mettre en place, mais elle a le mérite de garder une architecture claire et propre et est très facilement adaptable et personnalisable.

La bibliothèque Silverlight Database

J’avais évoqué dans un précédent post les mécanismes de persistance de données dans Windows Phone 7 via l’IsolatedStorage. J’avais ainsi terminé l’article sur le fait que Silverlight pour Windows Phone manquait encore de réel gestionnaire de base de données tel que SQL CE. Il existe en revanche plusieurs API Open Source qui comblent ce manque. L’une d’entre elles est la bibliothèque Silverlight Database, projet hébergé sur la plateforme de projets Open Source de Microsoft, CodePlex. Vous pouvez télécharger le projet sur sa page : http://silverdb.codeplex.com

Fonctionnalités de Silverlight Database

L’API est en réalité très simple à utiliser, avec seulement quelques classes importantes, en particulier Database et Table<T>. Grâce à ses classes vous allez pouvoir créer une représentation objet de votre base de données et la sauvegarder sur l’IsolatedStorage de votre client (que cela soit du Silverlight pour navigateur ou du Silverlight pour Windows Phone 7). Toute la manipulation de l’IsolatedStorage sont effectué par Silverlight Database sans que vous ayez à vous préoccuper de quoi que ce soit. Les fonctionnalités de l’API sont les suivantes :

  • Création et suppression d’une ou plusieurs bases
  • Ajout et suppression d’une ou plusieurs tables
  • Récupération des données avec possibilité d’utiliser du Lazy Loading (c’est-à-dire que les données ne seront récupérées que lorsque le programme en aura vraiment besoin)
  • Compression des données via la bibliothèque Open Source Silverlight SharpZipLib (également hébergée sur CodePlex : http://slsharpziplib.codeplex.com/)
  • Chiffrage des données par l’algorithme de chiffrage symétrique AES (via la classe AESManaged)

Ce que Silverlight Database n’est pas

Contrairement à ce que le nom suggère Silverlight Database n’est pas un vrai gestionnaire de base de données. En effet si j’ai une entité A reliée à une ou plusieurs entités B, on aura en fait une classe A qui contient un IEnumerable<B> ; cette liste sera sérialisée en même temps que l’objet de type A, et tous ces objets seront contenu dans la table de A, hors dans un véritable SGBD les instances de B devraient être conservées à part dans une table contenant tous les B.

Ce n’est pas vraiment une limitation, mais cela peut donner des choses étranges si on n’y fait pas attention. C’est pourquoi je pense que Silverlight Database est plus un gestionnaire de conteneur d’objets persisté plutôt qu’un SGBD. Silverlight Database n’en ai pas moins très facile et agréable à utiliser.

Utilisation de l’API

Pour commencer, vous devez créer une base via la méthode statique Database.CreateDatabase qui prend en argument un identifiant (pour retrouver la base plus tard) et optionnellement un mot de passe qui servira de clé de chiffrage. Si vous avez déjà créé une base vous pouvez la charger via la méthode statique Database.LoadDatabase qui prend les mêmes arguments que pour la création.

Depuis l’instance de Database que vous venez de récupérer vous pouvez ajouter une table (ou plus exactement un conteneur pour un certain type d’objet) en appelant la méthode CreateTable<ObjectType> ou récupérer une table déjà créée via la méthode Table<ObjectType>.

Le type Table<T> renvoyé par ces deux méthodes est une classe bien pratique, qui dérive d’ObservableCollection<T>. Cela signifie que vous pouvez par exemple lier une Table<T> directement par Binding à une ListBox dans votre XAML. Comme l’ObservableCollection<T> lève des évènements lors d’ajouts et de suppressions, la ListBox sera ainsi directement mise à jour en fonction de vos traitements métiers sans aucun effort supplémentaire.

Bien évidemment, qui dit ObservableCollection dit Collection et donc possibilité d’utiliser les méthodes d’extensions de LINQ. Vous pourrez donc exécuter des requêtes sur vos tables en utilisant les méthodes Where, Any, Take et ainsi de suite. Pour terminer un appel à la méthode Save de la classe Database vous permettra de sauvegarder les modifications.

Conclusion

Pour conclure Silverlight Database est une bibliothèque très pratique qui permet de persister dans l’IsolatedStorage des objets managés de manière simple et profite de fonctionnalités intéressantes telles que la compression ou le chiffrage. Ce n’est pas exactement un SGBD comme le serait l’hypothétique SQL CE pour Windows Phone 7, mais néanmoins il permet d’abstraire une tâche peu intéressante que l’on retrouve dans un vaste panel d’application et d’ainsi accélérer vos développements.

Utiliser un DataTemplateSelector en Silverlight

Il existe en WPF beaucoup de classes intervenant dans une utilisation avancée du DataBinding. Certaines d’entre elles ne sont pas présentes à ce jour dans Silverlight. Parmi elles, la classe System.Windows.Controls.DataTemplateSelector qui permet de choisir un DataTemplate s’appliquant à un objet selon des règles définies par le développeur. Heureusement, il est très facile de recréer cette classe en Silverlight et cet article va vous permettre de voir comment.

Note : cet article reprend la solution proposé par Raul Mainardi Neto proposée sur CodeProject.

Le problème

Imaginons que vous ayez une ListBox dans laquelle se trouvent plusieurs objets métiers avec des caractéristiques différentes. Pour définir l’affichage de vos éléments, vous allez remplir la propriété ItemTemplate du ListBox. Seulement voilà, vous souhaiteriez changer quelque peu, voir intégralement, l’affichage de l’élément selon ses caractéristiques. C’est dans ce cas que le DataTemplateSelector va vous être utile ; comme son nom l’indique il permet de sélectionner le Template.

C’est par exemple le problème auquel j’ai été confronté dans mon application WP7 proposé pour le concours SNCF (qui soit dit en passant a été sélectionnée pour les phases finales) dans laquelle je voulais varier les logos des transports depuis une gare, selon le type de transport.

La solution

Pour résoudre ce problème, nous allons d’abord créer une classe abstraite, qu’il faudra ensuite dériver selon nos besoins. Cette classe, très simple est la classe DataTemplateSelector. Elle étend la classe ContentControl, car effectivement elle va appliquer un Template au contenu qui lui est passé.

    public abstract class DataTemplateSelector : ContentControl
    {
        public virtual DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            return null;
        }

        protected override void OnContentChanged(object oldContent, object newContent)
        {
            base.OnContentChanged(oldContent, newContent);

            ContentTemplate = SelectTemplate(newContent, this);
        }
    }

Ceci étant fait, créons maintenant une classe fille qui ajoute des propriétés de type DataTemplate (les Templates qui seront sélectionnable par la classe) et qui redéfinit la méthode SelectTemplate, méthode qui va définir selon quelles règles il faut choisir le Template.

    public class TransportDataTemplateSelector : DataTemplateSelector
    {
        public DataTemplate MetroDataTemplate { get; set; }
        public DataTemplate RerDataTemplate { get; set; }
        public DataTemplate BusDataTemplate { get; set; }

        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            Transport t = item as Transport;
            if (t != null)
            {
                switch (t.Type)
                {
                    case TransportType.None:
                        break;
                    case TransportType.Metro:
                        return MetroDataTemplate;
                    case TransportType.Rer:
                        return RerDataTemplate;
                    case TransportType.Bus:
                        return BusDataTemplate;
                    default:
                        break;
                }
            }

            return base.SelectTemplate(item, container);
        }
    }

Il ne reste plus qu’à l’utiliser depuis votre code XAML. Ajouter un namespace xml et utiliser votre TransportDataTemplateSelctor de la manière suivante :

<ListBox Grid.Row="1" ItemsSource="{Binding CurrentStation.Transports}">
  <ListBox.ItemTemplate>
    <DataTemplate>
      <views:TransportDataTemplateSelector Content="{Binding}">
        <views:TransportDataTemplateSelector.MetroDataTemplate>
          <DataTemplate>
            <StackPanel … />
          </DataTemplate>

Et voilà rien de plus simple, pour un résultat très appréciable !