[new:30/09/2012]De Silverlight à WinRT en passant les Smartphones, sous Windows ou d’autres OS, les patterns de type MVVM sont devenues indispensables. Toutefois gérer des données de Design pour faciliter la création des UI est souvent mal géré ou oublié. Cela est aussi essentiel pourtant...
MVVM “Générique”
Il existe de nombreux frameworks pour gérer la séparation UI/Code, qu’ils soient basés sur MVVM, MVC ou d’autres patterns. C’est pourquoi je vais faire abstraction ici de toutes ces subtilités pour discuter des données de design sans faire appel à aucun de ces frameworks (même si l’article suit la logique de MVVM).
C’est à la fois attirer votre attention sur le principe de fournir des données de design qui m’intéresse ici et la mise en œuvre de moyens simples réutilisables dans une multitude de contextes (quitte à les adapter).
Silverlight
L’exemple pourrait être fait avec WinRT, WPF, WP7 ou WP8 ou même d’autres technologies, cela n’a pas grande importance. Comme il faut faire un choix et que j’aime toujours autant Silverlight c’est donc au travers d’une application de ce type que nous allons voir comment créer des données de design. Bien que sans framework spécialisé, l’exemple suivra la logique de MVVM, ce qui est adaptable à d’autres patterns du même type.
Principes de base des données de design
Tout d’abord j’aimerais insister sur la nécessité de proposer des données de design. C’est vraiment quelque chose d’essentiel pour l’infographiste ou pour l’intégrateur qui devra marier les jolies présentations avec le code des développeurs. Sans données de design il y a fort à parier que c’est lors des premières exécutions (si on a de la chance) qu’on s’apercevra que telles zones est trop longue ou trop courte, trop haute ou pas assez... C’est là aussi qu’on verra que telle fonte qu’on croyait géniale ne passe visuellement pas lorsque toute la fiche est remplie d’information. Et plein d’autres choses qui ne peuvent se voir que s’il y a quelque chose à voir !
Les données de design doivent être :
- Disponibles au moment du design, c’est à dire sans avoir besoin d’exécuter l’application
- Utilisables sous Visual Studio et Expression Blend (pour la plateforme Windows)
- Représentatives du contenu réel
- Suffisamment “passe partout” pour que l’œil lors du design ou de l’intégration ne soit pas “attrapé” par le texte de design mais qu’il reste concentré sur le design lui-même
- Discrète au runtime : c’est à dire que le code les générant ne doit pas être intégré à l’exécutable final
- Conçues pour le design, par pour des tests de montée en charge.
Il ne s’agit que des grands principes de base. Mais ils sont très importants.
Par exemple la “blendability”, le fait de pouvoir voir les données sous Blend ou VS en mode conception est vital.
La représentabilité des données est tout aussi importante. Un nom et un prénom ce n’est pas du Lorem Ipsum. En revanche un commentaire doit être du Lorem Ipsum ! L’attention ne doit pas être perturbée par un contenu lisible, le cerveau à trop d’attirance pour ce qu’il sait reconnaitre... Et c’est une distraction inutile.
De même, les données de design se doivent d’être réalistes mais peu nombreuses : en conception pas besoin de 5000 entités “personne” pour mettre en place l’affichage d’une seule personne...
Enfin, les données de design doivent savoir s’effacer au runtime, le code qui les génère, les données externes utilisées, etc, tout cela doit être absent du code exécutable final.
Lorem Ipsum
Ce sont les premiers mots d’un texte en pseudo latin utilisé par les imprimeurs pour tester les mises en page. Son intérêt est d’être proche du français (même alphabet, longueur des mots, fréquences de ceux-ci, longueur des phrases etc) mais de ne pas être du français (ni même du vrai latin) afin que celui qui s’en sert ne soit pas perturbé, distrait, par un texte ayant un sens.
Vous trouverez à l’adresse suivante un générateur de mots, de phrases et de paragraphes Lorem Ipsum que j’ai écrit sous Silverlight et qui est accessible gratuitement, utilisez-le lorsque vous avez besoin de générer du Lorem Ipsum !
E-Naxos Lorem Ipsum Generator
View, ViewModel et Model
Nous allons partir d’une simple application Silverlight de base, sans aucune fioriture. Je vous fais grâce des étapes de cette création d’un nouveau projet de ce type.
Le résultat est un projet vierge qui contient une page principale appelée MainPage.xaml. C’est le comportement par défaut de Visual Studio dans ce cas.
La première chose que nous allons faire est d’ajouter un sous-répertoire “ViewModels” pour conserver une structure propre et réutilisable même si, ici, nous n’aurons qu’un seul ViewModel.
Rien d’extraordinaire alors continuons...
Le Modèle
Pour parfaire l’exemple et le rendre le plus réaliste possible je vais ajouter selon le même principe un sous répertoire “Models”.
Il contiendra la classe Personne qu’on supposera faire partie d’un ensemble constituant le BOL et le DAL de l’application.
Dans ce répertoire je vais ajouter la classe “Personne” :
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace DesignTimeData.Models
{
public class Personne : INotifyPropertyChanged
{
#region fields
private int id;
private string nom;
private readonly ObservableCollection<string> applications = new ObservableCollection<string>();
#endregion
#region properties
public int ID
{
get
{
return id;
}
set
{
if (id==value) return;
id = value;
doChanged("ID");
}
}
public string Nom
{
get
{
return nom;
}
set
{
if (nom==value) return;
nom = value;
doChanged("Nom");
}
}
public ObservableCollection<string> Applications
{
get
{
return applications;
}
}
#endregion
#region INotifyPropertyChanged
private void doChanged(string propertyName)
{
var p = PropertyChanged;
if (p==null) return;
p(this,new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
}
Cette classe est constituée de trois champs : ID, Nom et Applications.
Le premier sera l’identifiant de la fiche Personne, le Nom... vous avez deviné, et Applications sera une liste des applications que la personne a installées sur son ordinateur. Tout cela est purement fictif pour l’exemple.
La création de Modèles ou de classes métier n’est pas le sujet du jour mais vous remarquerez que bien que modeste notre classe “Personne” n’en respecte pas moins les minimum vitaux : séparations propres avec des régions, support de INotifyPropertyChanged, champ liste géré par une ObservableCollection initialisée par l’instance et en readonly, la propriété Applications ne contient qu’un getter et surtout pas de setter, une méthode spécifique est créée pour gérer les notifications de changement (doChanged).
Le ViewModel
Dans le répertoire ViewModels je vais rajouter une classe qui s’appellera MainPageViewModel. Le suffixe “ViewModel” est une habitude que je conseille, cela permet rapidement d’identifier ces classes spéciales, quant au nom lui-même, de préférence on utilise le nom de la Vue (ici MainPage).
Comme j’ai déporté tout ce qui est “données” dans la classe Personne, le ViewModel qui expose une personne sera très simple :
using System.ComponentModel;
using DesignTimeData.Models;
namespace DesignTimeData.ViewModels
{
public partial class MainPageViewModel : INotifyPropertyChanged
{
#region fields
private Personne personne;
#endregion
#region properties
public Personne Personne
{
get
{
return personne;
}
set
{
if (personne==value) return;
personne = value;
doChanged("Personne");
}
}
#endregion
#region INotifyPropertyChanged
private void doChanged(string propertyName)
{
var p = PropertyChanged;
if (p == null) return;
p(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
}
C’est un “"vrai” ViewModel, supportant notamment INotifyPropertyChanged.
On notera juste une petite chose mais qui va faire la différence : la classe est marquée “partial”.
Pourquoi ?
Parce que justement, nous arrivons au cœur du sujet, nous allons exploiter cette possibilité pour “externaliser” le code de création des données de design dans un autre fichier. Il ne faut surtout pas que le code de conception vienne se mélanger à la classe réelle qui sera utilisée en exploitation...
Création d’un switch de compilation ou pas ?
Arrivé à cette étape je suis obligé de vendre un peu la mèche... le code de génération de données pour le design est un code qui ne devra pas être compilé dans la version finale du logiciel. Pour se faire je vais utiliser une compilation conditionnelle (nous verrons qu’ici il y a une feinte à connaître...).
La première idée consiste à ajouter dans les paramètres de Build du projet, et pour le mode Debug, un nouveau switch qu’on pourrait appeler “Design” par exemple. Absent du build “release” par défaut il remplira parfaitement son rôle.
Toutefois cela revient à créer un synonyme de “Debug” déjà définit par VS ... Aurons-nous besoin de données de design autrement qu’en mode Debug ?
La question reste ouverte... Et chacun fera en fonction de son contexte. Pour ma part je ne vais pas ajouter de switch et j’utiliserai “Debug” comme marqueur de phase de conception.
Du coup, rien à ajouter ni à modifier nulle part. Mais comme l’apprend tout bon prof de philo à ses élèves éberlués par cette évidence à laquelle ils auront une vie pour réfléchir : Décider de ne rien changer est un changement en soi...
L’ajout du code de génération des données de design
C’est ici que les choses intéressantes commencent vraiment. En tout cas celles concernant le sujet de ce billet... Où créer les données de design ?
J’ai vu certains développeurs bricoler cela au niveau des Modèles. Je ne suis vraiment pas pour cette approche. Les modèles doivent être “purs”, ils sont le socle de l’application (sans données... pas d’application) et moins on tripote un code déjà testé mieux on se porte. D’autant plus que si les modèles peuvent être du POCO comme dans mon exemple, le plus souvent il va s’agir d’une couche de services type Web ou RIA Services. Et là, il est plus délicat (voire impossible) d’aller faire du bricolage dans le code de ces derniers où de leurs proxy auto-générés.
Le ViewModel est là pour faire l’adaptation des données pour sa vue (et mémoriser son état). C’est son job.
C’est donc au niveau du ViewModel que les données de design doivent être créées.
D’où la raison qui m’a fait ajouter “partial” au code du ViewModel et non à celui de la classe “Personne”.
Ce code “partiel” sera contenu dans un fichier séparé et certainement pas mélangé à celui, définitif, du ViewModel. Tout l’intérêt de “partial” est là d’ailleurs et VS s’en sert abondamment dans ce sens pour simplifier le développement ou rendre extensible du code auto-généré par exemple.
Une astuce : en choisissant le nom de ce code partiel avec doigté VS le considèrera comme un comme spécial de design et le fichier apparaitra alors sous le nom du ViewModel comme tous les fichiers de design (que cela soit sous Windows Forms ou ASP.NET notamment, donc cette “norme” est déjà bien ancrée !).
Puisque le ViewModel s’appelle MainPageViewModel.cs, le fichier que nous allons créer s’appellera MainPageViewModel.designer.cs.
Il apparaitra sous le nom du ViewModel en y étant comme lié :
Malin, isn’t it ?
La génération des données
Que va contenir ce fichier de code ?
Tout simplement une partie “cachée” de MainPageViewModel puisque cette classe est partielle justement pour cela...
Nous allons ainsi compléter la classe avec une méthode de génération de données qui va créer ici une instance de Personne avec des informations fictives.
using System.ComponentModel; // designer properties
using System.Diagnostics; // conditional
using DesignTimeData.Models;
namespace DesignTimeData.ViewModels
{
public partial class MainPageViewModel
{
[Conditional("DEBUG")]
private void createDesignData()
{
if (!DesignerProperties.IsInDesignTool) return;
personne = new Personne
{
ID = 1526,
Nom = "Olivier Dahan",
Applications =
{
"Visual Studio",
"Expression Suite",
"MS Office",
"PaintShop Pro",
"Ableton Live"
}
};
}
}
}
Comme vous le constatez, une seule méthode a été ajoutée à la classe MainPageViewModel, il s’agit de “createDesignData”.
Deux choses à noter:
- La méthode teste le mode conception et s’en retourne aussi vite si aucun designer n’est accroché à l’application. C’est une simple sécurité de bon sens (et cela permet d’exécuter le code en mode Debug sans pour autant voir les données de conception...).
- La méthode est décorée par l’attribut “Conditional” et la condition est liée à la présence du switch de compilation DEBUG.
L’attribut Conditional est particulier et fort sympathique en ce sens que le code ainsi décoré ne sera compilé que si le switch passé en paramètre est présent. Pour fabriquer un mode “démo” d’une application cela est très pratique et très sûr : il suffit de placer le code réel des fonctions non activées dans la démo sous un Conditional testant le mode release et aucun pirate, même le plus malin ne pourra transformer votre démo en application utilisable... et pour cause, le code non autorisé ne sera tout simplement pas dans l’exécutable !
Vous allez me dire, c’est bien gentil de faire apparaitre ou disparaitre du code comme ça façon magicien... mais les appels à ce code ? Ils sont où et ils deviennent quoi quand le code n’est pas compilé ?
C’est là que Conditional est vraiment pratique ! Les appels aux méthodes ainsi marquées peuvent rester à leur place, si le code de ces méthodes n’est pas compilé, les appels à ces méthodes seront elles aussi ignorées et de façon automatique !
Nous allons d’ailleurs le voir tout de suite.
Notre code de design créée une Personne pour aider la conception visuelle de l’application. Mais il reste à appeler ce code quelque part...
Pour ce faire nous allons ajouter l’appel dans le constructeur du ViewModel. Ici nous n’avons pas de constructeur (ce qui est rare pour un ViewModel qui au minimum en général créée les commandes ICommand à cet endroit ) nous allons en ajouter un.
C’est bien entendu dans le code MainPageViewModel.cs et non dans le code MainPageViewModel.designer.cs que le constructeur sera ajouté (tout simplement parce le ViewModel peut réellement avoir besoin d’un constructeur et qu’il serait stupide de le cacher dans le code design).
#region constructor
public MainPageViewModel()
{
createDesignData();
}
#endregion
Ce n’est pas bien compliqué...
La méthode createDesignData() est appelée par le constructeur du ViewModel. Si nous sommes en mode conception (ici détecté par le switch DEBUG mais nous avons vu que nous aurions pu créer un switch à part) la méthode sera appelée. Si un concepteur est accroché au projet la méthode créera une instance de Personne disponible durant la conception. Cela est pratique lors de l’exécution du code même en debug, les données de design ne s’affichent pas.
Si nous ne sommes pas en mode de conception (absence du switch DEBUG dans notre cas, par exemple en mode Release), à la fois le code conditionnel de createDesignData() ne sera pas compilé dans l’exécutable et l’appel à cette méthode sera ignoré... Il n’y a donc rien à modifier dans le ViewModel. Il pourrait y avoir cent appels à la méthode cachés un peu partout dans notre code, ils seraient ignorés de la même façon...
Créer des données de design est une façon d’utiliser Conditional. Faire des démos sans le code fonctionnel et donc in-piratable est une autre idée que je développais plus haut, mais on peut trouver bien d’autres utilisations à cette technique (méthodes de test, version “light” / version “pro”, etc...).
Le design
Après tout, nous avons fait tout cela pour simplifier le design...
La MainPage par défaut que VS a créé pour nous sera parfaite, nous n’avons pas besoin d’enjoliver dans cet exemple. En revanche il faut bien relier cette page Xaml à son ViewModel.
La plupart des frameworks offrent des moyens bien à eux pour ce faire. Comme ici nous n’en utilisons aucun il faudra faire cette liaison à “la main” :
<UserControl x:Class="DesignTimeData.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:ViewModels="clr-namespace:DesignTimeData.ViewModels"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.DataContext>
<ViewModels:MainPageViewModel/>
</Grid.DataContext>
</Grid>
</UserControl>
La première chose à faire est d’ajouter une référence à notre espace de noms des ViewModels, c’est le rôle de la ligne soulignée par mes soins dans l’entête de l’objet UserControl.
Ensuite il faut affecter le DataContext de la grille principale (LayoutRoot) pour qu’il pointe vers le ViewModel. C’est le rôle de la balise <Grid.DataContext> et du code qu’elle enchâsse.
Voilà... Le code de design est créé, il fabrique automatiquement des données pour mettre en page facilement l’application, ce code sera automatiquement supprimé de la version Release de notre application.
Ajoutons un TextBlock pour afficher l’ID de la Personne et ouvrons le gestionnaire de Binding sous Visual Studio, cela donne :
On voir la valeur “1526” s’affichée immédiatement après avoir sélectionné la propriété “Personne” du DataContext et la propriété ID de Personne...
Cela fonctionne de la même façon sous Expression Blend, à la différence de présentation des dialogues de Binding.
La mise en page peut s’effectuer, le designer ou l’intégrateur verra immédiatement si les zones sont bien placées, si elles sont assez grandes, etc...
(mode design sous Visual Studio)
Si nous exécutons l’application, les données ne seront pas affichées et cela bien que nous soyons toujours en mode DEBUG. C’est la raison d’être du petit test qui détecte le mode conception dans la méthode de génération de données de design...
Mais si nous passons le code sous Reflector par exemple, nous verrons bien que le code de design est toujours là.
En revanche en basculant en mode “Release” ce code aura totalement disparu ainsi que l’appel qui est fait dans le ViewModel sans avoir à toucher à quoi que ce soit...
Vous me croyez sur parole, pas besoin de rallonger ce billet par d’autres captures écran
Conclusion
Les données de design sont essentielles pour garantir une bonne mise en page de l’UI d’une application. Elles simplifient le travail du designer / intégrateur.
Comme nous venons de le voir ici, il suffit de très peu de choses pour créer de telles données de façon propre et totalement transparente au regard de l’exécutable définitif.
Il n’y a donc aucune raison de s’en passer... quel que soit le framework MVVM que vous utilisez et quelle que soit la plateforme choisie : Silverlight, WPF ou même WinRT.
Stay Tuned !