Using WPF Converters to Affect Multiple Properties
I’m working on my first WPF application. I’ve done a ton of reading on WPF and it’s helped me in my understandings of how WPF works. So far, I’m really liking the data binding flexibility WPF gives you. That, along with a very testable MVVM-based architecture, has made the experience a fun one!
Yesterday, I came across a situation where I needed a single item from my view model (an enum value) to affect two properties on a WPF element (the Foreground and Background colors of a DataGridCell). I’m familiar with value converters, but they only convert to a single value. I did some research on MultiValueConverters, but, as seasoned WPF developer Anne Marsan pointed out, they take multiple inputs but still only produce a single output. How to proceed?
NOTE: I’ve provided sample code in a BitBucket project here. Each “attempt” that I detail below is tagged in the repository so you can easily see the implementations of each approach by switching your working directory to a certain tag.
First Attempt (Tag: Attempt1)
My first attempt was brute force and worked, but was messy – I simply created two value converters: both would take my enum, but one would return the background color and one would return the foreground color.
[ValueConversion(typeof(CompareResult), typeof(String))]
class ForeColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var result = (CompareResult)value;
var ci = ColorInformation.FromResult(result);
return ci.ForeColor;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
[ValueConversion(typeof(CompareResult), typeof(String))]
public class BackColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var result = (CompareResult)value;
var ci = ColorInformation.FromResult(result);
return ci.BackColor;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Background" Value="{Binding CompareResult, Converter={StaticResource BackColorConverter}}"></Setter>
<Setter Property="Foreground" Value="{Binding CompareResult, Converter={StaticResource ForeColorConverter}}"></Setter>
</Style>
This was too much code duplication and I didn’t like it. But, I kept it around it as my fallback measure if I couldn’t find a more elegant solution.
Second Attempt (Tag: Attempt2)
Why mess with converters? Why not just expose the ForeColor and BackColor as properties on my view model. Simple data binding would make this effortless.
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Background" Value="{Binding ForeColor}"></Setter>
<Setter Property="Foreground" Value="{Binding BackColor}"></Setter>
</Style>
But I didn’t like this idea – it sounds too much like I’m mixing my UI and my view model. The view model (in my opinion) should just represent the data to be displayed. It’s up to the view to decide how this data should be displayed.
Third Attempt (No Tag)
Before I had talked with Anne Marsan about the MultiValueConverters, I thought about creating a multivalue converter that would take both the framework element I want to affect as well as my enum value and do the property setting right inside the converter. This was too complicated and it would have limited my converter to only working against a certain framework element (a DataGridCell in this case). I never did any coding for this and therefore there is no “Attempt3” tag.
Final Solution (Tag: Attempt4)
Then I learned about the ConvertParameter. This allows you to pass an additional parameter to an IValueConverter. I decided I could pass in a flag to indicate whether I wanted the foreground color or the background color.
[ValueConversion(typeof(CompareResult), typeof(String))]
public class ColorInformationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var result = (CompareResult)value;
var ci = ColorInformation.FromResult(result);
return parameter.ToString().Equals("forecolor", StringComparison.CurrentCultureIgnoreCase)
? ci.ForeColor
: ci.BackColor;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Background" Value="{Binding CompareResult, Converter={StaticResource ColorConverter}, ConverterParameter='backcolor'}"></Setter>
<Setter Property="Foreground" Value="{Binding CompareResult, Converter={StaticResource ColorConverter}, ConverterParameter='forecolor'}"></Setter>
</Style>
This solutions gets me to a single converter, is relatively clean in the XAML (I think) and doesn’t pollute my view model with view concerns.
At the end of August, I’ll be attending a ZeroToSilverlight training session and hope to learn more about doing these types of things in XAML. Perhaps I’ll revisit this post if I find a better way to do this. Is there a better way to do this? Comments welcomed.