.NET - Generic custom WPF/Silverlight value converters
- Date:
- Author: Stefan Cruysberghs
Data binding is one of the most powerful features of WPF/Silverlight. It allows developers to achieve more with less code. However, when building user interfaces with WPF or Silverlight, you often need to code a little bit in the form of custom value converters. Value converters fill the gap between the way data is stored in your classes and the way that this data will be displayed in your WPF window or Silverlight usercontrol. A common mistake made by people who are new to WPF/Silverlight is to implement custom value converters for each and every binding that requires one.
Of course value converters can be generalized an re-used. While developing with WPF for several months now, I have been implementing a library of generic custom value converters. In this article I will show you the source code of some very small but powerful value converters and I will demonstrate in which scenarios you can use them. Most of these value converters will also work with Silverlight 2.
- How to implement a WPF/Silverlight value converter ?
- How to use WPF/Silverlight value converters ?
- Generic WPF/Silverlight value converters
- Piping value converters
How to implement a WPF/Silverlight value converter ?
I will not fully describe how to write value converters, but the basics are quite simple.
1) Create a public class derived from the IValueConverter interface. There is also a IMultiValueConverter interface but I will not cover this interface in this article.
2) Implement the Convert() method. The method will return an object which represents the way the data should be displayed.
3) In WPF a ValueConversion attribute can be used to specify some restrictions on the input parameters. This ValueConversionAttributeClass does not exists in Silverlight 2. So in Silverlight you have to add some extra code to check the given parameters.
[ValueConversion(...)]
public class MyValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return ...;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
How to use WPF/Silverlight value converters ?
Let me also explain very briefly how to utilize a value converter in a XAML file.
1) Create a Xml namespace prefix (xmlns) for the namespace/assembly which holds the value converters. In these examples this prefix will be called "convert".
<Window x:Class="ScipBe.Demo.WpfRss.WindowRss"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ScipBe.Demo.WpfRss"
xmlns:convert="clr-namespace:ScipBe.Wpf.ValueConverters;assembly=ScipBe.Wpf.ValueConverters"
2) An instance of the value converter should be created. The easiest way to do this, is to add the value converter to the Window.Resources or UserControl.Resources dictionary in your XAML file. A Key is required so we can access the resource later.
<Window.Resources>
<convert:IsDifferentConverter x:Key="IsDifferentConverter"></convert:IsDifferentConverter>
</Window.Resources>
3) Use the value converter in the data binding. The Converter parameter should refer to the static resource (=key) which has already been instantiated. If the value converter uses a parameter, set it with the ConvertParameter option.
<Button Command="local:WindowRss.RefreshCommand"
IsEnabled="{Binding ElementName=listBoxFeeds, Path=Items.Count,
Converter={StaticResource IsDifferentConverter}, ConverterParameter=0}">Refresh
</Button>
Generic WPF value converters
In this part I will publish the source code of 10 generic value converters.
I've also created a small RSS Reader WPF demo application which will illustrate some UI behavior and layout scenarios in which you can use these converters.
InvertBoolConverter
This value converter will invert a boolean value. This can be useful when binding to the IsChecked, IsReadOnly or IsEnabled properties.
/// <summary>
/// WPF/Silverlight ValueConverter : Return inverted boolean (=Value)
/// </summary>
#if !SILVERLIGHT
[ValueConversion(typeof(bool), typeof(bool))]
#endif
public class InvertBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
IsEqualConverter
Check if a property value equals a given parameter. When this function is used in XAML, then the values will be compared as strings.
/// <summary>
/// WPF/Silverlight ValueConverter : return true if Value equals Parameter
/// </summary>
public class IsEqualConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (targetType != typeof(bool))
{
throw new ArgumentException("Target must be a boolean");
}
if (value == null)
{
return (parameter == null);
}
if (value is String)
{
return value.ToString().Equals(parameter.ToString());
}
return value.Equals(parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
IsDifferentConverter
Of course this value converter uses the opposite logic. The Convert() method will return true if a property value differs from a given parameter.
/// <summary>
/// WPF/Silverlight ValueConverter : return true if Value differs from Parameter
/// </summary>
public class IsDifferentConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return !((bool)new IsEqualConverter().Convert(value, targetType, parameter, culture));
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
Example 1
Enable the Refresh button when the list of feeds is not empty. Therefore we check if listBoxFeeds.Items.Count is not 0.
<Window.Resources>
<convert:IsDifferentConverter x:Key="IsDifferentConverter"></convert:IsDifferentConverter>
</Window.Resources>
<Button Command="local:WindowRss.RefreshCommand"
IsEnabled="{Binding ElementName=listBoxFeeds, Path=Items.Count,
Converter={StaticResource IsDifferentConverter}, ConverterParameter=0}">Refresh
</Button>
Example 2
The Delete button will only be enabled if there is a selected item in the ListBox. So check if SelectedIndex is different from -1.
<Window.Resources>
<convert:IsDifferentConverter x:Key="IsDifferentConverter"></convert:IsDifferentConverter>
</Window.Resources>
<Button Command="local:WindowRss.DeleteCommand"
IsEnabled="{Binding ElementName=listBoxFeeds, Path=SelectedIndex,
Converter={StaticResource IsDifferentConverter}, ConverterParameter=-1}">Delete
</Button>
IsLessThanConverter
The Convert() method from the IsLessThanConverter will return true if the property value is less than the given parameter. The type of the property and the parameter should be the same.
/// <summary>
/// WPF/Silverlight ValueConverter : return true if Value is less than Parameter
/// </summary>
public class IsLessThanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(bool))
{
throw new ArgumentException("Target must be a boolean");
}
if ((value == null) || (parameter == null))
{
return false;
}
double convertedValue;
if (!double.TryParse(value.ToString(), out convertedValue))
{
throw new InvalidOperationException("The Value can not be converted to a Double");
}
double convertedParameter;
if (!double.TryParse(parameter.ToString(), out convertedParameter))
{
throw new InvalidOperationException("The Parameter can not be converted to a Double");
}
return convertedValue < convertedParameter;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return new NotSupportedException();
}
}
IsGreaterThanConverter
The name of this value converter already explains its purpose.
/// <summary>
/// WPF/Silverlight ValueConverter : return true if Value is greater than Parameter
/// </summary>
public class IsGreaterThanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(bool))
{
throw new ArgumentException("Target must be a boolean");
}
if ((value == null) || (parameter == null))
{
return false;
}
double convertedValue;
if (!double.TryParse(value.ToString(), out convertedValue))
{
throw new InvalidOperationException("The Value can not be converted to a Double");
}
double convertedParameter;
if (!double.TryParse(parameter.ToString(), out convertedParameter))
{
throw new InvalidOperationException("The Parameter can not be converted to a Double");
}
return convertedValue > convertedParameter;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return new NotSupportedException();
}
}
Example 3
Enable the "Group by hostname" checkbox if there is more than one feed.
<Window.Resources>
<convert:IsGreaterThanConverter x:Key="IsGreaterThanConverter"></convert:IsGreaterThanConverter>
</Window.Resources>
<CheckBox Name="checkBoxGroupBy"
IsEnabled="{Binding ElementName=listBoxFeeds, Path=Items.Count,
Converter={StaticResource IsGreaterThanConverter}, ConverterParameter=1}">Group by hostname
</CheckBox>
IsMatchConverter
This value converter will use the Regex object. The Convert() method will return true if the string property matches the given regular expression. In Siverlight you have to remove the ValueConversion attribute!
/// <summary>
/// WPF/Silverlight ValueConverter : does Value match the regular expression (=Parameter) ?
/// </summary>
#if !SILVERLIGHT
[ValueConversion(typeof(string), typeof(bool), ParameterType = typeof(string))]
#endif
public class IsMatchConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if ((value == null) || (parameter == null))
{
return false;
}
Regex regex = new Regex((string)parameter);
return regex.IsMatch((string)value);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
Example 4
The "Add URL" button will only be enabled when a valid URL string will be entered. In this example the regular expression is stored as a Resource.
<Window.Resources>
<System:String x:Key="validUrlExpression">
^((ht|f)tp(s?)\:\/\/|~/|/)?([\w]+:\w+@)
?([a-zA-Z]{1}([\w\-]+\.)+([\w]{2,5}))(:[\d]{1,5})?((/?\w+/)+|/?)(\w+\.[\w]{3,4})?
((\?\w+=\w+)?(&\w+=\w+)*)?
</System:String>
<convert:IsMatchConverter x:Key="IsMatchConverter"></convert:IsMatchConverter>
</Window.Resources>
<Button Command="local:WindowRss.AddCommand"
IsEnabled="{Binding ElementName=textBoxUrl, Path=Text,
Converter={StaticResource IsMatchConverter}, ConverterParameter={StaticResource validUrlExpression}}">Add URL
</Button>
HtmlDecodeConverter
The HtmlDecodeConverter is also a very simple converter which will call the HttpUtility.HtmlDecode() method.
/// <summary>
/// WPF/Silverlight ValueConverter : Decode given string (Value) to HTML output
/// </summary>
#if !SILVERLIGHT
[ValueConversion(typeof(string), typeof(string))]
#endif
public class HtmlDecodeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value == null ? "" : HttpUtility.HtmlDecode((string)value);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
Example 5
Convert the HTML which is stored in the Summary.Text property of a SyndicationFeed to plain text.
<Window.Resources>
<convert:HtmlDecodeConverter x:Key="HtmlDecodeConverter"></convert:HtmlDecodeConverter>
</Window.Resources>
<TextBlock Text="{Binding Path=Summary.Text,
Converter={StaticResource HtmlDecodeConverter}}"
TextWrapping="Wrap">
</TextBlock>
ToStringEnumerationConverter
Before I created this ToStringEnumerationConverter, I had already implemented a LINQ extension method called AsStringEnumeration(). This extension method is a kind of aggregate function which will concatenate the values of a given property.
public static class LinqExtensionMethods
{
public static string AsStringEnumeration<TEntity, TProperty>(
this IEnumerable<TEntity> allItems,
Func<TEntity, TProperty> property)
where TEntity : class
{
string result = "";
foreach (var item in allItems)
{
result += property(item).ToString() + ", ";
}
if (!string.IsNullOrEmpty(result))
{
result = result.Remove(result.Length - 2);
}
return result;
}
}
The ToStringEnumerationConverter can be used on a collection. By using reflection a lamba function will be created and this will pass the name of a property to the AsStringEnumeration() LINQ extension method.
/// <summary>
/// WPF/Silverlight ValueConverter : Convert collection (Value) to a string enumeration
/// </summary>
#if !SILVERLIGHT
[ValueConversion(typeof(IEnumerable), typeof(string))]
#endif
public class ToStringEnumerationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (!(parameter is string))
{
throw new ArgumentException("Parameter must be a string with the name of a property");
}
if ((value == null) || ((string)parameter == ""))
return "";
Func<object, object> func = item => item.GetType().GetProperty((string)parameter).GetValue(item, null);
#if SILVERLIGHT
return (value as IEnumerable<object>).AsStringEnumeration(func);
#else
return (value as IEnumerable).OfType<object>().AsStringEnumeration(func);
#endif
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
Example 6
The hint will show a string which is the concatenation of all Name values of the Categories collection in a SyndicationFeed object.
<Window.Resources>
<convert:ToStringEnumerationConverter x:Key="ToStringEnumerationConverter"></convert:ToStringEnumerationConverter>
</Window.Resources>
<TextBlock Text="{Binding Path=Categories,
Converter={StaticResource ToStringEnumerationConverter}, ConverterParameter=Name}"
TextWrapping="Wrap">
</TextBlock>
VisibilityConverter
This VisiblityConverter can be used to convert a boolean to one of Visibility enumeration values (Visible or Collapsed)./// <summary>
/// WPF/Silverlight ValueConverter : Convert boolean to XAML Visibility
/// </summary>
#if !SILVERLIGHT
[ValueConversion(typeof(bool), typeof(Visibility))]
#endif
public class VisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (value != null && (bool)value) ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
Visibility visibility = (Visibility)value;
return (visibility == Visibility.Visible);
}
}
FormatConverter
I also created a FormatConverter that called the String.Format() method for formatting numbers and dates. e.g. {0:dddd MMMM}, {0:C}, {0:E03}, ...
/// <summary>
/// WPF/Silverlight ValueConverter : Format numbers and dates by passing a composite string format
/// </summary>
#if !SILVERLIGHT
[ValueConversion(typeof(object), typeof(string))]
#endif
public class FormatConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null)
{
return "";
}
if (parameter != null)
{
if (!parameter.ToString().StartsWith("{0:"))
{
parameter = "{0:" + parameter;
}
if (!parameter.ToString().EndsWith("}"))
{
parameter += "}";
}
return string.Format(culture, parameter.ToString(), value);
}
return value.ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Since Service Pack 1 for .NET 3.5 new binding parameters are introduced. This means that you are able to format strings directly in bindings and many controls by using the StringFormat, ItemStringFormat, ContentStringFormat, ... properties. Because of this custom value converters for string formatting will become redundant in WPF.
Here are some examples of the new StringFormat property:
<TextBlock Text="{Binding Path=SalesDate, StringFormat=MMMM}"></TextBlock>
<TextBlock Text="{Binding Path=SalesDate, StringFormat=MM/yyyy}"></TextBlock>
<TextBlock Text="{Binding Path=SalesDate, StringFormat='Month={0:MM}, Year={0:yyyy}'}"></TextBlock>
<TextBlock Text="{Binding Path=SalesDate, StringFormat=dd MMMM yyyy}"></TextBlock>
<TextBlock Text="{Binding Path=SalesDate, StringFormat='dd MMMM yyyy'}"></TextBlock>
<TextBlock Text="{Binding Path=SalesDate, StringFormat='{}{0:dd MMMM yyyy}'}"></TextBlock>
<TextBlock>
<TextBlock.Text>
<Binding Path="SalesDate" StringFormat="MMMM"/>
</TextBlock.Text>
</TextBlock>
<TextBlock>
<TextBlock.Text>
<Binding Path="SalesDate" StringFormat="dd MMMM yyyy"/>
</TextBlock.Text>
</TextBlock>
<TextBlock>
<TextBlock.Text>
<Binding Path="SalesDate" StringFormat="{}{0:dd MMMM yyyy}"/>
</TextBlock.Text>
</TextBlock>
<TextBlock Text="{Binding Path=Weight, StringFormat=F2}"></TextBlock>
<TextBlock Text="{Binding Path=Weight, StringFormat=F3}"></TextBlock>
<TextBlock Text="{Binding Path=Weight, StringFormat='{}{0:F5}'}"></TextBlock>
<TextBlock Text="{Binding Path=Weight, StringFormat='{}{0:F10}'}"></TextBlock>
<TextBlock Text="{Binding Path=Weight, StringFormat='N'}"></TextBlock>
<TextBlock Text="{Binding Path=Weight, StringFormat='{}{0:N2}'}"></TextBlock>
<TextBlock Text="{Binding Path=Weight, StringFormat='{}{0:N6}'}"></TextBlock>
Silverlight does not support these StringFormat parameters so you still have to use something like my FormatConverter class.
Example 7 (Silverlight)
<data:DataGrid>
<data:DataGrid.Columns>
<data:DataGridTemplateColumn Header="Order date">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding OrderDate,
Converter={StaticResource FormatConverter}, ConverterParameter='{0:dd-MMM-yyyy}'}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<basics:DatePicker
SelectedDate="{Binding OrderDate, Mode=TwoWay}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
</data:DataGrid.Columns>
</data:DataGrid>
ImageBytesConverter
I would also like to refer to one of my older value converters. The ImageBytesConverter which I demonstrated in my WPF treeviews en LINQ to SQL article can be used to convert the Photo in the Employee table of the Northwind database to a WPF BitmapImage. Following value converter has been modified so it works with WPF (BMP, JPG, PNG) and Silverlight (JPG & PNG).
/// <summary>
/// WPF/Silverlight ValueConverter : Convert Photo (=stream of bytes) to BitmapImage
/// </summary>
public class ImageBytesConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
BitmapImage bitmap = new BitmapImage();
if (value != null)
{
byte[] photo = (byte[])value;
MemoryStream stream = new MemoryStream();
stream.Write(photo, 0, photo.Length);
#if SILVERLIGHT
bitmap.SetSource(stream);
#else
bitmap.BeginInit();
bitmap.StreamSource = stream;
bitmap.EndInit();
#endif
}
return bitmap;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
EnumValueToDescriptionConverter & ResourceKeyToResourceConverter
Furthermore I would like to refer to the WPF library from Josh Smith which contains 2 additional value converters; EnumValueToDescriptionConverter and ResourceKeyToResourceConverter.
Piping value converters
Finally I would like to recommand an article from Josh Smith about piping value converters. Josh implemented a powerful ValueConverterGroup class which makes it possible to combine different value converters. This ValueConverterGroup only works in WPF and all value converters should be decorated with the ValueConversion attribute.
Example 8
Next example will demonstrate how to combine two of my value converters. First the Count of a collection is compared with value 0 by using the IsGreaterThanConverter. Then this boolean return value is passed to the second value converter VisibilityConvert which will return Visibility.Visible or Visibility.Collapsed.
First you have to declare a ValueConvertGroup as a resource. The sequence is important and it is only possible to specify ConvertParameters for the first value converter in the group.
<convert:ValueConverterGroup x:Key="ValueConverterGroup">
<convert:IsGreaterThanConverter />
<convert:VisibilityConverter />
</convert:ValueConverterGroup>
Just handle a ValueConverterGroup like a regular ValueConverter when databinding:
<TextBlock Visibility="{Binding Path=Categories.Count,
Converter={StaticResource ValueConverterGroup},
ConverterParameter=0}">Categories</TextBlock>
Summary
As you can see, all these value converters are really quite simple. Because they are generic, you can re-use them in every WPF/Silverlight project. So, copy the source and include them in your own library. I hope you can take advantage of this free code. If you have any remarks or suggestions, please let me know.