Mon premier blog

Aller au contenu | Aller au menu | Aller à la recherche

Tag - WPF

Fil des billets

vendredi, octobre 9 2009

Comment trouver un élément dans l'Arbre logique WPF grâce à son nom.

Depuis que j'utilise le WPF, un problème récurrent m'est apparu. En effet, il n'y a aucune fonction de recherche dans l'arbre logique des composants d'un logiciel WPF. Il existe une classe, VisualTreeHelper, qui propose quelques méthodes statiques intéressantes mais pas assez poussées. Imaginons que nous désirons récupérer un élément particulier de l'arbre logique, à partir de n'importe quel autre composant. Par exemple, nous disposons d'une structure telle que:

Structure_test_FindTreeHelper.jpg

Il serait intéressant de pouvoir accéder à partir d'un événement déclenché sur le bouton1 au bouton2. Pour ce faire, la propriété Name d'un objet de type FrameworkElement ou dérivé, est unique dans toute une arborescence WPF, et sur cette propriété que nous allons faire notre recherche de FrameworkElement

Si on s'attarde un peut sur les composants de base que nous fournit WPF dans le namespace System.Windows.Controls, on s'aperçoit qu'il existe 4 conteneurs différents. J'entends par conteneur un objet de type primitif FrameworkElement qui peut contenir d'autres objets. Voici la liste des trois conteneurs ainsi que la propriété qui permet de référencer les éléments enfants.

  • Decorator : il permet de référencer un et un seul élément enfant dans sa propriété Child
  • ContentControl : il permet de référencer un et un seul élément enfant dans sa propriété Content
  • ItemsControl : il permet de référencer une liste d'éléments enfants dans sa propriété Items
  • Panel : il permet de référencer une liste d'éléments enfants dans sa propriété Children

Bien que dans ce listage on ne peut pas voir la différence entre les objets du type ItemsControl et Panel, elle est majeur. D'un point de vue architecture, ItemsControl tout comme ContentControl héritent de Control. Par contre Panel hérite directement de FrameworkElement tout comme Control. De plus les items d'un objet ItemsControl seront affichés les uns derrière les autres avec des styles qui peuvent être différents si vous utilisez un DataTemplateSelector. Cette liste d'items peut bénéficier des fonctionnalités des scrolls. Les objets hérités de Panel quant à eux, permettent un placement logique des éléments enfants comme un Grid qui nous permet de les organiser en ligne et/ou en colonne, ou encore un Canvas où le développeur est libre de choisir la position relative des enfants par rapport à sa taille et sa position. Voici comment se présente cette hiérarchie:

Hierarchie_ContentCtrl_ItemsCtrl_Panel.jpg

Maintenant entrons dans le vif du sujet. Les composants WPF s'organisent dans un arbre qui est du type Arbre n-aires, pour ce faire, il nous faut faire une recherche sur un string qui correspondra à l'élément cherché. Deux mots clé C# vont nous être très utile : as pour tout ce qui est cast et var qui permet de prendre n'importe quel type.

Pour initialiser une recherche, il est préférable de se positionner sur l'élément racine de l'arbre, c'est-à-dire l'élément qui à pour Parent la valeur null. Voici la méthode qui permet d'atteindre la racine à partir de n'importe quel élément de l'arborescence :

FindTreeHelper_GetRacine.jpg

Maintenant que nous pouvont atteindre la racine, à partir de n'importe quel élément, nous allons effectuer notre recherche. Nous devons parcourir tous les éléments enfants en prenant compte les quatre types de conteneurs du framework. Cet algorithme est un exemple de parcours en profondeur sur un arbre n-aires. C'est par ce même parcours que les navigateurs interprètent les applications Silverlight basées également sur du WPF. Voici la méthode de recherche :

FindTreeHelper_GetElementFromLogicalTree.jpg

Cet algorithme est efficace pour la recherche d'un seul élément. Mais imaginons maintenant que nous voulons effectuer une recherche sur une clé qui n'est pas unique. Dans ce cas, deux choix s'offre à nous. Soit on décide de récupérer le premier élément possédant la clé, ce qui n'est pas très judicieux si l'élément ciblé se trouve, par exemple, en troisième position. Soit on récupère la liste de tout les éléments concordants avec la clé. Pour se faire un nouveau mot clé nous sera utile : yield. Ce mot clé offert par .Net nous permet de remplir un IEnumerable<> dans une boucle. Prenons le cas où nous voulons récupérer tous les éléments d'un type donné. Par exemple si l'on veut récupérer l'ensemble des boutons d'un arbre logique WPF. Voici à quoi doit ressembler l'algorithme.

FindTreeHelper_GetElementsFromLogicalTree.jpg

Nous avons désormais deux parcours similaires mais avec un retour différent. Maintenant nous pouvons imaginer n'importe quelle recherche sur n'importe quelle clé. Il nous reste à faire un accès à ces nouvelles fonctionnalité par la méthode décrite ci-dessous.

FindTreeHelper_Find.jpg

Le parcours de l'arbre logique n'a plus de secret désormais mais qu'en est-il de l'arbre visuel, c'est-à-dire, une arborescence définie dans le Template d'un composant. L'extension à l'arbre visuel est aisée. En effet La classe VisualTreeHelper, grâce à sa méthode GetChild, nous permet d'accéder à l'élément racine dans le Template de l'élément ciblé. De plus, toute l'arborescence définie dans un Template n'est autre qu'un arbre logique. Donc il nous suffit d'atteindre cet élément racine au sein du Template et de lancer la recherche effectuée par les algorithmes précédents.

Je tiens à remercier Arnaud Auroux pour l'idée.

samedi, septembre 12 2009

MVVM - Utiliser l'event d'un Control WPF dans son ViewModel

Comme on le sait le pattern MVVM requiert aucun code behind au niveau des vues. Ce processus proscrit tout abonnement à n'importe quel event d'un element graphique, en l'occurence à un UIElement pour être le plus générique possible. Par contre, le pattern nous permet d'utiliser avec aisance les commandes. Donc on créé un comportement qui possède 2 propriétés de dépendences attachées.

  • Une pour la commande à appeler.
  • Une pour le nom de l'évènement à surveiller.

Dès que l'on affecte une valeur à la propriété servant de surveillance sur l'evenenement qui nous interresse, on s'abonne à celui-ci. Le nom de l'event sous forme de string nous sert de clé d'accès. Lorsque l'evènement est déclenché, on appele la commande que l'on à bindée. La réflexion est employée afin de retrouver le delegate (ou l'event pour être type-safe) appelant et de lancer un delegate anonyme afin d'executer la commande.

Voici les 2 propriétés de dépendance rattachées:

MVVM_BehaviorCommand_DependencyProperties.bmp

La méthode qui suit nous permet de faire la réflexion nécessaire :

MVVM_BehaviorCommand_SinkControlEvent.bmp

Voici la classe qui est appelée grace à la réflexion et qui permet d'étendre l'abonnement de l'event jusqu'au déclenchement de notre commande lors de son Invoke:

MVVM_BehaviorCommand_AnonymousDelegateClass.bmp

jeudi, septembre 3 2009

Surface - Synchroniser le scroll de n SurfaceScrollViewers

Le SurfaceScrollViewer est une surcharge de l'élément WPF ScrollViewer. Le framework 3.5 SP1, ne nous permet pas d'accéder, en code xaml, sur les propriétés HorizontalOffset et VerticalOffset de ce composant. Or ces 2 propriétés nous indiquent la position du scroll, respectivement horizontale et verticale, du ScrollViewer. Cette restriction vient du fait que l'on ne peut pas effectuer un binding en mode TwoWay sur ces propriétés. En effet, on peut récupérer la valeur, mais on neut peut la modifier. Afin de modifier ces 2 propriétés, on doit utiliser les méthodes ScrollToHorizontalOffset et ScrollToVerticalOffset respectivement. Dans cet article je vais vous détailler comment contourner cette carence de DataBinding. On va synchronizer différents SurfaceScrollViewers entre eux, selon leur groupe d'appartenance dont le développeur pourra définir.

Pour cela on créé une classe qui dérive de DependencyObject. Car, on sait que l'objet que l'on créé n'a pas de représentation graphique. Par contre, il doit être capable de fournir une propriété de dépendance et de s'attacher à un composant graphique, en l'occurence un SurfaceScrollViewer, d'où cet héritage.

declarer_class_scrollviewer_synchronizer.bmp

Dans cette classe nous allons créé une nouvelle propriété de dépendance, en utiilisant la méthode statique RegisteryAttached. Cette méthode permet d'inscrire une propriété jointe avec le système de propriétés de son appartenance. On lui spécifie le nom de la propriété dans un string, le type de la propriété, le type du propriétaire et les métadonnées de la propriété spécifiée. Voici la déclaration de la DependencyPorperty et son encapsulation.

dependency_property_srcollviewer_synchronizer.bmp

Les méthodes d'encapsulations sont statiques, ce qui leur permettent d'être partagées avec n'importe quel autre objet. Pour en savoir un peu plus sur les DependencyProperty je vous conseille de lire mon article sur le sujet.

Maintenant que nous avons notre propriété de dépendance sur laquelle on peu effectuer un binding, il nous faut stocker des données concernant les SurfaceScrollViewers afin de pouvoir synchroniser leurs scrolls. On définit 3 dictionnaires de données. Le premier permet de stocker les objets de type SurfaceScrollViewer en tant que clé du dictionnaire. Chaque clé a une valeur assignée de type string. Cette valeur correspond au groupe de synchronisation des scrolls dans lequel le SurfaceScrollViewer appartient. Le groupe correspond à la valeur de la propriété de dépendance ScrollGroupProperty que l'on aurra encré dans les propriétés, en code xaml, des SurfaceScrollViewers que l'on désire synchroniser.

dictionnaire1_scrollviewer_synchronizer.bmp

Les 2 dictionnaires suivant permettent de stocker la position du scroll horizontal et du scroll vertical respectivement. Ilsprennent comme clé un string qui est la valeur stockée dans le premier dictionnaire, c'est-à-dire le groupe de synchronisation.

dictionnaires_scrolls_scrollViewer_Synchronizer.bmp

On revient sur la méthode spécifiée dans les métadonnées de la propriété de dépendance ScrollGroupProperty. Elle est déclenchée suite à la modification de la propriété. Cette méthode va nous permettre de réagir à 2 choses :

  • On désabonne la méthode ScrollViewer_ScrollChanged de l'event ScrollChanged et on supprime le SurfaceScrollViewer du premier dictionnaire.
  • On réabonne la méthode ScrollViewer_ScrollChanged à l'event ScrollChanged, après avoir placé les scrolls selon les caractéristiques du groupe et l'insertion de la nouvelle KeyValuePair dans le dictionnaire.

OnScrollGroupChanged_ScrollViewer_synchronizer.bmp

La méthode ScrollViewer_ScrollChanged que l'on abonne à l'event ScrollChanged de chaque SurfaceScrollViewer ayant un groupe de définit, nous permet de positionner le scroll de tous les autres SurfaceScrollViewer du même goupe à la position de l'appelant. Merci aux références ^). C'est dans cette méthode que l'on pourra utiliser les méthodes ScrollToHorizontalOffset et ScrollToVerticalOffset.

ScrollViewer_ScrollChanged_ScrollViewer_Synchronizer.bmp

Pour utiliser cette nouvelle classe en xaml, on doit définir au préalable le namespace dans un xmlns. Voici une fois fait, comment uriliser la synchronisation.

SynchronizerScroll_SurfaceScrollViewer_xaml.bmp

lundi, août 31 2009

Les Dependency Properties

Le DataBinding est un outil très répandu et facilement utilisable en WPF. Cet outil permet de lier des données d'une couche métier à la couche de l'interface utilisateur. On peut aisément coupler une liste d'objets métiers aux items d'une ListBox et WPF s'occupe d'habiller le composant ainsi que chacune de ses items.

On peut demander à WPF de mettre à jour l'interface, à chaque modification de l'objet source. Pour cela on à 2 possibilités, soit on passe par la réflexion de .Net, soit en déclarant des Dependency Property. La réflexion s'utilise en implémentant l'interface INotifyPropertyChanged et déclenche l'event PropertyChanged à chaque fois qu'une propriété est modifiée.

Les Dependency Properties sont utilisables dans des Dependency Objects. Cette classe est la base d'un très grand nombre de classe de WPF. Les Dependency Properties permettent aux objets qui les détiennent, d'avoir des propriétés dont la valeur peut dépendre de nombreuses choses. En effet, elles peuvent dépendre d'un DataBinding ou d'une animation. De plus elles fournissent un support pour ce qui est de l'auto-validation ou de la gestion des valeurs par défaut. Les classes dérivées peuvent modifier très facilement le comportement d'une DependencyProperty héritée.

Déclarer une DependencyProperty

Avant toute chose, il faut savoir que la déclaration d'une DependencyProperty est soumise à quelques contraintes. En effet, elle doivent respectées cette synthaxe :

DependencyProperty_declaration_2.bmp

On peut déclarer de différentes manières une DependencyProperty. Nous allons voir la plus utilisée et la plus simple :

DependencyProperty_declaration_1.bmp

On enregistre dans un dictionnaire la propriété grâce à la méthode statique DependencyProperty.Register(). Cette méthode peut prendre de 3 à 5 paramètres.

  1. On donne la clé du dictionnaire sous forme de string qui sera le nom de la propriété tronquée de "Property". (string)
  2. On précise le type de la propriété. (Type)
  3. On précise le type de l'objet qui la déclare. (Type)
  4. (facultatif) On créé des métadonnées qui définirons le comportement de la propriété. (PropertyMetadata)
  5. (facultatif) On référence un rappel qui doit exécuter toute validation personalisé de la valeur de la propriété de référence, au delà de la validation de type standard. (ValidateValueCallBack).

Les deux derniers paramètres requièrent des méthodes d'extensions qui doivent être statiques, vue q'une Dependency Property l'est aussi. Voici à quoi ressemble la déclaration de ces 2 méthodes :

DependencyProperty_declaration_3.bmp

Afin de rendre les Dependency Porperties complètement transparentes dans une classe, on les encapsule dans des propriétés CLR. Cette encapsulation nous permet donc de pouvoir appliquer le DataBinding. La classe qui permet d'encapsuler les Dependency Properties, doit hériter de DependencyObject. En dérivant de cette classe on peut utiliser les méthodes GetValue et SetValue. Voici le code d'encapsulation :

Encapsulation d'une DependencyProperty

La méthode GetValue revoie un object on doit donc obligatoirement caster son retour par le type approprié, i.e. le 2e paramètre de la méthode statique Register. On note également que la propriété CLR doit respecter la synthaxe de la clé passé dans le dictionnaire lors de l'enregistrement.

mardi, août 25 2009

MVVM 1.2 - Mise en place de la solution

Dans cet article nous allons voir un exemple d'organisation d'une solution utilisant MVVM. Vous en trouverez surement d'autres, mais j'affectionne particulièrement celle-ci, car elle marque bien la séparation des couches.

On créé tout d'abord un nouveau projet dans File -> New Project ... On choisit une application de type WPF ou bien Sylverlight ou encore Surface...

La solution se génère et nous créée un nouveau projet comportant 2 fichiers : App.xaml et Window1.xaml

  1. On supprime le fichier Window1.xaml
  2. On supprime la propriété StartupUri dans le fichier App.xaml car nous avons supprimé la classe affecté à celle-ci ;)
  3. On crée une nouvelle fenêtre que l'on nomme MainWindow.xaml. Cette classe sera notre fenêtre principale. Elle aura par la suite une classe ViewModel attribuée, que l'on nommera MainWindowViewModel.

Pour attribuer une ViewModel à une View, on affecte la ViewModel à la propriété DataContext de la View. Ce procédé permet d'effectuer la Binding soit OneWay ou TwoWay entre les composants de la View et les propriétés de la ViewModel.

4. Dans le code behind de la classe App, on surcharge la méthode OnStartup(StartupEventArgs e). Cette méthode remplace la propriété StartupUri en code xaml, mais nous permet de coupler la View au ViewModel.

Voici l'implémentation de cette méthode :

MVVM_OnStartup_Mise_en_place.bmp

Ceci étant fait, on va s'occuper des couches. Pour cela on créé 3 nouveaux projets dans la solution de type Librairie de classes, que l'on nomme respectivement Model, View et ViewModel. Pour les assemblies, nous avons besoin de référencer la librairie Model dans celle de ViewModel. Cette référence nous permettra de récupérer les données externes et de pouvoir les rendre accessible par binding aux Views. Nous n'avons pas besoins de référencement entre les couches View et ViewModel. Par contre, on ajoutera les 2 assemblies dans le projet principal afin de pouvoir coupler chaque View avec son propre ViewModel, grâce entre autres aux DataTemple, mais nous y reviendrons dans un prochain articel.

Pour finir afin de garder un couplage net entre les UserControls de la librairie View et leur ViewModel associé, on créé un nouveau répertoire dans le projet ViewModel que l'on nomme ViewModelsList. Ce répertoire, en toute logique comportera autant de fichiers que le projet View et permettra de dédier un namespace propre au couplage. La structure de base de la couche ViewModel, ainsi que le ViewModel associé à la fenêtre principale (MainWindowViewModel.cs) seront stockées à l'extérieur de ce répertoire.

Voila à quoi devrai ressembler votre arborescence :

MVVM_Aborescence_Mise_en_place.bmp

dimanche, août 23 2009

MVVM 2.1 - Les RelayCommands

Dans toutes les applications, l'utilisateur a besoin d'interagir avec les composants de l'interface graphique. En WinForms, on doit abonner des méthodes à des évènements qui englobent des delegates, afin de les rendre entre autres type-safe. Par exemple, lorsque l'on veut déclencher une tâche, suite au clic de l'utilisateur sur un bouton, on abonne une nouvelle méthode EventHandler à l'event Click. Cependant le pattern MVVM demande qu'il y ait le minimum de (le mieux serai aucun) code behind dans les Views. On ne peut donc s'abonner à un évènement d'une View dans cette architecture. Pour palier à cette contrainte, on fait appel à des Routed Commands qui pourront être bindés dans les vues en langage déclaratif (xaml).

La classe RelayCommand

Cette classe implémente l'interface ICommand qui permet d'être identifiée en tant que commande dans le code xaml. Elle peut être affectée à la propriété Command d'un UIElement adéquat. Une RelayCommand permet de relayer ses fonctionnalités à un autre objet en invoquant des delegates.

La classe comporte 2 propriétés :

  • Une propriété de type Action<object> dans l'assembly System.Core qui est la méthode appelée pour l'exécution de la commande.
  • Une propriété Predicate<object> dans l'assembly System.Core également. Elle représente la méthode qui définit un ensemble de critères et détermine si l'objet spécifié répond à ces critères afin d'exécuter la commande ou non.

Implémentation

Nous allons voir maintenant à quoi ressemble l'implémentation de cette classe.

Dans le projet ViewModelLibrary :

  • Ajouter la référence PresentationCore qui nous permet de disposer de l'interface ICommand et de la classe CommandManager
  • Créer une nouvelle classe RelayCommand et implémenter ICommand.

RelayCommand.bmp

Utilisation

Pour utiliser une commande dans une classe ViewModel et pour pouvoir la binder dans la View associée, on crée simplement une propriété de type ICommand que l'on initialisera dans le constructeur. Voici l'exemple d'un cas d'uilisation :

Exemple_utilisation_RelayCommand.bmp

Notre ActionCommand est une simple propriété, par conséquent, on peut la binder dans un code xaml d'un View :

Exemple_utilisation_RelayCommand_xaml.bmp

vendredi, août 21 2009

MVVM 1.1 - Qu'est-ce donc cet acronyme ?

En Bref

MVVM ou Model-View-ViewModel est un design pattern qui est apparu pour la nouvelle génération d'interface utilisateur .Net : Windows Presentation Fondation (WPF). Cette architecture permet un découpage en plusieurs couches d'une application. Le gros avantage est que sa structure permet le travail en parallèle du développeur et du designer.

Voici un schéma explicatif : Schéma MVVM

La couche Model

La couche model est la couche la plus simpliste de l'architecture, mais la plus délicate, car chaque classe doit être, en théorie, interopérable. Lorsque vous construisez ces classes, veillez à ce qu'elles puissent être réutilisable dans d'autres applications. Les classes de cette couche peuvent servir dans le pattern comme des contrats qui lient, par exemple, la couche d'accès aux données et la couche ViewModel. Afin d'étoffer ces classes, on pourra si nécessaire faire des validations pour la validation d'attributs. Si on prend l'exemple d'un formulaire basique avec nom, prénom... On aura une classe Person qui devra valider les champs que l'utilisateur renseigne afin de savoir si l'adresse mail est au bon format par exemple. Les classes issuent d'ADO .Net qui mappent les tables d'une base de données, peuvent aisemment faire partie de cette couche.

La couche View

Le rendu graphique d'une application WPF, plus communément appelé la couche View, est complètement désolidarisé du reste des couches. De plus, les rendus graphiques en WPF utilisent le xaml qui est un langage déclaratif. Le designer travaillant, par exemple sur Expression Blend, génère les fichiers xaml correspondants aux vues, en parallèle du développeur qui utilise Visual Studio. Les données seront envoyées et/ou reçues dans le code behind grâce au databinding. Il est fortement déconseillé, et d'ailleurs, c'est le but de ce pattern, de n'avoir aucun code behind dans ces classes. A la limite, juste le constructeur, qui fait appel à la méthode InitilizeComponents() qui charge le code xaml. On ne verra plus d'abonnement à des évènements, bien que cela reste une possibilité de dernier recours. On passera par le binding des commandes suite au clic d'un bouton par exemple. Les commandes ne sont autres que des méthodes anonymes lancées par exemple, à partir d'expressions lambdas.

La couche ViewModel

C'est la couche la plus importante du pattern. Elle est assimilable à la couche Controller, du pattern MVC bien connu. Elle permet de gérer les commandes, ainsi que la notification des propriétés pour le binding aux Views. Il y aura une classe ViewModel attribuée à chaque classe View de créée. Les classes ViewModel sont désignées pour récupérer les données, via la couche d'accès aux données, stockées dans des base de données, des fichiers XML ou encore des flux RSS... La récupération de données est contractée (formatée/Mappée) par les classes de la couche Model. Une classe ViewModel peut afficher de nouvelles Views en instanciant simplement les ViewModels associés. Cette couche demande une hiérarchie de base afin de nous faciliter la tâche et de nous éviter de réecrire 50 fois les mêmes bouts de codes. Nous verrons dans un prochain article cette structure plus en détail. Une fois l'architecture mise en place, le développement s'accélère "Vitesse grand V". En effet, le développeur n'a plus à ce soucier du mécanisme, et peut se concentrer seulement sur les données à binder ou les commandes à déclencher, pendant que le designer prend en charge les Views.