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

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'élement 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 source 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ée il est hérité du premier parent qui le spécifie. Donc lorsque la source n'est pas précisé 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.

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

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

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

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

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

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

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

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

 
Sélectionnez
<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>
 
Sélectionnez
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 mois élégante) est de lever une exception dans le setter. Le fonctionnement est analogue.

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

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

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

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

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

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

 
Sélectionnez
<TextBox Grid.Row="0"
		 Margin="2">
	<TextBox.Text>
		<Binding Path="Value"
				 UpdateSourceTrigger="PropertyChanged"
				 UpdateSourceExceptionFilter="OnUpdateSourceExceptionFilter">
			<Binding.ValidationRules>
				<ExceptionValidationRule />
			</Binding.ValidationRules>
		</Binding>
	</TextBox.Text>
</TextBox>
 
Sélectionnez
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.

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

 
Sélectionnez
<TextBox x:Name="textbox" Text="{Binding Path=Value, IsAsync=True, FallbackValue=Loading...}" />
 
Sélectionnez
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).

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

 
Sélectionnez
<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é (cf ).
Partons de l'exemple de base suivant :

 
Sélectionnez
<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>
 
Sélectionnez
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é au .
Dans sa version la plus simple, voici le Converter :

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

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

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

 
Sélectionnez
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 comme par exemple avec une couleur (premier exemple), un template (second exemple) ou une image (troisième exemple).

Exemple avec couleur
Sélectionnez
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();
	}
}
Exemple avec couleur
Sélectionnez
<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>
Exemple avec template
Sélectionnez
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();
	}
}
Exemple avec template
Sélectionnez
<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>
Exemple avec image
Sélectionnez
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();
	}
}
Exemple avec image
Sélectionnez
<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 comme 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.


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.