I'm going to come right out and say it, binding converters are one of
the nicest pieces of WPF I've run across so far. Simply put, they
provide a translation between your binding source and destination. The
most common use I've found is when I'm binding to user interface
elements, because on many occasions I don't store something in a data
member that can just be stuck on the screen. This tutorial will provide
an introduction to what binding converters are and how to use them.
The example application I'm going to make today is going to be a traffic
light. The graphics will all be written in XAML and will be bound to an
object that represents the state of the light (green, yellow, and red).
Let's start off with the object. This is a very simple object and should
be very straight forward.
public class TrafficLight : INotifyPropertyChanged
{
/// <summary>
/// Fired whenever a property changes. Required for
/// INotifyPropertyChanged interface.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// The current state of the traffic light
/// </summary>
private States _state;
/// <summary>
/// All possible states of the traffic light
/// </summary>
public enum States
{
GREEN,
YELLOW,
RED
}
/// <summary>
/// Gets or sets the state of the traffic light
/// </summary>
public States State
{
get { return _state; }
set
{
if (value != _state)
{
_state = value;
if(PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("State"));
}
}
}
}
Above is a pretty basic class that will control the state of our traffic
light. It implements the
INotifyPropertyChanged
interface, which is located in the System.ComponentModel namespace. This
interface is used by WPF's data binding engine to know when a property
changes so the binding can be updated. I think the rest is pretty self
explanatory, so on to some XAML.
<UserControl x:Class="BindingConverterTutorial.TrafficLightControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="120" Height="350">
<Grid>
<Border Background="Yellow"
CornerRadius="10"
BorderBrush="Gray"
BorderThickness="2">
<StackPanel VerticalAlignment="Center">
<StackPanel.Resources>
<Style TargetType="{x:Type Ellipse}">
<Setter Property="Width" Value="100" />
<Setter Property="Height" Value="100" />
<Setter Property="Fill" Value="LightGray" />
<Setter Property="Stroke" Value="Gray" />
<Setter Property="StrokeThickness" Value="2" />
<Setter Property="Margin" Value="4" />
</Style>
</StackPanel.Resources>
<Ellipse />
<Ellipse />
<Ellipse />
</StackPanel>
</Border>
</Grid>
</UserControl>
Now if we slap that control on our Window:
<Window x:Class="BindingConverterTutorial.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:BindingConverterTutorial"
Title="Window1" Height="512" Width="512">
<Grid>
<my:TrafficLightControl />
</Grid>
</Window>
We get something that looks like this:
OK, so there's not much going on here. Let's modify our window to create
a TrafficLight object and bind it to our control.
<Window x:Class="BindingConverterTutorial.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:BindingConverterTutorial"
Title="Window1" Height="512" Width="512">
<Window.Resources>
<my:TrafficLight State="Green" x:Key="myTrafficLight" />
</Window.Resources>
<Grid>
<my:TrafficLightControl DataContext="{StaticResource myTrafficLight}" />
</Grid>
</Window>
If you run this code, you still won't see a green traffic light. That's
because we haven't set up the binding in the actual TrafficLightControl
object. Let's look at that now.
<UserControl x:Class="BindingConverterTutorial.TrafficLightControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="120" Height="350">
<Grid>
<Border Background="Yellow"
CornerRadius="10"
BorderBrush="Gray"
BorderThickness="2">
<StackPanel VerticalAlignment="Center">
<StackPanel.Resources>
<Style TargetType="{x:Type Ellipse}">
<Setter Property="Width" Value="100" />
<Setter Property="Height" Value="100" />
<Setter Property="Fill" Value="LightGray" />
<Setter Property="Stroke" Value="Gray" />
<Setter Property="StrokeThickness" Value="2" />
<Setter Property="Margin" Value="4" />
</Style>
</StackPanel.Resources>
<Ellipse Fill="{Binding State}" />
<Ellipse Fill="{Binding State}" />
<Ellipse Fill="{Binding State}" />
</StackPanel>
</Border>
</Grid>
</UserControl>
Because we set the DataContext of the TrafficLightControl in the Window,
we can use the Binding attributes to bind to properties of the
TrafficLight object. In this case, I bound the Fill color of each
ellipse to the state of the traffic light. As you can imagine this won't
work, because we're trying to tell the binding engine to make an enum
into a Brush. Needless to say, it doesn't know how to do this.
If you were to run this code in debug mode, you'd see some output in the
console telling you that it did not know how to perform this binding.
System.Windows.Data Error: 1 : Cannot create default converter to
perform 'one-way' conversions between types
'BindingConverterTutorial.TrafficLight+States' and
'System.Windows.Media.Brush'. Consider using Converter property of
Binding.
This is where the converters come in. A converter can be any class that
implements
IValueConverter.
This interface requires that you implement two methods:
Convert
and
ConvertBack.
Here's the converter that I'm going to apply to each light:
[ValueConversion(typeof(TrafficLight.States), typeof(Brush))]
public class ColorConverter : IValueConverter
{
public enum Lights
{
GREEN,
YELLOW,
RED
}
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
TrafficLight.States state = (TrafficLight.States)value;
Lights light = (Lights)Enum.Parse(typeof(Lights), (string)parameter);
switch (state)
{
case TrafficLight.States.GREEN:
if (light == Lights.GREEN)
return new SolidColorBrush(Colors.Green);
break;
case TrafficLight.States.YELLOW:
if (light == Lights.YELLOW)
return new SolidColorBrush(Colors.Yellow);
break;
case TrafficLight.States.RED:
if (light == Lights.RED)
return new SolidColorBrush(Colors.Red);
break;
}
return new SolidColorBrush(Colors.LightGray);
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
return null;
}
}
The first thing you might notice is the
ValueConversion
attribute. This is not actually required, but it helps tell developer
tools what type is going to be converted to what other type. The state
of the light is passed in through the value parameter. The value,
targetType, and culture are all automatically passed in by WPF's binding
engine. The parameter is an optional object that I used to tell the
converter which light is requesting the conversion. I have to convert it
from a string since the XAML syntax I used sends the argument as a
string. I then check the state and the light to determine what the color
is supposed to be.
The ConvertBack function can be used to go in the opposite direction. So
if you had a user interface that let the user select which light was
lit, you could pass the brush back through the binding conversion and
convert it back to a States enum. In this case, since the binding is
one-way, we simply return null.
Now let's look at the XAML that uses this converter.
<UserControl x:Class="BindingConverterTutorial.TrafficLightControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="120" Height="350">
<Grid>
<Border Background="Yellow"
CornerRadius="10"
BorderBrush="Gray"
BorderThickness="2">
<StackPanel VerticalAlignment="Center">
<StackPanel.Resources>
<Style TargetType="{x:Type Ellipse}">
<Setter Property="Width" Value="100" />
<Setter Property="Height" Value="100" />
<Setter Property="Fill" Value="LightGray" />
<Setter Property="Stroke" Value="Gray" />
<Setter Property="StrokeThickness" Value="2" />
<Setter Property="Margin" Value="4" />
</Style>
</StackPanel.Resources>
<Ellipse Fill="{Binding State,
Converter={StaticResource colorConverter},
ConverterParameter=RED}" />
<Ellipse Fill="{Binding State,
Converter={StaticResource colorConverter},
ConverterParameter=YELLOW}" />
<Ellipse Fill="{Binding State,
Converter={StaticResource colorConverter},
ConverterParameter=GREEN}" />
</StackPanel>
</Border>
</Grid>
</UserControl>
The first thing I do is create a ColorConverter object up in my
control's resources. I then tell each Ellipse to use that converter and
set the ConverterParameter to the appropriate light. And that does it
for setting up the binding. Now when you launch the application, you get
something that looks like this:
Now, whenever the State of the bound TrafficLight object changes, the
binding will automatically update the display to the correct light.
Let's add some buttons to the windows to illustrate this.
<Window x:Class="BindingConverterTutorial.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:BindingConverterTutorial"
Title="Window1" Height="512" Width="512">
<Window.Resources>
<my:TrafficLight State="Green" x:Key="myTrafficLight" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="350" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<my:TrafficLightControl
Grid.Row="1"
DataContext="{StaticResource myTrafficLight}" />
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Grid.Row="2"
Height="30" >
<Button Content="Green"
Width="100"
Margin="2"
Click="ButtonClicked"
Name="btnGreen" />
<Button Content="Yellow"
Width="100"
Margin="2"
Click="ButtonClicked"
Name="btnYellow" />
<Button Content="Red"
Width="100"
Margin="2"
Click="ButtonClicked"
Name="btnRed" />
</StackPanel>
</Grid>
</Window>
Here I've added three buttons that when clicked call the same event
handler. Here's the code inside the event handler:
private void ButtonClicked(object sender, RoutedEventArgs e)
{
TrafficLight tLight = (TrafficLight)this.FindResource("myTrafficLight");
if (sender == btnGreen)
tLight.State = TrafficLight.States.GREEN;
else if (sender == btnYellow)
tLight.State = TrafficLight.States.YELLOW;
else if (sender == btnRed)
tLight.State = TrafficLight.States.RED;
}
In this function I pull the TrafficLight object out of my window's
resources. Then, depending on what button was pressed, I simply set the
state of the light. The UI will now automatically update and fill the
correct circle. That's all there is to it.
I've found binding converters to be an indispensable resource when
building WPF applications. It helps keep architectures clean by removing
the need to contain UI logic and members within your data objects. It
also greatly reduces the amount and complexity of code required to
update the user interface. You can download all of the source code from
here.
Source Files:
Comments
Post a Comment