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>

