Site Logo

Pixel-in-Gene

Exploring Frontend Development with Design / Graphics / Technology

IronPython is the new IValueConverter, IMultiValueConverter

Applying simple mathematical operations inside WPF DataBindings has always been a non-trivial task. By default the {Binding} or MultiBinding syntax does not allow the use of arbitrary expressions inside the Path. They are restricted to the dot-syntax of referencing properties on an object, possibly with a chain of references, something like Person.Phones[0].Phone.Number. Simple expressions like Person.Age + 1 are disallowed.

There are several cases where you would like to do a little bit of math inside of the {Binding} expressions. Say you wanted to translate an Ellipse based on the (Time, Value) pair of the DataPoint. However the Time and Value properties have values between [0,1]. So you cannot assign the (Time, Value) pair to Canvas.Left, Canvas.Top properties of the Ellipse. Instead you would have to do something like

Canvas.Left = dp.Time * canvas.ActualWidth,

Canvas.Top = dp.Value * canvas.ActualHeight

where canvas is the instance of the Canvas element, dp is the instance of DataPoint.

This requires the use of a converter that would do the actual mapping of values to the bounds of the Canvas. Writing converters is not terribly difficult but requires unnecessary effort. If you have myriad of math expressions to evaluate, you could soon have a MathConverter, say, that would have a huge switch-case for the different expressions. Then in your binding syntax you would pass the exact expression that you want to use with a ConverterParameter. After a period of time, it is plain frustrating and increases the readability cost of the Xaml file.

The solution space

There are already several possible solutions to this problem and each of them adopt some kind of a DSL syntax to specify the expression. However that requires custom parsing and validation and also adds a maintenance cost.

Ideally you would want the complete flexibility of a scripting language inside of your Binding expressions. Note however that there is a tendency to over-use / abuse a scripting functionality. That however is for the team to decide what level of sanity needs to be maintained in the script. With judicious usage, a scripting language for evaluating binding expressions is supremely powerful. As of today, IronPython is a great choice for exposing scripting functionality.

The ScriptConverter

ScriptConverter is a custom converter that I implemented that evaluates python expressions and statements. It hosts an IronPython engine and evaluates scripts specified inside the binding expression. Note that it is just a class that implements both IValueConverter and IMultiValueConverter. It is not a MarkupExtension or a custom Binding class, although one could certainly do both. The decision to implement it as a converter rather than a markup-extension was purely to keep the well-known {Binding} and MultiBinding syntax in your Xaml files.

To use the ScriptConverter, you would write the usual Binding expression and use the Converter property to point to an instance of the ScriptConverter. The expression to evaluate is specified in the ConverterParameter.

<Polyline Stroke="Black" Stretch="Fill">
  <Polyline.Points>
    <MultiBinding Converter="{StaticResource ScriptConverter}"
              ConverterParameter="...written below...">
            <Binding Path="Items"
                     Source="{StaticResource DS}" />
            <Binding Path="ActualWidth"
                     ElementName="ChartHost" />
            <Binding Path="ActualHeight"
                     ElementName="ChartHost" />
      </MultiBinding>
   </Polyline.Points>
</Polyline>

<expr> = PointCollection([Point(p.Time * $1, p.Value * $2) for p in $0])

[I just wrote the expression below the Xaml, since it was too long. The actual expression would be inside quotes]

Note the use of the $0, $1, $2 placeholder variables, which point to the indexed <Binding /> tags of the MultiBinding. At runtime they are replaced by the bound values of the MultiBinding. So $0 will actually point to the instance of the Items collection, $1 = ActualWidth and $2 = ActualHeight.

Simplifying the expressions

The IronPython statement that we saw above is fairly long and can become hard to read on smaller monitors. Also you may want to break up your expression into multiple lines. To do just that, ScriptConverter also supports startup-scripts, which are text files that contain valid IronPython code. Typically it contains some imports and some functions that encapsulate long expressions and statements. For the example above, I can wrap the expression into a method like so:

def ConvertToPoints(dataPoints, width, height):
    list = PointCollection([Point(p.Time * width,
                            p.Value * height) for p in dataPoints])
    return list

Then in the binding, I could just use an expression like ConvertToPoints($0, $1, $2),making it far more readable and potentially reusable. In a fairly large application, having a single starter-script that encapsulates all the Value-conversions is certainly more maintainable that sprinkling converters throughout the Xaml files. It also gives you a chance to refactor value-conversions and encapsulate them into methods.

A stretch of mind

It is not too hard to imagine the various possibilities with embedded scripting engines like IronPython. ScriptConverter is certainly one of the many use-cases. The other use-case I am most excited about right now is designer-developer workflow. Imagine a tool that opens up next to a running WPF application, which allows you to make runtime changes using an IronPython script. You can create controls on the fly, change the look and feel, change behavior and see it all happen live! Well, you don’t have to imagine :-), because I have built a tool that does all of the above and a lot more ;-) I’ll talk about it in a later blog post.

IronPython is the new IValueConverter, IMultiValueConverter
Pavan Podila
Pavan Podila
November 11th, 2008