See Sharp
Is it really just a matter of point of view ?

This silver is kinda light on the bindings : you can't do them with style !

February 2, 2010 14:00 by salfab

Depuis peu, voilà qu'il m'est donné la possibilité de jouer quelque peu avec Silverlight :

Beaucoup de choses disponibles en WPF ne le sont pas en Silverlight. Habituellement, des choses facilement contournables, mais la petite aventure du jour mérite certainement d'être consignée : 

Silverlight en tous cas jusqu'à la version 3 ne supporte pas le databinding dans les style setters.

Voyons un cas dans lequel ceci peut être embêtant :

Imaginons une ListBox dont on voudrait remplacer le pannel par défaut par un Canvas, de façon à y positionner les Items. Leur position serait définie par des coordonnées stockées dans les éléments eux-même, et exposées par une DependencyProperty.

Malheureusement, le contrôle ListBox encapsule ses éléments dans un ListBoxItem. Si l'on modifie le ItemTemplate pour ajouter les propriétés attachées Canvas.Top et Canvas.Left à l'élément racine du DataTemplate, l'effet ne sera pas appliqué.

Ceci s'explique par le fait que l'élément "racine" du DataTemplate ne sera pas directement dans le canvas : il sera contenu dans un ListBoxItem. Il faut donc définir les propriétés attachées Canvas.Left et Canvas.Top dans le ListBoxItem.

Ceci est faisable grâce à la propriété ItemContainerStyle qui contient le style du ListBoxItem.

<ListBox.ItemContainerStyle>
   
<Style>
       
<Setter Property="Canvas.Top" Value="{Binding Path=Y}" />
        
<Setter Property="Canvas.Left" Value="{Binding Path=X}" />
   
</Style>
</ListBox.ItemContainerStyle>

En WPF, ceci ne pose pas de problème, on peut définir avec un Style Setter, les propriétés attachées du Canvas, en leur appliquant un Binding.

En Silverlight, malheureusement, s'il est effectivement possible de donner des valeurs fixes pour les propriétés attachées du Canvas, il n'est pas possible de procéder de la sorte pour récupérer par Binding les coordonnées auxquelles placer l'élément : comme le DataBinding n'est pas supporté dans les style setter, il faudra trouver un moyen de créer ce binding dans le codebehind.

Ceci sera faisable en sous-classant le contrôle ListBox et en créant un override de la méthode PrepareContainerForItemOverride(DependencyObject element, object item), dans laquelle il sera possible de définir le Binding par code :    

        FrameworkElement contentitem = element as FrameworkElement; 
       
Binding leftBinding = new Binding("Left"); // "Left" is the property path that you want to bind the value to. 
        contentitem
.SetBinding(Canvas.LeftProperty, leftBinding); 
 
       
base.PrepareContainerForItemOverride(element, item); 

À noter que ItemsControl encapsule également ses éléments dans un ItemContainer, mais ItemContainerStyle n'est pas exposé par ItemsControl en Silverlight...


Tips for Binding On ToolTips

July 24, 2008 14:41 by salfab

Introduction 

Mon quotidien Allemand est principalement axé sur la technologie WPF, ces temps.

Au programme d'aujourd'hui : Un outil permettant la localisation de ressources :

Charger un fichier XAML contenant un ResourceDictionnary, en extraire toutes les ressources de type sys:String, et les afficher à l'écran. L'idée est de pouvoir éditer lesdites chaines, nous allons donc les charger dans une TextBox.

Et puisqu'il est intéressant de pouvoir se rappeller quelle était la valeur par défaut : Nous voulons afficher dans le ToolTip, la valeur originale.

Implémentation 

En admettant que nous ayons un XmlDataSource déclaré dans le XAML ( ne pas oublier de déclarer aussi les différents namespaces XML !! ) nommé xmlTranslationProvider.

Pour utiliser un contrôle visuel léger, utilisons ItemsControl : Puisque nous n'utilisons pas les mécanismes de sélection d'éléments, nous pouvons nous passer de l'overhead d'une ListBox.

Nous devrons ensuite lier l'ItemSource de ItemsControl à notre XmlDataProvider, afin de définir quels seront les Items à afficher : Les éléments de type sys:String.

<ItemsControl ItemsSource="{Binding Source={StaticResource xmlTranslationProvider}, XPath=sys:String }" >

Pour définir le rendu de chacun de nos Items : Définissons le template de ItemsControl.ItemTemplate, où nous insèrerons la TextBox qui servira à la traduction.

Intuitivement, les Bindings appliqués devraient ressembler à celà :

<TextBox Name="tbTranslationField" Text="{Binding XPath=.}" Grid.Column="0" ToolTip="{Binding XPath=.,Mode=OneTime}" />

Le binding de ItemsControl.ItemsSource définissant quels seront les items, nous définissons ici comment les représenter : La propriété Text affichera la valeur du noeud, de même pour le ToolTip, à l'exception près que nous voulons que le ToolTip garde sa valeur d'origine, d'où le Mode OneTime. OneTime permet de ne recharger les données que lorsque l'application démarre, où que le DataContext est changé.

Les ennuis commencent

Malheureusement, dans les faits, à chaque fois que le ToolTip est affiché, il contient les dernières modifications apportées à la chaîne à traduire.

Pour quelle raison ?

Analysons la démarche :

Créons un nouveau TextBox dont la propriété Text possèdera un Binding identique au ToolTip ci-dessus : Le comportement est celui que nous attendions : son contenu est mis à jour une seule fois, et n'est jamais changé, en dépit du fait que la donnée liée, elle, change : Le mode OneTime fonctionne : Que se passe-t-il ?

Le TextBox est généré au lancement de l'application. il va chercher les données auxquelles il est lié, l'affiche à l'écran, et ne les touche plus.

Explications

Qu'y a-t-il de différent avec un ToolTip ?

Le ToolTip attend que le pointeur de la souris passe au dessus de la zone appropriée, puis à ce moment là, et uniquement à ce moment là, il est construit : Il va donc chercher les données auxquelles il est lié, et ne reçoit plus jamais de notification en cas de modification des données auxquelles il est lié.

Si l'utilisateur déplace la souris, le ToolTip est détruit.

Si l'utilisateur déplace la souris à nouveau au dessus de la zone sensible, le ToolTip est à nouveau reconstruit de A à Z. Il va donc rechercher les informations auxquelles il est lié, les affiche à l'écran, et ne reçoit plus jamais de notification en cas de modification des données liées.

Que se passe-t-il si l'utilisateur, entre la destruction du ToolTip et sa nouvelle création, modifie les données auxquelles le ToolTip est lié ?

Le ToolTip est créé, puis il va rechercher les données auxquelles il est lié. Mais attention, nous avons spécifié le mode OneTime, WPF possède donc un mécanisme pour s'assurer que les données liées ne doivent être chargées qu'au démarrage de l'application ou lors d'un chargement de DataContext....

Mais alors pourquoi diable, mon ToolTip change-t-il tout de même de valeur ??

La réponse est simple : Les données liées n'ont pas changé !!!

Et pourtant le ToolTip, lui, ne possède pas le même contenu ! Pourquoi ?

Tout ceci est dû au fait que nous avons fait une erreur de sémantique dans notre Binding : Nous avons en effet lié un Noeud XML à notre TextBox et à notre ToolTip, et non pas une chaîne de caractère. L'objet XmlNode en question, qui est lié à nos propriétés ToolTip et Text, n'a absolument pas été changé. Il a seulement été altéré !

Les ValueConverters très puissants de WPF ont simplement converti un noeud XML en représentation sous forme de string à notre place.

Ainsi, lorsque le ToolTip est créé après que le Noeud XML auquel il est lié ait été altéré, le mécanisme de WPF s'assure qu'il n'ait pas été changé, et le ToolTip peut sans autre lire le texte se trouvant dans le XmlNode, cependant, celui-ci a été changé.

C'est de par la nature même du ToolTip que l'on peut observer ces différences de comportement entre un TextBox qui est rendu à l'écran une fois pour toutes, et un ToolTip qui est recréé à partir de zéro plusieurs fois au cours du cycle de vie de l'application.

Résoudre le problème :

L'idée est donc d'indiquer au mécanisme du mode OneTime de s'assurer non-pas que l'objet XmlNode reste inchangé, mais bien que le texte qu'il contient reste le même ! Le texte en question est représenté par la propriété InnerText de l'objet XmlNode.

Par conséquent, nous allons modifier les Bindings de notre TextBox comme suit :

<TextBox Name="tbTranslationField" Text="{Binding XPath=., Path=InnerText}" Grid.Column="0" ToolTip="{Binding XPath=.,Mode=OneTime, Path=InnerText}" />

A noter que le même genre de subtilités survient lorsqu'une DependencyProperty de type List<T> (n'implémentant pas INotifyCollectionChanged) est altérée : Si l'on ajoute un élément à une List<T>, la propriété référencant cette liste n'est pas modifiée, car l'adresse mémoire de l'objet pointé par la propriété est resté le même, en dépit du fait que l'objet lui même ait été altéré.

En eséprant que cet article puisse aider quelques personnes à mieux comprendre les subtilités du binding.

Your code is running even though the program is stopped (And the truth is out there)

February 14, 2008 09:15 by salfab

Récemment, je me suis trouvé face au cas fort étrange d'une application WPF donc l'interprétation du XAML par le designer générait une erreur alors que l'exécution du programme compilé se déroulait sans encombre. Phénomène normal ou paranormal ? La vérité est ici !

Tout d'abord, situons un peu le cadre du problème. Afin d'avoir un exemple simple pour illustrer le problème, j'ai créé un exemple de toute pièce.

Il s'agit d'une application WPF, et comme toute bonne application WPF, elle contient des Binding : Une TextBox qui est liée à une DependencyProperty (DP) de type int. La valeur de cet int sera située entre 1 et 7, et indiquera le nom du jour à afficher dans la textbox. malheureusement, comme nous sommes liés à un entier, WPF n'a aucun moyen de savoir que nous voulons afficher un nom de jour de la semaine.

Afin de directement couper cours aux NitPickers, je tiens à préciser qu'il s'agit ici d'un exemple un peu tiré par les cheveux mais qui a le mérite d'illustrer des problèmes du même acabit qui pourraient être rencontrés dans des contextes bien plus complexes. Dans un cas d'application réel, nous aurions utilisé l'enum DayOfWeek. Maintenant que ceci est clarifié, revenons-en au coeur du problème.

Si l'on se contente de faire un Binding entre une textbox et un entier, la représentation donnée par la méthode ToString apparaitra dans la textbox. Pour nous permettre de convertir notre entier en nom de jour de la semaine, nous allons utiliser un Value Converter.

les value converters permettent d'ajouter la tuyauterie manquante pour adapter la valeur de la source à celle que la cible attend.

Si par malheur, dans notre ValueConverter, nous utilisons un objet qui n'a pas de valeur par défaut, ou n'a pas été instancié par le constructeur d'une ressource déclarée dans le XAML - Les ressources déclarées dans le XAML sont également instanciées, et ce, même en mode design - la compilation du projet en cours se passera sans encombres, l'exécution également, mais le designer ne pourra pas afficher la fenêtre du programme à l'écran et ajoutera une erreur dans la fenêtre ErrorList. L'erreur qui sera mentionnée peut prendre plusieurs formes. Il s'agit généralement d'une erreur de référence nulle sur un objet, mais il arrive parfois d'avoir quelques surprises. Nous verrons en détails pourquoi. Si nous naviguons jusqu'à a portion de code concernée, nous verrons qu'elle se trouvera dans le XAML, sur l'élément auquel nous avons appliqué un Binding.

Pourquoi donc notre code fonctionne-t-il au runtime, et pas en mode design ? 

Dans le projet d'exemple attaché, nous avons initialisé un tableau de chaînes de caractères dans le constructeur de App. Ce constructeur, n'a aucune raison d'être exécuté par le designer puisqu'aucune application n'a été instanciée, jusqu'ici, rien de surprenant. Un comportement que les développeurs de contrôles utilisateurs sous winforms auront certainement déjà rencontré est le fait que lors du design d'une application utilisant un contrôle utilisateur, le contrôle vit. Il possède son propre comportement en mode design, et d'une certaine façon, il est, lui, déjà en cours d'exécution.

En ce qui concerne le Binding en WPF, il s'agit du même phénomène : Le fait de permettre l'affichage à l'écran en live des modifications de notre interface graphique implique qu'une certaine logique applicative décrive la façon dont ces modifications seront reflètées. Dans la plupart des cas, cette logique applicative est incluse dans la framework .Net et est tout à fait transparente pour le développeur, cependant, dans noter cas, nous utilisons une fonctionnalité avancée pour nous permettre de crocher notre propre logique applicative dans le processus de rendu.

Afin de pouvoir continuer à reflèter les changements lors du design de façon cohérente, le designer va prendre en compte notre ValueConverter, et l'exécuter. Si celui-ci contient un bug, une exception sera générée. Généralement, il s'agit d'une erreur de type Object reference not set to an instance of an object, mais dans certains cas, l'erreur mentionnée peut être d'un autre type, par exemple, une erreur de casting. Ceci s'explique par le fait qu'au lieu d'être levée et affichée dans une fenêtre d'exception comme au runtime, elle sera affichée dans la fenêtre ErrorList. Celle-ci ne permettant pas de naviguer dans les InnerExceptions, elle affiche uniquement l'exception de niveau le plus élevé. Le choix de représenter ces erreurs dans la fenêtre ErrorList n'est pas anodin. En utilisant cette représentation, il est possible de signaler plusieurs erreurs simultanément, chose qui ne serait pas possible avec l'affichage séquentiel de plusieurs fenêtres d'exceptions, et qui pourrait être réellement handicapant dans le cas où un nombre important d'erreurs serait commis.

Il existe diverses manières de résoudre ce problème :

  • La première, qu'il est recommandé d'adopter, est de revoir le design de l'application : Les cibles de Binding sont généralement des DependencyProperties. Il est également possible d'utiliser des collections ou des propriétés qui implémentent INotifyPropertyChanged ou INotifyCollectionChanged. Dans ce cas, il est recommandé d'opter plutôt pour une DependencyProperty, qui permettent, elles, de définir une valeur par défaut. Il conviendra de ne pas choisir la valeur null comme valeur par défaut, bien évidemment.
  • Il n'est malheureusement pas toujours possible de changer le design de son application sans remettre en cause une grande partie du code de l'application. Il existe donc une autre façon de procéder, moins élégante, mais qui peut parfois sauver des vies : De la même façon qu'en WinForms il existe la propriété DesignerMode, WPF tient à jour une DependencyProperty IsInDesignMode, accessible par la méthode GetIsInDesignMode. Cette méthode fait partie de la classe DesignerProperties.

En attachement, un petit projet Visual Studio 2008 dont certaines portions de code sont en commentaires, N'hésitez pas à vous amuser à commenter / décommenter les différentes portions du code pour mettre en action les différents comportements.

XFiles.zip (12,58 kb)