Variable Value Converters
I've been asked how to create a Value Converter that you can configure the behavior of. In my Total Training series I showed you how to create a Value Converter that would return a color, Red if the value was below a hard coded value, Yellow if it's between the Red threshold and another value, and Green otherwise.
But, in that case, the Red, Yellow and Green thresholds were hard coded into the VC. What if I want a more generic Value Converter that I can specify the thresholds, and even further, what if I want to specify all the colors myself. (I.e. if the value is 100, turn Blue or something).
This can be done in WPF by creating a parameterized Value Converter, and Blend does support working with those kinds of VCs, so let's look at how to do it.
First, create the simple "Rectangle, TextBlock, Slider" combination. We're going to bring the Slider to the text of the TextBlock, and change the Fill of the Rectangle based on the value.
Here's some XAML if you want to just use what I've got:
<Grid Margin="77,60,255,0" VerticalAlignment="Top" Height="34">
<Rectangle HorizontalAlignment="Left" Margin="0,0,0,6" Width="49" Fill="#FFFFFFFF" Stroke="#FF000000" RadiusX="7.5" RadiusY="7.5"/>
<TextBlock HorizontalAlignment="Left" Margin="53,6,0,0" Width="19" Text="{Binding Path=Value, ElementName=Slider, Mode=OneWay}" TextWrapping="Wrap"/>
<Slider Margin="76,0,0,6" x:Name="Slider" LargeChange="10" Maximum="100" SmallChange="1" IsSnapToTickEnabled="True"/>
</Grid>
Ok, so now let's create the basic three-tier Value Converter. For now, we're going to say a value of 80 or higher is Green, 50 or higher is Yellow and anything else is Red. Here's the code:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Data;
using System.Windows.Media;
namespace ValueConverters
{
class ThreeTierHardCodedVC: IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
try
{
double inputValue = System.Convert.ToDouble(value);
if (inputValue >= 80)
{
return new SolidColorBrush(Colors.Green);
}
else if (inputValue >= 50)
{
return new SolidColorBrush(Colors.Yellow);
}
else
{
return new SolidColorBrush(Colors.Red);
}
}
catch (Exception e)
{
return new SolidColorBrush(Colors.White);
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new Exception("The method or operation is not implemented.");
}
#endregion
}
}
Ok, so far so good. If you databind the fill of the Rectangle to the Text of the TextBlock, and pass it through this VC, you'll see that the Rectangle changes color as you move the slider. So, the next step is how we're going to parameterize the VC. For now, I'm just going to accept a two element string, seperated by semicolons. I use semicolons instead of commas just so I don't need to worry about internationalization right now.
So, in effect, if the user passes 80;50, I'm stating that when the value is greater then 80, turn green, between 80 and 50, turn yellow, and below 50, turn red. The parameter will be passed to us (unsurprisingly) as the parameter named "parameter". We'll take it, split it into an array, and just parse those values out. To see how this works, try hooking up one slider like this using 80;50 and another at 95;5.
This is better, except that you're forced into the red/yellow/green colors. What if you want to expand on that? What if you want to be able to configure those colors yourself. To do this, I wrote a quick function that takes a string and converts it to a color. It's not too difficult to write, but if you want to use it, here it is:
private Color GetColorFromString(string input)
{
Type colorClass = typeof(Colors);
List<MethodInfo> colorMethods = new List<MethodInfo>(colorClass.GetMethods());
foreach (MethodInfo info in colorMethods)
{
string methodName = info.Name;
if (info.Name.StartsWith("get_"))
{
string actualColor = info.Name.Split('_')[1];
if (String.Compare(input, actualColor, true) == 0)
{
// We have a match!
return (Color)info.Invoke(null, null);
}
}
}
return Colors.Black;
}
This method will take any color that is in the Colors object, and return the appropriate color. (If someone knows an easier way to do this, I'd love to hear it). You will need to add System.Windows.Media, System.Windows.Data and System.Reflection to use my method. If you want more info on the Colors class, take a look at: https://msdn2.microsoft.com/en-us/library/system.windows.media.colors.aspx
Ok, so now with this, we're going to modify our VC one more time, to allow it to take three arguments. My plan is to go through the arguments one at a time, check to see if our input is greater then the first, if so, return the color, if not, go on to the second. I'm actually going to ignore the first piece of the final argument, since it will simply end up as the default. I'm going to seperate the arguments with ';', and individual pieces with a ':'. So, here's the new code:
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
try
{
string paramAsString = parameter.ToString();
string[] splitParam = paramAsString.Split(';');
string[] firstArgument = splitParam[0].Split(':');
string[] secondArgument = splitParam[1].Split(':');
string[] thirdArgument = splitParam[2].Split(':');
double firstInterval = System.Convert.ToDouble(firstArgument[0]);
Color firstColor = this.GetColorFromString(firstArgument[1]);
double secondInterval = System.Convert.ToDouble(secondArgument[0]);
Color secondColor = this.GetColorFromString(secondArgument[1]);
Color thirdColor = this.GetColorFromString(thirdArgument[1]);
double inputValue = System.Convert.ToDouble(value);
if (inputValue >= firstInterval)
{
return new SolidColorBrush(firstColor);
}
else if (inputValue >= secondInterval)
{
return new SolidColorBrush(secondColor);
}
else
{
return new SolidColorBrush(thirdColor);
}
}
catch (Exception e)
{
return new SolidColorBrush(Colors.White);
}
}
If you give THIS one a try, set the parameter to something like: 95:Azure;10:Coral;0:SeaGreen. Now we've got a VC that you can configure all three brackets, as well as the colors that go with them.
Of course, this is hideously inelegant, so I want to make one last change. Instead of hard coding and breaking the parameters apart this way, I'm going to just create a loop. This will tighten up the code, and actually allow us to go an arbitrary number of brackets. So, if you need to have 15 different brackets of different colors from Indigo to MintCream, you can do it. So, we'll pull those ugly "secondParameter" lines out, and replace it with a foreach. (I've included the entire class here, in case you want to cut and paste)
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Data;
using System.Windows.Media;
using System.Reflection;
namespace ValueConverters
{
class FullyVariableVC : IValueConverter
{
private Color GetColorFromString(string input)
{
Type colorClass = typeof(Colors);
List<MethodInfo> colorMethods = new List<MethodInfo>(colorClass.GetMethods());
foreach (MethodInfo info in colorMethods)
{
string methodName = info.Name;
if (info.Name.StartsWith("get_"))
{
string actualColor = info.Name.Split('_')[1];
if (String.Compare(input, actualColor, true) == 0)
{
// We have a match!
return (Color)info.Invoke(null, null);
}
}
}
return Colors.Black;
}
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
try
{
string paramAsString = parameter.ToString();
string[] splitParam = paramAsString.Split(';');
double inputValue = System.Convert.ToDouble(value);
foreach (string bracket in splitParam)
{
string[] bracketParameters = bracket.Split(':');
Double bracketLimit;
try
{
bracketLimit = System.Convert.ToDouble(bracketParameters[0]);
}
catch (InvalidCastException)
{
return this.GetColorFromString(bracketParameters[1]);
}
if (inputValue >= bracketLimit)
{
return new SolidColorBrush(this.GetColorFromString(bracketParameters[1]));
}
}
}
catch (Exception e)
{
return new SolidColorBrush(Colors.White);
}
return new SolidColorBrush(Colors.White);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new Exception("The method or operation is not implemented.");
}
#endregion
}
}
Give this one a fly, and set the parameter to: 95:Azure;75:HotPink;40:Blue;10:Coral;-9999:Orange.
Enjoy :).
(As always, code here is provided "As-is" with no warranty, implied or otherwise. Use at your own risk)
Comments
Anonymous
August 01, 2007
Hi Dante, I am wondering why you didn't use ColorConverter to do the trick with the colors... Like: Color color = (Color)System.Windows.Media.ColorConverter.ConvertFromString("Blue");Anonymous
August 02, 2007
Well... that would have made things a lot easier. I didn't use it, primarily because I didn't know about it. But, that would make things much easier. I'll have to try to incorporate that. Thanks for the tip! --D