WPF has an extremely rich data binding model. It revolves around the notion that you can take almost any object as your binding source and bind it to almost any target UI element. The binding source can be another UI element, a property of the same element, an XML file, a custom business object, a database, or an in-memory collection. The binding target can be a WPF property, an individual UI element, or a WPF user control or window. But the essential idea is that once a binding is established, the data in the source is automatically and dynamically propagated to the binding target, and vice versa.
How the data object is displayed visually is controlled primarily by data templates, data converters, and data triggers. These take an object as the binding source and translate it for display into a visual structure of UI elements. It is through this mechanism that you can, for example, take a collection of custom business objects, representing products in a product catalog, and display them in a rich and visually compelling manner. Any object can be converted via the data binding system into the UI elements you specify in templates and converters and can adapt and change its display based on your triggers and data template selectors.
Once you become familiar with the data binding system in WPF, you will find it an immensely productive, simple, and effective approach to rich GUI development. The amount of custom application logic you find yourself writing in the code-behind files will dwindle to nonexistence. Before long, you will be enthusiastically and whole-heartedly embracing the wonders of true object-orientated GUI development and wondering how you ever managed without it! This chapter aims to get you up to speed as quickly as possible.
The recipes in this chapter describe how to:
Bind to a property of a UI element (recipe 5-1)
Create a two-way binding (recipe 5-2)
Bind a property of a UI element to itself (recipe 5-3)
Bind to CLR objects, existing object instances, and XML data (recipes 5-4, 5-5, and 5-6)
Bind to a method, a command, and the values in an enumeration (recipes 5-7, 5-8, and 5-9)
Specify a default value for a binding (recipe 5-10)
Use data templates, value converters, and data triggers to display bound data (recipes 5-11, 5-12, 5-13, and 5-14)
Validate bound data (recipes 5-15 and 5-16)
Bind to collections and sort, group, and filter their data (recipes 5-17 - 5-22)
Bind to application settings and resource strings (recipes 5-23 and 5-24)
You need to bind a property of a UI element to a property of another UI element. For example, you need to bind the Text
property of a System.Windows.Controls.TextBlock
control to the Value
property of a System.Windows.Controls.Slider
control so that the text is automatically and dynamically updated when the slider is changed.
Use the System.Windows.Data.Binding
markup extension, and specify the ElementName
and Path
attributes.
The Binding
class creates a relationship between two properties: a binding source and a binding target. In this case, the target is the property of the element with the value you want to set. The source is the property of the element you want to get the value from. The target property must be a System.Windows.DependencyProperty
, is designed to support data binding.
In the XAML for the property you want to set, declare a Binding
statement inside curly braces. Set the ElementName
attribute to the name of the element to use as the binding source object. Set the Path
attribute to the property on the source where the data should come from.
The following example demonstrates a window containing a Slider
control and a TextBlock
control. The XAML statement for the Text
property of the TextBlock
specifies a Binding
statement. This statement binds it to the Value
property of the slider so that when the slider's value changes, the Text
property automatically changes to reflect it.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_01.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 5_01" Height="100" Width="260"> <StackPanel> <Slider Name="slider" Margin="4" Interval="1" TickFrequency="1"
IsSnapToTickEnabled="True" Minimum="0" Maximum="100"/> <StackPanel Orientation="Horizontal" > <TextBlock Width="Auto" HorizontalAlignment="Left" Margin="4" Text="The value property of the slider is:" /> <TextBlock Width="40" HorizontalAlignment="Center" Margin="4" Text="{Binding ElementName=slider, Path=Value}" /> </StackPanel> </StackPanel> </Window>
Figure 5-1 shows the resulting window.
You need to create a two-way binding so that when the value of either property changes, the other one automatically updates to reflect it.
Use the System.Windows.Data.Binding
markup extension, and set the Mode
attribute to System.Windows.Data.BindingMode.TwoWay
. Use the UpdateSourceTrigger
attribute to specify when the binding source should be updated.
The data in a binding can flow from the source property to the target property, can flow from the target property to the source property, or can flow in both directions. For example, suppose the Text
property of a System.Windows.Controls.TextBox
control is bound to the Value
property of a System.Windows.Controls.Slider
control. In this case, the Text
property of the TextBox
control is the target of the binding, and the Value
property of the Slider
control is the binding source. The direction of data flow between the target and the source can be configured in a number of different ways. It could be configured such that when the Value
of the Slider
control changes, the Text
property of the TextBox
is updated. This is called a one-way binding. Alternatively, you could configure the binding so that when the Text
property of the TextBox
changes, the Slider
control's Value
is automatically updated to reflect it. This is called a one-way binding to the source. A two-way binding means that a change to either the source property or the target property automatically updates the other. This type of binding is useful for editable forms or other fully interactive UI scenarios.
It is the Mode
property of a Binding
object that configures its data flow. This stores an instance of the System.Windows.Data.BindingMode
enumeration and can be configured with the values listed in Table 5-1.
Table 5-1. BindingMode
Values for Configuring the Data Flow in a Binding
Value | Description |
---|---|
| The |
| The target property is updated when the control is first loaded or when the data context changes. This type of binding is appropriate if the data is static and won't change once it has been set. |
| The target property is updated whenever the source property changes. This is appropriate if the target control is read-only, such as a |
| This is the opposite of |
| Changes to either the target property or the source automatically update the other. |
Bindings that are TwoWay
or OneWayToSource
listen for changes in the target property and update the source. It is the UpdateSourceTrigger
property of the binding that determines when this update occurs. For example, suppose you created a TwoWay
binding between the Text
property of a TextBox
control and the Value
property of a Slider
control. You could configure the binding so that the slider is updated either as soon as you type text into the TextBox
or when the TextBox
loses its focus. Alternatively, you could specify that the TextBox
is updated only when you explicitly call the UpdateSource
property of the System.Windows.Data.BindingExpression
class. These options are configured by the Binding
's UpdateSourceTrigger
property, which stores an instance of the System.Windows.Data.UpdateSourceTrigger
enumeration. Table 5-2 lists the possible values of this enumeration.
Therefore, to create a two-way binding that updates the source as soon as the target property changes, you need to specify TwoWay
as the value of the Binding
's Mode
attribute and PropertyChanged
for the UpdateSourceTrigger
attribute.
Table 5-2. UpdateSourceTrigger
Values for Configuring When the Binding Source Is Updated
Value | Description |
---|---|
| The |
| Updates the binding source only when you call the |
| Updates the binding source whenever the binding target element loses focus. |
| Updates the binding source immediately whenever the binding target property changes. |
To detect source changes in OneWay
and TwoWay
bindings, if the source property is not a System.Windows.DependencyProperty
, it must implement System.ComponentModel.INotifyPropertyChanged
to notify the target that its value has changed.
The following example demonstrates a window containing a System.Windows.Controls.Slider
control and a System.Windows.Controls.TextBlock
control. The XAML statement for the Text
property of the TextBlock
specifies a Binding
statement that binds it to the Value
property of the Slider
control. In the binding statement, the Mode
attribute is set to TwoWay
, and the UpdateSourceTrigger
attribute is set to PropertyChanged
. This ensures that when a number from 1 to 100 is typed into the TextBox
, the Slider
control immediately updates its value to reflect it.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_02.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 5_02" Height="100" Width="260"> <StackPanel> <Slider Name="slider" Margin="4" Interval="1" TickFrequency="1" IsSnapToTickEnabled="True" Minimum="0" Maximum="100"/> <StackPanel Orientation="Horizontal" > <TextBlock Width="Auto" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="4" Text="Gets and sets the value of the slider:" />
<TextBox Width="40" HorizontalAlignment="Center" Margin="4" Text="{Binding ElementName=slider, Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> </StackPanel> </StackPanel> </Window>
Figure 5-2 shows the resulting window.
Use the RelativeSource
property of the System.Windows.Data.Binding
markup extension, and specify a System.Windows.Data.RelativeSource
of Self
.
The RelativeSource
property of a Binding
designates the binding source by specifying its relationship to the binding target. If the value of this property is set to RelativeSource.Self
, then the source element is the same as the target element.
The following example demonstrates a window containing a System.Windows.Controls.Slider
control. The XAML statement for the ToolTip
property of the Slider
control specifies a Binding
statement that binds it to the Value
property of itself.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_03.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF Recipes 5_03" Height="100" Width="240"> <Grid> <Slider Name="slider" Margin="4" Interval="1" TickFrequency="1" IsSnapToTickEnabled="True" Minimum="0" Maximum="100" ToolTip="{Binding RelativeSource= {RelativeSource Self}, Path=Value}"/> </Grid> </Window>
Figure 5-3 shows the resulting window.
To use a CLR object as a binding source, implement a property change notification mechanism such as the System.ComponentModel.INotifyPropertyChanged
interface.
If you are binding to a CLR object using a System.Windows.Data.BindingMode
of either OneWay
or TwoWay
, you must implement a property change notification mechanism if you want your UI to update dynamically when the source CLR properties change. Such a mechanism is necessary to inform the System.Windows.Data.Binding
that it should update the binding target with the new value in the binding source.
The recommended notification mechanism is for the CLR class to implement the INotifyPropertyChanged
interface. This interface has just one member, an event called PropertyChanged
. When you raise this event, you pass in an instance of the System
.
ComponentModel.PropertyChangedEventArgs
class. This contains a property called PropertyName
, which informs the binding mechanism that the property of the binding source with the specified name has changed its value.
There are alternative notification systems to INotifyPropertyChanged
. You can provide change notifications by supporting the PropertyChanged
pattern for each property that you want change notifications for. To implement this system, you define a PropertyName Changed
event for each property, where PropertyName is the name of the property. You need to raise this event every time the property changes. This was the preferred method to bind to CLR objects in version 1.0 of the .NET Framework and is still supported.
Another option is to back your CLR properties with a corresponding System.Windows.DependencyProperty
. These provide built-in support for data binding.
The recommended pattern for implementing INotifyPropertyChanged
in a CLR class is as follows. Create a method called OnPropertyChanged
that takes the name of the property that has changed as a parameter. In this method, raise the PropertyChanged
event, passing in a new instance of the PropertyChangedEventArgs
class as the event arguments. Initialize the event arguments with the name of the property passed in to the method. It is common practice to implement this pattern in a base CLR class that all your custom business objects derive from. Then, in the setter part of each property in your class, simply call OnPropertyChanged
whenever it is assigned a new value.
The following example demonstrates a window that data binds to an instance of the Person
class in its constructor. It uses three System.Windows.Controls.TextBox
controls and a System.Windows.Controls.ComboBox
control to display the name, age, and occupation data for a person. These UI elements have two-way data bindings to the corresponding CLR properties of the Person
class.
Additionally, there is a System.Windows.Controls.TextBlock
control that has a one-way binding to the read-only Description
property. The value of this CLR property changes whenever the values of the other properties change. To notify the TextBlock
that the description has changed, the Person
class implements the INotifyPropertyChanged
interface.
In the setters for the properties in the Person
class, the OnPropertyChanged
method is called twice. It's called first to notify any bound targets that the value of this property has changed. It's called a second time to notify them that the Description
property has also changed. The OnProperty-Changed
method raises the PropertyChanged
event, passing in the property name.
Figure 5-4 shows the resulting window. If the value in any of the TextBox
controls or the ComboBox
control is changed, then when it loses focus, the description of the person will automatically and dynamically update.
In the code-behind for the window, there is code in the constructor to create and configure an instance of the Person
class and assign it to the DataContext
of the window.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_04.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 5_04" Height="180" Width="260"> <Grid>
<Grid.ColumnDefinitions> <ColumnDefinition Width="74"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="26"/> <RowDefinition Height="26"/> <RowDefinition Height="26"/> <RowDefinition Height="26"/> <RowDefinition Height="10"/> <RowDefinition Height="26"/> </Grid.RowDefinitions> <TextBlock Margin="4" Text="First Name" VerticalAlignment="Center"/> <TextBox Text="{Binding Path=FirstName, Mode=TwoWay}" Margin="4" Grid.Column="1"/> <TextBlock Margin="4" Text="Last Name" Grid.Row="1" VerticalAlignment="Center"/> <TextBox Margin="4" Text="{Binding Path=LastName, Mode=TwoWay}" Grid.Column="1" Grid.Row="1"/> <TextBlock Margin="4" Text="Age" Grid.Row="2" VerticalAlignment="Center"/> <TextBox Margin="4" Text="{Binding Path=Age, Mode=TwoWay}" Grid.Column="1" Grid.Row="2"/> <TextBlock Margin="4" Text="Occupation" Grid.Row="3" VerticalAlignment="Center"/>
<ComboBox x:Name="cboOccupation" IsEditable="False" Grid.Column="1" Grid.Row="3" HorizontalAlignment="Left" Text="{Binding Path=Occupation, Mode=TwoWay}" Margin="4" Width="140"> <ComboBoxItem>Student</ComboBoxItem> <ComboBoxItem>Skilled</ComboBoxItem> <ComboBoxItem>Professional</ComboBoxItem> </ComboBox> <TextBlock Margin="4" Text="Description" FontWeight="Bold" FontStyle="Italic" Grid.Row="5" VerticalAlignment="Center"/> <TextBlock Margin="4" Text="{Binding Path=Description, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" FontStyle="Italic" Grid.Column="1" Grid.Row="5"/> </Grid> </Window>
The code-behind for the window is as follows:
using System.Windows; using System.Windows; namespace Recipe_05_04 { public partial class Window1 : Window { public Window1() { InitializeComponent(); // Set the DataContext to a Person object this.DataContext = new Person()
{ FirstName = "Elin", LastName = "Binkles", Age = 26, Occupation = "Professional" }; } } }
The code for the Person class is as follows:
using System.ComponentModel; namespace Recipe_05_04 { public class Person : INotifyPropertyChanged { private string firstName; private string lastName; private int age; private string occupation; // Each property calls the OnPropertyChanged method // when its value changed, and each property that // affects the Person's Description also calls the // OnPropertyChanged method for the Description property. public string FirstName { get { return firstName; } set { if(firstName != value) { firstName = value; OnPropertyChanged("FirstName"); OnPropertyChanged("Description"); } } }
public string LastName { get { return lastName; } set { if(this.lastName != value) { this.lastName = value; OnPropertyChanged("LastName"); OnPropertyChanged("Description"); } } } public int Age { get { return age; } set { if(this.age != value) { this.age = value; OnPropertyChanged("Age"); OnPropertyChanged("Description"); } } } public string Occupation { get { return occupation; } set { if (this.occupation != value) { this.occupation = value; OnPropertyChanged("Occupation"); OnPropertyChanged("Description"); } } }
// The Description property is read-only // and is composed of the values of the // other properties. public string Description { get { return string.Format("{0} {1}, {2} ({3})", firstName, lastName, age, occupation); } } #region INotifyPropertyChanged Members /// Implement INotifyPropertyChanged to notify the binding /// targets when the values of properties change. public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged( string propertyName) { if(this.PropertyChanged != null) { // Raise the PropertyChanged event this.PropertyChanged( this, new PropertyChangedEventArgs( propertyName)); } } #endregion } }
Figure 5-4 shows the resulting window.
You need to bind a number of UI elements to an object that can be created and populated only at runtime, for example, a custom business object containing live data or a System.Data.DataSet
object that's created in response to a database query.
Create System.Windows.Data.Binding
statements in the XAML for your elements to bind the properties of your UI elements to the properties of your data source. Use the Path
property of the Binding
class to specify the name of the property of the data source to bind to, but do not specify a value for its Source
property.
At runtime, assign an existing object instance to the DataContext
property of a System
. Windows.FrameworkElement
. This FrameworkElement
must be a UI element that is a parent element of all the child elements that need to bind to it.
This is also the recommended method of data binding when you need to bind more than one property to a particular source. Because the DataContext
of a parent element is inherited by all its child elements, as explained in the "How It Works" section, you don't need to specify the source multiple times. However, when you need to bind only one property to a source, it can be simpler and more convenient to define a data source as a static resource and reference it in the Source
property of the binding. This can be easier to debug, because you can see all the information about the binding in one place, instead of having to search for the nearest DataContext
to understand what is happening.
In this chapter, you have so far seen three different ways of specifying the data source of a Binding
: using its ElementName
, RelativeSource
, and Source
properties. Table 5-3 lists these different options.
Table 5-3. Ways of Specifying the Data Source for a Binding
Property | Description |
---|---|
| Use this to reference an instance of an object created as a resource. |
| Use this to specify a UI element that is relative to the binding target. |
| Use this to specify another UI element on your application. |
However, if none of these properties has been set, the binding system will traverse up the tree of elements, looking for the nearest one with a value for its DataContext
property. This allows a DataContext
to be established for one root element and then inherited automatically by all its child elements.
Setting the DataContext
of a FrameworkElement
programmatically at runtime automatically updates any inherited bindings on its child elements. This makes it an ideal candidate to use when binding multiple elements to multiple properties of the same source object. It also makes it ideal when you want to bind your controls to a different instance of the same class at runtime.
The following example demonstrates a window containing three System.Windows.Controls.TextBox
objects that display the name and age data for a person. The Person
class is defined in the code-behind for the window and represents a simple custom business object.
In the XAML for the window, the Text
property of each TextBox
is set to a Binding
. Each Binding
specifies a property of the Person
class as its Path
but doesn't specify anything for its Source
.
In the code-behind for the window, there is code in the constructor to create and configure an instance of the Person
class and assign it to the DataContext
of the window.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_05.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 5_05" Height="120" Width="300"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="60"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions>a <RowDefinition Height="26"/> <RowDefinition Height="26"/> <RowDefinition Height="26"/> </Grid.RowDefinitions> <TextBlock Margin="4" Text="First Name" VerticalAlignment="Center"/> <TextBox Margin="4" Text="{Binding Path=FirstName}" Grid.Column="1"/> <TextBlock Margin="4" Text="Last Name" Grid.Row="1" VerticalAlignment="Center"/> <TextBox Margin="4" Text="{Binding Path=LastName}" Grid.Column="1" Grid.Row="1"/>
<TextBlock Margin="4" Text="Age" Grid.Row="2" VerticalAlignment="Center"/> <TextBox Margin="4" Text="{Binding Path=Age}" Grid.Column="1" Grid.Row="2"/> </Grid> </Window>
The code-behind for the window is as follows:
using System.Windows; using Recipe_05_05; namespace Recipe_05_05 { public partial class Window1 : Window { public Window1() { InitializeComponent(); // Set the DataContext to a Person object this.DataContext = new Person() { FirstName = "Nelly", LastName = "Blinks", Age = 26 }; } } }
Figure 5-5 shows the resulting window.
Create a System.Windows.Data.XmlDataProvider
in the System.Windows.ResourceDictionary
for your window, and either embed the XML data inline as a data island or set the Source
property to reference an embedded XML file and then use this XmlDataProvider
as the Source
of a binding.
The XmlDataProvider
class provides a simple way to create a bindable data source from XML data. It can be declared in a Resources
section and then referenced via a key. Its data can be declared either inline via the XmlDataProvider's Content
property; or, if the XML resides in a separate file, you can use the Source property to reference it via an appropriate Uri
.
The XmlDataProvider
can be referenced in a binding as a static resource, and the XPath
property of the binding can be used to set an XPath query to populate it with the required subset of data. XPath, short for XML Path Language, is a W3C Recommendation published at http://www.w3.org/TR/xpath
.
When embedding XML data directly into the Content
property of an XmlDataProvider
, it must be given an empty xmlns
attribute, or your XPath queries will not work as expected. Instead, they will be qualified by the System.Windows
namespace, and your output window will show that a System.Windows. Data.Error
exception has occurred.
The following example demonstrates a window that creates an XmlDataProvider
as a static resource and sets its content to embedded XML data containing a list of countries. The XmlDataProvider
is given a key and is then referenced in the ItemsSource
property of a System.Windows.Controls.ListBox
.
The XPath
property of the ListBox
specifies that the relevant data is the Name
attribute of each Country
in Countries
.
<Window x:Class="Recipe_05_06.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 5_06" Height="240" Width="200"> <Window.Resources> <!-- Use the Source attribute to specify an embedded XML Data File--> <!--<XmlDataProvider x:Key="CountriesXML" Source="Countries.xml" XPath="Countries"/>--> <!-- Or embed the data directly --> <XmlDataProvider x:Key="CountriesXML"> <x:XData>
<Countries xmlns="" > <Country Name="Great Britan" Continent="Europe" /> <Country Name="USA" Continent="NorthAmerica" /> <Country Name="Canada" Continent="NorthAmerica" /> <Country Name="France" Continent="Europe" /> <Country Name="Germany" Continent="Europe" /> <Country Name="Italy" Continent="Europe" /> <Country Name="Spain" Continent="Europe" /> <Country Name="Brazil" Continent="SouthAmerica" /> <Country Name="Argentina" Continent="SouthAmerica" /> <Country Name="China" Continent="Asia" /> <Country Name="India" Continent="Asia" /> <Country Name="Japan" Continent="Asia" /> <Country Name="South Africa" Continent="Africa" /> <Country Name="Tunisia" Continent="Africa" /> <Country Name="Egypt" Continent="Africa" /> </Countries> </x:XData> </XmlDataProvider> </Window.Resources> <Grid> <ListBox ItemsSource="{Binding Source={StaticResource CountriesXML}, XPath=/Countries/Country/@Name}" /> </Grid> </Window>
Figure 5-6 shows the resulting window.
Use the System.Windows.Data.ObjectDataProvider
class to make a method of a class available as a binding source, and bind to its results.
The ObjectDataProvider
can be created as a resource in your window or control and acts as a wrapper to expose a method as a binding source. Use its ObjectType
and MethodName
properties to specify the names of the class and the method to bind to. Then simply reference the ObjectDataProvider
in a binding statement, and the target property will receive the return value of the method.
If the method expects any parameters, they must be declared in the ObjectDataProvider's MethodParameters
collection. To specify the values of the parameters to pass to the method, you can create separate bindings that pull in the values from other UI elements. To do this, create binding statements that reference the ObjectDataProvider
as the binding source. Then set the Path
attribute to the relevant item in the MethodParameters
collection. Set the BindsDirectlyToSource
property of the ObjectDataProvider
to True
. This signals to the ObjectDataProvider
that the binding Path
statement should be evaluated relative to itself, not to the data item it wraps.
The following example demonstrates a window containing an ObjectDataProvider
that creates a binding source for a method called Convert
on the DistanceConverter
class. The purpose of this method is to convert miles into kilometers, and vice versa. It takes two parameters: a double
specifying the amount and a DistanceType
enumeration that represents the unit that the amount is in. These parameters are declared in the ObjectDataProvider's MethodParameters
collection.
The window displays a System.Windows.Controls.TextBlock
control that binds to the result of the method. There is also a System.Windows.Controls.TextBox
control and a System.Windows. Controls.ComboBox
control. These bind to the first and second parameters of the method, respectively.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_07.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:system="clr-namespace:System;assembly=mscorlib" xmlns:Recipe_05_07="clr-namespace:Recipe_05_07"
Title="WPF Recipes 5_07" Width="240" Height="150" > <Window.Resources> <Recipe_05_07:DoubleToString x:Key="doubleToString" /> <!-- The ObjectDataProvider exposes the method as a binding source --> <ObjectDataProvider x:Key="convertDistance" ObjectType="{x:Type Recipe_05_07:DistanceConverter }" MethodName="Convert" > <!-- Declare the parameters the method expects--> <ObjectDataProvider.MethodParameters> <system:Double>0</system:Double> <Recipe_05_07:DistanceType>Miles</Recipe_05_07:DistanceType> </ObjectDataProvider.MethodParameters> </ObjectDataProvider> </Window.Resources> <Grid Margin="10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="0.5*"/> <ColumnDefinition Width="0.5*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="31" /> <RowDefinition Height="31" /> <RowDefinition Height="31" /> </Grid.RowDefinitions> <TextBlock Margin="5" Grid.ColumnSpan="2" VerticalAlignment="Center" Text="Enter a distance to convert:"/> <!-- This TextBox binds to the 1st paramter of the method --> <TextBox Grid.Row="1" Grid.Column="0" Margin="5" Text ="{Binding Source={StaticResource convertDistance}, Path=MethodParameters[0], BindsDirectlyToSource=true, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource doubleToString}}"/>
<!-- This TextBox binds to the 1st paramter of the method --> <ComboBox Grid.Row="1" Grid.Column="1" Margin="5" Width="80" HorizontalAlignment="Left" SelectedValue="{Binding Source={StaticResource convertDistance}, Path=MethodParameters[1], BindsDirectlyToSource=true}" > <Recipe_05_07:DistanceType>Miles</Recipe_05_07:DistanceType> <Recipe_05_07:DistanceType>Kilometres</Recipe_05_07:DistanceType> </ComboBox> <TextBlock Grid.Row="2" HorizontalAlignment="Right" Margin="5" Text="Result:"/> <!-- The TextBlock that binds to the results of the method.--> <TextBlock Grid.Row="2" Grid.Column="1" Margin="5" Text="{Binding Source={StaticResource convertDistance}}"/> </Grid> </Window>
The code for the DistanceConverter
class is as follows:
using System; namespace Recipe_05_07 { public enum DistanceType { Miles, Kilometres } public class DistanceConverter { /// <summary> /// Convert miles to kilometres and vice versa. /// </summary> /// <param name="amount">The amount to convert.</param> /// <param name="distancetype">The units the amount is in.</param> /// <returns>A string containing the converted amount.</returns> public string Convert( double amount, DistanceType distancetype)
{ if(distancetype == DistanceType.Miles) return (amount * 1.609344).ToString("0.##") + " km"; if(distancetype == DistanceType.Kilometres) return (amount * 0.621371192).ToString("0.##") + " m"; throw new ArgumentOutOfRangeException("distanceType"); } } }
Figure 5-7 shows the resulting window.
You need to bind a System.Windows.Controls.Button
control directly to a System.Windows. Input.ICommand
. This enables you to execute custom logic when the Button
is clicked, without having to handle its Click
event and call a method. You can also bind the IsEnabled
property of the Button
to the ICommand
object's CanExecute
method.
Create a class that implements ICommand
, and expose an instance of it as a property on another class or business object. Bind this property to a Button
control's Command
property.
The Button
control derives from the System.Windows.Controls.Primitives.ButtonBase
class. This implements the System.Windows.Input.ICommandSource
interface and exposes an ICommand
property called Command
. The ICommand
interface encapsulates a unit of functionality. When its Execute
method is called, this functionality is executed. The CanExecute
method determines whether the ICommand
can be executed in its current state. It returns True
if the ICommand
can be executed and returns False
if not.
To execute custom application logic when a Button
is clicked, you would typically attach an event handler to its Click
event. However, you can also encapsulate this custom logic in a command and bind it directly to the Button
control's Command
property. This approach has several advantages. First, the IsEnabled
property of the Button
will automatically be bound to the CanExecute
method of the ICommand
. This means that when the CanExecuteChanged
event is fired, the Button
will call the command's CanExecute
method and refresh its own IsEnabled
property dynamically. Second, the application functionality that should be executed when the Button
is clicked does not have to reside in the code-behind for the window. This enables greater separation of presentation and business logic, which is always desirable in object-oriented programming in general, and even more so in WPF development, because it makes it easier for UI designers to work alongside developers without getting in each other's way.
To bind the Command
property of a Button
to an instance of an ICommand
, simply set the Path
attribute to the name of the ICommand
property, just as you would any other property. You can also optionally specify parameters using the CommandParameter
attribute. This in turn can be bound to the properties of other elements and is passed to the Execute
and CanExecute
methods of the command.
The following example demonstrates a window containing three System.Windows.Controls.TextBox
controls. These are bound to the FirstName
, LastName
, and Age
properties of a custom Person
object. The Person
class also exposes an instance of the AddPersonCommand
and SetOccupationCommand
as read-only properties. There are two Button
controls on the window that have their Command
attribute bound to these command properties. Custom logic in the CanExecute
methods of the commands specifies when the Buttons
should be enabled or disabled. If the ICommand
can be executed and the Button
should therefore be enabled, the code in the CanExecute
method returns True
. If it returns False
, the Button
will be disabled. The Set Occupation Button
control also binds its CommandParameter
to the Text
property of a System. Windows.Controls.ComboBox
control. This demonstrates how to pass parameters to an instance of an ICommand
.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_08.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 5_08" Height="224" Width="300"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="60"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="26"/> <RowDefinition Height="26"/> <RowDefinition Height="26"/> <RowDefinition Height="40"/>
<RowDefinition Height="34"/> <RowDefinition Height="26"/> </Grid.RowDefinitions> <TextBlock Margin="4" Text="First Name" VerticalAlignment="Center"/> <TextBox Text="{Binding Path=FirstName}" Margin="4" Grid.Column="1"/> <TextBlock Margin="4" Text="Last Name" Grid.Row="1" VerticalAlignment="Center"/> <TextBox Margin="4" Text="{Binding Path=LastName}" Grid.Column="1" Grid.Row="1"/> <TextBlock Margin="4" Text="Age" Grid.Row="2" VerticalAlignment="Center"/> <TextBox Margin="4" Text="{Binding Path=Age}" Grid.Column="1" Grid.Row="2"/> <!-- Bind the Button to the Add Command --> <Button Command="{Binding Path=Add}" Content="Add" Margin="4" Grid.Row="3" Grid.Column="2"/> <StackPanel Orientation="Horizontal" Grid.Column="2" Grid.Row="4">
<ComboBox x:Name="cboOccupation" IsEditable="False" Margin="4" Width="100"> <ComboBoxItem>Student</ComboBoxItem> <ComboBoxItem>Skilled</ComboBoxItem> <ComboBoxItem>Professional</ComboBoxItem> </ComboBox> <Button Command="{Binding Path=SetOccupation}" CommandParameter="{Binding ElementName=cboOccupation, Path=Text}" Content="Set Occupation" Margin="4" /> </StackPanel> <TextBlock Margin="4" Text="Status" Grid.Row="5" VerticalAlignment="Center"/> <TextBlock Margin="4" Text="{Binding Path=Status, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" FontStyle="Italic" Grid.Column="1" Grid.Row="5"/> </Grid> </Window>
The code-behind for the window sets its DataContext
property to a new Person
object. The code for this is as follows:
using System.Windows; using Recipe_05_08; namespace Recipe_05_08 { public partial class Window1 : Window { public Window1() { InitializeComponent();
// Set the DataContext to a Person object this.DataContext = new Person() { FirstName = "Ellin", LastName = "Blinks", }; } } }
The code for the Person
class, which also contains the command classes, is as follows:
using System; using System.ComponentModel; using System.Windows.Input; namespace Recipe_05_08 { public class Person : INotifyPropertyChanged { private string firstName; private int age; private string lastName; private string status; private string occupation; private AddPersonCommand addPersonCommand; private SetOccupationCommand setOccupationCommand; public string FirstName { get { return firstName; } set { if(firstName != value) { firstName = value; OnPropertyChanged("FirstName"); } } }
public string LastName { get { return lastName; } set { if(this.lastName != value) { this.lastName = value; OnPropertyChanged("LastName"); } } } public int Age { get { return age; } set { if(this.age != value) { this.age = value; OnPropertyChanged("Age"); } } } public string Status { get { return status; } set { if(this.status != value) { this.status = value; OnPropertyChanged("Status"); } } }
public string Occupation { get { return occupation; } set { if(this.occupation != value) { this.occupation = value; OnPropertyChanged("Occupation"); } } } /// Gets an AddPersonCommand for data binding public AddPersonCommand Add { get { if(addPersonCommand == null) addPersonCommand = new AddPersonCommand(this); return addPersonCommand; } } /// Gets a SetOccupationCommand for data binding public SetOccupationCommand SetOccupation { get { if(setOccupationCommand == null) setOccupationCommand = new SetOccupationCommand(this); return setOccupationCommand; } } #region INotifyPropertyChanged Members /// Implement INotifyPropertyChanged to notify the binding /// targets when the values of properties change. public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName) { if(this.PropertyChanged != null) { this.PropertyChanged( this, new PropertyChangedEventArgs(propertyName)); } } #endregion } public class AddPersonCommand : ICommand { private Person person; public AddPersonCommand(Person person) { this.person = person; this.person.PropertyChanged += new PropertyChangedEventHandler(person_PropertyChanged); } // Handle the PropertyChanged event of the person to raise the // CanExecuteChanged event private void person_PropertyChanged( object sender, PropertyChangedEventArgs e) { if(CanExecuteChanged != null) { CanExecuteChanged(this, EventArgs.Empty); } } #region ICommand Members /// The command can execute if there are valid values /// for the person's FirstName, LastName, and Age properties /// and if it hasn't already been executed and had its /// Status property set. public bool CanExecute(object parameter) { if(!string.IsNullOrEmpty(person.FirstName)) if(!string.IsNullOrEmpty(person.LastName)) if(person.Age > 0) if(string.IsNullOrEmpty(person.Status)) return true;
return false; } public event EventHandler CanExecuteChanged; /// When the command is executed, update the /// status property of the person. public void Execute(object parameter) { person.Status = string.Format("Added {0} {1}", person.FirstName, person.LastName); } #endregion } public class SetOccupationCommand : ICommand { private Person person; public SetOccupationCommand(Person person) { this.person = person; this.person.PropertyChanged += new PropertyChangedEventHandler(person_PropertyChanged); } // Handle the PropertyChanged event of the person to raise the // CanExecuteChanged event private void person_PropertyChanged( object sender, PropertyChangedEventArgs e) { if(CanExecuteChanged != null) { CanExecuteChanged(this, EventArgs.Empty); } } #region ICommand Members /// The command can execute if the person has been added, /// which means its Status will be set, and if the occupation
/// parameter is not null public bool CanExecute(object parameter) { if(!string.IsNullOrEmpty(parameter as string)) if(!string.IsNullOrEmpty(person.Status)) return true; return false; } public event EventHandler CanExecuteChanged; /// When the command is executed, set the Occupation /// property of the person, and update the Status. public void Execute(object parameter) { // Get the occupation string from the command parameter person.Occupation = parameter.ToString(); person.Status = string.Format("Added {0} {1}, {2}", person.FirstName, person.LastName, person.Occupation); } #endregion } }
Figure 5-8 shows the resulting window.
You need to bind a System.Windows.Controls.ItemsControl
to all the possible values of an enumeration.
Use the System.Windows.Data.ObjectDataProvider
class to make the values of a System.Enum
available as a binding source. Bind the ObjectDataProvider
to the ItemsSource
property of an ItemsControl
.
You can create the ObjectDataProvider
as a resource in your window or control and can expose the values of an Enum
as a binding source. In your XAML, declare an ObjectDataProvider
, and set the MethodName
and ObjectType
attributes to the GetValues
method of the System.Enum
class. Add the type of the Enum
you want to convert to the MethodParameters
collection of the ObjectDataProvider
. Then simply bind the ItemsSource
property of an ItemsControl
, such as a System.Windows.Controls.ComboBox
or System.Windows.Controls.ListBox
control, to this ObjectDataProvider
.
The following example demonstrates a window containing an ObjectDataProvider
that creates a binding source for an enumeration called DaysOfTheWeek
. Unsurprisingly, this enumerates the days of the week, from Monday to Sunday. The window contains a ComboBox
that binds to the ObjectDataProvider
and displays the values of this Enum
.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_09.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:System="clr-namespace:System;assembly=mscorlib" xmlns:Recipe_05_09="clr-namespace:Recipe_05_09" Title="WPF Recipes 5_09" Height="100" Width="180"> <Window.Resources> <!-- The ObjectDataProvider exposes the enum as a binding source --> <ObjectDataProvider x:Key="daysData" MethodName="GetValues" ObjectType="{x:Type System:Enum}" >
<!-- Pass the DaysOfTheWeek type to the --> <!-- GetValues property of System.Enum. --> <ObjectDataProvider.MethodParameters> <x:Type TypeName="Recipe_05_09:DaysOfTheWeek"/> </ObjectDataProvider.MethodParameters> </ObjectDataProvider> </Window.Resources> <StackPanel> <TextBlock Margin="5" Text="Select the day of the week:"/> <!-- Binds to the ObjectDataProvider --> <ComboBox Margin="5" ItemsSource="{Binding Source={StaticResource daysData}}" /> </StackPanel> </Window>
The DaysOfTheWeek
enumeration is declared in the code-behind for the window, which is as follows:
using System.Windows; namespace Recipe_05_09 { /// <summary> /// The Days of the Week enumeration /// </summary> public enum DaysOfTheWeek { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }
public partial class Window1 : Window { public Window1() { InitializeComponent(); } } }
Figure 5-9 shows the resulting window.
You need to specify a default value for a data binding as a fallback in case the source property of the binding cannot always be resolved.
The FallbackValue
property specifies the value to use when the binding is unable to return a value. A binding may be unable to return a value for any of the following reasons:
The path to the binding cannot be resolved successfully.
The value converter, if there is one, cannot convert the resulting value.
The resulting value is not valid for the binding target property.
In any of these cases, the target property is set to the value of the FallbackValue
, if one is available.
Specifying a default value for a binding can be very useful when working with design tools such as Microsoft Expression Blend. If the data source for a binding is assigned only at runtime, then the target property will not display anything in design mode. This means designers don't see a realistic view of the UI they are designing.
The following example demonstrates a window containing three System.Windows.Controls.TextBox
objects that display the name and age of a person. The window is never actually assigned a data source, so when the application is run, the TextBox
objects are empty by default. However, in the binding statement for each TextBox
, the FallbackValue
is specified. This ensures that when the application is run, the TextBox
objects display default values.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_10.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 5_10" Height="120" Width="300"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="60"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="28"/> <RowDefinition Height="28"/> <RowDefinition Height="28"/> </Grid.RowDefinitions> <TextBlock Margin="4" Text="First Name" VerticalAlignment="Center"/> <TextBox Margin="4" Text="{Binding Path=FirstName, FallbackValue=First name goes here}" FontStyle="Italic" Grid.Column="1"/> <TextBlock Margin="4" Text="Last Name" Grid.Row="1" VerticalAlignment="Center"/>
<TextBox Margin="4" Text="{Binding Path=LastName, FallbackValue=Second name goes here}" FontStyle="Italic" Grid.Column="1" Grid.Row="1"/> <TextBlock Margin="4" Text="Age" Grid.Row="2" VerticalAlignment="Center"/> <TextBox Margin="4" Text="{Binding Path=Age, FallbackValue=Age goes here}" FontStyle="Italic" Grid.Column="1" Grid.Row="2"/> </Grid> </Window>
Figure 5-10 shows the resulting window.
Create a System.Windows.DataTemplate
to define the presentation of your data objects. This specifies the visual structure of UI elements to use to display your data.
When you bind to a data object, the binding target displays a string representation of the object by default. Internally, this is because without any specific instructions the binding mechanism calls the ToString
method of the binding source when binding to it. Creating a DataTemplate
enables you to specify a different visual structure of UI elements when displaying your data object. When the binding mechanism is asked to display a data object, it will use the UI elements specified in the DataTemplate
to render it.
The following example demonstrates a window that contains a System.Windows.Controls.ListBox
control. The ItemsSource
property of the ListBox
is bound to a collection of Person
objects. The Person
class is defined in the Data.cs
file and exposes FirstName, LastName, Age
and Photo
properties. It also overrides the ToString
method to return the full name of the person it represents. Without a DataTemplate
, the ListBox
control would just display this list of names. Figure 5-11 shows what this would look like.
However, the ItemTemplate
property of the ListBox
is set to a static resource called personTemplate
. This is a DataTemplate
resource defined in the window's System.Windows.ResourceDictionary
. The DataTemplate
creates a System.Windows.Controls.Grid
control inside a System.Windows.Controls.Border
control. Inside the Grid
, it defines a series of System. Windows.Controls.TextBlock
controls and a System.Windows.Controls.Image
control. These controls have standard binding statements that bind their properties to properties on the Person
class. When the window opens and the ListBox
binds to the collection of Person
objects, the binding mechanism uses the set of UI elements in the DataTemplate
to display each item. Figure 5-12 shows the same ListBox
as in Figure 5-11 but with its ItemTemplate
property set to the DataTemplate
.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_11.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_05_11="clr-namespace:Recipe_05_11" Title="WPF Recipes 5_11" Height="298" Width="260"> <Window.Resources>
<!-- Creates the local data source for binding --> <Recipe_05_11:People x:Key="people"/> <!-- Styles used by the UI elements in the DataTemplate --> <Style x:Key="lblStyle" TargetType="{x:Type TextBlock}"> <Setter Property="FontFamily" Value="Tahoma"/> <Setter Property="FontSize" Value="11pt"/> <Setter Property="VerticalAlignment" Value="Center"/> <Setter Property="Margin" Value="2"/> <Setter Property="Foreground" Value="Red"/> </Style> <Style x:Key="dataStyle" TargetType="{x:Type TextBlock}" BasedOn="{StaticResource lblStyle}"> <Setter Property="Margin" Value="10,2,2,2"/> <Setter Property="Foreground" Value="Blue"/> <Setter Property="FontStyle" Value="Italic"/> </Style> <!-- DataTemplate to use for displaying each Person item --> <DataTemplate x:Key="personTemplate"> <Border BorderThickness="1" BorderBrush="Gray" Padding="4" Margin="4" Height="Auto" Width="Auto"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="80"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <StackPanel> <TextBlock Style="{StaticResource lblStyle}" Text="First Name" /> <TextBlock Style="{StaticResource dataStyle}" Text="{Binding Path=FirstName}"/>
<TextBlock Style="{StaticResource lblStyle}" Text="Last Name" /> <TextBlock Style="{StaticResource dataStyle}" Text="{Binding Path=LastName}" /> <TextBlock Style="{StaticResource lblStyle}" Text="Age" /> <TextBlock Style="{StaticResource dataStyle}" Text="{Binding Path=Age}" /> </StackPanel> <Image Margin="4" Grid.Column="1" Width="96" Height="140" Source="{Binding Path=Photo}"/> </Grid> </Border> </DataTemplate> </Window.Resources> <Grid> <!-- The ListBox binds to the people collection, and sets the --> <!-- DataTemplate to use for displaying each item --> <ListBox Margin="10" ItemsSource="{Binding Source={StaticResource people}}" ItemTemplate="{StaticResource personTemplate}"/> <!-- Without specifying a DataTemplate, the ListBox just --> <!-- displays a list of names. --> <!--<ListBox Margin="10" ItemsSource="{Binding Source={StaticResource people}}"/>--> </Grid> </Window>
Figure 5-12 shows the resulting window.
You need to convert the source value of a binding in order to assign it to the target value. For example, you need to bind one type of property to a completely different type of property, such as binding an integer
value to a System.Windows.Controls.Control.Foreground
property. Alternatively, you may need to bind two values of the same type but derive the value of the target property from a calculation based on the value of the source. For example, your data has a property of type double
that you want to bind to a System.Windows.FrameworkElement.Width
property.
Create a class that implements the System.Windows.Data.IValueConverter
interface. Add custom logic to the Convert
method to apply a conversion to the data that will be assigned to the binding. Declare this converter class as a static resource, and reference it as the Converter
property of a System.Windows.Data.Binding
in your XAML.
WPF includes a few value converters out of the box for common data binding scenarios. For example, the System.Windows.Controls.BooleanToVisibilityConverter
class, which converts a Boolean value to a System.Windows.Visibility
value. This is extremely useful for specifying whether a particular UI element and its children should be displayed at runtime, based on the value of a Boolean property.
The IValueConverter
interface has two methods, Convert
and ConvertBack
. When you specify the Converter
property of a binding, the source value is not bound directly to the target value. Instead, it is passed in as the value
parameter to the Convert
method of the converter. The Convert
method that receives the value is then free to apply conversion logic based on its value. It returns an instance of the type expected by the binding target.
The ConvertBack
method is called in a two-way binding, when the binding target propagates a value to the binding source. In one-way bindings, it can simply throw a System.NotImplementedException
instance, because it should never be called.
If you need to convert the values from multiple properties into a single value to assign to a binding target, use a System.Windows.Data.IMultiValueConverter
. This associates a converter with a System.Windows.Data.MultiBinding
class, which attaches a collection of System.Windows. Data.Binding
objects to a single binding target property. This is useful when the value of a bound property should be updated whenever the values of multiple source properties change.
When naming a converter, it is good practice to use the convention <Source Type or Name>To<Target Type>Converter
, for example, DoubleToWidthProperty
or ProbabilityToOpacityConverter
. It is also good practice to decorate the converter class with the System.Windows.Data.ValueConversionAttribute
to indicate to development tools the data types involved in the conversion.
The following example demonstrates a window containing a System.Windows.Controls.ItemsControl
that displays a collection of DataItem
objects. This object exposes a Percent
property, which contains a double
value from −100 to +100. The window contains a System.Windows.DataTemplate
in its Resources
collection. This specifies that each DataItem
should be displayed as a System.Windows.Shapes.Rectangle
, which has the effect of presenting the data items in the form of a simple bar graph.
The window declares two converter classes. These are referenced in the binding statements in the XAML for the rectangle. The Height
property of each rectangle is bound to the Percent
property of its DataItem
and is sent through an instance of the PercentToHeightConverter
. In this case, both the source property, Percent
, and the target property, Height
, are double
values. However, a conversion has to take place to translate the value of Percent
, into a valid Height
value. The Fill
property of the rectangle is also bound to the Percent
property of DataItem
. However, this binding requires a value converter, because a double
value cannot be converted directly to a System.Windows.Media.Brush
value. The PercentToFillConverter
intercepts the binding and returns one color of Brush
if the Percent
value is positive and another value if it is negative.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_12.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Using_Value_Converters" xmlns:Recipe_05_12="clr-namespace:Recipe_05_12" x:Name="thisWindow" Title="WPF Recipes 5_12" Height="240" Width="280"> <Window.Resources> <local:DataItems x:Key="dataItems"/> <!-- Declare two converter classes --> <Recipe_05_12:PercentToHeightConverter x:Key="percentToHeightConverter" /> <Recipe_05_12:PercentToFillConverter x:Key="percentToFillConverter" /> <!-- Bind the rectangle's height and color to the data's --> <!-- Percent property, but apply a conversion --> <!-- to it using the two converter classes. --> <DataTemplate x:Key="dataItemtemplate"> <Rectangle Margin="4" Width="30" VerticalAlignment="Bottom" Height="{Binding Path=Percent, Converter={StaticResource percentToHeightConverter}}" Fill="{Binding Path=Percent, Converter={StaticResource percentToFillConverter}}"/> </DataTemplate> </Window.Resources> <Grid Margin="20"> <Grid.ColumnDefinitions> <ColumnDefinition Width="1"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition Height="1"/> </Grid.RowDefinitions>
<ItemsControl Grid.Column="1" Margin="4,0,0,4" ItemsSource="{Binding Source={StaticResource dataItems}}" ItemTemplate="{StaticResource dataItemtemplate}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> <Line Grid.RowSpan="2" Stroke="Black" StrokeThickness="2" X1="0" Y1="0" X2="0" Y2="{Binding ElementName=thisWindow, Path=ActualHeight}"/> <Line Grid.Row="1" Grid.ColumnSpan="2" Stroke="Black" StrokeThickness="2" X1="0" Y1="0" X2="{Binding ElementName=thisWindow, Path=ActualWidth}" Y2="0"/> </Grid> </Window>
The code for the PercentToHeightConverter
class is as follows:
using System; using System.Windows.Data; using System.Globalization; namespace Recipe_05_12 { [ValueConversion(typeof (double), typeof (double))] public class PercentToHeightConverter : IValueConverter { // Converts a Percent value to a new height value. // The data binding engine calls this method when // it propagates a value from the binding source to the binding target. public Object Convert( Object value, Type targetType, Object parameter, CultureInfo culture)
{ double percent = System.Convert.ToDouble(value); // if the value is negative, invert it if(percent < 0) percent *= −1; return percent * 2; } // Converts a value. The data binding engine calls this // method when it propagates a value from the binding // target to the binding source. // As the binding is one-way, this is not implemented. public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }
The code for the PercentToFillConverter
is as follows:
using System; using System.Windows; using System.Windows.Data; using System.Windows.Media; using System.Globalization; using System.Collections.Generic; namespace Recipe_05_12 { [ValueConversion(typeof (double), typeof (Brush))] public class PercentToFillConverter : IValueConverter { // Declares a Brush to use for negative data items private static readonly Brush negativeColor = new LinearGradientBrush( new GradientStopCollection( new List<GradientStop>( new GradientStop[]
{ new GradientStop( Color.FromArgb(255, 165, 0, 0), 0), new GradientStop( Color.FromArgb(255, 132, 0, 0), 0) } )), new Point(0.5,0), new Point(0.5,1)); // Declares a Brush to use for positive data items private static readonly Brush positiveColor = new LinearGradientBrush( new GradientStopCollection( new List<GradientStop>( new GradientStop[] { new GradientStop( Color.FromArgb(255, 0, 165, 39), 1), new GradientStop( Color.FromArgb(255, 0, 132, 37), 0) } )), new Point(0.5, 0), new Point(0.5, 1)); // Converts a Percent value to a Fill value. // Returns a Brush based on whether Percent is positive or negative. // The data binding engine calls this method when // it propagates a value from the binding source to the binding target. public Object Convert( Object value, Type targetType, Object parameter, CultureInfo culture) { double percent = System.Convert.ToDouble(value); if(percent > 0) { return positiveColor; } else { return negativeColor; } }
// Converts a value. The data binding engine calls this // method when it propagates a value from the binding // target to the binding source. // As the binding is one-way, this is not implemented. public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }
Figure 5-13 shows the resulting window.
You need to change the appearance of a bound data object when its property values meet a certain condition. For example, you are binding to a list of products, and you want those items that are out of stock to be given a different visual appearance than the others.
Create a System.Windows.DataTemplate
to define the visual structure of your data object, and add a System.Windows.DataTrigger
to it. The DataTrigger
sets appearance property values on the UI elements in the DataTemplate
when the property value of the data object matches a specified condition.
Like the System.Windows.Style
and System.Windows.Controls.ControlTemplate
classes, the DataTemplate
class has a collection of triggers. A trigger applies property values or performs actions when the bound data meets a specified condition.
Create a DataTrigger
in the Triggers
collection of your DataTemplate
. A DataTrigger
has three components to configure. First, it has a Binding
property that specifies the property of the data object it should be bound to. Set this using a standard binding statement, and assign the name of the property in the binding's Path
attribute. It is this property that the trigger will be evaluating to determine whether it should be applied. Second, specify the DataTrigger's Value
attribute. This stores the value that the bound property should contain in order for the trigger to be applied. For example, suppose you want to apply a different visual appearance to out-of-stock items in a product catalog. You could create a DataTrigger
, set its Binding
property to the IsOutOfStock
property of the data object, and set its Value
property to True
.
The third component in a DataTrigger
is its Setters
property. This contains a collection of System.Windows.Setter
objects, which describe the appearance property values to apply when the bound property has the specified value. Each Setter
object specifies the UI element in the template to target, the property of that target it should set, and the value to set it to. For example, if you wanted to highlight the names of out-of-stock products in red, you would create a Setter
with a TargetName
of txtName
, a Property
of Foreground
, and a Value
of Red
.
A DataTrigger
can contain multiple Setter
objects that change the visual appearance of the DataTemplate
in different ways when the trigger's condition is met. For example, if a product is out of stock, you might want to not only highlight its name in red but also hide the Add to Shopping Basket button.
The following example demonstrates a window containing a System.Windows.Controls.ItemsControl
that displays a collection of DataItem
objects. The window contains a System. Windows.DataTemplate
in its Resources
collection, which specifies that each DataItem
should be displayed as a System.Windows.Shapes.Rectangle
. This has the effect of presenting the data items in the form of a simple bar graph.
The DataTemplate
contains a DataTrigger
. This binds to a Boolean
property on the DataItem
class called IsPositive
. When the value of this property is True
, the Setter
in the DataTrigger
changes the Fill
color of the rectangle to red.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_13.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_05_13="clr-namespace:Recipe_05_13" x:Name="thisWindow" Title="WPF Recipes 5_13" Height="240" Width="280"> <Window.Resources> <Recipe_05_13:DataItems x:Key="dataItems"/>
<!-- Declare two converter classes --> <Recipe_05_13:AmountToHeightConverter x:Key="amountToHeightConverter" /> <!-- Creates a DataTemplate that displays a colored bar --> <!-- for each DataItem. Its height is calculated by a converter.--> <DataTemplate x:Key="dataItemtemplate"> <Rectangle x:Name="rectangle" Margin="4" Width="30" VerticalAlignment="Bottom" Fill="Green" Height="{Binding Path=Amount, Converter={StaticResource amountToHeightConverter}}"/> <!-- A DataTigger that binds to the IsPositive property --> <!-- of a DataItem, and changes the color of the bar to --> <!-- red if IsPositive is False. --> <DataTemplate.Triggers> <DataTrigger Binding="{Binding Path=IsPositive}" Value="False"> <Setter TargetName="rectangle" Property="Fill" Value="Red"/> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> </Window.Resources> <Grid Margin="20"> <Grid.ColumnDefinitions> <ColumnDefinition Width="1"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition Height="1"/> </Grid.RowDefinitions>
<ItemsControl Grid.Column="1" Margin="4,0,0,4" ItemsSource="{Binding Source={StaticResource dataItems}}" ItemTemplate="{StaticResource dataItemtemplate}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> <Line Grid.RowSpan="2" Stroke="Black" StrokeThickness="2" X1="0" Y1="0" X2="0" Y2="{Binding ElementName=thisWindow, Path=ActualHeight}"/> <Line Grid.Row="1" Grid.ColumnSpan="2" Stroke="Black" StrokeThickness="2" X1="0" Y1="0" X2="{Binding ElementName=thisWindow, Path=ActualWidth}" Y2="0"/> </Grid> </Window>
Figure 5-14 shows the resulting window.
You need to select a different DataTemplate
to display a data object, based on properties of the data object or custom application logic.
Create two or more System.Windows.DataTemplate
instances that define the UI elements to display a data object. Create a class that inherits from System.Windows.Controls.DataTemplateSelector
, and override the SelectTemplate
method. Supply custom application logic to determine which DataTemplate
to return for a given data object. Assign this DataTemplateSelector
to the target element in your binding.
The DataTemplateSelector
class provides a way to choose a DataTemplate
, based on the data object and custom application logic. This allows you to define multiple templates and dynamically choose which one to apply to any given data object.
Create a class that derives from DataTemplateSelector
, and override the SelectTemplate
method. This method takes a parameter called item
, which is the instance of your data object for which the binding requires a template. If you are binding to a list of items, it will call this method once for each item in the list. You can define custom application logic to determine which DataTemplate
to return. Use the FindResource
method of System.Windows.FrameworkElememt
to locate the required template resource.
Assign your DataTemplateSelector
to an appropriate property of the binding target element, instead of assigning a single DataTemplate
to it. For example, instead of setting the ItemTemplate
property of a System.Windows.Controls.ListBox
control, set its ItemTemplateSelector
property.
Several other controls in the standard WPF control suite also expose a DataTemplateSelector
property. For example, both the System.Windows.Controls.ContentControl
and System. Windows.Controls.ContentPresenter
classes expose a DataTemplateSelector
via their ContentTemplateSelector
properties. Assigning a DataTemplateSelector
to either of these properties, or the controls that derive from them, allows you to dynamically choose different UI elements to display their content. The HeaderTemplateSelector
property of both System. Windows.Controls.HeaderedItemsControl
and System.Windows.Controls.HeaderedContentControl
allows you to dynamically select a template to use to display a header. Furthermore, when using a System.Windows.Controls.GridView
control, you can select templates for cells and column headers. The System.Windows.Controls.TabControl
control also exposes DataTemplateSelector
properties for selecting the template to use to display the content of its tabs and its selected tab.
To make your DataTemplateSelector
behave nicely in designers, such as the WPF designer for Visual Studio or Microsoft Expression Blend, return null
in the SelectTemplate
method at design time. This is because the DataTemplate
resources referenced by the FindResource
method of FrameworkElement
might not be available at design time. This results in an exception being shown when you try to open your window or control in a designer. By checking for design mode and returning null
, your controls will display themselves in the designer, albeit without applying your custom DataTemplate
instances.
The following example demonstrates a window containing a System.Windows.Controls.ListBox
control that displays a collection of TaskItem
objects. The ItemTemplateSelector
property of the ListBox
is set to a DataTemplateSelector
class called TaskItemDataTemplateSelector
. In the SelectTemplate
method of this class, there is custom logic to check the value of the Priority
property of each TaskItem
. If this value is 1, it returns a DataTemplate
called highPriorityTaskTemplate
. If not, it returns a DataTemplate
called defaultTaskTemplate
.
Both templates are defined in the window's Resources
collection. Note that the DataTemplateSelector
is also defined as a static resource in this collection.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_14.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_05_14="clr-namespace:Recipe_05_14" Title="WPF Recipes 5_14" Height="360" Width="330"> <Window.Resources> <!-- Create the TaskList data --> <Recipe_05_14:TaskList x:Key="taskList"/> <!-- Create the DataTemplateSelector --> <Recipe_05_14:TaskItemDataTemplateSelector x:Key="taskItemDataTemplateSelector"/> <!-- Default DataTemplate for tasks --> <DataTemplate x:Key="defaultTaskTemplate"> <Border Name="border" BorderBrush="LightBlue" BorderThickness="1" Padding="5" Margin="5">
<Grid> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="80" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <TextBlock Grid.Row="0" Grid.Column="0" Text="Name:"/> <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=Name}" /> <TextBlock Grid.Row="1" Grid.Column="0" Text="Description:"/> <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Path=Description}"/> </Grid> </Border> </DataTemplate> <!-- DataTemplate for high priority tasks --> <DataTemplate x:Key="highPriorityTaskTemplate"> <Border Name="border" BorderBrush="Red" BorderThickness="2" Margin="5"> <DockPanel Margin="4" HorizontalAlignment="Center"> <TextBlock FontSize="18" Text="{Binding Path=Description}" /> <Image Margin="20,4,4,4" Height="55" Width="39" Source="Exclamation.png"/> </DockPanel> </Border> </DataTemplate>
</Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height="24"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <TextBlock Margin="4" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="14" Text="Task List:"/> <!-- Bind the ListBox to the data --> <!-- and assign the DataTemplateSelector --> <ListBox Margin="10" Grid.Row="1" HorizontalContentAlignment="Stretch" ItemsSource="{Binding Source={StaticResource taskList}}" ItemTemplateSelector="{StaticResource taskItemDataTemplateSelector}"/> </Grid> </Window>
The code for the DataTemplateSelector
is as follows:
using System.Windows; using System.Windows.Controls; using Recipe_05_14; namespace Recipe_05_14 { public class TaskItemDataTemplateSelector : DataTemplateSelector { // Override the SelectTemplate method to // return the desired DataTemplate. // public override DataTemplate SelectTemplate( object item, DependencyObject container)
{ if (item != null && item is TaskItem) { TaskItem taskitem = item as TaskItem; Window window = Application.Current.MainWindow; // To run in design mode test for design mode, and // return null, as it will not find the DataTemplate resources // in the following code. if (System.ComponentModel.DesignerProperties.GetIsInDesignMode( window)) return null; // Check the Priority of the TaskItem to // determine the DataTemplate to display it. // if (taskitem.Priority == 1) { // Use the window's FindResource method to // locate the DataTemplate return window.FindResource( "highPriorityTaskTemplate") as DataTemplate; } else { return window.FindResource( "defaultTaskTemplate") as DataTemplate; } } return null; } } }
Figure 5-15 shows the resulting window.
You need to validate user input in a bound UI element, reject invalid data, and give feedback to the user as to why the input is invalid.
Create a class that derives from System.Windows.Controls.ValidationRule
, and specify custom validation logic. Add it to the ValidationRules
collection property of a System.Windows.Data.Binding
. Optionally, create a custom control template to override the default Validation
. ErrorTemplate
, and change the appearance of a UI element when its data is invalid.
The ValidationRules
collection of a Binding
provides a way to check the data passed into a binding and mark it as invalid if it fails any of the rules. The ValidationRule
class has a method called Validate
, which can be overridden to provide the custom validation logic. The Validate
method takes the data passed into the binding as a parameter and returns an instance of the System.Windows.Controls.ValidationResult
class. The IsValid
property of the ValidationResult
specifies whether the rule has passed or failed. Set this property to False
if the data should be marked as invalid. The ValidationResult
class also has an ErrorContent
property, which can be used to inform the user as to why the data is invalid. In the following example, this property is used to set an error message that is then displayed in a System.Windows.FrameworkElement.ToolTip
control.
In the XAML for a UI element, declare a Binding
object as a nested element, instead of as an inline attribute. This allows you to add rules to the Binding's ValidationRules
property.
By default, when a binding for a UI element is invalid, its control template is altered so that it is displayed with a thin red border around it. However, you can create a custom System. Windows.Style
that assigns a different control template to the Validation.ErrorTemplate
attached property on the target element. This allows you to give a control a more sophisticated appearance when its data is invalid. It also allows you to give feedback to the user by exposing the ErrorContent
value of the ValidationResult
.
The following example demonstrates a window containing a System.Windows.Controls.Slider
control and a System.Windows.Controls.TextBlock
control. The XAML statement for the Text
property of the TextBlock
specifies a Binding
statement that binds it to the Value
property of the Slider
control. In the binding statement, the Mode
attribute is set to TwoWay
and the UpdateSourceTrigger
attribute to PropertyChanged
. This ensures that when a number from 1 to 100 is typed into the TextBox
, the Slider
control immediately updates its value to reflect it.
In the Binding
declaration, a local ValidationRule
called PercentageRule
is added to the ValidationRules
collection. This class checks that the value entered into the TextBox
is a number between 0 and 100. If it is not, the ValidationRule
returns a ValidationResult
with the IsValid
property set to False
and an ErrorContent
that states "Must be a number between 0 and 100."
The TextBox
is assigned a Style
called textBoxInErrorStyle
, which is declared in the window's Resources
collection. This Style
does two things. First, it ensures that when the attached property Validation.HasError
is set to True
, it assigns the value of the ErrorContent
to the ToolTip
property of the TextBox
. Second, it assigns a new control template to the Validation
. ErrorTemplate
property. This ensures that when the TextBox
is invalid, an error icon is shown to the right of it, and if the user hovers over this icon with the mouse, the error description is displayed in its ToolTip
.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_15.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_05_15="clr-namespace:Recipe_05_15" Title="WPF Recipes 5_15" Height="100" Width="260"> <Window.Resources> <!-- A TextBox style to replace the default ErrorTemplate. --> <-- When the validation rule fails, an error icon is --> <-- shown next to the TextBox, and the error message is --> <-- displayed in the ToolTip. -->
<Style x:Key="textBoxInErrorStyle" TargetType="{x:Type TextBox}" > <Style.Triggers> <!-- A Property Trigger that sets the value of the --> <!-- Tooltip to the error message, when the binding --> <!-- has a validation error. --> <Trigger Property="Validation.HasError" Value="true"> <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/> </Trigger> </Style.Triggers> <!-- A Property Setter that sets the ErrorTemplate to --> <!-- display an error icon to the right of the TextBox. --> <Setter Property="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <DockPanel DockPanel.Dock="Right"> <AdornedElementPlaceholder/> <Image Source="Error.png" Width="16" Height="16" ToolTip="{Binding Path=AdornedElement.ToolTip, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Adorner}}}"/> </DockPanel> </ControlTemplate> </Setter.Value> </Setter> </Style> </Window.Resources> <StackPanel>
<!-- A Slider control that displays a value from 0 to 100 --> <Slider Name="slider" Margin="4" Interval="1" TickFrequency="1" IsSnapToTickEnabled="True" Minimum="0" Maximum="100"/> <StackPanel Orientation="Horizontal" > <TextBlock Width="Auto" Margin="4" HorizontalAlignment="Left" VerticalAlignment="Center" Text="Gets and sets the value of the slider:" /> <!-- A TextBox with a two-way binding between its Text property --> <!-- and the Slider control's Value property. The --> <!-- textBoxInErrorStyle resource is assigned as its Style property. --> <TextBox Width="40" Margin="4" Style="{StaticResource textBoxInErrorStyle}" HorizontalAlignment="Center" > <TextBox.Text> <Binding ElementName="slider" Path="Value" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" > <!-- Adds a ValidationRule, specifiying --> <!-- the local PercentageRule class. --> <Binding.ValidationRules> <Recipe_05_15:PercentageRule/> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> </StackPanel> </StackPanel> </Window>
The code for the PercentageRule class is as follows:
using System.Globalization; using System.Windows.Controls;
namespace Recipe_05_15 { /// <summary> /// ValidationRule class to validate that a value is a /// number from 0 to 100. /// </summary> public class PercentageRule : ValidationRule { // Override the Validate method to add custom validation logic // public override ValidationResult Validate( object value, CultureInfo cultureInfo) { string stringValue = value as string; // Check whether there is a value if(!string.IsNullOrEmpty(stringValue)) { // Check whether the value can be converted to a double double doubleValue; if(double.TryParse(stringValue, out doubleValue)) { // Check whether the double is between 0 and 100 if(doubleValue >= 0 && doubleValue <= 100) { // Return a ValidationResult with the IsValid // property set to True return new ValidationResult(true, null); } } } // Return a ValidationResult with the IsValid // property set to False. Also specify an error message, // which will be displayed in the ToolTip. return new ValidationResult( false, "Must be a number between 0 and 100"); } } }
Figure 5-16 shows the resulting window.
You need to bind to a data object that implements the System.ComponentModel.IDataErrorInfo
interface and display its error messages when the object is in an invalid state.
Create a class that implements IDataErrorInfo
, and specify custom validation logic that returns error messages when a property value is invalid. Set the ValidatesOnErrors
property of a System.Windows.Data.Binding
to True
. Optionally, create a custom control template to override the default Validation.ErrorTemplate
, and change the appearance of a UI element when its data is invalid.
The IDataErrorInfo
interface is the standard construct in .NET for supporting the validation of CLR objects. It returns error messages for properties that are in an invalid state. Typically, you would implement a business rules engine or validation framework to determine whether any property values are invalid. Alternatively, you can simply embed your validation logic in your classes, as in the following example.
To enable a binding for validation, set the ValidatesOnDataErrors
property of your Binding
object to True
. Internally, this will add a System.Windows.Controls.DataErrorValidationRule
to the Binding's ValidationRules
collection. The WPF binding system will now interrogate the data source's IDataErrorInfo
members for validation errors.
By default, when a binding for a UI element is invalid, its control template is altered so that it is displayed with a thin red border around it. However, you can create a custom System. Windows.Style
that assigns a different control template to the Validation.ErrorTemplate
attached property on the target element.
The following example demonstrates a window that displays the name and age data of a Person
object using three System.Windows.Controls.TextBox
controls. The Person
object is assigned to the DataContext
property of the window in its constructor. The Person
class implements the IDataErrorInfo
and contains custom validation logic to check that its properties have valid values. In the binding statements for the controls, the ValidatesOnErrors
property is set to True
. A custom System.Windows.Style
resource is also assigned to the TextBox
. This ensures that when the data in the TextBox
is invalid, an error icon is displayed to its right, and the ToolTip
property displays the error message.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_16.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 5_16" Height="116" Width="260"> <Window.Resources> <!-- A TextBox style to replace the default ErrorTemplate. --> <!-- When the validation rule fails, an error icon is --> <!-- shown next to the TextBox, and the error message is --> <!-- displayed in the ToolTip. --> <Style x:Key="textBoxInErrorStyle" TargetType="{x:Type TextBox}" > <Style.Triggers> <!-- A Property Trigger that sets the value of the --> <!-- Tooltip to the error message, when the binding --> <!-- has a validation error. --> <Trigger Property="Validation.HasError" Value="true"> <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/> </Trigger> </Style.Triggers> <!-- A Property Setter that sets the ErrorTemplate to --> <!-- display an error icon to the right of the TextBox. --> <Setter Property="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <DockPanel DockPanel.Dock="Right">
<AdornedElementPlaceholder/> <Image Source="Error.png" Width="16" Height="16" ToolTip="{Binding Path=AdornedElement.ToolTip, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Adorner}}}"/> </DockPanel> </ControlTemplate> </Setter.Value> </Setter> </Style> </Window.Resources> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="74"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="14"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="26"/> <RowDefinition Height="26"/> <RowDefinition Height="26"/> </Grid.RowDefinitions> <TextBlock Margin="4" Text="First Name" VerticalAlignment="Center"/> <TextBox Style="{StaticResource textBoxInErrorStyle}" Text="{Binding Path=FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" Margin="4" Grid.Column="1"/> <TextBlock Margin="4" Text="Last Name"
Grid.Row="1" VerticalAlignment="Center"/> <TextBox Margin="4" Style="{StaticResource textBoxInErrorStyle}" Text="{Binding Path=LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" Grid.Column="1" Grid.Row="1"/> <TextBlock Margin="4" Text="Age" Grid.Row="2" VerticalAlignment="Center"/> <TextBox Style="{StaticResource textBoxInErrorStyle}" Margin="4" Text="{Binding Path=Age, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" Grid.Column="1" Grid.Row="2"/> </Grid> </Window>
The code-behind for the window is as follows:
using System.Windows; using Recipe_05_16; namespace Recipe_05_16 { public partial class Window1 : Window { public Window1() { InitializeComponent(); // Set the DataContext to a Person object this.DataContext = new Person()
{ FirstName = "Elin", LastName = "Binkles", Age = 26, }; } } }
The code for the Person
class is as follows:
using System.ComponentModel; namespace Recipe_05_16 { public class Person : INotifyPropertyChanged, IDataErrorInfo { private string firstName; private string lastName; private int age; public Person() { FirstName = "spod"; } public string FirstName { get { return firstName; } set { if(firstName != value) { firstName = value; OnPropertyChanged("FirstName"); } } } public string LastName { get
{ return lastName; } set { if(this.lastName != value) { this.lastName = value; OnPropertyChanged("LastName"); } } } public int Age { get { return age; } set { if(this.age != value) { this.age = value; OnPropertyChanged("Age"); } } } #region INotifyPropertyChanged Members /// Implement INotifyPropertyChanged to notify the binding /// targets when the values of properties change. public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged( string propertyName) { if(this.PropertyChanged != null) { // Raise the PropertyChanged event this.PropertyChanged( this, new PropertyChangedEventArgs( propertyName)); } }
#endregion #region IDataErrorInfo Members // Implement IDataErrorInfo to return custom // error messages when a property value // is invalid. public string Error { get { return string.Empty; } } public string this[string propertyName] { get { // Return an empty string if there are no errors string message = string.Empty; switch(propertyName) { case "FirstName": if(string.IsNullOrEmpty(firstName)) message = "A person must have a first name."; break; case "LastName": if(string.IsNullOrEmpty(lastName)) message = "A person must have a last name."; break; case "Age": if(age < 1) message = "A person must have an age."; break; case "Occupation": if(string.IsNullOrEmpty(firstName)) message = "A person must have an occupation."; break; default: break; }
return message; } } #endregion } }
Figure 5-17 shows the resulting window. If you delete the values in any of the TextBox
controls, the error icon is displayed, and the tool tip shows the error message.
You need to bind to the items in a data collection and display more information about the selected item. For example, you might display a list of product names and prices on one side of the screen and a more detailed view of the selected product on the other side.
Bind a data collection to the ItemsSource
property of a System.Windows.Controls.ItemsControl
such as a System.Windows.Controls.ListBox, System.Windows.Controls.ListView
, or System.Windows.Controls.TreeView
. Implement the System.Collections.Specialized.INotifyCollectionChanged
on the data collection to ensure that insertions or deletions in the collection update the UI automatically. Implement the master-detail pattern by binding a System.Windows.Controls.ContentControl
to the same collection.
To bind an ItemsControl
to a collection object, set its ItemsSource
property to an instance of a collection class. This is a class that implements the System.Collections.IEnumerable
interface, such as System.Collections.Generic.List<T>
or System.Collections.ObjectModel.Collection<T>
, or the System.Collections.IList
and System.Collections.ICollection
interfaces. However, if you bind to any of these objects, the binding will be one-way and read-only. To set up dynamic bindings so that insertions or deletions in the collection update the UI automatically, the collection must implement the System.Collections.Specialized.INotifyCollectionChanged
interface. This interface provides the mechanism for notifying the binding target of changes to the source collection, in much the same way as the System.ComponentModel.INotifyPropertyChanged
interface notifies bindings of changes to properties in single objects.
INotifyCollectionChanged
exposes an event called CollectionChanged
that should be raised whenever the underlying collection changes. When you raise this event, you pass in an instance of the System.Collections.Specialized.NotifyCollectionChangedEventArgs
class. This contains properties that specify the action that caused the event, for example, whether items were added, moved, or removed from the collection and the list of affected items. The binding mechanism listens for these events and updates the target UI element accordingly.
You do not need to implement INotifyCollectionChanged
on your own collection classes. WPF provides the System.Collections.ObjectModel.ObservableCollection<T>
class, which is a built-in implementation of a data collection that exposes INotifyCollectionChanged
. If your collection classes are instances of the ObservableCollection<T>
class or they inherit from it, you will get two-way dynamic data binding for free.
To fully support transferring data values from source objects to targets, each object in your collection that supports bindable properties must also implement the INotifyPropertyChanged
interface. It is common practice to create a base class for all your custom business objects that implements INotifyPropertyChanged
and a base collection class for collections of these objects that inherits from ObservableCollection<T>
. This automatically enables all your custom objects and collection classes for data binding.
To implement the master-detail scenario of binding to a collection, you simply need to bind two or more controls to the same System.Windows.Data.CollectionView
object. A CollectionView
represents a wrapper around a binding source collection that allows you to navigate, sort, filter, and group the collection, without having to manipulate the underlying source collection itself. When you bind to any class that implements IEnumerable
, the WPF binding engine creates a default CollectionView
object automatically behind the scenes. So if you bind two or more controls to the same ObservableCollection<T>
object, you are in effect binding them to the same default CollectionView
class. If you want to implement custom sorting, grouping, and filtering of your collection, you will need to define a CollectionView
explicitly yourself. You do this by creating a System.Windows.Data.CollectionViewSource
class in your XAML. This approach is demonstrated in the next few recipes in this chapter. However, for the purpose of implementing the master-detail pattern, you can simply bind directly to an ObservableCollection<T>
and accept the default CollectionView
behind the scenes.
To display the master aspect of the pattern, simply bind your collection to the ItemsSource
property of an ItemsControl
, such as a System.Windows.Controls.ListBox, System.Windows. Controls.ListView
, or System.Windows.Controls.TreeView
. If you do not specify a DataTemplate
for the ItemTemplate
property of the ItemsControl
, you can use the DisplayMemberPath
property to specify the name of the property the ItemsControl
should display. If you do not support a value for DisplayMemberPath
, it will display the value returned by the ToString
method of each data item in the collection.
To display the detail aspect of the pattern for the selected item, simply bind a singleton object to the collection, such as a ContentControl
. When a singleton object is bound to a CollectionView
, it automatically binds to the CurrentItem
of the view.
If you are explicitly creating a CollectionView
using a CollectionViewSource
object, it will automatically synchronize currency and selection between the binding source and targets. However, if you are bound directly to an ObservableCollection<T>
or other such IEnumerable
object, then you will need to set the IsSynchronizedWithCurrentItem
property of your ListBox
to True
for this to work. Setting the IsSynchronizedWithCurrentItem
property to True
ensures that the item selected always corresponds to the CurrentItem
property in the ItemCollection
. For example, suppose there are two ListBox
controls with their ItemsSource
property bound to the same ObservableCollection<T>
. If you set IsSynchronizedWithCurrentItem
to True
on both ListBox
controls, the selected item in each is the same.
The following example demonstrates a window that data binds to an instance of the PersonCollection
class in its constructor. The PersonCollection
class is an ObservableCollection<T>
of Person
objects. Each Person
object exposes name, age, and occupation data, as well as a description.
In the top half of the window, a ListBox
is bound to the window's DataContext
. This is assigned an instance of the PersonCollection
in the code-behind for the window. The ItemTemplate
property of the ListBox
references a DataTemplate
called masterTemplate
defined in the window's Resources
collection. This shows the value of the Description
property for each Person
object in the collection. It sets the UpdateSourceTrigger
attribute to System.Windows.Data. UpdateSourceTrigger.PropertyChanged
. This ensures that the text in the ListBox
item is updated automatically and immediately when the Description
property of a Person
changes. In the bottom half of the window, a ContentControl
binds to the same collection. Because it is a singleton UI element and does not display a collection of items, it automatically binds to the current item in the PersonCollection
class. Because the IsSynchronizedWithCurrentItem
property of the ListBox
is set to True
, this corresponds to the selected item in the ListBox
. The ContentControl
uses a DataTemplate
called detailTemplate
to display the full details of the selected Person
.
When the data displayed in the details section is changed, it automatically updates the corresponding description in the master section above it. This is made possible for two reasons. First, the System.Windows.Controls.TextBox
controls in the details section specify a System. Windows.Data.Binding.BindingMode
of TwoWay
, which means that when new text is input, it is automatically marshaled to the binding source. Second, the Person
class implements the INotifyPropertyChanged
interface. This means that when a value of a property changes, the binding target is automatically notified.
At the bottom of the window, there is a System.Windows.Controls.Button
control marked Add Person. When this button is clicked, it adds a new Person
object to the collection. Because the PersonCollection
class derives from ObservableCollection<T>
, which in turn implements INotifyCollectionChanged
, the master list of items automatically updates to show the new item.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_17.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 5_17" Height="370" Width="280"> <Window.Resources> <DataTemplate x:Key="masterTemplate"> <TextBlock Margin="4" Text="{Binding Path=Description, UpdateSourceTrigger=PropertyChanged}"/> </DataTemplate> <DataTemplate x:Key="detailTemplate"> <Border BorderBrush="LightBlue" BorderThickness="1"> <Grid Margin="10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="74"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="26"/> <RowDefinition Height="26"/> <RowDefinition Height="26"/> <RowDefinition Height="26"/> </Grid.RowDefinitions> <TextBlock Margin="4" Text="First Name" VerticalAlignment="Center"/> <TextBox Text="{Binding Path=FirstName, Mode=TwoWay}" Margin="4" Grid.Column="1"/> <TextBlock Margin="4" Text="Last Name" Grid.Row="1" VerticalAlignment="Center"/>
<TextBox Margin="4" Text="{Binding Path=LastName, Mode=TwoWay}" Grid.Column="1" Grid.Row="1"/> <TextBlock Margin="4" Text="Age" Grid.Row="2" VerticalAlignment="Center"/> <TextBox Margin="4" Text="{Binding Path=Age, Mode=TwoWay}" Grid.Column="1" Grid.Row="2"/> <TextBlock Margin="4" Text="Occupation" Grid.Row="3" VerticalAlignment="Center"/> <ComboBox x:Name="cboOccupation" IsEditable="False" Grid.Column="1" Grid.Row="3" HorizontalAlignment="Left" Text="{Binding Path=Occupation, Mode=TwoWay}" Margin="4" Width="140"> <ComboBoxItem>Student</ComboBoxItem> <ComboBoxItem>Engineer</ComboBoxItem> <ComboBoxItem>Professional</ComboBoxItem> </ComboBox> </Grid> </Border> </DataTemplate> </Window.Resources> <StackPanel Margin="5"> <TextBlock VerticalAlignment="Center" FontSize="14" Margin="4" Text="People"/>
<!-- The ItemsControls binds to the collection. --> <ListBox ItemsSource="{Binding}" ItemTemplate="{StaticResource masterTemplate}" IsSynchronizedWithCurrentItem="True" /> <TextBlock VerticalAlignment="Center" FontSize="14" Margin="4" Text="Details"/> <!-- The ContentControl binds to the CurrentItem of the collection. --> <ContentControl Content="{Binding}" ContentTemplate="{StaticResource detailTemplate}" /> <!-- Add a new person to the collection. --> <Button Margin="4" Width="100" Height="34" HorizontalAlignment="Right" Click="AddButton_Click"> Add Person </Button> </StackPanel> </Window>
The code-behind for the window is as follows:
using System.Windows; using Recipe_05_17; namespace Recipe_05_17 { public partial class Window1 : Window { // Create an instance of the PersonCollection class PersonCollection people = new PersonCollection(); public Window1() { InitializeComponent(); // Set the DataContext to the PersonCollection this.DataContext = people; }
private void AddButton_Click( object sender, RoutedEventArgs e) { people.Add(new Person() { FirstName = "Nelly", LastName = "Bonks", Age = 26, Occupation = "Professional" }); } } }
The code for the Person
class is omitted for brevity. It is identical to the Person
class used in recipe 5-4, so you can see the full code in that recipe. The code for the PersonCollection
class is as follows:
using System.Collections.ObjectModel; using Recipe_05_17; namespace Recipe_05_17 { public class PersonCollection : ObservableCollection<Person> { public PersonCollection() { // Load the collection with dummy data // Add(new Person(){FirstName = "Elin", LastName = "Binkles", Age = 26, Occupation = "Professional"}); Add(new Person(){FirstName = "Samuel", LastName = "Bourts", Age = 28, Occupation = "Engineer"}); Add(new Person(){FirstName = "Alan", LastName = "Jonesy", Age = 37, Occupation = "Engineer"});
Add(new Person(){FirstName = "Sam", LastName = "Nobles", Age = 25, Occupation = "Engineer"}); } } }
Figure 5-18 shows the resulting window.
Create a System.Windows.Data.CollectionViewSource
as a static resource, and bind it to the data collection. Specify a System.ComponentModel.SortDescription
using the name of the property you want to sort on.
A CollectionViewSource
is a layer on top of the binding source collection that allows you to expose a custom System.Windows.Data.CollectionView
class for your data. A CollectionView
represents a view of the items in a data collection and can supply custom grouping, sorting, filtering, and navigation.
To specify how the items in the collection view are sorted, create a System.ComponentModel.SortDescription
object, and add it to the CollectionViewSource's SortDescriptions
collection. A SortDescription
defines the direction and the property name to be used as the criteria for sorting the data.
The following example creates a CollectionViewSource
as a static resource that binds to a collection of countries. The CollectionViewSource
has a SortDescription
property that sorts the data according to each item's Name
property. A System.Windows.Controls.ItemsControl
binds to the CollectionViewSource
and shows the sorted collection.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_18.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ComponentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase" xmlns:local="clr-namespace:Recipe_05_18" Title="WPF Recipes 5_18" Height="244" Width="124"> <Window.Resources> <!-- Create an instance of the collection class --> <local:Countries x:Key="countries"/> <!-- Wrap it in a CollectionViewSource --> <CollectionViewSource x:Key="cvs" Source="{Binding Source={StaticResource countries}}"> <!-- Add a SortDescription to sort by the Name --> <CollectionViewSource.SortDescriptions> <ComponentModel:SortDescription PropertyName="Name" /> </CollectionViewSource.SortDescriptions> </CollectionViewSource> </Window.Resources> <Grid> <!-- Bind an ItemsControl to the CollectionViewSource --> <!-- Set its DisplayMemberPath to display the Name property -->
<ItemsControl ItemsSource="{Binding Source={StaticResource cvs}}" DisplayMemberPath="Name" /> </Grid> </Window>
The code for the data collection and data object is omitted for brevity. Figure 5-19 shows the resulting window.
Create a custom class that implements the System.Collections.IComparer
interface. Add the custom sorting logic to the Compare
method to sort the collection of data items based on your custom sort criteria. Use the static GetDefaultView
method of the System.Windows.Data.CollectionViewSource
class to get the default view your collection. Set the CustomSort
property of this view to an instance of your IComparer
class.
When you bind to a collection class, the WPF binding system creates a default System.Windows. Data.CollectionView
behind the scenes. Internally, this wraps your collection and exposes it as a binding source. There is a static method on the CollectionViewSource
class called GetDefaultView
. This gets the default collection view from your collection. This will be an instance of the System. Windows.Data.ListCollectionView
class if your data collection is a System.Collections.IList
.
Once you have your ListCollectionView
object, you can set its CustomSort
property to a class that implements IComparer
. This interface exposes a method that compares two objects. Add custom logic to this method to sort your data collection.
The following example demonstrates a window that creates a System.Collections.ObjectModel. ObservableCollection<T>
of strings called SortableCountries
as a static resource. The collection contains names of countries prefixed by a number and is displayed in a System.Windows. Controls.ItemsControl
. Using the normal SortDescription
property of a CollectionViewSource
to sort the countries would result in all those beginning with a 1 being before the others. For example, "14 USA" would be above "4 China." In the code-behind for the window, there is an implementation of IComparer
called SortCountries
. When the System.Windows.Controls.Button
control marked Sort is clicked, there is code in the event handler to get the default view from the collection and set an instance of this SortCountries
class to the CustomSort
property.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_19.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Recipe_05_19" Title="WPF Recipes 5_19" Height="300" Width="180"> <Window.Resources> <!-- Create an instance of the collection class --> <local:SortableCountries x:Key="sortableCountries"/> </Window.Resources> <Grid Margin="16"> <StackPanel> <ItemsControl ItemsSource="{StaticResource sortableCountries}" /> <Button Click="SortButton_Click" Content="Sort" Margin="8" /> </StackPanel> </Grid> </Window>
In the SortCountries
implementation of IComparer
, there is custom logic to sort the numeric prefixes as numbers, not as strings. The full code for this comparison logic is omitted for brevity. However, you can find the code to apply the IComparer
in the following code-behind for the window:
using System; using System.Collections; using System.Windows; using System.Windows.Data; using Recipe_05_19; namespace Recipe_05_19 { public partial class Window1 : Window { public Window1() { InitializeComponent(); } private void SortButton_Click( object sender, RoutedEventArgs args) { // Get the ObservableCollection from the window Resources SortableCountries sortableCountries = (SortableCountries) (this.Resources["sortableCountries"]); // Get the Default View from the ObservableCollection ListCollectionView lcv = (ListCollectionView) CollectionViewSource.GetDefaultView(sortableCountries); // Set the Custom Sort class lcv.CustomSort = new SortCountries(); } } public class SortCountries : IComparer { public int Compare(object x, object y) { // Custom sorting logic goes here. // (Omitted for brevity). // string stringX = x.ToString(); string stringY = y.ToString();
int ret = 0; // [...] return ret; } } }
Figure 5-20 shows the difference between the two lists, before and after the custom sorting logic is applied.
Create a System.Windows.Data.CollectionViewSource
as a static resource, and bind it to the data collection. Set the Filter
property of the CollectionViewSource
to a System.Windows. Data.FilterEventHandler
. In the code for this event handler, add custom logic to determine which items in the collection should be displayed.
A CollectionViewSource
wraps a binding source collection and allows you to expose a custom view of its data based on sort, filter, and group queries. When a FilterEventHandler
is assigned to its Filter
property, the event handler is called for each item in the collection. The event handler takes an instance of the System.Windows.Data.FilterEventArgs
class as its event argument. If a data item should be included in the collection view, set the Accepted
property of the FilterEventArgs to True. If it should not pass through the filter, simply set the Accepted
property to False
.
The following example creates a CollectionViewSource
as a static resource that binds to a collection of countries. The CollectionViewSource
has a Filter
property that references an EventHandler
called CollectionViewSource_EuropeFilter
in the code-behind for the window. This event handler filters out countries in the collection that are not in Europe.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_20.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Recipe_05_20" Title="WPF Recipes 5_20" Height="124" Width="124"> <Window.Resources> <!-- Create an instance of the collection class --> <local:Countries x:Key="countries"/> <!-- Wrap it in a CollectionViewSource --> <!-- Set the Filter property to a FilterEventHandler --> <CollectionViewSource x:Key="cvs" Source="{Binding Source={StaticResource countries}}" Filter="CollectionViewSource_EuropeFilter" /> </Window.Resources> <Grid> <!-- Bind an ItemsControl to the CollectionViewSource --> <!-- Set its DisplayMemberPath to display the Name property --> <ItemsControl ItemsSource="{Binding Source={StaticResource cvs}}" DisplayMemberPath="Name"/> </Grid>
</Window>
The code-behind for the window is as follows:
using System.Windows; using System.Windows.Data; using Recipe_05_20; namespace Recipe_05_20 { public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Filter the collection of countries. private void CollectionViewSource_EuropeFilter( object sender, FilterEventArgs e) { // Get the data item Country country = e.Item as Country; // Accept it into the collection view, if its // Continent property equals Europe. e.Accepted = (country.Continent == Continent.Europe); } } }
The data for the collection and its data items is as follows:
using System.Collections.ObjectModel; namespace Recipe_05_20 { public class Country { private string name; private Continent continent; public string Name { get{ return name;} set{name = value;} }
public Continent Continent { get{return continent;} set{continent = value;} } public Country(string name, Continent continent) { this.name = name; this.continent = continent; } } public enum Continent { Asia, Africa, Europe, NorthAmerica, SouthAmerica, Australasia } public class Countries : Collection<Country> { public Countries() { this.Add(new Country("Great Britan", Continent.Europe)); this.Add(new Country("USA", Continent.NorthAmerica)); this.Add(new Country("Canada", Continent.NorthAmerica)); this.Add(new Country("France", Continent.Europe)); this.Add(new Country("Germany", Continent.Europe)); this.Add(new Country("Italy", Continent.Europe)); this.Add(new Country("Spain", Continent.Europe)); this.Add(new Country("Brazil", Continent.SouthAmerica)); this.Add(new Country("Argentina", Continent.SouthAmerica)); this.Add(new Country("China", Continent.Asia)); this.Add(new Country("India", Continent.Asia)); this.Add(new Country("Japan", Continent.Asia)); this.Add(new Country("South Africa", Continent.Africa)); this.Add(new Country("Tunisia", Continent.Africa)); this.Add(new Country("Egypt", Continent.Africa)); } } }
Figure 5-21 shows the resulting window.
Use a System.Windows.Data.CollectionViewSource
to wrap a collection and group its items, and create a System.Windows.Controls.GroupStyle
to control how the group headers are displayed.
A CollectionViewSource
is a layer on top of the binding source collection that allows you to expose a custom view of the collection based on sort, filter, and group queries.
Create the CollectionViewSource
as a static resource in the System.Windows.ResourceDictionary
for your window, and bind it to the collection you want to group. Add a System.Windows.Data.PropertyGroupDescription
to the GroupDescriptions
collection property of the CollectionViewSource
, and specify the name of the property you want to group the items by. Use the GroupStyle
property of the System.Windows.Controls.ItemsControl
to specify a HeaderTemplate
to use for the group headers.
The following example creates a CollectionViewSource
as a static resource that binds to a collection of countries. The Country
class has two properties, Name
and Continent
, and the CollectionViewSource
uses a PropertyGroupDescription
to group the countries according to the value of their Continent
property. A System.Windows.Controls.ItemsControl
binds to the CollectionViewSource and shows the grouped collection. It declares a GroupStyle
that references a System.Windows.DataTemplate
called groupingHeaderTemplate
. This DataTemplate
defines the display style for group headers.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_21.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_05_21="clr-namespace:Recipe_05_21" Title="WPF Recipes 5_21" Height="294" Width="160"> <Window.Resources> <!-- Create an instance of the collection class --> <Recipe_05_21:Countries x:Key="countries"/> <!-- Wrap it in a CollectionViewSource --> <CollectionViewSource x:Key="cvs" Source="{Binding Source={StaticResource countries}}"> <!-- Add a PropertyGroupDescription to group by the Continent --> <CollectionViewSource.GroupDescriptions> <PropertyGroupDescription PropertyName="Continent"/> </CollectionViewSource.GroupDescriptions> </CollectionViewSource> <!-- DataTemplate to display the group header --> <DataTemplate x:Key="groupingHeaderTemplate"> <Border Height="28"> <Label VerticalAlignment="Center" Content="{Binding}" BorderBrush="#FF8F8D8D" BorderThickness="0,0,0,0.5" Foreground="#FF666666"> <Label.Background> <LinearGradientBrush EndPoint="0.506,-0.143" StartPoint="0.502,11.643"> <GradientStop Color="#FF000000" Offset="0"/> <GradientStop Color="#FFFFFFFF" Offset="1"/> </LinearGradientBrush> </Label.Background> </Label> </Border> </DataTemplate> </Window.Resources> <Grid> <!-- Bind an ItemsControl to the CollectionViewSource --> <!-- Set its DisplayMemberPath to display the Name property -->
<ItemsControl ItemsSource="{Binding Source={StaticResource cvs}}" DisplayMemberPath="Name"> <!-- Create a GroupStyle that uses the DataTemplate --> <ItemsControl.GroupStyle> <GroupStyle HeaderTemplate= "{StaticResource groupingHeaderTemplate}" /> </ItemsControl.GroupStyle> </ItemsControl> </Grid> </Window>
Figure 5-22 shows the resulting window.
Create a System.Windows.Data.CollectionViewSource
as a static resource, and bind it to the collection. Create a class that implements the System.Windows.Data.IValueConverter
interface and contains the custom grouping logic. Declare the IValueConverter
implementation as a static resource. Add a PropertyGroupDescription
to the GroupDescriptions
collection property of the CollectionViewSource
, and specify the Converter
property. Use the GroupStyle
property of the System.Windows.Controls.ItemsControl
to specify the default GroupStyle
.
When the CollectionViewSource
is bound to the collection, the IValueConverter.Convert
method is invoked for each item in the collection. This contains custom logic in the code-behind for deciding to which group each item belongs.
The following example creates a CollectionViewSource
as a static resource that binds to a collection of countries. It also declares an IValueConverter
class as a static resource, which is defined in the code-behind for the window and which contains the code to divide the countries into two groups. The resulting grouped data collection is displayed in an ItemsControl
.
If you don't create a custom DataTemplate
to define the display of your groups' headers, you have to specify the default GroupStyle
. This indents the items in a group. For more sophisticated visualizations, create a DataTemplate
to display a group header, and specify it as the HeaderTemplate
property of your ItemsControl
's GroupStyle
.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_22.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_05_22="clr-namespace:Recipe_05_22" Title="WPF Recipes 5_22" Height="294" Width="160"> <Window.Resources> <!-- Create an instance of the collection class --> <Recipe_05_22:Countries x:Key="countries"/> <!-- Create an instance of the GroupByContinentConverter class --> <Recipe_05_22:GroupByContinentConverter x:Key="GroupByContinentConverter"/> <!-- Wrap the collection in a CollectionViewSource --> <!-- Set the Filter property to a FilterEventHandler --> <CollectionViewSource x:Key="cvs" Source="{Binding Source={StaticResource countries}}">
<!-- Add a PropertyGroupDescription that uses --> <!-- the GroupByContinentConverter class to create the groups --> <CollectionViewSource.GroupDescriptions> <PropertyGroupDescription Converter="{StaticResource GroupByContinentConverter}" /> </CollectionViewSource.GroupDescriptions> </CollectionViewSource> </Window.Resources> <Grid> <!-- Bind an ItemsControl to the CollectionViewSource. --> <!-- Set its DisplayMemberPath to display the Name property. --> <!-- Set the GroupStyle to use the Default. --> <ItemsControl Margin="10" ItemsSource="{Binding Source={StaticResource cvs}}" DisplayMemberPath="Name" > <!-- The default GroupStyle indents the items in a group --> <ItemsControl.GroupStyle> <x:Static Member="GroupStyle.Default"/> </ItemsControl.GroupStyle> </ItemsControl> </Grid> </Window>
The code in the IValueConverter
checks the Continent
property of each country and decides whether it should be in the "Americas" or the "Rest of the World" group:
using System; using System.Globalization; using System.Windows.Data; using Recipe_05_22; namespace Recipe_05_22 { public class GroupByContinentConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { Country country = (Country)value;
// Decide which group the country belongs in switch (country.Continent) { case Continent.NorthAmerica: case Continent.SouthAmerica: return "Americas"; default: return "Rest of the World"; } } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }
Figure 5-23 shows the resulting window.
Reference your application's Properties.Settings.Default
class as a static binding source in your binding statements.
Visual Studio provides a handy mechanism for storing and retrieving application settings dynamically. Using the Settings page of the Project Designer, you can add custom properties and give each one a name, a type, and an initial value. Visual Studio then automatically generates a Settings
class and creates standard .NET properties for each setting. It exposes the properties via a static Settings
property called Default
.
To bind your application settings to UI elements, set the Source
property of a System.Windows.Data.Binding
to the static Properties.Settings.Default
class, and set the Path
property to the name of a setting. Set the Mode
property to System.Windows.Data.BindingMode.TwoWay
to automatically update the application setting when it is changed by your target UI element.
To save changes to your settings, override the OnClosing
method of your window, and call the Properties.Savings.Default.Save
method.
The following example demonstrates a window that displays the values of the window's Height
, Width
, Left
, and Top
properties. On the project's Settings page, there are four corresponding double properties. Figure 2-24 shows these application settings.
In the XAML for the window, the window's Height
, Width
, Left
, and Top
properties are bound to the values of these application settings. The binding statements reference the Properties
. Settings.Default
class as a static binding source. This ensures that when the window opens, it gets its initial size and position from the application settings.
In the code-behind for the window, the Settings.Default
.Save method is called in the OnClosing
method. This ensures that when you move or resize the window, these settings are saved and restored the next time the application runs.
The XAML for the window is as follows:
<Window x:Class="Recipe_05_23.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Properties="clr-namespace:Recipe_05_23.Properties" x:Name="MainWindow" WindowStartupLocation="Manual" Title="WPF Recipes 5_23" Height="{Binding Source={x:Static Properties:Settings.Default}, Path=Height, Mode=TwoWay}" Width="{Binding Source={x:Static Properties:Settings.Default}, Path=Width, Mode=TwoWay}" Left="{Binding Source={x:Static Properties:Settings.Default}, Path=Left, Mode=TwoWay}" Top="{Binding Source={x:Static Properties:Settings.Default},
Path=Top, Mode=TwoWay}" > <Grid> <Grid VerticalAlignment="Center" HorizontalAlignment="Center"> <Grid.ColumnDefinitions> <ColumnDefinition Width="100"/> <ColumnDefinition Width="40"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="24"/> <RowDefinition Height="24"/> <RowDefinition Height="24"/> <RowDefinition Height="24"/> </Grid.RowDefinitions> <TextBlock Text="Window Height:"/> <TextBlock Text="{Binding ElementName=MainWindow, Path=Height, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" FontWeight="Bold"/> <TextBlock Text="Window Width:" Grid.Row="1"/> <TextBlock Text="{Binding ElementName=MainWindow, Path=Width, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" Grid.Row="1" FontWeight="Bold"/> <TextBlock Text="Window Left:" Grid.Row="2"/> <TextBlock Text="{Binding ElementName=MainWindow, Path=Left, UpdateSourceTrigger=PropertyChanged}"
Grid.Column="1" Grid.Row="2" FontWeight="Bold"/> <TextBlock Text="Window Top:" Grid.Row="3"/> <TextBlock Text="{Binding ElementName=MainWindow, Path=Top, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" Grid.Row="3" FontWeight="Bold"/> </Grid> </Grid> </Window>
The code-behind for the window is as follows:
using System.Windows; using Recipe_05_23.Properties; namespace Recipe_05_23 { public partial class Window1 : Window { public Window1() { InitializeComponent(); } /// <summary> /// Override the OnClosing method and save the current settings /// </summary> /// <param name="e"></param> protected override void OnClosing( System.ComponentModel.CancelEventArgs e) { // Save the settings Settings.Default.Save(); base.OnClosing(e); } } }
Figure 5-25 shows the resulting window.
You need to bind UI elements to application resource strings to automatically use their values in your controls.
Use the System.Reflection
namespace to add the resource strings to the System.Windows.Application
instance's System.Windows.ResourceDictionary
when your application starts up. Reference the names of your resource strings in your binding statements using the ResourceKey
property of the System.Windows.StaticResourceExtension
class.
You can add resource strings to your application on the Resources page of the Project Designer in Visual Studio. This automatically generates corresponding .NET properties on your application's Resources
class. However, because these properties are marked as static
, you cannot bind to them directly. Instead, override the OnStartup
method of your Application
class in the App.xaml.cs
file, and use reflection to retrieve all the string properties from the Properties
. Resources
class. Add these properties and their values to the Application's Resources
collection. This makes them available for data binding throughout your application.
To bind the resource strings to your UI elements, use the StaticResource
markup extension in your binding statements, and set the value of the ResourceKey
property to the name of a resource string.
The resource strings are added to the Application
's Resources
collection at runtime. This means that when you reference them in your XAML, you may see error messages warning you that the StaticResource
reference was not found. However, the project will still compile and run perfectly.
The following example demonstrates a window that displays two System.Windows.Controls.TextBlock
controls and binds them to two resource strings that contain a welcome message and a copyright notice. These resource strings are defined on the project's Resources
page, which is shown in Figure 5-26.
In the App.xaml.cs
file, the OnStartup
method of the Application
class is overridden, and the resource strings are added to the resource dictionary. The code-behind for this is as follows:
using System; using System.Reflection; using System.Windows; namespace Recipe_05_24 { public partial class App : Application { /// <summary> /// Override the OnStartup method to add the /// resource strings to the Application's ResourceDictionary /// </summary> /// <param name="e"></param> protected override void OnStartup( StartupEventArgs e) { // Use reflection to get the PropertyInfo // for the Properties.Resources class Type resourcesType = typeof(Recipe_05_24.Properties.Resources); PropertyInfo[] properties = resourcesType.GetProperties( BindingFlags.Static | BindingFlags.NonPublic);
// Add properties to XAML Application.Resources foreach(PropertyInfo property in properties) { // If the property is a string, add it to the // application's resources dictionary if(property.PropertyType == typeof(string)) Resources.Add( property.Name, property.GetValue(null, null)); } base.OnStartup(e); } } }
The XAML for the window is as follows:
<Window x:Class="Recipe_05_24.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" WindowStartupLocation="Manual" Title="WPF Recipes 5_24" Height="180" Width="240"> <StackPanel Margin="10"> <TextBlock Text="{StaticResource ResourceKey=WelcomeMessage}" HorizontalAlignment="Center" FontSize="16" FontWeight="Bold"/> <TextBlock Text="{StaticResource ResourceKey=Copyright}" HorizontalAlignment="Center" Margin="10" Grid.Row="1" TextWrapping="Wrap"/> </StackPanel> </Window>
Figure 5-27 shows the resulting window.