Bonjour à tous et à toutes.
WMF / Modèle MVVM
Lien de référence : wpf Creating Parameterized Styles With Attached Properties/
Voici le probblème auquel je me suis heurté :
J'utilise 3 tooltips dans mon application : un bleu (information), un orange (warning) et un rouge (erreur).
Les tooltips rouges et bleus sont affiché sur une petite puce affichée à chaque fois qu'un champ est en erreur : le vueModel implémente l'interface IDataErrorInfo.
C'est assez basique.
Sauf que les 3 styles de tooltips doivent être écrit 3 fois, un pour chaque style alors qu'il n'y a pas grand chose comme différence (couleurs / image / texte).
Je me suis donc penché sur wpf Creating Parameterized Styles With Attached Properties/ qui aurait pu être utilisé.
Mais créer une classe Thème et être obligé d'ajouter un nouveau champ à chaque nouvelle valeur dans le template ne me convenait pas, je préfère être FULL xaml.
J'ai donc créer une classe TemplateProperties qui contient des valeurs qui peuvent être chargées par le template.
Voici le code et le xaml que je vais commenter, même si je ne comprend pas tout... (même si c'est moi qui ai tout fait)
Le XAML qui contient tous mes styles :
Et le code :Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="DataManager.Styles.StyleDefaultImplementation" xmlns:local="clr-namespace:DataManager" xmlns:toolkit="clr-namespace:LE_NOM_DE_MON_ASSEMBLY;assembly=WPFExtToolkit"> <!-- ================================================================================================================================== --> <!-- --> <!-- Tooltips plus jolis que ceux par défaut (template) --> <!-- --> <!-- ================================================================================================================================== --> <ControlTemplate TargetType="ToolTip" x:Key="TooltipTemplate"> <Border CornerRadius="7" HorizontalAlignment="Center" VerticalAlignment="Top" Padding="5,5,10,10" BorderThickness="1,1,1,1" BorderBrush="Black" Margin="0,0,25,25"> <Border.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(toolkit:TemplateProperties.TemplateValue), Converter={x:Static toolkit:TemplatePropertiesConverter.Converter}, ConverterParameter='GradientStartColor'}" Offset="0"/> <GradientStop Color="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(toolkit:TemplateProperties.TemplateValue), Converter={x:Static toolkit:TemplatePropertiesConverter.Converter}, ConverterParameter='GradientStopColor'}" Offset="1"/> </LinearGradientBrush> </Border.Background> <Border.Effect> <DropShadowEffect BlurRadius="10" ShadowDepth="5" Color="#6B6B6B"/> </Border.Effect> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Image Grid.Row="0" Grid.Column="0" Margin="0,2,0,2" Stretch="None" VerticalAlignment="Center" Source="{TemplateBinding toolkit:TemplateProperties.TemplateValue, Converter={x:Static toolkit:TemplatePropertiesConverter.Converter}, ConverterParameter='Image'}"/> <TextBlock FontFamily="Tahoma" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="1" Margin="5,2,0,2" FontSize="13" FontWeight="Bold" VerticalAlignment="Center" TextAlignment="Left" Text="{TemplateBinding toolkit:TemplateProperties.TemplateValue, Converter={x:Static toolkit:TemplatePropertiesConverter.Converter}, ConverterParameter='Title'}" Foreground="{TemplateBinding toolkit:TemplateProperties.TemplateValue, Converter={x:Static toolkit:TemplatePropertiesConverter.Converter}, ConverterParameter='TitleColor'}"/> <TextBlock FontFamily="Tahoma" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Margin="0,5,0,0" FontSize="11" Text="{TemplateBinding Content}" Foreground="#000000" TextAlignment="Left"/> </Grid> </Border> </ControlTemplate> <!-- ================================================================================================================================== --> <!-- --> <!-- Tooltips plus jolis que ceux par défaut (Information) --> <!-- --> <!-- ================================================================================================================================== --> <Style TargetType="ToolTip" x:Key="{x:Type ToolTip}"> <Setter Property="HasDropShadow" Value="True"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="Title|Information"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="Image|pack://siteoforigin:,,,/Images/IconInformation.png"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="TitleColor|#003399"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="GradientStartColor|#FFFAFFFF"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="GradientStopColor|#FFD3F4FF"/> <Setter Property="Template" Value="{StaticResource TooltipTemplate}"/> </Style> <!-- ================================================================================================================================== --> <!-- --> <!-- Tooltips plus jolis que ceux par défaut (Warning) --> <!-- --> <!-- ================================================================================================================================== --> <Style TargetType="ToolTip" x:Key="ToolTipWarning"> <Setter Property="HasDropShadow" Value="True"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="Title|Avertissement"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="Image|pack://siteoforigin:,,,/Images/IconWarning.png"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="TitleColor|#FF7F00"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="GradientStartColor|#FFFAFFFF"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="GradientStopColor|#FFFFDBBF"/> <Setter Property="Template" Value="{StaticResource TooltipTemplate}"/> </Style> <!-- ================================================================================================================================== --> <!-- --> <!-- Tooltips plus jolis que ceux par défaut (Erreur) --> <!-- --> <!-- ================================================================================================================================== --> <Style TargetType="ToolTip" x:Key="ToolTipError"> <Setter Property="HasDropShadow" Value="True"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="Title|Erreur"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="Image|pack://siteoforigin:,,,/Images/IconError.png"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="TitleColor|#FF3232"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="GradientStartColor|#FFFAFFFF"/> <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="GradientStopColor|#FFF4D3D3"/> <Setter Property="Template" Value="{StaticResource TooltipTemplate}"/> </Style> </ResourceDictionary>
Le principe est le suivant : pour chaque élément à réuitiliser, on donne un nom et une valeur : "LeNom|LaValeur"Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263 using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Dynamic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Data; using System.Windows.Markup; namespace WPFExtToolkit { /// <summary> /// Record properties for templates. /// </summary> [ContentProperty("TemplateValue")] public class TemplateProperties { /// <summary> /// instance of Dependency Property. /// </summary> public static readonly DependencyProperty TemplateValueProperty = DependencyProperty.RegisterAttached("TemplateValue", typeof(TemplatePropertiesValues), typeof(TemplateProperties), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits)); /// <summary> /// Get the properties values /// </summary> /// <param name="obj"></param> /// <returns></returns> public static TemplatePropertiesValues GetTemplateValue(DependencyObject obj) { return (TemplatePropertiesValues) obj.GetValue(TemplateValueProperty); } /// <summary> /// Set the properties values. /// /// Set the value only once, after merge all properties. /// </summary> /// <param name="_obj">Dependency object.</param> /// <param name="_value">Value object.</param> public static void SetTemplateValue(DependencyObject _obj, TemplatePropertiesValues _value) { _obj.SetValue(TemplateValueProperty, _value); } } /// <summary> /// Classe used to record values. /// </summary> [TypeConverter(typeof(TemplatePropertiesValuesTypeConverter))] public class TemplatePropertiesValues { /// <summary> /// Dictionnary of values. /// </summary> private Dictionary<String, object> m_dicValues = new Dictionary<String, object>(); /// <summary> /// Constructor. /// </summary> private TemplatePropertiesValues() { } /// <summary> /// Constructor. /// /// The format is : tag|value /// </summary> /// <param name="_strTagValueName">Value with tag name and value</param> public TemplatePropertiesValues(string _strTagValueName) { int iIndex = _strTagValueName.IndexOf('|'); if (iIndex != -1) { m_dicValues[_strTagValueName.Substring(0,iIndex)] = _strTagValueName.Substring(iIndex + 1); } else { Logger.FatalLogger.Fatal(String.Format("Bad value for '{0}', must be '<tag>|<value>'\nCheck your xaml files!",_strTagValueName)); } } /// <summary> /// Get a value from key. /// /// If not found, return null value. /// </summary> /// <param name="_strKey">Key to find</param> /// <returns>The value if found, null else</returns> public Object GetValue(String _strKey) { return m_dicValues.ContainsKey(_strKey) ? m_dicValues[_strKey] : null; } /// <summary> /// Set value. /// /// Détecte au runtime la présence ou pas de ce membre puis set sa valeur. /// </summary> /// <param name="_strKey">Key</param> /// <param name="_oValue">value</param> public void SetValue(String _strKey, Object _oValue) { m_dicValues[_strKey] = _oValue; } /// <summary> /// Merge all values of _value into this /// </summary> /// <param name="_value">Value to merge into this</param> /// <returns>this</returns> internal TemplatePropertiesValues Merge(TemplatePropertiesValues _value) { foreach(var keyValue in _value.m_dicValues) { m_dicValues[keyValue.Key] = keyValue.Value; } return this; } } /// <summary> /// Get and convert a TemplatePropertiesValues to any value type.. /// </summary> [ValueConversion(typeof(TemplatePropertiesValues), typeof(Object))] public class TemplatePropertiesConverter : IValueConverter { /// <summary> /// Static converter classused into xaml files. /// </summary> public static readonly TemplatePropertiesConverter Converter = new TemplatePropertiesConverter(); /// <summary> /// Convert any value of TemplatePropertiesValues find by key in value type. /// </summary> /// <param name="_oValue">TemplatePropertiesValues value</param> /// <param name="_tTargetType">Target type</param> /// <param name="_oParameter">Key to find.</param> /// <param name="_culture">Culture</param> /// <returns>An objet type, null not found.</returns> public object Convert(object _oValue, Type _tTargetType, object _oParameter, System.Globalization.CultureInfo _culture) { TemplatePropertiesValues oPropertyValue = _oValue as TemplatePropertiesValues; string strParamKey = _oParameter as string; // This paramerter is done with ConverterParameter if (strParamKey != null || strParamKey.Length > 0) { // Find value object oRetValue = oPropertyValue.GetValue(strParamKey); if (oRetValue != null && !_tTargetType.IsInstanceOfType(oRetValue)) { // Not into the correct type => convert it TypeConverter converter = TypeDescriptor.GetConverter(_tTargetType); Debug.Assert(converter != null); if (converter != null) { // Converter found Debug.Assert(converter.CanConvertFrom(oRetValue.GetType())); if (converter.CanConvertFrom(oRetValue.GetType())) { // If converted, record converted value to don't make it all the time oRetValue = converter.ConvertFrom(oRetValue); oPropertyValue.SetValue(strParamKey, oRetValue); } } } return oRetValue; } throw new Exception("The Parameter type is mandatory!"); } /// <summary> /// Not used. /// /// Not implemented. /// </summary> /// <param name="_oValue">Value</param> /// <param name="_tTargetType">Target tyep</param> /// <param name="_oParameter">Parameter.</param> /// <param name="_culture">Culture</param> /// <returns>Exception raised</returns> public object ConvertBack(object _oValue, Type _tTargetType, object _oParameter, System.Globalization.CultureInfo _culture) { throw new NotImplementedException(); } } /// <summary> /// Convert to TemplatePropertiesValues only. /// </summary> public class TemplatePropertiesValuesTypeConverter : TypeConverter { /// <summary> /// Record all values by context. /// </summary> private static Dictionary<ITypeDescriptorContext, TemplatePropertiesValues> m_dicAllValuesByContext = new Dictionary<ITypeDescriptorContext, TemplatePropertiesValues>(); /// <summary> /// Only can convert from string. /// </summary> /// <param name="_context">Context</param> /// <param name="_sourceType">Source type</param> /// <returns></returns> public override bool CanConvertFrom(ITypeDescriptorContext _context, Type _sourceType) { return _sourceType == typeof(string); } /// <summary> /// The context is used as key to record each TemplatePropertiesValues in a static class. /// </summary> /// <param name="_context">Context</param> /// <param name="_culture">Culture</param> /// <param name="_oValue">Value</param> /// <returns></returns> public override object ConvertFrom(ITypeDescriptorContext _context, System.Globalization.CultureInfo _culture, object _oValue) { TemplatePropertiesValues propValue = null; if (!m_dicAllValuesByContext.ContainsKey(_context)) { propValue = new TemplatePropertiesValues((string) _oValue); m_dicAllValuesByContext[_context] = propValue; } else { propValue = m_dicAllValuesByContext[_context]; propValue.Merge(new TemplatePropertiesValues((string) _oValue)); } return propValue; } /// <summary> /// Only convert to string (Not used). /// </summary> /// <param name="_context">Context</param> /// <param name="_destinationType">Destination type</param> /// <returns></returns> public override bool CanConvertTo(ITypeDescriptorContext _context, Type _destinationType) { return _destinationType == typeof(string); } /// <summary> /// Not used. /// </summary> /// <param name="_context">Context</param> /// <param name="_culture">Culture</param> /// <param name="_oValue">Value</param> /// <param name="_destinationType">Destination type</param> /// <returns></returns> public override object ConvertTo(ITypeDescriptorContext _context, System.Globalization.CultureInfo _culture, object _oValue, Type _destinationType) { return null; } } }
Le premier | est trouvé pour différencier le nom et la valeur.
Création des valeurs :
- Dans le xaml, écrire <Setter Property="toolkit:TemplateProperties.TemplateValue" Value="LeNom|LaValeur"/> autant de fois que nécessaire dans le style qui va utiliser le template.
- La classe TemplateProperties sert à mémoriser les informations et être utilisé dans le xaml, c'est un DependencyProperty classique.
1ère chose que je ne m'explique pas : les fonctions GetTemplateValue et SetTemplateValue ne sont JAMAIS appelées ... bordel mais à quoi ça sert alors ? Je ne sais pas trop, à part l'utiliser dans le xaml.- La classe TemplatePropertiesValues : elle contient la valeur. En fait un ensemble de valeurs qu'il faut mémoriser pour un contexte donné. (le texte, les couleurs, l'url de l'image à charger). Un dictionnaire est utilisé pour mémoriser les valeurs classées par clefs.
Le Markup extension [TypeConverter(typeof(TemplatePropertiesValuesTypeConverter))] est très important car c'est via ce markup que l'on dit à WPF comment, à partir d'une chaîne de caractères (du xaml), on obtient une instance TemplatePropertiesValues.- La classe TemplatePropertiesValuesTypeConverter sert à faire la conversion entre le xaml et la classe TemplatePropertiesValues. Seule la fonction ConvertFrom est utilisée.
Là, encore une astuce : comme on set plusieurs fois les mêmes clefs avec des valeurs différentes (c'est le but que Title soit une fois 'Information', une fois 'Avertissement', une fois 'Erreur'), j'utilise l'instance de context en tant que clef dans un dictionnaire statique qui contient toutes les instances de TemplatePropertiesValues. A priori WPF crée un context différent à chaque fois qu'il crée un nouveau style.- Une fois les valeurs créées, il faut pouvoir les rappeler et les convertir dans le bon type (une couleur, une image, etc). C'est là que la classe TemplatePropertiesConverter entre en jeu.
Dans la méthode Convert, le paramètre est utilisé pour trouver le nom de la valeur. Une fois la valeur trouvée, il faut la convertir dans le bon type : si elle n'est pas déjà dans le bon type, l'appel de TypeDescriptor.GetConverter(_tTargetType) permet de trouver un convertisseur vers ce type. La valeur est donc convertie dans le bon type, puis écrase la valeur brute de sorte que la conversion ne soit plus nécessaire lors du prochain appel pour cette valeur. Inconvénient : une même valeur ne peut pas être utilisé pour deux types différents : en supprimant la ligne oPropertyValue.SetValue(strParamKey, oRetValue); on s'affranchie de ce problème mais le code est un peu moins optimisé...- Dans le xaml, pour récupérer la valeur, il faut donc utiliser ce converter. Là encore, deuxième question : pourquoi deux syntaxes différentes ? Je ne sais pas :
J'ai tout de suite utilisé TemplateBinding qui fonctionnait bien mais j'ai remarqué que pour pour GradientStartColor / GradientStopColor la valeur retournée était nulle !
En cherchant, j'ai trouvé une syntaxe qui fonctionne mais je ne sais pas pourquoi ni pourquoi celle utilisant TemplateBinding retourne null !!!!
- Soit : "{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(toolkit:TemplateProperties.TemplateValue), Converter={x:Static toolkit:TemplatePropertiesConverter.Converter}, ConverterParameter='GradientStartColor'}"
- Soit : "{TemplateBinding toolkit:TemplateProperties.TemplateValue, Converter={x:Static toolkit:TemplatePropertiesConverter.Converter}, ConverterParameter='Title'}"
Voilà, ce code fonctionne super bien et permet de paramétriser les templates et d'éviter la réécriture des mêmes types d'éléments même lorsqu'il n'y a que peu de différence.
En plus c'est 100% générique, pas la peine de créer un Thème, on peut mettre autant de valeurs que l'on veut !
Si quelqu'un comprend les deux questions marquées en rouge ou peu améliorer ce code je suis preneur !!
Si ça peut aider, tant mieux.
Merci.