Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Silverlight et le DataTemplateSelector

[new:23/03/2011]WPF et Silverlight c’est tout Xaml de père en fils, la seule différence, du point de vue programmation, c’est que le fiston à un costume plus petit que le papa. Et quand on a besoin d’un grand costume, celui de Silverlight est parfois un peu juste aux entournures. C’est le cas principalement pour certains scénarios dits “avancés” comme ceux mettant en scène le DataTemplateSelector. Comment y remédier ?

DataTemplateSelector

Dans DataTemplateSelector il y a DataTemplate et Selector (même ceux qui ne parlent pas l’anglais l’auront vu j’en suis sûr !). Il s’agit donc de pouvoir sélectionner des DataTemplates.

DataTemplate

Un DataTemplate, pour mémoire, est une ressource décrivant la mise en page d’un item au sein d’une collection d’items affichés par une ListBox ou autre contrôle du même type. La puissance de Xaml se révèle dans ce genre de possibilité d’ailleurs, surtout si, pour le visuel, on s’aide de Expression Blend.

Donc vous avez une ListBox (par exemple) et pour définir la façon dont s’affiche chaque item vous créez tout simplement un DataTemplate. Sous Blend cela se fait entièrement visuellement. Et qui dit DataTemplate dit Binding puisque les éléments affichés par le DataTemplate seront issus d’un contexte particulier : l’item en cours d’affichage.

Pourquoi vouloir sélectionner un DataTemplate et dans quel contexte ?

Imaginons une application gérant des Prospects et des Clients. Les deux classes dérivent ou non d’une classe commune, cela n’a pas d’importance. Ce qui compte c’est qu’elles ont des points communs (comme la propriété NomDeSociété) mais aussi des différences (DateDernièreFacture pour le Client et ARevoirLe pour le Prospect, toujours par exemple).

Supposons maintenant qu’à un moment donné, dans mon application je doive afficher une liste (filtrée et triée par des procédés qui ne nous intéressent pas ici) qui mélange des clients et des prospects.

Si je créée un DataTemplate proposant un TextBlock bindé à la propriété NomDeSociété, tout ira bien car il se trouve que les deux classes proposent cette propriété (encore une fois même si elle ne dérivent pas d’une classe commune, c’est le nom de propriété qui compte pour le binding). J’aurai ainsi une belle liste de noms de sociétés.

Mais visuellement je ne donnerais pas la possibilité à l’utilisateur de voir quels sont les clients et les prospects. Je ne pourrais pas non plus afficher la date de dernière facture pour les clients (car le binding échouera pour les prospects, laissant un libellé suivi d’un “vide”), pas plus que je ne pourrais indiquer la date de prochaine visite pour les prospects (même raison, même effet).

C’est une situation fâcheuse à plus d’un titre.

D’abord sur le plan du cahier des charges : si on me demande d’afficher ces informations complémentaires selon le cas (client ou prospect) il va bien falloir que je respecte le contrat...

Ensuite sur le plan de l’UX l’affichage du seul nom de société n’est vraiment pas très informatif. La distinction prospect / client et les informations supplémentaires qui pourraient être données simplifieraient grandement la tâche à l’utilisateur. Cela rendrait le logiciel plus convivial et plus “lisible”.

Bref, voici une situation simple dans laquelle il serait bien agréable de pouvoir disposer de DataTemplates personnalisés et qu’au runtime tout cela se débrouille tout seul sans avoir à écrire plein de code.

Aribba el DataTemplateSelector !

Tel un Zorro qui arrive toujours à pic, WPF nous offre un procédé assez simple et élégant, le DataTemplateSelector.

Mais ¡caramba! le fiston Silverlight ne supporte pas cette mécanique ... ¡Qué lástima!

Trêve de lamentations hispanisantes, voyons comment contourner ce problème.

Comment fait WPF ?

Avant de se lancer à l’assaut de son clavier et de dégainer l’artillerie lourde il est toujours préférable dans un tel cas de regarder comment fait WPF. D’abord il y a la documentation qui explique, puis il y a des tutors, et enfin il y a Reflector qui permet de décompiler le Framework et d’apprendre du code existant.

Je vais vous épargner cette démarche en résumant ici l’essentiel :

Création des DataTemplates

Comme il s’agit de fabriquer des DataTemplates, imaginons les deux cités plus haut (client / prospect). Pour l’exemple ils seront rudimentaire :

<DataTemplate x:Key="ClientTemplate">
  <StackPanel>
   <TextBlock Text="{Binding NomSociété}"/>
   <TextBlock Text="{Binding DateDernièreFacture}"/>
  </StackPanel>
</DataTemplate>
 
<DataTemplate x:Key="ProspectTemplate">
  <StackPanel>
   <TextBlock Text="{Binding NomSociété}"/>
   <TextBlock Text="{Binding ARevoirLe}"/>
  </StackPanel>
</DataTemplate>

Les DataTemplates seront définis dans un dictionnaire de ressources séparés ou localement, peu importe ce n’est pas la question ici.

Ce qui compte c’est que maintenant nous possédons deux DataTemplates, chacun ayant une clé différente et, bien entendu, un visuel différent (même si ici la différence n’est pas énorme).

Création du DataTemplateSelector

Ici il faut du code. En effet, la prise de décision, le choix entre un ou l’autre des DataTemplates (il pourrait y en avoir 5 ou 50 le procédé n’est pas limité à une alternative) peut dépendre de conditions aussi diverses que variées. Dans notre exemple nous utilisons le type de l’item pour choisir le template, mais dans un autre contexte les items peuvent être de même type et les templates seraient alors choisis en fonction de conditions, de valeurs précises de certaines propriétés. On peut envisager n’importe quoi.

Dès lors le moyen le plus simple, plutôt que d’inventer un codage pseudo-visuel qui aurait très vite ses limites, WPF laisse le développeur écrire un peu de code pour effectuer la sélection du template. C’est clair, ouvert et simple.

Pour cela on écrit une classe dérivant de DataTemplateSelector :

public class ClientProspectTemplateSelector : DataTemplateSelector
{
  public DataTemplate ClientTemplate { get; set; }
  public DataTemplate ProspectTemplate { get; set; }
 
  public override DataTemplate SelectTemplate(object item, 
    DependencyObject container)
  {
    return item is Client ? ClientTemplate : ProspectTemplate;
  }
}

Comme on le voit, rien de sorcier. On créée une classe dérivée de DataTemplateSelector dans laquelle on override la méthode protégée SelectTemplate. Celle-ci propose en paramètre une référence vers l’item concerné et vers le conteneur. Ce dernier ne nous intéresse même pas. Un simple test permet de retourner le DataTemplate ad hoc, selon que l’item est de type Client ou non.

On remarquera bien entendu les deux propriétés que nous avons déclarées, toutes les deux de type DataTemplate (une pour le template client, l’autre pour les prospects). Elles stockeront les références vers les templates alternatifs, ceux retournés justement par SelectTemplate.

Instancier le sélecteur

Nous disposons des deux DataTemplates et d’une classe dérivée de DataTemplateSelector qui sait à la fois stocker des références vers ces templates et retourner celui qui convient pour tout item qui lui est présenté.

Il faut maintenant instancier notre sélecteur. Le moyen le plus simple est d’utiliser la faculté de Xaml de créer une instance automatiquement liée à une clé dans une ressource :

<local:ClientProspectTemplateSelector 
   ClientTemplate="{StaticResource ClientTemplate}" 
   ProspectTemplate="{StaticResource ProspectTemplate}" 
   x:Key="clientProspectTemplateSelector" />

Le namespace “Local” sera défini dans le UserControl en cours (la fenêtre en général) pour pointer vers la déclaration du sélecteur. L’ensemble de la balise sera intégrée aux ressources locales de la fenêtre (mais on peut envisager une déclaration globale pour tout le projet, c’est une question de stratégie qui est hors sujet ici).

Utiliser le sélecteur

Ne reste plus qu’à utiliser notre sélecteur. Pour cela prenons une ListBox ou une ListView :

<Grid>
    <ListView ItemsSource="{Binding ElementName=This, Path=TestCollection}" 
              ItemTemplateSelector="{StaticResource clientProspectTemplateSelector}">
    </ListView>
</Grid>

On suppose ici que dans l’objet en cours (disons Window1 de type Window, nous sommes sous WPF) nous avons déclaré une propriété publique TestCollection qui contient la liste des clients et des prospects.

Ce qui nous intéresse plus particulièrement ce n’est pas l’ItemsSource, mais l’ItemTemplateSelector. Une propriété disponible sous WPF qui permet justement d’indiquer non pas un DataTemplate pour les items mais l’instance d’un sélecteur qui retournera les DataTemplates au moment voulu.

WPF montre la voie

Voilà... c’est aussi simple que ça et pourtant c’est un procédé d’une grande souplesse et d’une grande puissance. Il s’agit à la fois de programmation (mais très légère comme nous l’avons vu) et de Design, puisque le Designer doit avoir présent à l’esprit cette possibilité lorsqu’il “pense” une interface. Charge aux développeurs de lui fournir le code du sélecteur. C’est un peu comme pour les convertisseurs.

Emuler la fonction WPF sous Silverlight

Le procédé offert par WPF est si simple et si séduisant qu’on aimerait bien l’émuler à 100%. Hélas les contrôles ne possèdent pas de propriétés ItemTemplateSelector. Recréer la classe DataTemplateSelector, on le verra plus loin, est un jeu d’enfant et se fait en quelques lignes. En revanche le problème est bien de savoir comment l’utiliser de façon aussi simple que sous WPF...

On trouve plusieurs solutions sur Internet. Beaucoup ont choisi de partir sur la base d’un convertisseur de valeur (interface IConverter). Cela fonctionne mais je n’aime pas cette solution qui détourne le sens même des convertisseurs et dont la mise en œuvre m’apparait un peu distordue.

j’ai vu d’autres implémentations qui dans une logique jusqu’au-boutiste utilisaient une propriété attachée pour recréer la propriété ItemTemplateSelector simulant à 100% WPF mais au prix de contorsions qui, là aussi, m’apparaissent être des bricolages.

De plus, la plupart de ces solutions, si ce n’est toutes, ne sont pas “blendable”. Et c’est pour moi un critère essentiel. SI ça ne passe pas sous Blend, ce n’est pas utilisable.

La seule implémentation que j’ai vue qui émule au plus près WPF sans pour autant empêcher l’utilisation de Blend est celle de Raul Mainardi Neto publiée en 2010 sur The Code Project.

Vous pouvez bien entendu lire l’article directement (en anglais). Cela m’évitera en plus des redites.

Mais pour résumer la démarche :

On créée une classe abstraite DataTemplateSelector qui jouera le rôle de son homonyme WPF. Cette classe est très courte :

/// <summary>
/// WPF emulation. see WPF class of same name.
/// </summary>
public abstract class DataTemplateSelector : ContentControl
{
    /// <summary>
    /// Selects the template.
    /// </summary>
    /// <param name="item">The item.</param>
    /// <param name="container">The container.</param>
    /// <returns></returns>
    public virtual DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        return null;
    }
 
    /// <summary>
    /// Called when the value of the <see cref="P:System.Windows.Controls.ContentControl.Content"/> property changes.
    /// </summary>
    /// <param name="oldContent">The old value of the <see cref="P:System.Windows.Controls.ContentControl.Content"/> property.</param>
    /// <param name="newContent">The new value of the <see cref="P:System.Windows.Controls.ContentControl.Content"/> property.</param>
    protected override void OnContentChanged(object oldContent, object newContent)
    {
        base.OnContentChanged(oldContent, newContent);
 
        ContentTemplate = SelectTemplate(newContent, this);
    }
}

C’est une classe abstraite donc l’idée est, comme sous WPF, d’en créer des dérivées. Le principe retenu consiste à faire descendre la classe de ContentControl. La ruse sera de fournir un DataTemplate tout à fait “normal” au contrôle hôte, et d’y placer uniquement une instance d’un dérivé de DataTemplateSelector qui lui contiendra les variantes de template.

Mais continuons d’explorer ce petit code pour comprendre comment cela fonctionne.

DataTemplateSelector (version Silverlight donc) se contente de surcharger la méthode OnContentChanged, méthode protégée de ContentControl qui est appelée à chaque fois que la propriété Content est modifiée. Le code appelle la méthode originale mais en profite pour modifier la valeur de la propriété ContentTemplate du ContentControl. Car heureusement ContentControl possède une propriété fixant le template à appliquer à son contenu. Et ce ContentTemplate n’est pas modifié n’importe comment, il l’est en appelant une méthode virtuelle SelectTemplate qui recevra en paramètre à la fois le nouveau contenu et la référence vers le conteneur (le DataTemplateSelector). Toute l’astuce de cette implémentation est qu’elle semble calquer WPF à 100%, même nom de classe, même noms de méthodes, même stratégie. Mais en réalité elle est très différente.

Sous WPF ce sont ensuite les contrôles “normaux” qui exposent une propriété ItemTemplateSelector alors qu’ici notre DataTemplateSelector est un descendant de ContentControl, un conteneur que nous utiliserons à l’intérieur d’un premier DataTemplate (le “vrai” item template du contrôle visé). Tel un parasite, notre DataTemplateSelector s’accrochera ainsi à un template existant qu’il trompera en quelque sorte en substituant au cas le cas le véritable DataTemplate... Assez malin, tout en restant simple et compatible avec du templating sous Blend.

Généralement, comme je le disais dans l’exemple WPF, on ne se sert que de l’item pour baser le choix du template. Mais avoir une référence sur le conteneur peut certainement servir à couvrir des cas encore plus sophistiquées.

Une fois la classe DataTemplateSelector simulée sous Silverlight, le reste de la démarche ressemble à celle qui prévaut sous WPF... On écrit des DataTemplates, puis on écrit une classe dérivée de DataTemplateSelector, classe ayant une propriété par DataTemplate et une surcharge de SelectTemplate qui effectue le choix en fonction de l’item qui lui est passé.

Ne reste plus qu’à créer l’instance du sélecteur, exactement comme sous WPF et à l’utiliser.

C’est là que la différence existe avec WPF. Comme il n’existe toujours pas de propriété ItemTemplateSelector sous Silverlight depuis tout à l’heure... il va falloir jouer plus fin.

On va alors créer par les voies “normales” un DataTemplate d’item pour le contrôle (ListBox, ListView...). Blend va alors lier ce DataTemplate à l’objet afficheur (le ListView de l’exemple WPF). Au départ, le template est donc vide (sinon il faut le vider).

C’est là qu’on lui ajoute pour seul contenu une instance de notre descendant de DataTemplateSelector dont chacune des propriétés DataTemplate (les variantes de templates) sera liée aux ressources statiques correspondantes (chaque DataTemplate déclaré séparément).

En faisant de cette façon plutôt qu’en imbriquant les DataTemplates dans le descendant de DataTemplateSelector (ce qui est fait dans la solution de Raul Mainardi Neto) on garde la possibilité d’intervenir visuellement sur chaque DataTemplate sous Blend, sinon ce dernier ne les “voit” pas. Xaml étant tellement souple et permissif, que, forcément, Blend impose certaines limites et contraintes qui lui permettent de comprendre ce que le développeur veut faire et proposer ainsi les bons outils au bon endroit.

Code Exemple

Pour la mise en œuvre sur un exemple simple de l’émulation du DataTemplateSelector je vous renvoie à l’article de Raul. C’est plus pratique.

En revanche, je me suis amusé à mettre en pratique la chose sur un exemple un peu plus complexe qui utilise un TreeView. L’animal, venant du Toolkit, n’utilise pas des DataTemplates mais des HierarchicalDataTemplate (qui heureusement descendent des premiers).

C’est une solution que j’ai utilisé en situation réelle et que j’ai simplifiée à l’extrême pour l’exemple. Dans le contexte, il s’agit d’une gestion de droits d’utilisation. De façon ultra simplifiée : un client peut avoir plusieurs dossiers, et un utilisateur a soit accès tous les dossiers d’un client soit il n’a le droit d’accéder qu’à une sélection de dossiers.

Un moyen simple est d’utiliser un TreeView affichant la liste des clients (niveau 1). Chaque client possède une case à cocher. Si elle est cochée l’utilisateur voit tous ses dossiers. Sinon on peut développer le nœud client et on voit tous les dossiers, eux-mêmes précédés d’une case à cocher pour les sélectionner. Lorsque la case à cochée du client est checkée (donc accès tous les dossiers de ce client) les nouds dossiers deviennent disabled et sont tous décochés.

De même, lorsqu’il n’y a qu’une sélection de dossiers, le nombre de dossiers autorisés est affiché à côté du nom du client (ainsi que le nombre total de dossiers).

Enfin, je gère un cas spécial : on décoche un client, ce qui signifie qu’on ne donne accès qu’à une sélection de dossier mais on ne choisit aucun dossier dans liste. L’utilisateur n’a donc plus du tout accès à ce client. Pour matérialiser cette situation (possible), un petit carré rouge apparait alors à côté du nom du client.

C’est un exemple très simplifié d’une petite partie d’une application Silverlight. Ces explications sur le contexte vous aideront à mieux le comprendre mais le plus important c’est le code Sourire

Il comporte quelques astuces notamment dans la gestion des comptages, la façon de désactiver certains éléments dans les DataTemplates en utilisant un convertisseur, etc. Ce n’est pas bien gros et vous devriez finir par vous y retrouver. Le principal étant bien entendu la mise en pratique du DataTemplateSelector pour Silverlight...

Conclusion

Silverlight n’est pas fourni out-of-the-box avec tout ce que sait faire WPF même s’il s’en rapproche à chaque version un peu plus. Dans ce qu’on appelle les “scénarios avancés” on touche parfois les limites. Mais le plus souvent il suffit d’un peu de ruse et d’un soupçon de code pour rebondir et fournir ainsi une expérience utilisateur de même niveau qu’une application desktop WPF.

Le DataTemplateSelector est, une fois qu’on connait son existence, un objet essentiel qu’on regrette de ne pas avoir de base dans Silverlight tellement il ouvre de possibilités. Ce billet vous aura, je l’espère, fait découvrir cette feature de WPF qui n’est pas très compliquée à reproduire sous Silverlight.

Bon code !

Pour le source de mon exemple (projet VS 2010 SP1 + Silverlight Toolkit + Blend 4) :

blog comments powered by Disqus