Dot.Blog

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

Xamarin.Forms : La puissance des DataTemplateSelector

Appliquer un visuel différent aux éléments d’une liste n’est pas si simple. La sémantique peut l’obliger (alerte, mise en évidence…), la technique aussi (classes différentes). Grâce à la technique du DataTemplateSelector voyons comme rendre cela bien plus simple et efficace…

Le problème des listes non homogènes

Une liste est non homogène lorsque ses éléments imposent une mise en page différente. Il peut s’agir d’une obligation technique (les éléments ne sont pas tous de même classe ou contiennent des liens avec des données supplémentaires mais non obligatoires…) ou d’obligations de mise en page pour des raisons liées à l’UX (mettre certains items en évidence par exemple, ou au contraire en affaiblir visuellement quelques autres, etc).

Il peut s’agit juste de changer une couleur (pour une mise en évidence un nom de personne en rouge alors que les autres sont en bleu suffira) ce qui peut se réaliser de plusieurs façons assez simples. Mais il peut aussi s’agir de changements plus radicaux : mise en page totalement différente, affichage d’informations différentes, etc.

Lorsque ces différences deviennent trop grandes, vouloir jongler avec des triggers, des behaviors, des styles, devient vite du code spaghetti… Il est alors nécessaire de faire appel à une technique beaucoup plus souple, plus simple à maintenir, et extensible à volonté.

Cette technique c’est celle du DataTemplateSelector.

Un DataTemplate on voit ce que c’est. C’est le contenu visuel d’un élément de ListView par exemple.

Un Sélecteur de DataTemplate n’est donc pas très difficile à cerner : le but sera de choisir au moment voulu le “bon” DataTemplate pour l’item à afficher. Le “bon” DataTemplate implique qu’il faudra tester l’Item en cours avant d’appliquer le DataTemplate. Le “bon” signifie aussi qu’il peut y en avoir des mauvais, donc plusieurs DataTemplate parmi lesquels choisir.

La technique réside donc

  • Dans le fait de créer plusieurs DataTemlate, chacun adapté à un type d’Item précis
  • De mettre en place un mécanisme permettant de charger à la volée le “bon” DataTemplate au moment de l’affichage.

Comme c’est cette technique qui nous intéresse ici je vais essayer de rester le plus simple possible afin que la complexité de l’exemple ne brouille pas la compréhension. Toutefois il faudra garder à l’esprit que l’exemple est par force ultra réducteur et qu’il ne faut pas se laisser enfermer par lui. La technique est utilisable dans de nombreux autres contextes, plus sophistiqués, que je vous laisse imaginer. De plus je partirai d’un ListView mais comme je le faisais remarquer en introduction tout conteneur sachant afficher des items peut bénéficier de la technique du DataTemplateSelector.

Liste à visuel variable

Pour qu’il y ait un visuel variable il faut une bonne raison, et cette dernière ce cache dans les données à traiter. Il y a donc des différences dans les données, soit leur type, soit leur contenu, soit sémantiquement une ou plusieurs propriétés ayant une importance nécessitant une mise en page différente. Pour l’exemple comme indiqué je vais faire simple. La classe affichée sera unique, la classe Student (étudiant) qui possède un nom et un booléen indiquant si cet étudiant joue d’un instrument de musique ou non. C’est la valeur de ce champ que je vais choisir pour modifier la mise en page.

La classe est donc la suivante :

public class Student
{
     public String Name { get; set; }
     public bool PlayMusic { get; set; }
}

Pour faire fonctionner l’exemple il nous faut des données de test que le programme utilisera en place et lieu d’une base de données ou d’une interrogation de service distant.

Une classe simulant un service de donnée retourne une liste de dix étudiants :

    public static class DataService
    {
        ObservableCollection<Student> GetData()
        {
            return new ObservableCollection<Student>
            {
                new Student { Name = "Saul", PlayMusic = false },
                new Student { Name = "Ismael", PlayMusic = true },
                new Student { Name = "Jason", PlayMusic = false },
                new Student { Name = "Deacon", PlayMusic = false },
                new Student { Name = "Angel", PlayMusic = true },
                new Student { Name = "Salomon", PlayMusic = false },
                new Student { Name = "Rebekah", PlayMusic = false },
                new Student { Name = "Noah", PlayMusic = true },
                new Student { Name = "Claudia", PlayMusic = false },
                new Student { Name = "Luke", PlayMusic = true }
            };
        }

   }

Nous voici donc armés d’une liste de données qu’il va falloir afficher dans une liste. L’exigence d’UX sera que les étudiants musiciens devront être affichés de façon distinctive. On pourrait ajouter une petite clé de sol en fin de ligne de leur nom, mettre un fond en couleur, changer la fonte, faire une mise en page totalement différente (si on avait plus d’infos on pourrait même afficher une miniature de l’instrument joué). Tout cela doit être ultra adapté, c’est à dire que les étudiants ne jouant pas de musique doivent être listés de la façon la plus standard (par rapport au reste de l’application).

Pour continuer à donner dans le simple je vais me limiter ici à changer la couleur du nom, les musiciens seront en rouge. Bien entendu pour faire juste ça il existe d’autres moyens plus légers, mais vous avez compris les restrictions de la démo qui ne montre que le mécanisme et non un exemple complet prouvant l’utilité de la démarche. Votre imagination devrait suffire !

Mise en œuvre

Pour commencer il nous faut une liste à afficher, dans une page, la seule de cette App de démo. Je vais utiliser la MainPage.Xaml créée par défaut. Ici aussi simplicité sera le maître mot, MVVM sera simulé par la page de code-behind qui deviendra le DataContext.

Mais voyons d’abord le résultat visuel, c’est plus stimulant !

image


Comme vous le constatez la liste est alphabétisée et les noms des étudiants pratiquant un instrument de musique sont bien en rouge, et en plus, en fin de ligne, quelques notes de musiques viennent rappeler visuellement cette particularité.

Dès lors choisir un musicien dans la liste pour l’invité à une jam session sur le campus peut se faire d’un coup d’œil et du bout d’un doigt !

Je ne reviens pas sur le service de données, même si dans la démo finale j’ai compliqué un tout petit peu pour ajouter un tri sur le nom en utilisant Linq.

Trois choses nous importent dans la mise en œuvre de cet exemple :

  • La création des Templates (où ? comment ?)
  • La création du sélecteur de template (où ? en quel langage ? comment ?)
  • La déclaration de ListView pour que tout cela fonctionne (comment ?)

Création des templates

Il faut créer les templates dans la partie ressource de la page (ou dans un dictionnaire partagé par App ou autre si on souhaite les réutiliser dans l’App ce que je conseille vivement). Ce qui donne ici dans notre ContentPage “MainPage.Xaml” :

<ContentPage.Resources>
        <ResourceDictionary>
            <DataTemplate x:Key="NonPlayerTemplate">
                <ViewCell>
                    <StackLayout>
                        <Label HorizontalOptions="StartAndExpand"
                               VerticalOptions="CenterAndExpand"
                               Text="{Binding Name}"
                               FontSize="20"/>
                    </StackLayout>
                </ViewCell>
            </DataTemplate>

           <DataTemplate x:Key="PlayerTemplate">
                <ViewCell>
                    <StackLayout Orientation="Horizontal">
                        <Label Text="{Binding Name}"
                               FontSize="20"
                               TextColor="Red"
                               HorizontalOptions="StartAndExpand"
                               VerticalOptions="CenterAndExpand"/>
                        <Label Text="" TextColor="DarkRed"></Label>
                    </StackLayout>
                </ViewCell>
            </DataTemplate>


            <DataTemplate x:Key="DataErrorTemplate">
              <ViewCell>
                  <StackLayout>
                      <Label Text="Data Error !" TextColor="DeepPink"/>
                  </StackLayout>
              </ViewCell>
            </DataTemplate>


            <view:StudentTemplateSelector x:Key="StudentTemplateSelector"
                  NonPlayerTemplate="{StaticResource NonPlayerTemplate}"
                  PlayerTemplate="{StaticResource PlayerTemplate}" />


        </ResourceDictionary>
</ContentPage.Resources>

Il y a 4 sections dans les ressources déclarées, chacune a une couleur :

Les trois premières (rouge, bleue, gris) sont les fameux DataTemplate dont nous avons besoin pour faire marcher notre sélecteur de Template. Pourquoi trois ? Parce que si dans la démo le cas n’est pas possible, dans la “réalité” il est bon de prévoir un template dans le cas d’une donnée mal formée ou non reconnue par le filtre du sélecteur. Ici un tempalte “DataErrorTemplate” est défini et serait retourné si l’étudiant passé au sélecteur (ce que nous allons voir plus loin) était à null.

Ce sont ainsi les deux premiers templates qui nous intéressent. Le premier “NonPlayerTemplate” sera utilisé pour les étudiants qui ne jouent pas d’instrument (propriété PlayMusic à False donc). Seul le nom est écrit en utilisant la couleur de texte défaut.

Le second template “PlayerTemplate” est celui des étudiants dont la fiche voit sa propriété “PlayMusic” à True. Ils jouent d’un instrument et leur nom est simplement écrit en rouge. Petit bonus, en fin de ligne apparait quelques notes de musique (utilisant le symbole U+1F3B6).

la 4ème section des ressources, en vert, déclare le sélecteur de template. Il reçoit une clé “StudentTemplateSelector” qui permettra de se référer à cette ressource plus tard (notamment dans la ListView) ainsi qu’il fixe la valeur de deux propriétés “NonPlayerTemplate” et “PlayerTemplate” qui ici ne sont pas les noms des templates qui ont été définis mais bien les noms de deux propriétés exposées par le sélecteur de template. C’est pour cela que leur valeur est fixée en utilisée StaticResource. Le nom des templates définis dans les ressources sont identiques, cela évite d’inventer des synonymes perturbant (mais il faut tout de même comprendre que ces mots bien qu’identiques ne désignent pas exactement la même chose, c’est le contexte qui permet de le comprendre).

La définition des templates étant terminée, voyons comment la liste est implémentée…

Définition de la liste

Ce n’est pas la partie la plus complexe :

<ListView ItemsSource="{Binding Students}"
                 HasUnevenRows="True"
                 ItemTemplate="{StaticResource StudentTemplateSelector}"
                 SeparatorVisibility="None"
                 VerticalOptions="StartAndExpand"
                 Margin="5">

L’ItemSources est bindée à la propriété Students exposée par le code behind (sachant que le constructeur indique “BindingContext=this”). C’est la source de donnée qui sera retournée par un service utilisant une base de données, un service distant etc…

La propriété HasUnEvenRows est placée à True car les différents DataTemplate peuvent fort bien définir des tailles de lignes différentes. Il est plus prudent d’initialiser la variable à la création de la liste. Ici nos templates font tous la même hauteur mais demain qui sait… et il faudra alors ne pas oublier de vérifier ce détail, en le prévoyant maintenant on se met à l’abri d’un tel oubli dans le futur (programmation défensive dans l’esprit).

Vient ensuite la déclaration de l’ItemTemplate, une propriété simple et connue de tous ceux qui utilisent une ListView. Sauf qu’ici au lieu de pointer un véritable DataTemplate qui serait défini ailleurs, nous pointons la ressource StudenTemplateSelector qui est notre sélecteur de template. C’est lui qui en fonction de l’item retournera “à la volée” le “bon” DataTemplate”. En fait la Liste ne sait rien ou presque de ce tour de passe-passe…

Les autres propriétés sont classiques.

Créer le sélecteur de template

Le clou du spectacle ! Il fallait bien y arriver un jour !

Le sélecteur de template de notre exemple se présente ainsi :

    public class StudentTemplateSelector : DataTemplateSelector
     {

        public DataTemplate NonPlayerTemplate { get; set; }
         public DataTemplate PlayerTemplate { get; set; }

        public DataTemplate DataErrorTemplate { get; set; }

        protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
         {
             return !(item is Student student) ? DataErrorTemplate :
                 student.PlayMusic ? PlayerTemplate : NonPlayerTemplate;
         }
     }

Première chose à relever, un sélecteur de template est une classe “normale”, mais il doit descendre de DataTemplateSelector. Ce qui l’oblige à définir la méthode OnSelectTemplate qui est responsable du choix du template.

Seconde chose, le sélecteur déclare des propriétés publiques de type DataTemplate. Le contenu de ces propriétés sera retourné par le sélecteur selon le contexte. Pourquoi des propriétés et pas de simple champs privés ? Parce que le code du sélecteur ne sait pas encore ce que seront exactement les DataTemplate à utiliser… Il pourrait très bien les figer dans son code, mais quel manque de souplesse ! Ici cela nous permet lors de la déclaration de la ressource StudentTemplateSelector dans la page d’indiquer en même temps les templates à utiliser… On en change quand on veut sans avoir à bricoler le code du sélecteur.

Enfin, la précieuse méthode obligatoire de cette classe : OnSelectTemplate.

Comme vous le constatez son corps est court, et cela doit être le cas tout le temps. Un éventuel sélecteur de cas peut être utilisé mais ce code doit rester court, il ne fait que tester l’item pour choisir le template à retourner.

Ici si l’item est null c’est le template d’erreur qui est retourné, idem si l’item n’est pas de la bonne classe attendue (Student). Sinon selon la valeur de PlayMusic c’est soit le template de musicien ou de non musicien qui est renvoyé.


Conclusion

Les sélecteurs de template permettent une grande souplesse dans les mise en page car ils sont utilisable partout ou un DataTemplate est attendu. Le principe est simple et efficace. Tout en restant très souple.

Bref que des avantages.

Bonne UX et..;

Stay Tuned !

blog comments powered by Disqus