IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Comprendre le Binding en WPF et Silverlight

Comprendre le Binding en WPF et Silverlight


précédentsommairesuivant

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 multi-binding 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.

 
Sélectionnez
<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 multi-binding. 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 palier 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.

 
Sélectionnez
<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>
 
Sélectionnez
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 n'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é renvoient une erreur. À l'exécution, on peut voir que le dernier TextBlock affiche successivement les trois valeurs.

 
Sélectionnez
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));
	}
}
 
Sélectionnez
<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é 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.

 
Sélectionnez
<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é :

 
Sélectionnez
<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>
 
Sélectionnez
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.

 
Sélectionnez
<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 largueur. Lorsque la fenêtre est redimensionnée, la valeur se rafraichi.

 
Sélectionnez
<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.

 
Sélectionnez
<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>

précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2012 Nathanael Marchand. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.