WPF: Embedding DLR Scripts in XAML (Python, Ruby) - Part 5, A DLR Event Handler

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

As discussed in the previous article, GUIs are all about layout, style and wiring. WPF has the first two in spades; XAML is an excellent language for doing layout and styling of WPF GUI elements. However, sorely lacking in WFP is the ability to route commands and events in dynamically loaded XAML. This makes any attempt to build truly dynamic XAML based GUIs near on impossible as at some point you will have to handle an event, but it is hard enough to route an event to a static method let alone have a script execute in response to an event being fired!

Inspired by Josh Smith's work, I demonstrate how to use an attached property to establish a DLR script as an event handler for any WPF routed event.

The following class declares an attached property named "Handler" that can be attached to any UIElement. When the attached property is set, the OnPropertyChanged method is called; this is where the magic happens. The two arguments passed to OnPropertyChanged are:

DependencyObject:
This is set to UIElement containing the RoutedEvent that we wish to attach to.
DependencyPropertyChangedEventArgs:
This contains the previous and new values to set for the Attached Property. The intention is to set the Attached Property value to a ScriptEventHandler instance.

In OnPropertyChanged we extract the information we need and wire up the Routed Event to the OnEvent method where the script is executed. Now every time the event fires, the script is executed!

  1. public class ScriptEventHandler
  2. {
  3. #region Attached Property
  4. public static ScriptEventHandler GetHandler(DependencyObject obj)
  5. {
  6. return (ScriptEventHandler)obj.GetValue(HandlerProperty);
  7. }
  8.  
  9. public static void SetHandler(DependencyObject obj, ScriptEventHandler value)
  10. {
  11. obj.SetValue(HandlerProperty, value);
  12. }
  13.  
  14. public static readonly DependencyProperty HandlerProperty =
  15. DependencyProperty.RegisterAttached(
  16. "Handler",
  17. typeof(ScriptEventHandler),
  18. typeof(ScriptEventHandler),
  19. new UIPropertyMetadata(OnPropertyChanged));
  20.  
  21. private static void OnPropertyChanged(DependencyObject d,
  22. DependencyPropertyChangedEventArgs e)
  23. {
  24. UIElement uie = d as UIElement;
  25. if (uie == null)
  26. {
  27. throw new Exception("Attempt to set EventHandler on non-UIElement Type");
  28. }
  29.  
  30. ScriptEventHandler oldHandler = e.OldValue as ScriptEventHandler;
  31. ScriptEventHandler newHandler = e.NewValue as ScriptEventHandler;
  32.  
  33. // unhook the old event
  34. if (oldHandler != null)
  35. {
  36. uie.RemoveHandler(oldHandler.RoutedEvent,
  37. new RoutedEventHandler(oldHandler.OnEvent));
  38. }
  39.  
  40. // hook up the new
  41. if (newHandler != null)
  42. {
  43. uie.AddHandler(newHandler.RoutedEvent,
  44. new RoutedEventHandler(newHandler.OnEvent));
  45. }
  46. }
  47. #endregion
  48.  
  49. public RoutedEvent RoutedEvent { get; set; }
  50. public string Script { get; set; }
  51. public string Language { get; set; }
  52.  
  53. private void OnEvent(object sender, RoutedEventArgs e)
  54. {
  55. Dictionary<string, object> scopeVars = new Dictionary<string, object>();
  56. scopeVars["sender"] = sender;
  57. scopeVars["e"] = e;
  58. DlrUtils.Execute(Language, Script, scopeVars);
  59. }
  60. }

Using this in XAML is quite straight forward. Here a Button is used to increment an integer stored in the Text property of a TextBlock. The increment is performed by a Python script. The Button.Click event of the button is routed to the script by using our ScriptEventHandler.Handler attached property. Note that because the script is more than one line long, we have to do the whitespace preservation dance.

  1. <Button Content="Increment using event">
  2. <local:ScriptEventHandler.Handler>
  3. <local:ScriptEventHandler Language="Python" RoutedEvent="Button.Click">
  4. <local:ScriptEventHandler.Script>
  5. <system:String xml:space="preserve">
  6. <![CDATA[
  7. tb = sender.FindName( "TheEventTarget" )
  8. tb.Text = str( int(tb.Text) + 1 )
  9. ]]>
  10. </system:String>
  11. </local:ScriptEventHandler.Script>
  12. </local:ScriptEventHandler>
  13. </local:ScriptEventHandler.Handler>
  14. </Button>
  15. <TextBlock Name="TheEventTarget">0</TextBlock>

This technique allows dynamically loaded XAML to include scripts that handle routed events, making it one step closer to having all the features of its compiled brethren.