WPF: Embedding DLR Scripts in XAML (Python, Ruby) - Part 2, A Simple DLR Markup Extension

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

This article demonstrates how to embed DLR scripts in XAML using a custom MarkupExtension. The scripting language can be any language supported by the DLR, such as Python or Ruby.

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 simple MarkupExtension that evaluates a DLR expression.

The purpose of a MarkupExtension is to provide a value during XAML parsing. The MarkupExtension is then substituted with this value and parsing continues. A MarkupExtension that evaluates a DLR Expression is shown below.

  1. public class ScriptExtension : System.Windows.Markup.MarkupExtension
  2. {
  3. public string Script{ get; set; }
  4. public string Expression{ get; set; }
  5. public string Language { get; set; }
  6. public override object ProvideValue(IServiceProvider serviceProvider)
  7. {
  8. return DlrUtils.Evaluate( Language, Script, Expression, null );
  9. }
  10. }

While very simple, there are two problems with the class as written:

  1. The object returned from ProvideValue might not match the property that is being set. It is possible, and extremely handy, for the MarkupExtension to do some coersion for us.
  2. The executing script might need access to the IServiceProvider that is passed into ProvideValue, so it should be passed into the script.

The full listing for the class with these enhancements is as follows:

  1. public static class MarkupUtils
  2. {
  3. public static Type GetPropertyType(object property)
  4. {
  5. if (property is System.Reflection.PropertyInfo)
  6. {
  7. return ((System.Reflection.PropertyInfo)property).PropertyType;
  8. }
  9. else if (property is System.Windows.DependencyProperty)
  10. {
  11. return ((System.Windows.DependencyProperty)property).PropertyType;
  12. }
  13. throw new Exception("Unknown property type");
  14. }
  15.  
  16. public static object Coerce(object value, IServiceProvider serviceProvider)
  17. {
  18. IProvideValueTarget target =
  19. (IProvideValueTarget)serviceProvider.GetService(
  20. typeof(IProvideValueTarget));
  21.  
  22. Type targetPropertyType = GetPropertyType(target.TargetProperty);
  23. if (targetPropertyType != null)
  24. {
  25. return System.Convert.ChangeType(value, targetPropertyType);
  26. }
  27. return null;
  28. }
  29. }
  30.  
  31. public class ScriptExtension : System.Windows.Markup.MarkupExtension
  32. {
  33. public string Script{ get; set; }
  34. public string Expression{ get; set; }
  35. public string Language { get; set; }
  36. public override object ProvideValue(IServiceProvider serviceProvider)
  37. {
  38. Dictionary< string, object > scopeVars = new Dictionary<string,object>();
  39. scopeVars["serviceProvider"] = serviceProvider;
  40. return MarkupUtils.Coerce(
  41. DlrUtils.Evaluate(Language, Script, Expression, scopeVars),
  42. serviceProvider);
  43. }
  44. }

Now we can use the ScriptExtension in our XAML files. The following will display the value "11" in a TextBlock, where "11" was calculated by evaluating the Python expression "5+6":

  1. <TextBlock Text="{local:Script Language=Python, Expression=5+6}" />

The MarkupExtension notation (that inside the braces, "{...}") limits what we can put in the expression. For example, we can not use quotes, so some expressions in some languages simply can't be written. To overcome this we use Property Element notation. The following will display the string "Hello World" in a TextBlock, the string was calculated by evaluating the Python expression "Hello " + "World":

  1. <TextBlock>
  2. <TextBlock.Text>
  3. <local:Script Language="Python">
  4. <local:Script.Expression>"Hello " + "World"</local:Script.Expression>
  5. </local:Script>
  6. </TextBlock.Text>
  7. </TextBlock>

As discussed in Part 1 of this series, it is often necessary for the expression to be evaluated in the context of some other script. This can be done by setting the Script property of the ScriptExtension. This script will inevitably be a multiline string and we will hit problems with the preservation of whitespace during XAML parsing. I have written an article on this previously and use that technique here.

The following example defines a recursive function to calculate a factorial in the Python script. The Text property of the TextBlock will be set to 120, which is the factorial of 5:

  1. <TextBlock>
  2. <TextBlock.Text>
  3. <local:Script Language="Python" Expression="factorial( 5 )">
  4. <local:Script.Script>
  5. <system:String xml:space="preserve">
  6. <![CDATA[
  7. def factorial(n):
  8. if n == 0:
  9. return 1
  10. else:
  11. return n * factorial(n-1)
  12. ]]>
  13. </system:String>
  14. </local:Script.Script>
  15. </local:Script>
  16. </TextBlock.Text>
  17. </TextBlock>

This DLR Script MarkupExtension was very simple to write, yet allows the programmer so much freedom when setting object property values. The next articles in this series will demonstrate how to use DLR scripting in other parts of WFP such as ValueConverters, Commands and Event Handlers.