V. Les Paramètres de Binding▲
Lorsque l'on utilise le binding, on voit que l'IntelliSense nous indique plusieurs propriétés, nous avons utilisé par exemple Path, Mode, ElementName. Voici une liste exhaustive des propriétés disponibles avec des exemples pour illustrer. L'utilisation de l'élément Binding entouré d'accolades dans le XAML revient à créer une instance de la classe Binding. Par conséquent, les différentes propriétés que nous allons examiner sont des propriétés de la classe Binding. Il est bien sûr possible de définir les mêmes Binding via code-behind.
V-A. Les sources de Binding▲
Un binding peut avoir plusieurs types de sources différentes, elles s'excluent mutuellement. Si aucune n'est précisée, c'est le DataContext du contrôle qui est utilisé et si celui-ci n'est pas précisé, il est hérité du premier parent qui le spécifie. Donc lorsque la source n'est pas précisée c'est le DataContext de l'objet qui fait foi.
V-A-1. Source (WPF+Silverlight)▲
Utilisée pour préciser l'objet source, cela peut être par exemple pour lier à une ressource (via StaticResource) ou un objet dans le XAML.
<TextBox
Text
=
"{Binding Source={StaticResource myString}, Mode=OneWay}"
/>
V-A-2. RelativeSource (WPF+Silverlight)▲
Utilisée pour dire que la source est relative au contrôle. Dans l'exemple suivant, la valeur affichée par le TextBox est la largeur de celui-ci.
<TextBox
Text
=
"{Binding Path=ActualWidth, RelativeSource={RelativeSource Mode=Self}, Mode=OneWay}"
/>
Dans l'exemple suivant, la valeur affichée par le TextBox est la largeur de la grille parente de celui-ci.
<TextBox
Text
=
"{Binding Path=ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Grid}, Mode=OneWay}"
/>
Ce mode contient quelques nouveautés dans Silverlight 5, nous y reviendrons plus en détail .
V-A-3. ElementName (WPF+Silverlight)▲
Utilisée pour dire que la source est désignée par son nom dans le XAML.
<TextBox
x
:
Name
=
"txtSample"
Text
=
"{Binding Path=ActualWidth, ElementName=txtSample, Mode=OneWay}"
/>
V-B. Les chemins du Binding▲
Le chemin est un point important du binding, il permet de dire sur quelle propriété de la source s'attacher.
V-B-1. Path (WPF+Silverlight)▲
On peut naviguer dans les propriétés de l'objet grâce à Path, on peut également utiliser les indexeurs pour les collections, dictionnaires, etc. Dans l'exemple suivant, la deuxième TextBox affiche la longueur du texte de la première et la troisième affiche la première lettre.
<TextBox
x
:
Name
=
"myTextBox"
Text
=
"Hello world!"
/>
<TextBox
Text
=
"{Binding ElementName=myTextBox, Path=Text.Length}"
/>
<TextBox
Text
=
"{Binding ElementName=myTextBox, Path=Text[0]}"
/>
V-B-2. XPath (WPF)▲
Lorsque l'on veut utiliser des données XML, il peut être intéressant d'utiliser un XmlDataProvider comme source de données. Ainsi, on peut utiliser la propriété XPath pour naviguer dans notre source comme dans l'exemple suivant :
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
</Grid.RowDefinitions>
<Grid.Resources>
<XmlDataProvider
x
:
Key
=
"Movies"
>
<
x
:
XData>
<Movies>
<Movie
title
=
"Top Gun"
>
<Actor>
Tom Cruise</Actor>
</Movie>
</Movies>
</
x
:
XData>
</XmlDataProvider>
</Grid.Resources>
<TextBox Grid.
Row
=
"0"
Text
=
"{Binding Source={StaticResource Movies}, XPath=Movies/Movie[1]/@title}"
/>
<TextBox Grid.
Row
=
"1"
Text
=
"{Binding Source={StaticResource Movies}, XPath=Movies/Movie[1]/Actor[1]}"
/>
</Grid>
V-B-3. BindsDirectlyToSource (WPF+Silverlight)▲
De manière assez analogue, lorsque l'on travaille avec ObjectDataProvider, la propriété Path navigue dans la donnée du ObjectDataProvider et non pas sur le ObjectDataProvider lui-même. Ainsi, si je veux accéder à une propriété comme ObjectType du ObjectDataProvider, je suis obligé de préciser que je me branche sur la source. L'exemple suivant est parlant : sans BindsDirectlyToSource, je navigue dans ma collection de films, avec je peux me « binder » sur la propriété ObjectType de ObjectDataProvider.
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
</Grid.RowDefinitions>
<Grid.Resources>
<ObjectDataProvider
x
:
Key
=
"Movies"
>
<ObjectDataProvider.ObjectInstance>
<
WpfApplication2
:
Movies>
<
WpfApplication2
:
Movie
Title
=
"Top Gun"
/>
<
WpfApplication2
:
Movie
Title
=
"Star Wars: A new hope"
/>
</
WpfApplication2
:
Movies>
</ObjectDataProvider.ObjectInstance>
</ObjectDataProvider>
</Grid.Resources>
<TextBox Grid.
Row
=
"0"
Text
=
"{Binding Source={StaticResource Movies}, Path=[0].Title}"
/>
<TextBox Grid.
Row
=
"1"
Text
=
"{Binding Source={StaticResource Movies}, BindsDirectlyToSource=True, Path=ObjectType}"
/>
</Grid>
V-C. Les conversions▲
Les conversions seront abordées plus en détail dans le chapitre suivant. La description des éléments à l'aide de l'exemple suivant sera donc très succincte.
<Grid
x
:
Name
=
"LayoutRoot"
>
<Grid.Resources>
<
WpfApplication2
:
MyConverter
x
:
Key
=
"MyConverter"
/>
</Grid.Resources>
<TextBox
Text
=
"{Binding ElementName=LayoutRoot, Path=ActualWidth, Converter={StaticResource MyConverter}, ConverterParameter=2, ConverterCulture=en-US, Mode=OneWay}"
/>
</Grid>
V-C-1. Converter (WPF+Silverlight)▲
Permet de définir le Converter utilisé pour la conversion de type. Il est bien souvent une ressource de l'application. Dans l'exemple, c'est une instance de la classe MyConverter qui est utilisée.
V-C-2. ConverterParameter (WPF+Silverlight)▲
Permet de passer des paramètres au Converter. Dans l'exemple, on passe le paramètre 2. Attention, bien qu'il soit tentant de le faire, il est malheureusement impossible de faire un binding avec le ConverterParameter, car ça n'est pas une DependencyProperty de Binding. Dommage !
V-C-3. ConverterCulture (WPF+Silverlight)▲
Bien souvent le Converter est utilisé pour des opérations de mise en forme, cela permet de forcer une culture. Dans l'exemple, la culture anglaise US est forcée, ce qui se traduit par un formatage des chiffres avec un point comme séparateur décimal.
V-D. La validation du Binding▲
Il y a plusieurs solutions pour vérifier la saisie de l'utilisateur : les interfaces IDataErrorInfo et INotifyDataErrorInfo ou lever une exception dans le setter. Ensuite, WPF et Silverlight utilisent ces mécanismes pour, par exemple, cercler une TextBox de rouge et prévenir l'utilisateur.
V-D-1. ValidatesOnDataErrors (WPF+Silverlight)▲
À utiliser lorsque l'object sur lequel on se « binde » implémente IDataErrorInfo. Dans l'exemple qui suit, préciser ValidatesOnDataErrors permet de vérifier les règles à chaque fois que la propriété change. Entrer 200 dans la TextBox provoquera un surlignage en rouge ainsi qu'une info-bulle avec le message d'erreur.
<Grid
x
:
Name
=
"LayoutRoot"
>
<Grid.Resources>
<Style
TargetType
=
"TextBox"
>
<Style.Triggers>
<Trigger
Property
=
"Validation.HasError"
Value
=
"true"
>
<Setter
Property
=
"ToolTip"
Value
=
"{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"
/>
<Setter
Property
=
"Foreground"
Value
=
"Red"
/>
</Trigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<TextBox
Text
=
"{Binding Path=Value, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"
/>
</Grid>
public
partial
class
MainWindow :
IDataErrorInfo,
INotifyPropertyChanged
{
private
decimal
_value;
public
decimal
Value
{
get
{
return
_value;
}
set
{
_value =
value
;
RaisePropertyChanged
(
"Value"
);
}
}
public
MainWindow
(
)
{
InitializeComponent
(
);
DataContext =
this
;
}
#region IDataErrorInfo
public
string
this
[
string
columnName]
{
get
{
if
(
columnName ==
"Value"
)
{
if
(
Value <
0
||
Value >
100
)
return
"Value must be between 0 and 100"
;
}
return
String.
Empty;
}
}
public
string
Error
{
get
{
return
this
[
"Value"
];
}
}
#endregion
#region INotifyPropertyChanged
public
event
PropertyChangedEventHandler PropertyChanged;
private
void
RaisePropertyChanged
(
string
propertyName)
{
if
(
PropertyChanged !=
null
)
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
propertyName));
}
#endregion
}
V-D-2. ValidatesOnExceptions (WPF+Silverlight)▲
Une autre façon de faire (que je trouve personnellement moins élégante) est de lever une exception dans le setter. Le fonctionnement est analogue.
<Grid
x
:
Name
=
"LayoutRoot"
>
<Grid.Resources>
<Style
TargetType
=
"TextBox"
>
<Style.Triggers>
<Trigger
Property
=
"Validation.HasError"
Value
=
"true"
>
<Setter
Property
=
"ToolTip"
Value
=
"{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"
/>
<Setter
Property
=
"Foreground"
Value
=
"Red"
/>
</Trigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<TextBox
Text
=
"{Binding Path=Value, UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True}"
/>
</Grid>
public
partial
class
MainWindow :
INotifyPropertyChanged
{
private
decimal
_value;
public
decimal
Value
{
get
{
return
_value;
}
set
{
if
(
value
<
0
||
value
>
100
)
throw
new
ArgumentOutOfRangeException
(
"value"
,
"Value must be between 0 and 100"
);
_value =
value
;
RaisePropertyChanged
(
"Value"
);
}
}
public
MainWindow
(
)
{
InitializeComponent
(
);
DataContext =
this
;
}
#region INotifyPropertyChanged
public
event
PropertyChangedEventHandler PropertyChanged;
private
void
RaisePropertyChanged
(
string
propertyName)
{
if
(
PropertyChanged !=
null
)
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
propertyName));
}
#endregion
}
V-D-3. ValidatesOnNotifyDataErrors (Silverlight)▲
Le fonctionnement est similaire à ce qu'il y a au-dessus, la propriété travaille avec INotifyDataErrorInfo.
Tandis que IDataErrorInfo n'accepte de remonter qu'une erreur, INotifyDataErrorInfo retourne un IEnumerable avec un élément par erreur. C'est d'après moi la méthode la plus propre et efficace cependant elle n'est disponible qu'avec Silverlight. Par défaut, elle est activée.
<Grid
x
:
Name
=
"LayoutRoot"
Background
=
"White"
>
<Grid.RowDefinitions>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
</Grid.RowDefinitions>
<TextBox Grid.
Row
=
"0"
Margin
=
"2"
Text
=
"{Binding Path=Value, Mode=TwoWay, ValidatesOnNotifyDataErrors=False}"
/>
<TextBox Grid.
Row
=
"1"
Margin
=
"2"
Text
=
"{Binding Path=Value, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}"
/>
</Grid>
public
partial
class
MainPage :
INotifyPropertyChanged,
INotifyDataErrorInfo
{
private
decimal
_value;
public
decimal
Value
{
get
{
return
_value;
}
set
{
_value =
value
;
RaisePropertyChanged
(
"Value"
);
RaiseErrorsChanged
(
"Value"
);
}
}
public
MainPage
(
)
{
InitializeComponent
(
);
DataContext =
this
;
}
#region INotifyPropertyChanged
public
event
PropertyChangedEventHandler PropertyChanged;
private
void
RaisePropertyChanged
(
string
property)
{
if
(
PropertyChanged !=
null
)
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
property));
}
#endregion
#region INotifyDataErrorInfo
public
IEnumerable GetErrors
(
string
propertyName)
{
var
errors =
new
List<
String>(
);
if
(
propertyName ==
"Value"
&&
(
Value <
0
||
Value >
100
))
errors.
Add
(
"Value must be between 0 and 100"
);
if
(
propertyName ==
"Value"
&&
(
Value %
2
!=
0
))
errors.
Add
(
"Value must be even"
);
return
errors;
}
public
bool
HasErrors
{
get
{
return
Value <
0
||
Value >
100
||
Value %
2
!=
0
;
}
}
public
event
EventHandler<
DataErrorsChangedEventArgs>
ErrorsChanged;
private
void
RaiseErrorsChanged
(
string
propertyName)
{
if
(
ErrorsChanged !=
null
)
ErrorsChanged
(
this
,
new
DataErrorsChangedEventArgs
(
propertyName));
}
#endregion
}
V-D-4. ValidationRules (WPF)▲
WPF permet de définir des règles de validation pour les différentes propriétés. En héritant de la classe ValidationRule, il est possible de définir ses propres règles de validation. Ici, il y a une règle qui vérifie que l'entier est dans une plage et une règle qui vérifie qu'il est pair.
<TextBox Grid.
Row
=
"0"
Margin
=
"2"
>
<TextBox.Text>
<Binding
Path
=
"Value"
UpdateSourceTrigger
=
"PropertyChanged"
>
<Binding.ValidationRules>
<
WpfApplication2
:
RangeRule
Min
=
"0"
Max
=
"100"
/>
<
WpfApplication2
:
EvenRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
public
class
RangeRule :
ValidationRule
{
public
int
Min {
get
;
set
;
}
public
int
Max {
get
;
set
;
}
public
override
ValidationResult Validate
(
object
value
,
CultureInfo cultureInfo)
{
int
valueToCheck;
if
(!(
value
is
String) ||
!
int
.
TryParse
(
value
as
String,
out
valueToCheck))
return
new
ValidationResult
(
false
,
"Value is not in a valid integer"
);
if
(
valueToCheck <
Min ||
valueToCheck >
Max)
return
new
ValidationResult
(
false
,
String.
Format
(
"Value must be between {0} and {1}"
,
Min,
Max));
return
new
ValidationResult
(
true
,
null
);
}
}
public
class
EvenRule :
ValidationRule
{
public
override
ValidationResult Validate
(
object
value
,
CultureInfo cultureInfo)
{
int
valueToCheck;
if
(!(
value
is
String) ||
!
int
.
TryParse
(
value
as
String,
out
valueToCheck))
return
new
ValidationResult
(
false
,
"Value is not in a valid integer"
);
if
(
valueToCheck %
2
!=
0
)
return
new
ValidationResult
(
false
,
"Value must be even"
);
return
new
ValidationResult
(
true
,
null
);
}
}
V-D-5. BindingGroupName (WPF)▲
BindingGroupName permet d'utiliser un BindingGroup. C'est très pratique lors de la validation de données éclatées en plusieurs champs : une date sur trois TextBox si l'on n'utilise pas de DatePicker, une adresse, etc.
La modification est transactionnelle, le binding n'est effectué que lorsque l'objet est valide d'après les règles définies. Dans l'exemple suivant, notre object BusinessObject est en fait une date. On veut s'assurer que l'utilisateur ne rentre pas de dates fantaisistes (le 32 janvier, par exemple), le TextBlock ne se met à jour que lorsque la date est valide et que l'utilisateur la soumet.
<Grid
x
:
Name
=
"LayoutRoot"
Margin
=
"5"
>
<Grid.RowDefinitions>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
</Grid.RowDefinitions>
<Grid.BindingGroup>
<BindingGroup
Name
=
"DateGroup"
NotifyOnValidationError
=
"True"
>
<BindingGroup.ValidationRules>
<
WpfApplication2
:
DateRule
ValidatesOnTargetUpdated
=
"True"
/>
</BindingGroup.ValidationRules>
</BindingGroup>
</Grid.BindingGroup>
<TextBox
x
:
Name
=
"day"
Grid.
Row
=
"0"
Margin
=
"2"
Text
=
"{Binding Path=Day, BindingGroupName=DateGroup, UpdateSourceTrigger=Explicit}"
/>
<TextBox
x
:
Name
=
"month"
Grid.
Row
=
"1"
Margin
=
"2"
Text
=
"{Binding Path=Month, BindingGroupName=DateGroup, UpdateSourceTrigger=Explicit}"
/>
<TextBox
x
:
Name
=
"year"
Grid.
Row
=
"2"
Margin
=
"2"
Text
=
"{Binding Path=Year, BindingGroupName=DateGroup, UpdateSourceTrigger=Explicit}"
/>
<Button Grid.
Row
=
"3"
Margin
=
"2"
Click
=
"SubmitClick"
Content
=
"Submit"
/>
<Button Grid.
Row
=
"4"
Margin
=
"2"
Click
=
"CancelClick"
Content
=
"Cancel"
/>
<Grid Grid.
Row
=
"5"
>
<TextBlock>
<TextBlock.Text>
<MultiBinding
StringFormat
=
"{}{0}-{1}-{2}"
>
<Binding
Path
=
"Day"
/>
<Binding
Path
=
"Month"
/>
<Binding
Path
=
"Year"
/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</Grid>
public
partial
class
MainWindow
{
public
MainWindow
(
)
{
InitializeComponent
(
);
LayoutRoot.
DataContext =
new
BusinessObject
(
);
LayoutRoot.
BindingGroup.
BeginEdit
(
);
}
private
void
SubmitClick
(
object
sender,
RoutedEventArgs e)
{
if
(
LayoutRoot.
BindingGroup.
CommitEdit
(
))
{
LayoutRoot.
BindingGroup.
BeginEdit
(
);
}
}
private
void
CancelClick
(
object
sender,
RoutedEventArgs e)
{
LayoutRoot.
BindingGroup.
CancelEdit
(
);
LayoutRoot.
BindingGroup.
BeginEdit
(
);
}
}
public
class
BusinessObject :
INotifyPropertyChanged
{
private
int
_day;
private
int
_month;
private
int
_year;
public
int
Day
{
get
{
return
_day;
}
set
{
_day =
value
;
RaisePropertyChanged
(
"Day"
);
Trace.
WriteLine
(
"Day updated"
);
}
}
public
int
Month
{
get
{
return
_month;
}
set
{
_month =
value
;
RaisePropertyChanged
(
"Month"
);
Trace.
WriteLine
(
"Month updated"
);
}
}
public
int
Year
{
get
{
return
_year;
}
set
{
_year =
value
;
RaisePropertyChanged
(
"Year"
);
Trace.
WriteLine
(
"Year updated"
);
}
}
#region INotifyPropertyChanged
public
event
PropertyChangedEventHandler PropertyChanged;
private
void
RaisePropertyChanged
(
string
propertyName)
{
if
(
PropertyChanged !=
null
)
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
propertyName));
}
#endregion
}
public
class
DateRule :
ValidationRule
{
public
override
ValidationResult Validate
(
object
value
,
CultureInfo cultureInfo)
{
if
(!(
value
is
BindingGroup))
return
new
ValidationResult
(
false
,
"Invalid group"
);
var
group
=
value
as
BindingGroup;
var
day =
Convert.
ToInt32
(
group
.
GetValue
(
group
.
Items[
0
],
"Day"
));
var
month =
Convert.
ToInt32
(
group
.
GetValue
(
group
.
Items[
0
],
"Month"
));
var
year =
Convert.
ToInt32
(
group
.
GetValue
(
group
.
Items[
0
],
"Year"
));
try
{
new
DateTime
(
year,
month,
day);
}
catch
(
Exception)
{
return
new
ValidationResult
(
false
,
"This is not a valid date"
);
}
return
new
ValidationResult
(
true
,
null
);
}
}
V-E. Les notifications▲
V-E-1. NotifyOnSourceUpdated (WPF)▲
Si cette valeur est vraie, un évènement est levé lorsque la source est mise à jour.
<Grid
x
:
Name
=
"LayoutRoot"
Margin
=
"5"
>
<Grid.RowDefinitions>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
</Grid.RowDefinitions>
<TextBox
x
:
Name
=
"day"
Grid.
Row
=
"0"
Margin
=
"2"
Text
=
"{Binding Path=Value, NotifyOnSourceUpdated=True, NotifyOnTargetUpdated=True, UpdateSourceTrigger=PropertyChanged}"
SourceUpdated
=
"OnSourceUpdated"
TargetUpdated
=
"OnTargetUpdated"
/>
<Button Grid.
Row
=
"1"
Content
=
"Click me!"
Click
=
"OnClick"
/>
</Grid>
public
partial
class
MainWindow :
INotifyPropertyChanged
{
private
string
_value;
public
String Value
{
get
{
return
_value;
}
set
{
_value =
value
;
RaisePropertyChanged
(
"Value"
);
}
}
public
MainWindow
(
)
{
InitializeComponent
(
);
DataContext =
this
;
}
private
void
OnTargetUpdated
(
object
sender,
DataTransferEventArgs e)
{
Debug.
WriteLine
(
"Target updated, property {0}"
,
e.
Property);
}
private
void
OnSourceUpdated
(
object
sender,
DataTransferEventArgs e)
{
Debug.
WriteLine
(
"Source updated, property {0}"
,
e.
Property);
}
#region INotifyPropertyChanged
public
event
PropertyChangedEventHandler PropertyChanged;
private
void
RaisePropertyChanged
(
string
propertyName)
{
if
(
PropertyChanged !=
null
)
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
propertyName));
}
#endregion
private
void
OnClick
(
object
sender,
RoutedEventArgs e)
{
Value +=
Value;
}
}
V-E-2. NotifyOnTargetUpdated (WPF)▲
Le fonctionnement est exactement le même. L'exemple est d'ailleurs celui du dessus.
V-E-3. NotifyOnValidationError (WPF+Silverlight)▲
De manière analogue aux deux exemples du dessus, on peut lever un évènement lorsqu'il y a un problème lors de la validation du binding (voir V-B).
Il faut impérativement mettre à « vrai » une technique de validation. Dans cet exemple, on utilise ValidateOnExceptions.
<Grid
x
:
Name
=
"LayoutRoot"
Margin
=
"5"
>
<Grid.RowDefinitions>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
</Grid.RowDefinitions>
<TextBox Grid.
Row
=
"0"
Margin
=
"2"
Text
=
"{Binding Path=Value, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True}"
Validation.
Error
=
"OnError"
/>
</Grid>
public
partial
class
MainWindow :
INotifyPropertyChanged
{
private
string
_value;
public
String Value
{
get
{
return
_value;
}
set
{
if
(
value
.
Length >
10
)
throw
new
ArgumentException
(
"value"
);
_value =
value
;
RaisePropertyChanged
(
"Value"
);
}
}
public
MainWindow
(
)
{
InitializeComponent
(
);
DataContext =
this
;
}
#region INotifyPropertyChanged
public
event
PropertyChangedEventHandler PropertyChanged;
private
void
RaisePropertyChanged
(
string
propertyName)
{
if
(
PropertyChanged !=
null
)
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
propertyName));
}
#endregion
private
void
OnError
(
object
sender,
ValidationErrorEventArgs e)
{
Debug.
WriteLine
(
"An error happened"
);
}
}
Il est à noter qu'en Silverlight, il y a le composant ValidationSummary dans le SDK. Ce composant indispensable s'abonne à cet évènement et affiche un résumé des erreurs de validation.
V-F. Mise à jour du Binding▲
V-F-1. UpdateSourceTrigger (WPF+Silverlight)▲
Lorsque l'on utilise un TextBox et qu'on « binde » le texte, on s'aperçoit que le setter de la propriété n'est déclenché qu'à la perte de focus du contrôle. C'est la méthode la plus performante, cependant dans les cas où l'on voudrait vérifier au fur et à mesure, cela peut poser problème. Il est possible également de vouloir garder le contrôle sur le moment où le binding sera effectué.
Ainsi, cette propriété peut prendre plusieurs valeurs :
Valeur |
Signification |
---|---|
Default |
C'est le défaut pour le contrôle. Par exemple pour un TextBox, c'est la perte de focus. |
PropertyChanged |
Dès que la valeur change. Attention, ceci n'existe que sur WPF et Silverlight 5. |
LostFocus |
Lors de la perte de focus. Attention, ceci n'existe que sur WPF. |
Explicit |
Doit être fait explicitement, en faisant par exemple monTextBox.GetBindingExpression(TextBox.TextProperty).UpdateSource() . |
V-F-2. UpdateSourceExceptionFilter (WPF)▲
Cette propriété permet de définir un callback qui sera appelé en cas d'erreur de validation. Il ne peut être utilisé qu'en conjonction de la validation rule ExceptionValidationRule comme dans l'exemple suivant ou l'exception est loguée dans la fenêtre de debug. Si null est retourné, il y aura une ValidationError ajoutée à la collection d'erreurs de validation et qui contiendra l'exception de validation. Si une ValidationError est retournée alors c'est celle-ci qui sera ajoutée à la collection d'erreurs. Enfin, pour un retour d'un autre type c'est l'objet retourné qui sera encapsulé dans la ValidationError.
<TextBox Grid.
Row
=
"0"
Margin
=
"2"
>
<TextBox.Text>
<Binding
Path
=
"Value"
UpdateSourceTrigger
=
"PropertyChanged"
UpdateSourceExceptionFilter
=
"OnUpdateSourceExceptionFilter"
>
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
public
partial
class
MainWindow :
INotifyPropertyChanged
{
private
string
_value;
public
String Value
{
get
{
return
_value;
}
set
{
if
(
value
.
Length >
10
)
throw
new
ArgumentException
(
"value"
);
_value =
value
;
RaisePropertyChanged
(
"Value"
);
}
}
public
MainWindow
(
)
{
InitializeComponent
(
);
DataContext =
this
;
}
#region INotifyPropertyChanged
public
event
PropertyChangedEventHandler PropertyChanged;
private
void
RaisePropertyChanged
(
string
propertyName)
{
if
(
PropertyChanged !=
null
)
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
propertyName));
}
#endregion
object
OnUpdateSourceExceptionFilter
(
object
bindingExpression,
Exception exception)
{
Debug.
WriteLine
(
exception);
return
exception;
}
}
V-G. Options de Binding▲
V-G-1. FallbackValue (WPF+Silverlight)▲
En cas d'erreur dans le binding ou erreur de conversion, c'est cette valeur qui sera affichée. Ce n'est pas la valeur affichée en cas de null.
<TextBox
x
:
Name
=
"textbox"
Text
=
"{Binding ElementName=textbox, Path=Value1, FallbackValue='Erreur!'}"
/>
V-G-2. IsAsync (WPF)▲
Lorsque l'on est face à une propriété qui peut être assez longue à charger (une requête Web synchrone par exemple), il est possible de faire le binding de manière asynchrone. Ainsi, le thread de l'interface graphique n'est pas bloqué, car le binding se fait sur un thread à part et la fenêtre peut s'afficher correctement. Il est intéressant de communiquer à l'utilisateur le chargement via le FallbackValue comme dans l'exemple suivant.
<TextBox
x
:
Name
=
"textbox"
Text
=
"{Binding Path=Value, IsAsync=True, FallbackValue=Loading...}"
/>
public
partial
class
MainWindow :
INotifyPropertyChanged
{
private
string
_value;
public
String Value
{
get
{
Thread.
Sleep
(
3000
);
return
_value;
}
set
{
_value =
value
;
RaisePropertyChanged
(
"Value"
);
}
}
public
MainWindow
(
)
{
InitializeComponent
(
);
DataContext =
this
;
_value =
"toto"
;
}
#region INotifyPropertyChanged
public
event
PropertyChangedEventHandler PropertyChanged;
private
void
RaisePropertyChanged
(
string
propertyName)
{
if
(
PropertyChanged !=
null
)
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
propertyName));
}
#endregion
}
V-G-3. Mode (WPF+Silverlight)▲
Le binding peut se faire dans plusieurs sens entre la source (l'objet métier, le ViewModel, etc.) et la cible (le contrôle où est déclaré le binding). Les différentes valeurs de l'énumération sont regroupées dans ce tableau.
Valeur |
Signification |
---|---|
TwoWay |
La valeur peut aller de la source vers la cible et de la cible vers la source. C'est du Duplex. |
OneWay |
La valeur peut aller de la source vers la cible. Mode lecture seule. |
OneTime |
La valeur peut aller une fois de la source vers la cible lors du changement de DataContext. Utile pour le binding vers quelque chose qui ne varie pas. |
OneWayToSource |
La valeur peut aller de la cible vers la source. Mode écriture seule. N'existe qu'en WPF. |
Default |
Une des valeurs ci-dessus. Cela dépend du contrôle. En WPF, la propriété Text de TextBox est déclarée avec FrameworkPropertyMetadataOptions.BindsTwoWayByDefault elle est donc TwoWay. N'existe qu'en WPF. |
V-G-4. StringFormat (WPF+Silverlight)▲
Le StringFormat permet de formater la donnée présentée. Cela peut être un format d'heure (premier exemple), un format monétaire ou un formatage un peu plus particulier (second exemple).
<TextBox
x
:
Name
=
"textbox"
Grid.
Row
=
"0"
Margin
=
"2"
>
<TextBox.Text>
<Binding
Path
=
"Now"
StringFormat
=
"HH:mm:ss.ff"
>
<Binding.Source>
<
System
:
DateTime />
</Binding.Source>
</Binding>
</TextBox.Text>
</TextBox>
<TextBox
x
:
Name
=
"textbox"
Text
=
"{Binding Path=Name, StringFormat='Hello {0}'}"
/>
StringFormat ne supporte pas de commencer par des accolades, par exemple '{0} êtes le bienvenu'. Il faut échapper les accolades comme ceci : '{}{0} êtes le bienvenu'
V-G-5. TargetNullValue (WPF+Silverlight)▲
<TextBox
x
:
Name
=
"textbox"
Text
=
"{Binding Path=Value, TargetNullValue='Valeur nulle'}"
/>
VI. Les conversions (IValueConverter et IMultiValueConverter)▲
Un des gros avantages des technologies Silverlight/WPF est la séparation entre ce qui est représenté et comment c'est représenté. Un exemple assez simple est le cas d'un enum, il est intéressant de pouvoir associer à une valeur pas toujours très lisible une description plus explicite (variant suivant la culture) ou une icône. Il est également possible de s'en servir pour faire du formatage, mais je considère cet usage déprécié.
Partons de l'exemple de base suivant :
<Grid
x
:
Name
=
"LayoutRoot"
Margin
=
"5"
>
<Grid.RowDefinitions>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width
=
"100"
/>
<ColumnDefinition
Width
=
"100"
/>
<ColumnDefinition
Width
=
"100"
/>
<ColumnDefinition
Width
=
"100"
/>
<ColumnDefinition
Width
=
"100"
/>
<ColumnDefinition
Width
=
"100"
/>
<ColumnDefinition
Width
=
"100"
/>
<ColumnDefinition
Width
=
"100"
/>
<ColumnDefinition
Width
=
"Auto"
/>
</Grid.ColumnDefinitions>
<ContentPresenter
Content
=
"{Binding Path=Value}"
/>
<Button Grid.
Column
=
"1"
Content
=
"Switch"
Click
=
"PressSwitch"
/>
</Grid>
public
partial
class
MainWindow :
INotifyPropertyChanged
{
private
SwitchStatus _value;
public
SwitchStatus Value
{
get
{
return
_value;
}
set
{
_value =
value
;
RaisePropertyChanged
(
"Value"
);
}
}
public
MainWindow
(
)
{
InitializeComponent
(
);
DataContext =
this
;
_value =
SwitchStatus.
Undetermined;
}
#region INotifyPropertyChanged
public
event
PropertyChangedEventHandler PropertyChanged;
private
void
RaisePropertyChanged
(
string
propertyName)
{
if
(
PropertyChanged !=
null
)
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
propertyName));
}
#endregion
private
void
PressSwitch
(
object
sender,
RoutedEventArgs e)
{
Value =
(
Value ==
SwitchStatus.
On)
?
SwitchStatus.
Off
:
SwitchStatus.
On;
}
}
public
enum
SwitchStatus
{
Undetermined,
On,
Off,
}
Notre premier Converter va se contenter de donner un texte français plus explicite. Pour implémenter un converter, il suffit d'implémenter l'interface IValueConverter qui possède deux méthodes, Convert et ConvertBack. Convert sert pour convertir de la source vers la cible et ConvertBack pour le retour. Il est à noter que dans bien des cas (nous verrons lesquels), il n'est pas nécessaire de donner un corps à ConvertBack. Ces méthodes prennent des paramètres de conversion comme expliqué.
Dans sa version la plus simple, voici le Converter :
public
class
FrenchLabelConverter :
IValueConverter
{
public
object
Convert
(
object
value
,
Type targetType,
object
parameter,
CultureInfo culture)
{
if
(!(
value
is
SwitchStatus))
throw
new
ArgumentException
(
"Expected a SwitchStatus"
,
"value"
);
var
status =
(
SwitchStatus)value
;
switch
(
status)
{
case
SwitchStatus.
Undetermined:
return
"Indeterminé"
;
case
SwitchStatus.
Off:
return
"Eteint"
;
case
SwitchStatus.
On:
return
"Allumé"
;
default
:
throw
new
ArgumentOutOfRangeException
(
"value"
);
}
}
public
object
ConvertBack
(
object
value
,
Type targetType,
object
parameter,
CultureInfo culture)
{
throw
new
NotImplementedException
(
);
}
}
<TextBlock Grid.
Column
=
"2"
>
<TextBlock.Text>
<Binding
Path
=
"Value"
>
<Binding.Converter>
<
WpfApplication2
:
FrenchLabelConverter />
</Binding.Converter>
</Binding>
</TextBlock.Text>
</TextBlock>
On peut remarquer que ConvertBack n'est pas implémenté. En effet, ce binding ne sera utilisé qu'en mode OneWay, c'est donc uniquement Convert qui sera utilisé. Pour un binding TwoWay, il faut implémenter les deux.
Ensuite, on remarque que le type d'entrée est objet. Il est nécessaire de tester le type réel et de réagir s'il n'est pas correct. Le reste de l'implémentation est classique, on retourne une chaîne de caractères en fonction de la valeur.
Cependant, ces chaînes sont en « dur » dans le code. On peut penser à une version un peu plus sympathique à utiliser (notamment dans une collaboration designer/développeur lorsque celui-ci souhaite personnaliser les textes). On peut utiliser des propriétés pour remédier à ce problème :
public
class
LabelConverter :
IValueConverter
{
public
String UndeterminedLabel {
get
;
set
;
}
public
String OffLabel {
get
;
set
;
}
public
String OnLabel {
get
;
set
;
}
public
object
Convert
(
object
value
,
Type targetType,
object
parameter,
CultureInfo culture)
{
if
(!(
value
is
SwitchStatus))
throw
new
ArgumentException
(
"Expected a SwitchStatus"
,
"value"
);
var
status =
(
SwitchStatus)value
;
switch
(
status)
{
case
SwitchStatus.
Undetermined:
return
UndeterminedLabel;
case
SwitchStatus.
Off:
return
OffLabel;
case
SwitchStatus.
On:
return
OnLabel;
default
:
throw
new
ArgumentOutOfRangeException
(
"value"
);
}
}
public
object
ConvertBack
(
object
value
,
Type targetType,
object
parameter,
CultureInfo culture)
{
throw
new
NotImplementedException
(
);
}
}
<TextBlock Grid.
Column
=
"3"
>
<TextBlock.Text>
<Binding
Path
=
"Value"
>
<Binding.Converter>
<
WpfApplication2
:
LabelConverter
UndeterminedLabel
=
"Indeterminé"
OffLabel
=
"Eteint"
OnLabel
=
"Allumé"
/>
</Binding.Converter>
</Binding>
</TextBlock.Text>
</TextBlock>
Ce qui est beaucoup plus élégant ! Désormais, ce qui nous intéresse c'est de traduire en anglais et en français ces valeurs. Pour le besoin, un dictionnaire sera utilisé, mais cependant il serait préférable d'utiliser le système de ressources intégré à .Net qui permet une localisation aisée.
public
class
LocalizedLabelConverter :
IValueConverter
{
private
readonly
Dictionary<
SwitchStatus,
String>
_frenchDictionary;
private
readonly
Dictionary<
SwitchStatus,
String>
_englishDictionary;
public
LocalizedLabelConverter
(
)
{
_frenchDictionary =
new
Dictionary<
SwitchStatus,
string
>
{
{
SwitchStatus.
Undetermined,
"Indeterminé"
},
{
SwitchStatus.
Off,
"Eteint"
},
{
SwitchStatus.
On,
"Allumé"
},
};
_englishDictionary =
new
Dictionary<
SwitchStatus,
string
>
{
{
SwitchStatus.
Undetermined,
"Undetermined"
},
{
SwitchStatus.
Off,
"Switched off"
},
{
SwitchStatus.
On,
"Switched on"
},
};
}
public
object
Convert
(
object
value
,
Type targetType,
object
parameter,
CultureInfo culture)
{
if
(!(
value
is
SwitchStatus))
throw
new
ArgumentException
(
"Expected a SwitchStatus"
,
"value"
);
var
status =
(
SwitchStatus)value
;
return
culture.
Name.
StartsWith
(
"fr-"
)
?
_frenchDictionary[
status]
:
_englishDictionary[
status];
}
public
object
ConvertBack
(
object
value
,
Type targetType,
object
parameter,
CultureInfo culture)
{
throw
new
NotImplementedException
(
);
}
}
<TextBlock Grid.
Column
=
"4"
>
<TextBlock.Text>
<Binding
Path
=
"Value"
>
<Binding.Converter>
<
WpfApplication2
:
LocalizedLabelConverter />
</Binding.Converter>
</Binding>
</TextBlock.Text>
</TextBlock>
Attention ! Lors de l'utilisation, ceci peut ne pas marcher. En effet, lorsque la culture n'est pas explicitement précisée, c'est la culture du contrôle qui est utilisée. Or, pour des raisons qui me sont totalement obscures, celle-ci est toujours la culture anglaise. Pour y remédier, il suffit d'appliquer la culture de l'utilisateur dans le constructeur du contrôle racine (par exemple de la fenêtre) comme ceci :
public
MainWindow
(
)
{
InitializeComponent
(
);
Language =
XmlLanguage.
GetLanguage
(
CultureInfo.
CurrentUICulture.
Name);
}
Pour le moment, tous les Converters présentés se contentent de renvoyer du texte. On peut aller plus loin par exemple avec une couleur (premier exemple), un template (second exemple) ou une image (troisième exemple).
public
class
ColorConverter :
IValueConverter
{
public
Color UndeterminedColor {
get
;
set
;
}
public
Color OffColor {
get
;
set
;
}
public
Color OnColor {
get
;
set
;
}
public
object
Convert
(
object
value
,
Type targetType,
object
parameter,
CultureInfo culture)
{
if
(!(
value
is
SwitchStatus))
throw
new
ArgumentException
(
"Expected a SwitchStatus"
,
"value"
);
var
status =
(
SwitchStatus)value
;
switch
(
status)
{
case
SwitchStatus.
Undetermined:
return
new
SolidColorBrush
(
UndeterminedColor);
case
SwitchStatus.
Off:
return
new
SolidColorBrush
(
OffColor);
case
SwitchStatus.
On:
return
new
SolidColorBrush
(
OnColor);
default
:
throw
new
ArgumentOutOfRangeException
(
"value"
);
}
}
public
object
ConvertBack
(
object
value
,
Type targetType,
object
parameter,
CultureInfo culture)
{
throw
new
NotImplementedException
(
);
}
}
<TextBlock Grid.
Column
=
"5"
Text
=
"{Binding Path=Value}"
>
<TextBlock.Foreground>
<Binding
Path
=
"Value"
>
<Binding.Converter>
<
WpfApplication2
:
ColorConverter
UndeterminedColor
=
"LightGray"
OffColor
=
"Gray"
OnColor
=
"Lime"
/>
</Binding.Converter>
</Binding>
</TextBlock.Foreground>
</TextBlock>
public
class
TemplateConverter :
IValueConverter
{
public
DataTemplate UndeterminedTemplate {
get
;
set
;
}
public
DataTemplate OffTemplate {
get
;
set
;
}
public
DataTemplate OnTemplate {
get
;
set
;
}
public
object
Convert
(
object
value
,
Type targetType,
object
parameter,
CultureInfo culture)
{
if
(!(
value
is
SwitchStatus))
throw
new
ArgumentException
(
"Expected a SwitchStatus"
,
"value"
);
var
status =
(
SwitchStatus)value
;
switch
(
status)
{
case
SwitchStatus.
Undetermined:
return
UndeterminedTemplate;
case
SwitchStatus.
Off:
return
OffTemplate;
case
SwitchStatus.
On:
return
OnTemplate;
default
:
throw
new
ArgumentOutOfRangeException
(
"value"
);
}
}
public
object
ConvertBack
(
object
value
,
Type targetType,
object
parameter,
CultureInfo culture)
{
throw
new
NotImplementedException
(
);
}
}
<ContentPresenter Grid.
Column
=
"6"
Content
=
"{Binding Path=Value}"
>
<ContentPresenter.ContentTemplate>
<Binding
Path
=
"DataContext.Value"
ElementName
=
"LayoutRoot"
>
<Binding.Converter>
<
WpfApplication2
:
TemplateConverter>
<
WpfApplication2
:
TemplateConverter.UndeterminedTemplate>
<DataTemplate>
<Grid
Background
=
"LightGray"
>
<TextBlock
Text
=
"{Binding}"
HorizontalAlignment
=
"Center"
VerticalAlignment
=
"Center"
FontStyle
=
"Oblique"
/>
</Grid>
</DataTemplate>
</
WpfApplication2
:
TemplateConverter.UndeterminedTemplate>
<
WpfApplication2
:
TemplateConverter.OffTemplate>
<DataTemplate>
<Grid
Background
=
"Gray"
>
<TextBlock
Text
=
"{Binding}"
HorizontalAlignment
=
"Center"
VerticalAlignment
=
"Center"
/>
</Grid>
</DataTemplate>
</
WpfApplication2
:
TemplateConverter.OffTemplate>
<
WpfApplication2
:
TemplateConverter.OnTemplate>
<DataTemplate>
<Grid
Background
=
"LightGreen"
>
<TextBlock
Text
=
"{Binding}"
HorizontalAlignment
=
"Center"
VerticalAlignment
=
"Center"
/>
</Grid>
</DataTemplate>
</
WpfApplication2
:
TemplateConverter.OnTemplate>
</
WpfApplication2
:
TemplateConverter>
</Binding.Converter>
</Binding>
</ContentPresenter.ContentTemplate>
</ContentPresenter>
public
class
ImageConverter :
IValueConverter
{
public
BitmapSource UndeterminedImage {
get
;
set
;
}
public
BitmapSource OffImage {
get
;
set
;
}
public
BitmapSource OnImage {
get
;
set
;
}
public
object
Convert
(
object
value
,
Type targetType,
object
parameter,
CultureInfo culture)
{
if
(!(
value
is
SwitchStatus))
throw
new
ArgumentException
(
"Expected a SwitchStatus"
,
"value"
);
var
status =
(
SwitchStatus)value
;
switch
(
status)
{
case
SwitchStatus.
Undetermined:
return
UndeterminedImage;
case
SwitchStatus.
Off:
return
OffImage;
case
SwitchStatus.
On:
return
OnImage;
default
:
throw
new
ArgumentOutOfRangeException
(
"value"
);
}
}
public
object
ConvertBack
(
object
value
,
Type targetType,
object
parameter,
CultureInfo culture)
{
throw
new
NotImplementedException
(
);
}
}
<Image Grid.
Column
=
"7"
Width
=
"20"
Height
=
"20"
>
<Image.Source>
<Binding
Path
=
"Value"
>
<Binding.Converter>
<
WpfApplication2
:
ImageConverter>
<
WpfApplication2
:
ImageConverter.UndeterminedImage>
<BitmapImage
UriSource
=
"undetermined.bmp"
/>
</
WpfApplication2
:
ImageConverter.UndeterminedImage>
<
WpfApplication2
:
ImageConverter.OffImage>
<BitmapImage
UriSource
=
"off.bmp"
/>
</
WpfApplication2
:
ImageConverter.OffImage>
<
WpfApplication2
:
ImageConverter.OnImage>
<BitmapImage
UriSource
=
"on.bmp"
/>
</
WpfApplication2
:
ImageConverter.OnImage>
</
WpfApplication2
:
ImageConverter>
</Binding.Converter>
</Binding>
</Image.Source>
</Image>
Comme on peut le voir, à partir d'une seule donnée source, on arrive à l'afficher d'une multitude de façons !
Bien d'autres scénarios sont possibles par exemple réaliser un test logique dans le Converter et retourner une Visibility de contrôle pour afficher/masquer celui-ci.
Il est également possible de définir un Converter qui en fonction de l'entrée de l'utilisateur dans une textbox, change la valeur de l'interrupteur. Il faudrait alors implémenter la logique de traitement de la chaîne de caractères dans le ConvertBack et faire en sorte que ce ConvertBack retourne une valeur de SwitchStatus.