WPF: Embedding DLR Scripts in XAML (Python, Ruby) - Part 3, A DLR Value Converter

A Visual Studio 2008 Solution with the complete code listing for this series is attached to the final part.

The WPF Binging class is very powerful and its use should be encouraged. However, it can be cumbersome to do some very simple things with it, introducing a barrier to its use. One such barrier is defining a ValueConverter.

A ValueConcverter is a class that can convert the source property value of a binding before it is set on the target property. Often such conversions are simple expressions that do not warrant the creation of a new class - though this is what is done.

The first article in this series demonstrated how to host the DLR and presented some utility methods that make executing scripts simple. Here we build upon this to create a ValueConverter that evaluates a DLR expression that supports two way binding.

Let us first examine the usual method of writing a ValueConverter. Suppose you wanted to write a value converter that performs a Celsius to Fahrenheit conversion - even though the conversion is a simple expression, we have to write and compile a class to represent the conversion:

  1. public class CelsiusToFahrenheitValueConverter : IValueConverter
  2. {
  3. public object Convert(object value, Type targetType, object parameter,
  4. System.Globalization.CultureInfo culture)
  5. {
  6. double degC = System.Convert.ToDouble(value);
  7. return (9.0 / 5.0) * degC + 32;
  8. }
  9.  
  10. public object ConvertBack(object value, Type targetType, object parameter,
  11. System.Globalization.CultureInfo culture)
  12. {
  13. double degF = System.Convert.ToDouble(value);
  14. return (5.0 / 9.0) * (degF - 32);
  15. }
  16. }

Say that we want to hook up the text in two TextBox elements, one displaying Celsius and the other displaying Fahrenheit, we would want to use your converter like this:

  1. <TextBox Name="DegC">100</TextBox>
  2. <TextBox Name="DegF" Text="{Binding ElementName=DegC, Path=Text,
  3. Converter={local:CelsiusToFahrenheitValueConverter} }" />

But the "{}" notation can only be used to instantiate a MarkupExtension, not a ValueConverter. We can either write a MarkupExtension to act as a factory, use Element Property syntax or create the converter as a resource and then reference it using the StaticResource markup extension. Let's go down the static resource route as that's where Microsoft recommends we go.

  1. <SomeElement>
  2. <SomeElement.Resources>
  3. <local:CelsiusToFahrenheitValueConverter x:Key="TemperatureConverter" />
  4. </SomeElement.Resources>
  5.  
  6. ...
  7.  
  8. <TextBox Name="DegC">100</TextBox>
  9. <TextBox Name="DegF" Text="{Binding ElementName=DegC, Path=Text,
  10. Converter={StaticResource TemperatureConverter} }" />
  11. </SomeElement>

We shall now replicate this functionality using a ValueConverter that executes DLR scripts. The converter class is shown below:

  1. public class ScriptValueConverter : IValueConverter
  2. {
  3. public string Language { get; set; }
  4.  
  5. public string ForwardScript { get; set; }
  6. public string ForwardExpression { get; set; }
  7.  
  8. public string BackwardScript { get; set; }
  9. public string BackwardExpression { get; set; }
  10.  
  11. public object Convert(object value, Type targetType, object parameter,
  12. System.Globalization.CultureInfo culture)
  13. {
  14. Dictionary<string, object> scopeVars = new Dictionary<string, object>();
  15. scopeVars["value"] = value;
  16. scopeVars["targetType"] = targetType;
  17. scopeVars["parameter"] = parameter;
  18. scopeVars["culture"] = culture;
  19. return DlrUtils.Evaluate(Language, ForwardScript, ForwardExpression, scopeVars);
  20. }
  21.  
  22. public object ConvertBack(object value, Type targetType, object parameter,
  23. System.Globalization.CultureInfo culture)
  24. {
  25. Dictionary<string, object> scopeVars = new Dictionary<string, object>();
  26. scopeVars["value"] = value;
  27. scopeVars["targetType"] = targetType;
  28. scopeVars["parameter"] = parameter;
  29. scopeVars["culture"] = culture;
  30. return DlrUtils.Evaluate(Language, BackwardScript, BackwardExpression, scopeVars);
  31. }
  32. }

The class uses many of the concepts of that were introduced in Part 2, specifically the use of Script and Expression members. All of the parameters passed into the Convert and ConvertBack methods are passed to the Script and Expression and so are available to the scripts when they execute.

Using this class we can script our temperature conversion directly in XAML as follows:

  1. <SomeElement>
  2. <SomeElement.Resources>
  3. <local:ScriptValueConverter x:Key="TemperatureConverter" Language="Python"
  4. ForwardExpression="(9.0 / 5.0) * float(value) + 32"
  5. BackwardExpression="(5.0 / 9.0) * (float(value) - 32)" />
  6. </SomeElement.Resources>
  7.  
  8. ...
  9.  
  10. <TextBox Name="DegC">100</TextBox>
  11. <TextBox Name="DegF" Text="{Binding ElementName=DegC, Path=Text,
  12. Converter={StaticResource TemperatureConverter} }" />
  13. </SomeElement>

To make the whole thing easier to use, lets write a MarkupExtension that can act as a factory for the ScriptValueConverter:

  1. public class ScriptValueConverterExtension : System.Windows.Markup.MarkupExtension
  2. {
  3. public string Language { get; set; }
  4. public string ForwardScript { get; set; }
  5. public string ForwardExpression { get; set; }
  6. public string BackwardScript { get; set; }
  7. public string BackwardExpression { get; set; }
  8.  
  9. public override object ProvideValue(IServiceProvider serviceProvider)
  10. {
  11. ScriptValueConverter c = new ScriptValueConverter();
  12. c.Language = Language;
  13. c.ForwardScript = ForwardScript;
  14. c.ForwardExpression = ForwardExpression;
  15. c.BackwardScript = BackwardScript;
  16. c.BackwardExpression = BackwardExpression;
  17. return c;
  18. }
  19. }

Which removes the need for the cumbersome use of resources and gives us the following elegant result:

  1. <TextBox Name="DegC">100</TextBox>
  2. <TextBox Name="DegF" Text="{Binding ElementName=DegC, Path=Text,
  3. Converter={local:ScriptValueConverter Language=Python,
  4. ForwardExpression=(9.0 / 5.0) * float(value) + 32,
  5. BackwardExpression=(5.0 / 9.0) * (float(value) - 32) } }" />

This DLR Script Value Converter avoids a proliferation of classes that represent simple expressions and adds a dynamic aspect to XAML that is sorely missing.