Поделиться через


ArithmeticConverter

Using a declarative language like XAML to define a UI feels like the right thing to do, but it has its drawbacks.  I appreciate that disallowing procedural code eliminates race conditions and side effects, but what about purely functional operations like simple arithmetic?  The lack of simple math operations gets even more frustrating since we have such an awesome data binding language but no way apply it to mathematical formulas.  Wouldn’t it be great if we could assign a property using a <MultiBinding> that combines a bunch of individual <Binding>s by applying simple operators like +, -, *, /, or even ^ to them?

XAML doesn’t allow this natively, so I created a class called ArithmeticConverter as one of the first types in Presentation.More.  With ArithmeticConverter you can do exactly as I described above by giving the operations in the ConverterParameter.  It implements both IValueConverter and IMultiValueConverter, meaning you can use it either in an inline {Binding} or in a multi-line <MultiBinding>.

The first scenario only supports two operators: +, which returns converts the given value to its absolute value, and -, which returns the negative of the value:

 public object Convert(object value, Type targetType,
                      object parameter, CultureInfo culture)
{
    var operations = parameter as string ?? "";

    var result = System.Convert.ToDouble(value, culture);
    for (int o = 0; o < operations.Length; o++)
    {
        result = Interpret(operations[o], result);
    }
    return result;
}
 private static double Interpret(char operation, double operand)
{
    switch (operation)
    {
        case '+': return Math.Abs(operand);
        case '-': return -operand;
    }
}

If you look closely you’ll see that you can also specify more than one operator at once.  For example, setting ConverterParameter="+-" would always give you a negative value regardless of whether the input was positive or negative.

The second scenario supports +, -, *, /, %, and ^, which represent simple addition, subtraction, multiplication, division, modulus, and power:

 public object Convert(object[] values, Type targetType,
                      object parameter, CultureInfo culture)
{
    var operations = parameter as string ?? "";

    if (operations.Length != values.Length - 1)
    {
        throw new ArgumentException(
        "The number of arithmetic operators (" + operations.Length + ")
        does not match the number of values (" + values.Length + ").",
        "parameter");
    }

    var result = System.Convert.ToDouble(values[0], culture);
    for (int o = 0; o < operations.Length; o++)
    {
        var operand = System.Convert.ToDouble(values[o + 1], culture);
        result = Interpret(operations[o], result, operand);
    }
    return result;
}
 private static double Interpret(char operation,
                                double operand1,
                                double operand2)
{
    switch (operation)
    {
        case '+': return operand1 + operand2;
        case '-': return operand1 - operand2;
        case '*': return operand1 * operand2;
        case '/': return operand1 / operand2;
        case '%': return operand1 % operand2;
        case '^': return Math.Pow(operand1, operand2);
    }
}

Most error checking is omitted for brevity, but I’m intentionally showing you the check which enforces that the number of operators must match the number of values.  I actually left off this check at first, and if you supplied way more values than operators then I just kept applying the last operator repeatedly, but it quickly became too hard to debug my XAML when values weren’t coming out as expected because I forgot to match the number of operators with the operands.

The most common way of using value converters in XAML is to create an instance of your converter in a <ResourceDictionary>, and then refer to it like {Binding Converter={StaticResource MyConverter}}.  But I don’t like this for two reasons.  First, it doesn’t feel right that I have to create an instance of an inherently static class (after all, most converters don’t store any state, so the might as well be defined as static).  Second, I don’t like declaring the converter in a separate part of the XAML that’s so far away from the actual {Binding} that’s using it.  Not only does it make the code harder to read, but it makes it harder to cut-and-paste the {Binding} code to another file, since you have to remember to take the converter with it.  For example, here is how you’d normally use an instance of ArithmeticConverter to show the negative of a <Slider>‘s value:

 <Window xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
    <Window.Resources>
        <ArithmeticConverter x:Key="MyArithmeticConverter"/>
    </Window.Resources>
    <StackPanel>
        <Slider x:Name="MySlider"/>
        <TextBox Text="{Binding Path=Value, ElementName=MySlider, 
                        Converter={StaticResource MyArithmeticConverter}, 
                        ConverterParameter=-}"/>
    </StackPanel>
</Window>

To get around these issues in all of my custom converters that I add to Presentation.More, I always create a public static readonly Default property that returns a singleton instance of the converter.  This follows the pattern of Comparer.Default, which is another case where you need an instance of a class to represent a fundamentally static function (see my post on Comparator for more on this topic).  So by using the {x:Static} markup, you can use the converter directly like this:

 <Window xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
    <StackPanel>
        <Slider x:Name="MySlider"/>
        <TextBox Text="{Binding Path=Value, ElementName=MySlider, 
                        Converter={x:Static ArithmeticConverter.Default}, 
                        ConverterParameter=-}"/>
    </StackPanel>
</Window> 

And for a similar example using a <MultiBinding>, you could use it like this to compute the size (area) of an element:

 <Window xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
   <MultiBinding Converter="{x:Static ArithmeticConverter.Default}"
                 ConverterParameter="*">
       <Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}"/>
       <Binding Path="ActualHeight" RelativeSource="{RelativeSource Self}"/>
   </MultiBinding>
</Window>

I’ve attached the full ArithmeticConverter class with error checking to this post.  Enjoy!

ArithmeticConverter.cs