1. Problem
You are trying to bind to a
data source and need to convert the source value to either a different
type or a different value suitable for display in the UI.
2. Solution
Implement System.Windows.Data.IValueConverter to create a value converter type, and associate it to the binding to appropriately convert the value.
3. How It Works
Often, you will come
across scenarios where the source value that you are trying to bind to
is either a data type that needs to be converted before it can be bound
or has the same data type as the target but needs some logical or
contextual transformation before it can be meaningful to the UI.
As an example, imagine the Visibility property of a control. It is natural to think of Visibility as a Boolean, and thus express it in code as a bool. However, trying to bind a bool to the Visibility property of a Silverlight control will pose a challenge: in Silverlight, Visibility is expressed in terms of the Visibility enumeration, which has two values, Visible and Collapsed. In this case, you will need to convert from a source type (bool) to a target type (Visibility).
Imagine another scenario where
you have the monthly spending of a family broken into categories as a
data source, and you need to visually represent each expenditure as a
percentage of the total. In this case, the data types of both the source
and the target can be the same (say a double), but there is a logical transformation required between them—from an absolute value to a percentage.
3.1. Implementing Value Conversion
To use value conversion, you implement the System.Windows.Data.IValueConverter interface. The IValueConverter interface accommodates both source-to-target conversion through the Convert() method and target-to-source conversion through the ConvertBack() method.
Listing 1 shows a sample converter implementation that converts bool to Visibility and back.
Listing 1. Value Converter from bool to Visibility
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
//check to see that the parameter types are conformant
if (value.GetType() != typeof(bool) || targetType != typeof(Visibility))
return null;
bool src = (bool)value;
//translate
return (src == true) ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
//check to see that the parameter types are conformant
if (value.GetType() != typeof(Visibility) || targetType != typeof(bool))
return null;
Visibility src = (Visibility)value;
//translate
return (src == Visibility.Visible) ? true : false;
}
}
|
In both methods, the first parameter named value is the source value and the second parameter named targetType is the data type of the target to which the value needs to be converted. The ConvertBack()
method will need to be fully implemented if you have a two-way binding,
where an edit on the UI would require the change to be sent back to the
source. If you do not update the data through your UI, you can simply
either return null or throw a suitable exception from the ConvertBack() method.
Also note that each method accepts a parameter, aptly named parameter,
where you can pass additional information as may be required by the
conversion logic, as well as the target culture as the last parameter,
in case you need to take into account a difference in the culture
between source and target.
To use the value
converter, you first declare it as a resource in your XAML, with an
appropriate custom namespace mapping to bring in the assembly, in this
case local:
<local:BoolToVisibilityConverter x:Name="REF_BoolToVisibilityConverter" />
After the converter resource has been declared as shown here, you can associate it to a Binding by using its Converter property. Once the converter is associated, every piece of data flowing through the Binding either way is passed through the converter methods—Convert() if the data is flowing from the source to the target property, and ConvertBack() if it is the other way. A sample usage is shown here
<ContentControl Visibility="{Binding IsControlVisible,
Converter={StaticResource REF_BoolToVisibilityConverter}}"/>
where IsControlVisible is a Boolean property on a data source CLR type bound to the control.
4. The Code
The code sample builds a
simple spending analysis application for a family, where the expenditure
for different categories are maintained in a DataGrid
and also graphed in a bar graph as a percentage of the total. The
application allows you to change the spending in each category to
different values and watch the graph change accordingly. It also allows
you to drag any bar in the graph using your mouse and watch the
corresponding value change in the DataGrid, maintaining the same total. Figure 1 shows the application output.
Listing 2 shows the data classes used for this sample. The Spending class represents a specific expenditure, while the SpendingCollection extends ObservableCollection<Spending> to add some initialization code in a default constructor.
Listing 2. Application Data Classes
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
namespace Recipe4_4
{
public class SpendingCollection : ObservableCollection<Spending>,
INotifyPropertyChanged
{
public SpendingCollection()
{
this.Add(new Spending
{
ParentCollection = this,
Item = "Utilities",
Amount = 300
});
this.Add(new Spending
{
ParentCollection = this,
Item = "Food",
Amount = 350
});
this.Add(new Spending
{
ParentCollection = this,
Item = "Clothing",
Amount = 200
});
this.Add(new Spending
{
ParentCollection = this,
Item = "Transportation",
Amount = 75
});
this.Add(new Spending
{
ParentCollection = this,
Item = "Mortgage",
Amount = 3000
});
this.Add(new Spending
{
ParentCollection = this,
Item = "Education",
Amount = 500
});
this.Add(new Spending
{
ParentCollection = this,
Item = "Entertainment",
Amount = 125
});
this.Add(new Spending
{
ParentCollection = this,
Item = "Loans",
Amount = 750
});
this.Add(new Spending
{
ParentCollection = this,
Item = "Medical",
Amount = 80
});
this.Add(new Spending
{
ParentCollection = this,
Item = "Miscellaneous",
Amount = 175
});
}
public double Total
{
get
{
return this.Sum(spending => spending.Amount);
}
}
}
public class Spending : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
internal void RaisePropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
{
PropertyChanged(this, e);
}
}
SpendingCollection _ParentCollection = null;
public SpendingCollection ParentCollection
{
get { return _ParentCollection; }
set { _ParentCollection = value; }
}
private string _Item;
public string Item
{
get { return _Item; }
set
{
string OldVal = _Item;
if (OldVal != value)
{
_Item = value;
RaisePropertyChanged(new PropertyChangedEventArgs("Item"));
}
}
}
private double _Amount;
public double Amount
{
get { return _Amount; }
set
{
double OldVal = _Amount;
if (OldVal != value)
{
_Amount = value;
foreach (Spending sp in ParentCollection)
sp.RaisePropertyChanged(new PropertyChangedEventArgs("Amount"));
}
}
}
}
}
|
Listing 3 shows the XAML for the page. If you look at the resources section, you will notice two value converters. SpendingToBarWidthConverter converts a double value representing Spending to another double value representing a corresponding bar width, and vice versa. SpendingToPercentageStringConverter converts a Spending
value to a percentage of the total spending, and vice versa. These
converter implementations will be discussed in more detail momentarily.
Listing 3. XAML for the Page
<UserControl x:Class="Recipe4_4.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data=
"clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:local="clr-namespace:Recipe4_4"
Width="800" Height="510">
<UserControl.Resources>
<local:SpendingCollection x:Key="REF_SpendingList" />
<local:SpendingToBarWidthConverter x:Key="REF_SpendingToBarWidthConverter" />
<local:SpendingToPercentageStringConverter
x:Key="REF_SpendingToPercentageStringConverter" />
<DataTemplate x:Key="dtBarTemplate">
<Grid HorizontalAlignment="Left" VerticalAlignment="Stretch"
Height="30" Margin="0,2,0,0" >
<Grid.RowDefinitions>
<RowDefinition Height="0.5*" />
<RowDefinition Height="0.5*" />
</Grid.RowDefinitions>
<Rectangle Grid.Row="1" VerticalAlignment="Stretch"
Fill="Black" HorizontalAlignment="Left"
Width="{Binding Amount,Mode=TwoWay,
Converter={StaticResource REF_SpendingToBarWidthConverter},
ConverterParameter={StaticResource REF_SpendingList}}"
MouseMove="Rectangle_MouseMove"
MouseLeftButtonDown="Rectangle_MouseLeftButtonDown"
MouseLeftButtonUp="Rectangle_MouseLeftButtonUp"/>
<StackPanel Orientation="Horizontal" Grid.Row="0">
<TextBlock Text="{Binding Item}" FontSize="9" />
<TextBlock Text="{Binding Amount,
Converter={StaticResource REF_SpendingToPercentageStringConverter},
ConverterParameter={StaticResource REF_SpendingList}}"
Margin="5,0,0,0"
FontSize="9"/>
</StackPanel>
</Grid>
</DataTemplate>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White" Width="694">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<data:DataGrid HorizontalAlignment="Stretch" Margin="8,8,8,8"
VerticalAlignment="Stretch"
HeadersVisibility="Column" x:Name="dgSpending"
ItemsSource="{StaticResource REF_SpendingList}"
AutoGenerateColumns="False">
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="Item"
Binding="{Binding Item,Mode=TwoWay}"/>
<data:DataGridTextColumn Header="Value" Width="100"
Binding="{Binding Amount,Mode=TwoWay}"/>
</data:DataGrid.Columns>
</data:DataGrid>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Grid.Column="1" Margin="8,8,8,8" x:Name="GraphRoot"
DataContext="{StaticResource REF_SpendingList}">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<Rectangle Height="Auto" HorizontalAlignment="Left"
VerticalAlignment="Stretch" Width="2"
Stroke="#FF000000" StrokeThickness="0"
Fill="#FF000000" x:Name="rectYAxis" Margin="0,0,0,0"/>
<Rectangle Height="2" HorizontalAlignment="Stretch"
VerticalAlignment="Bottom" Fill="#FF000000"
Stroke="#FF000000" StrokeThickness="0"
Stretch="Fill" x:Name="rectXAxis" Margin="0,0,0,0"
Width="350" />
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Width="Auto" Grid.Row="1" Margin="2,0,0,0"
x:Name="gridMarkers">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.1*" />
<ColumnDefinition Width="0.1*" />
<ColumnDefinition Width="0.1*" />
<ColumnDefinition Width="0.1*" />
<ColumnDefinition Width="0.1*" />
<ColumnDefinition Width="0.1*" />
<ColumnDefinition Width="0.1*" />
<ColumnDefinition Width="0.1*" />
<ColumnDefinition Width="0.1*" />
<ColumnDefinition Width="0.1*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="0.3*" />
<RowDefinition Height="0.7*" />
</Grid.RowDefinitions>
<Rectangle Width="2" Fill="Black" VerticalAlignment="Stretch"
HorizontalAlignment="Right" Grid.Column="0" />
<Rectangle Width="2" Fill="Black" VerticalAlignment="Stretch"
HorizontalAlignment="Right" Grid.Column="1" />
<Rectangle Width="2" Fill="Black" VerticalAlignment="Stretch"
HorizontalAlignment="Right" Grid.Column="2" />
<Rectangle Width="2" Fill="Black" VerticalAlignment="Stretch"
HorizontalAlignment="Right" Grid.Column="3" />
<Rectangle Width="2" Fill="Black" VerticalAlignment="Stretch"
HorizontalAlignment="Right" Grid.Column="4" />
<Rectangle Width="2" Fill="Black" VerticalAlignment="Stretch"
HorizontalAlignment="Right" Grid.Column="5" />
<Rectangle Width="2" Fill="Black" VerticalAlignment="Stretch"
HorizontalAlignment="Right" Grid.Column="6" />
<Rectangle Width="2" Fill="Black" VerticalAlignment="Stretch"
HorizontalAlignment="Right" Grid.Column="7" />
<Rectangle Width="2" Fill="Black" VerticalAlignment="Stretch"
HorizontalAlignment="Right" Grid.Column="8" />
<Rectangle Width="2" Fill="Black" VerticalAlignment="Stretch"
HorizontalAlignment="Right" Grid.Column="9" />
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Stretch"
Grid.Row="1" Grid.Column="0" Text="10%" FontSize="11"
FontWeight="Bold" />
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Stretch"
Grid.Row="1" Grid.Column="1" Text="20%" FontSize="11"
FontWeight="Bold" />
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Stretch"
Grid.Row="1" Grid.Column="2" Text="30%" FontSize="11"
FontWeight="Bold" />
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Stretch"
Grid.Row="1" Grid.Column="3" Text="40%" FontSize="11"
FontWeight="Bold" />
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Stretch"
Grid.Row="1" Grid.Column="4" Text="50%" FontSize="11"
FontWeight="Bold" />
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Stretch"
Grid.Row="1" Grid.Column="5" Text="60%" FontSize="11"
FontWeight="Bold" />
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Stretch"
Grid.Row="1" Grid.Column="6" Text="70%" FontSize="11"
FontWeight="Bold" />
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Stretch"
Grid.Row="1" Grid.Column="7" Text="80%" FontSize="11"
FontWeight="Bold" />
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Stretch"
Grid.Row="1" Grid.Column="8" Text="90%" FontSize="11"
FontWeight="Bold" />
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Stretch"
Grid.Row="1" Grid.Column="9" Text="100%" FontSize="11"
FontWeight="Bold" />
</Grid>
<Grid Height="Auto" HorizontalAlignment="Stretch" Margin="2,0,0,2"
VerticalAlignment="Stretch" Width="Auto" x:Name="gridBars"
ShowGridLines="True">
<ItemsControl ItemsSource="{StaticResource REF_SpendingList}"
ItemTemplate="{StaticResource dtBarTemplate}" />
</Grid>
</Grid>
</Grid>
</UserControl>
|
The rest of the XAML is pretty simple. The SpendingCollection, through a resource reference named REF_SpendingList, is bound to a DataGrid named dgSpending. The bar graph is implemented as an ItemsControl, once again bound to the same SpendingCollection instance, using a DataTemplate named dtBarTemplate for each bar.
Note how you use the converters inside dtBarTemplate. You bind the Width of a Rectangle directly to the Amount property on the bound Spending instance and then use the Converter property of the Binding to associate the SpendingToBarWidthConverter. You also bind the Text property of a TextBlock similarly, using the SpendingToPercentageStringConverter instead. On both occasions, you also pass in the entire SpendingCollection instance through the ConverterParameter property of the Binding. The ConverterParameter property value maps to the method parameter named parameter in both the Convert() and ConvertBack() methods on the value converter. This makes the collection available inside the converter code.
SpendingToBarWidthConverter, shown in Listing 4, is used to convert a spending value to the length of the corresponding bar in the bar graph; both data types are double.
Listing 4. Value Converter Converting Spending to Bar Width
using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Shapes;
namespace Recipe4_4
{
public class SpendingToBarWidthConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
//verify validity of all the parameters
if (value.GetType() != typeof(double) || targetType != typeof(double)
|| parameter == null
|| parameter.GetType() != typeof(SpendingCollection))
return null;
//cast appropriately
double Spending = (double)value;
double Total = ((SpendingCollection)parameter).Total;
//find the xAxis
Rectangle rectXAxis = (Rectangle)((MainPage)Application.Current.RootVisual)
.FindName("rectXAxis");
//calculate bar width in proportion to the xAxis width
return (Spending / Total) * rectXAxis.Width;
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
//verify validity of all the parameters
if (value.GetType() != typeof(double) || targetType != typeof(double)
|| parameter == null
|| parameter.GetType() != typeof(SpendingCollection))
return null;
//cast appropriately
double BarWidth = (double)value;
double Total = ((SpendingCollection)parameter).Total;
//find the xAxis
Rectangle rectXAxis = (Rectangle)((MainPage)Application.Current.RootVisual)
.FindName("rectXAxis");
//calculate new spending keeping total spending constant based on
//new bar width to xAxis width ratio
return (BarWidth / rectXAxis.Width) * Total;
}
}
}
|
To convert the spending value into bar width in SpendingToBarWidthConverter.Convert(), you calculate the ratio of the spending value in question to the total spending evaluated from the SpendingCollection passed in as parameter. You then calculate the bar width as the same ratio applied to the total width of the X axis of the graph, also defined as a Rectangle named rectXAxis in XAML. In SpendingToBarWidthConverter.ConvertBack(), you reverse that calculation.
Listing 5 shows the SpendingToPercentageStringConverter code. The calculation of the percentage value in Convert() is again based off the spending total derived from the SpendingCollection instance and then formatted appropriately to a string. Since you never do the reverse conversion, you do not implement ConvertBack() in this case.
Listing 5. Value Converter Converting Spending to a Percentage String
using System;
using System.Windows.Data;
namespace Recipe4_4
{
public class SpendingToPercentageStringConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
//verify validity of all the parameters
if (value.GetType() != typeof(double) || targetType != typeof(string)
|| parameter == null
|| parameter.GetType() != typeof(SpendingCollection))
return null;
//cast appropriately
double Spending = (double)value;
double Total = ((SpendingCollection)parameter).Total;
//calculate the spending percentage and format as string
return ((Spending / Total) * 100).ToString("###.##") + " %";
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
|
NOTE
There is no
requirement that a value converter also perform a type conversion. In
the code sample for SpendingToBarWidthConverter, for example, you
convert values of the same data type double, where the conversion is one
of context—that is, from one kind of measure (Spending) to another
(Width). Therefore, it is called a value conversion.
Listing 6 shows the codebehind for the MainPage. Of note is the MouseMove handler Rectangle_MouseMove() for each Rectangle representing a bar in the ItemsControl.
In the handler, you calculate the distance moved as the difference of
the current mouse position and its previous position and change the Width of the bar accordingly. You then store the current position as the previous position for the next move.
Listing 6. Codebehind for the Page
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Shapes;
namespace Recipe4_4
{
public partial class MainPage : UserControl
{
private bool MouseLeftBtnDown = false;
Point PreviousPos;
public MainPage()
{
InitializeComponent();
}
private void Rectangle_MouseMove(object sender, MouseEventArgs e)
{
if (MouseLeftBtnDown)
{
Rectangle rect = (Rectangle)sender;
Point CurrentPos = e.GetPosition(sender as Rectangle);
double Moved = CurrentPos.X – PreviousPos.X;
if (rect.Width + Moved >= 0)
{
rect.Width += Moved;
}
PreviousPos = CurrentPos;
}
}
private void Rectangle_MouseLeftButtonDown(object sender,
MouseButtonEventArgs e)
{
MouseLeftBtnDown = true;
PreviousPos = e.GetPosition(sender as Rectangle);
}
private void Rectangle_MouseLeftButtonUp(object sender,
MouseButtonEventArgs e)
{
MouseLeftBtnDown = false;
}
}
}