Un modèle, une UI et… un pont

Lorsque l’on utilise WPF et son sympathique moteur de Binding, il peut arriver que l’on se retrouve avec une interface utilisateur trop proche du modèle de données.

Je m’explique, généralement, nous avons un élément d’interface lié à une propriété du modèle. Un problème qui peut se poser, c’est que notre modèle reflète l’interface utilisateur à tout instant, par exemple un Slider modifiera le modèle à chaque changement de valeurs alors que cela peut être inutile. Selon comment le modèle gère ses propriétés, nous pouvons nous retrouver avec des problèmes, par exemple si un traitement lourd est fait à chaque modification, ou bien si les modifications trop fréquentes ont des répercussions indésirables. Une des solutions très simple à mettre en place est d’indiquer au Binding qu’il ne doit mettre à jour la Source qu’à la perte du focus (UpdateSourceTrigger=LostFocus). Cela permet donc à l’utilisateur de modifier les valeurs depuis l’UI et de ne les valider qu’au moment où celui a vraiment fait son choix.

Mais imaginons que notre UI reflète plusieurs fois la même propriété. Prenons encore le cas d’un Slider (pour un changement de valeur rapide à la souris) mais cette fois associé à une TextBox (pour un changement précis). Si ces deux contrôles utilisent un Binding avec un UpdateSourceTrigger à LostFocus, la valeur de l’un ne sera mise à jour que lorsque la valeur de l’autre aura changé, le modèle avec. En bref, pour l’utilisateur ce n’est pas forcément l’idéal vu qu’il n’aura pas en direct l’information qu’il va valider.

Ce qui serait intéressant, c’est que l’utilisateur puisse voir en temps réel la propriété de la TextBox changer en même temps que le Slider, sans que le modèle ne soit modifié. Bien évidemment, quand le modèle est modifié, il faut que l’UI se mette à jour directement.
Les choses se corsent un tout petit peu. Mais la solution n’est pas forcément complexe. En effet, plutôt que de lier mon UI directement au modèle, je n’ai qu’à la lier à une autre propriété qui reflètera en permanence la valeur donnée par l’UI, et une fois l’interaction avec l’UI terminé, je n’aurais qu’à pousser la valeur dans le modèle.

Pour cela, j’ai mis au point ce que j’ai appelé un PropertyBridge. En gros, c’est ce composant qui va servir de pont entre le modèle et l’UI.

Le PropertyBridge est donc lié directement au modèle, tandis que l’UI est liée au PropertyBridge. Pour cet article, le modèle sera tout simple :

  1. public class Model : INotifyPropertyChanged
  2. {
  3.     public event PropertyChangedEventHandler PropertyChanged;
  4.  
  5.     private void NotifyPropertyChanged(string propName)
  6.     {
  7.         if (PropertyChanged != null)
  8.         {
  9.             PropertyChanged(this, new PropertyChangedEventArgs(propName));
  10.         }
  11.     }
  12.  
  13.     private string m_modelValue = "–";
  14.  
  15.     public string ModelValue
  16.     {
  17.         get { return m_modelValue; }
  18.         set { m_modelValue = value; NotifyPropertyChanged("ModelValue"); }
  19.     }
  20. }

Et tout aussi simple du côté de la vue :

  1. <Grid>
  2.     <Grid.DataContext>
  3.         <local:Model />
  4.     </Grid.DataContext>
  5.     <StackPanel>
  6.         <Slider Value="{Binding ModelValue}" />
  7.         <TextBlock><Run Text="Valeur du mod?le : " /><Run Text="{Binding ModelValue}" /></TextBlock>
  8.        
  9.         <local:PropertyBridge x:Name="Bridge" ModelProperty="{Binding ModelValue}" />
  10.         
  11.         <TextBox Text="{Binding ElementName=Bridge, Path=UIBridgeProperty, Mode=TwoWay}" />
  12.         <Slider Value="{Binding ElementName=Bridge, Path=UIBridgeProperty, Mode=TwoWay}">
  13.             <i:Interaction.Behaviors>
  14.                 <local:MouseUpBridgeBehavior Bridge="{Binding ElementName=Bridge}" />
  15.             </i:Interaction.Behaviors>
  16.         </Slider>
  17.     </StackPanel>
  18. </Grid>

Nous avons donc notre modèle dans le DataContext. Un Slider pour modifier directement le modèle (avec l’aperçu de la valeur directement en dessous), ainsi qu’un Slider et une TextBox pour modifier le modèle à travers le PropertyBridge.

Mais au final, il fait quoi ce PropertyBridge ? En fait, celui-ci est composé de deux propriétés. Une liée au modèle, et l’autre sur laquelle les contrôles UI sont liés, tout cela, en Binding TwoWay. Nous voilà donc avec un problème réglé : Tous les contrôles sont liés à la même propriété donc un changement dans l’un se répercutera dans l’autre, sans affecter le modèle.

Voici donc le code du PropertyBridge. Notez qu’il hérite de FrameworkElement pour pouvoir être ajouté à l’arbre visuel (si quelqu’un connait un moyen d’éviter ça🙂 ).

  1. public class PropertyBridge : FrameworkElement
  2. {
  3.     public object ModelProperty
  4.     {
  5.         get { return (object)GetValue(ModelPropertyProperty); }
  6.         set { SetValue(ModelPropertyProperty, value); }
  7.     }
  8.  
  9.     // The DependencyProperty that is bound the model
  10.     public static readonly DependencyProperty ModelPropertyProperty =
  11.         DependencyProperty.Register(    "ModelProperty",
  12.                                         typeof(object),
  13.                                         typeof(PropertyBridge),
  14.                                         new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
  15.  
  16.     public object UIBridgeProperty
  17.     {
  18.         get { return (object)GetValue(UIBridgePropertyProperty); }
  19.         set { SetValue(UIBridgePropertyProperty, value); }
  20.     }
  21.  
  22.     // Using a DependencyProperty as the backing store for UIBridgeProperty.  This enables animation, styling, binding, etc…
  23.     public static readonly DependencyProperty UIBridgePropertyProperty =
  24.         DependencyProperty.Register("UIBridgeProperty", typeof(object), typeof(PropertyBridge),
  25.         new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)
  26.         {
  27.             DefaultUpdateSourceTrigger = System.Windows.Data.UpdateSourceTrigger.Explicit
  28.         });
  29. }

 

Nous avons donc juste deux propriétés avec quelques configurations de base. Les deux propriétés utilisent par défaut un Binding TwoWay. Petite particularité de la propriété côté UI, celle-ci utilise un Binding avec l’UpdateSourceTrigger à Explicit.

Bien évidemment, en l’état actuel, le modèle ne sera jamais modifié… Bref, pour l’instant, ça ne sert à rien. On pourrait dire que nous avons commencé à construire un pont, sans relier les deux rives.

Nous avons donc une propriété spécifique à l’UI qui sera mise à jour directement par l’UI sur laquelle vont se lier tous les contrôles permettant de modifier la valeur du modèle. L’autre propriété elle est directement relié au modèle. Si nous regardons dans l’ensemble, nous avons un pont avec d’un côté une synchronisation avec le modèle, et de l’autre une synchronisation avec l’UI.

Il nous faut donc créer le lien entre ces deux parties en respectant certaines conditions :

  • Les mises à jour du modèles sont immédiatement répercutées dans l’UI.
  • Les mises à jour de l’UI ne sont poussées dans le modèle que de manière explicite.

    L’intérêt du Binding Explicit est donc immédiat. Il nous faut seulement définir un Binding à l’intérieur du PropertyBridge, la propriété de l’UI devant être lié à la partie Modèle, le tout en permettant au PropertyBridge de mettre à jour la source du Binding de manière explicite :

    1. public PropertyBridge()
    2. {
    3.     Binding internalBinding = new Binding();
    4.     internalBinding.Source = this;
    5.     internalBinding.Path = new PropertyPath("ModelProperty");
    6.  
    7.     this.SetBinding(PropertyBridge.UIBridgePropertyProperty, internalBinding);
    8. }
    9.  
    10. public void PushUIBridgePropertyToModel()
    11. {
    12.     this.GetBindingExpression(PropertyBridge.UIBridgePropertyProperty).UpdateSource();
    13. }

    Il nous reste au final une seule chose : Comment l’UI va dire au PropertyBridge « Tu peux mettre à jour le modèle » ? Etant donné que le but est d’être au plus proche du contrôle, il y a peu de possibilités. Soit refaire un contrôle qui sera capable de se brancher sur le PropertyBridge, soit en passant par un Behavior qui sera branché sur le PropertyBridge. Dans mon cas, j’ai opté pour la deuxième solution. L’avantage étant d’éviter d’hériter d’un contrôle juste pour y ajouter un comportement assez spécifique, et de pouvoir éventuellement utiliser le même Behavior pour plusieurs contrôles.

    1. public class MouseUpBridgeBehavior : Behavior<FrameworkElement>
    2. {
    3.     public PropertyBridge Bridge
    4.     {
    5.         get { return (PropertyBridge)GetValue(BridgeProperty); }
    6.         set { SetValue(BridgeProperty, value); }
    7.     }
    8.  
    9.     // Using a DependencyProperty as the backing store for Bridge.  This enables animation, styling, binding, etc...
    10.     public static readonly DependencyProperty BridgeProperty =
    11.         DependencyProperty.Register("Bridge", typeof(PropertyBridge), typeof(MouseUpBridgeBehavior), new UIPropertyMetadata(null));
    12.  
    13.         
    14.  
    15.     protected override void OnAttached()
    16.     {
    17.         base.OnAttached();
    18.  
    19.  
    20.         AssociatedObject.PreviewMouseUp += new System.Windows.Input.MouseButtonEventHandler(AssociatedObject_PreviewMouseUp);
    21.     }
    22.  
    23.     void AssociatedObject_PreviewMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
    24.     {
    25.         if (Bridge != null && AssociatedObject.IsMouseCaptureWithin)
    26.             Bridge.PushUIBridgePropertyToModel();
    27.     }
    28.  
    29.     protected override void OnDetaching()
    30.     {
    31.         AssociatedObject.PreviewMouseUp -= new System.Windows.Input.MouseButtonEventHandler(AssociatedObject_PreviewMouseUp);
    32.         base.OnDetaching();
    33.     }
    34. }

  • Laisser un commentaire

    Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

    Logo WordPress.com

    Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

    Image Twitter

    Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

    Photo Facebook

    Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

    Photo Google+

    Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

    Connexion à %s