10. Ressources graphiques▲
Depuis le début de ce livre, nous avons largement évoqué la capacité de Silverlight, quant à la simplification de la communication entre chaque acteur. Depuis quelques années, cette collaboration intermétiers est une problématique centrale et incontournable. Le développement informatique a débuté dès les années quarante grâce à des personnages emblématiques comme Alan Turing. Dès lors, nous avons vu l'apparition de nombreux modèles de conception toujours plus performants. Ce secteur est aujourd'hui parvenu à une sorte de maturité et offre un large éventail de modèles de conception.
Toutefois, l'arrivée de technologies comme WPF ou Silverlight remet en cause un certain nombre d'habitudes acquises au profit de l'ergonomie et de l'expérience utilisateur. Comparativement, la technologie Winforms, tournée essentiellement vers la fonctionnalité, n'a que très peu besoin d'évoluer, elle produit du fonctionnel de manière très satisfaisante et ne prend pas vraiment en compte la conception d'interfaces graphiques avancées. Certaines entreprises ont simplement décidé de ne pas évoluer vers WPF pour cette raison. Cela peut se comprendre aisément, bien que ce soit au détriment de l'utilisateur. Le design, l'ergonomie ou tout type de contrainte qui n'est pas directement liée à la fonctionnalité, mettent en danger sa conception même. Pourquoi ? Parce qu'il est bien plus facile de concevoir une application fonctionnelle en Winforms ou en Java, que d'obtenir la même qualité de conception tout en ajoutant des contraintes techniques et architecturales liées à l'expérience utilisateur ou au design.
Le but des plates-formes WPF et Silverlight est précisément de répondre à cette problématique consistant à marier le design d'interfaces visuelles (sous tous ses aspects) et le développement informatique. Ces plates-formes réussissent ce pari de manière élégante et efficace d'un point de vue architecture et productivité. Une partie de cette réussite provient de la notion de ressources. Une bonne gestion des ressources est l'une des clés du succès et facilite grandement l'adaptation des savoir-faire.
Si vous souhaitez simplifier votre manière de produire une application à travers la mise en place d'une collaboration intermétiers efficace, ce chapitre est fait pour vous. Dans un premier temps, vous apprendrez à catégoriser les ressources et découvrirez les principes de base inhérents à leur utilisation. Par la suite, vous utiliserez les ressources graphiques de manière concrète, à travers l'intégration graphique d'un lecteur multimédia. Ainsi, vous aborderez l'utilisation des ressources de couleurs et de pinceaux, puis celles qui sont liées à l'utilisation de médias externes, comme les images ou les polices de caractères. Dans un second temps, vous concevrez des styles et des modèles personnalisés permettant de modifier l'affichage des contrôles Silverlight. Vous découvrirez ainsi le confort de conception apporté par les mécanismes de liaison et comment créer des ensembles de données fictives facilitant la personnalisation de contrôle de données.
10-1. Qu'est-ce qu'une ressource▲
Les ressources ont pour objectif d'être réutilisées soit lors de la conception d'une application, soit durant leur exécution. Lorsque vous modifiez la valeur d'une ressource au sein d'Expression Blend ou de Visual Studio, toutes les occurrences d'objets utilisant cette ressource sont mises à jour. Ce principe est à la base du flux de production innovant et efficace proposé par Silverlight. C'est un héritage direct du workflow apporté par WPF, qui offre un modèle encore plus abouti.
10-1-1. Définition et types de ressources▲
Au sein de Silverlight, on distingue trois types de ressources : les ressources logiques, celles de type média et les ressources graphiques. Les ressources logiques font référence aux objets qui n'ont pas vocation à être directement affichés par le moteur vectoriel. Ainsi, vous pourriez définir une ressource logique de type String réutilisable en plusieurs endroits d'une même application. Les ressources de type média sont des fichiers externes que vous pouvez incorporer au sein des projets Silverlight, les fichiers image (jpg ou png) font partie de cette catégorie. Ils ne sont pas décrits au sein du code XAML, mais directement référencés dans le projet via le fichier csproj. Pour finir, les ressources graphiques sont codées en XAML ou en C#, elles sont utilisées par des objets destinés à être affichés. Les styles et les modèles de la classe Button, que nous avons créés au Chapitre 6 Boutons personnalisés, font partie de cette dernière catégorie. Le Tableau 11.1 fournit une liste non exhaustive des ressources classées par type.
Ressources logiques |
Ressources médias |
Ressources graphiques |
---|---|---|
Double |
Images : jpg / png |
Couleurs |
String |
Vidéos : .wmv |
Pinceaux |
Boolean |
Sons : .wma |
Styles |
Thickness,… |
Polices de caractères : .ttf |
Modèles |
Les ressources graphiques définies par le code XAML sont très pratiques à utiliser et à maintenir ; nous allons le démontrer à travers un cas simple d'utilisation. Créez un nouveau projet nommé RessourcesIntro, placez ensuite plusieurs composants de type Border sur le conteneur Layout-Root. Définissez pour chacun d'eux une couleur d'arrière-plan mauve (par exemple #FF937C9D). Sélectionnez l'un d'eux au hasard puis, dans le panneau Properties, cliquez sur l'icône carrée à droite du champ Opacity. Sélectionnez l'option Convert to New Resource… (voir Figure 11.1).
Une boîte de dialogue apparaît, elle vous permet de configurer la clé de ressource ainsi que l'endroit où la stocker. Modifiez simplement le nom de la clé de ressource en Border-Opacity. Nommer les ressources permet, entre autres, de les identifier facilement par la suite. Veillez à nommer vos ressources le plus explicitement possible, car un projet peut en contenir un nombre très important. Le but est de retrouver la ressource recherchée au plus vite et de permettre aux différents intervenants du projet d'identifier les ressources créées (voir Figure 11.2).
Ne changez pas les autres options, vous découvrirez leur intérêt tout au long de ce chapitre. Une fois le nom de la clé modifié, validez en cliquant sur OK. Comme vous le constatez, les ressources sont accessibles par des clés (x:Key en XAML). Ce comportement est tout à fait logique, car elles sont stockées dans des listes particulières implémentant l'interface IDictionnary. Ces listes sont constituées de couples clé/valeur et sont nommées dictionnaires. Accéder aux valeurs d'un dictionnaire est possible grâce aux clés qui leurs sont associées.
Ce principe s'applique parfaitement à WPF, mais diffère légèrement pour Silverlight. Les ressources Storyboard en sont un exemple. Au Chapitre 5 L'arbre visuel et logique, nous les avons évoquées à plusieurs reprises. Les animations sont des ressources qui ont la possibilité d'être directement accessibles via l'attribut x:Name. Ainsi, lorsque vous créez une animation au sein d'un projet Silverlight, celle-ci est bien contenue dans un dictionnaire de ressources propre au UserControl principal, mais elle est accessible par son nom d'exemplaire. Cela permet au développeur de les cibler directement via C#, comme il le ferait avec une instance d'objet standard.
Dans l'environnement WPF, gérer les animations se révèle légèrement plus complexe pour les débutants, car ils doivent utiliser la clé afin de récupérer l'instance qu'ils transtypent en tant que Storyboard. Finalement, la manière d'accéder aux animations dans Silverlight est plus pratique, mais se révèle limitée. En effet, il est impossible de déclencher un même Storyboard sur plusieurs objets à la fois, ce qui n'est pas le cas pour WPF.
Les dictionnaires de ressources sont des instances de la classe Dictionary spécifiques, car les clés sont forcément de type String. Voici le code XAML de notre projet :
<UserControl
xmlns
=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns
:
x
=
"http://schemas.microsoft.com/winfx/2006/xaml"
xmlns
:
System
=
"clr-namespace:System;assembly=mscorlib"
x
:
Class
=
"RessourcesIntro.MainPage"
Width
=
"640"
Height
=
"480"
>
<UserControl.Resources>
<
System
:
Double
x
:
Key
=
"BorderOpacity"
>
1</
System
:
Double>
</UserControl.Resources>
<Grid
x
:
Name
=
"LayoutRoot"
Background
=
"White"
>
<Border
Height
=
"115"
HorizontalAlignment
=
"Left"
Margin
=
"49,36,0,0"
VerticalAlignment
=
"Top"
Width
=
"118"
Opacity
=
"{StaticResource
BorderOpacity}"
Background
=
"#FF937C9D"
BorderBrush
=
"Black"
BorderThickness
=
"1"
/>
<Border
Margin
=
"223,203,294,180"
Background
=
"#FF937C9D"
BorderBrush
=
"Black"
BorderThickness
=
"1"
/>
<Border
Height
=
"138"
HorizontalAlignment
=
"Right"
Margin
=
"0,66,64,0"
VerticalAlignment
=
"Top"
Width
=
"134"
Background
=
"#FF937C9D"
BorderBrush
=
"Black"
BorderThickness
=
"1"
/>
</Grid>
</UserControl>
Comme vous le constatez, la valeur de l'opacité a été définie au sein d'une ressource de type double. La propriété Opacity du premier contrôle Border se contente de référencer la clé. Faites en sorte que chaque instance de la classe Border référence une valeur d'opacité pointant vers la ressource BorderOpacity. Pour cela, vous pouvez coder en XAML ou utiliser l'interface de Blend, ce dernier moyen reste le plus simple. Sélectionnez les deux autres objets Border et dans le panneau Properties, cliquez sur l'icône carrée située à droite du champ Opacity. Sélectionnez ensuite l'option Local Resource, puis cliquez sur BorderOpacity (voir Figure 11.3).
Désormais, toutes les instances de Border partagent la même opacité en faisant référence à la ressource BorderOpacity. Modifier leur opacité revient à changer la valeur de la ressource. À cette fin, vous pouvez utiliser le panneau Resources. Ouvrez-le et dépliez UserControl. De cette manière, vous accédez à toutes les ressources définies pour MainPage.xaml. Une fois l'arborescence du UserControl dépliée, la ressource apparaît en dessous. Changez sa valeur en passant le champ BorderOpacity à 0.4. Vous constatez que l'opacité de chaque Border est mise à jour dynamiquement. Vous pouvez procéder de la même manière avec la propriété BorderThickness ; il est bien pratique d'homogénéiser l'épaisseur des bords des instances de Border au sein d'une application.
10-1-2. Les dictionnaires de ressources▲
Malgré les apparences du code XAML généré par Blend, toutes les ressources qui ne sont pas de type multimédia (image, vidéo…) sont stockées au sein de dictionnaires de ressources. Nous expliquerons ce principe dans cette section.
10-1-2-1. Stockage et portée des ressources▲
Ces dictionnaires de ressources, déclarés par de simples balises XAML, peuvent être définis à plusieurs niveaux, ce qui détermine directement leur portée d'utilisation. Comme tous les objets de type FrameworkElement possèdent la propriété Resources typée ResourceDictionnary, ils ont la capacité d'avoir leur propre dictionnaire de ressources, dont ci-dessous les différents lieux de stockage possibles.
-
Au sein de l'application elle-même, soit dans le fichier App.xaml. Dans ce cas, la ressource sera disponible dans toutes les pages du projet. Ce type de stockage peut apparaître limité. Différents projets, au sein d'une même solution, n'auront pas accès aux ressources que les uns ou les autres possèdent. Voici le code XAML généré dans Blend par défaut :
Sélectionnez<Application.Resources>
<CornerRadius
x
:
Key
=
"CornerRadius1"
>
0</CornerRadius>
</Application.Resources>
Ne confondez pas projet et solution. Lorsque vous créez un nouveau projet via le menu New Project… vous générez en fait une solution qui contient un projet du même nom. Vous pouvez toutefois ajouter autant de projets à la solution que vous le souhaitez. Le fichier App.xaml est propre à chacun des projets de type Application Silverlight, contenus au sein d'une solution. Stocker les ressources dans le fichier App.xaml d'un projet limitera leur portée à ce projet, ce qui est souvent suffisant.
-
À la racine d'une page de l'application, par exemple au sein du fichier MainPage.xaml. C'est l'option proposée par défaut, au sein de l'interface de Blend, lors de la création d'une ressource. Tous les objets de la page auront accès aux ressources de cette dernière. Les objets présents dans une autre page de l'application n'y auront pas droit sauf dans certains cas spécifiques. L'écriture XAML est équivalente à celle des ressources d'application, mais User-Control remplace Application :
Sélectionnez<UserControl.Resources>
<
System
:
Double
x
:
Key
=
"BorderOpacity"
>
1</
System
:
Double>
</UserControl.Resources>
-
Directement dans la balise d'un contrôle. Un conteneur Grid posséderait ainsi une balise enfant <Grid.Resources> qui contiendrait les ressources uniquement disponibles à ses enfants. Par défaut, l'interface de Blend ne propose pas de paramétrer ce type de stockage. Cela n'est pas forcément pratique sauf cas exceptionnels.
- Dans un fichier externalisé et indépendant. Lorsque l'on parle de dictionnaires de ressources, on évoque généralement un dictionnaire de ressources externalisé. Au Chapitre 9 Prototypage dynamique avec SketchFlow, consacré à SketchFlow, les styles étaient centralisés dans un fichier unique nommé Sketch-Styles.xaml, celui-ci contenait des ressources au sein de la balise racine <ResourceDictionary>. Ce mode de stockage est sans doute le plus souple. Ses principaux avantages sont de faciliter la collaboration, le partage, la maintenance et l'accès aux ressources. Dès que vous concevez en ayant pour objectif la réutilisation et l'optimisation des flux de production, la création de dictionnaires de ressources externalisés se révèle idéale. Ils peuvent être référencés par tous les fichiers XAML qui ont alors accès à leur contenu. Ils sont par défaut référencés par App.xaml et bénéficient ainsi de la portée du projet. Ces dictionnaires ne souffrent donc pas des limitations qui existent pour les ressources directement contenues dans les fichiers App.xaml ou MainPage.xaml. Ils sont partageables entre plusieurs pages, projets et solutions. Il n'est toutefois pas toujours intéressant d'externaliser systématiquement les ressources. C'est notamment le cas lorsque vous souhaitez fournir un composant embarquant ses propres ressources. De plus, la gestion des accès peut parfois devenir délicate.
Vous pouvez consulter la Figure 11.4 illustrant différentes portées d'utilisation.
10-1-2-2. Déclaration XAML▲
Revenons sur le code XAML généré par Blend lorsque vous stockez les ressources dans une page de l'application (UserControl) :
<UserControl.Resources>
<
System
:
Double
x
:
Key
=
"BorderOpacity"
>
1</
System
:
Double>
</UserControl.Resources>
Cette écriture est en fait une version simplifiée de l'écriture d'un dictionnaire de ressources. Ci-dessous, vous pouvez lire un code XAML plus verbeux et précis, mais équivalent en terme de résultat :
<UserControl.Resources>
<ResourceDictionary>
<
System
:
Double
x
:
Key
=
"BorderOpacity"
>
0.4</
System
:
Double>
</ResourceDictionary>
</UserControl.Resources>
L'utilisation de la classe ResourceDictionary est souvent implicite, car Blend adopte par défaut l'écriture simplifiée. Toutefois, lorsque vous référencerez des dictionnaires externes, vous obtiendrez une déclaration XAML complète ainsi qu'une nouvelle balise <ResourceDictionary.MergedDictionaries>. Celle-ci permet de référencer des dictionnaires supplémentaires via la propriété Source de la classe ResourceDictionary :
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source
=
"BobStyles.xaml"
>
<ResourceDictionary
Source
=
"PatrickStyles.xaml"
>
</ResourceDictionary.MergedDictionaries>
<
System
:
Double
x
:
Key
=
"BorderOpacity"
>
0.4</
System
:
Double>
</ResourceDictionary>
</UserControl.Resources>
Il est possible de charger des dictionnaires de ressources de manière dynamique, cela peut-être très élégant et efficace dans le cas d'applications multiclient. Vous pourriez créer un ou plusieurs dictionnaires de ressources par client et les charger à la volée, selon le client connecté à l'application et sa charte graphique. Cela peut également rendre un designer plus autonome. Il pourrait modifier un dictionnaire de ressources sur son poste de travail puis remplacer celui présent sur le serveur Web. De cette manière, le visuel de l'application serait mis à jour sans avoir besoin de la recompiler. Cela est réalisable grâce à la méthode statique Load de la classe XamlReader. Cette méthode est la version managée de la méthode JavaScript createFromXaml disponible depuis la version 1 de Silverlight.
10-1-2-3. Externaliser les ressources▲
Le stockage de ressources dans un fichier séparé est une méthodologie recherchée par les designers et les développeurs. Il suffit de modifier les ressources contenues dans le fichier, puis de recompiler l'application pour qu'elle soit mise à jour. Dans ces conditions chaque intervenant y gagne : vous instaurez une séparation des tâches tout en respectant le travail de toutes les parties impliquées. Le graphiste n'aura pas accès au code logique, et le développeur ne se préoccupera pas de savoir ce qui a changé graphiquement entre deux versions de l'application. Finalement, il sera possible de mettre en place une réelle communication intermétiers de manière douce et progressive.
Comme nous l'avons vu (voir section 11.1.1 Un héritage pertinent), la création d'un dictionnaire de ressources peut se faire lorsque vous générez une nouvelle ressource (en cochant l'option adéquate). Il est également possible de produire un dictionnaire de ressources vide, puis de lui ajouter des ressources au fur et à mesure de la production. Le panneau Resources d'Expression Blend est d'une grande aide pour ce genre de tâches. Ouvrez-le et cliquez sur l'icône située en haut à droite (). Une boîte de dialogue apparaît, vous demandant de saisir le nom du dictionnaire à créer (voir Figure 11.5).
Comme les ressources stockées seront de type double, autant l'appeler DoubleRD.xaml.
Le nommage des dictionnaires est un point capital, car il donne du sens et structure le projet. À brève échéance, un bon nommage facilite la compréhension du projet pour chacun des acteurs. Nous aborderons les bonnes pratiques de stockage et d'organisation des ressources tout au long des exercices de ce chapitre.
Lorsque le dictionnaire est créé, le fichier App.xaml est modifié par défaut. En son sein, un lien logique, décrit en XAML, pointe désormais vers l'adresse du fichier DoubleRD.xaml. Il est possible de générer ou de supprimer ce type de lien via l'interface de Blend. Cela permet notamment de dédier des dictionnaires de ressources à certaines pages de l'application et pas à d'autres. Pour cela, chargez la page principale MainPage.xaml dans la fenêtre de création (si ce n'est pas déjà fait). Ensuite, au sein du panneau Resources, dépliez App.xaml, cliquez droit sur le lien nommé Link To : DoubleRD.xaml et sélectionnez l'option delete. Une fenêtre apparaît vous indiquant qu'il est possible que certaines ressources ne soient plus correctement référencées. Ce n'est pas grave puisque notre dictionnaire est vide ; validez le choix. Vous détruisez de cette manière le lien pointant vers le dictionnaire externe sans supprimer le fichier lui-même. Nous allons maintenant ajouter un lien dans la page principale ciblant le dictionnaire. Cliquez droit sur le UserControl principal, puis choisissez l'option Link to Resource Dictionary et le dictionnaire DoubleRD.xaml (voir Figure 11.6).
Désormais, le dictionnaire est directement référencé par MainPage.xaml et ses ressources ne seront plus accessibles aux autres pages de l'application. Nous allons insérer une ressource dans ce dictionnaire en quelques clics de souris. Au sein de l'arbre visuel de MainPage.xaml, créez une instance d'Ellipse, puis cliquez sur l'icône carrée située à droite du champ Opacity. Choisissez l'option Convert to New Resource. Dans la boîte de dialogue affichée, entrez EllipseOpacity comme clé de ressource, sélectionnez ensuite l'option Resource Dictionary, vérifiez que le dictionnaire choisi dans la liste est bien DoubleRD.xaml, puis cliquez sur OK (voir Figure 11.7).
Dans la section suivante, vous apprendrez à appliquer des ressources via l'interface de Blend et le langage XAML, mais également de manière dynamique en C#.
10-1-3. Appliquer des ressources▲
Maintenant que nous avons vu comment stocker les ressources, nous allons apprendre différentes façons de les affecter aux propriétés d'objets. La première manière de procéder consiste à cliquer sur l'icône carrée située à côté d'un champ, à sélectionner l'option Local Resource, puis de choisir la ressource dans la liste présentée. Dans tous les cas, Blend filtre les ressources accessibles en fonction du type de la propriété. Ainsi, il est impossible, via l'interface de Blend, d'appliquer une ressource de couleur à une propriété d'opacité, car cette dernière n'accepte que les valeurs de type double. Dans Blend, la deuxième méthode consiste à ouvrir le panneau Resources, à déplier l'un des conteneurs de ressources, puis à glisser-déposer la ressource de votre choix sur un objet. Une liste apparaît directement au sein de la fenêtre de création. Il suffit de choisir l'une des propriétés proposées dans la liste pour lui affecter la ressource (voir Figure 11.8).
Comme vous le constatez, Blend vous propose une liste de propriétés compatibles avec le type de la ressource déposée. La propriété Tag étant typée Object, celle-ci peut recevoir tout type de valeur. Le type Thickness est utilisable dans de nombreuses propriétés, dont les marges intérieures et extérieures (respectivement Padding et Margin). Contrairement aux autres types de ressources, les modèles peuvent également être appliqués via un clic droit sur l'objet. Nous utiliserons cette méthode à partir de la section 10.4 Styles et modèles de composants.
Dans tous les cas et au sein d'Expression Blend, lorsqu'une ressource est appliquée à une propriété d'objet graphique (UIElement), la propriété est entourée d'un liseré vert visible dans le panneau Properties. Ce repère visuel est utile pour identifier les propriétés liées.
D'une toute autre manière, modifier le code déclaratif XAML est pratique lorsque vous avez besoin d'affecter rapidement une ressource à plusieurs objets à la fois. En XAML, vous définissez le plus souvent des valeurs en dur, par exemple Opacity="0.4". Il est toutefois possible d'assigner des clés de ressources, des liaisons de données ou de modèles, ou encore la valeur null (via {x:Null}). Ces valeurs ne peuvent pas être directement écrites entre guillemets, car elles sont en fait des expressions qui doivent être évaluées à la compilation ou à l'exécution (mis à part pour null qui est une valeur particulière). Leur notation se différencie par des accolades, vous obtiendrez par exemple :
<UserControl.Resources>
<ResourceDictionary>
<
System
:
Double
x
:
Key
=
"BorderOpacity"
>
0.4</
System
:
Double>
</ResourceDictionary>
</UserControl.Resources>
<Border
x
:
Name
=
"MonBorder"
Opacity
=
"{StaticResource BorderOpacity}"
>
Le mot-clé StaticResource indique l'utilisation d'une ressource de type statique. Cela signifie que la ressource est définie à la compilation une bonne fois pour toutes. Le mot-clé Dynamic-Resource n'existe pas encore du côté de Silverlight. Il est uniquement présent du côté WPF. Il permet à la propriété d'être mise à jour, durant l'exécution, chaque fois que la valeur de la ressource est modifiée de l'extérieur.
Il est important de noter que le nœud déclarant un dictionnaire de ressources doit toujours être le premier des nœuds enfants d'un document XAML. La lecture des nœuds XAML est toujours faite dans l'ordre des enfants. Si un objet référence une ressource statique qui n'est déclarée qu'après lui-même, l'interpréteur XAML lèvera une erreur (voir Figure 11.9).
Théoriquement, ce type de problématique ne devrait pas survenir sous Expresion Blend, car les dictionnaires de ressources sont toujours déclarés en premier. Il peut toutefois être utile de connaître ce type de comportement.
Appliquer une ressource via C# est assez simple. Il vous faut tout d'abord récupérer la ressource en ciblant le dictionnaire qui la contient. L'écriture sera légèrement différente selon que le dictionnaire est déclaré dans l'application, une page ou une instance de FrameworkElement :
//Accès à la ressource dans le dictionnaire au sein de l'application
double
MonBorderOpacity=(
double
)App.
Current.
Resources[
"BorderOpacity"
];
//Dans la page principale de type MainPage
double
MonBorderOpacity =(
double
)this
.
Resources[
"BorderOpacity"
];
//Au sein de la grille principale LayoutRoot
double
MonBorderOpacity =(
double
)LayoutRoot.
Resources[
"BorderOpacity"
];
Les valeurs stockées dans le dictionnaire sont toujours typées object afin de garantir un maximum de souplesse. Il vous faudra donc convertir le type de manière explicite pour récupérer une valeur utilisable :
foreach
(
UIElement uie in
LayoutRoot.
Children)
{
uie.
Opacity =
MonBorderOpacity;
}
Les dictionnaires de ressources externalisés sont le plus souvent référencés par l'application via le fichier App.xaml. Dans ce cas, il faut les considérer comme faisant partie du dictionnaire contenu par App.Current.Resources. Vous pourriez toutefois créer une référence à un dictionnaire externalisé au sein de n'importe quelle page ou de n'importe quel composant. Ces dictionnaires sont simplement fusionnés avec ceux qui les référencent. Voici un exemple de code XAML décrivant ce type de cas de figure :
<UserControl
…
<UserControl.Resources>
<ResourceDictionary>
<
System
:
Double
x
:
Key
=
"BorderOpacity"
>
0.4</
System
:
Double>
</ResourceDictionary>
</UserControl.Resources>
<Grid
x
:
Name
=
"LayoutRoot"
Background
=
"White"
>
<Grid.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source
=
"ResourceDictionary1.xaml"
/>
</ResourceDictionary.MergedDictionaries>
<
System
:
Double
x
:
Key
=
"BorderHeight"
>
135</
System
:
Double>
</ResourceDictionary>
</Grid.Resources>
<Border
Height
=
"{StaticResource BorderHeight}"
Opacity
=
"{StaticResource BorderOpacity}"
…/>
<Border
Height
=
"{StaticResource BorderHeight}"
Opacity
=
"{StaticResource BorderOpacity}"
…/>
<Border
Height
=
"138"
BorderThickness
=
"{StaticResource BorderThickness}"
Opacity
=
"{StaticResource BorderOpacity}"
CornerRadius
=
"{StaticResource CornerRadius1}"
…/>
</Grid>
</UserControl>
Dans la situation décrite ci-dessus, la référence d'un dictionnaire externe est déclarée dans le dictionnaire de la grille LayoutRoot. Ainsi, pour accéder à une ressource contenue dans ce dernier, il suffit de cibler la clé appropriée via la propriété Resources de cette grille :
Thickness monBorderThickness =
(
Thickness)LayoutRoot.
Resources[
"BorderThickness"
];
Nous avons abordé les principes majeurs inhérents à l'utilisation des ressources. Dans les prochaines sections, nous mettrons en pratique les connaissances acquises pour manipuler des ressources graphiques.
10-2. Les pinceaux▲
Les pinceaux représentent les ressources graphiques par excellence. Ce sont des instances d'objets héritant de la classe abstraite Brush. Ils sont à la base de tout visuel, car les propriétés de remplissage comme BackgroundColor, BorderBrush, Fill, Stroke (Shape), propres aux objets graphiques, n'acceptent que ce type d'instances. Dans cette section, vous apprendrez à utiliser et à organiser vos pinceaux de manière efficace. Afin de travailler dans des conditions réelles, nous allons intégrer partiellement un lecteur multimédia. Les Figures 11.10 et 11.11 en donnent un aperçu.
Le lecteur multimédia possède trois états visuels et l'utilisateur reste dans une seule page lorsqu'il utilise le lecteur. Le premier état représente un écran d'accueil composé d'une mire de bienvenue. Le deuxième est constitué d'une liste de lecture proposant différents types de fichiers multimédias lisibles par Silverlight. Lorsque l'un de ceux-ci est sélectionné, le troisième état visuel apparaît. Dans ce mode d'affichage, la liste des médias est repliée à gauche, ne laissant ainsi apparaître que le titre et le type de chaque fichier. Dans l'espace libéré au centre et à droite, un lecteur multimédia jouera ou affichera la vidéo, le son ou l'image sélectionnés. Avant de continuer, il est nécessaire de télécharger les fichiers de travail ainsi que le projet LecteurMultiMedia_Base compressé au format zip. Les fichiers sont disponibles dans l'archive Assets.zip et le projet dans LecteurMultiMedia_Base.zip du dossier chap11. Ce dernier contient le visuel de base du lecteur multimédia, à partir duquel nous réaliserons les exercices qui suivent. Une fois les deux fichiers récupérés et décompressés sur votre disque dur, ouvrez le projet avec Expression Blend.
10-2-1. Couleurs et pinceaux▲
Les ressources de couleurs et les pinceaux de dégradés représentent l'une des bases fondamentales du flux de production Silverlight. Grâce à eux, il est facile de relooker tout un site en très peu de temps. Nous verrons comment créer et maintenir ces ressources.
10-2-1-1. Les ressources de couleurs▲
Lorsque vous concevez une application sous Expression Blend, l'une des premières étapes consiste à sauvegarder les couleurs de base définies par la charte graphique. Comme vous les utiliserez tout au long du projet, le mieux est de les centraliser au sein d'un dictionnaire de ressources externalisé. Dans le panneau Projects, créez un nouveau répertoire nommé RD et sélectionnez-le. Ce répertoire contiendra tous les dictionnaires de ressources utilisés dans l'application. Ajoutez-y un nouveau dictionnaire de ressources nommé Couleurs. Vous avez la possibilité d'utiliser le panneau Resources (référez-vous alors à la section 11.1 Contrôle utilisateur ). Une autre manière plus directe consiste à cliquer droit sur le répertoire RD, puis à choisir l'option Add New Item… (voir Figure 11.12).
Dans la boîte de dialogue apparaissant, choisissez Resource Dictionary et spécifiez Couleurs.xaml comme nom de fichier. Cliquez ensuite sur OK pour valider. Nous allons désormais créer chaque ressource de couleur dont nous avons besoin. Les couleurs nécessaires sont présentées au Tableau 11.2.
Couleurs |
Noms de ressource x:Key |
Valeurs hexadécimales |
---|---|---|
Gris transparent |
PanelBackground |
#CD9E9E9E |
Gris clair |
ControlsBackground |
#FFDCDBD8 |
Gris foncé |
PlayerBackground |
#FF444444 |
Rose foncé |
VideoTheme |
#FFC70963 |
Vert |
LayoutModeTheme |
#FF7AC909 |
Yellow |
SoundTheme |
#FFFFC800 |
Orange |
TimeCodeTheme |
#FFFF5900 |
Cyan |
BitmapTheme |
#FF08AFC8 |
Pour créer une ressource de couleur, il suffit de sélectionner un objet puis de cliquer sur l'une de ses propriétés de remplissage. Vous pouvez référencer la valeur de la propriété sous forme de ressource de couleur unie. Par exemple, vous pouvez sélectionner la grille principale LayoutRoot, puis sa propriété Background. Spécifiez ensuite la couleur souhaitée. Il suffit de cliquer sur l'icône représentant une double flèche située à gauche de la valeur hexadécimale (voir Figure 11.13).
Dans la boîte de dialogue Create Color Resource, définissez PlayerBackground comme clé de ressource, puis enregistrez la clé dans le dictionnaire Couleurs.xaml que nous avons créé précédemment. À cette fin, choisissez la dernière option de stockage. Vous venez de créer votre première ressource de couleur. Le code XAML généré est le suivant :
<ResourceDictionary
xmlns
=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns
:
x
=
"http://schemas.microsoft.com/winfx/2006/xaml"
>
<Color
x
:
Key
=
"PlayerBackground"
>
#FF444444</Color>
</ResourceDictionary>
L'écriture est relativement simple : une instance de la classe Color est stockée dans le dictionnaire. Elle accepte par défaut une valeur 32 bits (4 octets) décrivant quatre couches, alpha, rouge, vert et bleu, exprimées en hexadécimal. Par exemple, lorsqu'une couleur est complètement opaque, elle débute par le couple FF qui indique la valeur hexadécimale maximale de la couche de transparence. Il est également possible de fournir une propriété statique de la classe Colors. Vous pourriez ainsi écrire :
<Color x:Key="PlayerBackground">Cyan</Color>
Finalement, il est encore plus rapide de dupliquer le nœud XAML déjà présent pour générer les ressources de couleurs listées au Tableau 11.2. Voici le code XAML généré une fois cette étape passée :
<ResourceDictionary
xmlns
=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns
:
x
=
"http://schemas.microsoft.com/winfx/2006/xaml"
>
<Color
x
:
Key
=
"PlayerBackground"
>
#FF444444</Color>
<Color
x
:
Key
=
"PanelBackground"
>
#CD9E9E9E</Color>
<Color
x
:
Key
=
"ControlsBackground"
>
#FFDCDBD8</Color>
<Color
x
:
Key
=
"VideoTheme"
>
#FFC70963</Color>
<Color
x
:
Key
=
"LayoutModeTheme"
>
#FF7AC909</Color>
<Color
x
:
Key
=
"SoundTheme"
>
#FFFFC800</Color>
<Color
x
:
Key
=
"BitmapTheme"
>
#FF08AFC8</Color>
<Color
x
:
Key
=
"TimecodeTheme"
>
#FFFF5900</Color>
</ResourceDictionary>
Ouvrez maintenant le panneau Resources et dépliez le dictionnaire Couleurs.xaml, vous y trouverez toutes les couleurs que vous avez définies sous forme visuelle (voir Figure 11.14).
Maintenant que les ressources de couleurs sont créées, nous pouvons facilement les appliquer aux objets présents dans l'arbre visuel. Au sein de l'arbre visuel, dans la grille nommée Panneau-Liste, sélectionnez le tracé Panneau. Dans le panneau Brushes, sélectionnez sa couleur de remplissage, puis l'onglet Color Resources. Choisissez la ressource de couleur PanelBackground pour l'affecter en tant que remplissage (voir Figure 11.15).
Il est également possible de glisser-déposer une ressource de couleur directement sur un objet, puis de choisir la propriété sur laquelle vous souhaitez l'apposer. Comme nous l'avons évoqué au début de ce chapitre, l'avantage principal des ressources est de mettre à jour tous les objets graphiques qui y font référence lorsqu'elles sont modifiées. Vous pouvez mettre à jour une ressource de couleur de deux manières différentes : soit via le panneau Resources, soit directement dans l'onglet Color Resources du panneau Properties. Dans ces deux cas, il suffira de cliquer sur la case de couleur pour modifier son remplissage. Comme vous l'avez peut-être remarqué, la couleur de remplissage a légèrement changé lorsque nous avons appliqué la ressource. Si vous souhaitez moins de transparence sur cette couleur, il suffit de modifier la ressource via le premier couple hexadécimal ciblant l'opacité (voir Figure 11.16).
Comme vous le constatez, modifier la ressource change les objets l'utilisant. Cachez les grilles nommées PanneauListe et PseudoListe afin de faire apparaître l'écran d'accueil. Affectez-lui la même ressource et faites de même avec tous les objets utilisant la même couleur au sein du projet.
10-2-1-2. Les pinceaux de couleurs▲
Les pinceaux de couleurs sont des ressources différentes, en cela qu'elles contiennent également des options de remplissage. Elles héritent de la classe abstraite Brush. On en trouve trois catégories : les pinceaux de couleur unie de type SolidColorBrush, ceux de dégradé linéaire LinearGradientBrush et les pinceaux de dégradé radial RadialGradientBrush. Les premiers sont utilisés quand vous définissez une couleur unie, par exemple #FFFF5900, ou lorsque vous affectez une ressource de couleur simple. Voici l'écriture XAML correspondante aux deux affectations :
<Path
x
:
Name
=
"FondAccueil"
Stretch
=
"Fill"
StrokeThickness
=
"1"
…>
<Path.Fill>
<SolidColorBrush
Color
=
"{StaticResource PanelBackground}"
/>
</Path.Fill>
</Path>
<TextBlock
x
:
Name
=
"Title"
…>
<TextBlock.Foreground>
<SolidColorBrush
Color
=
"#FFFFC800"
/>
</TextBlock.Foreground>
</TextBlock>
Comme vous le constatez, les propriétés de remplissage acceptent des instances de type Brush. Le langage XAML accepte toutefois des raccourcis d'écriture impossibles du côté C#, ainsi on pourra écrire :
<Rectangle
Height
=
"35” Width="
53
" Fill="
#FF000000
" … />
Alors qu'affecter une ressource ou une couleur en C# devra toujours passer par l'affectation d'une instance de Brush. Dans le cas d'une affectation de ressource de couleur cela donnera :
//Lorsqu'on récupère une ressource, on crée un pinceau de couleur unie
SolidColorBrush scb =
new
SolidColorBrush
(
);
//on récupère une ressource qu'on transtype en tant qu'instance de Color
//puis on l'affecte à la propriété Color du pinceau
scb.
Color =
(
Color)App.
Current.
Resources[
"PanelBackground"
];
//Pour finir on applique le pinceau à la propriété Fill
//d'un exemplaire de Shape
Panneau.
Fill =
scb;
Nous pourrions tout aussi bien affecter le pinceau à la propriété Background d'une instance de Control. Pour affecter une couleur hexadécimale directement, écrivez :
//Lorsqu'on affecte une valeur brute, on crée un pinceau de couleur unie
SolidColorBrush scb =
new
SolidColorBrush
(
);
//on utilise la méthode FromArgb de la classe Color
scb.
Color =
Color.
FromArgb
(
0xFF
,
0xFF
,
0x59
,
0x00
);
//On peut également utiliser une propriété statique de la classe Colors
scb.
Color =
Colors.
Orange;
Panneau.
Fill =
scb;
Dès lors, il peut être intéressant pour le développeur que le graphiste crée des ressources de pinceaux, car elles sont faciles à affecter et conservent en mémoire des propriétés de remplissage personnalisées via leurs propriétés Transform et RelativeTransform. L'intérêt d'un tel scénario pour les instances de SolidColorBrush est moins grand, car la couleur est unie. Toutefois, pour les pinceaux de dégradé, le potentiel d'optimisation est assez élevé. Afin de générer une ressource pinceau, quel que soit son type, il suffit de cliquer sur l'icône carrée située à droite d'un champ de remplissage, puis de choisir l'option Convert to New Resource… Sélectionnez le TextBlock nommé Title, en bas à gauche de l'interface. Affectez-lui un dégradé linéaire, par défaut du noir au blanc. Nous allons créer une nouvelle ressource de type pinceau. Pour cela, le mieux est de la stocker soit dans un dictionnaire prévu à cet effet, soit dans celui qui stocke déjà les couleurs.
Si vous créez deux dictionnaires de ressources et que les ressources de l'un font référence à celles contenues dans le second, il est alors nécessaire de créer une liaison. Ainsi, si vous décidez de créer un dictionnaire de pinceaux et que celui-ci utilise des ressources de couleurs unies définies dans un autre dictionnaire de ressources, il faudra les relier via le panneau Resources.
Attention, avant toute action, sélectionnez le répertoire RD au sein du panneau Projects. Créez un nouveau dictionnaire nommé BobifysBrushes.xaml afin d'y ajouter le pinceau de dégradé linéaire. Vous pouvez maintenant convertir le dégradé en ressource pinceau (voir Figure 11.17).
Dans la boîte de dialogue affichée, sélectionnez le dictionnaire de ressources BobifysBrushes.xaml comme espace de stockage, puis nommez la ressource HotTitle. Voici le code XAML généré lors de la création du pinceau :
<LinearGradientBrush
x
:
Key
=
"HotTitle"
EndPoint
=
"0.5,1"
StartPoint
=
"0.5,0"
>
<GradientStop
Color
=
"Black"
Offset
=
"0"
/>
<GradientStop
Color
=
"White"
Offset
=
"1"
/>
</LinearGradientBrush>
Comme vous le constatez, le pinceau est lui-même constitué de couleurs, vous pouvez facilement les remplacer par des ressources de couleurs. L'avantage de procéder de cette manière est de pouvoir mettre à jour le pinceau de dégradé via la modification des ressources de couleurs. À cette fin, cliquez sur la ressource pour accéder au sélecteur de couleur. Sélectionnez le premier point de couleur du dégradé et appliquez-lui la ressource "SoundTheme" via l'onglet Color resources. Répétez l'opération pour le second point de couleur du dégradé et appliquez-lui la ressource "Timecode-Theme" (voir Figure 11.18).
Le pinceau de dégradé est mis à jour au sein du dictionnaire de ressources, son code a légèrement changé, car ses balises GradientStop référencent maintenant des ressources de couleurs :
<LinearGradientBrush
x
:
Key
=
"HotTitle"
EndPoint
=
"0.5,1"
StartPoint
=
"0.5,0"
>
<GradientStop
Color
=
"{StaticResource TimecodeTheme}"
Offset
=
"0"
/>
<GradientStop
Color
=
"{StaticResource SoundTheme}"
Offset
=
"1"
/>
</LinearGradientBrush>
À ce stade nous avons presque fini, si vous compilez vous risquez d'avoir des erreurs. C'est tout à fait logique, le dictionnaire BobifysBrushes.xaml utilise des ressources de couleurs appartenant à un autre dictionnaire. Celles-ci sont accessibles via l'interface de Blend, car les deux dictionnaires sont référencés par App.xaml. Toutefois, il faut créer une liaison au sein du dictionnaire de pinceaux pointant vers celui contenant les couleurs afin qu'il puisse les utiliser. Dans cette optique, utilisez l'interface de Blend de la manière vue à la section 10.1.2.3 Externaliser les ressources. Le code XAML généré par Blend donne :
<ResourceDictionary
xmlns
=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns
:
x
=
"http://schemas.microsoft.com/winfx/2006/xaml"
>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source
=
"Couleurs.xaml"
/>
</ResourceDictionary.MergedDictionaries>
<LinearGradientBrush
x
:
Key
=
"HotTitle"
EndPoint
=
"0.5,1"
StartPoint
=
"0.5,0"
>
<GradientStop
Color
=
"{StaticResource TimecodeTheme}"
Offset
=
"0"
/>
<GradientStop
Color
=
"{StaticResource SoundTheme}"
Offset
=
"1"
/>
</LinearGradientBrush>
</ResourceDictionary>
Vous remarquez qu'il n'y a pas besoin de spécifier l'accès au répertoire RD comme ce que nous avons dans le fichier App.xaml :
<Application
xmlns
=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns
:
x
=
"http://schemas.microsoft.com/winfx/2006/xaml"
x
:
Class
=
"LecteurMultiMedia.App"
>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source
=
"RD/Couleurs.xaml"
/>
<ResourceDictionary
Source
=
"RD/BobifysBrushes.xaml"
/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
C'est tout à fait logique, car les dictionnaires de ressources sont dans le même répertoire. L'avantage d'utiliser l'interface de Blend est que les accès seront résolus automatiquement pour vous dans de nombreux cas. Recompilez le projet, cette fois l'application est correctement affichée dans le navigateur.
Revenons sur la traduction du code XAML exprimant le dégradé linéaire. Les propriétés StartPoint et EndPoint définissent le début et la fin du dégradé sous forme de coordonnées relatives x et y. Ainsi la valeur 0.5 signifie que leurs coordonnées sont situées à 50 % de la largeur totale de l'objet ; la valeur 1 de la propriété EndPoint indique que la fin du dégradé est située à 100 % de la hauteur totale de l'objet (donc sur l'axe y). Il est possible de modifier ces propriétés simplement en dépliant l'onglet situé sous la barre des dégradés. Vous pouvez également afficher les coordonnées absolues des points de départ et d'arrivée grâce aux champs qu'il propose (voir Figure 11.19).
Pour apprendre le fonctionnement des dégradés, le mieux est encore de modifier les propriétés exposées dans l'onglet, sur des dégradés simples.
En tant que designer, il est possible que vous soyez tenté d'utiliser le manipulateur des dégradés dans la barre d'outil () afin de mettre à jour le pinceau (voir Figure 11.20). C'est une mauvaise idée : lorsque vous essayez de modifier le pinceau de dégradé ainsi, vous supprimez automatiquement son affectation au remplissage concerné. Ce comportement est logique puisque la configuration du dégradé est définie dans le pinceau lui-même. Blend considère que vous ne souhaitez plus utiliser le pinceau et génère un nouveau dégradé à partir du pinceau existant. Il est donc conseillé de finir complètement le dégradé avant de le transformer en pinceau. Vous pouvez également créer un nouveau dégradé et remplacer les balises au sein du pinceau par celles du nouveau dégradé. De cette manière, le pinceau est mis à jour plus facilement.
Il pourrait être utile de sauvegarder l'un des dégradés gérant le reflet lumineux de certains éléments de l'interface. Dans la grille nommée SliderTimeCode, sélectionnez le deuxième enfant de type Path. Il possède un remplissage qui génère un léger effet de réflexion. Définissez-le en tant que ressource de dégradé de la même manière que précédemment, nommez-le Reflection et stockez-le dans le dictionnaire dédié aux pinceaux. Le code généré est légèrement différent, car il s'agit cette fois d'un dégradé radial :
<RadialGradientBrush
x
:
Key
=
"Reflection"
RadiusX
=
"2.82514"
RadiusY
=
"4.23125"
Center
=
"0.504054,-2.13556"
GradientOrigin
=
"0.504054,-2.13556"
>
<RadialGradientBrush.RelativeTransform>
<TransformGroup/>
</RadialGradientBrush.RelativeTransform>
<GradientStop
Color
=
"#00FFFFFF"
Offset
=
"0.495"
/>
<GradientStop
Color
=
"#7FFFFFFF"
Offset
=
"0.618605"
/>
<GradientStop
Color
=
"#00FFFFFF"
Offset
=
"0.627"
/>
</RadialGradientBrush>
Comme vous le constatez, quatre propriétés sont utilisées pour le définir : RadiusX, RadiusY,
Center et GradientOrigin. Vous notez qu'il est également possible d'affecter des transformations relatives aux dégradés grâce au nœud RadialGradientBrush.RelativeTransform (également valables pour les pinceaux d'images et de vidéos). Au sein de Blend, vous y avez accès via le panneau Properties ou la barre d'outils. Restez appuyé sur l'outil des dégradés situé dans la barre d'outils des dégradés pour faire apparaître le modificateur de transformations relatives des dégradés ().
10-2-2. Les pinceaux d'images et de vidéos▲
Les images ou les vidéos sont, la plupart du temps, chargées de manière dynamique pour créer un portfolio, un catalogue ou une webtv. L'utilisation de pinceaux d'images (ou de pinceaux vidéo) est spécifique à certaines problématiques de conception. Cela est particulièrement utile lorsqu'une même image doit être affichée plusieurs fois ou que l'utilisation d'un tracé vectoriel se révèle moins pratique dans le flux de production. Dans ce cas, l'image n'est qu'une seule fois en mémoire et son pinceau peut être affiché plusieurs fois sans affecter les performances de l'application.
Les étapes nécessaires à la création de pinceaux d'images ou de vidéos sont similaires. Assez étrangement, il est obligatoire d'utiliser un composant de type Image ou MediaElement pour fabriquer ce type de pinceau. Ci-dessous la procédure de création.
- Vous devez commencer par importer une ressource média de type image ou vidéo compatible (png, jpg, wmv, etc.). Cette ressource sera par défaut compilée au sein du fichier xap dans la dll du même nom. C'est pourquoi il faut éviter qu'elle ne pèse trop lourd.
- Ensuite il faut la double-cliquer au sein du panneau Projects afin de la positionner et de l'afficher via l'utilisation d'un composant adéquat. Ainsi, le composant Media-Element jouera une vidéo, le composant Image affichera quant à lui les bitmaps.
- La dernière étape consiste à sélectionner le composant généré, puis à choisir depuis le menu Tools > Make Brush Ressource > Make Image Brush Resource ou Make Video Brush Resource selon le type de pinceau que vous souhaitez créer.
Via le panneau Projects, créez un nouveau répertoire nommé bitmaps. Faites un clic droit, puis sélectionnez l'option Add Existing Item… Choisissez le répertoire ImageBrushes, dans le dossier Assets que vous avez téléchargé et décompressé précédemment. Sélectionnez les trois images au format png placées dans le répertoire et importez-les. Double-cliquez-les afin de créer trois composants Image dans le conteneur LayoutRoot. Sélectionnez le répertoire RD, puis le composant Image correspondant à l'icône de la note de musique, faites-en un pinceau d'image via le menu Tools. Choisissez de créer un nouveau dictionnaire de ressources dédié aux pinceaux d'images et de vidéos. Nommez le dictionnaire VideosImagesBrushes.xaml et le pinceau IconMusic. Procédez de même avec les deux autres instances d'Image et stockez les pinceaux dans le nouveau dictionnaire. Supprimez ensuite les composants Image, car ils n'ont plus aucune utilité. Voici le code XAML généré :
<ResourceDictionary
xmlns
=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns
:
x
=
"http://schemas.microsoft.com/winfx/2006/xaml"
>
<ImageBrush
x
:
Key
=
"IconBitmap"
ImageSource
=
"bitmaps/Bitmap.png"
/>
<ImageBrush
x
:
Key
=
"IconMusic"
ImageSource
=
"bitmaps/Music.png"
/>
<ImageBrush
x
:
Key
=
"IconVideo"
ImageSource
=
"bitmaps/video.png"
/>
</ResourceDictionary>
Il est souvent nécessaire de régler certains détails pour que tout fonctionne correctement. Ouvrez le panneau Resources et dépliez le dictionnaire VideosImagesBrushes.xaml afin de voir si les images apparaissent bien. En réalité, un bogue de l'interface de Blend peut être assez déroutant : notre dictionnaire est dans le répertoire RD alors que les images sont dans le répertoire bitmaps, le chemin d'accès décrit dans le XAML au-dessus est donc faux. Voici le code XAML à modifier pour que les pinceaux d'images soient correctement affichés :
<ResourceDictionary
xmlns
=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns
:
x
=
"http://schemas.microsoft.com/winfx/2006/xaml"
>
<ImageBrush
x
:
Key
=
"IconBitmap"
ImageSource
=
"../bitmaps/Bitmap.png"
/>
<ImageBrush
x
:
Key
=
"IconMusic"
ImageSource
=
"../bitmaps/Music.png"
/>
<ImageBrush
x
:
Key
=
"IconVideo"
ImageSource
=
"../bitmaps/video.png"
/>
</ResourceDictionary>
Comme vous le constatez, nous avons créé un nouveau dictionnaire dédié à ce type de ressources. Plusieurs contraintes techniques propres au flux de production nous orientent dans cette direction. La plus importante est liée au fait que les pinceaux d'images font référence à des objets physiques propres au projet. Si vous devez par la suite exporter les pinceaux d'images ou de vidéos dans un autre projet, il vous faudra également dupliquer le répertoire physique contenant les images ou les vidéos. Cette contrainte n'existe pas pour les dictionnaires de pinceaux de dégradés ou les ressources de couleurs qui sont autonomes par nature. Ces ressources sont en effet décrites entièrement en XAML, exporter le dictionnaire suffit. Dans ces conditions autant scinder ces deux types de pinceaux du point de vue de l'organisation. La deuxième raison est plus prosaïque : nous aurions pu stocker les pinceaux dans la page principale, mais autant organiser les ressources sainement dès le départ et faciliter la maintenance de l'application.
Afin d'appliquer les pinceaux, supprimez les enfants des composants Grid anonymes situés au sein des grilles respectivement nommées RadioMusic, RadioImage, RadioVideo. Via le mode d'édition XAML transformez la grille en Rectangle. Cette tâche est assez aisée : il vous suffit de supprimer les propriétés non supportées par le Rectangle. Voici le code XAML avant le changement envisagé :
<Grid
x
:
Name
=
"RadioVideo"
Height
=
"53"
HorizontalAlignment
=
"Left"
Margin
=
"166,1,0,0"
VerticalAlignment
=
"Top"
Width
=
"49"
>
<TextBlock
FontFamily
=
"Tahoma"
FontSize
=
"12"
HorizontalAlignment
=
"Center"
VerticalAlignment
=
"Bottom"
Margin
=
"0,0,0,-2"
RenderTransformOrigin
=
"0.5,0.5"
><Run
Text
=
"Video"
Foreground
=
"#FFFFFFFF"
/></TextBlock>
<Grid
HorizontalAlignment
=
"Center"
VerticalAlignment
=
"Center"
Width
=
"28"
Height
=
"32"
Margin
=
"0,0,0,4"
>
<Path …/>
<Path …>
<Path.Fill>
<LinearGradientBrush
StartPoint
=
"0.497509,0.0614041"
EndPoint
=
"0.497509,0.929826"
>
<GradientStop
Color
=
"#00FFFFFF"
Offset
=
"0"
/>
<GradientStop
Color
=
"#BFFFFFFF"
Offset
=
"1"
/>
</LinearGradientBrush>
</Path.Fill>
</Path>
</Grid>
</Grid>
Voici le code XAML après modification :
<Grid
x
:
Name
=
"RadioVideo"
Height
=
"53"
HorizontalAlignment
=
"Left"
Margin
=
"166,1,0,0"
VerticalAlignment
=
"Top"
Width
=
"49"
>
<TextBlock
FontFamily
=
"Tahoma"
FontSize
=
"12"
HorizontalAlignment
=
"Center"
VerticalAlignment
=
"Bottom"
Margin
=
"0,0,0,-2"
RenderTransformOrigin
=
"0.5,0.5"
><Run
Text
=
"Video"
Foreground
=
"#FFFFFFFF"
/></TextBlock>
<Rectangle
HorizontalAlignment
=
"Center"
VerticalAlignment
=
"Center"
Width
=
"48"
Height
=
"32"
Margin
=
"0,0,0,4"
Fill
=
"{StaticResource IconVideo}"
/>
</Grid>
Vous constatez que ce code est beaucoup plus sobre et surtout très efficace en terme de flux de production. Un graphiste n'aura qu'à mettre à jour les icônes via n'importe quel outil externe, tel que Photoshop. Il n'aura pas besoin de connaître Expression Blend pour mettre à jour l'application, il lui suffira simplement de recompiler pour prendre en compte les icônes modifiées. L'une des pratiques dans ce domaine consiste à ne pas importer directement les ressources de type média dans le projet Silverlight, mais plutôt à créer un lien pointant sur elles. Vous pouvez accéder à cette option via un simple clic droit au sein du panneau Projects en sélectionnant l'option Link To Existing Item… (voir Figure 11.21).
Cette méthodologie est encore plus transparente pour le designer, mais donnera moins de souplesse, car elle force à ne pas déplacer ou réorganiser les ressources médias liées trop souvent à un projet.
Il est également possible de créer un pinceau d'image en dur sans le définir comme ressource. Dans cette optique, il vous faudra utiliser le quatrième onglet, nommé Tile Brush au survol, situé au sein du panneau Brushes, puis choisir le média que vous souhaitez utiliser en tant que remplissage. Toutefois cette méthodologie est anecdotique et ne devrait pas être privilégiée.
Du côté C#, appliquer un pinceau d'image est très facile, car le développeur peut à tout moment cibler la ressource créée par le graphiste. Ainsi au lieu d'avoir un code verbeux du type :
//1 - on crée un objet Uri qui pointe vers notre fichier image à utiliser
Uri adresseImage =
new
Uri
(
"video.png"
,
UriKind.
Relative);
//2 - on crée un objet de type BitmapImage en lui spécifiant
//l'adresse relative que nous venons de définir
BitmapImage bi =
new
BitmapImage
(
adresseImage );
//3 - On crée un pinceau d'image
ImageBrush ib =
new
ImageBrush
(
);
//4 - on spécifie l'image source de ce pinceau
ib.
ImageSource =
bi;
//5 - on affecte la propriété Fill d'une instance de Shape
monRectangle.
Fill =
ib;
On obtient quelque chose de beaucoup plus simple du point de vue développement, mais également dans la répartition des tâches de chaque pôle métier. Le développeur n'a pas besoin de connaître l'emplacement ou le nom de l'image à afficher. Il lui suffit de connaître le nom de la ressource. À cette fin, le mieux est encore de se baser sur une nomenclature définie dès le départ du projet en concertation avec l'ensemble des acteurs de la production :
//on récupère la ressource qu'on transtype en ImageBrush
ImageBrush ib =
(
ImageBrush)App.
Current.
Resources[
"IconVideo"
];
//Dans ce cas, on applique le pinceau d'image à la propriété Fill
//d'une instance de Shape
monRectangle.
Fill =
ib;
À la section 11.3 Contrôles personnalisables, nous découvrirons en quoi les pinceaux permettent en général d'éviter la prolifération excessive de styles et modèles de composant.
10-3. Les polices de caractères▲
Contrairement aux technologies habituelles du Web reposant sur la mise en forme XHTML, Silverlight offre de nombreux avantages en matière de conception graphique. L'un d'eux est de pouvoir afficher des polices de caractères personnalisées et de permettre des mises en forme assez avancées. L'affichage étant géré par un moteur vectoriel, certaines bonnes pratiques de conception doivent être connues pour bénéficier d'un affichage propre et performant.
10-3-1. Afficher et mettre en forme du texte▲
Au sein de Silverlight, deux mécanismes de mise en forme de texte cohabitent. Celui qui est le plus couramment utilisé repose sur les composants TextBlock, TextBox et sa sous-classe PasswordBox. Tous ces contrôles sont directement hérités de FrameworkElement. Ils n'ont donc pas de propriété Template permettant de modifier leur modèle. Autrement dit, leur affichage est uniquement déterminé par le texte qu'ils contiennent. Le composant TextBlock a pour unique but d'afficher du texte non-interactif. Il est très simple d'utilisation et possède des options de mise en forme assez similaires à ce que vous trouverez dans n'importe quel éditeur de texte. Les contrôles TextBox et PasswordBox sont des champs de saisie utilisateur n'ayant pas pour objectif de proposer des options de mises en forme avancées. Le second mécanisme de mise en forme consiste à utiliser la classe Glyphs qui fournit une API bas niveau de formatage de texte. Celle-ci est réellement utile lorsque vous avez besoin d'un contrôle total et dynamique sur le texte affiché dans Silverlight. Voici un exemple XAML simple de l'écriture d'une instance de Glyphs :
<Glyphs
Height
=
"100"
Width
=
"100"
Fill
=
"Red"
FontRenderingEmSize
=
"12"
FontUri
=
"Fonts/tahoma.ttf"
UnicodeString
=
"Hey"
OriginX
=
"10"
OriginY
=
"20"
Indices
=
",85;,64;,67;6"
/>
Nous n'allons pas nous étendre sur l'API bas niveau, car cela sortirait du cadre de ce livre. Nous apprendrons donc à utiliser le composant TextBlock qui répond à 80 % des cas d'utilisation. Grâce à ce dernier, nous pouvons appliquer une mise en forme générale via ses propriétés, qui sont accessibles dans la section Text du panneau Properties (voir Figure 11.22).
Ci-dessous une liste non exhaustive des propriétés que vous pouvez configurer.
- Le choix de la police de caractères (Arial, Verdana, Calibri, etc.). En pratique, lorsque vous choisissez une police différente de celle proposée par défaut, il vous faudra l'embarquer dans 90 % des cas. Nous abordons plus en détail l'intégration de polices à la section 11.3.2 Créer des contrôles .
- La taille de police exprimée en pixels ou en points selon la configuration de l'interface de Blend. Pour choisir l'une de ces deux unités, ouvrez le menu Tools > Options… et dans l'onglet Units, choisissez Points ou Pixels. Notez que dans tous les cas, le XAML est formaté en tenant compte de valeurs exprimées en pixels.
- Afficher le texte en gras, italique ou souligné est réalisé grâce aux propriétés respectives FontStyle, FontWeight et TextDecoration. Pour avoir un vrai gras, il faudra utiliser une police correspondante, dans le cas contraire il est simulé. C'est exactement le même principe avec l'italique.
- Il est également possible de modifier l'espacement entre les caractères (l'interlettrage) en utilisant la propriété FontStretch. La police n'est toutefois pas modifiée dynamiquement. Lorsque vous l'utilisez, cette propriété fait référence à une police correspondant à l'espacement spécifié (Condensed, par exemple). Lorsque cette police n'est pas disponible, modifier la valeur de cette propriété ne change rien et ne renvoie aucune erreur ou alerte.
- Vous pouvez aligner le texte à droite, à gauche ou au centre, via la propriété TextAlignment qui accepte une valeur de l'énumération du même nom. Malheureusement, il n'est pas possible de justifier le texte, car la valeur TextAlignment.Justify n'existe que dans WPF.
- Le retour à la ligne automatique est gérée grâce à la propriété TextWrapping et à l'énumération correspondante contenant les valeurs Wrap et NoWrap. Définir cette propriété à NoWrap empêchera tout retour à la ligne automatique. Cela est visible lorsque la largeur du champ texte est définie en pixels et non en mode automatique. A contrario, si la propriété Text-Wrapping est affectée de la valeur Wrap, que vous fixez la largeur du TextBlock en dur et que la hauteur est définie en mode Auto, alors le texte contenu ira automatiquement à la ligne. La hauteur du champ s'adaptera dynamiquement à la quantité de texte à afficher.
- Pour finir, il est possible de spécifier une hauteur de ligne via la propriété LineHeight.
Toutes ces capacités sont assez utiles, mais elles demeurent insuffisantes en l'état, car elles sont propres à TextBlock. A priori, cela nous oblige à avoir autant de TextBlock que de mises en forme. Fort heureusement, les TextBlock ont la capacité de posséder des éléments enfants mis en forme et qui sont des instances de la classe Inline. Ainsi, TextBlock possède la propriété Inlines qui n'est rien d'autre qu'une collection d'instances de type Inline. La classe Inline est abstraite, elle sert de base à deux classes concrètes en héritant : Run et LineBreak. Les instances de type Run possèdent presque toutes les capacités d'un TextBlock. Grâce à ces dernières, vous pouvez appliquer des mises en forme différentes au sein d'un même champ TextBlock. Ces balises sont générées dans Blend lorsque vous double-cliquez dans un TextBlock, que vous sélectionnez une portion spécifique du texte contenu, puis que vous définissez les options de mise en forme. Il est également possible de le faire via le panneau des propriétés en cliquant sur la propriété Inlines et en ajoutant chaque portion de texte manuellement. Les instances de LineBreak permettent, quant à elles, de faire des retour à la ligne. Voici le code XAML d'un exemple de texte mis en forme (voir Figure 11.23) via l'utilisation de ces balises dans Blend :
<TextBlock
Margin
=
"70,87,192,86"
TextWrapping
=
"Wrap"
>
<Run
FontFamily
=
"Calibri"
FontSize
=
"16"
FontWeight
=
"Bold"
Text
=
"3DLightEngine Library preview"
/>
<LineBreak/><Run
FontStyle
=
"Italic"
Foreground
=
"Gray"
Text
=
" Fri, Sep 4th, 2009"
/>
<LineBreak/><Run
Foreground
=
"BlueViolet"
Text
=
" Posted by Eric Ambrosi 01:09 PM"
/>
<LineBreak/><LineBreak/><Run
FontWeight
=
"Bold"
TextDecorations
=
"Underline"
Text
=
"us version"
/>
<LineBreak/><LineBreak/><Run
FontFamily
=
"Calibri"
Text
=
"Je mettrai bientôt à disposition une librairie ..."
/>
</TextBlock>
Cette écriture est simplifiée, vous pourriez tout aussi bien affecter la propriété Inlines en XAML de cette manière :
<TextBlock
Margin
=
"70,87,192,86"
TextWrapping
=
"Wrap"
>
</TextBlock.Inlines>
<Run
FontFamily
=
"Calibri"
FontSize
=
"16"
FontWeight
=
"Bold"
Text
=
"3DLightEngine Library preview"
/>
<LineBreak/><Run
FontStyle
=
"Italic"
Foreground
=
"Gray"
Text
=
"Fri, Sep 4th, 2009"
/>
<LineBreak/><Run
Foreground
=
"BlueViolet"
Text
=
" Posted by Eric Ambrosi 01:09 PM"
/>
<LineBreak/><LineBreak/><Run
FontWeight
=
"Bold"
TextDecorations
=
"Underline"
Text
=
"us version"
/>
<LineBreak/><LineBreak/><Run
FontFamily
=
"Calibri"
Text
=
"Je mettrai bientôt à disposition une librairie ..."
/>
</TextBlock.Inlines>
</TextBlock>
Comme vous le constatez, deux propriétés assez utiles ne sont pas fournies par la classe Run. La première, LineHeight, concerne la hauteur des lignes, la seconde nommée TextAlignment, gère l'alignement horizontal du texte. Ainsi le texte contenu dans un TextBlock aura en commun ces deux propriétés que le texte soit, ou non, décrit dans des balises Run. Il n'y a pas vraiment de solution élégante à cette problématique. L'idéal pour des mises en forme plus abouties est d'employer la classe Glyphs, mais elle est moins facile à utiliser au quotidien, car d'assez bas niveau.
Obtenir une mise en forme équivalente en C# est relativement simple. Il suffit de créer des instances de type Inline puis de les ajouter à la collection Inlines qui est une propriété de TextBlock :
Run r1 =
new
Run
(
);
r1.
FontFamily =
new
FontFamily
(
"Calibri"
);
r1.
FontSize =
13
;
r1.
FontWeight =
FontWeights.
Bold;
r1.
Text =
"Ce texte est formaté via une balise Run héritant de la classe abstraite Inline."
;
Run r2 =
new
Run
(
);
r2.
FontFamily =
new
FontFamily
(
"Consolas"
);
r2.
Foreground =
new
SolidColorBrush
(
Colors.
Orange);
r2.
FontSize =
16
;
r2.
FontWeight =
FontWeights.
Bold;
r2.
Text =
"Ceci est une autre balise run avec une mise en forme différente."
;
//on ajoute la première balise Run
MiseEnFormeDynamique.
Inlines.
Add
(
r1);
//on va à la ligne en ajoutant une instance de LineBreak
MiseEnFormeDynamique.
Inlines.
Add
(
new
LineBreak
(
));
//on ajoute une seconde balise Run
MiseEnFormeDynamique.
Inlines.
Add
(
r2);
Vous trouverez dans le dossier chap11 la solution contenant ces exemples : MiseEnForme.zip. Le résultat de cet exercice est visible à la Figure 11.24.
10-3-2. Polices personnalisées▲
Les polices de caractères occupent une position centrale dans la charte graphique et définissent en partie l'identité visuelle d'une entreprise. Il est donc important de comprendre comment intégrer ces dernières de manière efficace.
10-3-2-1. Les polices par défaut▲
Le lecteur Silverlight contient nativement un certain nombre de polices de caractères. Pour les identifier, référez-vous à la liste des polices accessibles par défaut au sein de tout composant TextBlock. Lorsque vous les utilisez, il n'est pas nécessaire de les embarquer, puisqu'elles le sont déjà. Les polices intégrées par défaut possèdent l'icône Silverlight à droite de leur nom (voir Figure 11.25).
Parmi celles qui sont intégrées, on distingue Portable User Interface. Celle-ci n'en est pas vraiment une à proprement parler. Dans les faits, elle est constituée d'un agglomérat de plusieurs polices de caractères. Pour les cultures occidentales, elle affiche l'équivalent de la police Lucida Sans Unicode ou Lucida Grande. Elle permet en outre de supporter l'affichage de texte pour d'autres cultures à travers le monde. Ainsi, sur un système d'exploitation chinois ou russe, elle apparaîtra de manière différente. Toutes les autres polices intégrées par défaut engendreront un rendu identique quelle que soit la culture du système d'exploitation client. Il est peu fréquent qu'une entreprise, une association ou une marque utilise l'une de ces polices, car elles sont trop standard et passe-partout. Les créatifs choisissent souvent des polices moins connues permettant ainsi à l'utilisateur d'identifier plus facilement une marque ou un produit.
Produire souvent du contenu visuel pour différentes marques, produits ou entreprises, a pour conséquence d'installer de nombreuses polices sur le système d'exploitation du concepteur ou des créatifs. Certaines des polices installées ne seront utiles que 15 jours ou moins. Dans cette optique, le mieux est toujours d'utiliser des gestionnaires de polices permettant de les activer ou de les désactiver à volonté. De cette manière, vous éviterez de polluer votre poste et vous pourrez même organiser ces dernières de manière plus élégante que ne le fait le répertoire Fonts. Le logiciel SuitCase de la société Extensis est l'exemple type d'un gestionnaire de polices. Vous le trouverez à l'adresse : http://www.extensis.com/fr/products/suitcasefusion2/.
10-3-2-2. Embarquer les polices▲
Vous avez sans doute remarqué les multiples messages d'alerte dans le panneau Results. Ils indiquent que vous utilisez des polices qui ne sont pas embarquées par défaut dans le lecteur Silverlight (voir Figure 11.26).
Si vous codez directement en XAML dans Expression Blend, la propriété FontFamily est soulignée afin de vous alerter. Lorsque vous compilez, les polices ne sont pas affichées au sein de l'application dans le navigateur. Plusieurs choix s'offrent à vous pour résoudre cette problématique.
- Vous pouvez utiliser l'une des polices intégrées par défaut. D'un point de vue expérience utilisateur, ce n'est pas un très bon choix. Il offre toutefois l'avantage de ne pas augmenter le poids final du fichier compilé.
- Il est également possible de vectoriser un composant TextBlock. Cette solution est viable pour un nombre limité de champs texte utilisant une police spécifique. Cela n'est valable que si ces derniers n'ont pas besoin d'être mis à jour dynamiquement ou fréquemment dans Blend. De cette manière, vous en faites des éléments graphiques servant de décors. Cette approche est désavantageuse en termes de maintenance, puisque vous ne pourrez plus changer leur contenu. Il faut toutefois la prendre en considération, car cette méthode vous permet également de profiter des avantages propres aux tracés vectoriels en matière de design.
- La troisième façon consiste à embarquer manuellement la police. Cette opération peut être réalisée sous Visual Studio ou Blend, mais, le plus souvent, c'est le designer interactif qui s'en occupe, puisqu'il joue le rôle d'intégrateur. De cette manière, la police sera compilée au sein du fichier xap et accessible par les composants qui y feront référence.
- Une autre manière de procéder consiste à télécharger dynamiquement une police de caractères présente sur le serveur Web. Nous étudierons cette voie à la section 10.5 Le modèle Slider.
-
La dernière solution, présente depuis Silverlight 3, consiste à utiliser des polices système. Il existe deux principes différents dans ce cas. Le premier consiste à faire référence à la police dans le XAML ou en C#, en affectant directement la propriété FontFamily. Dans ce cas, le nombre de polices système accessibles est limité : Calibri, Consolas, Constantia, etc. (voir la documentation pour une liste complète). La seconde possibilité est de charger dynamiquement n'importe quelle police du système d'exploitation. Toutefois, cette méthode n'est possible qu'en JavaScript et non via le code managé C# ou VB. De plus, il est nécessaire d'utiliser des mécanismes de mise en forme de texte de bas niveau reposant sur la classe Glyphs. Cette solution peut être avantageuse mais nécessite beaucoup plus d'efforts.
Dans tous les cas, si la police ciblée n'est pas sur le système client, elle sera remplacée par la police Portable User Interface dans le premier cas. Dans le second cas, le texte ne sera pas affiché. Ces solutions ne sont donc viables que dans des environnements maîtrisés, dans un intranet d'entreprise, par exemple.
Pour notre part, nous allons embarquer les polices au sein du fichier xap via Expression Blend, puis nous aborderons les options offertes par ce dernier. Dans le projet actuel, nous devons intégrer deux polices. La première, Showcard Gothic, est utilisée pour le logotype du lecteur multimédia. La seconde est Tahoma, qui concerne 95 % du texte affiché dans l'application.La police Tahoma fait partie de la liste des polices que Silverlight peut charger dynamiquement lorsqu'elles sont installées sur le poste client. Pour cette dernière, nous pourrions donc décider de nous reposer sur ce mécanisme et éviter de l'embarquer. Dans le cas où elle ne serait pas présente sur le système client, la police "Portable User Interface" la remplacerait. Ce choix de conception est à votre discrétion. Tout dépendra de vos objectifs et contraintes de production, mais également du type d'environnement et d'utilisateur que vous ciblez. L'impact peut être désastreux si la typographie initialement prévue est très différente de la police PortableUserInterface : taille, empâtement, interlettrage, les caractéristiques typographiques sont autant de paramètres qui diffèrent d'une police à l'autre. Le visuel global de votre application peut en être très fortement affecté.
Dans le menu Tools, choisissez le menu Font Manager… Une boîte de dialogue s'ouvre, c'est en fait un gestionnaire de polices propre à la solution Silverlight (voir Figure 11.27).
À travers ce gestionnaire, vous avez la capacité d'embarquer une ou plusieurs polices ainsi que de lister les caractères à intégrer pour chacune d'entre elles. Nous allons aborder plusieurs points de vue différents, l'objectif final étant d'optimiser au maximum le poids du fichier compilé tout en gardant le visuel intact.
L'une des manières d'aborder cette problématique est illustrée par l'intégration de la police Showcard Gothic. Trouvez cette dernière en tapant les premières lettres de son nom dans le champ de recherche. Cliquez sur la case à cocher à côté de la police. Vous avez le choix entre embarquer la police dans sa totalité ou seulement une partie des caractères dont vous avez besoin. Comme les champs Text l'utilisant sont des éléments purement graphiques, il n'est pas nécessaire de tout embarquer. Décochez l'option All glyphs. Seule l'option de remplissage automatique Auto fill reste cochée. Cette propriété est très utile, car elle permet de n'embarquer que les caractères dont la présence est détectée à la compilation. La différence entre les deux options est flagrante. Le poids du fichier xap est inférieur de 40 ko dans le second cas, cela n'est pas négligeable sur Internet. Le remplissage automatique est une bonne option de ce point de vue. Vous ne pourrez toutefois pas créer de textes affectés de cette police, à l'exécution, car vous prendriez le risque de ne pas pouvoir afficher certains caractères non embarqués à la compilation.
Lorsque vous avez embarqué la police, vous avez sans doute remarqué une très légère pause de fonctionnement. Celle-ci est due au fait que Blend a ajouté la police directement au sein du projet dans un répertoire Fonts spécialement créé à cet effet. Ce principe est très avantageux, il vous permet de partager le projet sans risque d'erreurs à la compilation. La personne à qui vous donnerez le projet n'aura pas besoin d'installer la police sur le système, puisque celle-ci fait partie du projet.
Ouvrez à nouveau le gestionnaire de polices si ce n'est pas fait. Cherchez la police Tahoma, vous devez maintenant vous poser une question. Devez-vous tout embarquer ou seulement certains caractères ? Pour répondre à cette question, vous devez répondre à plusieurs autres.
-
Quelle est la zone internationale ciblée par votre application ? Par exemple, celle-ci est-elle faite pour le continent nord-américain, l'Europe, les pays nordistes ou l'ensemble de ces contrées ? Si vous connaissez la réponse, vous savez quels caractères doivent être embarqués en cas de saisie utilisateur dans l'application. Vous pourrez ainsi prévoir les caractères auquel l'internaute aura accès pour remplir un formulaire, par exemple.
- S'il n'y a pas de saisie utilisateur prévue, dans quelles langue(s) ou culture(s) les champs textes dynamiques sont-ils susceptibles d'être mis à jour ?
Nous allons essayer de fournir la réponse la mieux adaptée. Laissez coché All glyphs, puis compilez le projet. Dans le répertoire Debug, vous pouvez constater que le poids du fichier est d'environ 780 ko, nous avons environ 400 ko de police embarquée en trop. Cela est normal, car la police Tahoma est très complète et supporte de nombreux caractères différents, intégrer tous ses caractères est une erreur d'optimisation. Nous n'avons besoin que d'une fraction de ceux-ci. Partant du principe que notre application ne ciblera que les cultures occidentales, autant intégrer uniquement ceux que nous sommes susceptibles d'afficher. Voici une liste de caractères que je garde sous la main et que j'incrémente au besoin au fur et à mesure des projets que je réalise :
- abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZàâäçéèêëîïôœùûüÿÀÂÄÇ
ÈÉÊËÎÏÔŒÙÜ'.,;:!?"«»€&~#{}()-_`\/@[]°=+-*%µ|0123456789©®™†€÷æÆß…£$¥
Attention à bien ajouter l'espace normale, car c'est un caractère à part entière. Vous pouvez vous inspirez de cette liste ou créer la vôtre. Dans le gestionnaire de polices, décocher l'option All Glyphs, laissez l'option Auto fill sélectionnée, mais ajoutez la liste des caractères ci-dessus dans le champ en bas nommé Include glyphs. Cliquez sur OK. Une fois encore la police a été ajoutée au répertoire Fonts (voir Figure 11.28).
Recompilez l'application. Le fichier xap fait désormais 380 ko, ce qui est beaucoup mieux. Le gain est d'environ 400 ko ce qui est vraiment appréciable en terme de téléchargement. L'utilisateur passera moins de temps à attendre l'affichage de votre application. Vous remarquez qu'aucune alerte n'est désormais visible dans le panneau Results.
Vous pouvez télécharger le projet LecteurMultiMedia_BF.zip contenant les différents pinceaux ainsi que l'intégration des polices.
10-4. Styles et modèles de composants▲
Les styles et les modèles sont des ressources avancées et représentent le fer de lance du principe de réutilisation mis en exergue par Silverlight et WPF. Ils ont pour objectif d'améliorer le flux de production en facilitant la réutilisation du jeu de composants fourni sur les plates-formes Silverlight et WPF. La création de composants visuels personnalisés via l'héritage, bien que possible, n'est plus autant nécessaire qu'avec la bibliothèque Winforms, grâce aux notions de styles et de modèles de contrôles introduits avec .Net 3. Dans les prochaines sections, nous concevrons des composants personnalisés à travers les exemples du Slider et de la ListBox. Ce faisant, nous apprendrons les bonnes pratiques et approfondirons nos connaissances des méthodologies de production. Comme bon nombre d'environnements de développement, la grande majorité des interactions et des fonctionnalités est apportée par le jeu de composants proposé par défaut ou par des bibliothèques comme Silverlight Toolkit. Chaque composant possède un rôle propre et délimité, ce qui lui permet d'être souvent réutilisé au sein de plusieurs applications. Afin d'apporter un maximum de souplesse de conception, la fonctionnalité et l'interactivité supportées par un contrôle (Slider par exemple) ne sont pas couplés fortement au design ou au style visuel de ce dernier. Réutiliser des composants existants sera toujours plus productif que les créer par vous-même. Grâce aux styles et modèles, vous pourrez conserver les fonctionnalités des contrôles tout en modifiant complètement leur aspect visuel.
10-4-1. Les styles▲
Un style est un groupe de propriétés définies que l'on peut affecter à n'importe quel objet de type FrameworkElement. Ainsi, un objet de type Shape, Panel ou Control a la capacité de posséder un style. Toutes ces classes possèdent la propriété Style héritée de FrameworkElement, cette propriété recevra une instance de la classe Style. Voici l'écriture XAML d'un style ciblant la classe Rectangle :
<Style
x
:
Key
=
"RectangleStyle"
TargetType
=
"Rectangle"
>
<Setter
Property
=
"Width"
Value
=
"150"
/>
<Setter
Property
=
"Height"
Value
=
"100"
/>
<Setter
Property
=
"Fill"
Value
=
"Red"
/>
</Style>
Comme vous le remarquez, un style nécessite plusieurs attributs pour fonctionner. Le premier est le nom de la clé de ressource, le second définit le type de l'objet qui pourra recevoir le style. La balise Style doit elle-même contenir des balises enfants de type Setter qui permettront de cibler une propriété et d'y associer une valeur. Voici l'affectation en XAML du style défini plus haut :
<Grid
x
:
Name
=
"LayoutRoot"
Background
=
"White"
>
<Rectangle
Style
=
"{StaticResource RectangleStyle}"
/>
</Grid
Lorsque vous appliquez un style à un objet, les valeurs par défaut des propriétés de ce dernier sont automatiquement écrasées par celles contenues dans le style. Les attributs d'objet définis en brut dans la déclaration XAML ou C# de l'objet seront toutefois prioritaires à celles définies par le style. Vous pourriez par exemple décider qu'un style propre aux composants Rectangle affecte leur largeur (Width) de 200 pixels. Toutefois à la compilation, si un Rectangle affecté du style possède déjà sa propriété Width définie à 300, alors celle-ci outrepassera celle du style :
<Grid
x
:
Name
=
"LayoutRoot"
Background
=
"White"
>
<!-- la propriété Width définie dans le rectangle écrase celle récupérée par le style -->
<Rectangle
Style
=
"{StaticResource RectangleStyle}"
Width
=
"300"
… />
<!-- la propriété Width définie dans le style définit la largeur par défaut du Rectangle -->
<Rectangle
Style
=
"{StaticResource RectangleStyle}"
/>
</Grid>
Créer un style en C# est une tâche simple, mais apporte en général moins d'intérêt. Créer un style dans Expression Blend et le stocker au sein d'un dictionnaire de ressources a plus de sens, car ce dernier reste accessible au designer qui peut le modifier directement de manière sensible via l'interface de Blend. Voici toutefois un exemple de style généré et appliqué en C# :
//on crée un nouveau style
Style s =
new
Style
(
typeof
(
Ellipse) );
//on crée une instance de Setter
//Le premier paramètre indique la propriété ciblée de l'objet
//Le second paramètre définit la valeur de la propriété ciblée
Setter setter =
new
Setter
(
Ellipse.
FillProperty,
new
SolidColorBrush
(
Colors.
Blue));
//on ajoute un élément de type Setter à la collection Setters
s.
Setters.
Add
(
setter);
//il n'est pas obligatoire d'ajouter le style au dictionnaire
//de ressources via la ligne ci-dessous
//Resources.Add("EllipseStyle", s);
//on affecte le style généré à la propriété Style
monEllipse.
Style =
s;
//monEllipse.Style = Resources["EllipseStyle"] as Style;
Depuis Silverlight 3, il est possible de baser des styles à partir d'autres déjà existant. À cette fin vous pouvez utiliser la propriété BasedOn. Elle vous permet de cibler le style hérité. Voici un
exemple simple illustrant cette méthode :
<UserControl.Resources>
<Style
x
:
Key
=
"RectangleStyle"
TargetType
=
"Rectangle"
>
<Setter
Property
=
"Width"
Value
=
"150"
/>
<Setter
Property
=
"Height"
Value
=
"100"
/>
<Setter
Property
=
"Fill"
Value
=
"Red"
/>
</Style>
<Style
x
:
Key
=
"RoundSquareStyle"
BasedOn
=
"{StaticResource RectangleStyle}"
TargetType
=
"Rectangle"
>
<Setter
Property
=
"Width"
Value
=
"100"
/>
<Setter
Property
=
"RadiusX"
Value
=
"10"
/>
<Setter
Property
=
"RadiusY"
Value
=
"10"
/>
</Style>
</UserControl.Resources>
Cela est réalisable uniquement côté XAML. Ce type d'écriture permet d'éviter de recopier un code fastidieux ou de dupliquer des styles entiers lorsque vous souhaitez les décliner. Il est possible d'ajouter ou d'écraser des balises Setter.
10-4-2. Affectation dynamique▲
Dans l'idéal, le style est créé dans l'interface de Blend, mais il peut être affecté dynamiquement, via C# ou VB, à plusieurs instances de FrameworkElement à la fois. De cette manière, vous profitez du meilleur des deux mondes, le graphisme et le ressenti utilisateur apportés par les créatifs, couplés à la fonctionnalité et l'automatisation des tâches fournies par le développeur. Nous allons le démontrer en créant un style via l'interface de Blend du point de vue d'un designer interactif, puis nous affecterons dynamiquement ce style à des objets de la liste d'affichage. Dans un projet de votre choix, créez un StackPanel centré horizontalement et aligné vers le haut. Instanciez dans ce conteneur cinq exemplaires de Button et affectez leur propriété Content des valeurs respectives : Accueil, Innovation, Technologie, Services, Portfolio. Sélectionnez le premier et dans le menu Object, sélectionnez Edit Style > Create Empty… Dans la fenêtre qui apparaît, nommez le style et stockez-le dans un dictionnaire de ressources externe. L'interface de Blend vous place en mode d'édition de style. Passez en mode de création mixte afin de voir le code XAML généré par vos actions sur le panneau Propriétés. L'idée est de personnaliser entièrement les propriétés du bouton, voici un exemple de style généré :
<Style
x
:
Key
=
"ButtonStyle1"
TargetType
=
"Button"
>
<Setter
Property
=
"Background"
>
<Setter.Value>
<SolidColorBrush
Color
=
"#FF550024"
/>
</Setter.Value>
</Setter>
<Setter
Property
=
"BorderThickness"
Value
=
"0,0,2,2"
/>
<Setter
Property
=
"BorderBrush"
Value
=
"#FF743A3A"
/>
<Setter
Property
=
"Foreground"
Value
=
"#FF8E4C4C"
/>
<Setter
Property
=
"FontFamily"
Value
=
"Courier New"
/>
<Setter
Property
=
"FontSize"
Value
=
"18"
/>
<Setter
Property
=
"Cursor"
Value
=
"Hand"
/>
</Style>
Du côté développeur, il suffit de lister tous les objets contenus dans le StackPanel, puis d'affecter le style dynamiquement à chacun d'eux, mis à part au premier qui le possède déjà. Au sein de Blend, sélectionnez la grille LayoutRoot, puis dans le panneau des événements, dans le champ MouseLeftButtonUp, entrez ApplyStyle comme nom de méthode. Selon les paramètres de l'onglet Projects du menu Options… cette action ouvre le fichier MainPage.xaml.cs dans Blend ou directement dans Visual Studio. Voici la méthode qui permet d'affecter le style créé par le designer dans Blend, à chaque bouton du StackPanel de manière dynamique :
private
void
ApplyStyle
(
object
sender,
MouseButtonEventArgs e)
{
foreach
(
UIElement uie in
MenuHaut.
Children)
{
Button b =
uie as
Button;
if
(
b !=
null
&&
b.
Content !=
"Accueil"
)
{
b.
Style =
App.
Current.
Resources[
"ButtonStyle1"
]
as
Style;
}
}
}
Testez le projet. Lorsque vous relâchez le bouton gauche de la souris n'importe où sur la grille LayoutRoot, le style est dynamiquement affecté à chaque instance de bouton présent dans le StackPanel. Concrètement, chacun des acteurs de la production joue son rôle sans influer ou interférer dans le travail des autres. Le projet StyleAndTemplate_Base.zip est disponible dans le dossier chap11 des exemples du livre.
10-4-3. Organiser et nommer les ressources▲
Le code XAML ci-dessus est montré à titre indicatif, le designer n'a pas forcément besoin de savoir ce qui a été généré par la personnalisation des propriétés du bouton. Toutefois, comme nous l'avons précisé, pour que le développeur et le designer interactif puissent collaborer, il est au moins nécessaire de s'accorder sur une nomenclature des clés de ressource. Revenons un peu en arrière afin de mieux comprendre comment nommer et organiser nos dictionnaires de ressources, nous pourrons alors envisager une nomenclature des ressources. La Figure 11.4 décrit les capacités d'organisation des dictionnaires de ressources d'un point de vue technique. Ce point de vue est important mais n'aborde pas les bonnes pratiques de découpage des ressources. Plusieurs facteurs importants se révèlent décisifs lorsque vous souhaitez organiser vos ressources de manière optimisée.
- Vous pouvez tout d'abord gérer vos dictionnaires de ressources en les nommant selon les thèmes visuels qu'ils traitent. Par exemple, SketchStyles.xaml contenu dans les projets SketchFlow, est un dictionnaire de ressources dédié au style visuel croquis. Ainsi, nous pourrions créer un autre dictionnaire de ressources nommé GlossyStyles.xaml ou Seventies-Styles.xaml.
- Il est également possible de nommer les dictionnaires et de centraliser les ressources en fonction des fonctionnalités qu'ils couvrent. Par exemple, si votre application possède un lecteur multimédia, vous pouvez fédérer les ressources dans un dictionnaire dédié au lecteur.
- Si vous travaillez pour des clients différents et que vous souhaitez industrialiser le développement, il est peut-être utile d'avoir des dictionnaires dédiés à chacun d'eux (Banque1-Brush.xaml et Banque2Brush.xaml). Ce modèle d'organisation est assez intéressant, car il vous permet de simplifier en factorisant l'affectation de dictionnaires.
- Les contraintes techniques propres aux ressources influent grandement sur les possibilités d'organisation. Par exemple, un Storyboard est une ressource, mais il cible une instance de UIElement au sein d'un arbre visuel. Il ne sera donc pas possible de l'externaliser dans un dictionnaire, puisque la référence de l'instance animée n'existe pas dans le dictionnaire. Les pinceaux d'images (ImageBrush) suivent un peu la même logique dans le sens où les partager entre différents projets passera forcément par l'importation des images qu'ils ciblent. On peut donc se demander s'il est légitime de centraliser les pinceaux d'images (et de vidéos) dans des dictionnaires externes. La réponse est oui dans la mesure où organiser les ressources dans des dictionnaires facilite la maintenance, la lecture du XAML ainsi que l'évolutivité des applications. Toutefois, comme leur logique de partage est contraignante, il faudra éviter de les insérer dans des dictionnaires de ressources contenant des pinceaux dont le visuel est uniquement dépendant du code déclaratif XAML (et non d'une quelconque ressource de type média).
- Dans le même ordre d'idée, le type des ressources que vous organisez est à prendre en compte. Vous pouvez décider de scinder les différents types de ressources dans autant de dictionnaires externes. Les pinceaux dans un dictionnaire, les ressources de couleurs dans un autre, les styles et les modèles encore dans un autre. L'avantage de procéder ainsi est de conserver une organisation propre et facile à maintenir. Changer les couleurs propres à la charte graphique reviendra à modifier les ressources de couleurs contenues dans le dictionnaire adéquat. Comme le code XAML est relativement simple pour ce type de ressource, il est facile pour un graphiste ou un designer de changer les valeurs des couleurs sans altérer l'architecture ou le travail de conception déjà réalisé. Si vous mélangez les styles, les pinceaux et les couleurs, cela complexifiera leur accès et diminuera leur visibilité au sein du projet.
La Figure 11.29 met en avant une organisation structurée tenant compte de certaines de ces pistes de réflexion.
Les concepts décrits ci-dessus influencent non seulement l'organisation des ressources, mais nous apportent également des indices concernant la conception d'une nomenclature. Par exemple, si vous deviez créer un style ayant pour thème visuel un effet vitré conçu pour les boutons de soumission, vous pourriez le nommer SubmitGlassyButtonStyle. De cette manière, vous saurez en lisant ce nom que cette ressource est un style avec un rendu "verre" pour les boutons de soumission de formulaire. Le développeur voit son travail facilité et n'a pas forcément besoin de s'entretenir avec le designer s'il doit l'appliquer dynamiquement.
10-4-4. Les modèles▲
Les modèles de composants sont souvent apparentés à la notion de style. Ils sont représentés par la classe ControlTemplate dont les instances peuvent être affectées à la propriété Template de tout objet héritant de Control. Ils permettent de remplacer l'arbre visuel et logique défini au sein des objets héritant de la classe Control. Par exemple, l'arbre visuel et logique d'une barre de défilement (ScrollBar) est entièrement modifiable si vous affectez à sa propriété Template une instance de ControlTemplate. À première vue, il peut vous sembler que les modèles sont une version plus puissante des styles que nous venons d'évoquer, puisque la forme même des composants peut être réinventée. En réalité, les instances de Style et de ControlTemplate n'agissent pas aux mêmes niveaux. Leurs objectifs sont complémentaires, un modèle est toujours affecté à la propriété Template d'un Control. Par ailleurs, comme les styles sont des groupes de propriétés définies, un style peut contenir une balise Setter dont le rôle sera d'appliquer une instance de ControlTemplate à la propriété Template d'un Control. Si créer un modèle est relativement simple du côté XAML, il est peu pertinent de procéder de cette manière. Que vous soyez développeur ou non, et même si cela est réalisable avec du temps et du café, concevoir un modèle en codant directement du XAML est contre-productif lorsque vous pouvez l'éviter. Cela s'avère fastidieux et peu rentable, notamment si vous souhaitez coder à la main les tracés vectoriels (même si vous utilisez une extension comme ReSharper). D'autre part, le modèle influence fortement le visuel final d'un composant (encore plus que les propriétés définies au sein d'un style) et contient souvent des transitions animées internes au contrôle.
Ainsi, coder les modèles en XAML est souvent un non-sens, il est très difficile de générer du visuel de manière sensible tant le code XAML est abstrait. Les créatifs procèdent souvent par tâtonnement pour arriver au visuel final d'une interface, pour cela il faut nécessairement utiliser une interface telle qu'en proposent Illustrator, Expression Design et Expression Blend. Il va sans dire que créer un modèle du début à la fin en C# est encore plus aberrant lorsqu'on souhaite un design abouti.
Voici l'exemple d'un style et d'un modèle de bouton, tous deux créés à la main en XAML et affectés à une instance de bouton :
<Style
x
:
Key
=
"ButtonStyle2"
TargetType
=
"Button"
>
<Setter
Property
=
"Background"
>
<Setter.Value>
<SolidColorBrush
Color
=
"#FF550024"
/>
</Setter.Value>
</Setter>
<Setter
Property
=
"BorderThickness"
Value
=
"0,0,2,2"
/>
<Setter
Property
=
"BorderBrush"
Value
=
"#FF743A3A"
/>
<Setter
Property
=
"Foreground"
Value
=
"#FFFFFFFF"
/>
<Setter
Property
=
"FontFamily"
Value
=
"Courier New"
/>
<Setter
Property
=
"FontSize"
Value
=
"18"
/>
<Setter
Property
=
"Cursor"
Value
=
"Hand"
/>
</Style>
<ControlTemplate
x
:
Key
=
"RoundButton"
TargetType
=
"Button"
>
<Grid >
<Ellipse
Fill
=
"BlueViolet"
Width
=
"100"
Height
=
"Auto"
/>
<ContentPresenter
VerticalAlignment
=
"Center"
HorizontalAlignment
=
"Center"
/>
</Grid>
</ControlTemplate>
…
<Button
Content
=
"Accueil"
Margin
=
"0,0,10,0"
$
Style
=
"{StaticResource ButtonStyle2}"
Template
=
"{StaticResource RoundButton}"
/>
L'exemple ci-dessus est assez brut. Comme vous le constatez, les balises Template et Style n'entretiennent aucun lien particulier. Cette structure ne correspond pas au code généré sous Blend. Ce dernier possède une manière bien à lui d'articuler les styles et les modèles. Dans la grande majorité des cas, lorsque vous créez un modèle de composant dans Blend, celui-ci l'encapsule automatiquement au sein d'un style. C'est pour cette raison que, dans Expression Blend, la fenêtre de création de modèles a pour titre "Create Style Resource". Même si ce comportement peut déconcerter au premier abord, il est efficace et permet d'appliquer le modèle indirectement à travers l'affectation du style. Voici le même exemple revu et corrigé afin de mettre en valeur ce concept :
<Style
x
:
Key
=
"ButtonStyle2"
TargetType
=
"Button"
>
<Setter
Property
=
"Background"
>
<Setter.Value>
<SolidColorBrush
Color
=
"#FF550024"
/>
</Setter.Value>
</Setter>
<Setter
Property
=
"BorderThickness"
Value
=
"0,0,2,2"
/>
<Setter
Property
=
"BorderBrush"
Value
=
"#FF743A3A"
/>
<Setter
Property
=
"Foreground"
Value
=
"#FFFFFFFF"
/>
<Setter
Property
=
"FontFamily"
Value
=
"Courier New"
/>
<Setter
Property
=
"FontSize"
Value
=
"18"
/>
<Setter
Property
=
"Cursor"
Value
=
"Hand"
/>
<Setter
Property
=
"Template"
>
<Setter.Value>
<ControlTemplate
TargetType
=
"Button"
>
<Grid>
<Rectangle
Fill
=
"BlueViolet"
RadiusX
=
"8"
RadiusY
=
"8"
/>
<ContentPresenter
VerticalAlignment
=
"Center"
HorizontalAlignment
=
"Center"
/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Le XAML généré peut paraître verbeux, mais il est au final assez simple et rapide à assimiler. Il est possible d'améliorer ce concept en référençant une ressource modèle dans plusieurs styles différents :
<ControlTemplate
x
:
Key
=
"RoundButton"
TargetType
=
"Button"
>
<Grid >
<Ellipse
Fill
=
"BlueViolet"
Width
=
"100"
Height
=
"Auto"
/>
<ContentPresenter
VerticalAlignment
=
"Center"
HorizontalAlignment
=
"Center"
/>
</Grid>
</ControlTemplate>
<Style
x
:
Key
=
"ButtonStyle1"
TargetType
=
"Button"
>
<Setter
Property
=
"Background"
>
<Setter.Value>
<SolidColorBrush
Color
=
"#FF550024"
/>
</Setter.Value>
</Setter>
<Setter
Property
=
"BorderThickness"
Value
=
"0,0,2,2"
/>
<Setter
Property
=
"Foreground"
Value
=
"#FFFFFFFF"
/>
<Setter
Property
=
"FontFamily"
Value
=
"Courier New"
/>
<Setter
Property
=
"FontSize"
Value
=
"18"
/>
<Setter
Property
=
"Template"
Value
=
"{StaticResource RoundButton}"
/>
</Style>
<Style
x
:
Key
=
"ButtonStyle2"
TargetType
=
"Button"
>
<Setter
Property
=
"Background"
>
<Setter.Value>
<SolidColorBrush
Color
=
"#FF240055"
/>
</Setter.Value>
</Setter>
<Setter
Property
=
"BorderThickness"
Value
=
"0,0,4,4"
/>
<Setter
Property
=
"Foreground"
Value
=
"#FFFFFFFF"
/>
<Setter
Property
=
"FontFamily"
Value
=
"Tahoma"
/>
<Setter
Property
=
"FontSize"
Value
=
"18"
/>
<Setter
Property
=
"Template"
Value
=
"{StaticResource RoundButton}"
/>
</Style>
De manière générale, il faut privilégier les styles aux modèles. L'application n'en sera que plus facile à maintenir et évolutive. Tout ce que vous définissez au sein d'un modèle est propre à l'ensemble des instances qui possèdent le modèle. La seule exception à cette règle est lorsque vous recourez à la liaison de modèles (TemplateBinding). Au contraire, toutes les propriétés définies au sein d'un style restent par la suite modifiables et personnalisables.
Le projet StyleAndTemplate_ModeleSimple.zip est disponible dans le dossier chap11 des exemples du livre.
Nous allons examiner deux cas concrets reposants sur le design d'un Slider et d'une ListBox. Ces derniers concentrent, à eux seuls, une grande majorité des problématiques communes à la personnalisation de tous types de contrôles.
10-5. Le modèle Slider▲
La personnalisation d'un Slider est très différente de celle d'un bouton (voir Chapitre 6 Boutons personnalisés). Un bouton ne possède par défaut aucun objet logique au sein de son arbre visuel. Autrement dit, rien dans le modèle d'un bouton n'est crucial en terme de fonctionnalités. Le seul objet logique, contenu par défaut dans tous les boutons, est le ContentPresenter. Il fournit au bouton la capacité d'afficher du texte ou tout autre objet comme unique enfant. Il est toutefois facultatif, car le bouton diffuse l'événement Click et réagit aux interactions utilisateur (MouseEnter, MouseLeave, etc.) sans lever d'erreurs lorsque le ContentPresenter est absent. Ce n'est pas le cas du Slider : ce dernier contient plusieurs éléments logiques nécessaires à son fonctionnement. Dans cette section, vous apprendrez les méthodologies et les bonnes pratiques de la personnalisation de composant.
10-5-1. Principes et architecture▲
Lorsque vous créez un modèle, deux manières de procéder s'offrent à vous. Soit vous le concevez à partir d'un objet d'affichage sélectionné dans l'arbre visuel de l'application, soit vous modifiez le modèle d'un composant fourni par défaut au sein de la bibliothèque Silverlight. Au Chapitre 6 Boutons personnalisés, nous avons utilisé la première méthode. C'était la meilleure option, car la classe ButtonBase et celles qui en découlent, comme Button, ToggleButton ou RadioButton, ne contiennent aucun objet logique. A contrario, comme l'architecture du composant Slider est plus complexe, il est préférable dans un premier temps de partir du modèle fourni en standard. Pour cela, utilisez le projet StyleAndTemplate_ModeleSimple.sln ou créez un nouveau projet de test. Placez un exemplaire de Slider n'importe où dans la grille principale. Cliquez droit dessus, sélectionnez Edit Template > Edit a copy… Dans la fenêtre de création de styles, vous pouvez cliquer sur OK. Dans un premier temps, notre objectif est de comprendre l'arbre visuel d'un Slider, il n'est donc pas nécessaire d'organiser le modèle dans un dictionnaire de ressources. Le modèle du Slider est affiché juste après cette étape (voir Figure 11.30).
Vous remarquez qu'un Slider contient deux grilles correspondant respectivement à un mode d'affichage horizontal et vertical. Pour identifier facilement les éléments logiques d'un contrôle, Blend affiche, à droite de ceux-ci, une icône représentant un morceau de puzzle surmonté d'une coche verte. La liste des parties nécessaires au fonctionnement d'un contrôle est gérée par le panneau Parts situé en haut à gauche de l'interface. Lorsqu'un de ces éléments logiques n'est pas présent dans l'arbre visuel, ce panneau affiche le morceau de puzzle sans coche verte. De cette manière, il est très facile de savoir si toutes les parties logiques du composant sont correctement définies dans l'arbre visuel. De fait, l'identification automatique des éléments logiques repose sur le nommage correct de ces derniers. Pour vous en convaincre, il suffit de supprimer le nom de la grille HorizontalTemplate, le panneau Parts vous indique dès lors que celui-ci manque en affichant l'icône puzzle sans coche verte. Vous n'avez pas besoin de connaître par cœur le nom des parties logiques pour chaque composant. Si vous souhaitez redéfinir la grille en tant que partie logique du Slider, il vous suffit d'un clic droit sur celle-ci, de sélectionner l'option Make Into Part of Slider, puis de cliquer sur HorizontalTemplate (voir Figure 11.31).
Dès lors, la grille est à nouveau nommée HorizontalTemplate. Nous allons maintenant essayer de comprendre comment le modèle Slider horizontal est structuré. Tout d'abord, lorsque la grille est sélectionnée, on remarque trois colonnes dont les deux premières sont en mode de redimensionnement automatique :
<Grid
x
:
Name
=
"HorizontalTemplate"
Background
=
"{TemplateBinding Background}"
>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width
=
"Auto"
/>
<ColumnDefinition
Width
=
"Auto"
/>
<ColumnDefinition
Width
=
"*"
/>
</Grid.ColumnDefinitions>
Les deux colonnes s'adaptent aux dimensions de leur contenu. La troisième colonne est en mode relatif et occupera par conséquent le reste de l'espace disponible. Dans les première et dernière colonnes se trouvent deux boutons de répétition (de type RepeatButton) transparents mais cliquables. Ils représentent des zones interactives qui permettent de déplacer le curseur de manière indirecte. Lorsque l'utilisateur clique sur l'un d'eux, la largeur du premier RepeatButton, HorizontalTrack-Large-Change-RepeatButton, augmente ou diminue. Cela a pour conséquence de déplacer le curseur dont la largeur est fixée en dur et de réduire ou d'augmenter la place restante allouée à la troisième colonne. Le curseur, dans la colonne du milieu est de type Thumb. Ce composant diffuse trois événements correspondant à ceux d'une interaction de type glisser-déposer : Drag-Started, DragDelta et DragCompleted. L'objet événementiel, de type DragDelta-Event-Args, récupéré par l'écouteur de l'événement de DragDelta, contient deux propriétés Horizontal-Change et Vertical-Change.
Lorsque vous personnalisez un contrôle, vous n'avez pas accès au code logique, nous pouvons toutefois prédire que ces valeurs servent à modifier la largeur du premier RepeatButton dans les limites des dimensions du Slider. La modification de la largeur décale automatiquement les deux autres colonnes. Nous pouvons maintenant créer un premier Slider en partant d'une base simple sans trop de difficultés. Vous devrez souvent choisir entre créer des styles personnalisés à partir de ceux fournis, ou créer entièrement l'arbre visuel de ces composants. Cette dernière méthode, bien qu'un peu plus éprouvante au début, est une excellente manière d'apprendre l'agencement ainsi que l'imbrication de contrôles.
10-5-2. Un Slider personnalisé▲
Nous allons utiliser la seconde méthode évoquée plus haut en nous basant sur une arborescence simple située dans MainPage.xaml. Nous produirons donc un modèle de Slider à partir de celle-ci. Il est nécessaire de décompresser la solution LecteurMultiMedia_BaseModele.zip du dossier chap11. Vous trouverez dans l'arbre visuel plusieurs boutons (Button, ToggleButton, Hyperlink-Button, RadioButton) dont les styles et modèles sont contenus dans le dictionnaire de ressources nommé BobifysStyles.xaml. Ouvrez le panneau Resources afin de prendre connaissance des différents styles et modèles présents qu'il propose (voir Figure 11.32).
Ce dictionnaire contient une liaison ciblant un dictionnaire dédié aux pinceaux ; il sera sans doute nécessaire d'en ajouter une autre pointant vers le dictionnaire contenant les ressources de couleurs. Tout ce qui est fait dans ce projet est une synthèse des précédents chapitres, nous n'y reviendrons donc pas. Le Slider aura pour but de filtrer les vidéos et les sons en fonction d'une durée maximum, le composant ListBox affichera la liste des médias en remplacement de la grille fictive PseudoListe (cette dernière n'est qu'une maquette).
Sélectionnez la grille nommée SliderTimeCode. Elle nous servira de base pour créer un Slider personnalisé. Définissez-lui trois colonnes, dont les deux premières doivent être en mode de redimensionnement automatique et la dernière en mode relatif. Positionnez ensuite deux instances de RepeatButton dans la première et la dernière colonne avec une opacité fixée à 0. Le premier RepeatButton doit posséder une largeur exprimée en pixels égale à 0, le second doit être en mode de redimensionnement automatique pour s'adapter dynamiquement à la troisième colonne. Faites en sorte que le futur composant Thumb, qui correspond à la grille contenant le curseur et l'étiquette, soit dans la colonne du milieu. Transformez cette grille en contrôle de type Thumb via le menu Tools, puis Make Control… Nommez le style généré BobifyTimeThumbStyle. Une fois dans le mode d'édition du modèle, définissez une largeur de 12,5 pixels pour la grille principale de ce composant. Sélectionnez ensuite les deux premiers tracés ainsi que le TextBlock et appliquez-leur une marge négative à droite de -50 pixels. Cette opération est importante, car elle réduit l'espace utilisé par le Thumb dans notre futur Slider, tout en conservant l'affichage de l'étiquette. Si nous ne procédions pas ainsi, le curseur ne pourrait pas se déplacer sur la totalité du Slider, car il serait bloqué par la largeur de l'étiquette (voir Figure 11.33).
Une fois cette opération réalisée, revenez au niveau de l'application. Vérifiez bien que les colonnes en largeur automatique ne possèdent pas une valeur minimale en pixels. À cette fin, il suffit de cliquer à côté de l'icône de redimensionnement Auto, pour les sélectionner et afficher leurs propriétés. Oublier cette étape contraindrait les déplacements du curseur. Sélectionnez la grille contenant les éléments du Slider, faites-en un nouveau style via le menu Make Control… Nommez-le Bobify-TimeFilterSliderStyle. Une fois dans le modèle, vérifiez que la largeur de la grille est en mode Auto. Cliquez-droit dessus et définissez-la en tant que partie logique Horizontal-Template, celle-ci est dès lors renommée. Le premier enfant de la grille correspond à un tracé et ne fait pas partie des enfants logiques obligatoires. Il est toutefois nécessaire de vérifier que sa largeur est en mode d'étirement automatique (afin qu'elle s'adapte à la grille dynamiquement). Ce tracé doit être étalé sur les trois colonnes, veillez à ce que sa propriété ColumnSpan soit égale à 3. Sélectionnez ensuite chacun des composants restants dans l'arbre visuel et donnez-leur un rôle logique adéquat (voir Figure 11.34).
Si tout s'est correctement déroulé, le Slider fonctionne parfaitement. Revenez au niveau de l'application, définissez une valeur minimale de 0 et une maximale de 600. Cela représentera le temps minimal et maximal exprimé en secondes. Lorsque l'utilisateur déplacera le curseur, il filtrera les vidéos ou les sons dont le temps sera supérieur à la valeur choisie. L'étiquette a pour but d'afficher cette durée mise à jour lors de déplacements du curseur. Vous pouvez encore améliorer le rendu en ajoutant une barre de couleur (Rectangle) superposée au tracé dans la première colonne (voir Figure 11.35).
Il est également possible d'animer les différents éléments durant le survol du Slider ou du Thumb. À cette fin, le mieux est de créer des liaisons de modèles entre les propriétés de remplissage des tracés présents dans l'arbre visuel du Thumb et les propriétés Background et BorderBrush de celui-ci (voir Figure 11.36).
De cette manière, il est possible d'animer les couleurs du Thumb via l'état MouseOver du Slider. Le Slider est maintenant presque terminé, il nous reste à trouver un moyen efficace de lier la valeur affichée par le TextBlock contenu dans le Thumb, et la valeur du Slider.
Le projet est dans l'archive LecteurMultiMedia_SliderModele.zip du dossier chap11.
10-6. Les liaisons▲
Au sein de Silverlight, deux familles de liaisons cohabitent : les liaisons de modèles et les liaisons de données. La première correspond à la notation XAML TemplateBinding. Elle ne fait pas référence à une classe proprement dite, mais à une expression spécifique, uniquement disponible au sein d'un modèle de composant. Elle n'est utile que dans le cadre de la personnalisation de composants et a été conçue en grande partie à l'intention des designers. La seconde, un sur-ensemble plus vaste, dotée des mêmes capacités que la première, offre toutefois beaucoup plus de capacités. Celle-ci est exprimée par la classe concrète Binding. Vous pouvez considérer que la notation TemplateBinding est une utilisation spécifique de la classe Binding facilitant et simplifiant l'écriture XAML.
10-6-1. Créer une liaison▲
Reprenez le projet lecteur multimédia là où vous l'aviez laissé, puis accédez au modèle Thumb contenu dans le Slider. Nous allons créer une suite de liaisons de modèles partant de la propriété Text du TextBlock contenu dans le Thumb jusqu'à la propriété Value du Slider. Lorsque cette dernière sera modifiée, le composant TextBlock affichera la durée au format mm:ss.ms. Le problème consiste à savoir sur quelle propriété du Thumb nous pouvons créer la liaison. La propriété Tag est idéale dans ce cas de figure, puisqu'elle est typée Object, elle est capable de recevoir n'importe quel type de valeur. Il est ensuite possible de lier la propriété Tag du Thumb à la propriété Value du Slider (voir Figure 11.37).
Dans la Figure 11.37, il faut comprendre que la propriété Text du TextBlock contenu dans le Thumb est liée (TemplateBinding) à l'attribut Tag de celui-ci ; puisque la propriété Tag du Thumb est liée à l'attribut Value du Slider. Autrement dit, lorsque la propriété Value du Slider sera mise à jour par l'utilisateur, elle affectera la propriété Tag du Thumb qui impactera à son tour la valeur Text du TextBlock (contenu dans le modèle du Thumb). Créez les trois liaisons de modèle décrites ci-dessus.
Vous constatez que le texte est affiché dans une autre couleur correspondant à la propriété Foreground du Thumb. Ce comportement est assez logique, le contrôle Thumb ne contient par défaut aucun TextBlock ou ContentPresenter dans son modèle, il possède cependant la propriété Foreground héritée de la classe Control. Celle-ci correspond à une couleur de texte et ne cible, initialement, aucun contrôle. Toutefois, dès qu'une liaison de modèles est mise en place pour la propriété Text, le champ texte est détecté à l'intérieur du modèle et sa propriété Foreground est liée à l'attribut Foreground du Thumb. Vous n'avez pas besoin de définir cette liaison.
Compilez et déplacez le curseur du contrôle Thumb, il affiche une valeur de type Double appelant en interne sa méthode ToString afin que le champ TextBlock puisse l'afficher. Cela met en valeur la nécessité de convertir les secondes au format mm:ss.ms. À cette fin, nous devons créer une liaison de données ainsi qu'une classe de conversion. Cette classe doit implémenter l'interface IValueConverter et définir les méthodes qui y sont décrites. Ouvrez le projet dans Visual Studio - cet environnement est bien plus adapté au développement C# qu'Expression Blend. Pour réaliser cette opération, nous avons deux choix.
- Le premier consiste à utiliser notre classe de conversion à l'intérieur de la définition du modèle donc dans notre dictionnaire de ressources. C'est le choix le plus logique et correct en terme d'architecture. Cela devrait fonctionner sans problème. Toutefois, il arrive parfois que l'interface de Blend lève une erreur pour des raisons d'accès et d'initialisation. Ce type d'erreurs est appelé Design Time Errors. Celles-ci sont souvent dues à l'éclatement des ressources logiques et graphiques dans des dictionnaires de ressources ainsi qu'à une interprétation limitée du code logique par Blend. Nous aborderons ce principe à la fin de cette section.
- Le second choix possède l'avantage d'afficher correctement la donnée convertie dans Expression Blend sans que ce dernier ne lève d'erreurs. L'objectif est d'utiliser la liaison de modèles standard vers une propriété du contrôle, par exemple la propriété Tag dans le cas de notre Slider. Il est ensuite nécessaire de lier la propriété Tag à une autre du même contrôle, Value dans notre exemple, en définissant une liaison de données (Binding). De cette manière, nous définissons une conversion au niveau de l'application, ce qui limite grandement les risques d'erreur d'interprétation du code logique sous Blend (voir Figure 11.38).
Nous allons aborder cette solution en premier. Comme vous le constatez à la Figure 11.37, il est possible de lier des propriétés du même objet ou d'objets différents situés dans le même arbre visuel. Générez une liaison de modèles pointant vers la propriété Tag du Slider en lieu et place de la propriété Value initialement ciblée. Cliquez ensuite sur l'icône carrée située à droite de la propriété Tag du Slider et sélectionnez le menu Data Binding… pour définir une liaison de données. Une boîte de dialogue apparaît, elle vous permet de créer une liaison d'UIElement à UIElement tant que les propriétés liées acceptent des types équivalents. Cliquez sur le second onglet nommé Element Property. La partie gauche de la fenêtre liste les objets présents dans l'arbre visuel de l'application. Conservez le Slider sélectionné. La partie droite montre les propriétés du Slider, choisissez Value (voir Figure 11.39).
Lorsque vous compilez, vous obtenez le même comportement qu'auparavant tout en ayant ajouté une liaison. Nous allons maintenant coder notre classe de conversion. Celle-ci doit implémenter l'interface IValueConverter. Dans Visual Studio, créez un répertoire nommé Utils, si celui-ci n'est pas présent. Cliquez droit sur ce répertoire, puis ajoutez une classe nommée Double2TimeCodeConverter.cs. Supprimez tous les espaces de noms générés par défaut, mis à part System, puis ajoutez System.Windows.Data. Générez ensuite le code implémentant les méthodes décrites par l'interface (voir Figure 11.40).
Une fois cette étape passée, vous obtenez le code ci-dessous :
using
System;
using
System.
Windows.
Data;
namespace
LecteurMultiMedia.
Utils
{
public
class
Double2TimeCodeConverter :
IValueConverter
{
#region IValueConverter Members
public
object
Convert
(
object
value
,
Type targetType,
object
parameter,
System.
Globalization.
CultureInfo culture)
{
throw
new
NotImplementedException
(
);
}
public
object
ConvertBack
(
object
value
,
Type targetType,
object
parameter,
System.
Globalization.
CultureInfo culture)
{
throw
new
NotImplementedException
(
);
}
#endregion
}
}
Les deux méthodes ont exactement le même objectif : convertir un type ou une valeur en un autre type ou valeur. La première, nommée Convert, correspond au sens défini par défaut lorsque vous créez une liaison de données. La seconde, ConvertBack, permet de gérer une liaison à double sens qu'il faut préciser en XAML. Si nous définissions un champ de saisie TextBox au lieu d'un champ texte classique de type TextBlock, l'utilisateur serait alors en mesure de saisir une valeur dans le TextBox. La propriété Text de ce dernier peut alors être convertie dynamiquement en Double et affectée à la propriété Value si la méthode est correctement implémentée. Dans cet exemple, il n'est pas très pertinent d'avoir ce type d'interaction, nous n'allons donc pas nous attarder sur la méthode ConvertBack. Modifiez la méthode Convert comme ci-dessous :
public
object
Convert
(
object
value
,
Type targetType,
object
parameter,
System.
Globalization.
CultureInfo culture)
{
TimeSpan ts =
new
TimeSpan
(
);
if
(
value
is
double
) ts =
TimeSpan.
FromSeconds
((
double
)value
);
return
ts.
Minutes.
ToString
(
"00"
)
+
":"
+
ts.
Seconds.
ToString
(
"00"
)
+
"."
+
ts.
Milliseconds.
ToString
(
"000"
);
}
Maintenant que nous avons créé le code adéquat, il faut référencer la classe de conversion en tant que ressource logique. Le mieux serait de la stocker dans un dictionnaire de ressources dédié. Créez-en un via Blend dans le répertoire RD. Le code ci-dessous décrit la manière dont vous pouvez déclarer des ressources logiques et référencer l'espace de noms Utils :
<ResourceDictionary
xmlns
=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns
:
x
=
"http://schemas.microsoft.com/winfx/2006/xaml"
xmlns
:
Utils
=
"clr-namespace:LecteurMultiMedia.Utils"
>
<
Utils
:
Double2TimeCodeConverter
x
:
Key
=
"D2TCConverter"
/>
</ResourceDictionary>
Comme ce dictionnaire est référencé par App.xaml, la classe de conversion est utilisable n'importe où dans l'application. Vous devrez toutefois prêter attention au fait qu'une seule instance de celle-ci sera utilisée systématiquement. Il faudra donc faire attention à ne pas employer de champs ou de propriétés de manière inconséquente. Dans l'exemple ci-dessus, la variable de type TimeSpan est locale à la méthode, de ce fait, celle-ci possède une référence unique à chaque appel de la méthode Convert. Il était important de ne pas utiliser de propriété ou de champ privé, car nous pourrions avoir plusieurs instances de Slider différents utilisant la même opération de conversion.
Nous allons maintenant modifier la liaison de données afin d'utiliser le convertisseur. Cliquez à nouveau sur l'icône carrée située à droite de la propriété Tag du Slider, puis choisissez le menu Data Binding… Dépliez la zone des options située en bas de la fenêtre affichée, choisissez D2TCConverter comme classe de conversion (voir Figure 11.41).
Le code XAML correspondant à la liaison de données a évolué :
<Slider …
Tag
=
"{Binding Value,
Converter={StaticResource D2TCConverter},
ElementName=slider, Mode=OneWay
}"
… />
Vous constatez que, mis à part le membre indiquant d'éventuels paramètres de conversion, le XAML reflète fidèlement les options de liaison décrites dans la fenêtre. Nous pourrions encore améliorer la conversion en passant un paramètre sous forme de chaîne de caractères par exemple :
<Slider …
Tag
=
"{Binding Value,
Converter={StaticResource D2TCConverter},
ElementName=slider,
ConverterParameter=00:00.000,
Mode=OneWay
}"
… />
Du côté C#, il nous faudrait modifier la classe de cette manière :
public
object
Convert
(
object
value
,
Type targetType,
object
parameter,
System.
Globalization.
CultureInfo culture)
{
TimeSpan ts =
new
TimeSpan
(
);
if
(
value
is
double
)
ts =
TimeSpan.
FromMilliseconds
((
double
)value
*
1000
);
string
p =
parameter as
string
;
if
(
p ==
"00:00.000"
)
{
return
ts.
Minutes.
ToString
(
"00"
)
+
":"
+
ts.
Seconds.
ToString
(
"00"
)
+
"."
+
ts.
Milliseconds.
ToString
(
"000"
);
}
return
ts.
Minutes.
ToString
(
"00"
)
+
":"
+
ts.
Seconds.
ToString
(
"00"
);
}
Si aucun paramètre n'est précisé, alors on affiche les minutes et secondes, dans le cas contraire et si la chaîne de caractères fournie correspond à "00:00.000", alors on montre également les millisecondes.
Revenons maintenant sur la première solution consistant à utiliser la classe de conversion dans le modèle du Slider (voir Figure 11.42).
Comme nous l'avons dit au début de cette section, la liaison de modèles n'est qu'une liaison de données spécifique. Il est possible de créer une liaison de données en précisant une source relative pointant vers la classe du modèle personnalisé. De cette manière, nous reproduisons le comportement d'une liaison de modèles :
Tag=
"
{
Binding
RelativeSource={
RelativeSource TemplatedParent},
Path=
Value,
Converter={
StaticResource D2TCConverter },
Mode=
OneWay
}
"
Cette liaison, bien que fonctionnelle à l'exécution, n'est toutefois pas correctement interprétée par Expression Blend. Le panneau Results affiche un message indiquant un code XAML invalide. Ce type d'erreurs est appelé Design Time Errors. Ces erreurs peuvent être handicapantes pour les designers. Dans certains cas, le composant est mal affiché dans Blend, dans d'autres la totalité de l'application ne sera pas affichée en mode création. Ceci est essentiellement dû au fait que le style est défini au sein d'un dictionnaire de ressources.
Créer une liaison de données en C# est assez simple, cela peut être intéressant lorsque vous développez votre propre framework. Admettons deux instances de Rectangle. Lorsque l'on met à jour la largeur du premier rectangle, cela modifie dynamiquement la largeur du second. Le code ci-dessous montre comment faire :
Random rnd =
new
Random
(
);
public
MainPage
(
)
{
InitializeComponent
(
);
//on crée une nouvelle instance de Binding
Binding b =
new
Binding
(
);
//on définit le type de liaison
b.
Mode =
BindingMode.
OneWay;
//on précise le nom du DependencyObject source
b.
ElementName =
rectangleSource.
Name;
//on déclare le chemin d'accès à la propriété source
b.
Path =
new
PropertyPath
(
"Width"
);
//on affecte la liaison à la propriété Width du rectangle cible
rectangleCible.
SetBinding
(
Rectangle.
WidthProperty,
b);
MouseLeftButtonDown +=
new
MouseButtonEventHandler
(
MainPage_MouseLeftButtonDown);
}
void
MainPage_MouseLeftButtonDown
(
object
sender,
MouseButtonEventArgs e)
{
//lorsque l'on clique sur LayoutRoot
//on met à jour la propriété source,
//ce qui modifie la propriété cible
rectangleSource.
Width =
(
double
)rnd.
Next
(
100
,
400
);
}
Le projet LecteurMultiMedia_SliderLiaisonModele.zip est disponible dans le dossier chap11 des exemples.
10-6-2. Les données fictives▲
Nous allons maintenant simuler l'affichage de données externes au sein d'Expression Blend. Nous utiliserons à cette fin une instance de ListBox que nous personnaliserons à la section 10.7 Le modèle ListBox. Lorsque vous modifiez le style d'un contrôle, il est nécessaire d'avoir le retour visuel des styles et des modèles en cours de modification. Bien évidemment, les contrôles standard, tels que la ScrollBar, sont assez faciles à tester puisque ce sont des éléments visuels dont les enfants ne sont pas générés dynamiquement à l'exécution. Ce n'est pas le cas des éléments d'une liste qui ne sont théoriquement visibles que lorsque les données sont chargées et affectées à la ListBox. Les ingénieurs de Microsoft ont pensé à cette éventualité et ont créé à cette fin la notion de données fictives. L'idée est simple : les designers ont la capacité de créer des collections de données typées aux valeurs aléatoires. Une fois créées, ils ont la possibilité de les affecter à une ListBox ou à n'importe quel autre contrôle dont le rôle est d'exposer des données.
Procéder ainsi permet aux designers de personnaliser les composants de données de manière autonome, sans solliciter le développeur. Lorsque les données réelles sont prêtes, il suffit de modifier une ligne de code pour les affecter en lieu et place des données fictives. Ouvrez le projet LecteurMultimedia" dans sa dernière version. Nous allons commencer par créer un jeu de données fictives via le panneau Data. Cliquez sur la première icône située en haut à gauche () de ce panneau, puis sélectionnez le menu Define New Sample Data… (voir Figure 11.43).
Dans la fenêtre de création qui s'affiche, choisissez de définir cette source pour l'ensemble du projet et nommez-la ListMedia. Vous pouvez décider d'afficher cette source de donnée à l'exécution ou uniquement dans l'interface de Blend. Laissez cette option inchangée et cliquez sur OK. Blend a automatiquement généré un répertoire dédié au stockage de ces données, celles-ci sont également affichées dans le panneau Data (voir Figure 11.44).
Afin que les données fictives soient accessibles au sein de l'ensemble du projet, Blend a modifié le fichier App.xaml en ajoutant une ressource de type SampleDataSource :
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source
=
"RD/BobifysLogicalResources.xaml"
/>
<ResourceDictionary
Source
=
"RD/Couleurs.xaml"
/>
<ResourceDictionary
Source
=
"RD/BobifysBrushes.xaml"
/>
<ResourceDictionary
Source
=
"RD/VideosImagesBrushes.xaml"
/>
<ResourceDictionary
Source
=
"RD/BobifysStyles.xaml"
/>
</ResourceDictionary.MergedDictionaries>
<
SampleData
:
ListMedia
x
:
Key
=
"ListMedia"
d
:
IsDataSource
=
"True"
/>
</ResourceDictionary>
La propriété IsDataSource n'est là que pour préciser si le panneau Data doit afficher ou non cette source de donnée. Au sein du panneau Data, deux propriétés sont créées par défaut. Nous allons les personnaliser et en ajouter d'autres afin de simuler une réelle source de données. Cliquez sur l'icône plus, située à droite de "Collection" afin d'ajouter autant de propriétés qu'il est nécessaire (voir Figure 11.45).
À l'exception des propriétés duree, idType (Number), type (Image), la majorité sera des chaînes de caractères String. L'équipe des ingénieurs Expression a poussé la conception relativement loin puisqu'il est possible de configurer le format de donnée représenté par un champ. Ainsi le champ auteur contiendra des noms de personnes fictives (voir Figure 11.46).
Vous pouvez vous référer au Tableau 11.3 pour configurer les champs que vous avez créés.
Nom des champs |
Type de valeur |
Format et spécificités de la valeur |
---|---|---|
auteur |
String |
Name |
couleur |
String |
Color |
date |
String |
Date |
description |
String |
Lorem Ipsum |
duree |
Number |
Nombre à 3 chiffres (inutile pour les médias) |
idType |
Number |
Nombre entre 1 et 3 |
titre |
String |
Lorem Ipsum |
type |
Image |
Préciser le chemin d'accès vers le répertoire bitmaps du projet. Les images choisies aléatoirement par Blend déterminent le type de média. |
url |
String |
Website |
La configuration des données fictives n'est pas encore terminée, vous pouvez modifier à la main les valeurs que vous souhaitez en cliquant sur l'icône de modification de données () située à droite de Collection. Dans le tableau qui s'affiche, appuyez-vous sur les images bitmap du champ Type afin de définir les champs idType et Color dont les valeurs doivent être en accord avec le type de média. La durée exprimée en secondes est également inutile pour les médias de type image, autant passer sa valeur à 0 dans ce cas (voir Figure 11.47).
Vous pouvez également modifier les titres si vous souhaitez obtenir un contenu réaliste. Une fois la configuration terminée, cliquez sur OK. La collection est prête à être affectée à une instance de ListBox. Si vous êtes designer, développer en C# peut vous sembler compliqué ou ne faisant pas partie de votre périmètre de travail. Ce n'est pas un problème puisque Blend vous permet de réaliser cette opération sans une ligne de code logique. Vérifiez que les grilles nommées Pseudo-Liste et PseudoScrollBar sont cachées en cliquant sur l'icône de l'œil située à droite des grilles dans l'arbre visuel. Ensuite, depuis le panneau Data, glissez la collection dans la grille nommée PanneauListe. Blend génère automatiquement un composant ListBox dont la propriété ItemsSource est liée à notre collection de données fictives. Définissez ses marges afin qu'il occupe l'espace laissé vacant par la grille cachée PseudoListe. :
<Slider
x
:
Name
=
"slider"
… />
<ToggleButton
x
:
Name
=
"DisplayModeToggleButton"
…
Cursor
=
"Hand"
/>
<ListBox
Margin
=
"2,69,14,4"
ItemTemplate
=
"{StaticResource ItemTemplate1}"
ItemsSource
=
"{Binding Collection}"
/>
<ToggleButton
x
:
Name
=
"ExpanderToggleButton"
… />
Nous avons parcouru la moitié du chemin. Si tout s'est correctement déroulé, vous devriez obtenir un visuel correspondant à la Figure 11.48
Le projet LecteurMultiMedia_SampleData.zip est dans le dossier chap11 des exemples de ce livre.
10-6-3. Le contexte de données▲
Dans l'exercice précédent, nous avons défini, sans nous en rendre compte, un contexte de données. Cette notion est importante en terme d'architecture, nous allons donc l'étudier dans cette section. Créez un projet temporaire de test, puis déposez côte à côte sur LayoutRoot une instance de conteneur Grid et un exemplaire de StackPanel. La grille contiendra une liste ; le StackPanel affichera le détail de l'élément sélectionné dans la liste. Comme nous l'avons vu, lorsque vous déposez une collection en provenance du panneau Data, vous générez par défaut une instance de ListBox exposant la totalité des champs décrits par la collection pour chaque élément de la liste. Il est toutefois possible de ne glisser-déposer qu'un ou plusieurs champs au choix via l'utilisation des touches Maj ou Ctrl. Dans ce cas, vous instanciez une liste n'affichant que le ou les champs qui ont été déposés sur le conteneur. Créez une collection de données fictives, puis choisissez l'une ou l'autre de ces méthodes pour déposer un exemplaire de ListBox dans la grille. Voici le code XAML généré par Blend :
<Grid
x
:
Name
=
"LayoutRoot"
Background
=
"White"
DataContext
=
"{Binding
Source={StaticResource ListMedia}}"
>
<Grid
HorizontalAlignment
=
"Left"
Margin
=
"54,25,0,206"
Width
=
"205"
>
<ListBox
x
:
Name
=
"listBox"
Margin
=
"0,0,5,0"
ItemTemplate
=
"{StaticResource ItemTemplate1}"
ItemsSource
=
"{Binding Collection}"
SelectedIndex
=
"-1"
/>
</Grid>
<StackPanel … />
Comme nous l'avons dit plus haut, une collection de données fictives est stockée comme ressource lorsqu'elle est générée dans Expression Blend. Cette dernière n'est pas directement affectée à l'instance de ListBox. En réalité, Blend a affecté la ressource ListMedia à la propriété DataContext de la grille principale. La grille principale est alors considérée comme le contexte de données principal. À ce titre, le contenu de la ressource ListMedia est accessible à tous les enfants de la grille LayoutRoot. Si nous abordons maintenant le composant ListBox instancié, nous remarquons que la propriété ItemsSource est liée au premier enfant de la ressource nommé Collection. Vous pourriez toutefois modifiez le nom de la collection. Cela signifie que vous pouvez en créer plusieurs au sein d'une même ressource de données fictives. Cette organisation est assez pratique dans certains scenarii d'applications multiclients, par exemple. Lors de l'affectation de ItemsSource, il n'est nul besoin de préciser une liaison complexe, car Collection est directement accessible par le contexte de données en cours.
Si vous êtes perdus à un moment ou à un autre, il vous suffit de consulter le panneau Data pour assimiler le code XAML. L'arborescence décrite dans le panneau montre explicitement que Collection est un enfant de la ressource listMedia. Pour finir, la propriété ItemTemplate accepte une valeur de type DataTemplate automatiquement générée par Expression Blend et formalisant les données de manière visuelle. Nous verrons ultérieurement comment modifier ou manipuler une instance de DataTemplate. Si vous appuyez sur Alt en même temps que vous glissez une collection, ce n'est pas une ListBox qui est générée. Dans ce cas, vous créez une liaison de données sur le conteneur ciblé, de sorte que, lorsque vous glisserez des champs de la collection dans un conteneur en maintenant la touche Alt enfoncée, les composants générés par cette action posséderont une liaison relative au contexte de donnée de ce conteneur. Cela est particulièrement utile lorsque vous souhaitez afficher le détail d'un élément sélectionné depuis un composant ListBox. Vous pouvez également utiliser l'icône de détail () située en haut à gauche du panneau Data pour arriver à ce résultat (voir Figure 11.49).
Maintenez la touche Alt appuyée et déposez la collection du panneau Resources sur le Stack-Panel, celui-ci est dorénavant affecté en tant que contexte de donnée. Sélectionnez tous les champs de la collection via la touche Maj ou Ctrl, puis glissez-les sur le StackPanel en maintenant la touche Alt enfoncée. Les intitulés sous forme de TextBlock, ainsi que les contrôles exposants la valeur de chaque champ, sont instanciés à l'intérieur du conteneur. Vous pouvez ajuster la mise en forme de ces derniers au sein du StackPanel (voir Figure 11.50).
Expression Blend a également anticipé le fonctionnement de l'application et a généré le code XAML dans ce sens. À l'exécution, le contexte de données du StackPanel est dépendant de l'élément sélectionné dans la liste. Le code XAML qui suit décrit ce fonctionnement :
<StackPanel …
DataContext
=
"{Binding SelectedItem, ElementName=listBox}"
d
:
DataContext
=
"{Binding Collection[0]}"
…>
<TextBlock …
Text
=
"auteur"
/>
<TextBlock
Text
=
"{Binding auteur}"
… />
<TextBlock …
Text
=
"couleur"
/>
<TextBlock
Text
=
"{Binding couleur}"
… />
<TextBlock …
Text
=
"date"
/>
<TextBlock
Text
=
"{Binding date}"
… />
<TextBlock …
Text
=
"description"
/>
<TextBlock
Text
=
"{Binding description}"
… />
<TextBlock …
Text
=
"duree"
/>
<TextBlock
Text
=
"{Binding duree}"
… />
<TextBlock …
Text
=
"idType"
/>
<TextBlock
Text
=
"{Binding idType}"
… />
<TextBlock …
Text
=
"titre"
/>
<TextBlock
Text
=
"{Binding titre}"
… />
<TextBlock …
Text
=
"type"
/>
<Image
Source
=
"{Binding type}"
… />
<TextBlock …
Text
=
"url"
/>
<TextBlock
Text
=
"{Binding url}"
… />
</StackPanel>
Dans ce cas, la propriété DataContext ne pointe pas vers une ressource, mais directement vers l'élément en cours de sélection dans la liste. Lorsqu'une propriété est préfixée de d: cela signifie qu'elle possède une affectation qui ne sera valable que durant la phase de conception sous Blend et non lors de l'exécution de l'application. Ainsi, dans le code XAML ci-dessous, il faut comprendre que lorsque vous êtes en cours de conception, le contexte de donnée du StackPanel est le premier élément de la collection :
- d:DataContext="{Binding Collection[0]}"
Cela vous permet de tester en temps réel l'affichage des valeurs de la collection dans la ListBox. Au contraire, durant l'exécution de l'application, le contexte de donnée évoluera en fonction de l'élément sélectionné dans la liste :
- DataContext="{Binding SelectedItem, ElementName=listBox}"
A priori, il serait possible d'obtenir le même comportement dans l'un ou l'autre des cas en modifiant l'aperçu dans Blend et en spécifiant exactement la même valeur que le contexte de données à l'exécution :
d:DataContext="{Binding SelectedItem, ElementName=listBox}"
Toutefois, Blend supporte très mal l'accès temps réel à l'élément sélectionné d'une liste, la compilation échoue si vous modifiez cette ligne. Laissez donc le code XAML tel qu'il est généré par Blend afin d'éviter les complications inutiles.
Compilez et testez l'application. Vous venez de créer de la logique applicative sans une seule ligne de code C#, cela en utilisant les mécanismes de liaison de données Silverlight ainsi que les outils fournis par Expression Blend.
10-6-4. Paramètres optionnels▲
Pour l'instant, l'écriture des liaisons (déclarées par le mot-clé Binding) est relativement simple, il existe toutefois un certain nombre de paramètres optionnels dont ci-dessous une liste non exhaustive.
- Source définit une source de données de type StaticResource dans la majorité des cas. Cette propriété est utilisée pour cibler des objets contenus au sein d'un dictionnaire de ressources.
- RelativeSource permet de définir un objet source relatif au contexte actuel. Cette propriété accepte deux valeurs, Self et TemplatedParent. Self cible l'objet définissant la liaison en cours comme source de données. TemplatedParent permet de cibler une propriété du modèle en cours de personnalisation et permet ainsi de reproduire le principe de liaison de modèle.
- ElementName permet de créer des liaisons entre différentes instances d'objets au sein d'un même arbre visuel. La valeur attendue est de type x:Name. L'objet ciblé est considéré comme source.
- Path définit le chemin d'accès vers la propriété d'un objet source.
- Converter définit une classe de conversion personnalisée.
- ConverterParameter est un attribut optionnel permettant d'envoyer un paramètre aux méthodes de conversion Convert et ConvertBack.
- Mode permet de gérer le type de liaison établie, elle prend trois types de valeur, OneTime, OneWay et TwoWay. La valeur OneTime permet d'établir une liaison de données prise en compte une seule fois au chargement du composant. OneWay et TwoWay synchroniseront les données durant l'exécution de manière unidirectionnelle ou bidirectionnelle. Pour finir, OneWay est le mode utilisé par défaut lorsqu'aucune valeur n'est définie pour Mode.
- ValidatesOnExceptions accepte une valeur booléenne, très utile dans le cadre de liaisons bidirectionnelles. Nous aborderons son utilisation au Chapitre 11 Composants personnalisés.
Vous remarquez que Source, RelativeSource et ElementName ont exactement le même rôle consistant à définir un objet source. Il n'est donc pas possible de déclarer plusieurs de ces attributs en même temps. Lorsque vous créez un jeu de données fictives, les liaisons sont en majorité à sens unique, il est toutefois possible de générer des liaisons à deux sens.
Par défaut, les liaisons bidirectionnelles ne sont générées que lorsque la collection de données fictives contient des valeurs booléennes. Pour ce type de champ, le composant généré est une case à cocher cliquable. Il faut donc modifier la donnée lorsque le composant CheckBox est coché ou décoché par l'utilisateur. La mise à jour des données est dans ce cas bidirectionnelle.
Pour tester le mode deux voies, il suffit de remplacer l'un des contrôles TextBlock par TextBox et préciser l'utilisation de ce mode en XAML :
<TextBlock …
Text
=
"titre"
/>
<TextBox
Text
=
"{Binding Path=titre, Mode=TwoWay}"
… />
Compilez et modifiez à la main la valeur du champ texte de saisie correspondant au titre. Ensuite, cliquez sur un autre composant pour qu'il perde le focus, la liste affiche alors la valeur mise à jour. Attention toutefois, mettre à jour la liste revient à modifier la collection.
De la même manière que pour le Slider personnalisé précédemment, il est possible d'utiliser une classe de conversion. Cela est même souvent conseillé, puisque dans de nombreux cas, les données présentées à l'utilisateur dans les vues ne sont pas stockées ou formatées de la même manière en base de données et dans l'interface utilisateur. Par exemple, en base, une date sera souvent stockée sous forme d'un nombre de seconde ou de millisecondes, qui représente le temps écoulé depuis le 1er janvier 1970 à minuit. Ainsi, le temps exprimé en seconde dans la base sera converti en date intelligible afin d'être affichée dans l'interface. À l'opposé, lorsque l'utilisateur saisira une date intelligible dans l'application, celle-ci sera convertie sous forme d'un nombre de secondes écoulées, lors de sa mise à jour dans la base SQL.
10-7. Le modèle ListBox▲
Nous allons maintenant personnaliser la ListBox que nous avons créé précédemment afin d'obtenir une liste de lecture dont le visuel correspond à la grille nommée PseudoListe. Cette grille simule l'affichage d'une liste de médias que nous diffuserons au sein du lecteur.
10-7-1. Structure et principes▲
Le contrôle ListBox figure parmi les plus complexes à personnaliser pour deux raisons. En premier lieu, il est constitué de nombreux composants de natures différentes qui sont situés à plusieurs niveaux d'imbrication. Il est nécessaire de les localiser et de les personnaliser les uns après les autres pour arriver au résultat final (voir Figure 11.51).
En second lieu, il est parfois obligatoire d'effectuer quelques manipulations délicates lorsque vous souhaitez arriver à vos fins. Finalement, l'architecture d'une ListBox, sans être réellement compliquée, est éclatée et permet une personnalisation très poussée de ce composant.
Cette architecture est articulée autour de trois familles de composants décrites sur le schéma de la Figure 11.42. La première catégorie concerne les contrôles courants de type ScrollViewer, ScrollBar, RepeatButton ou Thumb. Nous avons abordé leur personnalisation dans les grandes lignes et de ce point de vue, la seule difficulté consiste à les localiser dans l'arbre visuel. La deuxième famille est du même genre, sauf que c'est la première fois dans ce livre que nous y sommes confrontés de cette manière. Dans le cas d'une ListBox, elle est représentée par la classe ListBoxItem, pour les instances de listes déroulantes (de type ComboBox), il s'agira de la classe ComboBox-Item. Ces deux classes héritent en droite ligne de la classe abstraite ContentControl. Cela signifie qu'elles ont pour but premier d'afficher un ou plusieurs objets, tout en fournissant des bases logiques et graphiques minimum. C'est ce que vous constatez à la Figure 11.42 : un ListBox-Item contient un jeu d'états visuels permettant de personnaliser en partie l'affichage graphique et l'interactivité de chaque élément d'une liste. À l'instar d'un simple bouton, elles possèdent par défaut un enfant de type ContentPresenter qui définit la zone d'affichage des données. Dans la grande majorité des situations et au même titre que les boutons, un ListBoxItem affichera une simple chaîne de caractères. Cela est transparent, car chaque élément d'une collection appellera par défaut sa méthode ToString lorsque rien n'est précisé. Ces composants ont également la capacité de contenir un modèle de données personnalisé (DataTemplate) pour les cas plus élaborés.
Cette capacité est héritée de ContentControl via la propriété ContentTemplate, qui accepte les instances de DataTemplate. C'est en réalité une affectation en cascade générée via une liaison de modèles établie entre la propriété ContentTemplate du ListBoxItem et la propriété ContentTemplate de l'objet ContentPresenter contenu dans le modèle de ce dernier. Le DataTemplate est le troisième type d'objet utilisé pour personnaliser l'affichage d'une ListBox. Celui-ci n'est pas un contrôle a proprement dit, car il n'hérite pas la classe Control. Son but consiste à représenter de manière visuelle des données simples ou complexes, et il centralise à cette fin tous les contrôles nécessaires. Si vous avez besoin d'afficher un bitmap pour chaque élément d'une ListBox, c'est dans le DataTemplate que le composant Image sera placé par défaut pour afficher cette image. Il est en fait possible d'y imbriquer n'importe quel type de composant visuel. Les ListBox sont souvent utilisées pour présenter des objets métier. Chaque élément peut donc au besoin afficher des valeurs booléennes, des chaînes de caractères, des nombres, des sous-listes, des vidéos et de nombreux autres types d'objets adaptés à l'affichage de contenu. Le composant DataTemplate a pour rôle de centraliser tous ces objets.
Ce principe est à la fois pertinent, car généré par défaut dans Blend, mais peut également apparaître limité dans certains cas où le graphisme est élaboré. Finalement, l'idée consistant à séparer au maximum la pure donnée et sa représentation visuelle est toujours privilégiée. Il faut considérer ce principe comme un idéal à atteindre sans pour autant en faire un dogme. Le graphisme, l'expérience utilisateur et l'ergonomie sont également là, avec leur cahier des charges et leurs propres contraintes qui sont aujourd'hui des notions déterminantes en termes d'adoption. Ces contraintes remettent parfois en cause des architectures bien conçues mais laissant peu de place à l'innovation. Bien heureusement, Silverlight a pour particularité d'offrir suffisamment de souplesse de conception pour contenter tous les appétits.
Nous allons maintenant aborder ces problématiques à travers une manipulation concrète de la ListBox.
10-7-2. Modifier l'apparence d'une liste d'éléments▲
Ouvrez le projet LecteurMultimedia de l'archive LecteurMultiMedia_SampleData.zip du dossier chap11 des exemples. Sélectionnez la ListBox affichant les données fictives puis supprimez les remplissages de son arrière-plan et de sa bordure. Ensuite, créez un nouveau modèle de ListBox à partir de celui existant. Nommez le nouveau style BobifyMediaListBoxStyle, puis stockez-le dans le dictionnaire de ressources dédié aux styles BobifysStyles.xaml. Dans l'arbre visuel du modèle, vous remarquez que seule l'enveloppe de la liste sera concernée par le modèle que nous sommes en train de personnaliser. Ainsi, les éléments actuellement contenus par celle-ci ne peuvent pas être personnalisés à ce niveau, bien qu'ils soient tout de même accessibles par ailleurs. Ce n'est pas très grave, car dans un premier temps, vous allez vous concentrer sur les éléments interactifs gérant le défilement de la liste. Le contrôle ScrollViewer contient une instance de ScrollBar vertical qu'il va falloir modifier pour arriver à vos fins. Le fonctionnement d'une barre de défilement (ScrollBar) est très semblable à celui d'un Slider, mis à part que son modèle contient deux instances de RepeatButton, correspondant aux flèches du bas et du haut. Créez autant de styles personnalisés que nécessaire afin d'accéder au modèle de la barre de défilement. Étirez les dimensions fictives propres au mode design, de la grille nommée Root, afin de pouvoir personnaliser le composant ScrollBar plus confortablement (voir Figure 11.52).
Identifiez tous les composants logiques qu'il est nécessaire de personnaliser afin de reproduire le visuel de la grille nommée PseudoScrollBar. Elle est située sur la page principale dans le conteneur PanneauListe. Vous pouvez également faire des copier-coller de tracés, si nécessaire, pour les RepeatButton ainsi que pour le Thumb. Attention au fait que l'arrière-plan de Pseudo-ScrollBar est un tracé (Path) qu'il vaudra mieux remplacer par un composant de type Border dans le composant final. Utiliser ce dernier sera beaucoup plus pertinent, car il est plus adapté au redimensionnement qu'un tracé. L'ombre portée à gauche dans le visuel original, n'est pas simple à réaliser, mais il est tout de même possible soit de la remplacer, soit de la simuler en créant une épaisseur de la bordure à gauche du Border.
Même si ce travail a des aspects fastidieux, cela n'est pas plus difficile à faire que ce que nous avons déjà réalisé avec le Slider. Dans tous les cas, vous n'avez pas besoin de concevoir plus de deux modèles personnalisés, car les objets VerticalSmallDecrease et VerticalSmallIncrease peuvent accepter le même style. Il vous suffit d'appliquer une symétrie verticale sur l'un des deux boutons pour obtenir des directions opposées. Finalement, il est nécessaire de créer cinq styles différents qui sont dans l'ordre :
- BobifyMediaListBoxStyle, du contrôle ListBox lui-même ;
- BobifyMediaScrollViewerStyle associé au contrôle ScrollViewer contenu dans le composant ListBox ;
- BobifyMediaScrollBarStyle comme style la barre de défilement ;
- BobifyMediaScrollBarRepeatButtonStyle correspondant aux flèches basses et hautes ;
- BobifyMediaScrollBarThumbStyle pour le curseur la barre de défilement.
Une fois que vous avez modifié le visuel de chaque contrôle, l'idéal est de créer des animations entre les états visuels Normal, MouseOver et Pressed. La Figure 11.53 montre le visuel ainsi qu'une liste des styles finalisés sans toutefois afficher les transitions d'interactions utilisateur.
Le projet, LecteurMultiMedia_ScrollBar.zip, est dans le dossier chap11 des exemples du livre. Dans la prochaine section, nous allons nous consacrer à la personnalisation des éléments de la liste à afficher.
10-7-3. Data template versus ListBoxItem▲
Nous allons maintenant aborder la partie plus délicate concernant les éléments affichés sous forme de liste. Comme nous l'avons dit plus haut, ces derniers sont de type ListBoxItem et héritent de la classe ContentControl. À ce titre, ils possèdent un ContentPresenter au sein de leur modèle définissant la zone d'affichage du contenu. Lorsque vous affectez une instance de List<Object> à la propriété ItemsSource d'une ListBox, par défaut, chacun des objets invoque sa méthode ToString(). Le retour de cette méthode est affecté à la propriété Content dont l'affichage est géré par l'objet ContentPresenter de la ListBox. Ce n'est pas ce qui se passe dans notre cas. Par défaut, Blend a généré un DataTemplate stocké comme ressource afin de pouvoir afficher les champs de chaque enregistrement. Ce DataTemplate est affecté à la propriété Content-Template de chaque ListBoxItem. Il lui indique comment représenter visuellement l'enregistrement (également appelé value object) qui est affecté à la propriété Content de chacun des éléments de la liste. Pour accéder au DataTemplate, cliquez droit sur l'instance de ListBox, sélectionnez le menu Edit Additional Templates > Edit Generated Items (voir Figure 11.54).
Vous accédez à l'arbre visuel du modèle de donnée, vous y trouverez tous les composants nécessaires à l'affichage des données. Passez en mode d'édition XAML, voici le code correspondant :
<DataTemplate
x
:
Key
=
"ItemTemplate"
>
<StackPanel>
<TextBlock
Text
=
"{Binding auteur}"
/>
<TextBlock
Text
=
"{Binding couleur}"
/>
<TextBlock
Text
=
"{Binding date}"
/>
<TextBlock
Text
=
"{Binding description}"
/>
<TextBlock
Text
=
"{Binding duree}"
/>
<TextBlock
Text
=
"{Binding idType}"
/>
<TextBlock
Text
=
"{Binding titre}"
/>
<Image
Source
=
"{Binding type}"
HorizontalAlignment
=
"Left"
Height
=
"64"
Width
=
"64"
/>
<TextBlock
Text
=
"{Binding url}"
/>
</StackPanel>
</DataTemplate>
Incidemment, la ressource DataTemplate influence beaucoup le rendu final de la liste, elle n'est toutefois pas contenue directement dans le modèle du ListBoxItem. Cela engendre des limitations en terme de design. Par exemple, seul le contrôle ListBoxItem est capable de réagir aux interactions utilisateurs telles que le survol de la souris. Le mieux serait de copier-coller le StackPanel et son contenu dans l'arbre visuel du ListBoxItem afin de récupérer toutes les liaisons nécessaires simplement.
Copiez le code XAML décrivant la balise StackPanel et revenez au niveau de l'application. Cliquez droit sur la liste, choisissez le menu Edit Additional Templates > Edit Generated Item Container > Edit a Copy… Dans la fenêtre qui s'ouvre, nommez le style BobifyMediaListBoxItemStyle, puis stockez-le dans le dictionnaire adéquat. Collez le code XAML au bon endroit comme montré ci-dessous :
<Style
x
:
Key
=
"BobifyMediaListBoxItemStyle"
TargetType
=
"ListBoxItem"
>
<Setter
Property
=
"Padding"
Value
=
"3"
/>
…
<Setter
Property
=
"Template"
>
<Setter.Value>
<ControlTemplate
TargetType
=
"ListBoxItem"
>
<Grid
Background
=
"{TemplateBinding Background}"
>
<VisualStateManager.VisualStateGroups>
…
<Rectangle
x
:
Name
=
"fillColor"
Fill
=
"#FFBADDE9"
…/>
<Rectangle
x
:
Name
=
"fillColor2"
Fill
=
"#FFBADDE9"
…/>
<ContentPresenter
x
:
Name
=
"contentPresenter"
…/>
<Rectangle
x
:
Name
=
"FocusVisualElement"
…/>
<StackPanel>
<TextBlock
Text
=
"{Binding auteur}"
/>
<TextBlock
Text
=
"{Binding couleur}"
/>
<TextBlock
Text
=
"{Binding date}"
/>
<TextBlock
Text
=
"{Binding description}"
/>
<TextBlock
Text
=
"{Binding duree}"
/>
<TextBlock
Text
=
"{Binding idType}"
/>
<TextBlock
Text
=
"{Binding titre}"
/>
<Image
Source
=
"{Binding type}"
…/>
<TextBlock
Text
=
"{Binding url}"
/>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
Vous n'avez plus besoin du composant ContentPresenter dans l'arbre visuel, vous pouvez le supprimer. L'objectif est maintenant d'agencer les composants récupérés afin d'obtenir un visuel équivalent à celui exposé par la grille PseudoListe. Grâce à cette manipulation, nous pourrons obtenir une expérience utilisateur et une ergonomie plus poussées. La première étape consiste à remplacer l'affectation des remplissages des rectangles fillColor, fillColor2 et Focus-VisualElement par une liaison pointant vers la couleur :
<Rectangle
x
:
Name
=
"fillColor"
Fill
=
"{Binding couleur}"
RadiusX
=
"1"
RadiusY
=
"1"
IsHitTestVisible
=
"False"
Opacity
=
"0"
/>
<Rectangle
x
:
Name
=
"fillColor2"
Fill
=
"{Binding couleur}"
RadiusX
=
"1"
RadiusY
=
"1"
IsHitTestVisible
=
"False"
Opacity
=
"0"
/>
<Rectangle
x
:
Name
=
"FocusVisualElement"
Stroke
=
"{Binding couleur}"
StrokeThickness
=
"2"
RadiusX
=
"1"
RadiusY
=
"1"
Visibility
=
"Collapsed"
/>
Compilez et testez le projet. Vous remarquez que les couleurs de survol et de sélection correspondent désormais au type de média. Vous pouvez modifier les valeurs d'opacité des couleurs au sein des états visuels adéquats, afin d'obtenir un peu plus de punch. Pour plus de fluidité, créez des transitions d'environ 0,4 seconde avec une courbe de décélération de votre choix. Nommez ensuite les champs texte afin de pouvoir les identifier plus facilement dans l'arbre visuel. Voici la liste des tâches à réaliser pour arriver à une version plus aboutie :
- dégroupez le StackPanel et créez à la place trois colonnes et deux lignes dans la grille, ceci permet de mettre en page les composants de manière simplifiée, tout en prenant en compte d'éventuels redimensionnements de la liste.
- la première et la deuxième colonne possèdent respectivement des largeurs de 64 et de 100 pixels, la dernière est en mode relatif et occupe l'espace restant à disposition ;
- placez l'image et le champ exposant la durée, dans la première colonne ;
- le composant TextBock qui est lié au titre doit être dans la deuxième colonne ;
- la description, l'auteur et la date sont à positionner dans la dernière colonne ;
- vous pouvez supprimer les champs texte exposant l'URL, la couleur et la propriété idType ;
- définissez les marges et les options d'alignement adéquates ;
- la couleur des textes affichés doit par défaut être blanche, puis virer vers le noir lorsque l'un des éléments de la liste est sélectionné, qu'il ait le focus utilisateur ou non ;
- choisissez la police de caractères Tahoma que nous avons embarquée précédemment et spécifiez des tailles de polices différentes en fonction de l'importance de chaque champ ;
- créez une instance de Border s'adaptant à la totalité de la grille, puis définissez une bordure basse de 0,4 pixel d'épaisseur ; cette ligne sépare les éléments les uns des autres et améliore ainsi la lisibilité.
Une fois ces quelques étapes abouties, vous obtenez un visuel très proche de celui proposé par la maquette de départ. Il nous reste à trouver un moyen élégant d'afficher la durée au format adéquat. Pour cela il nous faut encore une fois utiliser notre classe de conversion. Il suffit juste de définir la valeur du paramètre Converter :
<TextBlock
x
:
Name
=
"duree"
Text
=
"{Binding duree,
Converter={StaticResource D2TCConverter},
Mode=OneWay
}"
HorizontalAlignment
=
"Center"
VerticalAlignment
=
"Bottom"
Foreground
=
"White"
FontFamily
=
"/LecteurMultiMedia;Component/Fonts/Fonts.zip#Tahoma"
FontSize
=
"10.667"
Grid.
Row
=
"1"
Margin
=
"0,0,0,8"
/>
Nous aurons tout de même à modifier la classe de conversion, car dans le cas d'une image, il n'y a pas besoin d'afficher de durée, il nous suffit juste de tester la valeur et de renvoyer une chaîne de caractères vide si la durée est égale à zéro :
public
object
Convert
(
object
value
,
Type targetType,
object
parameter,
System.
Globalization.
CultureInfo culture)
{
double
v ;
try
{
v =
(
double
)value
;
}
catch
(
Exception e)
{
return
String.
Empty;
}
if
(
v ==
0
) return
String.
Empty;
TimeSpan ts =
new
TimeSpan
(
);
if
(
value
is
double
) ts =
TimeSpan.
FromMilliseconds
(
v*
1000
);
string
p =
parameter as
string
;
if
(
p ==
"00:00.000"
)
{
return
ts.
Minutes.
ToString
(
"00"
)
+
":"
+
ts.
Seconds.
ToString
(
"00"
)
+
"."
+
ts.
Milliseconds.
ToString
(
"000"
);
}
return
ts.
Minutes.
ToString
(
"00"
)
+
":"
+
ts.
Seconds.
ToString
(
"00"
);
}
Compilez et testez le projet, nous avons presque atteint nos objectifs (voir Figure 11.55).
Il nous reste encore à gérer le redimensionnement de la liste. Diminuez sa largeur pour voir comment l'agencement de ses éléments réagit. Comme vous le constatez plusieurs points méritent notre attention. Tout d'abord, la grille ne doit pas excéder une hauteur trop importante. Dans l'idéal, les instances de ListBoxItem devront avoir les mêmes dimensions, une valeur de 100 pixels pour la propriété Height est idéale. Le bloc de texte contenant la description ne doit pas excéder 46 pixels de hauteur. Voici le code XAML définitif concernant la personnalisation du modèle de ListBoxItem :
<Border Grid.
RowSpan
=
"2"
BorderThickness
=
"0,0,0,0.4"
Grid.
ColumnSpan
=
"3"
Opacity
=
"0.4"
Margin
=
"4,0"
>
<Border.BorderBrush>
<SolidColorBrush
Color
=
"{StaticResource PlayerBackground}"
/>
</Border.BorderBrush>
</Border>
<Rectangle
x
:
Name
=
"fillColor"
Fill
=
"{Binding couleur}"
RadiusX
=
"1"
RadiusY
=
"1"
IsHitTestVisible
=
"False"
Opacity
=
"0"
Grid.
ColumnSpan
=
"3"
Grid.
RowSpan
=
"2"
/>
<Rectangle
x
:
Name
=
"fillColor2"
Fill
=
"{Binding couleur}"
RadiusX
=
"1"
RadiusY
=
"1"
IsHitTestVisible
=
"False"
Opacity
=
"0"
Grid.
ColumnSpan
=
"3"
Grid.
RowSpan
=
"2"
/>
<Rectangle
x
:
Name
=
"FocusVisualElement"
Stroke
=
"{Binding couleur}"
StrokeThickness
=
"2"
RadiusX
=
"1"
RadiusY
=
"1"
Visibility
=
"Collapsed"
Grid.
ColumnSpan
=
"3"
Grid.
RowSpan
=
"2"
/>
<TextBlock
x
:
Name
=
"auteur"
Text
=
"{Binding auteur}"
HorizontalAlignment
=
"Left"
Margin
=
"4,4,0,0"
VerticalAlignment
=
"Top"
Grid.
Column
=
"2"
Grid.
Row
=
"1"
Foreground
=
"White"
FontFamily
=
"/LecteurMultiMedia;Component/
Fonts/Fonts.zip#Tahoma"
FontSize
=
"10.667"
/>
<TextBlock
x
:
Name
=
"date"
Text
=
"{Binding date}"
HorizontalAlignment
=
"Right"
Margin
=
"0,4,4,0"
VerticalAlignment
=
"Top"
Grid.
Column
=
"2"
Grid.
Row
=
"1"
Foreground
=
"White"
FontFamily
=
"/
LecteurMultiMedia;Component/Fonts/Fonts.zip#Tahoma"
FontSize
=
"10.667"
/>
<TextBlock
x
:
Name
=
"description"
Text
=
"{Binding description}"
Margin
=
"4,8,4,0"
VerticalAlignment
=
"Top"
Grid.
Column
=
"2"
TextWrapping
=
"Wrap"
Foreground
=
"White"
FontFamily
=
"/
LecteurMultiMedia;Component/Fonts/Fonts.zip#Tahoma"
FontSize
=
"10.667"
Height
=
"46"
/>
<TextBlock
x
:
Name
=
"duree"
Text
=
"{Binding duree, Converter={StaticResource
D2TCConverter}, Mode=OneWay}"
HorizontalAlignment
=
"Center"
VerticalAlignment
=
"Top"
Foreground
=
"White"
FontFamily
=
"/
LecteurMultiMedia;Component/Fonts/Fonts.zip#Tahoma"
FontSize
=
"10.667"
Grid.
Row
=
"1"
Margin
=
"0,4,0,0"
/>
<TextBlock
x
:
Name
=
"titre"
Text
=
"{Binding titre}"
Margin
=
"4,0"
VerticalAlignment
=
"Center"
Grid.
Column
=
"1"
TextWrapping
=
"Wrap"
d
:
LayoutOverrides
=
"HorizontalAlignment"
Foreground
=
"White"
FontFamily
=
"/LecteurMultiMedia;Component/Fonts/Fonts.zip#Tahoma"
FontSize
=
"12"
Grid.
RowSpan
=
"2"
Height
=
"40"
/>
<Image
x
:
Name
=
"icon"
Source
=
"{Binding type}"
HorizontalAlignment
=
"Center"
Height
=
"32"
VerticalAlignment
=
"Center"
Width
=
"48"
/>
La personnalisation de la liste est terminée, vous pourriez encore l'améliorer ou bien vous entraîner avec le composant AutoCompleteBox. Ce dernier est un mélange entre une liste et un champ texte de saisie ; il constitue un petit défi. Vous possédez déjà le style de la ScrollBar contenue dans la liste sur lequel vous pouvez vous baser. Vous pouvez également supprimer les objets dont le nom est préfixé par Pseudo, puis créer les états visuels de l'application permettant d'étendre ou de replier la liste. Le bouton nommé ExpanderToggleButton a été créé à cet effet. En mode replié, seuls les titres, les icônes ainsi que la durée restent visibles. Vous pouvez télécharger le projet finalisé LecteurMultiMedia_DesignFinal.zip dans le dossier chap11 des exemples du livre.
Au Chapitre 11 Composants personnalisés, nous aborderons la conception de composants. Nous apprendrons ainsi à centraliser et à formaliser les fonctionnalités au sein de composants dédiés que vous pourrez partager et réutiliser dans vos projets.
11. Composants personnalisés▲
La conception de composants réutilisables a toujours été au centre de nombreux débats. Créer un composant from scratch pose un certain nombre de questions. Par exemple, est-ce que le composant n'existe pas déjà tout fait sur le Web ? Ou encore, est-il productif de concevoir un contrôle si celui-ci est spécifique à votre problématique actuelle ? Certains indices peuvent vous éclairer, avant de vous décider à concevoir un tel composant. Il vous faut d'abord envisager tous les cas de réutilisation possible. En effet, qui dit conception de composant dit réutilisation générique, cette notion vous fera automatiquement sortir du cadre de production fixé par le projet en cours. Autrement dit, pour que le composant soit réutilisable, il faut évaluer un certain nombre de cas d'utilisation qui ne sont pas forcément prévus dans le cahier des charges de votre projet. Il est bien sûr possible d'implémenter de nouvelles fonctionnalités au fur et à mesure de vos besoins et des projets. Toutefois, concevoir un composant pose obligatoirement une réflexion plus vaste que celle imposée par les nécessités du moment. Ce qui est fait n'est plus à faire, lorsque vous vous lancez dans ce type de développement, vous faites en quelque sorte un pari sur l'avenir, si le composant n'est pas réutilisé, c'est que ce pari aura échoué. Afin d'optimiser le temps, il vous faut clairement définir le cadre d'utilisation ainsi qu'une certaine granularité dans les niveaux de services fournis par vos composants. Une fois lancé, il est difficile de savoir où s'arrêter. De ce point de vue, le projet en cours joue le rôle d'un garde-fou. Dans ce chapitre, vous apprendrez à identifier vos besoins et à faire les bons choix de conception. Nous aborderons deux manières différentes de concevoir des contrôles.
11-1. Contrôle utilisateur ▲
Créer un composant de toute pièce revient toujours à hériter d'une classe. Celle-ci doit implémenter la logique ainsi que les mécanismes de base que vous utiliserez. Dans cette section, nous listerons les différents types d'héritage possibles, puis nous construirons notre premier composant utilisateur.
11-1-1. Un héritage pertinent▲
Plus vous dériverez vos composants de classes spécialisées, plus vous embarquerez de code à l'initialisation de ce dernier. Le tout est de connaître les capacités supportées par chaque famille de composants Silverlight afin de créer le minimum de logique tout en répondant au cahier des charges. Cela permet d'optimiser le poids et de minimiser la logique embarquée en ne laissant accès qu'au strict nécessaire. De cette manière, vous ne polluez pas l'utilisateur du composant par d'inutiles propriétés. On peut considérer plusieurs grandes familles de composants, mais seules certaines d'entre elles sont réellement exploitables dès lors que vous souhaitez créer vos propres composants.
- La première famille concerne les contrôles dont les modèles sont personnalisables. Ils héritent de la classe abstraite Control.
- Un sous-ensemble de la classe Control correspond aux contrôles utilisateur (de type UserControl) que nous allons aborder dans cette section.
- Les composants non personnalisables d'un point de vue modèle, comme TextBlock, qui héritent de FrameworkElement et ne possèdent donc pas la propriété Template. Plus vous utiliserez une classe générique, plus votre composant sera léger et pertinent. Hériter de Framework-Element s'avère pertinent dans certains cas particuliers.
- La famille des conteneurs à plusieurs enfants, tels que Grid ou StackPanel joue un rôle important. Il est toutefois plus rare d'avoir besoin d'un nouveau panneau. Ils héritent de la classe abstraite Panel et utilisent en interne les mécanismes propres au système d'agencement Silverlight. Nous ne concevrons pas de conteneur personnalisé dans ce chapitre, le blog http://www.tweened.org contient un article décrivant un tutoriel pas à pas à ce propos.
- Les conteneurs spécialisés à enfant unique tels que Popup, Border. Ils héritent directement de FrameworkElement et sont des classes fermées à la modification. La classe ContentControl qui hérite de Control en fait partie, car elle possède l'attribut ContentPropertyAttribute qui fournit cette capacité. La classe ViewBox hérite de ContentControl.
- Les classes de présentation telles que ContentPresenter et ItemsPresenter dont l'objectif est de formaliser des données ou certains types d'objets de manière visuelle.
- Les formes simples comme Ellipse ou Rectangle qui dérivent de Shape. En apparence, il est possible d'hériter de Shape, toutefois contrairement à WPF, cette classe ne possède pas la méthode qui permet de définir une nouvelle géométrie. Il n'est donc pas possible de créer de formes personnalisées par ce biais.
Cette classification est arbitraire et n'est valable que d'un point de vue fonctionnel et pratique. Pour notre part, nous aborderons la création de composants héritant de UserControl et de Control.
11-1-2. Navigation page par page▲
Lorsque vous concevez un composant, il convient d'identifier correctement ses objectifs. Apporte-t-il une nouvelle ergonomie, ou traite-t-il une fonctionnalité macro telle qu'un processus utilisateur. Dans le premier cas, les contrôles représentent une ergonomie, un type d'interaction ou une fonctionnalité spécifiques. Un RadioButton, une liste déroulante (ComboBox), une grille d'alignement (Grid) ou un champ de complétion automatique (AutoCompleteBox) sont autant d'exemples faisant partie de cette catégorie. Nous verrons leur conception à la section 12.3 Chargement de données.
Dans le second cas, le contrôle est très souvent composé de plusieurs autres contrôles et peut être considéré comme un module applicatif à part entière. Un lecteur vidéo, une fenêtre de connexion ou un catalogue de produits sont des cas représentatifs de cette catégorie. Ils font référence à des nécessités applicatives et contiennent en interne un flux d'utilisation métier. On les considère comme des modules autonomes et ils sont, à ce titre, généralement conçus sous forme d'instances de type UserControl. La classe principale MainPage étant elle-même issue de type UserControl, il est naturel et sain qu'un UserControl en contienne plusieurs autres ou soit lui-même chargé ou instancié dynamiquement (voir Figure 12.1).
La classe Page, dont vous pouvez voir plusieurs instances sur la Figure 12.1, hérite de User-Control et implémente des capacités supplémentaires en matière de navigation. À l'instar de UserControl, la classe Page est formalisée à l'aide de deux fichiers contenant respectivement le langage déclaratif XAML et le code logique C#. Ouvrez le projet de l'archive WebNavigation_Base.zip du chap12 des exemples de ce livre. Nous allons partir de ce projet pour créer un site Web multipage avec gestion de l'historique.
Supprimez la grille grise située au centre de LayoutRoot, en faisant attention au code logique qui pilote le composant TextBlock. Remplacez la grille par un composant Frame que vous trouverez dans le panneau Assets et nommez-le frame. Il doit posséder les mêmes marges et modes de redimensionnement que la grille supprimée. L'espace de noms navigation est automatiquement généré lors de l'instanciation de Frame. Ce composant a pour but de gérer l'affichage des différentes pages, leur mise en cache ainsi que l'historique de navigation. Celui-ci est directement pilotable via votre navigateur Web favori, l'utilisation d'URL avec ancres (#) ou les flèches avant et arrière. Cliquez droit sur le projet, puis sélectionnez le menu Add New Item… Dans la boîte de dialogue affichée, choisissez Page et nommez-la Nouveautes.xaml (voir Figure 12.2).
Répétez l'opération pour générer des pages correspondant aux menus affichés en haut de Layout-Root sans oublier la page Accueil.xaml qui sera chargée par défaut. Pour une meilleure organisation du projet, vous pouvez créer un répertoire nommé navPages, puis ajouter chaque page au projet par un clic droit sur celui-ci (voir Figure 12.3).
Mis à part l'espace de noms et le type, le code XAML de chaque page générée est similaire à celui d'un UserControl standard. Une grille principale transparente nommée LayoutRoot est générée par défaut et les dimensions de l'instance de Page sont en mode Auto. Celles-ci s'adaptent en fait par défaut à celles de l'objet Frame de la page principale, car il est responsable de leur affichage :
<
navigation
:
Page
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
:
navigation
=
"clr-namespace:System.Windows.Controls;assembly=
System.Windows.Controls.Navigation"
x
:
Class
=
"pleinEcran.Nouveautes"
Title
=
"Nouveautes Page"
d
:
DesignWidth
=
"640"
d
:
DesignHeight
=
"480"
>
<Grid
x
:
Name
=
"LayoutRoot"
/>
</
navigation
:
Page>
Vous pouvez créer du contenu pour ces pages. Afin de se concentrer sur le principe de navigation, définissez une couleur d'arrière-plan ainsi qu'un titre central pour chacune d'entre elles. Une fois cette tâche effectuée, revenez dans la page principale, sélectionnez le composant Frame, puis affectez la chaîne de caractères /navPages/Accueil.xaml à sa propriété Source :
<
navigation
:
Frame
x
:
Name
=
"navFrame"
Margin
=
"30,60,30,30"
Background
=
"#00742323"
Source
=
"/navPages/Accueil.xaml"
/>
Compilez l'application pour tester le chargement de la page Accueil.xaml. Il s'agit maintenant de mettre à jour cette propriété lors du clic sur chaque menu. Sélectionnez le premier bouton du WrapPanel, puis affectez la propriété Tag de la chaîne de caractères /navPages/Nouveautes.xaml. Répétez cette opération pour l'ensemble des boutons. Nommez le WrapPanel : MenuHaut, puis ouvrez la solution dans Visual Studio. Nous allons parcourir les enfants du WrapPanel et affecter un écouteur identique pour chacun d'eux. Nous allons récupérer le diffuseur de l'événement Click dynamiquement afin de récupérer la valeur de sa propriété Tag :
public
MainPage
(
)
{
InitializeComponent
(
);
foreach
(
Button b in
MenuHaut.
Children)
{
b.
Click +=
new
RoutedEventHandler
(
showPage);
}
}
void
showPage
(
object
sender,
RoutedEventArgs e)
{
Button b =
sender as
Button;
if
(
b !=
null
)
{
frame.
Navigate
(
new
Uri
(
b.
Tag.
ToString
(
),
UriKind.
Relative));
//frame.Source = new Uri(b.Tag.ToString(), UriKind.Relative);
}
}
Vous pouvez soit utiliser la méthode Navigate soit la propriété Source pour arriver à vos fins. Testez l'application dans le navigateur, le composant Frame affiche chaque page l'une après l'autre, après qu'un clic ait été diffusé par chacun des menus. Frame permet également de gérer la navigation via l'utilisation directe de la barre d'adresse du navigateur. La propriété UsesJournalParent est responsable de ce comportement. Par défaut, elle est en mode automatique, si l'instance de Frame n'est pas affichée dans une autre Frame, elle utilise le journal de navigation du navigateur Web. Vous obtenez des URL du type :
- http://localhost:49160/Default.html#/navPages/Nouveautes.xaml
La navigation, dans ce type d'application, est également réalisable via des instances d'Hyperlink-Button. Il suffit de renseigner la propriété Target avec le nom du composant Frame puis de préciser l'URL de la page chargée dans la propriété Source de l'HyperLinkButton.
Les instances de Page associées au composant Frame offrent des fonctionnalités de navigation puissantes. Il est ainsi possible de générer des URL plus standard et lisibles de manière dynamique en mappant les adresses XAML de type #/navPages/Nouveautes.xaml. Il est également possible de détecter le chargement ou le déchargement d'une page. Cela permet d'ajouter des transitions ou de la logique métier comme le chargement de données ou la sauvegarde d'actions effectuées dans la page. Vous trouverez le projet finalisé dans le dossier chap12 des exemples : WebNavigation.zip.
Nous allons maintenant créer un UserControl d'un point de vue composant.
11-2. Boîte de connexion▲
Dans la grande majorité des cas, vous concevrez un UserControl lorsque vous aurez besoin de centraliser la logique et l'interface utilisateur dédiées à une fonctionnalité spécifique de votre application. L'exemple le plus emblématique est la mire de connexion. Téléchargez le projet Lecteur-Multimedia_BaseUC présent dans le chap12 des exemples de ce livre.
11-2-1. Conception visuelle▲
Sélectionnez la grille nommée PanneauAccueil, puis utilisez le raccourci F8 ou le menu Tools et l'option Make Into UserControl… Une fenêtre apparaît vous demandant de renseigner le nom du nouveau UserControl. Renseignez LoginBox dans le champ de saisie correspondant et laissez l'autre option décochée. Celle-ci vous permet de laisser l'arborescence intacte si vous la cochez. Dans notre cas, nous souhaitons remplacer la grille PanneauAccueil par notre UserControl (voir Figure 12.4).
Blend génère automatiquement un UserControl à la racine du projet, qui est constitué d'un fichier XAML et d'un document C#. Le code logique contient une classe partielle LoginBox faisant directement référence au code déclaratif XAML contenu dans LoginBox.xaml. Les principes d'architecture sont strictement identiques à ceux déjà évoqués dans MainPage.xaml. Il est donc possible de créer des états visuels pour LoginBox, de la logique, des données fictives et tout autre type de ressources. La seule différence est que LoginBox est affiché au sein d'un autre User-Control. À ce titre, il ne peut être visualisé correctement dans ce dernier sans être préalablement compilé. Dans le cas contraire, si vous ouvrez à nouveau MainPage.xaml, une icône en forme de point d'exclamation est affichée en haut à gauche et un cadre jaune/orange entoure l'instance du UserControl LoginBox (voir Figure 12.5).
Compilez afin de visualiser le rendu final : le point d'exclamation et la bordure jaune disparaissent. L'arbre visuel de l'application principale a été allégé, puisque seule l'instance du User-Control généré est visible, son arborescence est dorénavant située dans le fichier LoginBox.xaml.
Vous allez concevoir plusieurs états visuels permettant à un éventuel utilisateur de se connecter avant de voir les astuces s'afficher. Il vous faut un état visuel affichant la mire de login par défaut, un état de connexion avec une option avancée ainsi que l'état affichant une astuce une fois que la connexion est acceptée. Créez un groupe nommé Login-States dans le panneau States, puis ajoutez les états Login, AdvanceLogin et Tips (voir Figure 12.6). Vous pourriez également créer un état correspondant au refus de connexion côté serveur, vous allez toutefois procéder de manière différente pour gérer les erreurs de connexion. Il vous faut également déplacer le fond gris, l'éclaboussure jaune et le titre dans la grille LayoutRoot. De cette façon, nous allons centraliser les éléments relatifs aux astuces et ceux du futur formulaire de connexion dans des conteneurs différents, cela facilitera la gestion des transitions (voir Figure 12.6).
Renommez et réorganisez selon vos besoins l'arbre visuel de l'application, vous pouvez également vous fier à la Figure 12.6. Cachez la grille nommée astuces via l'icône de l'œil dans l'arbre visuel et logique. De cette manière vous allez pouvoir travailler sur le formulaire sans vous encombrer d'éléments inutiles. Dans la grille formulaire, créez cinq lignes en redimensionnement automatique en cliquant sur le liseré bleu présent à gauche de celle-ci, puis créez une colonne en redimensionnement relatif (icône du cadenas ouvert) de 30 % à gauche et de 60 % à droite. Cela va nous permettre de positionner les éléments du formulaire simplement. À part la première ligne qui doit posséder environ 180 pixels de hauteur en dur, vous devez vérifier que les lignes en redimensionnement automatique possèdent 0 comme valeur minimum allouée (voir Figure 12.7).
Arrangez-vous pour que la hauteur (Height) de tous les éléments du formulaire soient en mode de redimensionnement automatique, mis à part le TextBlock "Adresse" ainsi que le champ de saisie renseignant l'adresse du serveur proxy. Garder des valeurs en dur permet de les animer plus simplement lorsque vous aurez besoin de gérer la transition permettant d'afficher ou non le mode de connexion avancé. Sélectionnez le UserControl racine principal, puis dans le code XAML, supprimez la propriété d:DesignHeight afin d'avoir un aperçu de l'adaptation de sa hauteur, en fonction du contenu affiché. Vérifiez également que le fond et la grille astuces possèdent une hauteur en mode Auto (voir Figure 12.8).
Sélectionnez chaque état visuel et modifiez les états respectifs de chaque élément en générant des animations traditionnelles. Cela permet de désactiver les interactions utilisateur sur les objets cachés en passant la valeur de leur propriété Visibility à Collapsed en toute fin d'animation. Vous avez de nombreuses modifications à faire, mais la bonne pratique consiste à définir l'état de base comme celui affiché par défaut. Dans notre cas, il s'agit de faire apparaître la mire de login sans le paramétrage éventuel d'un proxy. Il sera ensuite plus aisé de configurer les trois états visuels. N'oubliez pas également que certaines modifications sont à prévoir dans le fichier MainPage.xaml. Vous pouvez télécharger le projet avec les états visuels et l'intégration du contrôle utilisateur. Il se trouve dans l'archive LecteurMultimedia_LoginBox.zip du dossier chap12. Ce dernier pourra être utile pour la deuxième partie de cet exercice.
11-2-2. Validation par liaison de données ▲
Depuis Silverlight 3, les objets de l'arbre visuel peuvent capter les exceptions levées lors de la mise à jour de sources de données. Le cas le plus flagrant concerne les formulaires. Il est souvent pratique de définir un objet métier comme contexte de données du formulaire, les champs de saisie définissent des liaisons à deux voies permettant la mise à jour de l'objet métier lors de la saisie utilisateur. La classe métier C# possède elle-même des accesseurs qui lèvent des erreurs lorsque les données renseignées ne correspondent pas au format attendu. Ces erreurs sont captées par les instances de FrameworkElement de la liste d'affichage, qui affichent en retour une alerte visuelle indiquant l'erreur rencontrée. Cela est assez simple à réaliser, reprenez le projet Lecteur-Multimedia dans sa dernière version.
Dans Visual Studio, créez un répertoire classes, cliquez droit dessus et ajoutez une classe nommée UserCredentials. Celle-ci possède une suite de champs privés et de propriétés associées qui contiennent des accesseurs gérant la validation des données fournies. L'une de ces données permet de valider le formatage d'un e-mail et lève une exception en cas d'erreur de format détectée :
…
using
System.
Text.
RegularExpressions;
namespace
LecteurMultiMedia.
classes
{
public
class
UserCredentials
{
private
string
email;
private
string
password;
private
bool
useProxy =
false
;
private
string
proxyAdress =
String.
Empty;
public
string
Email
{
get
{
return
this
.
email;
}
set
{
Regex myRegex =
new
Regex
(
@"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]
{1,3}\.[0-9]{1,3}\.[0-9]
{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|
[0-9]{1,3})(\]?)$"
,
RegexOptions.
IgnoreCase);
if
(!
myRegex.
IsMatch
(
value
))
{
throw
new
Exception
(
"L'adresse email fournie n'est pas valide."
);
}
else
{
this
.
email =
value
;
}
}
}
public
string
Password
{
get
{
return
this
.
password;
}
set
{
if
(
String.
IsNullOrEmpty
(
value
) ||
value
.
Length <
4
)
{
throw
new
Exception
(
"Le mot de passe possède au moins 4 caractères."
);
}
else
{
this
.
password =
value
;
}
}
}
public
bool
UseProxy
{
get
{
return
this
.
useProxy;
}
set
{
this
.
useProxy =
value
;
}
}
public
string
ProxyAdress
{
get
{
return
this
.
proxyAdress;
}
set
{
if
(
this
.
useProxy ==
true
)
{
this
.
proxyAdress =
value
;
}
else
{
this
.
proxyAdress =
String.
Empty;
}
}
}
}
}
Chaque propriété peut lever une exception personnalisée lors de l'affectation d'une nouvelle valeur, cela est réalisé assez simplement via l'instruction throw new Exception(). Dans le code ci-dessus, vous pouvez améliorer le contrôle d'affectation de l'adresse proxy à l'aide d'une autre expression régulière. Une fois la classe de données créée, il suffit d'en affecter une instance comme contexte de données de la grille formulaire. Celle-ci est située au sein du contrôle utilisateur LoginBox. Le mieux est de faire de l'objet de données un membre de la classe LoginBox, de cette manière, il reste disponible tout au long de l'application :
public
partial
class
LoginBox :
UserControl
{
private
UserCredentials currentUserCredentials =
new
UserCredentials
(
);
public
LoginBox
(
)
{
InitializeComponent
(
);
formulaire.
DataContext =
currentUserCredentials;
}
}
Du côté d'Expression Blend, nous n'avons plus qu'à définir une liaison de données à deux voies. Lorsque l'utilisateur renseignera les informations de connexion demandées, il mettra à jour l'objet de donnée et sera notifié des éventuelles erreurs de saisie levées par les accesseurs de la classe UserCredentials. Le code XAML est assez simple :
<TextBox …
Text
=
"{Binding Email, Mode=TwoWay, ValidatesOnExceptions=True}"
…>
<PasswordBox …
Password
=
"{Binding Password, Mode=TwoWay, ValidatesOnExceptions=True}"
/>
<ToggleButton
Content
=
"Serveur Proxy "
…
IsChecked
=
"{Binding UseProxy, Mode=TwoWay}"
… />
<TextBox
x
:
Name
=
"UrlProxyTxt"
Text
=
"{Binding ProxyAdress, Mode=TwoWay, ValidatesOnExceptions=True}"
…/>
Il est important de remarquer que les données saisies sont affectées à l'objet source lorsque l'élément du formulaire de saisie perd le focus utilisateur. Ainsi les exceptions ne sont éventuellement levées qu'à cet instant. Compilez l'application et entrez des valeurs erronées. Afin que le champ de saisie de l'e-mail perde le focus, sélectionnez celui correspondant au mot de passe. La valeur de sa propriété Text est ainsi mise à jour et une erreur est levée. Le champ de saisie de l'e-mail est entouré d'un cadre rouge et la chaîne de caractères définie dans l'exception est affichée dans une étiquette qui apparaît sur le côté (voir Figure 12.9).
Le visuel affiché en cas d'erreur de saisie est entièrement paramétrable et cela à différents niveaux. Dans un premier temps, vous pouvez configurer l'aspect visuel du cadre rouge apparaissant autour du composant TextBox lorsqu'une exception est levée. À cette fin, vous n'avez qu'à modifier le modèle du composant et à accéder au groupe d'états nommés ValidationStates. Dans un second temps, il est possible de modifier l'aspect de l'étiquette qui apparaît sur le côté du champ de saisie. Lorsque vous créez un modèle personnalisé de TextBox, Blend génère automatiquement un modèle nommé ValidationToolTipTemplate. Il est accessible via le panneau Resources et contient deux états nommés Open et Closed. Vous pouvez paramétrer la forme et la couleur de l'étiquette à cet endroit. En dernier ressort, la gestion visuelle et logique des exceptions de saisie peut-être contrôlée via la diffusion de l'événement BindingValidationError par le moteur de liaison. L'événement est en général diffusé par le contexte de donnée, il faut toutefois qu'au moins un de ses enfants définisse à true la valeur de la propriété NotifyOnValidationError pour que l'événement soit écouté. Voici le code logique correspondant à l'écoute de l'événement ainsi que la personnalisation du visuel résultant de cet événement :
public
partial
class
LoginBox :
UserControl
{
private
UserCredentials currentUserCredentials =
new
UserCredentials
(
);
public
LoginBox
(
)
{
InitializeComponent
(
);
formulaire.
DataContext =
currentUserCredentials;
formulaire.
BindingValidationError +=
new
EventHandler<
ValidationErrorEventArgs>(
formulaire_BindingValidationError);
}
void
formulaire_BindingValidationError
(
object
sender,
ValidationErrorEventArgs e)
{
if
(
e.
Action ==
ValidationErrorEventAction.
Added)
{
FondAccueil.
BorderBrush =
new
SolidColorBrush
(
Colors.
Red);
}
else
if
(
e.
Action ==
ValidationErrorEventAction.
Removed)
{
FondAccueil.
BorderBrush =
new
SolidColorBrush
(
Colors.
White);
}
}
}
Du côté XAML, il suffit d'ajouter l'attribut de liaison NotifyOnValidationError permettant la diffusion de l'événement :
<TextBox …
Text
=
"{Binding Email,
Mode=TwoWay,
ValidatesOnExceptions=True,
NotifyOnValidationError=true}"
…/>
Cette fois-ci, non seulement une étiquette rouge apparaît lors d'une erreur, mais la bordure de la fenêtre de login change également de couleur. Pour bien faire, le mieux est de créer des états de validation propres au contrôle LoginBox puis d'afficher les uns ou les autres selon qu'il y ait ou non diffusion d'événements indiquant une erreur. Vous laisserez ainsi au designer le soin de paramétrer le visuel générique de la fenêtre de connexion en cas d'erreur de saisie (voir Figure 12.10).
Tant qu'une erreur de saisie est détectée, le bouton de connexion est désactivé. Vous évitez ainsi de polluer le serveur. Autrement dit, si le format de l'e-mail renseigné est faux de prime abord, autant ne pas soumettre ce dernier à l'approbation du serveur. Du côté C#, il faut légèrement modifier le code afin d'utiliser les états de validation créés auparavant. Dans l'état Invalid, la propriété IsEnabled du bouton de connexion est passée à false :
public
partial
class
LoginBox :
UserControl
{
private
UserCredentials currentUserCredentials =
new
UserCredentials
(
);
public
LoginBox
(
)
{
InitializeComponent
(
);
VisualStateManager.
GoToState
(
this
,
"Default"
,
false
);
formulaire.
DataContext =
currentUserCredentials;
formulaire.
BindingValidationError +=
new
EventHandler<
ValidationErrorEventArgs>(
formulaire_BindingValidationError);
}
void
formulaire_BindingValidationError
(
object
sender,
ValidationErrorEventArgs e)
{
Debug.
WriteLine
(
"diffuseur"
+(
sender as
FrameworkElement).
Name);
if
(
e.
Action ==
ValidationErrorEventAction.
Added)
{
VisualStateManager.
GoToState
(
this
,
"Invalid"
,
true
);
}
else
if
(
e.
Action ==
ValidationErrorEventAction.
Removed)
{
VisualStateManager.
GoToState
(
this
,
"Valid"
,
true
);
}
}
}
Du côté XAML, l'idéal est de définir la propriété NotifyOnValidationError à true pour le contrôle PasswordBox. L'événement est diffusé lorsque la saisie du mot de passe ne correspond pas au format attendu. Dans notre cas, le mot de passe fait au moins quatre caractères. Le code C# ci-dessus permet également de vérifier que les deux conditions, login et mot de passe, sont réunies avant de donner accès à l'état visuel Valid permettant la soumission du formulaire. Le projet est dans le dossier chap12 des exemples du livre : LecteurMultimedia_Validation.zip.
11-2-3. Rafraîchissement manuel de liaison de données▲
Nous avons encore quelques problématiques d'expérience utilisateur. Pour l'instant, les liaisons de données sont rafraîchies de manière automatique. Les composants TextBox et PasswordBox doivent perdre le focus pour rafraîchir leur valeur et permettre ainsi à la liaison de données de lever ou non une erreur. Le mieux serait d'afficher les erreurs de saisie lorsque l'utilisateur clique sur le bouton de connexion. Pour cela, il nous faut configurer des liaisons de données en mode Explicit. Ce mode permet de rafraîchir les valeurs manuellement. Nommez les champs de saisie respectivement emailTxt et passwordTxt. Ensuite, laissez le bouton de connexion actif (IsEnabled=true) quel que soit l'état visuel Valid, Invalid ou Default. Il pourra ainsi être cliquable et rafraîchir les liaisons. Toujours côté XAML, définissez l'attribut UpdateSourceTrigger à Explicit. Voici le code XAML modifié :
<TextBox
x
:
Name
=
"emailTxt"
Text
=
"{Binding Email,
Mode=TwoWay,
NotifyOnValidationError=true,
ValidatesOnExceptions=True,
UpdateSourceTrigger=Explicit}"
/>
<PasswordBox
x
:
Name
=
"passwordTxt"
…
Password
=
"{Binding Password,
Mode=TwoWay,
ValidatesOnExceptions=True,
NotifyOnValidationError=true,
UpdateSourceTrigger=Explicit}"
/>
Le rafraîchissement de la liaison est désormais en mode manuel. Afin de mettre les valeurs, vous devez récupérer la liaison côté C#, puis appelez la méthode UpdateSource. Nous allons le faire lorsque le bouton de connexion diffuse l'événement Click. Nommez ce dernier connection-Button, puis définissez l'écoute de l'événement Click dans le constructeur. Pour finir, récupérez les instances de Binding-Expression au sein de l'écouteur, et appelez leur méthode UpdateSource :
public
LoginBox
(
)
{
InitializeComponent
(
);
VisualStateManager.
GoToState
(
this
,
"Default"
,
false
);
formulaire.
DataContext =
currentUserCredentials;
formulaire.
BindingValidationError +=
new
EventHandler <
ValidationErrorEventArgs>(
formulaire_BindingValidationError);
connectionButton.
Click +=
new
RoutedEventHandler
(
connectionButton_Click);
}
void
connectionButton_Click
(
object
sender,
RoutedEventArgs e)
{
BindingExpression be1 =
emailTxt.
GetBindingExpression
(
TextBox.
TextProperty);
be1.
UpdateSource
(
);
BindingExpression be2 =
passwordTxt.
GetBindingExpression
(
PasswordBox.
PasswordProperty);
be2.
UpdateSource
(
);
}
Cette fois, les liaisons sont rafraîchies lors du clic sur le bouton de connexion et lèvent une erreur, si besoin est. Compilez dans Visual Studio l'application sans le mode Debug via le raccourci Ctrl+F5. Le compilateur n'arrêtera pas l'exécution et vous bénéficierez de l'expérience utilisateur final. L'archive du projet est dans le chap12 des exemples : Lecteur-Multimedia_LiaisonManuelle.zip.
11-2-4. Notification de changement de propriété ▲
Pour le moment, lorsque vous cliquez sur le bouton, aucun mécanisme ne permet de tester réellement si les valeurs saisies par l'utilisateur sont correctes. Pour cela, il suffit d'ajouter quelques membres à notre classe UserCredentials. Il nous faut créer deux champs privés booléens ainsi qu'une troisième propriété publique booléenne. Ces membres représentent respectivement : la validité de l'e-mail (isEmailFilled), celle du mot de passe (isPasswordFilled) et pour finir, la combinaison des deux précédentes (IsCredentials-Filled). Cette dernière valide la totalité du formulaire. Voici le code C# de la classe UserCredentials mise à jour :
public
class
UserCredentials
{
private
string
email;
private
string
password;
private
bool
useProxy =
false
;
private
string
proxyAdress =
String.
Empty;
private
bool
isEmailFilled =
false
;
private
bool
isPasswordFilled =
false
;
private
bool
isCredentialFilled =
false
;
public
bool
IsCredentialsFilled
{
get
{
return
isCredentialFilled;
}
set
{
isCredentialFilled =
value
;
}
}
public
string
Email
{
get
{
return
this
.
email;
}
set
{
Regex myRegex =
new
Regex
(
@"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.
[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))
([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$"
,
RegexOptions.
IgnoreCase);
isEmailFilled =
myRegex.
IsMatch
(
value
);
IsCredentialsFilled =
(
isEmailFilled &&
isPasswordFilled);
if
(!
isEmailFilled)
{
throw
new
Exception
(
"L'adresse email fournie n'est pas valide."
);
}
else
{
this
.
email =
value
;
}
}
}
public
string
Password
{
get
{
return
this
.
password;
}
set
{
isPasswordFilled =
(
value
.
Length >
3
);
IsCredentialsFilled =
(
isEmailFilled &&
isPasswordFilled);
if
(!
isPasswordFilled)
{
throw
new
Exception
(
"Le mot de passe contient au moins 4 caractères."
);
}
else
{
this
.
password =
value
;
}
}
}
public
bool
UseProxy
{
get
{
return
this
.
useProxy;
}
set
{
this
.
useProxy =
value
;
}
}
public
string
ProxyAdress
{
get
{
return
this
.
proxyAdress;
}
set
{
if
(
this
.
useProxy ==
true
)
{
this
.
proxyAdress =
value
;
}
else
{
this
.
proxyAdress =
String.
Empty;
}
}
}
}
Du côté de la classe LoginBox, il nous suffit de tester la valeur de la propriété IsCredentials-Filled. Dans un premier temps, ce test peut être réalisé lorsque l'utilisateur clique sur le bouton de connexion et après rafraîchissement des liaisons de données :
void
connectionButton_Click
(
object
sender,
RoutedEventArgs e)
{
BindingExpression be1 =
emailTxt.
GetBindingExpression
(
TextBox.
TextProperty);
be1.
UpdateSource
(
);
BindingExpression be2 =
passwordTxt.
GetBindingExpression
(
PasswordBox.
PasswordProperty);
be2.
UpdateSource
(
);
if
(
currentUserCredentials.
IsCredentialsFilled)
{
//On fait quelque chose
//si les identifiants sont correctement saisis
}
}
Concrètement, le rafraîchissement des liaisons déclenche la mise à jour des propriétés liées Password et Email de l'instance currentUserCredentials. Celles-ci réaffectent la propriété IsCredentialsFilled au sein de leur accesseur Set. Ce mécanisme est intéressant, mais nous pouvons encore le perfectionner.
Par exemple, il est possible de lier la propriété IsEnabled du bouton de connexion à la propriété IsCredentialsFilled de notre classe UserCredentials. L'utilisateur ne sera pas tenté de cliquer sur le bouton s'il est désactivé lorsque le format des identifiants est incorrect. Le bouton de connexion n'a plus besoin de rafraîchir les liaisons des champs de saisie. De plus, il ne doit plus gérer les erreurs d'identifiant lors de l'écoute de l'événement Click, car ceux-ci seront testés en amont :
void
connectionButton_Click
(
object
sender,
RoutedEventArgs e)
{
//Si le clic est possible, c'est que les identifiants sont
//correctement renseignés. Il n'est donc plus nécessaire de les tester
}
Il est nécessaire de modifier UserCredentials. Jusqu'à maintenant, nous avons évoqué et utilisé la liaison de données à de nombreuses reprises sans réellement nous pencher sur les mécanismes internes fournissant cette fonctionnalité. Les propriétés d'objets n'héritant pas de Dependency-Object, ne possèdent pas cette faculté par défaut, il vous faudra l'implémenter.
Deux méthodologies sont possibles. La première consiste à utiliser des propriétés de dépendance (DependencyProperty) qui contiennent par défaut le mécanisme de liaison. La seconde est d'implémenter l'interface INotifyPropertyChanged. Dans le premier cas, seules les classes héritant de DependencyObject peuvent contenir ce type de propriété. Ce n'est pas une solution adéquate, car elle concerne avant tout les objets graphiques. Nous opterons pour la seconde méthode, car implémenter l'interface est plus simple et donc plus logique, dans le cas d'objets métiers ou de classes non graphiques. Cette interface est avant tout constituée d'un événement de type Property-ChangedEventHandler. Voici son implémentation dans la classe UserCredentials :
public
class
UserCredentials :
INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public
event
PropertyChangedEventHandler PropertyChanged ;
#endregion
Plusieurs propriétés de la classe UserCredentials peuvent désormais utiliser le moteur de liaisons de manière transparente. Pour cela, il suffit de diffuser l'événement lorsque la propriété est modifiée. La bonne pratique consiste à ajouter une méthode gérant la diffusion de l'événement :
public
void
NotifyPropertyChanged
(
string
propertyName)
{
if
(
PropertyChanged !=
null
)
{
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
propertyName));
}
}
Il nous faut maintenant diffuser l'événement dans l'accesseur set de la propriété IsCredentials-Filled via l'appel de la méthode NotifyPropertyChanged :
public
bool
IsCredentialsFilled
{
get
{
return
isCredentialFilled;
}
set
{
isCredentialFilled =
value
;
NotifyPropertyChanged
(
"IsCredentialsFilled"
);
}
}
La propriété IsCredentialsFilled peut désormais être liée à n'importe quelle autre via l'utilisation d'une liaison standard. Dans notre cas, nous pouvons lier la propriété IsEnabled de notre bouton de connexion à IsCredentialsFilled. De cette manière, notre bouton sera actif uniquement lorsque les identifiants seront correctement renseignés. Dans le constructeur de la classe du contrôle utilisateur LoginBox, nous ajoutons une liaison reliant ces deux propriétés :
public
LoginBox
(
)
{
…
Binding b =
new
Binding
(
);
b.
Path =
new
PropertyPath
(
"IsCredentialsFilled"
);
b.
Source =
currentUserCredentials;
b.
Mode =
BindingMode.
OneWay;
connectionButton.
SetBinding
(
Button.
IsEnabledProperty,
b);
Ce n'est pas suffisant. Nous avons paramétré les liaisons des champs de saisie en mode explicite (via l'attribut UpdateSourceTrigger=Explicit). Nous pourrions revenir sur nos pas et les définir en mode automatique. Toutefois cela ne résoudrait pas notre problématique, car elles ne seraient rafraîchies que lorsque les champs perdraient le focus. Contrairement à WPF, la propriété UpdateSourceTrigger n'accepte pas la valeur Property-Changed. Nous devons donc rafraîchir ces liaisons manuellement en écoutant deux événements correspondants à la modification temps réel du mot de passe et de l'identifiant :
public
LoginBox
(
)
{
//…
passwordTxt.
PasswordChanged +=
new
RoutedEventHandler
(
passwordTxt_PasswordChanged);
emailTxt.
TextChanged +=
new
TextChangedEventHandler
(
emailTxt_TextChanged);
}
void
emailTxt_TextChanged
(
object
sender,
TextChangedEventArgs e)
{
BindingExpression be =
emailTxt.
GetBindingExpression
(
TextBox.
TextProperty);
be.
UpdateSource
(
);
}
void
passwordTxt_PasswordChanged
(
object
sender,
RoutedEventArgs e)
{
BindingExpression be =
passwordTxt.
GetBindingExpression
(
PasswordBox.
PasswordProperty);
be.
UpdateSource
(
);
}
Vous pouvez tester l'application. Le mieux est de la compiler dans Visual Studio, sans le débogueur via le raccourci Ctrl+F5. De cette manière, vous n'aurez pas d'arrêt dû aux exceptions levées par les liaisons, lors de l'exécution.
Nous avons abordé divers mécanismes permettant d'indiquer à l'utilisateur une erreur de saisie ou lui évitant de cliquer inutilement sur le bouton de connexion. Ce projet est un cas d'école et permet d'aborder diverses techniques. Toutefois en termes d'ergonomie, choisissez l'une de ces méthodologies, mais pas toutes en même temps : vous risqueriez de perturber ou de noyer votre utilisateur avec trop d'informations visuelles à traiter. L'idéal est de réserver l'état Invalid lorsque les identifiants sont inconnus en base de données et qu'ils ont le bon format. Vous pourriez ajouter un message indiquant une erreur d'identifiants dans cet état. Vous pouvez commenter ou supprimer le code gérant la souscription à l'événement BindingValidationError.
Nous pourrions penser que cette gestion des erreurs est assez compliquée puisqu'il est possible d'utiliser les événements directement et de tester les valeurs en leur sein. Vous pourriez ainsi afficher un visuel spécifique lors des erreurs de formulaire. Bien que cette solution soit possible, vous vous priveriez non seulement de la gestion des erreurs levées par les liaisons de données, mais également de leur gestion visuelle associée nativement au sein des modèles de composants. Ces mécanismes permettent au designer de s'intégrer dans le flux de production sans avoir à élaborer de stratégies avec le développeur.
Le projet est dans l'archive LecteurMultimedia_LiaisonNotification.zip du chap12 des exemples du livre.
11-2-5. Événements personnalisés ▲
Nous avons encore un peu de travail à accomplir pour finaliser notre contrôle utilisateur. Nous devons maintenant concevoir le processus de connexion à l'application et piloter la lecture des transitions de manière pertinente. Lorsque l'utilisateur saisit ses identifiants et demande une connexion, la requête est traitée côté serveur qui valide ou invalide la connexion au service. Comme nous n'avons pas encore abordé l'échange de données client-serveur, nous allons simuler une réponse du serveur. Si la connexion est valide, l'écran d'accueil contenant les astuces devient disponible, dans le cas contraire, nous affichons les états Invalid et Login (voir Figure 12.11).
Comme vous le constatez, il serait bienvenu que notre UserControl LoginBox diffuse un événement de soumission lors de la demande de connexion. De cette manière, une méthode de l'application principale jouera le rôle d'écouteur et sera déclenchée dès la diffusion de ce dernier. Cette méthode a pour objectif d'appeler un service distant sur le serveur dont la réponse est attendue par l'application (via l'utilisation d'une méthode de rappel). À réception de celle-ci, l'application principale pilotera les états visuels du contrôle utilisateur afin d'afficher l'écran d'accueil, ou de rediriger l'utilisateur vers l'état de connexion Invalid dans le cas où les identifiants fournis sont incorrects.
Prenez le projet LecteurMultimedia_LiaisonNotification.zip du chap12 des exemples du livre. La première étape consiste à créer un événement personnalisé. Vous pouvez y parvenir de différentes manières. La première méthode n'est pas très standard : elle consiste à créer une délégation décrivant le contrat de diffusion de l'événement. Il suffit ensuite de créer un événement personnalisé membre de la classe LoginBox du type correspondant à celui défini par la délégation :
public
delegate
void
LeDelegue (
object
param1,
object
nParam);
public
class
LoginBox :
UserControl
{
public
event
LeDelegue UnEvenement;
//une méthode déclenchée commençant par On (Lorsque)
private
void
OnQuelqueChoseArrive (
)
{
if
(
UnEvenement !=
null
)
{
//on déclenche l'événement si un écouteur y a souscrit
UnEvenement
(
UneInstanceDObjet,
UneAutreInstanceDObjet ) ;
}
}
…
}
Cette méthode ne respecte pas vraiment les bonnes pratiques. Par convention la signature de l'événement prend toujours deux arguments. Le diffuseur de l'événement est le premier paramètre. Dans le cas présent, il s'agira de this correspondant à l'instance de la LoginBox diffusant l'événement. Dans le but de faciliter la maintenance, nous partons du principe que l'événement pourrait être diffusé par différents types d'objets. Celui-ci est typé object afin de garantir ce principe. En second paramètre, on passe l'objet événementiel souvent hérité de la classe EventArgs. Il permet d'envoyer des informations pertinentes concernant l'événement lui-même. C'est exactement ce qui arrive lorsque vous écoutez les événements MouseLeftButtonChanged d'un bouton, ou Value-Changed diffusé entre autres, par les instances de Slider. Pour respecter ces conventions, il n'est pas nécessaire de créer une délégation spécifique. Il suffit de créer un événement générique de type EventHandler :
public
class
LoginBox :
UserControl
{
public
event
EventHandler<
SubmitedConnectionEventArgs>
SubmitedConnection;
…
}
Un type générique est une classe dont le nom est toujours suivi de balises entourant le type supporté par l'instance. Lorsque vous procédez de cette manière, il n'y a pas de contrat de délégation à définir, le diffuseur est l'instance en cours et l'objet événementiel correspond à la classe renseignée comme type géré (entre balises inférieur et supérieur). Pour des raisons de simplicité et de performance, l'objet événementiel hérite de la classe EventArgs. L'héritage permet d'ajouter des méthodes, des propriétés ou tout type de données utiles lors du déclenchement de la méthode d'écoute. Dans Visual Studio, ajoutez une classe dans le répertoire classes et nommez-la Submited-ConnectionEventArgs. Voici le code logique décrivant l'objet événementiel :
…
using
LecteurMultiMedia.
classes;
namespace
LecteurMultiMedia.
classes
{
public
class
SubmitedConnectionEventArgs :
EventArgs
{
public
UserCredentials Credentials {
get
;
set
;
}
}
}
Le mieux est encore de déclarer directement une instance de type UserCredentials en tant que membre de la classe SubmitedConnectionEventArgs. De cette manière, nous récupérons directement toutes les informations renseignées depuis le formulaire via la liaison à deux voies. Nous stockons le mot de passe, le login, l'éventuelle URL du proxy saisie par l'utilisateur. Il devient trivial de les récupérer du côté de l'application au sein de MainPage.xaml.cs, puis de les envoyer à un service distant de connexion disponible côté serveur. Voici le code final de LoginBox. Nous déclenchons l'essai de connexion lorsque l'utilisateur relâchera le bouton de connexion. Comme vu précédemment, celui-ci est actif et cliquable dès que le formulaire est correctement renseigné :
void
connectionButton_Click
(
object
sender,
RoutedEventArgs e)
{
if
(
SubmitedConnection !=
null
)
{
SubmitedConnectionEventArgs sce =
new
SubmitedConnectionEventArgs
(
);
sce.
Credentials =
currentUserCredentials;
SubmitedConnection
(
this
,
sce);
}
}
Il nous suffit d'écouter l'événement, puis de récupérer les identifiants de connexion afin de décider d'une éventuelle action à entreprendre :
public
partial
class
MainPage :
UserControl
{
UserCredentials aknowledgeUserCredentials =
new
UserCredentials
(
);
public
MainPage
(
)
{
InitializeComponent
(
);
loginBox.
SubmitedConnection +=
new
EventHandler<
SubmitedConnectionEventArgs>(
loginBox_SubmitedConnection);
}
void
loginBox_SubmitedConnection
(
object
sender,
SubmitedConnectionEventArgs e)
{
string
email =
e.
Credentials.
Email;
string
pass =
e.
Credentials.
Password;
string
proxy =
e.
Credentials.
ProxyAdress;
if
(
email ==
"eric@tweened.org"
&&
pass ==
"eric"
)
{
aknowledgeUserCredentials =
e.
Credentials;
VisualStateManager.
GoToState
(
loginBox,
"Tips"
,
true
);
//supprime le cadre rouge en cas d'identifiants erronés
//lors d'une précédente tentative
VisualStateManager.
GoToState
(
loginBox,
"Valid"
,
true
);
}
else
{
VisualStateManager.
GoToState
(
loginBox,
"Invalid"
,
true
);
}
}
}
Nous simulons un appel serveur, il est évident que toute gestion des identifiants en dur dans le code C# présente un risque de sécurité important, puisqu'un fichier compilé peut-être décompilé par des outils disponibles pour le grand public. L'exemple suivant n'est en aucun cas à suivre, car les identifiants doivent être stockés en base de données distante et gérés dynamiquement.
Lorsque l'événement SubmitedConnection est diffusé par l'instance loginBox, nous vérifions, dans la méthode d'écoute, si le couple d'identifiants e-mail et mot de passe correspond à un utilisateur existant en base. Si oui, nous affichons l'état Tips de la mire de connexion. Dans le cas contraire, nous affichons l'état Invalid décrivant une erreur d'identifiants (voir Figure 12.12).
Nous avons pratiquement terminé la création de notre contrôle utilisateur. Il nous reste à gérer deux comportements : d'une part, la transition vers l'état AdvanceLogin permettant de renseigner un éventuel serveur proxy, d'autre part la disparition du composant LoginBox dans l'application principale afin de laisser apparaître la liste des médias. La première étape est assez simple à réaliser via des comportements, ou du code logique, associés au ToggleButton correspondant. Dans ce cas, nommez le ToggleButton en optionalProxyToggleButton. Le code logique créé est assez simple :
public
LoginBox (
)
{
…
optionalProxyToggleButton.
Checked +=
new
RoutedEventHandler
(
optionalProxyToggleButton_Checked);
optionalProxyToggleButton.
Unchecked +=
new
RoutedEventHandler
(
optionalProxyToggleButton_Unchecked);
}
void
optionalProxyToggleButton_Unchecked
(
object
sender,
RoutedEventArgs e)
{
VisualStateManager.
GoToState
(
this
,
"Login"
,
true
);
;
}
void
optionalProxyToggleButton_Checked
(
object
sender,
RoutedEventArgs e)
{
VisualStateManager.
GoToState
(
this
,
"AdvanceLogin"
,
true
);
}
Concernant la disparition de la fenêtre de connexion, l'idéal est que celle-ci diffuse un événement lorsque le bouton OK de la fenêtre des astuces est cliqué :
public
event
EventHandler<
RoutedEventArgs>
RemoveRequest;
…
public
LoginBox (
)
{
…
removeLoginBoxButton.
Click +=
new
RoutedEventHandler
(
removeLoginBoxButton_Click);
}
void
removeLoginBoxButton_Click
(
object
sender,
RoutedEventArgs e)
{
if
(
RemoveRequest!=
null
)
{
RemoveRequest
(
this
,
e);
}
}
De cette manière, l'application gère la suppression de l'instance de manière optimale.
Nous pourrions décider que ce comportement appartient à la mire de login mais cela poserait certaines problématiques ; l'une d'elles étant que la fenêtre de connexion peut-être contenue par divers types de contrôles qu'il faudra tester pour la supprimer de la liste d'affichage. Les propriétés Child, Content et Children peuvent chacune contenir au moins une instance de UIElement. Notre contrôle utilisateur ne peut pas savoir à l'avance laquelle de ces propriétés contient sa référence, il doit donc tester chaque type de conteneur. De ce point de vue, diffuser un événement et déléguer la gestion de sa suppression à l'application est à la fois plus simple et plus souple en termes de conception. Le code complet de notre application est succinct :
public
partial
class
MainPage :
UserControl
{
UserCredentials aknowledgeUserCredentials =
new
UserCredentials
(
);
public
MainPage
(
)
{
InitializeComponent
(
);
loginBox.
SubmitedConnection +=
new
EventHandler<
SubmitedConnectionEventArgs>(
loginBox_SubmitedConnection);
loginBox.
RemoveRequest +=
new
EventHandler<
RoutedEventArgs>(
loginBox_RemoveRequest);
}
void
loginBox_RemoveRequest
(
object
sender,
RoutedEventArgs e)
{
loginBox.
SubmitedConnection -=
loginBox_SubmitedConnection;
loginBox.
RemoveRequest -=
loginBox_RemoveRequest;
LayoutRoot.
Children.
Remove
(
loginBox);
loginBox =
null
;
VisualStateManager.
GoToState
(
this
,
"Liste"
,
true
);
}
void
loginBox_SubmitedConnection
(
object
sender,
SubmitedConnectionEventArgs e)
{
string
email =
e.
Credentials.
Email;
string
pass =
e.
Credentials.
Password;
string
proxy =
e.
Credentials.
ProxyAdress;
if
(
email ==
"eric@tweened.org"
&&
pass ==
"eric"
)
{
aknowledgeUserCredentials =
e.
Credentials;
VisualStateManager.
GoToState
(
loginBox,
"Tips"
,
true
);
VisualStateManager.
GoToState
(
loginBox,
"Valid"
,
true
);
}
else
{
VisualStateManager.
GoToState
(
loginBox,
"Invalid"
,
true
);
}
}
}
Vous constatez que nous détruisons par principe tous les écouteurs propres aux événements diffusés par la fenêtre de connexion, puis nous supprimons l'instance de LoginBox de la liste d'affichage et passons sa référence à null. De cette manière, le passage du ramasse-miettes (garbage collector) est facilité. Il libère aisément les ressources utilisées par l'instance de LoginBox.
Les contrôles utilisateur possèdent plusieurs qualités importantes en termes de conception. Ils sont faciles à concevoir, à utiliser en production, à partager et à faire évoluer au cours du temps. Il est, par exemple, assez simple de modifier la fenêtre de connexion que nous avons conçue. Remplacer le champ de saisie de l'e-mail ou du proxy par d'autres ne prend pas plus de cinq minutes. Ces contrôles possèdent toutefois quelques désavantages mis en valeur lors de la conception de contrôles personnalisables. Le projet finalisé est dans l'archive LecteurMultimedia_Evene-ment.zip du dossier chap12 des exemples de ce livre.
11-3. Contrôles personnalisables▲
Dans l'exemple précédent, nous avons démontré la souplesse de conception apportée par les contrôles utilisateur. Toutefois, leurs codes logique et déclaratif sont ouverts à la modification sans garde-fou. Le code logique étant accessible et fortement couplé au code déclaratif, une mauvaise manipulation du designer ou de l'intégrateur peut très facilement conduire à des dysfonctionnements. De plus, les contrôles utilisateur ne possèdent pas réellement de style et de modèle proprement dit, alors que les contrôles personnalisables apportent beaucoup de souplesse en la matière.
Les contrôles personnalisables répondent à ces différentes problématiques de manière élégante mais demandent beaucoup plus de réflexion. Lorsque vous créez des contrôles de ce type, vous permettez au designer de concevoir des styles et des modèles pour ces contrôles. Le code logique est inaccessible et seul l'arbre visuel et logique peut être modifié. Cette architecture est un gage de stabilité et de robustesse, car le fond et la forme sont complètement séparés. Toutefois, les contrôles personnalisables s'inscrivent généralement dans des problématiques d'utilisation plus vastes que la production en cours ne le suggère. Ils sont faits pour être réutilisés, mais il est difficile de savoir ce qui peut ou va être réutilisé dans vos futurs projets. Attention donc à ne pas perdre de vue le projet lui-même en vous lançant dans une conception trop éparpillée. Préparez un cahier des charges du composant et ne le dépassez qu'en cas d'extrême nécessité.
Dans cette section, nous allons concevoir deux composants qui permettront de sélectionner une couleur dans un nuancier en mode HSL - hue, saturation, lightness (teinte, saturation, luminosité) - et d'affecter cette dernière à d'autres contrôles.
11-3-1. ColorChooser et ColorPicker▲
Les contrôles ColorChooser et ColorPicker ont des rôles très différents et n'existent ni dans la bibliothèque Silverlight fournie par défaut, ni dans le Silverlight Toolkit. On peut facilement imaginer leur utilité lorsque l'utilisateur doit paramétrer son interface graphique ou tout type de contenu visuel. Comme ils n'existent pas, vous pourriez être amené à en concevoir par nécessité.
Les objectifs de ces composants sont complémentaires. ColorChooser permet à l'utilisateur de choisir une couleur au sein d'un nuancier, il est possible de récupérer la valeur hexadécimale de cette couleur ou une instance de SolidColorBrush. La couleur sélectionnée peut ensuite être affectée à l'une des propriétés de remplissage de tout autre contrôle durant l'exécution. ColorPicker permet quant à lui, de faire apparaître le ColorChooser, qui est plus discret et occupe moins de place dans l'interface. Le ColorPicker récupère un certain nombre de propriétés du ColorChooser. Nous allons concevoir le ColorChooser en premier. Il possède la propriété publique Selected-Color ainsi que l'événement ColorChanged. Ce dernier est diffusé lorsque la couleur est modifiée au sein du nuancier. ColorPicker expose exactement les mêmes membres et en ajoute quelques-uns. Il contient notamment la propriété IsOpen qui permet d'afficher ou non le nuancier. Cette propriété est associée à deux événements FillBoxOpened et FillBoxClosed qui sont diffusés lorsque le ColorChooser apparaît ou disparaît. De plus, ColorPicker possède également la capacité d'associer un style personnalisé à ColorChooser qu'il instancie à la compilation. Cette opération est réalisée grâce à sa propriété publique ColorChooserStyle. La Figure 12.13 expose nos composants finalisés mis en situation.
Concernant la création du dégradé principal, il peut être généré par code via l'écriture directe de pixels au sein d'une instance de type WritableBitmap. Cette technique n'est toutefois pas très optimisée en termes de performance, car il faut réécrire, à l'aide d'un algorithme, une nouvelle image bitmap lorsque la nuance est modifiée par l'utilisateur ou lorsque le contrôle est redimensionné. Une surface de dégradé de 200 pixels par 200 signifie que 40 000 pixels doivent ainsi être redéfinis à chaque fois. Nous prenons donc le parti de créer deux dégradés en dur superposés à un aplat de couleur symbolisant la nuance sélectionnée (voir Figure 12.14).
Cette technique est très utilisée sur le Web depuis de nombreuses années. Toutefois nous n'utiliserons pas de bitmap de taille fixe, mais des instances de pinceaux de dégradé (Linear-Gradient-Brush) affichées par le moteur vectoriel Silverlight. De cette manière, notre composant sera redimensionnable.
11-3-2. Créer des contrôles ▲
Créez un nouveau projet de type Silverlight Application + Website et nommez-le TestCustomControls. Le mieux est d'ouvrir la solution dans Visual Studio si ce n'est pas déjà le cas. Ajoutez un nouveau projet Silverlight de type Class Library à la solution en cours et nommez-le TweenedControls.
Si vous codez avec Expression Blend, ajoutez un projet de type Silverlight Control Library. Il n'est pas conseillé de concevoir le contrôle uniquement dans Expression Blend. La complétion et l'IntelliSense ne sont pas aussi performants que sous Visual Studio. Expression Blend est beaucoup plus efficace pour la conception visuelle de votre composant. L'idéal est de travailler avec ces deux logiciels tout au long du processus de conception.
Supprimez le ou les fichiers générés par défaut à la racine du nouveau projet. Cliquez droit sur le projet et ajoutez un nouvel élément de type "Silverlight Templated Control" (voir Figure 12.15).
Nommez ce dernier ColorPicker, puis validez. Compilez directement l'application pour qu'Expression Blend puisse rafraîchir son interface et prendre en compte le composant généré. Visual Studio a généré un fichier de code logique C# nommé ColorPicker.cs ainsi qu'un fichier nommé generic.xaml contenant du code déclaratif XAML. Celui-ci contient le style et le modèle que le composant ColorPicker utilise par défaut. C'est un dictionnaire de ressources spécifique associé aux contrôles personnalisables conçus dans notre projet. Il est toujours placé dans le répertoire Themes des projets. Du côté C#, la classe ColorPicker hérite de Control, ce qui en fait un contrôle personnalisable. Cette classe fait référence au style appliqué par défaut dans son constructeur via la propriété DefaultStyleKey :
namespace
TweenedControls
{
public
class
ColorPicker :
Control
{
public
ColorPicker
(
)
{
this
.
DefaultStyleKey =
typeof
(
ColorPicker);
}
}
}
Retournez sous Blend et ouvrez le fichier Generic.xaml présent dans le répertoire Themes.
Le panneau Resources liste les styles contenus dans le dictionnaire. Cliquez sur le style (voir Figure 12.16).
Dans l'arbre visuel, cliquez droit sur le style afin de modifier le modèle. Blend a instancié un Border, remplacez-le par une grille nommée Root. Passez en mode de design mixte, cela vous permet d'afficher le XAML :
<Style
TargetType
=
"local:ColorPicker"
>
<Setter
Property
=
"Template"
>
<Setter.Value>
<ControlTemplate
TargetType
=
"local:ColorPicker"
>
<Grid
x
:
Name
=
"LayoutRoot"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Le contenu de ce contrôle peut être apparenté à celui d'un simple bouton. Il ne possède en effet aucun élément logique, une seule instance de Rectangle suffit à son fonctionnement. Il est toutefois nécessaire que l'instance de Rectangle - ou la grille principale - possède un remplissage afin de définir une surface cliquable. Vous pouvez bien sûr concevoir un arbre logique plus élaboré. Répétez les étapes précédentes afin de générer un contrôle de type ColorChooser. Si le style n'est pas créé dans le fichier Generic.xaml, n'hésitez pas à ajouter la balise Style comme ci-dessous :
<Style
TargetType
=
"local:ColorChooser"
>
<Setter
Property
=
"Template"
>
<Setter.Value>
<ControlTemplate
TargetType
=
"local:ColorChooser"
>
<Border
x
:
Name
=
"LayoutRoot"
>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Laissez le composant racine en tant que Border et nommez-le LayoutRoot. Son objectif est de contenir les éléments de l'arbre visuel tout en étant un élément de design. Le dictionnaire de ressources Generic.xaml contient à présent les styles du ColorPicker et du ColorChooser. Nous allons maintenant aborder leur conception à proprement parler.
11-3-3. Le contrat de modèle▲
Un contrat de modèle est un ensemble d'attributs propre au contrôle. Il fournit des indices sur ses capacités ainsi qu'un certain nombre de contraintes à son fonctionnement. Un contrat de modèle est très utile aux graphistes qui souhaitent personnaliser un contrôle. Concrètement ces attributs définissent les états visuels ainsi que les parties logiques du contrôle qui sont liées à son fonctionnement.
11-3-3-1. Gérer les états visuels▲
ColorPicker contient des attributs d'états visuels ainsi qu'un autre, rarement utilisé, nommé Style-TypedProperty. ColorPicker permet d'appliquer un style au contrôle ColorChooser qu'il contient directement via l'interface de Blend. Le contrat de modèle est toujours à placer directement au-dessus de la classe définissant le contrôle personnalisable. Le voici pour Colo-rPicker :
namespace
TweenedControls
{
#region Contrat du modèle
[TemplateVisualState(Name =
"Normal"
, GroupName =
"CommonStates"
)]
[TemplateVisualState(Name =
"MouseOver"
, GroupName =
"CommonStates"
)]
[TemplateVisualState(Name =
"Pressed"
, GroupName =
"CommonStates"
)]
[TemplateVisualState(Name =
"Disabled"
, GroupName =
"CommonStates"
)]
[TemplateVisualState(Name =
"Focused"
, GroupName =
"FocusStates"
)]
[TemplateVisualState(Name =
"Unfocused"
, GroupName =
"FocusStates"
)]
[TemplateVisualState(Name =
"Opened"
, GroupName=
"ColorChooserStates"
)]
[TemplateVisualState(Name =
"Closed"
, GroupName=
"ColorChooserStates"
)]
[StyleTypedProperty(Property =
"ColorChooserStyle"
, StyleTargetType =
typeof
(ColorChooser))]
#endregion
public
class
ColorPicker :
Control
{
Nous reviendrons sur l'utilisation du dernier attribut ultérieurement. Compilez le projet. Ensuite, dans le modèle du ColorPicker, ouvrez le panneau States. Il affiche désormais tous les états visuels que le contrôle gère par défaut. Le designer prend connaissance de ces derniers par une simple lecture du panneau. Arrangez-vous pour définir des noms explicites pour chaque état.
Vous devrez parfois fermer le fichier Generic.xaml, puis ouvrir à nouveau le modèle pour que les attributs soient correctement pris en compte par l'interface d'Expression Blend. Cela est particulièrement vrai pour les attributs définissant des parties logiques de contrôles. N'hésitez pas à recharger entièrement le dictionnaire de ressources et à recompiler l'application sous Blend si nécessaire via le raccourci Ctrl+B.
L'idée est de naviguer entre les différents états visuels en ajoutant un peu de code logique à nos composants. Plutôt que d'écouter directement les événements, il est préférable de surcharger leurs méthodes afin de les gérer en amont. L'événement IsEnabled est le seul qui ne peut être surchargé, il faut donc l'écouter pour accéder aux états correspondants :
public
ColorPicker
(
)
{
this
.
DefaultStyleKey =
typeof
(
ColorPicker);
this
.
IsEnabledChanged +=
new
DependencyPropertyChangedEventHandler
(
this
.
ColorPicker_IsEnabledChanged);
}
#region handler and override
protected
override
void
OnLostFocus
(
RoutedEventArgs e)
{
base
.
OnLostFocus
(
e);
VisualStateManager.
GoToState
(
this
,
"Unfocused"
,
true
);
}
protected
override
void
OnGotFocus
(
RoutedEventArgs e)
{
base
.
OnGotFocus
(
e);
VisualStateManager.
GoToState
(
this
,
"Focused"
,
true
);
}
protected
override
void
OnMouseLeave
(
MouseEventArgs e)
{
base
.
OnMouseLeave
(
e);
VisualStateManager.
GoToState
(
this
,
"Normal"
,
true
);
}
protected
override
void
OnMouseEnter
(
MouseEventArgs e)
{
base
.
OnMouseEnter
(
e);
VisualStateManager.
GoToState
(
this
,
"MouseOver"
,
true
);
}
protected
override
void
OnMouseLeftButtonDown
(
MouseButtonEventArgs e)
{
base
.
OnMouseLeftButtonDown
(
e);
VisualStateManager.
GoToState
(
this
,
"Pressed"
,
true
);
}
private
void
ColorPicker_IsEnabledChanged
(
object
sender,
DependencyPropertyChangedEventArgs e)
{
if
(
this
.
IsEnabled)
{
VisualStateManager.
GoToState
(
this
,
"Normal"
,
true
);
}
else
{
VisualStateManager.
GoToState
(
this
,
"Disabled"
,
true
);
}
}
Le code logique ci-dessus est assez simple et sera similaire pour les deux composants. L'une des différences concerne les états Opened et Closed liés tous deux à la propriété IsOpen du ColorPicker. Nous aborderons la conception de cette propriété ultérieurement.
11-3-3-2. Associer des parties logiques▲
Ouvrez maintenant le fichier de code logique ColorChooser.cs. A contrario du ColorPicker, ColorChooser contient des parties logiques. Afin de les identifier facilement, le mieux est toujours de créer un croquis de votre futur contrôle (voir Figure 12.17).
Pour que ColorChooser fonctionne correctement, il faut qu'au moins quatre composants en fassent partie. Les deux premiers, ColorPickerPanel et HuePickerPanel, sont des panneaux (Panel) qui créent dynamiquement les dégradés. Les deux autres, Color-SelectorThumb et HueSelectorThumb, sont des instances de la classe Thumb qui ont pour avantage de faciliter les opérations de glisser-déposer. L'utilisateur pourra les déplacer à volonté afin de sélectionner la nuance ainsi que la couleur de son choix. Voici le contrat de modèle de ColorChooser :
//Espace de noms nécessaire à l'utilisation de la classe Thumb
using
System.
Windows.
Controls.
Primitives;
namespace
TweenedControls
{
#region Contrat du modèle ColorChooser
[TemplateVisualState(Name =
"Normal"
, GroupName =
"CommonStates"
)]
[TemplateVisualState(Name =
"MouseOver"
, GroupName =
"CommonStates"
)]
[TemplateVisualState(Name =
"Pressed"
, GroupName =
"CommonStates"
)]
[TemplateVisualState(Name =
"Disabled"
, GroupName =
"CommonStates"
)]
[TemplateVisualState(Name =
"Focused"
, GroupName =
"FocusStates"
)]
[TemplateVisualState(Name =
"Unfocused"
, GroupName =
"FocusStates"
)]
[TemplatePart(Name =
"ColorPickerPanel"
, Type =
typeof
(Panel))]
[TemplatePart(Name =
"HuePickerPanel"
, Type =
typeof
(Panel))]
[TemplatePart(Name =
"ColorSelectorThumb"
, Type =
typeof
(Thumb))]
[TemplatePart(Name =
"HueSelectorThumb"
, Type =
typeof
(Thumb))]
#endregion
public
class
ColorChooser :
Control
{
…
L'idéal est de typer les parties de contrôle de la manière la plus large possible. Ce n'est pas réellement possible dans le cas présent, puisque ce sont des fonctionnalités propres aux classes Thumb et Panel qui nous importent.
Compilez le projet dans l'interface d'Expression Blend. Le panneau Parts est mis à jour et affiche les parties logiques du contrôle qui sont nécessaires. Si ce n'est pas le cas, n'hésitez pas à fermer le fichier Generic.xaml et à le rouvrir. Les parties ne sont pas encore associées à d'éventuelles instances d'objet au sein de l'arbre visuel, une coche verte n'est donc pas visible à côté de chacune d'elles (voir Figure 12.18).
Pour associer une partie logique de contrôle à un élément présent dans l'arbre visuel, il est nécessaire de surcharger la méthode OnApplyTemplate héritée de la classe Control. Au sein de cette méthode, nous affecterons un élément de l'arbre à un membre de notre classe via l'appel de la méthode GetTemplateChild. Il nous faut tout d'abord générer deux propriétés privées dont le but sera de gérer la création des dégradés grâce à leur accesseur Set :
//Bitmap du dégradé de nuances
private
WriteableBitmap hueBitmap;
private
Panel huePickerPanel =
null
;
private
Panel HuePickerPanel
{
get
{
return
this
.
huePickerPanel;
}
set
{
this
.
huePickerPanel =
value
;
if
(
this
.
huePickerPanel !=
null
)
{
this
.
CreateHueGradientShape
(
);
}
}
}
//Bitmap du dégradé des HSL (Hue-Saturation-Luminosity)
private
WriteableBitmap colorBitmap;
private
Panel colorPickerPanel =
null
;
private
Panel ColorPickerPanel
{
get
{
return
this
.
colorPickerPanel;
}
set
{
this
.
colorPickerPanel =
value
;
if
(
this
.
colorPickerPanel !=
null
)
{
this
.
CreateLuminositySaturationGradients
(
);
}
}
}
Chaque propriété est associée à une instance de WritableBitmap qui a pour but de photographier le dégradé afin d'en conserver une image bitmap. Grâce à ce mécanisme, nous pouvons récupérer la couleur d'un des pixels selon la position des curseurs de sélection. Nous allons maintenant associer ces propriétés via la méthode GetTemplateChild. Voici le code C# correspondant :
public
override
void
OnApplyTemplate
(
)
{
//on appelle la méthode de la super-classe Control dans un premier temps
base
.
OnApplyTemplate
(
);
this
.
HuePickerPanel =
GetTemplateChild
(
"HuePickerPanel"
) as
Panel;
this
.
ColorPickerPanel =
GetTemplateChild
(
"ColorPickerPanel"
) as
Panel;
}
Lorsque les propriétés privées sont affectées, elles appellent une méthode générant les dégradés. Voici le contenu de cette méthode nommée CreateHueGradientShape :
private
void
CreateHueGradientShape
(
)
{
Shape hueGradientShape =
new
Rectangle
(
);
LinearGradientBrush lgb =
new
LinearGradientBrush
(
);
lgb.
StartPoint =
new
Point
(
0
,
0
.
5
);
lgb.
EndPoint =
new
Point
(
1
,
0
.
5
);
lgb.
GradientStops.
Add
(
new
GradientStop
(
) {
Color =
Color.
FromArgb
(
0xFF
,
0xFF
,
0x00
,
0x00
),
Offset =
0
}
);
lgb.
GradientStops.
Add
(
new
GradientStop
(
) {
Color =
Color.
FromArgb
(
0xFF
,
0xFF
,
0xFF
,
0x00
),
Offset =
0
.
1666666
}
);
lgb.
GradientStops.
Add
(
new
GradientStop
(
) {
Color =
Color.
FromArgb
(
0xFF
,
0x00
,
0xFF
,
0x00
),
Offset =
0
.
3333333
}
);
lgb.
GradientStops.
Add
(
new
GradientStop
(
) {
Color =
Color.
FromArgb
(
0xFF
,
0x00
,
0xFF
,
0xFF
),
Offset =
0
.
5
}
);
lgb.
GradientStops.
Add
(
new
GradientStop
(
) {
Color =
Color.
FromArgb
(
0xFF
,
0x00
,
0x00
,
0xFF
),
Offset =
0
.
66666666
}
);
lgb.
GradientStops.
Add
(
new
GradientStop
(
) {
Color =
Color.
FromArgb
(
0xFF
,
0xFF
,
0x00
,
0xFF
),
Offset =
.
83333333
}
);
lgb.
GradientStops.
Add
(
new
GradientStop
(
) {
Color =
Color.
FromArgb
(
0xFF
,
0xFF
,
0x00
,
0x00
),
Offset =
1
}
);
hueGradientShape.
Fill =
lgb;
huePickerPanel.
Children.
Add
(
hueGradientShape);
Canvas.
SetZIndex
(
hueGradientShape,
-
1
);
}
// currentHueFillShape représente le rectangle situé sous les dégradés
private
Shape currentHueFillShape;
private
void
CreateLuminositySaturationGradients
(
)
{
currentHueFillShape =
new
Rectangle
(
);
currentHueFillShape.
Fill =
new
SolidColorBrush
(
Colors.
Red);
colorPickerPanel.
Children.
Add
(
currentHueFillShape);
Canvas.
SetZIndex
(
currentHueFillShape,
-
1
);
//dégradé transparent blanc
Rectangle wr =
new
Rectangle
(
);
LinearGradientBrush wlgb =
new
LinearGradientBrush
(
);
wlgb.
StartPoint =
new
Point
(
0
,
0
.
5
);
wlgb.
EndPoint =
new
Point
(
1
,
0
.
5
);
wlgb.
GradientStops.
Add
(
new
GradientStop
(
) {
Color =
Color.
FromArgb
(
0xFF
,
0xFF
,
0xFF
,
0xFF
),
Offset =
0
}
);
wlgb.
GradientStops.
Add
(
new
GradientStop
(
) {
Color =
Color.
FromArgb
(
0x00
,
0xFF
,
0xFF
,
0xFF
),
Offset =
.
99
}
);
wr.
Fill =
wlgb;
colorPickerPanel.
Children.
Add
(
wr);
Canvas.
SetZIndex
(
wr,
-
1
);
//dégradé transparent noir
Rectangle br =
new
Rectangle
(
);
LinearGradientBrush blgb =
new
LinearGradientBrush
(
);
blgb.
StartPoint =
new
Point
(
0
.
5
,
1
);
blgb.
EndPoint =
new
Point
(
0
.
5
,
0
);
blgb.
GradientStops.
Add
(
new
GradientStop
(
) {
Color =
Color.
FromArgb
(
0xFF
,
0x00
,
0x00
,
0x00
),
Offset =
0
}
);
blgb.
GradientStops.
Add
(
new
GradientStop
(
) {
Color =
Color.
FromArgb
(
0xFF
,
0x00
,
0x00
,
0x00
),
Offset =
0
.
01
}
);
blgb.
GradientStops.
Add
(
new
GradientStop
(
) {
Color =
Color.
FromArgb
(
0x00
,
0x00
,
0x00
,
0x00
),
Offset =
1
}
);
br.
Fill =
blgb;
colorPickerPanel.
Children.
Add
(
br);
Canvas.
SetZIndex
(
br,
-
1
);
}
Compilez le projet dans Visual Studio, puis dans Blend afin de le recharger complètement. Dans le modèle du ColorChooser, créez une grille en contenant deux autres dans le Border racine. Agrandissez la zone de prévisualisation si nécessaire, placez les deux grilles l'une en dessous de l'autre et définissez-les en tant que partie de contrôle. Au moment où cette opération est réalisée, la méthode OnApplyTemplate est appelée, les grilles sont associées comme propriétés privées de la classe ColorChooser et les dégradés sont générés (voir Figure 12.19).
Comme les dégradés sont créés dynamiquement, ils n'apparaissent pas dans l'arbre visuel et logique. Nous devons recommencer le même travail avec les deux curseurs de sélection. Voici le code logique générant les propriétés privées associées :
private
Thumb colorSelectorThumb=
null
;
private
Thumb ColorSelectorThumb
{
get
{
return
this
.
colorSelectorThumb;
}
set
{
if
(
this
.
colorSelectorThumb !=
null
)
{
this
.
colorSelectorThumb.
DragDelta -=
this
.
ColorSelectorThumb_DragDelta;
}
this
.
colorSelectorThumb =
value
;
if
(
this
.
colorSelectorThumb !=
null
)
{
this
.
colorSelectorThumb.
DragDelta +=
new
DragDeltaEventHandler
(
this
.
ColorSelectorThumb_DragDelta);
}
}
}
private
int
oriX =
0
;
private
int
oriY =
0
;
private
void
ColorSelectorThumb_DragDelta
(
object
sender,
DragDeltaEventArgs e)
{
if
(
this
.
colorBitmap ==
null
)
{
this
.
colorBitmap =
new
WriteableBitmap
(
this
.
colorPickerPanel,
null
);
this
.
colorBitmap.
Pixels[
0
]
=
(
255
<<
24
) |
(
255
<<
16
) |
(
255
<<
8
) |
255
;
}
this
.
oriX +=
(
int
)e.
HorizontalChange;
this
.
oriX =
Math.
Max
(
0
,
this
.
oriX);
this
.
oriX =
Math.
Min
(
this
.
colorBitmap.
PixelWidth -
1
,
this
.
oriX);
this
.
oriY +=
(
int
)e.
VerticalChange;
this
.
oriY =
Math.
Max
(
0
,
this
.
oriY);
this
.
oriY =
Math.
Min
(
this
.
colorBitmap.
PixelHeight -
1
,
this
.
oriY);
this
.
colorSelectorThumb.
Margin =
new
Thickness
(
this
.
oriX -
8
,
this
.
oriY -
8
,
0
,
0
);
this
.
RefreshColorPreview
(
false
);
}
private
Thumb hueSelectorThumb =
null
;
private
Thumb HueSelectorThumb
{
get
{
return
this
.
hueSelectorThumb;
}
set
{
if
(
this
.
hueSelectorThumb !=
null
)
{
this
.
hueSelectorThumb.
DragDelta -=
this
.
hueSelectorThumb_DragDelta;
}
this
.
hueSelectorThumb =
value
;
if
(
this
.
hueSelectorThumb !=
null
)
{
this
.
hueSelectorThumb.
DragDelta +=
new
DragDeltaEventHandler
(
this
.
hueSelectorThumb_DragDelta);
}
}
}
private
int
oriHueX =
0
;
private
void
hueSelectorThumb_DragDelta
(
object
sender,
DragDeltaEventArgs e)
{
if
(
this
.
hueBitmap ==
null
)
{
this
.
hueBitmap =
new
WriteableBitmap
(
this
.
huePickerPanel,
null
);
this
.
hueBitmap.
Pixels[
0
]
=
(
255
<<
24
) |
(
255
<<
16
) |
(
0
<<
8
) |
0
;
}
this
.
oriHueX +=
(
int
)e.
HorizontalChange;
this
.
oriHueX =
Math.
Max
(
0
,
this
.
oriHueX);
this
.
oriHueX =
Math.
Min
(
this
.
hueBitmap.
PixelWidth -
1
,
this
.
oriHueX);
this
.
hueSelectorThumb.
Margin =
new
Thickness
(
this
.
oriHueX -
10
,
0
,
0
,
0
);
int
pixelIndice =
this
.
oriHueX;
string
hexaColor =
Convert.
ToString
(
this
.
hueBitmap.
Pixels[
pixelIndice],
16
).
ToUpper
(
);
byte
r =
Convert.
ToByte
(
hexaColor.
Substring
(
2
,
2
),
16
);
byte
g =
Convert.
ToByte
(
hexaColor.
Substring
(
4
,
2
),
16
);
byte
b =
Convert.
ToByte
(
hexaColor.
Substring
(
6
,
2
),
16
);
SolidColorBrush scb =
new
SolidColorBrush
(
Color.
FromArgb
(
0xFF
,
r,
g,
b));
if
(
this
.
currentHueFillShape !=
null
)
{
this
.
currentHueFillShape.
Fill =
scb;
}
this
.
RefreshColorPreview
(
true
);
}
Dans le code ci-dessus, plusieurs éléments sont à prendre en considération. D'un point de vue global, nous ajoutons deux propriétés privées, ColorSelectorThumb et HueSelectorThumb. Leur but est de garantir le fonctionnement des curseurs de sélection. Au sein de leur attribut Set, nous vérifions si l'instance du curseur n'est pas nulle. Si c'est le cas nous supprimons l'écoute de l'événement DragDelta. Puis nous écoutons cet événement diffusé par le nouvel élément défini comme partie logique. De cette manière, nous évitons de consommer des ressources systèmes inutiles. Le curseur de choix de la nuance subit un déplacement horizontal alors que celui du choix de la couleur peut être déplacé sur les axes x et y. Dans le cas où l'instance de chaque curseur est nulle, le ColorChooser ne fonctionnera pas. C'est tout à fait normal puisque ces propriétés font référence à des parties logiques du contrôle. Si elles sont nulles, c'est que le designer interactif ne les a pas assignées dans Blend.
Vous remarquez que le déplacement des curseurs de sélection est réalisé via l'affectation de la propriété Margin. Cette solution n'est pas idéale, car elle signifie que les conteneurs parents de chacune des instances de Thumb doivent être de type Grid ou posséder cette propriété. Margin n'est pas accessible au sein d'un Canvas, par exemple. L'idéal serait d'utiliser une transformation relative. La bibliothèque ProxyRenderTransform que je mets à disposition sur le portail Code Plex permet de faciliter cette approche. Vous pouvez la télécharger à l'adresse : http://proxyrd.codeplex.com/wikipage?title=Home&ProjectName=proxyrd. Importez la bibliothèque en tant que dll, puis faites référence à l'espace de noms au sein de votre projet. Cette bibliothèque ajoute des méthodes d'extension aux instances d'UIElement. La ligne de code suivante :
this
.
hueSelectorThumb.
Margin =
new
Thickness
(
this
.
oriHueX -
10
,
0
,
0
,
0
);
et celles qui sont similaires peuvent être remplacées par le type d'expression suivante :
this
.
hueSelectorThumb.
SetX
(
this
.
oriHueX);
Dans les deux méthodes gérant les déplacements, nous créons des images bitmap des dégradés, ce sont ces dernières qui vont être utilisées pour récupérer la valeur des pixels :
- this.hueBitmap = new WriteableBitmap(this.huePickerPanel, null);
Le deuxième paramètre du constructeur, de type Transform, permet de prendre en compte une éventuelle transformation appliquée à l'objet. Le fait de passer la valeur null enregistre une image bitmap ne tenant pas compte des transformations appliquées à l'objet. Vous pouvez également créer un agrandissement en passant une matrice de transformation ou une instance de RenderTransform.
Lorsque nous récupérons l'image bitmap de l'instance HuePickerPanel, nous remplaçons la valeur du premier pixel par du rouge. Cela permet d'obtenir un nuancier cohérent :
this
.
hueBitmap.
Pixels[
0
]
=
(
255
<<
24
) |
(
255
<<
16
) |
(
0
<<
8
) |
0
;
Nous procédons de même avec la partie logique ColorPickerPanel, mais en affectant un blanc pur à l'image récupérée. Nous n'entrerons pas dans le détail des explications concernant les opérations sur les bits, il faut juste savoir que les expressions entre parenthèses symbolisent les quatre octets d'un pixel, à savoir l'alpha (transparence), le rouge, le vert et le bleu :
this
.
colorBitmap.
Pixels[
0
]
=
(
255
<<
24
) |
(
255
<<
16
) |
(
255
<<
8
) |
255
Cela peut paraître quelque peu brouillon, mais nous évite bien des complications. Les dégradés sont générés sous forme vectorielle via des instances de LinearGradientBrush, de plus ils sont interprétés par le moteur vectoriel Silverlight. Vous n'avez donc que peu de chances d'obtenir des couleurs primaires pures aux extrémités. Modifier les pixels manuellement nous évite d'utiliser des dégradés sous forme d'images bitmap embarquées.
Lorsque nous déplaçons le curseur des nuances, nous régénérons constamment l'image du dégradé de couleur. Limitez à tout prix la création dynamique de bitmap via la classe WritableBitmap, celle-ci est très puissante, mais consomme des ressources. La méthode RefreshColorPreview, exposée ci-dessous, permet de rafraîchir la couleur en cours de sélection :
private
void
RefreshColorPreview
(
bool
mustGenerate)
{
if
(
mustGenerate)
{
this
.
colorBitmap =
new
WriteableBitmap
(
this
.
colorPickerPanel,
null
);
//on crée un pixel blanc en haut à gauche
//afin d'avoir un blanc pur
this
.
colorBitmap.
Pixels[
0
]
=
(
255
<<
24
) |
(
255
<<
16
) |
(
255
<<
8
) |
255
;
}
int
pixelIndice =
this
.
oriY *
this
.
colorBitmap.
PixelWidth +
this
.
oriX;
string
hexaColor =
Convert.
ToString
(
this
.
colorBitmap.
Pixels[
pixelIndice],
16
).
ToUpper
(
);
this
.
SelectedColor =
Color.
FromArgb
(
0xFF
,
Convert.
ToByte
(
hexaColor.
Substring
(
2
,
2
),
16
)
,
Convert.
ToByte
(
hexaColor.
Substring
(
4
,
2
),
16
)
,
Convert.
ToByte
(
hexaColor.
Substring
(
6
,
2
),
16
)
);
}
Si besoin est, la méthode RefreshColorPreview réactualise l'image bitmap de l'objet Color-PickerPanel. Elle accepte à cette fin un paramètre booléen. Si vous testez le code suivant, plusieurs erreurs sont levées dès la compilation. C'est tout à fait logique, car la propriété Selected-Color de notre composant ColorChooser n'est pas encore déclarée dans notre classe. Commentez SelectedColor dans le code afin d'éviter toute erreur. L'arbre visuel et logique de notre composant est visible à la Figure 12.20.
Le projet est disponible dans l'archive TestCustomControl_ControlParts.zip du chap12 des exemples.
11-3-4. Les propriétés de dépendance▲
À l'instar de son grand frère WPF, Silverlight propose un système de propriétés facilitant l'implémentation de fonctionnalités avancées. Concrètement, ce système ajoute de nouvelles capacités aux propriétés CLR existantes via la création de propriétés de dépendance typées DependencyProperty. Celles-ci permettent le support des liaisons de données au même titre que l'implémentation de l'interface INotifyPropertyChanged que nous avons abordée à la section 12.2 Conception MVVM.
Ce n'est toutefois pas le seul cas où vous utiliserez les propriétés de dépendance. À partir du moment où vous souhaitez qu'une propriété supporte le système d'animation, de style, de notification de changement ou qu'elle possède une valeur par défaut, il devient préférable d'utiliser une propriété de dépendance. Seules les classes héritant de DependencyObject possèdent des propriétés de dépendance. Cela ne suffit toutefois pas à implémenter des liaisons de données. Le concept de liaison de données est avant tout conçu pour mettre en relation le modèle et la vue. De ce point de vue, l'implémentation réelle des mécanismes de liaison via la résolution de valeur de membres et de contextes de données est réalisée par la classe FrameworkElement. Celle-ci est en effet le socle sur lequel repose la création d'interfaces visuelles et contient, de ce fait, les fondations des API de liaison. Le code ci-dessous expose la déclaration d'une propriété de dépendance :
public
int
MyProperty
{
get
{
return
(
int
)GetValue
(
MyPropertyProperty);
}
set
{
SetValue
(
MyPropertyProperty,
value
);
}
}
public
static
readonly
DependencyProperty MyPropertyProperty =
DependencyProperty.
Register
(
"MyProperty"
,
// le nom de la propriété CLR enregistrée
typeof
(
int
),
// le type de la propriété
typeof
(
ownerclass),
//La classe qui possède la propriété
null
);
//définition d'une valeur par défaut et/ou méthode de rappel
Pour coder rapidement ce type de propriété sous Visual Studio, il vous suffit d'écrire propdp puis d'appuyer deux fois la touche Tabulation. Comme vous le constatez, la propriété publique utilise en interne les méthodes SetValue et GetValue qui sont fournies par l'API du système de propriétés Silverlight. Pour des raisons liées à l'initialisation de l'application, les propriétés de dépendance sont statiques. En pratique, la méthode statique Register de la classe DependencyProperty renvoie une propriété de dépendance. Pour cela, la méthode Register accepte comme paramètre, le nom de la propriété CLR publique sous forme de chaîne de caractères, suivi de son type et du type de l'objet qui l'expose.
Dans la grande majorité des cas, la classe qui possède la propriété est celle qui contient la propriété publique. Un dernier paramètre permet de spécifier une valeur par défaut à l'initialisation ainsi qu'une méthode statique de rappel déclenchée lorsque la valeur de la propriété est modifiée. Cela est particulièrement utile lorsque vous souhaitez diffuser des événements ou définir une rétroaction. Ce dernier paramètre n'est toutefois pas obligatoire, il vous suffit de passer null pour l'éluder. Si vous pouvez écrire ces quelques lignes, c'est que la classe dans laquelle vous codez hérite de Dependency-Object. Vous pourriez être tenté de créer un objet métier comme ci-dessous :
namespace
SilverlightApplication1
{
public
class
Customer :
DependencyObject
{
public
bool
IsConnected
{
get
{
return
(
bool
)GetValue
(
IsConnectedProperty);
}
set
{
SetValue
(
IsConnectedProperty,
value
);
}
}
private
static
readonly
DependencyProperty IsConnectedProperty =
DependencyProperty.
Register
(
"IsConnected"
,
typeof
(
bool
),
typeof
(
Customer),
new
PropertyMetadata
(
false
,
new
PropertyChangedCallback
(
OnChangedStatus) ) );
public
static
void
OnChangedStatus
(
DependencyObject dp,
DependencyPropertyChangedEventArgs e)
{
//rétroaction
}
}
}
Comme vous le constatez, le dernier paramètre permet de définir une valeur par défaut, dans notre cas à false, ainsi qu'une méthode statique privée de rappel (callback) déclenchée lorsque la propriété change de valeur. Il n'est pas forcément recommandé d'utiliser cette méthodologie. Comme nous l'avons dit précédemment, hériter de Dependency-Object n'apporte pas la totalité des fonctionnalités. Seule la notification du changement de propriétés est supportée. Il vaut donc mieux implémenter l'interface INotify-Property-Changed pour les objets métiers ou pour ceux qui ne font pas partie de la liste d'affichage. Ceci est une meilleure option, car l'héritage multiple n'est pas supporté par C#, alors que l'implémentation multiple d'interfaces est possible ainsi que largement recommandée. Une fois que vous aurez hérité de DependencyObject, vous ne pourrez pas hériter d'une autre classe, si besoin est et vous devrez peut-être revoir votre conception dans le pire des cas.
Voyons maintenant ce que cela donne pour notre ColorChooser. Il possède une propriété de dépendance SelectedColor de type Color :
[Category(
"Common Properties"
)]
[Description(
"La couleur courante choisie par l'utilisateur."
)]
[EditorBrowsable(EditorBrowsableState.Advanced)]
public
Color SelectedColor
{
get
{
return
(
Color)GetValue
(
SelectedColorProperty);
}
set
{
SetValue
(
SelectedColorProperty,
value
);
}
}
public
static
readonly
DependencyProperty SelectedColorProperty =
DependencyProperty.
Register
(
"SelectedColor"
,
typeof
(
Color),
typeof
(
ColorChooser),
new
PropertyMetadata
(
Colors.
White,
new
PropertyChangedCallback
(
OnSelectedColorChanged))
);
Il est possible de définir des métadonnées à deux niveaux. Vous pouvez en tout premier lieu les placer directement au-dessus de la propriété entre crochets. Ces métadonnées permettent, entre autres, l'apport d'informations complémentaires lisibles dans l'interface d'Expression Blend.
L'attribut Category nous permet de définir la catégorie au sein du panneau des propriétés de Blend, dans lequel la propriété sera accessible : Description offre la possibilité d'afficher un résumé de l'intérêt de cette propriété au survol de celle-ci dans Blend. L'attribut Editor-Browsable permet, quant à lui, de ne pas afficher la propriété dans le panneau, de la rendre visible ou alors de la placer dans une sous-partie d'option avancée accessible via la double flèche (voir Figure 12.21).
La seconde possibilité consiste à définir les métadonnées dans le dernier paramètre de la méthode Register comme nous l'avons évoqué plus haut. Nous y définissons une couleur blanche comme valeur par défaut, car le curseur de sélection est placé en haut à gauche du sélecteur. De plus, la méthode de rappel OnSelectedColorChanged est exécutée chaque fois que la couleur est modifiée. Cette dernière diffuse alors l'événement ColorChanged :
private
static
void
OnSelectedColorChanged
(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
ColorChooser cc =
d as
ColorChooser;
if
(
cc.
ColorChanged !=
null
)
{
ColorChangedEventArgs cce =
new
ColorChangedEventArgs
(
);
cce.
NewValue =
(
Color)e.
NewValue;
cce.
OldValue =
(
Color)e.
OldValue;
cc.
ColorChanged
(
cc,
cce);
}
}
L'objet événementiel de type ColorChangedEventArgs hérite de la classe EventArgs et contient deux propriétés supplémentaires, NewValue et OldValue, vous permettant de récupérer respectivement la nouvelle couleur et l'ancienne couleur sélectionnée. Nous partons du principe que ces propriétés ne doivent pas être accessibles en écriture à l'extérieur de l'espace de noms TweenedControls :
public
class
ColorChangedEventArgs :
EventArgs
{
public
Color NewValue
{
get
;
internal
set
;
}
public
Color OldValue
{
get
;
internal
set
;
}
}
Du côté de la classe ColorPicker, la propriété est définie de manière similaire. Toutefois l'implémentation est légèrement différente, car la classe ColorPicker possède une instance de ColorChooser et écoute l'événement ColorChanged de cette instance afin de modifier sa propre propriété SelectedColor :
public
ColorPicker
(
)
{
…
this
.
colorChooser.
ColorChanged +=
new
EventHandler<
ColorChangedEventArgs>(
this
.
colorChooser_ColorChanged);
}
private
void
colorChooser_ColorChanged
(
object
sender,
ColorChangedEventArgs e)
{
this
.
SelectedColor =
e.
NewValue;
}
Le principe mis en œuvre est une délégation au sens propre du terme. Il reste encore une problématique à régler : il faut donner au concepteur la capacité de définir un style sur le ColorChooser contenu par le ColorPicker. Il est nécessaire de créer une propriété de dépendance à cette fin :
[Category(
"Common Properties"
)]
[Description(
"The current style applied to the ColorPicker."
)]
[EditorBrowsable(EditorBrowsableState.Advanced)]
public
Style ColorChooserStyle
{
get
{
return
(
Style)GetValue
(
ColorChooserStyleProperty);
}
set
{
SetValue
(
ColorChooserStyleProperty,
value
);
}
}
public
static
readonly
DependencyProperty ColorChooserStyleProperty =
DependencyProperty.
Register
(
"ColorChooserStyle"
,
typeof
(
Style),
typeof
(
ColorPicker),
new
PropertyMetadata
(
OnColorChooserStyleChanged));
private
static
void
OnColorChooserStyleChanged
(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
Style s =
e.
NewValue as
Style;
ColorPicker cp =
d as
ColorPicker;
if
(
cp !=
null
)
{
Style os =
e.
OldValue as
Style;
cp.
colorChooser.
Style =
s;
}
}
Cette solution est tout à fait cohérente, mais nous n'avons pas fini. À ce stade, un développeur peut facilement affecter un style au ColorChooser. Un designer le pourra également en passant par le panneau des propriétés, bien que cela ne soit pas habituel pour lui. Là encore, il vous faut vous arranger pour que le designer puisse, via Expression Blend, appliquer un style de son choix en passant par les menus habituels ou par un clic droit. Il vous faut pour cela ajouter une métadonnée au-dessus de la classe ColorPicker, et recompiler, voire reconstruire le projet sous Blend. L'attribut StyleTypedProperty responsable de cette intégration est exposé ci-dessous :
[StyleTypedProperty(Property =
"ColorChooserStyle"
,
StyleTargetType =
typeof
(
ColorChooser))]
public
class
ColorPicker :
Control
{
Concrètement, vous pouvez désormais accéder au style de manière directe et standard dans Blend par le menu Object de la barre du haut ou via un simple clic droit (voir Figure 12.22).
Vous avez encore un peu de travail pour finaliser les deux composants par vous-même. Vous pouvez toutefois télécharger la version finalisée, TestCustomControl, dans le chap12 des exemples.
Au prochain chapitre, nous aborderons la lecture de médias à travers l'utilisation de différents contrôles utilisateur spécialement créés pour l'occasion. Nous étudierons ensuite le chargement de données distantes au sein d'un projet basé sur le modèle de conception MVVM.
12. Médias et données▲
Parmi tous les principes apportés par le développement Web, le plus représentatif est sans aucun doute le comportement asynchrone lié au chargement de contenus distants. Celui-ci est toujours présent et brille soit par son absence de prise en charge, soit par un support étendu qui améliore la qualité et l'intérêt des applications. Il est au cœur de nombreuses problématiques de production qui se sont complexifiées avec l'évolution rapide des besoins. L'un d'eux concerne l'échange de données entre, d'une part, les applications clientes, et d'autre part, les applications ou services distants hébergés côté serveur. Les technologies dites asynchrones comme AJAX, Flash ou dernièrement Silverlight y apportent des réponses plus ou moins efficaces et élégantes. La technologie AJAX (pour Asynchronous JavaScript and XML) est en fait résumée par un seul objet asynchrone, il ne peut donc pas se positionner au même niveau qu'une plate-forme comme Silverlight qui possède de nombreux objets et méthodes asynchrones. Ce chapitre est consacré à l'apprentissage des mécanismes propres au chargement et à l'envoi de données. Nous aborderons tout d'abord le chargement de médias externes de type bitmap et vidéo, puis nous étudierons les contraintes liées à leur diffusion. Par la suite, nous apprendrons à concevoir une application orientée données, en nous basant sur le modèle de conception MVVM. Puis, nous apprendrons à charger et à consommer des flux de données au format JSON et XML via différentes technologies dont LINQ. Pour finir, nous aborderons les principes de base inhérents à la sécurité et à l'échange d'informations interdomaines.
12-1. Chargement de médias▲
Dans cette section, vous allez apprendre à télécharger des images. Nous créerons à cette occasion une classe qui centralisera la logique de chargement et diffusera des événements personnalisés, ainsi qu'une vue gérant l'affichage de l'image. Dans un second temps, nous allons reprendre le lecteur vidéo créé au Chapitre 6 Boutons personnalisés pour diffuser de la vidéo. Nous en profiterons pour en faire un contrôle réutilisable.
12-1-1. Chargement dynamique d'images▲
Deux formats d'image, JPG et PNG, sont actuellement supportés par Silverlight. En règle générale, la compression JPG est efficace lorsque vous n'avez pas besoin de couche d'opacité ainsi que pour des résolutions supérieures à 128 pixels de largeur par 128 pixels de hauteur. Le format PNG possède la capacité d'être défini par une palette de couleurs indexées 8 bits (256 couleurs) ou 24 bits (32 millions de couleurs) ainsi qu'un mode "couleurs véritables non indexées". Lorsque le fichier PNG est en codé en 8 bits, son poids est relativement léger pour des images de faible résolution, tout en conservant une qualité optimale. Ce format permet en outre d'obtenir une intégration simplifiée de l'image au sein de l'arbre visuel et logique d'une application via sa couche de transparence. De ce point de vue, son utilisation est pertinente lorsque vous souhaitez afficher ou concevoir des icônes. Quel que soit le format utilisé, charger dynamiquement des images se révèle payant à plus d'un titre. D'une part, cela allège le poids de votre application, car les images ne sont pas stockées dans le fichier compilé mais directement sur le serveur. D'autre part, cela dynamise vos sites et vos applications, les rendant ainsi plus attractifs. Pour finir, cela facilite leur maintenance et leur évolution, car une partie de la maquette graphique peut être gérée indépendamment de l'utilisation de Blend.
12-1-1-1. La classe BitmapImage▲
Nous avons vu à plusieurs reprises que le composant Image possède la capacité d'afficher des images bitmap. Dans ce cas, les images peuvent au choix être compilées au sein du fichier xap ou être accessibles directement sur un serveur Web. Dans ce dernier cas, les fichiers seront dans le même répertoire que le xap ou dans un autre. À noter que dans le cas d'un référencement relatif, le chemin de l'image est calculé à partir de l'emplacement du xap. Si vous ajoutez des images via le menu Add Existing Item, celles-ci seront par défaut compilées dans le fichier xap. Il est possible de modifier ce comportement au sein de Visual Studio en changeant les options. Dans tous les cas, il est possible de définir la propriété Source directement en XAML à partir du moment où les images sont embarquées ou placées dans le répertoire adéquat :
<Image Source="monImage.jpg" />
Du côté C#, cela est légèrement moins évident lorsque l'on n'utilise pas la classe Bitmap-Image. Il nous faudra convertir la chaîne de caractères en instance de type ImageSource comme montré ci-dessous :
public
partial
class
MainPage :
UserControl
{
string
urlImage =
"http://www.tweened.org/wp-content/files/livre/images/lapin-cretin.jpg"
;
public
MainPage
(
)
{
InitializeComponent
(
);
ImageSourceConverter isc =
new
ImageSourceConverter
(
);
ImageSource imgSource =
isc.
ConvertFromString
(
urlImage) as
ImageSource;
uneInstanceImage.
Source =
imgSource;
}
}
Il est possible d'écouter les événements ImageOpened et ImageFailed respectivement diffusés lorsque l'image est correctement chargée ou, au contraire, quand son chargement échoue :
public
partial
class
MainPage :
UserControl
{
string
urlImage =
" http://www.tweened.org/wp-content/files/livre/images/lapin-cretin.jpg "
;
public
MainPage
(
)
{
// Required to initialize variables
InitializeComponent
(
);
ImageSourceConverter isc =
new
ImageSourceConverter
(
);
ImageSource imgSource =
isc.
ConvertFromString
(
urlImage) as
ImageSource;
uneInstanceImage.
ImageFailed +=
new
EventHandler<
ExceptionRoutedEventArgs>(
uneInstanceImage_ImageFailed);
uneInstanceImage.
ImageOpened +=
new
EventHandler<
RoutedEventArgs>(
uneInstanceImage_ImageOpened);
uneInstanceImage.
Source =
imgSource;
}
void
uneInstanceImage_ImageOpened
(
object
sender,
RoutedEventArgs e)
{
TextBlock t =
new
TextBlock
(
);
t.
Text =
"L'image "
+
urlImage +
" a été chargée."
;
LayoutRoot.
Children.
Add
(
t);
}
void
uneInstanceImage_ImageFailed
(
object
sender,
ExceptionRoutedEventArgs e)
{
throw
new
NotImplementedException
(
"Le format et/ou le chemin d'accès de l'image "
+
urlImage +
" sont incorrects."
);
}
}
Le code ci-dessus ne fonctionne qu'à une seule condition : il vous faut exécuter l'application en environnement http lorsque le chemin d'accès est une URL absolue commençant par http://localhost:49188/TestPage.html. Autrement dit, sous Blend vous n'aurez aucun problème puisque ce dernier lance systématiquement l'application via une instance du serveur de développement. Sous Visual Studio, si vous n'êtes pas dans un projet Silverlight 3 + Website ou utilisant une page ASP.NET, le chemin d'accès au site sera du type file:///C:/Users/votre-nom/Desktop/test/test/Bin/Debug/TestPage.html. Dans ce cas, l'événement ImageFailed sera diffusé indiquant l'échec du chargement. Cela est lié à des contraintes de sécurité. Une dernière restriction au chargement d'images concerne leur emplacement. Il n'est pas possible d'utiliser un chemin d'accès relatif du type ../images/uneImage.jpg, et cela également pour des raisons de sécurité. Ce n'est pas un problème puisqu'il est possible de récupérer l'URL absolue du site, puis de concaténer le bon chemin d'accès aux images afin de passer une URL de type absolue, comme montré ci-dessous :
string
SitePath =
""
;
public
MainPage
(
)
{
InitializeComponent
(
);
if
(
App.
Current.
Host.
Source !=
null
)
{
SitePath =
new
Uri
(
App.
Current.
Host.
Source,
"../"
).
ToString
(
)
}
…
Notre conception précédente est trop directe et n'est pas idéale pour différentes raisons. D'une part, nous ne pouvons pas surveiller l'état de téléchargement de l'image distante, d'autre part, nous n'avons pas accès simplement aux dimensions natives de l'image à la réception de celle-ci. La classe BitmapImage résout ces deux problématiques assez simplement en donnant accès à des propriétés et capacités supplémentaires (voir Tableau 13.1).
Méthode asynchrone |
Propriétés |
Événements |
---|---|---|
SetSource(Stream) |
UriSource |
ImageFailed |
Non documenté |
PixelWidth |
ImageOpened |
Non documenté |
PixelHeight |
DownloadProgress |
Non documenté |
CreateOptions |
Non documenté |
Comme vous le constatez, tous les ingrédients sont présents pour assurer la gestion d'un chargement dynamique. Pour finir, la classe BitmapImage n'est pas un objet de type UI-Element. Les instances de ce type ne peuvent donc pas être ajoutées à la liste d'affichage de notre application Silverlight. Vous devrez utiliser un composant Image ou un pinceau d'image, ImageBrush, pour afficher cette dernière. Le code ci-dessous illustre un exemple concret d'utilisation de la classe BitmapImage :
…
using
System.
Windows.
Media.
Imaging;
namespace
SimpleBitmapLoader
{
public
partial
class
MainPage :
UserControl
{
public
MainPage
(
)
{
InitializeComponent
(
);
Uri uri =
new
Uri
(
"http://www.tweened.org/wp-content/files/livre/images/lapin-cretin.jpg"
,
UriKind.
RelativeOrAbsolute);
BitmapImage bi =
new
BitmapImage
(
uri);
Image img =
new
Image
(
);
img.
Source =
bi;
LayoutRoot.
Children.
Add
(
img);
}
}
}
Comme nous l'avons évoqué plus haut, il est également possible d'afficher l'image en utilisant une instance de type ImageBrush :
//Chargement d'une image via l'utilisation d'un pinceau d'image
Uri uri =
new
Uri
(
"http://www.tweened.org/wp-content/files/livre/images/lapin-cretin.jpg"
,
UriKind.
RelativeOrAbsolute);
BitmapImage bi =
new
BitmapImage
(
uri);
ImageBrush ib =
new
ImageBrush
(
);
ib.
ImageSource =
bi;
LayoutRoot.
Background =
ib;
Il n'y a pas vraiment de différences flagrantes entre l'une ou l'autre de ces méthodes. Seul le mode d'étirement est différent : il est par défaut en mode Fill pour un pinceau d'image, alors qu'il est de type Uniform par défaut pour les instances d'Image. Le rendu final est assez différent puisqu'en mode Fill, l'image est étirée afin de remplir la totalité de la surface mise à disposition. En outre, d'un point de vue réutilisation, le pinceau d'image est avantageux, car il peut être affecté à n'importe quelle propriété acceptant une instance de type Brush, tout en étant stocké une seule fois en mémoire. Le projet est dans l'archive SimpleBitmapLoader.zip du dossier chap13 des exemples.
12-1-1-2. Conception du contrôle ImageLoader▲
Nous allons maintenant approfondir l'utilisation de la classe BitmapImage à travers la conception d'un contrôle personnalisé. Le but est de simplifier le chargement d'images à travers la réutilisation de ce composant. Nous implémenterons ainsi la quasi-totalité des fonctionnalités possibles dont le préchargement, la gestion des erreurs, la diffusion d'événements et la récupération des dimensions de l'image téléchargée. Au sein de Blend, générez un projet Silverlight + Website nommé TestMediaCustomControls. Ouvrez la solution dans Visual Studio, puis ajoutez dans la solution un autre projet de type "Silverlight Class Library", MediaTweenedControls. Supprimez la classe créée par défaut et insérez un nouvel élément de type "Silverlight Templated Control" (voir Figure 13.1).
Nommez ce composant ImageLoader, puis validez en cliquant sur le bouton Add. Dans la classe ImageLoader, définissez le contrat de modèle ainsi que les événements comme indiqué dans le code ci-dessous :
[TemplateVisualState(GroupName =
"CommonStates"
, Name =
"Normal"
)]
[TemplateVisualState(GroupName =
"CommonStates"
, Name =
"MouseOver"
)]
[TemplateVisualState(GroupName =
"CommonStates"
, Name =
"Pressed"
)]
[TemplateVisualState(GroupName =
"CommonStates"
, Name =
"Disabled"
)]
[TemplateVisualState(GroupName =
"LoadingStates"
, Name =
"Unloaded"
)]
[TemplateVisualState(GroupName =
"LoadingStates"
, Name =
"Loading"
)]
[TemplateVisualState(GroupName =
"LoadingStates"
, Name =
"Completed"
)]
public
class
ImageLoader :
Control
{
#region événements
public
event
EventHandler<
ExceptionRoutedEventArgs>
ImageFailed;
public
event
EventHandler<
RoutedEventArgs>
ImageOpened;
public
event
EventHandler<
DownloadProgressEventArgs>
DownloadProgress;
#endregion
…
Outre les états interactifs standard, nous en décrivons trois autres qui correspondent à l'état du chargement de l'image. Cela permettra au designer d'élaborer un visuel spécifique à chacun d'eux. Il pourra ainsi afficher ou faire disparaître la barre de progression, et générer des transitions de fondu de l'image. Modifiez le modèle du composant afin d'obtenir l'arbre visuel de la Figure 13.2.
Le composant ProgressBar hérite de RangeBase, comme Slider. Nous ne forçons pas sa création comme partie logique, car sa propriété Value est liée la propriété de dépendance Progress que nous définissons sur ImageLoader :
[Description(
"Progression du téléchargement de l'image de 0 à 100."
)]
public
double
Progress
{
get
{
return
(
double
)GetValue
(
ProgressProperty);
}
private
set
{
SetValue
(
ProgressProperty,
value
);
}
}
public
static
readonly
DependencyProperty ProgressProperty =
DependencyProperty.
Register
(
"Progress"
,
typeof
(
double
),
typeof
(
ImageLoader),
null
);
Vous remarquez que nous n'autorisons pas le designer ou le développeur à modifier cette propriété sur l'instance de ImageLoader. Nous définissons pour cela son accesseur set en accès privé. Cette propriété est en fait liée à l'état du téléchargement, il ne serait donc pas logique de la définir manuellement. D'un autre côté, il peut être très utile d'y avoir accès en lecture. Comme nous l'avons indiqué plus haut, la propriété Value de notre instance de ProgressBar y est reliée via une liaison de modèles, toutefois celle-ci n'est pas visible dans l'interface de Blend, car son écriture est en accès privé (voir Figure 13.3).
Il est toutefois possible de définir la liaison directement en XAML, puisque la propriété est accessible en lecture :
- <ProgressBar x:Name="progressBar" Value="{TemplateBinding Progress}" … />
Vous pourriez être tenté d'utiliser l'attribut suivant pour résoudre cette problématique :
- [EditorBrowsable( EditorBrowsableState.Always )]
Cela ne changera strictement rien, la propriété ne sera pas accessible pour autant dans l'interface de Blend. La conséquence directe de ce comportement est que le designer ne pourra pas créer une liaison de modèles simplement vers la propriété Progress, car l'interface ne lui indique pas son existence. C'est au développeur de choisir l'implémentation idéale. Mais il est possible de trouver un compromis. Vous pouvez définir cette propriété en écriture tout en la cachant dans l'onglet des options avancées. Il suffit pour cela de définir l'attribut EditorBrowsable comme indiqué ci-dessous :
[EditorBrowsable( EditorBrowsableState.Advanced )]
Dans l'avenir, nous pourrions imaginer que les propriétés de dépendance pourraient être visibles dans l'interface lorsqu'elles sont en lecture seule… D'autres propriétés sont dans le même cas, comme la propriété LoadedBitmapImage, qui correspond à l'instance BitmapImage contenant les données de l'image chargée. Il en va de même pour OriginalBitmapSize qui permet de récupérer les dimensions natives de l'image chargée. Voici leur implémentation :
//Bitmap Data
[Description(
"Données de la bitmap chargée."
)]
[EditorBrowsable(EditorBrowsableState.Advanced)]
[Category(
"Common Properties"
)]
public
BitmapImage LoadedBitmapImage
{
get
{
return
(
BitmapImage)GetValue
(
LoadedBitmapImageProperty);
}
set
{
SetValue
(
LoadedBitmapImageProperty,
value
);
}
}
public
static
readonly
DependencyProperty LoadedBitmapImageProperty =
DependencyProperty.
Register
(
"LoadedBitmapImage"
,
typeof
(
BitmapImage),
typeof
(
ImageLoader),
null
);
//Dimensions d'origine
[Description(
"Dimension d'origine de l'image chargée."
)]
[EditorBrowsable( EditorBrowsableState.Advanced )]
[Category(
"Common Properties"
)]
public
Size OriginalBitmapSize
{
get
{
return
(
Size)GetValue
(
OriginalBitmapSizeProperty);
}
set
{
SetValue
(
OriginalBitmapSizeProperty,
value
);
}
}
public
static
readonly
DependencyProperty OriginalBitmapSizeProperty =
DependencyProperty.
Register
(
"OriginalBitmapSize"
,
typeof
(
Size),
typeof
(
ImageLoader),
null
);
Il nous faut également exposer une propriété de dépendance Source qui contiendra une URL pointant vers l'image à afficher. Cette propriété définit une méthode de rappel déclenchée lorsque sa valeur est modifiée :
//chemin d'accès à l'image
[Category(
"Common Properties"
)]
[Description(
"Url de l'image à charger."
)]
public
string
Source
{
get
{
return
(
string
)GetValue
(
SourceProperty);
}
set
{
SetValue
(
SourceProperty,
value
);
}
}
public
static
readonly
DependencyProperty SourceProperty =
DependencyProperty.
Register
(
"Source"
,
typeof
(
string
),
typeof
(
ImageLoader),
new
PropertyMetadata
(
new
PropertyChangedCallback
(
OnSourceUriChanged)));
private
static
void
OnSourceUriChanged
(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
ImageLoader il =
d as
ImageLoader;
Uri uri =
new
Uri
(
e.
NewValue as
string
,
UriKind.
RelativeOrAbsolute);
il.
LoadedBitmapImage =
new
BitmapImage
(
uri);
il.
LoadedBitmapImage.
CreateOptions =
BitmapCreateOptions.
IgnoreImageCache;
il.
LoadedBitmapImage.
DownloadProgress +=
new
EventHandler<
DownloadProgressEventArgs>(
il.
LoadedBitmapImage_DownloadProgress);
il.
LoadedBitmapImage.
ImageOpened +=
new
EventHandler<
RoutedEventArgs>(
il.
LoadedBitmapImage_ImageOpened);
il.
LoadedBitmapImage.
ImageFailed +=
new
EventHandler<
ExceptionRoutedEventArgs>(
il.
LoadedBitmapImage_ImageFailed);
VisualStateManager.
GoToState
(
il,
"Loading"
,
true
);
}
Comme vous le constatez, aucune partie logique n'est nécessaire pour afficher l'image. Nous n'avons pas besoin d'utiliser du code logique pour affecter LoadedBitmapImage au contrôle Image nommé ImageContainer présent dans l'arbre visuel et logique de notre modèle. Nous n'avons qu'à définir une liaison de modèles entre la propriété Source de l'objet ImageContainer et la propriété de dépendance LoadedBitmapImage (voir Figure13.4).
En procédant ainsi, le designer reste libre et peut modifier plus facilement le contrôle. L'utilisation du contrôle est au final très simple :
public
MainPage
(
)
{
InitializeComponent
(
);
il.
ImageFailed +=
new
EventHandler<
ExceptionRoutedEventArgs>(
il_ImageFailed);
il.
ImageOpened +=
new
EventHandler<
RoutedEventArgs>(
il_ImageOpened);
il.
Source =
"http://www.tweened.org/wp-content/files/livre/images/lapin-cretin.jpg"
;
}
void
il_ImageOpened
(
object
sender,
RoutedEventArgs e)
{
//on peut récupérer la taille originale de l'image par exemple
//il.Width = il.OriginalBitmapSize.Width+4;
//il.Height = il.OriginalBitmapSize.Height+4;
}
void
il_ImageFailed
(
object
sender,
ExceptionRoutedEventArgs e)
{
throw
e.
ErrorException;
}
Le projet complet est dans l'archive TestMediaCustomControl.zip du dossier chap13 des exemples. Le composant finalisé est également téléchargeable sur le blog www.tweened.org. Il possède des capacités supplémentaires qui faciliteront vos développements.
12-1-2. Formats vidéo et modes de diffusion▲
Le chargement de contenu vidéo est l'un des principaux enjeux commerciaux des plates-formes de diffusion plurimédia telles que Silverlight. Ce dernier fournit un support natif de divers formats d'encodage, ce qui le place en tête des solutions de diffusion vidéo pour le Web. Ainsi, lorsque vous concevez un lecteur vidéo pour Silverlight, vous bénéficiez d'un certain nombre de puissants formats optimisés pour l'environnement Internet. Cela évite à l'internaute de fastidieuses mises à jour logicielles tout en simplifiant l'élaboration de lecteurs vidéo sophistiqués. Au sein de la plate-forme Silverlight, plusieurs conteneurs et formats sont disponibles. Un conteneur est un fichier qui contient des données vidéo et/ou audio encodées dans divers formats. Il a pour rôle de référencer ces données en décrivant leur mode de stockage. Il contient également des métadonnées de description des médias. Il est ainsi possible de stocker des informations concernant le chapitrage, le copyright, le nom de l'auteur ou toute autre information liée aux médias contenus. Chaque conteneur possède des capacités propres et n'accepte qu'un nombre limité de formats d'encodage.
Vous devrez toutefois faire la différence entre conteneur et extensions de fichiers, l'extension ne joue finalement pas de rôle à proprement parler alors que le format du conteneur structure de manière spécifique les métadonnées. Le conteneur MOV en est le parfait exemple : ce conteneur est polymorphe. Il n'est théoriquement pas supporté par Silverlight, toutefois si ce dernier contient des données encodées au bon format, il sera lu, interprété et affiché correctement par ce dernier. Voici la liste des conteneurs et de quelques formats supportés par Silverlight :
- Windows Media Video et Audio (wmv et wma) ;
- "353" - Microsoft Windows Media Audio v7 v8 and v9.x Standard (WMA Standard) ;
- "354" - Microsoft Windows Media Audio v9.x and v10 Professional (WMA Professional) ;
- WMV1 (Windows Media Video 7) ;
- WMV2 (Windows Media Video 8) ;
- WMV3 (Windows Media Video 9) ;
- MP4 ;
- H.264 (ITU-T H.264 / ISO MPEG-4 AVC), AAC-LC ;
- MP3 ;
-
"85" - ISO MPEG-1 Layer III (MP3).
Certaines associations d'encodage ne sont pas supportées par Silverlight. Par exemple, il ne supporte pas l'association d'un format vidéo WMV (version 1, 2 et 3) avec un encodage de bande sonore de type MP3 ou AAC. Vous trouverez une liste complète des formats supportés à l'adresse : http://msdn.microsoft.com/en-us/library/cc189080%28VS.95%29.aspx.
En résumé, il est fortement conseillé d'encoder vos vidéos au sein d'un conteneur de type MP4. Ce conteneur vous apporte le meilleur rapport qualité d'image / bande passante à travers le format de compression vidéo H.264 et le format audio AAC. Il est toutefois très fréquent de récupérer des formats de ce type contenus dans une extension MOV ou un conteneur MOV. Vérifiez simplement que le type d'encodage correspond à celui d'un conteneur de type MP4 (soit H.264 et AAC). Lorsque vous utilisez la propriété Source de l'objet MediaElement, celui-ci ne vous propose pas les extensions .mov. Il est toutefois possible de renseigner le chemin d'accès au fichier manuellement, dans ce cas le fichier MOV est lu sans aucun problème. Silverlight 3 supporte différents standard du H.264, qui correspondent chacun à autant de résolutions listées ci-dessous. Attention au fait que Silverlight est capable de lire des vidéos dans n'importe quelles résolutions. Celles indiquées ci-dessous correspondent uniquement à des formats d'enregistrement et de diffusion :
-
480p n'est pas vraiment utilisé et cible les résolutions 4/3 et 16/9 ayant un total de 480 lignes et de 480 colonnes ;
-
720p correspond à la norme HD Ready d'un format 16/9 en 1 280 C 720 pixels ;
-
1 080p fait référence à la norme Full HD d'un format 16/9 en 1 920 C 1 080 pixels.
Une fois que vous aurez choisi un type d'encodage, il vous faudra déterminer un mode de diffusion adapté à vos besoins et à vos ressources. Vous devrez choisir entre une diffusion de type progressif ou continu. Cette décision technologique impactera directement vos budgets ainsi que votre production.
Le mode de téléchargement progressif est le plus couramment rencontré sur Internet. Il consiste à placer un média sur votre serveur et à le lire en cours de téléchargement sur le disque local. Dans ce cas, le protocole utilisé est HTTP, et aucune technologie côté serveur n'est nécessaire mise à part un simple serveur Web (voir Figure 13.5). Cette solution est peu coûteuse en termes d'argent et de temps de développement mais diminue la qualité de l'expérience utilisateur. À l'origine, il n'était pas possible de déplacer la tête de lecture jusqu'à un temps donné si la vidéo n'avait pas été téléchargée jusqu'à cet instant. Ce comportement a évolué avec l'amélioration des serveurs Web et des plates-formes de diffusion côté client.
La diffusion en continu, également appelée streaming, apporte de nombreux avantages mais nécessite Windows Media Server - un module de Internet Information Service 7. II est disponible sur Windows Server 2008, Vista et Windows 7. Dans ce mode de diffusion, il est possible pour l'utilisateur de déplacer la tête de lecture à n'importe quel instant de la vidéo et d'éviter ainsi un téléchargement fastidieux. Dans ce cas, la vidéo n'est pas stockée dans la mémoire cache du navigateur, mais dynamiquement en RAM. Cela est avantageux en termes de sécurité et de droits d'auteur.
Grâce au streaming, il est également possible de retransmettre un flux live pour des émissions en direct ce qui n'est pas réalisable avec un téléchargement progressif. Plusieurs protocoles d'échanges sont disponibles dans ce cas. Le protocole de diffusion temps réel, nommé Real Time Streaming Protocol (RTSP), est le dernier en date utilisé par défaut sur Windows Media Server et IIS7 pour échanger des flux de données continus entre le client Silverlight et le serveur. Il opère sur le port 554. Cela pose certaines problématiques si vous êtes dans une configuration réseau contraignante avec firewall, ce qui est le cas de nombreuses entreprises. Silverlight prend cela en considération et utilise par défaut le protocole d'échange WMSP, signifiant Windows Media HTTP Streaming Protocol. Ce dernier est basé sur une spécification HTTP et utilise par conséquent les ports 80 et 443 pour HTTPS, ces derniers sont généralement disponibles. A contrario de HTTP, WMSP est capable de maintenir des connexions persistantes client/serveur qui sont nécessaires à la diffusion continue (voir Figure 13.6). Côté Windows Media Server, il vous suffira de cocher l'option de diffusion pour le Web, vous n'aurez rien à faire, il vous suffira de placer vos fichiers à l'emplacement adéquat pour qu'ils soient accessibles.
Le streaming est une solution élégante, mais assez coûteuse en terme de ressources côté serveur. Pour les jeux olympiques de Pékin en 2008, Microsoft utilise une nouvelle technologie nommée Adaptative Streaming. Cette technologie est également présente pour des technologies gratuites telles que PHP. Il s'agit en réalité d'un mode de téléchargement progressif simulant une lecture en streaming. Dans cette configuration, chaque média est scindé physiquement sur le serveur en de nombreux fragments d'une durée égale. Il est possible de préparer le média pour différentes qualités de bande passante. Sur le serveur, cela se traduit par plusieurs groupes de fragments qui sont organisés selon la bande passante qu'ils ciblent et leur poids qui correspond à un débit spécifique. Du côté client cette configuration est idéale, car il suffit de télécharger le fragment qui correspond à un instant (time code) spécifique du flux (voir Figure 13.7).
Cette solution, bien que très pratique, recèle toutefois un inconvénient de taille : elle force le serveur à gérer des centaines de fragments sur son disque. Dans le cas des jeux olympiques, les fragments représentaient deux secondes de vidéo. Pour une heure de vidéo on obtient donc le calcul : 60 minutes x 60 secondes / 2, ce qui représente 1 800 portions. Si nous recréons ces fragments pour du 512 kbs et du 1 024 kbs, nous obtenons 3 600 fragments à gérer - ce qui est assez conséquent. Le Smooth Streaming est né de cette problématique. L'idée est simple : il s'agit de représenter l'ensemble des fragments d'un même poids au sein d'un seul fichier (voir Figure 13.8).
Cette méthodologie limite les accès disque et améliore les performances côté serveur. Vous devrez déployer différents types de fichiers sur ce dernier : -
le premier, nommé ismv, contient de la vidéo et de l'audio, ou de la vidéo uniquement et les fragments qu'il détient sont au format MP4 ;
-
les fichiers de type isma sont dédiés au contenu audio uniquement ;
-
un document déclaratif de type ism est nécessaire, il décrit les relations entre les fichiers et les qualités de bande passante associées, il est dédié au serveur et ne sera pas lu par le client ;
- le document .ismc décrit les flux disponibles, leur qualité, le type d'encodage utilisé, le chapitrage et les informations utiles pour que le poste client puisse lire les médias.
La grande différence avec les précédents systèmes est que l'utilisateur n'a plus besoin de choisir la bande passante qu'il souhaite utiliser. Le type de fragment téléchargé, correspondant à une bande passante spécifique, est choisi de manière dynamique et transparente, selon les capacités de la connexion durant la lecture du média. Générer une vidéo pour Silverlight est une opération assez simple réalisée via Expression Encoder. L'utilisation de ce logiciel sort du cadre de ce livre, mais sachez qu'il vous permettra simplement de créer des montages simples, via l'ajout d'un générique de début et/ou de fin à la vidéo principale. Grâce à Expression Encoder, il devient très simple d'encoder de la vidéo et du son pour du téléchargement progressif, du streaming ou du Smooth Streaming dans l'ensemble des formats disponibles pour chacun de ces modes de diffusion. Un ensemble de paramétrages prédéfinis vous permettront d'encoder la vidéo sans vraiment vous poser de questions dans un premier temps (voir Figure 13.9). Avec le temps il est conseillé de bien comprendre et d'assimiler les réglages de compression afin d'optimiser au maximum le trafic utile.
Pour diffuser du Smooth Streaming, il suffit d'installer un serveur IIS 7, Windows Media Services 3 à travers l'application Web Platform Installer disponible gratuitement sur le portail de téléchargement Microsoft : http://www.microsoft.com/downloads/. Microsoft diffuse une librairie disponible à l'adresse http://smf.codeplex.com. Elle contient un lecteur capable de gérer les listes de lecture, du contenu préparé pour le Smooth Streaming et des vidéos standard. Elle apporte notamment un contrôle de type MediaElement qui possède la capacité de lire des médias préparés pour ce mode de diffusion. Il vous suffira de déposer les médias encodés pour le Smooth Streaming directement sur un répertoire du site de test local, d'activer le service de Smooth Streaming dans le gestionnaire IIS et d'y faire appel via le MediaElement amélioré.
Maintenant que nous avons énuméré les différents types de diffusion, nous allons créer un lecteur vidéo simple à travers la reprise d'un projet réalisé précédemment.
12-1-3. Un lecteur vidéo simple▲
Afin de partir d'une base existante, vous pouvez télécharger le projet PlayerVideo_base.zip disponible dans le dossier chap13 des exemples. Il est également nécessaire de télécharger le fichier compressé Assets.zip. Il contient une vidéo dans différents formats pour une diffusion standard et pour du Smooth Streaming. Ouvrez le projet dans Blend, le lecteur vidéo a été légèrement modifié depuis le Chapitre 6 Boutons personnalisés. Au sein de l'arbre visuel, vous trouverez un contrôle personnalisable, typé Pie-ProgressBar, que j'ai réalisé pour l'occasion. Il s'agit d'une barre de progression circulaire qui hérite de RangeBase à l'instar des contrôles de type ProgressBar ou Slider. De ce fait, ce contrôle est hérité de nombreux comportements et propriétés inhérents à cette classe abstraite. Il n'est pas fourni en standard dans Silverlight. Sa conception est toutefois possible grâce à l'utilisation de figures géométriques de l'API de dessin Silverlight. Il possède en outre la capacité d'être directement utilisé comme jauge interactive. Il suffit pour cela de passer sa propriété Is-Interactive à true (voir Figure 13.10). Ainsi, lorsque vous cliquerez n'importe où sur sa surface, sa propriété Value sera mise à jour.
Ajoutez une instance de type MediaElement, vous trouverez ce contrôle dédié à la lecture des médias dans le panneau Assets. Placez-le dans la grille juste au-dessus de l'image d'arrière-plan, en mode de redimensionnement étiré, sans lui donner de marges extérieures. Il est nécessaire de fournir l'adresse d'une vidéo à la propriété Source de notre MediaElement. Importez la vidéo PetitPanOcean.wmv téléchargée depuis le fichier Assets-.zip (ou une autre de votre choix) dans le projet. Celle-ci est désormais embarquée à la compilation, ce qui n'est pas l'idéal mais nous permet de débuter le code. Dans tous les cas, il est nécessaire de lire une vidéo chapitrée afin de suivre la totalité de ce tutoriel. Nous allons maintenant créer le code logique nécessaire à la lecture de la vidéo. Dans la propriété Source, sélectionnez la vidéo PetitPanOcean.wmv dans la liste déroulante.
Il est également possible de renseigner une URL distante mais nous aborderons le mode asynchrone ultérieurement. De plus, si vous essayez avec l'un des deux formats mov ou mp4 contenus dans l'archive Assets.zip, ces derniers n'apparaissent pas dans la liste de choix, car les extensions mov et mp4 ne sont pas directement reconnues sous Blend. Ces fichiers ne sont donc pas embarqués dans le fichier xap, car étant de type inconnu pour Blend, ils ne sont pas référencés comme ressource de type média.
Commençons par les interactions simples : la gestion de la lecture et du volume. Nous passerons ensuite aux contrôles d'avance et de retour rapide. Ouvrez le projet dans Visual Studio, la classe MediaElement possède des méthodes assez simples permettant le contrôle de la tête de lecture. Il nous suffit d'écouter les événements des contrôles afin de les appeler :
public
MainPage
(
)
{
InitializeComponent
(
);
…
playPauseBtn.
Checked +=
new
RoutedEventHandler
(
playPauseBtn_Checked);
playPauseBtn.
Unchecked +=
new
RoutedEventHandler
(
playPauseBtn_Unchecked);
volumeDownBtn.
Click +=
new
RoutedEventHandler
(
volumeDownBtn_Click);
volumeUpBtn.
Click +=
new
RoutedEventHandler
(
volumeUpBtn_Click);
}
void
playPauseBtn_Unchecked
(
object
sender,
RoutedEventArgs e)
{
mediaElement.
Pause
(
);
}
void
playPauseBtn_Checked
(
object
sender,
RoutedEventArgs e)
{
mediaElement.
Play
(
);
}
void
volumeUpBtn_Click
(
object
sender,
RoutedEventArgs e)
{
mediaElement.
Volume +=
0
.
10
;
mediaElement.
Volume =
Math.
Min
(
mediaElement.
Volume,
1
);
}
void
volumeDownBtn_Click
(
object
sender,
RoutedEventArgs e)
{
mediaElement.
Volume -=
0
.
10
;
mediaElement.
Volume =
Math.
Max
(
mediaElement.
Volume,
0
);
}
Décochez l'option AutoPlay du MediaElement, de cette manière la vidéo ne sera pas lue par défaut au lancement du projet. Nous pouvons maintenant gérer les marqueurs de temps qui permettent la conception d'un chapitrage. Il nous faut une variable indiquant le numéro du chapitre et récupérer la liste des marqueurs :
TimelineMarkerCollection tmc =
new
TimelineMarkerCollection
(
);
int
currentTimeLineMarker =
-
1
;
public
MainPage
(
)
{
InitializeComponent
(
);
…
backwardBtn.
Click +=
new
RoutedEventHandler
(
backwardBtn_Click);
forwardBtn.
Click +=
new
RoutedEventHandler
(
forwardBtn_Click);
mediaElement.
MediaOpened +=
new
RoutedEventHandler
(
mediaElement_MediaOpened);
}
void
backwardBtn_Click
(
object
sender,
RoutedEventArgs e)
{
if
(
tmc.
Count >
1
)
{
currentTimeLineMarker =
--
currentTimeLineMarker >=
0
?
currentTimeLineMarker :
tmc.
Count-
1
;
mediaElement.
Position =
tmc[
currentTimeLineMarker].
Time;
}
}
void
forwardBtn_Click
(
object
sender,
RoutedEventArgs e)
{
if
(
tmc.
Count >
1
)
{
currentTimeLineMarker =
++
currentTimeLineMarker %
mediaElement.
Markers.
Count;
mediaElement.
Position =
tmc[
currentTimeLineMarker].
Time;
}
}
void
mediaElement_MediaOpened
(
object
sender,
RoutedEventArgs e)
{
tmc =
mediaElement.
Markers;
if
(
tmc.
Count <
2
)
{
backwardBtn.
Visibility =
Visibility.
Collapsed;
forwardBtn.
Visibility =
Visibility.
Collapsed;
}
else
{
backwardBtn.
Visibility =
Visibility.
Visible;
forwardBtn.
Visibility =
Visibility.
Visible;
}
}
Le code ci-dessus est assez simple : nous modifions l'index du marqueur en cours, puis nous déplaçons la tête de lecture de la vidéo à la durée indiquée par le marqueur de temps ciblé. La propriété Position du MediaElement attend une instance de type TimeSpan. On peut donc directement lui affecter la propriété Time du nouveau marqueur cible. Les deux expressions incrémentant ou décrémentant l'index du marqueur en cours permettent de boucler dans la collection de marqueurs contenue dans le MediaElement. Pour finir, si ce dernier possède plus d'un marqueur de temps, nous rendons accessibles les boutons forwardBtn et backwardBtn. Dans le cas contraire, autant les cacher pour éviter que l'utilisateur ne clique dessus inutilement. Le mieux est de réaliser cette opération lorsque le média est complètement ouvert et que la liste des marqueurs de temps est récupérée. Il est également indispensable de récupérer l'index du marqueur atteint lorsque la vidéo est en cours de lecture. Nous pourrons ainsi mettre à jour notre variable permettant de gérer les boutons forwardBtn et backwardBtn. Pour cela, nous pouvons écouter l'événement MarkerReached du MediaElement :
public
MainPage
(
)
{
InitializeComponent
(
);
…
mediaElement.
MarkerReached +=
new
TimelineMarkerRoutedEventHandler
(
media Element_MarkerReached);
}
void
mediaElement_MarkerReached
(
object
sender,
TimelineMarkerRoutedEventArgs e)
{
//l'instruction ci-dessous ne fonctionne pas,
//car la référence est perdue :
//currentTimeLineMarker = tmc.IndexOf(e.Marker);
//il est donc nécessaire d'utiliser une boucle
//afin de trouver le bon marqueur
foreach
(
TimelineMarker tm in
tmc)
{
if
(
tm.
Time ==
e.
Marker.
Time)
{
currentTimeLineMarker =
tmc.
IndexOf
(
tm);
break
;
}
}
}
Il peut être utile de récupérer le contenu de la propriété Text liée à chaque marqueur, ainsi que la vignette au format jpeg qui y est associée. De cette manière, il serait possible d'afficher les différents chapitres contenus dans la vidéo. Nous n'allons pas nous attarder sur cette fonctionnalité, à la place, nous allons voir comment contrôler la barre de progression circulaire nommée timeLineProgress. Celle-ci indique à l'utilisateur le temps écoulé depuis le début. Toute la problématique consiste à récupérer la position courante de la tête de lecture. Aucun événement n'existe à cette fin, il nous faut donc utiliser une instance de type DispatcherTimer pour rafraîchir cette valeur. Celle qui est déjà présente ne nous est d'aucune aide, car elle concerne la gestion de la disparition des contrôles après un laps de temps d'inactivité. Nous devons modifier plusieurs éléments dans notre code. Dans un premier temps nous devons initialiser correctement les valeurs maximum est minimum de l'instance de PieProgressBar, cela peut être réalisé dans l'événement MediaOpened :
void
mediaElement_MediaOpened
(
object
sender,
RoutedEventArgs e)
{
tmc =
mediaElement.
Markers;
if
(
tmc.
Count ==
0
)
{
backwardBtn.
Visibility =
Visibility.
Collapsed;
forwardBtn.
Visibility =
Visibility.
Collapsed;
}
else
{
backwardBtn.
Visibility =
Visibility.
Visible;
forwardBtn.
Visibility =
Visibility.
Visible;
}
timeLineProgress.
Minimum =
0
;
timeLineProgress.
Maximum =
mediaElement.
NaturalDuration.
TimeSpan.
TotalMilliseconds;
}
La propriété NaturalDuration permet de récupérer le temps total de la vidéo, nous pouvons le traduire sous forme de millisecondes ce qui est approprié si nous souhaitons obtenir une définition relativement bonne. Dans un second temps, nous devons ajouter une instance de Dispatcher-Timer comme champ privé de notre classe principale, puis écouter l'événement Tick :
//on crée un métronome pour espionner la position de la tête de lecture
DispatcherTimer dtPos =
new
DispatcherTimer
(
)
{
Interval =
TimeSpan.
FromMilliseconds
(
250
) };
public
MainPage
(
)
{
InitializeComponent
(
);
…
dtPos.
Tick +=
new
EventHandler
(
dtPos_Tick);
…
}
void
dtPos_Tick
(
object
sender,
EventArgs e)
{
timeLineProgress.
Value =
mediaElement.
Position.
TotalMilliseconds;
}
Dans le code ci-dessous, nous définissons un métronome qui diffusera l'événement Tick quatre fois par seconde. Au sein de la méthode d'écoute, nous affectons le laps de temps traduit en millisecondes à la propriété Value de notre PieProgressBar. Il est ensuite nécessaire d'arrêter le métronome lorsque la vidéo est en pause et de le redémarrer lorsqu'elle est lue :
void
playPauseBtn_Unchecked
(
object
sender,
RoutedEventArgs e)
{
mediaElement.
Pause
(
);
dtPos.
Stop
(
);
}
void
playPauseBtn_Checked
(
object
sender,
RoutedEventArgs e)
{
mediaElement.
Play
(
);
dtPos.
Start
(
);
}
Il nous reste encore une tâche à accomplir. La propriété IsInteractive de timeLineProgress est égale à true. Ceci signifie que nous pouvons changer sa valeur en cliquant n'importe où sur la barre. Nous pourrions ainsi modifier la position de la tête de lecture dynamiquement. Nous n'avons qu'à écouter l'événement ValueChanged de cette dernière pour obtenir ce résultat :
public
MainPage
(
)
{
InitializeComponent
(
);
…
timeLineProgress.
ValueChanged +=
new
RoutedPropertyChangedEventHandler<
double
>(
timeLineProgress_ValueChanged);
…
}
void
timeLineProgress_ValueChanged
(
object
sender,
RoutedPropertyChangedEventArgs<
double
>
e)
{
if
(
mediaElement.
Position.
TotalMilliseconds !=
e.
NewValue)
{
mediaElement.
Position =
TimeSpan.
FromMilliseconds
(
e.
NewValue);
}
}
Comme vous le constatez, il faut faire attention à ne pas réaffecter deux fois la propriété Position entre les événements écoutés Tick et ValueChanged. Ce dernier ne doit être réellement utilisé que pour les changements importants qui sont dus au clic de l'utilisateur sur la ligne de temps. C'est pour cette raison que nous comparons les deux valeurs au sein d'une condition avant de réaffecter la position de la tête de lecture. Pour bien faire, il nous faut encore une fois boucler au sein de la liste des marqueurs afin de récupérer l'index de celui situé juste avant la position de la tête de lecture :
void
timeLineProgress_ValueChanged
(
object
sender,
RoutedPropertyChangedEventArgs<
double
>
e)
{
if
(
mediaElement.
Position.
TotalMilliseconds !=
e.
NewValue)
{
mediaElement.
Position =
TimeSpan.
FromMilliseconds
(
e.
NewValue);
foreach
(
TimelineMarker tm in
tmc)
{
if
(
mediaElement.
Position.
TotalMilliseconds <
tm.
Time.
TotalMilliseconds )
{
currentTimeLineMarker =
tmc.
IndexOf
(
tm)-
1
;
break
;
}
}
}
}
Le lecteur est presque terminé, nous devons encore gérer le chargement de la vidéo de manière dynamique. Vous trouverez le lecteur en dans le dossier chap13 : PlayerVideo_baseControl.zip.
12-1-4. Chargement dynamique de vidéos▲
Créer un lecteur de flux vidéo utilisant le téléchargement progressif est une opération assez simple : il faut alimenter le MediaElement avec un flux binaire adapté. Il suffit pour cela d'affecter sa propriété Source avec une instance de type Uri pointant vers un fichier vidéo existant. Avant de commencer, nous allons modifier légèrement notre application afin de pouvoir la réutiliser ultérieurement. Après tout, une application Silverlight étant un composant de type UserControl, nous pouvons imaginer qu'il peut être pratique d'en instancier plusieurs exemplaires dans un autre projet.
Cela est particulièrement vrai dans notre cas, car notre UserControl (MainPage) est dédié à une tâche bien spécifique : la lecture de médias vidéo. Pour arriver à nos fins, nous allons faire un peu de refactoring sous Visual Studio. Ce concept très puissant, complètement intégré dans Visual Studio, permet de modifier des définitions ou des noms d'objets logiques de manière intelligente. Il ne s'agit pas d'un simple chercher/remplacer mais plutôt d'une opération très élaborée capable de réinterpréter le code logique et de le modifier en conséquence. Dans le fichier MainPage.xaml.cs, remplacez le nom de la classe MainPage par VideoElement. Une petite barre rouge apparaît juste en dessous du nom modifié. Cliquez dessus, puis sélectionnez le menu Rename 'Video.MainPage' to 'Video.VideoElement' (voir Figure 13.11).
Une fois cette opération accomplie, recompilez via le raccourci Ctrl+Maj+B pour vérifier qu'aucune erreur n'est levée. Dans l'explorateur de projet situé à droite dans l'interface de Visual Studio,
modifiez également le nom du fichier MainPage.xaml par Video-Element.xaml. Le fichier C# associé est également modifié. Redéfinir le nom de l'application nous permettra par la suite d'instancier autant de lecteurs vidéo que nous en avons besoin. Testez le projet afin de vérifier son bon fonctionnement. Définissez une nouvelle propriété de dépendance nommée String-UriSource. Lorsque celle-ci sera modifiée, la méthode de rappel associée nous permettra de redéfinir la propriété Source du MediaElement.
Le code ci-dessous décrit son implémentation au sein de la classe VideoElement :
public
string
StringUriSource
{
get
{
return
(
string
)GetValue
(
StringUriSourceProperty);
}
set
{
SetValue
(
StringUriSourceProperty,
value
);
}
}
public
static
readonly
DependencyProperty StringUriSourceProperty =
DependencyProperty.
Register
(
"StringUriSource"
,
typeof
(
string
),
typeof
(
VideoElement),
new
PropertyMetadata
(
string
.
Empty,
new
PropertyChangedCallback (
OnUriChanged)));
private
static
void
OnUriChanged
(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
VideoElement ve =
d as
VideoElement;
ve.
mediaElement.
Source =
new
Uri
(
e.
NewValue as
string
,
UriKind.
RelativeOrAbsolute);
}
Afin de tester cette propriété, ajoutez un nouveau UserControl de test (voir Figure 13.12).
Au sein du fichier de code logique App.xaml.cs, il est nécessaire de remplacer le type du UserControl chargé, VideoElement, par TestPage. La ligne à modifier est centralisée dans la méthode Application_Startup :
private
void
Application_Startup
(
object
sender,
StartupEventArgs e)
{
this
.
RootVisual =
new
TestPage
(
);
}
Au sein de VideoElement.xaml, sélectionnez le MediaElement, et videz l'affectation de sa propriété Source. Il est nécessaire de créer une instance de la classe VideoElement dans la grille principale de TestPage. Vous pouvez accomplir cette opération grâce au panneau Assets via la catégorie Project. Sélectionnez l'instance de VideoElement, ouvrez le panneau Properties et dans la catégorie Miscellaneous entrez PetitPanOcean.wmv. Recompilez le projet, cette fois, la vidéo est lue via l'instance de VideoElement présente dans la page de test. Nous pouvons ajouter sereinement l'ensemble du code permettant la gestion du téléchargement, puis écouter les événements MediaFailed, Download-Progress-Changed et MediaEnded. Ceux-ci sont respectivement diffusés en cas d'échec de la lecture, durant la progression du téléchargement et lorsque la lecture du média est terminée :
public
VideoElement
(
)
{
InitializeComponent
(
);
…
mediaElement.
MediaFailed +=
new
EventHandler<
ExceptionRoutedEventArgs>(
mediaElement_MediaFailed);
mediaElement.
MediaEnded +=
new
RoutedEventHandler
(
mediaElement_MediaEnded);
mediaElement.
DownloadProgressChanged +=
new
RoutedEventHandler
(
mediaElement_DownloadProgressChanged);
}
void
mediaElement_DownloadProgressChanged
(
object
sender,
RoutedEventArgs e)
{
downloadProgress.
Value =
mediaElement.
DownloadProgress;
}
void
mediaElement_MediaEnded
(
object
sender,
RoutedEventArgs e)
{
mediaElement.
Stop
(
);
}
void
mediaElement_MediaFailed
(
object
sender,
ExceptionRoutedEventArgs e)
{
Debug.
WriteLine
(
"Le format de la vidéo et/ou son adresse sont incorrects."
);
}
Assurez-vous que la propriété Maximum de l'instance downloadProgress est égale à 1. Cela est important, car le média élément renvoie la progression du téléchargement sous forme d'un chiffre exprimé en pourcentage de 0 à 1 (à travers sa propriété Download-Progress qu'il ne faut pas confondre avec l'instance PieProgressBar).
Pour tester tous ces comportements, vous pouvez utiliser l'adresse de la vidéo suivante qui est en ligne : http://www.tweened.org/wp-content/uploads/videos/14_advanceVisual-StateManager.wmv. Il s'agit d'un webcast que j'ai réalisé durant l'été 2008 et dont le but était de donner les bases de Silverlight. Il est également possible de donner à l'internaute une idée du remplissage de la mémoire tampon allouée. Ainsi, un texte pourrait apparaître lorsque le buffer se remplit. Le projet finalisé PlayerVideo_final.zip est dans le dossier chap13.
12-2. Conception MVVM▲
Jusqu'à présent, nous avons fortement couplé les vues au code logique gérant nos interfaces. Nous allons éviter cet écueil, dans cette section, grâce à l'utilisation du modèle de conception Modèle Vue Vue-Modèle. Ce motif est un dérivé de MVC (Modèle Vue Contrôleur) propre à Silverlight et WPF. Il permet de séparer complètement la logique régissant les vues de celle régissant la fonctionnalité pure. Une fois cette architecture mise en place, nous y intégrerons facilement le chargement de données externes. Téléchargez le projet MVVM_PatternBase.zip du dossier chap13 des exemples.
12-2-1. Principes▲
Concrètement, le modèle de conception MVVM sépare le code en trois sous-ensembles, Modèle, Vue et Vue-Modèle, qui possèdent une responsabilité spécifique. Une vue représente une interface graphique (ou IHM) liée à un flux d'utilisation. Dans MVVM, le Modèle est représenté par l'ensemble des classes et collections contenant les données et le code logique assurant la gestion du chargement de données distantes. La partie Vue-Modèle regroupe les classes dont le but est d'établir une passerelle entre Vue et Modèle et de gérer la fonctionnalité côté Vue. Vue-Modèle a donc deux responsabilités :
- appeler des méthodes ou récupérer des données contenues par le Modèle lorsque la vue en a besoin ;
- rendre accessible et présenter les données à la vue de manière efficace en notifiant cette dernière lorsque les données changent ;
La vue gère toutes les interactions utilisateur et exécute les commandes par un mécanisme de liaison. Elle possède un minimum de code logique qui se traduit souvent par une affectation de sa propriété DataContext à une instance de Modèle-Vue. Finalement, Vue-Modèle est pleinement responsable de la communication entre les couches basses et hautes de l'application (voir Figure 13.13). La liaison de données est très importante dans ce contexte, car cela permet à Modèle-Vue de notifier la vue sans effort, et inversement dans le cas d'une liaison à deux voies.
Les principaux avantages de cette architecture sont de faciliter la maintenance, la mise à jour, la réutilisation et de simplifier la conception. En interne MVVM utilise le modèle de conception Command. Une commande est une instruction connue par la vue et dont l'exécution est définie sur Vue-Modèle. Finalement, la Vue sait ce qu'elle souhaite accomplir mais n'a pas besoin de connaître les détails d'implémentation - ce principe fait référence au concept d'encapsulation.
C'est en suivant ces principes que nous allons finaliser le projet MVVM_PatternBase que vous avez téléchargé. Dans ce dernier, vous trouverez un répertoire SLExtensionsMVVM reprenant une infime partie des classes proposées par la bibliothèque SLExtensions que vous trouverez sur CodePlex. Vous trouverez également deux répertoires correspondant aux classes propres au Modèle et à Vue-Modèle (voir Figure 13.14).
Il n'est pas forcément nécessaire de stocker les vues dans un répertoire dédié puisque la racine du projet peut les centraliser. Ce projet est une base adéquate pour apprendre MVVM.
L'une des critiques que l'on pourrait faire consiste à dire que la Vue connaît la commande alors qu'elle pourrait diffuser des événements qui exécutent les commandes. Toutefois, cette remarque n'est pas pertinente, car le code logique gérant l'écoute de l'événement est forcément situé sur le processus (Thread) graphique, donc la vue.
12-2-2. Modèle▲
En règle générale, les collections de données fictives (voir Chapitre 10 Ressources graphiques) sont conçues de manière éclairée par le développeur. Les éléments de ces collections correspondent très souvent à des objets métiers. Un objet métier est une classe spécifique à une problématique métier. Les informations relatives aux clients d'une entreprise de services seront ainsi stockées grâce une classe métier dédiée : Customer. Celle-ci possède diverses propriétés comme le nom du client, son titre, son numéro de compte client ou son e-mail. La problématique consiste souvent à transformer (sérialiser) un enregistrement récupéré depuis une source de donnée quelconque, en un objet métier côté client utilisable par la vue. En règle générale, on sépare logique métier et logique valeur.
Un objet valeur (Value Object), quant à lui, a pour objectif de gérer l'une de ces propriétés. Par exemple, la classe Email aurait pour but de stocker la valeur de l'e-mail client fourni, de vérifier sa validité et de lever les exceptions le cas échéant. Nous n'entrerons pas dans ces considérations, car cela est un peu éloigné du sujet traité.
Nous allons nous contenter de créer un objet dont le but sera de centraliser toutes les propriétés d'un Media. Dans le contexte MVVM, cet objet fait partie du Modèle. Ouvrez le projet téléchargé à la fois dans Blend et dans Visual Studio, puis créez les classes Author et Media dans le répertoire Model. La première correspond à la description d'un auteur, la seconde contient l'ensemble des données utiles à la lecture du média. Voici l'implémentation de la classe Author :
public
class
Author
{
public
string
Nom {
get
;
set
;
}
public
string
Prenom {
get
;
set
;
}
public
string
Blog {
get
;
set
;
}
public
Author
(
string
nom,
string
prenom)
{
this
.
Nom =
nom;
this
.
Prenom =
prenom;
}
public
Author
(
string
nom,
string
prenom,
string
blog)
{
this
.
Nom =
nom;
this
.
Prenom =
prenom;
this
.
Blog =
blog;
}
public
override
string
ToString
(
)
{
return
this
.
Prenom +
" "
+
this
.
Nom;
}
public
string
ToString
(
bool
displayBlog)
{
if
(
this
.
Blog ==
string
.
Empty ||
!
displayBlog)
{
return
this
.
Prenom +
" "
+
this
.
Nom;
}
else
{
return
this
.
Prenom +
" "
+
this
.
Nom +
" - "
+
this
.
Blog;
}
}
}
La classe Author est très simple et possède deux constructeurs selon que vous souhaitiez fournir ou non le titre du blog de l'auteur. Si vous utilisez C# 4, il est possible d'utiliser un paramètre optionnel, ce qui est plus élégant dans ce cas. La première méthode To-String() surcharge celle héritée de Object. La seconde est propre à l'objet Author et accepte un paramètre permettant d'indiquer si vous souhaitez renvoyer le titre du blog. Le code suivant expose la classe Media :
public
class
Media
{
public
string
Titre {
get
;
set
;
}
public
string
Description {
get
;
set
;
}
public
TimeSpan Duree {
get
;
set
;
}
public
string
Date {
get
;
set
;
}
public
string
BaseUrl {
get
;
set
;
}
public
string
FichierMedia {
get
;
set
;
}
public
string
Url
{
get
{
return
BaseUrl +
FichierMedia;
}
}
public
string
FichierVignette {
get
;
set
;
}
public
string
UrlVignette
{
get
{
return
BaseUrl +
FichierVignette;
}
}
public
Author Auteur {
get
;
set
;
}
public
override
string
ToString
(
)
{
return
Titre;
}
public
string
ToString
(
bool
displayAuthor)
{
if
(
Auteur.
ToString
(
) ==
string
.
Empty ||
!
displayAuthor)
{
return
Titre;
}
else
{
return
Titre +
" - "
+
Auteur.
ToString
(
false
);
}
}
}
Là encore, on surcharge la méthode ToString() afin d'avoir une représentation simple d'un Media.
Cela sera particulièrement utile lorsque nous souhaiterons afficher la liste des médias au sein d'une instance de ListBox.
La bonne pratique consiste à centraliser les classes dans des espaces de noms dédiés à chaque ensemble défini par MVVM. Les classes du Modèle seront donc stockées dans l'espace de noms MVVM_Pattern.Model, celles correspondantes à l'ensemble Vue-Modèles seront quant à elles dans MVVM_Pattern.ViewModel.
Il nous reste maintenant à créer une classe qui centralisera nos données et s'occupera des appels distants. Dans un premier temps, nous allons juste créer le squelette de cette classe et gérer des données en dur. Nous pouvons appeler cette classe MediasDatas, celle-ci est assez simple :
public
static
class
MediasDatas
{
public
static
ObservableCollection<
Media>
Datas {
get
;
set
;
}
static
MediasDatas
(
)
{
Datas =
new
ObservableCollection<
Media>(
);
}
public
static
void
GetAllMedias
(
)
{
Datas.
Add
(
new
Media
(
)
{
BaseUrl =
"http://www.tweened.org/wp-content/uploads/videos/"
,
FichierMedia =
"15_createJsonStream.wmv"
,
Auteur =
new
Author (
"Ambrosi"
,
"Eric"
,
"Tweened.org"
),
Description=
"C'est une jolie vidéo"
,
Duree=
TimeSpan.
FromSeconds
(
312
),
Titre=
"Créer un flux JSON"
,
Date=
"10 Juillet 2008"
}
);
}
}
En premier lieu la classe MediasDatas est statique, il n'est pas nécessaire d'en créer des instances puisqu'elle symbolise une source de données unique. Elle possède une propriété de type générique ObservableCollection<object>. Contrairement à une collection List standard, celle-ci notifie les objets qui y font référence à travers la liaison de données lorsqu'elle est modifiée d'une quelconque façon (élément ajouté ou supprimé par exemple). Ces derniers sont donc mis à jour dynamiquement grâce à une liaison de modèles OneWay, la vue peut modifier ce type de collection à travers une liaison à deux voies (TwoWay). Dans ce contexte, elle implémente les interfaces INotify-PropertyChanged et INotifyCollectionChanged nativement.
Nous avons ajouté un constructeur statique qui affecte une instance de Observable-Collection<Media>. Cela évite par la suite de tester si le membre statique Datas est null, et de lui affecter l'instance le cas échéant. Dans la méthode statique Get-AllMedias, nous simulons la réception de données en ajoutant un élément en dur. Nous avons accompli la première partie de la conception MVVM, nous allons maintenant nous occuper des parties Vue-Modèle et Vue.
12-2-3. Vue-Modèle▲
Comme nous l'avons précisé plus haut, Vue-Modèle possède deux facettes différentes. Elle doit tout d'abord présenter les données à la vue et notifier celle-ci lorsque les données évoluent. Les classes qui font partie de Vue-Modèle implémentent à cette fin l'interface IPropertyNotifyChanged permettant de gérer la liaison de données. C'est une bonne pratique à respecter, et cela devient indispensable si vous utilisez des types ne possédant pas de mécanismes de notification par défaut. Le plus simple est d'étendre une classe, par exemple ViewModel, qui implémente l'interface par défaut :
namespace
MVVM_Pattern.
ViewModel
{
public
class
ViewModel :
INotifyPropertyChanged
{
public
event
PropertyChangedEventHandler PropertyChanged;
public
void
NotifyPropertyChanged
(
string
propertyName)
{
if
(
PropertyChanged !=
null
)
{
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
propertyName));
}
}
}
}
Pour l'instant, rien de très difficile, nous avons déjà implémenté cette interface dans le chapitre précédent. Voyons maintenant la classe MediaViewModel qui joue un rôle concret dans notre application :
namespace
MVVM_Pattern.
ViewModel
{
public
class
MediaViewModel :
ViewModel
{
public
ObservableCollection<
Media>
Medias {
get
;
set
;
}
public
ICommand LoadMedias {
get
;
private
set
;
}
public
MediaViewModel
(
)
{
LoadMedias =
new
Command
(
DoGetMedias);
Medias =
MediasDatas.
Datas;
}
public
void
DoGetMedias
(
)
{
MediasDatas.
GetAllMedias
(
);
//dans ce cas, Notifier la vue n'est pas utile, puisque
//Medias est une collection observable implémentant
//nativement IPropertyNotifyChanged
//NotifyPropertyChanged("Medias");
}
}
}
Celle-ci est très simple, elle expose deux membres importants : une collection observable nommée Datas, ainsi qu'une commande LoadMedias. La propriété ItemsSource de la ListBox présente dans notre vue (MainPage) sera liée à la collection Datas. La commande est, quant à elle, liée à une propriété attachée CommandServices.Command que nous allons définir sur le bouton.
La commande est exécutée sur l'événement Click mais il est possible de la déclencher à travers n'importe quel événement en utilisant des comportements. La bibliothèque SLExtensions fournit tout ce qu'il faut à cette fin.
Lorsque nous affectons la commande, nous passons dans le constructeur la référence de la méthode de MediaViewModel que celle-ci (la commande) doit déclencher. Dans notre cas, il s'agit de la méthode DoGetMedias qui fait un appel de la méthode statique GetAllMedias. Comme nous l'avons vu précédemment dans la classe MediasDatas, GetAllMedias modifie la collection contenue par le membre statique Datas. Comme la propriété statique Datas est affectée à la propriété Medias de MediaViewModel, la collection Medias est bien mise à jour. Celle-ci implémente tout ce qu'il faut pour notifier la vue. Dans ce cas, il n'est donc pas nécessaire d'utiliser NotifyPropertyChanged.
A contrario, si nous avions utilisé une liste dans MediaViewModel en lieu et place d'une instance de type ObservableCollection, cela aurait été indispensable. Il nous reste à modifier légèrement le code logique et déclaratif de notre vue, MainPage. La première chose que nous pouvons faire consiste à affecter une instance de MediaViewModel comme contexte de données :
public
MainPage
(
)
{
InitializeComponent
(
);
this
.
DataContext =
new
MediaViewModel
(
);
}
Dans l'idéal, c'est le seul code logique qui doit être présent dans la vue.
Il ne faut pas privilégier une certaine pureté de code au détriment de la productivité ou des contraintes liées au design et à l'expérience utilisateur. Cela peut faire l'objet d'un long et douloureux débat. Dans tous les cas, il ne faut pas perdre de vue le résultat, ce que voit et ce que ressent le client et l'utilisateur final. Il vaut mieux faire, puis critiquer et corriger à partir d'une première base, qu'essayer de faire une application parfaite d'un seul coup. Cela vous permettra également de faire participer le client à divers niveaux.
Modifions maintenant le code déclaratif. La première chose à faire consiste à référencer l'espace de noms SLExtensions dans le UserControl racine :
<UserControl
xmlns
=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns
:
x
=
"http://schemas.microsoft.com/winfx/2006/xaml"
xmlns
:
Slei
=
"clr-namespace:SLExtensions.Input;assembly=MVVM_Pattern"
x
:
Class
=
"MVVM_Pattern.MainPage"
Width
=
"640"
Height
=
"480"
>
Vous remarquez que la bibliothèque référencée correspond à celle de notre projet. C'est tout à fait logique, car nous utilisons les sources dont nous avons besoin directement dans notre projet au lieu de référencer des bibliothèques (dll). Il nous reste maintenant à référencer la commande Load-Medias dans notre bouton, ainsi qu'à créer une liaison de données entre la ListBox et la propriété Medias contenue par MediaViewModel :
<Grid
x
:
Name
=
"LayoutRoot"
Background
=
"White"
>
<ListBox
ItemsSource
=
"{Binding Path=Medias}"
… />
<Button
Slei
:
CommandService.
Command
=
"{Binding LoadMedias}"
… />
</Grid>
Vous pouvez compilez l'application et effectuer un premier test. Lorsque vous cliquez sur le bouton, la commande est invoquée, les données sont récupérées et la ListBox reçoit les données par la liaison de données. Comme la classe Media surcharge ToString(), qui renvoie la propriété Titre, le titre du média ajouté dans GetAllMedias est affiché. Vous pourriez créer un modèle de données (DataTemplate) afin d'afficher toutes les propriétés du média. Il est également possible de passer un paramètre à la commande invoquée, celui-ci sera typé object au sein de la méthode déclenchée par la commande. Le projet complet MVVM_Pattern.zip se trouve dans le dossier chap13 des exemples.
Dans la prochaine section, nous allons profiter de cette architecture pour nous concentrer sur la classe MediasDatas qui va gérer le chargement dynamique de contenu XML.
12-3. Chargement de données▲
Microsoft apporte de nombreuses solutions répondant à cette problématique. Ainsi la bibliothèque RIA Services pour Silverlight, Windows Communication Foundation ou
ADO.NET Entity Framework sont quelques-unes des solutions vous permettant de gérer ces échanges de données. De nombreux livres abordent l'échange de données entre clients et serveurs, toutefois, le livre de John Papa, Data Driven Services aux éditions O'Reilly Media est une référence dans ce domaine. Je vous conseille vivement sa lecture si vous souhaitez apprendre de manière approfondie les différentes technologies proposées par Microsoft. De notre côté, nous nous placerons du point de vue Silverlight uniquement, nous n'étudierons pas le code logique côté serveur. Nous aborderons l'objet asynchrone WebClient, le modèle de conception MVVM, LINQ et les expressions lambda. Pour finir, nous apprendrons à consommer des flux au format XML et JSON et nous ferons un rapide tour d'horizon des contraintes de sécurité liées à l'échange de données interdomaines.
12-3-1. L'objet WebClient▲
Nous allons récupérer du contenu distant au format XML. Silverlight est une technologie ouverte, qu'il est facile d'intégrer dans un environnement libre. Le XML est donc largement supporté à travers les services Web ou la récupération de flux RSS. Dans cette section, nous traiterons le chargement de données XML distantes à travers l'objet simplifié WebClient. Nous envisagerons ensuite différents moyens de consommer et de sérialiser ces données via l'utilisation d'objets métier et d'attributs de sérialisation.
L'objet WebClient est contenu dans l'espace de noms System.Net. Il possède différentes méthodes asynchrones dont le but est de télécharger ou d'envoyer des données. Le contenu peut être perçu de deux manières différentes : en tant que chaîne de caractères ou en tant que flux binaire. Cela dépendra de la méthode asynchrone que vous utiliserez, elles sont au nombre de quatre : DownloadStringAsync, UploadStringAsync, OpenReadAsync ou OpenWriteAsync. Généralement le fichier distant à télécharger est l'un de ceux listés ci-dessous :
- un fichier de texte brut, une extension txt, par exemple ;
- un document au format XML, XAML, etc. ;
- une chaîne de caractères présentée sous forme de JSON - nous aborderons JSON ultérieurement ;
- un script côté serveur renvoyant l'un des trois types précédents.
Dans tous les cas, la chaîne de caractères contenue doit être encodée, de préférence, au format UTF8. Cet encodage permet à Silverlight d'interpréter les caractères spéciaux (français par exemple) correctement. Il supporte également les données texte encodées en UTF16 (Big Endian unicode) et en Unicode. Les différents types de données récupérées ou envoyées sous forme de flux binaire, sont listés ci-dessous : - tous types de formats texte listés précédemment : il n'y a pas de différence de performances, qui peut le plus peut le moins ;
- les médias (sons, vidéos, les images) : pour affecter le flux binaire récupéré à l'instance de type Image ou MediaElement, il est nécessaire d'utiliser leur méthode SetSource ; l'objet WebClient offre plus de capacités que BitmapImage ou MediaElement et il peut, par exemple, annuler le téléchargement du média via la méthode CancelAsync ;
- des applications ou des bibliothèques Silverlight compilées : Silverlight 4 offre de ce point de vue de nombreuses possibilités que nous n'abordons pas dans ce chapitre ;
- des données transmises directement dans un format binaire compressé via des services de type Windows Communication Foundation ou SOAP, par exemple : dans ce cas, les échanges client-serveur sont largement optimisés, car Silverlight est capable d'interpréter le format binaire directement ;
- les fichiers compressés de type zip sont également téléchargeables : Silverlight est capable de les parcourir pour récupérer les documents qu'ils contiennent ; télécharger un unique fichier compressé est toujours plus efficace que d'en charger des centaines en termes de bande-passante.
L'objet WebClient possède la capacité de ne pas bloquer le processus (Thread) à l'intérieur duquel la méthode asynchrone est appelée. Cela peut paraître évident sur d'autres plates-formes, toutefois Silverlight étant une technologie multithread, Microsoft aurait pu fournir une classe moins conviviale et déléguer cette gestion au développeur.
En interne, WebClient utilise la classe WebRequest dont le fonctionnement s'articule autour de méthodes de rappel asynchrones. Son principal avantage est d'être simple d'utilisation d'un point de vue code, car le code complexe est encapsulé. Toutefois, la classe WebClient n'offre pas autant de possibilités que peut le faire une classe comme Http-WebRequest (héritant de WebRequest). HttpWebRequest fournit un contrôle plus fin du déroulement des appels.
Téléchargez la solution MVVM_Pattern.zip ou réutilisez le projet finalisé de la dernière section. Placez le fichier XML videosxml.zip dans le répertoire ClientBin du projet MVVM_PatternSite. Le code logique assurant l'appel se situe dans la classe MediasDatas, voici comment récupérer le document XML distant :
namespace
MVVM_Pattern.
Model
{
public
static
class
MediasDatas
{
protected
static
int
Percent =
0
;
public
static
Uri URL_MEDIA =
new
Uri
(
"videos.xml"
,
UriKind.
RelativeOrAbsolute);
private
static
WebClient wc =
new
WebClient
(
);
public
static
ObservableCollection<
Media>
Datas {
get
;
set
;
}
static
MediasDatas
(
)
{
Datas =
new
ObservableCollection<
Media>(
);
wc.
OpenReadCompleted +=
new
OpenReadCompletedEventHandler
(
wc_OpenReadCompleted);
wc.
DownloadProgressChanged +=
new
DownloadProgressChangedEventHandler
(
wc_DownloadProgressChanged);
wc.
Encoding =
Encoding.
UTF8;
}
static
void
wc_DownloadProgressChanged
(
object
sender,
DownloadProgressChangedEventArgs e)
{
//throw new NotImplementedException();
Percent =
e.
ProgressPercentage;
}
static
void
wc_OpenReadCompleted
(
object
sender,
OpenReadCompletedEventArgs e)
{
if
(
e.
Error !=
null
)
{
throw
e.
Error;
}
else
if
(
e.
Cancelled)
{
//l'appel asynchrone a été annulé
//via la méthode CancelAsync
//ou une erreur de sécurité est levée
}
else
{
//on reçoit les données contenues dans e.Result
}
}
public
static
void
GetAllMedias
(
)
{
if
(!
wc.
IsBusy)
{
wc.
OpenReadAsync
(
URL_MEDIA);
}
}
…
Tout d'abord, nous définissons une instance de WebClient comme champ static de la classe MediasDatas. Au sein du constructeur static, nous écoutons les événements gérant la progression et la réception des données. Il n'y a pas réellement de méthode dédiée aux erreurs de téléchargement. Tout se déroule dans OpenReadCompleted. L'objet événementiel reçu contient trois propriétés définissant le type de résultat : Result,
Error et Cancelled. Nous pouvons traiter le résultat facilement à partir de là. La méthode Get-AllMedias est légèrement changée au lieu de modifier la collection en dur : nous réalisons l'appel asynchrone via la méthode OpenReadAsync de l'objet WebClient. Vous remarquez toutefois que cet appel de méthode ne peut se faire que si l'objet WebClient est disponible. Imaginons que l'utilisateur clique deux fois très rapidement sur le bouton. Dans ce cas, l'instance WebClient n'a pas eu le temps de recevoir les premières données qu'un nouvel appel est déclenché. Cela lève automatiquement une erreur, car WebClient ne possède pas cette faculté (voir Figure 13.15). Il est incapable de gérer plus d'un appel asynchrone à la fois.
La propriété IsBusy résout cette problématique en renvoyant l'état d'occupation de l'objet. Une autre manière plus élégante de gérer cette problématique consiste à lier la propriété IsEnabled du bouton à la valeur opposée de la propriété IsBusy. De cette manière, le bouton devient inactif durant le laps de temps correspondant à l'appel asynchrone. L'avantage de cette méthode est à la fois de tenir compte et d'utiliser le travail fourni par le designer. Ainsi, chacun y gagne sans effort ou presque. Dans la section 12.3.4 Consommer du XML avec LINQ, nous traitons les données reçues mais auparavant il nous faut découvrir la technologie LINQ.
12-3-2. Introduction à LINQ ▲
LINQ (Language INtegrated Query) est un mini-langage phrasé directement utilisable en C#. Il permet d'écrire des requêtes très simples à partir de mots-clés équivalents à ceux existants pour les bases de données. LINQ est une technologie à utiliser de concert avec des ORM comme Entity Framework ou Link2Sql, il permet de sérialiser les enregistrements de bases de données échappant à une logique orientée objet, en objet fortement typé. LINQ est donc notamment utile lorsque vous souhaitez adresser des requêtes à une base SQL (notamment grâce à la bibliothèque RIA Services). Ce n'est toutefois pas son seul cadre d'utilisation. LINQ étant accessible en C#, d'autres sources de données peuvent être ciblées. Il est ainsi possible d'adresser des requêtes à des contenus de type XML, JSON ou Object. Ce contenu correspond à tous types C# implémentant les interfaces IEnumerable ou IEnumerable<T>. Ainsi tout objet de type List, Array, Dictionnary, Collec-tion ou Queue, est à même d'être parcouru par une requête LINQ. Cela est particulièrement utile pour les opérations de tri, car LINQ est très simple d'utilisation. Dans un nouveau projet, commencez par définir une énumération ainsi qu'une classe Author comme indiqué ci-dessous :
public
enum
TypeAuteur :
int
{
Musicien=
0
,
Ecrivain=
1
,
Poete=
2
}
public
class
Author
{
public
string
Nom {
get
;
set
;
}
public
string
Prenom {
get
;
set
;
}
public
string
Oeuvre {
get
;
set
;
}
public
TypeAuteur Type {
get
;
set
;
}
public
Author
(
string
nom,
string
prenom)
{
this
.
Nom =
nom;
this
.
Prenom =
prenom;
}
public
Author
(
string
nom,
string
prenom,
string
oeuvre,
TypeAuteur ta)
{
this
.
Nom =
nom;
this
.
Prenom =
prenom;
this
.
Oeuvre =
oeuvre;
this
.
Type =
ta;
}
public
override
string
ToString
(
)
{
return
this
.
Prenom +
" "
+
this
.
Nom;
}
public
string
ToString
(
bool
displayBlog)
{
if
(
this
.
Oeuvre ==
string
.
Empty ||
!
displayBlog)
{
return
this
.
Prenom +
" "
+
this
.
Nom;
}
else
{
return
this
.
Prenom +
" "
+
this
.
Nom +
" - "
+
this
.
Oeuvre;
}
}
}
Créez trois exemplaires de ListBox : le premier affiche une liste d'auteurs non filtrée, les deux autres vont représenter la liste filtrée à partir de deux requêtes différentes. Placez également un champ de saisie nommé filtreTxt, dans LayoutRoot - la valeur saisie va jouer le rôle de filtre. L'idéal serait de définir un modèle de donnée (DataTemplate) afin d'afficher le détail de chaque élément contenu. Voici le code initial de la classe MainPage :
public
partial
class
MainPage :
UserControl
{
List<
Author>
auteurs;
public
MainPage
(
)
{
InitializeComponent
(
);
auteurs =
new
List<
Author>(
)
{
new
Author
(
"Mozart"
,
"Amadeus"
,
"La flute enchantée"
,
TypeAuteur.
Musicien),
new
Author
(
"Melville"
,
"Herman"
,
"Moby dick"
,
TypeAuteur.
Ecrivain),
new
Author
(
"Wilde"
,
"Oscar"
,
"Le portrait de Dorian Gray"
,
TypeAuteur.
Poete),
new
Author
(
"Dickens"
,
"Charles"
,
"Un chant de noël"
,
TypeAuteur.
Ecrivain),
new
Author
(
"Hugo"
,
"Victor"
,
"Les travailleurs de la mer"
,
TypeAuteur.
Ecrivain),
new
Author
(
"Van Beethoven"
,
"Ludwig"
,
"L'hymne à la joie"
,
TypeAuteur.
Musicien),
new
Author
(
"Gordon Byron"
,
"George"
,
"Don Juan"
,
TypeAuteur.
Poete),
new
Author
(
"Shelley"
,
"Marie"
,
"frankenstein"
,
TypeAuteur.
Ecrivain)")
};
liste.
ItemsSource =
auteurs;
}
…
Affectez la liste d'auteurs à la propriété ItemsSource de la première instance de ListBox comme montré dans le code ci-dessus. Écoutez l'événement TextChanged diffusé par le champ de saisie. Dans la méthode d'écoute, nous allons créer notre requête LINQ. Il nous faut toutefois référencer l'espace de noms System.Linq. Voici un exemple de requête simple défini dans la méthode d'écoute :
private
void
FilterChanged
(
object
sender,
TextChangedEventArgs e)
{
string
filtre =
filtreTxt.
Text;
var
query =
from
auteur
in
auteurs
where
auteur.
Oeuvre.
Contains
(
filtre)
select
auteur;
listeFiltrees.
ItemsSource =
query ;
}
Vous remarquez tout d'abord l'utilisation du mot-clé var, l'inférence de type nous évite de préciser le type de l'objet récupéré par la requête. Dans tous les cas, elle est de type IEnumerable ce qui est bien suffisant pour la réutiliser ou l'affecter par la suite. Vous pouvez envisager une requête LINQ un peu comme une boucle foreach d'un point de vue interprétation. La requête ci-dessus peut être traduite par la phrase suivante : pour chaque objet récupéré dans la liste auteurs, lorsque la propriété Oeuvre de cet objet contient la chaîne de caractères saisie, alors on récupère cet objet comme nouvel enregistrement de la requête. Cela peut paraître assez étrange de prime abord, car le mot-clé select est situé à la fin de la requête. Cette notation est toutefois légitime, car LINQ est un langage de requêtes intégré au code logique, cette écriture est vraiment proche des principes d'une boucle. Une fois la requête formalisée, vous pouvez l'affecter à la propriété ItemsSource de la seconde ListBox. Testez et compilez votre application : le filtre est pleinement fonctionnel (voir Figure 13.16).
Contrairement aux apparences, ce n'est pas parce que les types ne sont pas précisés qu'il n'y a pas de typage fort. Ainsi, vous pourriez ne récupérer que le nom ou le prénom de l'auteur en précisant l'une de ces deux propriétés en fin de requête. L'IntelliSense de Visual Studio reste disponible et vous aide dans cette démarche puisque le type Author est récupéré dynamiquement (voir Figure 13.17).
Finalement, l'utilisation d'une inférence de type couplée à LINQ est adéquate, car elle apporte beaucoup de flexibilité au développeur. Mise à part cette formalisation un peu spécifique, vous retrouverez de nombreuses clauses et des mots-clés similaires à ceux trouvés dans une syntaxe SQL classique. Ainsi, where est la clause par excellence puisqu'elle permet de filtrer les éléments simplement. Elle peut être suivie de plusieurs expressions qui seront séparées par les opérateurs logiques ||, && et ! (signifiant respectivement ou, et, opposé de) comme montré ci-dessous :
var query =
from auteur in auteurs
where auteur.
Oeuvre.Contains
(
filtre)
||
auteur.
Nom.Contains
(
filtre)
select
auteur;
Placez une instance de type ComboBox sur LayoutRoot et nommez-la typeCombo. Elle va nous permettre de filtrer les artistes en fonction de leur domaine respectif. Ajoutez trois éléments à cette liste déroulante par un clic droit depuis l'interface de Blend. Nommez les objets ComboBoxItem dans l'ordre suivant : Musicien, Écrivain, Poète. Nous allons nous servir de l'index de chaque élément dans la liste comme correspondance avec l'une des valeurs de l'énumération. Écoutez l'événement SelectionChanged de la liste déroulante. Nous pouvons maintenant réécrire notre requête dans les deux méthodes d'écoute :
var query =
from auteur in auteurs
where auteur.
Oeuvre.Contains
(
filtreTxt.
Text)
&& (
typeCombo.
SelectedIndex == (
int)auteur.
Type
||
typeCombo.
SelectedIndex ==
-
1
)
select
auteur;
listeFiltrees.
ItemsSource =
query;
Lorsqu'aucun type n'est choisi dans la liste déroulante, l'index sélectionné correspond à une valeur de -1. C'est pour cette raison que nous utilisons l'opérateur logique || qui permet de renvoyer true lorsqu'une des deux expressions à droite ou à gauche est vérifiée. Nous pourrions également appliquer un tri à la liste des auteurs sélectionnés :
var query =
from auteur in auteurs
where auteur.
Oeuvre.Contains
(
filtreTxt.
Text)
&& (
typeCombo.
SelectedIndex == (
int)auteur.
Type
||
typeCombo.
SelectedIndex ==
-
1
)
orderby auteur.
Prenom
select
auteur;
Par défaut le tri est réalisé dans un ordre ascendant, il est possible de renverser l'ordre grâce au mot-clé descending à placer après le champ utilisé pour le tri. Dans un tout autre registre, la clause group by permet de regrouper les éléments filtrés selon un critère. La requête ci-dessous récupère le nombre d'artistes répartis par domaines artistiques, dont l'œuvre contient la chaîne de caractère renseignée :
var query =
from auteur in auteurs
where auteur.
Oeuvre.Contains
(
filtreTxt.
Text)
orderby auteur.
Prenom
group auteur by auteur.
Type into tmpGroup
select
tmpGroup.Count
(
);
Il est également possible de récupérer la totalité des auteurs triés par ordre alphabétique et par domaine artistique :
var query =
from auteur in auteurs
where auteur.
Oeuvre.Contains
(
filtreTxt.
Text)
&& (
typeCombo.
SelectedIndex == (
int)auteur.
Type
||
typeCombo.
SelectedIndex ==
-
1
)
orderby auteur.
Prenom
group auteur by auteur.
Type into tmpGroup
from artiste in tmpGroup
select
artiste;
Dans ce cas, on opère une nouvelle requête à partir du résultat de la première. Pour finir, la clause join permet d'opérer des jointures entre diverses sources de données, nous ne verrons pas son utilisation qui déborde un peu du sujet de ce livre. Sachez simplement que celle-ci est utile, au même titre que son homologue côté SQL, mais que la nature des différentes données reliées peuvent être diverses et variées. C'est l'un des grands avantages procurés par LINQ.
12-3-3. Expression lambda▲
Une autre manière de créer des requêtes avec LINQ consiste à utiliser les expressions lambda. Celles-ci sont apparues avec C# 3, vous pouvez les considérer comme des fonctions anonymes. L'opérateur => définit une expression lambda. Les délégations couplées aux fonctions anonymes offrent de nombreux avantages. Le code ci-dessous expose leur utilisation dans ce contexte ainsi que deux manières différentes de les coder :
//définition de la délégation
delegate
string
del
(
Author i);
public
partial
class
MainPage :
UserControl
{
//une première écriture possible
del nomParOeuvre =
x =>
x.
Nom +
"est l'auteur de :: "
+
x.
Oeuvre;
//une seconde délégation avec lambda expression
del prenomNom =
x =>
{
return
x.
Prenom +
" "
+
x.
Nom;
};
List<
Author>
auteurs;
public
MainPage
(
)
{
InitializeComponent
(
);
auteurs =
new
List<
Author>(
)
{
new
Author
(
"Mozart"
,
"Amadeus"
,
"La flute enchantée"
,
TypeAuteur.
Musicien),
new
Author
(
"Melville"
,
"Herman"
,
"Moby dick"
,
TypeAuteur.
Ecrivain),
new
Author
(
"Wilde"
,
"Oscar"
,
"Le portrait de Dorian Gray"
,
TypeAuteur.
Poete),
new
Author
(
"Dickens"
,
"Charles"
,
"Un chant de noël"
,
TypeAuteur.
Ecrivain),
new
Author
(
"Hugo"
,
"Victor"
,
"Les travailleurs de la mer"
,
TypeAuteur.
Ecrivain),
new
Author
(
"Van Beethoven"
,
"Ludwig"
,
"L'hymne à la joie"
,
TypeAuteur.
Musicien),
new
Author
(
"Gordon Byron"
,
"George"
,
"Don Juan"
,
TypeAuteur.
Poete),
new
Author
(
"Shelley"
,
"Marie"
,
"frankenstein"
,
TypeAuteur.
Ecrivain)
};
//on récupère la chaîne de caractères du premier délégué
string
chaine1 =
nomParOeuvre
(
auteurs[
3
]
);
//on récupère la chaîne de caractères du premier délégué
string
chaine2 =
prenomNom
(
auteurs[
7
]
);
Debug.
WriteLine
(
"chaine 1 :: {0}
\n
chaine 2 :: {1}"
,
chaine1,
chaine2);
Lorsque vous avez référencé l'espace de noms System.Linq, les objets de type IE-numerable ont reçu de nouvelles capacités par le biais de méthodes d'extension correspondantes aux clauses utilisées pour écrire une requête. Il devient dès lors possible de coupler l'utilisation de fonctions anonymes à LINQ, ce qui engendre un nombre impressionnant de possibilités. Il est ainsi facile de créer des conditions et des instructions élaborées. L'expression ci-dessous en est un exemple simple :
LambdaListeFiltrees.ItemsSource =
auteurs.Where(a => a.Oeuvre.Contains(filtre)).Select(a=>a.Prenom);
La variable notée arbitrairement a, correspond à chaque élément parcouru au sein de la liste auteurs. L'expression placée à droite de l'opérateur => est appelée prédicat. Ce terme désigne une fonction qui renvoie un booléen. Grâce à la seconde écriture possible d'une expression lambda ((a)=>{return a;}), vous pouvez réécrire entièrement la requête LINQ :
LambdaListeFiltrees.
ItemsSource =
auteurs.
Where
(
(
a) =>
{
if
(
a.
Oeuvre.
Contains
(
filtre))
{
if
(
typeCombo.
SelectedIndex ==
-
1
)
{
return
true
;
}
else
{
return
typeCombo.
SelectedIndex ==
(
int
)a.
Type;
}
}
else
{
return
false
;
}
}
).
Select
(
a =>
a.
Prenom);
La fonction anonyme retourne bien un booléen, ce qui est attendu par la clause Where. Le code ci-dessus peut être amélioré en renvoyant directement l'expression booléenne :
LambdaListeFiltrees.
ItemsSource =
auteurs.
Where
(
(
a) =>
{
if
(
a.
Oeuvre.
Contains
(
filtre))
{
return
typeCombo.
SelectedIndex ==
-
1
||
typeCombo.
SelectedIndex ==
(
int
)a.
Type;
}
else
{
return
false
;
}
}
).
Select
(
a =>
a.
Prenom);
Comme nous l'avons expliqué précédemment, un prédicat correspond à une fonction renvoyant un booléen. Dès lors, ce qui fait d'une fonction un prédicat est tout simplement son contexte d'utilisation. Lorsque les conditions de la clause where deviennent complexes, il convient de formaliser le prédicat sous cette forme. Le code est plus lisible de cette manière, c'est ce qui est réalisé ci-dessous :
…
LambdaListeFiltrees.
ItemsSource =
auteurs.
Where
(
(
a) =>
FiltreAuteurParType
(
filtre,
a))
.
Select
(
a =>
a.
Prenom);
}
private
bool
FiltreAuteurParType
(
string
filtre,
Author a)
{
if
(
a.
Oeuvre.
Contains
(
filtre))
{
return
typeCombo.
SelectedIndex ==
-
1
||
typeCombo.
SelectedIndex ==
(
int
)a.
Type;
}
else
{
return
false
;
}
}
Le projet finalisé LinqToObject.zip se trouve dans le dossier chap13 des exemples.
Maintenant que nous avons étudié les différents outils nous permettant de créer des requêtes, nous allons revenir sur notre précédent projet MVVM et traduire le contenu XML en objets C#.
12-3-4. Consommer du XML avec LINQ▲
Le XML est le format de prédilection lorsqu'il s'agit de diffuser des données facilement interprétables. Il permet, d'une part de structurer l'information simplement et d'autre part, il peut être parcouru par des fonctions récursives même lorsque l'on ne connaît pas son contenu à l'avance. L'exemple ci-dessous expose un extrait de données au format XML récupérées depuis l'adresse : http://www.tweened.org/wp-content/uploads/videos/videos.xml :
<VIDEOS>
…
<Video
visible
=
"true"
id
=
"13"
titre
=
"Gestionnaire d'états visuels"
duree
=
"00:07:13"
date
=
"08/15/2008"
>
<Auteur
nom
=
"Ambrosi"
prenom
=
"Eric"
Blog
=
"http://www.tweened.org"
/>
<description>
<![CDATA[
Utilisation avancée du Visual State Manager
]]>
</description>
<PreRequis>
<PR>
12</PR>
</PreRequis>
<file>
14_advanceVisualStateManager.wmv</file>
<baseurl>
http://www.tweened.org/wp-content/uploads/videos/</baseurl>
<filethumb>
14_advanceVisualStateManager_0.000.jpg</filethumb>
</Video>
<Video
visible
=
"false"
id
=
"14"
titre
=
"Créer un flux Json"
duree
=
"00:05:09"
date
=
"08/16/2008"
>
<Auteur
nom
=
"Ambrosi"
prenom
=
"Eric"
Blog
=
"http://www.tweened.org"
/>
<description>
<![CDATA[
Qu'est-ce que le Json et comment créer un flux Json avec php 5 et mysql
]]>
</description>
<PreRequis>
<PR></PR>
</PreRequis>
<file>
15_createJsonStream.wmv</file>
<baseurl>
http://www.tweened.org/wp-content/uploads/videos/</baseurl>
<filethumb>
15_createJsonStream_0.000.jpg</filethumb>
</Video>
…
</VIDEOS>
Notre objectif est de récupérer ce flux et d'alimenter le composant ListBox du précédent projet. Récupérez le précédent projet MVVM ou téléchargez le projet MVVM_PatternWebClient.zip présent dans le dossier chap13 des exemples.
La première chose à faire est de référencer les espaces de noms adéquats. Nous avons besoin de System.Linq mais cela ne suffit pas, il nous faut également utiliser la bibliothèque C# (fichier dll) System.Xml.Linq et l'espace de noms correspondant (voir Figure 13.18).
À partir de là, nous pouvons commencer à parcourir le document XML dans la méthode de réception wc_OpenReadCompleted. Tout est contenu dans la propriété Result de l'objet événementiel. Il nous faut un objet XML contenant les données, l'espace de noms System-.Xml.Linq donne accès à de nouveaux objets préfixés d'un X. Ainsi deux d'entre eux, XDocument et XElement, ont la capacité d'écrire du XML à partir du flux binaire récupéré. Nous utiliserons plutôt la méthode Load de XElement afin de simplifier le chemin d'accès aux nœuds XML enfants. Vous pouvez vérifier que le document est correctement interprété en le traçant comme montré dans le code qui suit :
static
void
wc_OpenReadCompleted
(
object
sender,
OpenReadCompletedEventArgs e)
{
if
(
e.
Error !=
null
)
{
throw
e.
Error;
}
else
if
(
e.
Cancelled)
{
//l'appel asynchrone a été annulé
}
else
{
ParseXMLResult
(
e);
}
}
private
static
void
ParseXMLResult
(
OpenReadCompletedEventArgs e)
{
XElement xmlResult =
XElement.
Load
(
e.
Result);
Debug.
WriteLine
(
xmlResult.
ToString
(
));
}
Si tout se passe bien, la fenêtre de sortie affiche le contenu du document (voir Figure 13.19).
Nous allons maintenant sérialiser chaque élément en objet C# Media. Pour cela, nous allons parcourir les collections XML fournies par LINQ avec une simple requête. Pour récupérer une collection contenant un type de nœud XML précis, le plus simple consiste à utiliser la méthode Descendants. Elle prend une chaîne de caractères comme argument, qui correspond au nom du nœud que vous souhaitez récupérer dans la collection. Pour chaque élément récupéré dans cette collection, nous pouvons instancier la classe Media. Il reste ensuite à affecter la requête à notre collection observable de type ObservableCollection<Media>. Toutefois, même si le parseur de Visual Studio ne détecte aucune erreur lors de l'affectation de Datas, le code ci-dessous ne fonctionnera pas à l'exécution :
private
static
void
ParseXMLResult
(
OpenReadCompletedEventArgs e)
{
XElement xmlResult =
XElement.
Load
(
e.
Result);
var
query =
from
media in
xmlResult.
Descendants
(
"Video"
)
select
new
Media
(
);
Datas =
(
ObservableCollection<
Media>
)query;
}
Il n'est pas possible de transformer un type IEnumerable en ObservableCollection. De plus, la variable query ne possède aucune méthode équivalente à ToList ou ToArray permettant de renvoyer une collection observable. Cette problématique étant assez standard, la bibliothèque SL-Extensions fournit une classe ObservableCollection implémentant de nombreuses fonctionnalités supplémentaires. Elle possède notamment la méthode AddRange que nous allons utiliser. C'est pour cette raison que nous ne faisons pas référence à l'espace de noms standard System.Collections.ObjectModel pour utiliser des collections de ce type, mais plutôt à celui de SL-Extensions SLExtensions.Collections.ObjectModel. La méthode AddRange récupère d'un seul coup tous les éléments contenus dans query :
private
static
void
ParseXMLResult
(
OpenReadCompletedEventArgs e)
{
XElement xmlResult =
XElement.
Load
(
e.
Result);
var
query =
from
media in
xmlResult.
Descendants
(
"Video"
)
select
new
Media
(
);
Datas.
AddRange
(
query);
}
Si vous compilez tout se déroule bien, mais rien n'apparaît en apparence dans la liste. En apparence seulement, car si vous cliquez dans la liste, vous sélectionnerez une ligne d'enregistrement. Celle-ci est vide, car nous n'avons pas réellement affecté les propriétés de chaque Media. Pour cela nous pouvons utiliser un objet d'initialisation et les méthodes XML fournies par LINQ :
private
static
void
ParseXMLResult
(
OpenReadCompletedEventArgs e)
{
XElement xmlResult =
XElement.
Load
(
e.
Result);
var
query =
from
media in
xmlResult.
Descendants
(
"Video"
)
select
new
Media
(
)
{
Titre =
(
string
)media.
Attribute
(
"titre"
),
Description =
(
string
)media.
Element
(
"description"
),
BaseUrl =
(
string
)media.
Element
(
"baseurl"
),
FichierMedia =
(
string
)media.
Element
(
"file"
),
FichierVignette =
(
string
)media.
Element
(
"filethumb"
),
Auteur =
new
Author
(
(
string
)media.
Element
(
"Auteur"
).
Attribute
(
"nom"
),
(
string
)media.
Element
(
"Auteur"
).
Attribute
(
"prenom"
),
(
string
)media.
Element
(
"Auteur"
).
Attribute
(
"blog"
)
),
Date =
(
string
)media.
Element
(
"Auteur"
).
Attribute
(
"date"
),
Duree =
TimeSpan.
Parse
((
string
)media.
Attribute
(
"duree"
))
};
Datas.
AddRange
(
query);
}
Recompilez l'application et cliquez sur le bouton afin de charger les données dans la liste. Comme vous le constatez, celle-ci est désormais remplie correctement. Le projet finalisé et légèrement complété, MVVM_PatternXML.zip, est disponible dans le dossier chap13 des exemples.
12-3-5. Charger un flux JSON▲
JSON est un format de présentation des données au même titre que XML à la différence près que cet héritage direct de JavaScript. JSON signifie JavaScript Object Normalisation. Silverlight possède la capacité de consommer du JSON par différents moyens que nous allons étudier.
12-3-5-1. Pourquoi utiliser JSON▲
XML est très puissant lorsqu'il s'agit de présenter des données de manière structurée, car il repose sur un principe de relations familiales. Il est donc assez légitime de savoir en quoi JSON peut être compétitif. Tout d'abord, au même titre que XML, JSON ne conserve pas les types. Toute donnée contenue est de type string. Les données sont, par exemple, formalisées à travers des objets anonymes. L'objet anonyme ci-dessous décrit une œuvre :
{
"peintre"
:
"Vassily Kandinski"
,
"titre"
:
"Jaune-rouge-bleu"
,
"annee"
:
"1925"
,
"type"
:
"abstraction lyrique"
}
Il est également possible de décrire des listes d'objets anonymes :
[
{
"peintre"
:
"Vassily Kandinski"
,
"titre"
:
"Jaune-rouge-bleu"
,
"annee"
:
"1925"
,
"type"
:
"abstraction lyrique"
},
{
"peintre"
:
"Vassily Kandinski"
,
"titre"
:
"Dans le gris"
,
"annee"
:
"1919"
,
"type"
:
"abstraction lyrique"
},
{
"peintre"
:
"Vassily Kandinski"
,
"titre"
:
"Avec l'arc noir"
,
"annee"
:
"1912"
,
"type"
:
"abstraction lyrique"
}
]
Ainsi, les médias que nous avions récupérés précédemment au format XML pourraient être traduits en JSON de cette façon :
[
{
"titre"
:
"Gestionnaire d'état visuel"
,
"duree"
:
"00:07:13"
,
"baseurl"
:
"http://www.tweened.org/wp-content/uploads/
videos/"
,
"file"
:
"14_advanceVisualStateManager.
wmv"
,
"filethumb"
:
"14_advanceVisualStateManager_0.000.
jpg"
,
"description"
:
"Utilisation avancée du Visual State
Manager"
,
"date"
:
"08/15/2008"
},
{
"titre"
:
"Créer un flux Json"
,
"duree"
:
"00:05:09"
,
"baseurl"
:
"http://
www.tweened.org/wp-content/uploads/videos/"
,
"file"
:
"15_
createJsonStream.wmv"
,
"filethumb"
:
"15_createJsonStream_0.000.
jpg"
,
"description"
:
"Qu'est-ce que le Json et comment créer un
flux Json avec php 5 et mysql"
,
"date"
:
"08/15/2008"
}
]
JSON est en réalité très proche d'une représentation plate de type base de données, les objets du tableau représentent l'équivalent des enregistrements contenus dans une table SQL. Comme vous l'aurez compris, le premier avantage de JSON est d'être moins verbeux que XML. On peut considérer un gain entre 30 et 40 % de poids en moins comparé à XML. C'est autant de performances de bande passante gagnée. Toutefois, le but n'est pas ici d'établir une quelconque compétition entre ces deux formats. Il faut simplement concevoir que XML est là, non seulement pour transmettre des données, mais également pour leur donner un sens et une structure logique.
Ainsi, XML vous oblige à présenter vos données à un niveau plus élevé que JSON, puisque les relations familiales entre chaque nœud déterminent une certaine logique entre chacun d'eux. Avec XML, vous prenez parti sur la manière dont les données sont présentées. Pas avec JSON : ce dernier est une représentation de table SQL qui n'a d'autre objectif que transférer des données. Les 30 ou 40 % que vous gagnerez le seront donc forcément au détriment de liens logiques, définis entre les données, tels que vous les trouvez dans XML. C'est pourquoi il faut choisir l'un ou l'autre de ces formats selon le cas de figure. Alors que XML est largement répandu quel que soit l'environnement de développement, à travers des services Web, des flux RSS ou au sein d'applications bureautiques, JSON est réellement lié à Internet et aux environnements de développement tels que PHP ou MySQL. Il est toutefois intéressant de savoir que Windows Communication Foundation est capable de transmettre des données dans ce type de format. De nombreuses bibliothèques existent sous PHP, mais la plus efficace en termes de sérialisation est sans doute celle fournie nativement par PHP 5, car elle est codée en langage C. Vous trouverez de nombreux tests de performance à ce sujet sur Internet. Encoder un enregistrement de base de données en JSON est réellement très simple avec PHP. C'est en partie ce qui fait le succès de ce format, le script ci-dessous le démontre :
$maConnexion
=
new MysqlConnect("
localhost
"
,
"
root
"
,
""
,
"
catalogue
"
);
$req
=
"
SELECT titre as Titre,commentaire as Commentaire,url_image as UrlImage,date as Date,artiste.nom as Nom,prenom as Prenom,style.nom as NomStyle
FROM disque, artiste, style
WHERE disque.idArtiste = artiste.idArtiste AND disque.idStyle = style.idStyle
"
;
//on supprime le fichiers mis en cache
header("
Cache-Control: no-cache, must-revalidate
"
);
header("
Content-Type: text/plain
"
);
$res
=
mysql_query($req
) or die (mysql_error());
$tabDisque
=
array();
while($ligne
=
mysql_fetch_array($res
) )
{
foreach( $ligne
as $key
=>
$value
)
{
$ligne
[
$key
]
=
utf8_encode($value
);
}
array_push ($tabDisque
,
$ligne
);
}
echo json_encode($tabDisque
);
En quelques lignes, nous encodons la totalité des enregistrements renvoyés par une requête sans effort. Il faudra toutefois faire attention à certains détails sous PHP. Il est tout d'abord impossible d'encoder un retour de requête MySQL directement. Vous devrez parcourir l'objet MySQL réceptionné et réaffecter chaque ligne contenue dans un nouveau tableau, grâce à une boucle, par exemple. De plus, comme nous l'avons déjà précisé, Silverlight fonctionne par défaut en UTF-8, il est donc nécessaire d'encoder chaque enregistrement en UTF-8 avant de renvoyer le tableau à Silverlight.
12-3-5-2. Contrat de sérialisation▲
Commencez par télécharger le projet MVVM_PatternJSON.zip depuis les exemples du livre. Nous allons récupérer le flux JSON situé dans le répertoire ClientBin du projet correspondant au site Web. Il est tout d'abord important de référencer deux bibliothèques externes nommées System.Json.dll et System.ServiceModel.Web.dll (voir Figure 13.20). La première va nous permettre de créer des objets JSON via C#, la seconde sera utile dans un second temps, afin de sérialiser en seulement deux lignes la totalité du contenu JSON récupéré. Ces dernières permettent respectivement l'importation des espaces de noms System.Json et System.Runtime.Serialization.Json, via l'instruction using.
Le membre statique de notre classe MediasDatas nommé URL_MEDIA doit être modifié pour cibler notre fichier JSON :
Nous avons plusieurs techniques pour consommer un flux JSON. La première consiste à parcourir notre tableau d'objets anonymes et à transformer chaque enregistrement dynamiquement en objets typés Media. Attention, bien que portant le même nom que dans le projet dédié au chargement XML, cette classe est très différente :
public
class
Media
{
public
string
Titre {
get
;
set
;
}
public
string
UrlImage {
get
;
set
;
}
public
string
Commentaire {
get
;
set
;
}
public
string
Date {
get
;
set
;
}
public
string
Nom {
get
;
set
;
}
public
string
Prenom {
get
;
set
;
}
public
string
NomStyle {
get
;
set
;
}
public
override
string
ToString
(
)
{
return
Titre +
" - Date :: "
+
Date;
}
}
Notre classe MediasDatas n'est que très peu modifiée finalement. L'écouteur de réception des données déclenche la méthode ParseJSONResult au lieu de la méthode ParseXML-Result :
static
void
wc_OpenReadCompleted
(
object
sender,
OpenReadCompletedEventArgs e)
{
if
(
e.
Error !=
null
)
{
throw
e.
Error;
}
else
if
(
e.
Cancelled)
{
//l'appel asynchrone a été annulé
}
else
{
ParseJSONResult
(
e);
}
}
private
static
void
ParseJSONResult
(
OpenReadCompletedEventArgs e)
{
//ici on traite le flux de données contenu dans
//l'objet événementiel
}
À ce stade, il existe différentes manières de gérer les données au format JSON, en parcourant les objets JSON de type JsonArray ou JsonObject grâce à une boucle, via LINQ pour JSON (disponible grâce à l'espace de noms System.Linq) ou grâce à un contrat de sérialisation. Les deux premières méthodologies sont un peu rébarbatives et fastidieuses lorsqu'on les compare à la troisième. Le code ci-dessous illustre la première solution :
/*Méthode 1 avec boucle foreach*/
//on commence par récupérer le tableau d'objets
JsonArray arr =
(
JsonArray)JsonArray.
Load
(
e.
Result as
Stream);
//et on le parcourt
foreach
(
JsonObject media in
arr)
{
Datas.
Add
(
new
Media
(
)
{
Commentaire =
media[
"Commentaire"
],
Date =
media[
"Date"
],
Nom =
media[
"Nom"
],
NomStyle =
media[
"NomStyle"
],
Prenom =
media[
"Prenom"
],
Titre =
media[
"Titre"
],
UrlImage =
media[
"UrlImage"
],
}
);
}
Comme vous le constatez, on accède à la valeur de chaque propriété d'objet grâce à une clé d'accès de type string. Le terme à gauche du signe égal correspond à une propriété de la classe Media, le terme à droite correspond à la valeur récupérée côté JSON. Vous remarquez qu'il n'est pas nécessaire de transtyper la valeur récupérée dans notre cas. Cela est valable, car toutes les propriétés de la classe Media qui sont affectées sont de type String, ce qui est renvoyé nativement par les propriétés d'un objet JSON. Une autre méthode consiste à utiliser LINQ avec les collections JSON, cela est assez similaire mais permet en plus de filtrer les résultats plus efficacement :
JsonArray arr = (
JsonArray)JsonArray.Load
(
e.
Result as Stream);
//LINQ à faire
var query =
from media in arr
where
((
string)media[
"Nom"
]
).Contains
(
"a"
)
select
new Media
(
)
{
Commentaire =
media[
"Commentaire"
],
Date =
media[
"Date"
],
Nom =
media[
"Nom"
],
NomStyle =
media[
"NomStyle"
],
Prenom =
media[
"Prenom"
],
Titre =
media[
"Titre"
],
UrlImage =
media[
"UrlImage"
],
};
Datas.AddRange
(
query);
Cela est facile à faire, car LINQ possède la capacité de parcourir n'importe quel type de liste implémentant IEnumerable ce qui est le cas des objets JSON de type JsonArray ou JsonObject. Une dernière méthode est bien plus efficace et rapide. Elle consiste à créer un contrat de sérialisation. Cela est également réalisable avec XML mais légèrement plus contraignant. Côté JSON, il suffira de s'arranger pour que les propriétés des objets JSON et de la classe ciblée possèdent également les mêmes noms. Une fois que vous vous êtes assuré de cela, le reste est trivial :
//on crée un contrat de sérialisation
DataContractJsonSerializer contrat =
new
DataContractJsonSerializer
(
typeof
(
List<
Media>
));
//on parcourt l'objet grâce à la méthode ReadObject qui se charge
//des correspondances et on appelle la méthode AddRange
//de la collection observable
Datas.
AddRange
(
(
List<
Media>
)contrat.
ReadObject
(
e.
Result as
Stream) );
Le code réel prend deux lignes en tout et pour tout ce qui est assez appréciable. Le contrat de sérialisation JSON est un dérivé simplifié des bibliothèques existantes pour d'autres types de données. Cela pour la bonne raison que la structure même du JSON est tellement simple qu'il n'y a qu'un seul type de propriété. Au contraire, dans XML une valeur peut être contenue dans un nœud élément, dans un attribut ou dans une balise CDATA. Il est du coup nécessaire de modifier la classe en ajoutant des métadonnées en en-tête de propriété et de classe.
Le projet MVVM_PatternJSONFinal.zip est dans le dossier chap13 des exemples de cet ouvrage.
Nous allons maintenant évoquer les mécanismes de sécurité du lecteur Silverlight dans un contexte de chargement de données.
12-3-6. Sécurité interdomaines▲
Tout au long de ce chapitre, nous avons chargé des médias ou des données sans réellement nous préoccuper des contraintes liées à la sécurité. Nous sommes partis du fait que nous chargions des données présentes sur le même site, dans une sous-arborescence de ce dernier ou à sa racine. Ce n'est toutefois pas représentatif des applications riches qui ont souvent pour vocation de charger du contenu très varié à partir de sources très éclatées. Les notions de cloud computing et de services Web impliquent directement le partage de ressources interdomaines. Toutefois, pour des raisons de sécurité, la politique par défaut des plates-formes de développement comme Silverlight est d'encadrer ce modèle afin de limiter les abus, les fraudes et le piratage en tout genre.
Depuis Silverlight, il est impossible de télécharger des données présentes dans un répertoire parent de celui où est situé le fichier xap, en utilisant la syntaxe d'accès relatif ../. Ceci pour la bonne raison que le lecteur Silverlight pourrait atteindre des données en dehors de ce qui est accessible à la racine du site, donc dans l'arborescence non publique du serveur.
La méthodologie permettant de renforcer la sécurité est assez simple. Concrètement, chaque site est responsable du type de contenu et de connexion qu'il partage et qu'il laisse à disposition des autres sites ou des services Web externes. Dans la grande majorité des cas, les sites n'autorisent pas le chargement de leur contenu externe, car lorsqu'aucune configuration n'a été mise en place, le comportement par défaut est de refuser tous chargements extérieurs. Chaque administrateur de site Web peut décider de mettre à disposition ou non des données à l'extérieur via la création d'un fichier dédié que nous allons étudier. Dans tous les cas la machine cliente n'a aucune prise sur ce comportement. Reprenez le projet MVVM_PatternJSON et modifiez l'adresse de chargement du fichier json.txt comme exposé ci-dessous :
public
static
Uri URL_MEDIA =
new
Uri
(
"http://referencesilverlight.tweened.org/json.txt"
,
UriKind.
RelativeOrAbsolute);
Compilez, puis chargez les données. Comme vous le constatez, l'application lève une erreur de sécurité dont le détail n'est pas vraiment explicite. Vous avez essayé de charger du contenu externe à Silverlight depuis le site http://referencesilverlight.tweened.org, qui ne le permet pas d'emblée (voir Figure 13.21).
En réalité, Silverlight ne s'autorise pas ce type de téléchargement sans en avoir expressément la permission du site contenant les données distante. Il va essayer de trouver et d'interpréter l'un des deux fichiers qui décrivent les règles d'accès. Si aucun d'eux n'est présent, le contenu distant n'est pas autorisé à être chargé. Le premier est nommé crossdomain.xml et fut apporté il y a quelques années par la plate-forme Flash. Celle-ci connaît des contraintes équivalentes à Silverlight dans ce domaine. Pour des raisons de comptabilité et afin de faciliter le travail de chacun, Silverlight est capable d'interpréter la balise allow-access-from contenue dans ce type de fichiers.
Afin de concrétiser ce concept, téléchargez le fichier crossdomain.xml présent à la racine du site de Twitter : http://www.twitter.com/crossdomain.xml :
<cross-domain-policy
xsi
:
noNamespaceSchemaLocation
=
"http://www.adobe.com/xml/schemas/PolicyFile.xsd"
>
<allow-access-from
domain
=
"twitter.com"
/>
<allow-access-from
domain
=
"api.twitter.com"
/>
<allow-access-from
domain
=
"search.twitter.com"
/>
<allow-access-from
domain
=
"static.twitter.com"
/>
<site-control
permitted-cross-domain-policies
=
"master-only"
/>
<allow-http-request-headers-from
domain
=
"*.twitter.com"
headers
=
"*"
secure
=
"true"
/>
</cross-domain-policy>
Pour ceux qui ne connaissent pas Twitter, sachez qu'il s'agit d'un site et d'un réseau de micro-bloging. Son principe repose sur la diffusion d'informations concentrées et limitées en chaîne de caractères. Il suffit de s'abonner au compte d'une personne sur Twitter pour recevoir les informations que ce dernier propage en temps réel sous la forme d'un tchat multiutilisateur. De nombreuses plates-formes proposent des applications Twitter et reposent sur l'API qu'il fournit.
Ce genre de fichiers déclaratifs est accessible à tous mais cela ne pose aucun problème. Il est important que les règles de sécurité soient exposées très clairement, et permettent à tous les développeurs de connaître leurs droits et contraintes d'accès au serveur. Ce document déclaratif est assez simple à lire, il autorise de nombreux sous-domaines de Twitter à accéder aux données. Vous remarquez à ce propos que le sous-domaine api.twitter.com fait partie de cette liste. Il est nécessaire de spécifier les droits pour chaque domaine et sous-domaine. Le protocole, le port ou le serveur utilisé, sont autant de paramètres qu'il faudra également gérer d'un point de vue accès. Un deuxième type de fichier propre à Silverlight est recherché par défaut, et cela avant le crossdomain.xml. Il se nomme clientaccesspolicy.xml. Ce fichier est appelé en premier, s'il n'est pas trouvé ce sont les réglages du crossdomain.xml qui s'appliquent. Ce fichier ajoute quelques fonctionnalités comme la capacité de gérer chaque sous-dossier de manière indépendante. Modifiez l'URL du fichier JSON afin de cibler le site www.tweened.org :
public
static
Uri URL_MEDIA =
new
Uri
(
"http://www.tweened.org/json.txt"
,
UriKind.
RelativeOrAbsolute);
Cette fois-ci tout se passe bien à la compilation, car un fichier clientaccesspolicy.xml est présent à la racine de www.tweened.org. Ce fichier laisse la porte ouverte, car il n'a pas de contenu réellement sensible. Voici ce qu'il contient :
<?xml version="1.0" encoding="utf-8"?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from
http-request-headers
=
"*"
>
<domain
uri
=
"*"
/>
</allow-from>
<grant-to>
<resource
path
=
"/"
include-subpaths
=
"true"
/>
</grant-to>
</policy>
</cross-domain-access>
</access-policy>
Concrètement, les droits définissent un accès total quel que soit le domaine à l'origine de la demande et cela pour la totalité des répertoires et des sous-répertoires du site www.tweened.org.