Intermédiaire

BUILD 2015 : Databinding compilé

logo-build Windows 10 apporte de nombreuses nouveautés aux développeurs d’applications XAML, notamment du côté du databinding. Il s’agit probablement d’une des fonctionnalités les plus importantes (en tout cas ma préférée !), qui vient gommer le désavantage du binding actuel : le manque de performance.

Le Binding actuel

<TextBlock Text="{Binding FullName, Mode=OneWay}" />

Tous les développeurs XAML connaissent cette syntaxe pour connecter les données entre l’interface graphique et le modèle de données. Mais concrètement, comment ça marche ?

Au runtime, le framework XAML utilise la reflection pour retrouver les propriétés bindées et s’abonne aux événements adéquats pour être notifié en cas de modification.

Plusieurs soucis :
– Si on change le nom d’une propriété (par exemple FullName devient Name), le binding ne fonctionne plus. Et on ne le constate qu’au runtime, aucun avertissement, aucune erreur de compilation, rien !
– On le sait, la reflection n’est pas gratuite et impacte les performances. Il est d’ailleurs souvent conseillé de limiter le nombre de bindings, pour éviter de dégrader l’expérience sur les appareils peu puissants.

Mais tout ça, c’était avant !

Binding compilé

Le XAML des Universal Windows Apps apporte la compilation du databinding. Un parseur génère le code nécessaire pour lier l’UI aux données, le tout à la compilation.

<TextBlock Text="{x:Bind FullName, Mode=OneWay}" />

On voit la nouvelle syntaxe à base de {x:Bind}, qui le différencie de son grand frère le {Binding}. Ce binding est fortement typé et résolu à la compilation, ce qui nous épargne quelques fautes de frappe malheureuses, plus le droit à l’erreur !

Attention, par défaut le binding compilé est en mode OneTime, il faut explicitement demander du OneWay ou TwoWay, ce qui diminue les performances.

Pour répercuter les changements en OneWay, il faut toujours binder sur une classe qui implémente au choix :
INotifyPropertyChanged
Dependency Property
INotifyCollectionChanged / IObservableVector

Pour du TwoWay, il faudra que la propriété de l’UI soit de type Dependency Property. La TextBox reste un cas à part, puisque le binding sera mis à jour au moment du LostFocus.

Toutes les syntaxes habituelles sont supportées :
{x:Bind Model.FullName} : Chemin vers des sous-propriétés
{x:Bind Model.Address[0].Street} : Support de l’index dans une collection
{x:Bind Model.IsVisible, Converter={StaticResource BoolToVisilbity}} : Support des converters

Note importante : Dans la version actuelle du framework (au 14/05/15), les converters utilisés avec x:Bind doivent absolument être déclarés dans les ressources de l’Application. Autrement vous aurez droit à une exception au runtime.

Adieu DataContext

Avec le {Binding}, la source par défaut du databinding est le DataContext. Cette propriété provient de la classe FrameworkElement et peut être changée à tout moment.

Le compilateur a besoin de connaître le type des objets bindés pour les résoudre et générer le code correspondant. C’est pourquoi il n’était pas possible d’utiliser le DataContext qui est de type object.

Dès lors, le contexte du {x:Bind} est désormais la Page ou UserControl lui-même, il est fixe et ne peut être modifié. Le binding est compatible avec les champs et les propriétés.

La différence majeure se situe lorsqu’on souhaite binder sur un élément graphique de la page. Comme il s’agit d’une propriété de la Page, voici ce que le binding devient :

<!-- Binding actuel -->
<TextBlock Text="{Binding IsOn, ElementName=ToggleSwitch1}"/>

<!-- Binding compilé -->
<TextBlock Text="{x:Bind ToggleSwitch1.IsOn, Mode=OneWay}" />

On note bien que l’on peut directement accéder au ToggleSwitch, sans passer par ElementName ou RelativeSource=Self.

N.B.: Le TextBlock est notifié des changements de valeur de la propriété “IsOn”, car il s’agit d’une Dependency Property. Les DP possèdent un mécanisme de notification en cas de modification de sa valeur et sont compatibles avec le binding OneWay, comme indiqué précédemment.

Gain de performances

Quelques tests de performance ont été réalisés durant la session de la Build. Les résultats sont largement en faveur du binding compilé, sans surprise.

CPU

Binding sur le background de 1600 rectangles :

binding_benchmark

No Bindings xBind OneTime Binding OneTime xBind OneWay Binding OneWay
27ms 72ms 490ms 71ms 553ms
Changement : 16ms Changement : 60ms

On voit bien que x:Bind l’emporte sur le Binding classique, que ce soit pour l’initialisation ou le changement de valeur.

Mémoire

binding_memory

Le x:Bind est également moins gourmand en mémoire puisqu’en comparaison, le Binding opère tous ses traitements au runtime.

Pour l’anecdote, l’application People, après adoption du binding compilé, a vu une amélioration de 11% au démarrage, et 21% de réduction dans l’utilisation mémoire.

DataTemplates

Type à préciser

Comme x:Bind est fortement typé, le framework a besoin de connaître le type appliqué à un DataTemplate à la compilation. On le spécifie avec un nouveau markup : x:DataType.

ResourceDictionary

Pour profiter du binding dans les ResourceDictionary, par exemple pour y déclarer des DataTemplates, il faut désormais :
– Lier le ResourceDictionary à une classe dans laquelle le code behind pourra être généré
– S’assurer que le code behind est une partial class qui appelle InitializeComponent() dans son constructeur
– Déclarer le ResourceDictionary dans les MergedDictionaries en l’instanciant directement, et non via une ResourceDictionary + Source

<!-- ResourceDictionary déclaré dans un fichier et contenant un DataTemplate -->
<ResourceDictionary
    x:Class="xBindSampleCS.MyTemplates"
    xmlns:model="using:xBindSampleModel">

    <DataTemplate x:Key="FullNameTemplateInRDFile" x:DataType="model:IEmployee">
        <TextBlock Text="{x:Bind Name}" />
    </DataTemplate>

</ResourceDictionary>
// Code behind associé au fichier ci-dessus
namespace xBindSampleCS
{
    public partial class MyTemplates
    {
        public MyTemplates()
        {
            InitializeComponent();
        }
    }
}
<!-- Déclaration du ResourceDictionary via instanciation -->
<UserControl.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
	       <!-- avec binding -->
            <local:MyTemplates/>
            <!-- sans binding -->
            <ResourceDictionary Source="filename" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</UserControl.Resources>

Rendu progressif des éléments d’une liste

Dans les épisodes précédents

Lorsqu’on scrolle dans une liste virtualisée, plusieurs choses se déroulent. Tout d’abord, lorsqu’un item disparaît de l’écran, celui-ci va être recyclé : on réutilise ses composants graphiques pour les réafficher avec un contexte de données différent. Ainsi, on évite de réinstancier les contrôles graphiques, une opération coûteuse pour le CPU.

Pour éviter que cette opération de recyclage soit trop visible pour l’utilisateur, elle est au maximum effectuée en dehors de l’écran. Problème : si le PC ou téléphone est trop lent et qu’on scrolle rapidement, l’opération devient visible à l’écran, une expérience utilisateur qu’on préférerait éviter.

Pour plus de détails sur le principe de virtualisation dans les listes, un excellent article sur le sujet : ListView basics and virtualization concepts.

Pour remédier à ce problème, le SDK Windows 8.1 proposait l’évènement ContainerContentChanging. Il était possible de prioriser l’affichage de certains éléments, histoire d’éviter l’effet “page blanche” lorsqu’on scrolle rapidement. Malheureusement, l’implémentation nécessitait un code complexe, souvent au détriment du databinding.

x:Phase à la rescousse

Avec Windows 10, x:Phase vient à notre rescousse ! Ce merveilleux attribut allège le CPU en priorisant l’ordre d’affichage des bindings au sein d’un DataTemplate.

<DataTemplate x:DataType="model:Profile">
  <StackPanel Width="200" Height="100">
    <Image Source="{x:Bind ImageUrl}" x:Phase="1" />
    <TextBlock Text="{x:Bind Name}" />
  </StackPanel>
</DataTemplate>

Exemple classique : une liste contient des images et du texte. Il semble logique de prioriser le chargement du texte en premier, puis l’image en second. Non seulement cela améliore les performances lors du scroll, mais on évite en prime de charger inutilement certaines images qu’on passerait trop rapidement.

Le rendu se fera donc dans l’ordre spécifié par les différentes x:Phase. Plusieurs points à noter :
– Les nombres n’ont pas besoin d’être contigus
– Il est recommandé d’avoir un nombre de phases raisonnable, au risque de perdre ses bénéfices

Binding sur évènements

Oui, ils l’ont fait : il est possible de binder un événement de l’UI vers une méthode !

<Button Click="{x:Bind Model.Profiles[0].Coucou}">Fais coucou</Button>

Dans ce cas, la méthode “bindée” peut avoir plusieurs signatures :

// Aucun paramètre
void Coucou() {…}

// Correspond aux paramètres de l'évènement
void Coucou(object sender, RoutedEventArgs e) {…}

// Correspond aux paramètres de base / interface de l'évènement
void Coucou(object sender, object e) {…}

L’overloading n’est pas supporté, le binding cherchera la première version de Coucou si plusieurs sont déclarées.

Tous les évènements sont supportés, et on voit bien son utilité en remplacement des commandes (ICommand), habituellement utilisées en MVVM. A noter cependant qu’il n’y a pas de support pour les paramètres de commande, ou la validation via ICommand.CanExecute.

Sous le capot

Ok, la syntaxe change et on gagne en performance, en utilisation mémoire, etc. Mais concrètement, comment ça marche ?

Tous les bindings provenant du x:Bind sont en réalité générés à la compilation. Il suffit de se rendre dans le dossier obj/ du projet pour y trouver une classe partielle “XXX.g.cs” étendant la Page/UserControl. C’est cette classe qui est enrichie à la compilation et implémente principalement le mécanisme de binding.

Quand le XAML est chargé, l’événement Loaded signale lorsque le rendu graphique est terminé. Le framework XAML voit l’ajout de l’évènement Loading, déclenché avant Loaded et au “dernier meilleur moment” pour initialiser le binding.

binding_code_gen

Pour accéder aux bindings générés, on peut manipuler le champ “Bindings” de la Page. Plusieurs méthodes s’offrent à nous :
Initialize() : appelée au Loading pour initialiser le binding
Update() : utile pour mettre à jour tous les bindings (incluant les OneTime bindings), par exemple suite à un appel asynchrone.
N.B.: On ne peut pas mettre à jour un binding en particulier, c’est du tout ou rien.
StopTracking() : stoppe la mise à jour automatique des bindings, pratique lorsqu’on fait un traitement de masse. Pour le réactiver, appeler Bindings.Update().

Considérations

La conversion des données se fait automatiquement dans certains cas :
– Vers une string : utilise le .ToString()
– Depuis une string : utilise le parser XAML (ex.: lorsqu’on initialise une valeur de Background avec “Red”, automatiquement retranscrit en Colors.Red).
– Autrement : si la conversion nécessite du code, il faudra utiliser des converters

Le mode OneWay coûte plus cher que OneTime. La méthode Bindings.Update() peut être appelée pour mettre à jour tous les OneTime bindings, ce qui peut s’avérer plus efficace que du OneWay dans certains scénarios.

Et les limitations ?

Pourquoi avoir ajouté une nouvelle annotation au lieu de remplacer le comportement du {Binding} actuel ?

La raison est simple, x:Bind est légèrement différent et possède plusieurs limitations :
Setter de Style : Il n’est pas possible d’utiliser x:Bind dans le Setter d’un Style
– Comme aucun objet de type Binding n’est généré, les bindings compilés ne peuvent être générés ou modifiés au runtime (forcément, ils sont “compilés”…)
– Dynamic incompatible : Le binding compilé ayant besoin d’un typage fort, il n’est pas compatible avec des objets dynamiques
– Réutilisation des DataTemplates : les DataTemplates demandent un x:DataType, il n’est donc plus possible de réutiliser un template d’un modèle à l’autre (dans le cas où les noms des propriétés sont identiques)
RelativeSource=TemplatedParent n’est pas supporté, x:Bind ne fonctionne pas au sein d’un control template. TemplateBinding reste l’alternative optimisée pour ce genre de scénario.

Une autre limitation peu commode, issue d’un scénario relativement usuel :

<DataTemplate>
   <!-- On souhaite réagir au clic du bouton -->
   <Button x:Name="CoucouButton" Click="OnClick" Content="{Binding Name"} />
</DataTemplate>
private void OnClick(object sender, EventArgs args)
{
    // Récupération de l'élément sur lequel on a cliqué (le bouton en réalité)
    var element = sender as FrameworkElement;
    // Récupération du contexte de données
    var profile = element.DataContext as Profile;

    if (profile != null)
    {
        // Traitement
    }
}

Comme le DataContext n’existe plus avec x:Bind, il est plus difficile de récupérer le contexte de données d’un DataTemplate. Une possibilité serait de plutôt se baser sur le binding sur événement et déclencher une méthode dans l’objet Profile.

De même, voici un autre scénario classique :

<DataTemplate>
    <! -- Astuce pour changer le contexte de données du Binding -->
    <Button Command="{Binding DataContext.ClickCommand, ElementName=Root}" />
</DataTemplate>

Le x:Bind ne permet pas de sortir du contexte de binding du DataTemplate qui est imposé par l’attribut x:DataType. Il faudra donc ruser autrement parmi toutes les autres possibilités offertes !

Conclusion

On peut réellement parler de révolution du binding au service des performances. Toutes ces nouveautés feront le bonheur des développeurs XAML sous Windows 10, attention néanmoins à bien en saisir les limites. Evidemment, le binding classique reste disponible et nous rendra probablement encore quelques services.

Liens utiles

– Sample Binding Windows 10
– Data Binding: Boost Your Apps’ Performance Through New Enhancements to XAML Data Binding

Nombre de vue : 235

COMMENTAIRES 2 commentaires

  1. Nk54 dit :

    Super article, j’adore cette nouvelle syntaxe. Mais on risque de voir de plus en plus de code différent d’un projet à l’autre. Ca sera peut être moins évident pour récupérer les paramètres.

    Je pense me servir de cette technique plus souvent dans mes user controls réutilisables où je n’ai souvent pas de viewModel dédié et toutes les property sont donc dans la classe partielle. Du coup, accessible sans avoir besoin de passer par le DataContext.

    Le binding sur event, j’aime moyen. Je préfère avoir la possibilité de passer mes CommandParameter et gérer mon CanExecute. De plus, l’ancienne méthode est quasiment devenu un standard : écriture quasi identique dans Xamarin. Je ne pense pas que ce type de binding sera supporté sous peu.

    Ps: rien à voir, à quand la possibilité d’ajouter une URL pour avoir des flux rss dans Quoties, exemple : tes derniers articles de blog 🙂

    Merci encore pour l’article

  2. […] en savoir plus sur le binding, je vous invite à lire l’article de Cyril Cathala sur le binding […]

AJOUTER UN COMMENTAIRE