VIII. Spécificités des plateformes▲
WPF et Silverlight ne sont pas deux technologies strictement identiques. Comme nous avons pu le voir dans les paramètres de Binding auparavant, chacun possède ses spécificités. Il y a également des spécificités plus importantes que nous allons passer en revue.
VIII-A. Spécificités WPF▲
VIII-A-1. MultiBinding▲
Le multibinding permet de définir plusieurs sources au sein d'un même binding. Cela peut être utile lorsque l'on souhaite par exemple concaténer un prénom et un nom dans le même bloc de texte comme dans l'exemple suivant.
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Grid.
Row
=
"0"
Text
=
"First Name"
/>
<TextBlock Grid.
Row
=
"1"
Text
=
"Last Name"
/>
<TextBlock Grid.
Row
=
"2"
Text
=
"Full Name"
/>
<TextBox Grid.
Row
=
"0"
Grid.
Column
=
"1"
Text
=
"{Binding Path=FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
/>
<TextBox Grid.
Row
=
"1"
Grid.
Column
=
"1"
Text
=
"{Binding Path=LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
/>
<TextBlock Grid.
Row
=
"2"
Grid.
Column
=
"1"
>
<TextBlock.Text>
<MultiBinding
StringFormat
=
"{}{0} {1}"
>
<Binding
Path
=
"FirstName"
/>
<Binding
Path
=
"LastName"
/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
Il est à noter qu'il existe deux façons de fonctionner pour un multibinding. La première comme ci-dessus, s'appuie sur le StringFormat. Elle possède l'inconvénient d'être en lecture seule. En effet, il serait très complexe pour le framework de deviner l'opération inverse.
Pour pallier ce manque, on peut s'appuyer sur la deuxième façon de fonctionner : un Converter qui sait faire des conversions dans un sens puis dans l'autre (pour peu qu'il soit conçu correctement). Cette fois-ci, ça n'est pas sur l'interface IValueConverter qu'il faut s'appuyer, mais sur sa petite sœur : IMultiValueConverter qui travaille avec des tableaux d'objets.
Voici le même exemple que ci-dessus avec une implémentation de IMultiValueConverter.
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Grid.
Row
=
"0"
Text
=
"First Name"
/>
<TextBlock Grid.
Row
=
"1"
Text
=
"Last Name"
/>
<TextBlock Grid.
Row
=
"2"
Text
=
"Full Name"
/>
<TextBox Grid.
Row
=
"0"
Grid.
Column
=
"1"
Text
=
"{Binding Path=FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
/>
<TextBox Grid.
Row
=
"1"
Grid.
Column
=
"1"
Text
=
"{Binding Path=LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
/>
<TextBox Grid.
Row
=
"2"
Grid.
Column
=
"1"
>
<TextBox.Text>
<MultiBinding>
<MultiBinding.Converter>
<
WpfApplication
:
SpaceSeparatorConverter />
</MultiBinding.Converter>
<Binding
Path
=
"FirstName"
/>
<Binding
Path
=
"LastName"
/>
</MultiBinding>
</TextBox.Text>
</TextBox>
</Grid>
public
class
SpaceSeparatorConverter :
IMultiValueConverter
{
public
object
Convert
(
object
[]
values,
Type targetType,
object
parameter,
CultureInfo culture)
{
return
String.
Join
(
" "
,
values);
}
public
object
[]
ConvertBack
(
object
value
,
Type[]
targetTypes,
object
parameter,
CultureInfo culture)
{
return
((
String) value
).
Split
(
' '
);
}
}
Bien sûr, comme pour tous les Converters, on peut imaginer tout un tas de scénarios comme, par exemple, transformer trois entiers en une couleur et réciproquement.
VIII-A-2. PriorityBinding▲
Le PriorityBinding est assez similaire au MultiBinding : il prend en entrée plusieurs Bindings sauf qu'il n'en utilise qu'un à la fois. Si le premier est disponible, alors il l'utilise sinon il bascule sur le second et ainsi de suite. Pour qu'un binding soit considéré comme disponible, il faut que : le Path soit correct, l'éventuel Converter réussisse la conversion, la valeur soit conforme au type attendu (pas une couleur pour une hauteur de contrôle, par exemple) et que la propriété ne renvoie pas d'erreur.
Dans l'exemple suivant, nous avons trois propriétés. La première est initialisée à la construction, la seconde après cinq secondes et la troisième après quinze secondes. Lorsqu'elles n'ont pas été initialisées, les propriétés renvoient une erreur. À l'exécution, on peut voir que le dernier TextBlock affiche successivement les trois valeurs.
public
class
MyObject :
INotifyPropertyChanged
{
private
string
_immediateValue;
private
string
_shortDelayValue;
private
string
_longDelayValue;
public
String ImmediateValue
{
get
{
if
(
_immediateValue ==
null
)
throw
new
Exception
(
"Value not ready!"
);
return
_immediateValue;
}
set
{
_immediateValue =
value
;
RaisePropertyChanged
(
"ImmediateValue"
);
}
}
public
String ShortDelayValue
{
get
{
if
(
_shortDelayValue ==
null
)
throw
new
Exception
(
"Value not ready!"
);
return
_shortDelayValue;
}
set
{
_shortDelayValue =
value
;
RaisePropertyChanged
(
"ShortDelayValue"
);
}
}
public
String LongDelayValue
{
get
{
if
(
_longDelayValue ==
null
)
throw
new
Exception
(
"Value not ready!"
);
return
_longDelayValue;
}
set
{
_longDelayValue =
value
;
RaisePropertyChanged
(
"LongDelayValue"
);
}
}
public
MyObject
(
)
{
ImmediateValue =
"I'm the immediate value"
;
ThreadPool.
QueueUserWorkItem
(
o =>
{
Thread.
Sleep
(
5000
);
Application.
Current.
Dispatcher.
Invoke
(
new
Action
((
) =>
ShortDelayValue =
"I'm the short delay value"
));
}
);
ThreadPool.
QueueUserWorkItem
(
o =>
{
Thread.
Sleep
(
15000
);
Application.
Current.
Dispatcher.
Invoke
(
new
Action
((
) =>
LongDelayValue =
"I'm the long delay value"
));
}
);
}
public
event
PropertyChangedEventHandler PropertyChanged;
private
void
RaisePropertyChanged
(
string
propertyName)
{
if
(
PropertyChanged !=
null
)
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
propertyName));
}
}
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Grid.
Row
=
"0"
Text
=
"Immediate Value"
/>
<TextBlock Grid.
Row
=
"1"
Text
=
"Short Delay Value"
/>
<TextBlock Grid.
Row
=
"2"
Text
=
"Long Delay Value"
/>
<TextBlock Grid.
Row
=
"3"
Text
=
"Priority Value"
/>
<TextBlock Grid.
Row
=
"0"
Grid.
Column
=
"1"
Text
=
"{Binding Path=ImmediateValue}"
/>
<TextBlock Grid.
Row
=
"1"
Grid.
Column
=
"1"
Text
=
"{Binding Path=ShortDelayValue}"
/>
<TextBlock Grid.
Row
=
"2"
Grid.
Column
=
"1"
Text
=
"{Binding Path=LongDelayValue}"
/>
<TextBlock Grid.
Row
=
"3"
Grid.
Column
=
"1"
>
<TextBlock.Text>
<PriorityBinding>
<Binding
Path
=
"LongDelayValue"
/>
<Binding
Path
=
"ShortDelayValue"
/>
<Binding
Path
=
"ImmediateValue"
/>
</PriorityBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
VIII-B. Spécificités Silverlight▲
VIII-B-1. L'interface INotifyDataErrorInfo▲
Cette interface spécifique à Silverlight permet d'effectuer un retour visuel efficace pour l'utilisateur concernant la validation de sa saisie. Son utilisation a été évoquée lors du paragraphe sur .
VIII-C. Le RelativeSource▲
Auparavant disponible en WPF et de manière limitée sur Silverlight, le RelativeSource est désormais, sur Silverlight 5, pleinement fonctionnel (enfin quasiment).
Il y a plusieurs modes disponibles, ils sont regroupés dans ce tableau, nous verrons juste après la mise en application.
Mode |
Disponibilité |
Fonctionnement |
---|---|---|
PreviousData |
WPF |
Permet de se binder sur la valeur précédente dans la liste (utile dans un ItemsControl par exemple) |
TemplatedParent |
WPF + Silverlight |
Valide uniquement dans un template, permet de se brancher sur l'élément « templaté » |
Self |
WPF + Silverlight |
Permet de s'affranchir du DataContext du contrôle pour se binder sur une propriété de celui-ci |
FindAncestor |
WPF + Silverlight 5 |
Permet de remonter l'arbre visuel jusqu'à trouver un ancêtre. Nouveauté Silverlight 5. |
VIII-C-1. Mode PreviousData▲
Assez peu connu, ce mode est assez élégant pour dégager des tendances (comparer l'élément courant à l'objet précédent). Ainsi, dans l'exemple suivant, on branche une liste de valeurs numériques et à chaque ligne on affiche la valeur et celle d'avant.
<ListBox
ItemsSource
=
"{Binding Path=Values}"
>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel
Orientation
=
"Horizontal"
>
<TextBlock
Text
=
"{Binding}"
FontWeight
=
"Bold"
/>
<TextBlock
Text
=
"{Binding RelativeSource={RelativeSource Mode=PreviousData}, TargetNullValue='', StringFormat=' (Previous was {0})'}"
/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
En poussant l'idée un petit peu et en utilisant un Converter, on peut comparer les deux et dire si on augmente ou si on diminue par rapport au passé :
<ListBox
ItemsSource
=
"{Binding Path=Values}"
>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel
Orientation
=
"Horizontal"
>
<TextBlock
Text
=
"{Binding}"
FontWeight
=
"Bold"
/>
<TextBlock>
<TextBlock.Text>
<MultiBinding
StringFormat
=
" (trend is {0})"
>
<MultiBinding.Converter>
<
WpfApplication
:
TrendConverter />
</MultiBinding.Converter>
<Binding />
<Binding
RelativeSource
=
"{RelativeSource Mode=PreviousData}"
/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
public
class
TrendConverter :
IMultiValueConverter
{
public
object
Convert
(
object
[]
values,
Type targetType,
object
parameter,
CultureInfo culture)
{
if
(
values ==
null
||
values.
Length <
2
)
return
"N/A"
;
var
previous =
(
decimal
?
) values[
1
];
var
current =
(
decimal
?
) values[
0
];
if
(
previous ==
null
)
return
"N/A"
;
if
(
current ==
null
)
return
"N/A"
;
if
(
previous >
current)
return
"Down"
;
else
if
(
previous <
current)
return
"Up"
;
else
return
"Neutral"
;
}
public
object
[]
ConvertBack
(
object
value
,
Type[]
targetTypes,
object
parameter,
CultureInfo culture)
{
throw
new
NotImplementedException
(
);
}
}
VIII-C-2. Mode TemplatedParent▲
Lorsque l'on définit des templates pour ses contrôles, il est nécessaire de pouvoir se brancher sur ce que l'on veut binder, cela peut se faire avec le markup TemplateBinding. Cependant, on est bien vite limité par les possibilités assez restreintes (impossibilité d'utiliser des Converter, par exemple). La parade est donc d'utiliser le TemplatedParent. Ainsi, dans l'exemple suivant, on redéfinit le template d'une TextBox par une autre TextBox liée à une case à cocher. Cette case à cocher est « bindée » sur la propriété IsReadOnly de la TextBox. Ainsi, lorsqu'elle est cochée, impossible de saisir du texte.
<TextBox
Text
=
"I'm a textbox!"
>
<TextBox.Template>
<ControlTemplate
TargetType
=
"TextBox"
>
<StackPanel>
<TextBox
Text
=
"{TemplateBinding Text}"
/>
<CheckBox
Content
=
"IsReadOnly"
IsChecked
=
"{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsReadOnly, Mode=TwoWay}"
/>
</StackPanel>
</ControlTemplate>
</TextBox.Template>
</TextBox>
VIII-C-3. Mode Self▲
Le mode Self permet de s'affranchir du DataContext ambiant pour plutôt se brancher sur les propriétés du contrôle. Ainsi, dans l'exemple suivant, on a un TextBlock qui affiche sa largeur. Lorsque la fenêtre est redimensionnée, la valeur se rafraichit.
<
TextBlock Text=
"{Binding Path=ActualWidth, RelativeSource={RelativeSource Mode=Self}}"
/>
VIII-C-4. Mode FindAncestor▲
FindAncestor agit sur le même principe que Self, mais en remontant dans la hiérarchie visuelle des contrôles. Il faut préciser deux éléments : AncestorType le type d'ancêtre que l'on recherche (une Grid, un StackPanel, etc.) et AncestorLevel, le niveau jusqu'auquel il faut remonter (1 est le premier rencontré). Ainsi, dans l'exemple suivant, le premier TextBox affiche la largeur de la grille la plus à l'intérieur (qui est fixée à 100) c'est le premier ancêtre, le second affiche la largeur de celle à l'extérieur et qui est relative à la taille de la fenêtre.
<Grid>
<Grid
Width
=
"100"
>
<Grid.RowDefinitions>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Grid.
Row
=
"0"
Text
=
"{Binding Path=ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType=Grid}}"
/>
<TextBlock Grid.
Row
=
"1"
Text
=
"{Binding Path=ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=2, AncestorType=Grid}}"
/>
</Grid>
</Grid>