WPF: Embedding DLR Scripts in XAML (Python, Ruby) - Part 1, Hosting the DLR

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

The XAML framework allows us to easily switch between the Declarative and Imperative programming models easily. Examples such as event handlers are obvious cases where this happens, while others, such as MarkupExtensions and ValueConverters for Bindings are not so clear - when you implement the ProvideValue method of a custom MarkupExtension, you are suddenly in the the imperative world of C# (or CLR language of your choice) where as the XAML document that declared the MarkupExtension is declarative in nature.

The ability to dynamically load XAML using the XamlParser class allows the programmer to switch in and out of the declarative and imperative worlds with ease, affording them great power and flexibility. Or so it would seem...

Dynamically loaded XAML does not integrate with the imperative world as well as it's compiled brethren. For example, you completely lose the ability to route events to handlers when loading XAML dynamically. This greatly hampers the usefulness of dynamically loaded XAML. The stance in the community is that you should simply compile your XAML if you want to handle events. In many instances this is just not an acceptable solution!

There are a number of interpreted (hence dynamic) imperative languages that bind to the CLR, such as Python and Ruby. The new Dynamic Language Runtime (DLR) will encourage the development off new dynamic languages as well as the binding of existing dynamic languages to .NET. In this series of 6 articles I will demonstrate how to embed any and all DLR languages in XAML.

First some background reading. The dynamic nature of XAML (or lack there of) has been discussed many times in public forums. There have been attempts to embed Python and other dynamic languages (such as the Lambda Converter Extension below) into XAML, all with mixed success. Below are links to interesting documents worth reading:

Hosting the DLR is not difficult. The DLR is still under development, but my experience with it has been positive. It seems quite stable and performs well with IronPython and IronRuby.

Note: The code in this series was written against the DLR redistributed with IronPython 2.0 Beta 3. There have been some breaking changes in the DLR API since then, so code listed here may need modification to work withh later versions of the DLR.

The DLR is contained in two assemblies, Microsoft.Scripting.dll and Microsoft.Scripting.Core.dll. The easiest way to get moving is to download a compiled version of either IronPython or IronRuby as both redistribute the DLR assemblies.

Your application should reference both Microsoft.Scripting.dll and Microsoft.Scripting.Core.dll, but does not need to reference the Python or Ruby assemblies as these will be loaded on demand by the DLR. To avoid issues with locating DLLs at runtime, I suggest just dumping all these DLLs in the bin directory of your app to get moving.

The following is a simple example of hosting the DLR using C# and evaluating a simple Python expression. After execution, result is set to 11 as expected.

  1. using Microsoft.Scripting.Hosting;
  2. using Microsoft.Scripting;
  4. // create a ScriptRuntime instance
  5. ScriptRuntime runtime = ScriptRuntime.Create();
  7. // Get the engine for the target language
  8. ScriptEngine eng = runtime.GetEngine("python");
  10. // A scope is used to setup local variables - the context in
  11. // which the script will be executed.
  12. //
  13. // We add:
  14. // a = 5
  15. // b = 6
  16. ScriptScope scope = eng.CreateScope();
  17. scope.SetVariable("a", 5);
  18. scope.SetVariable("b", 6);
  20. // Create a ScriptSource object from our script:
  21. //
  22. // a + b
  23. ScriptSource source =
  24. eng.CreateScriptSourceFromString(
  25. "a + b", SourceCodeKind.Expression);
  27. // evaluate the expression - result == 11.
  28. object result = source.Execute(scope);

In the interest of clarity and brevity, little attention will be paid to performance or error handling in this series. The following utility methods will be used throughout this series:

  1. using Microsoft.Scripting.Hosting;
  2. using Microsoft.Scripting;
  4. static class DlrUtils
  5. {
  6. // We use a static ScriptRuntime as all scripts will run in
  7. // the current app domain and because all scripts will be
  8. // called from the GUI thread, so there are no treading issues.
  9. private static ScriptRuntime s_runtime = ScriptRuntime.Create();
  11. //
  12. // Execute a script of the specified language. A dictionary containing
  13. // variables to be injected into the scope before script execution
  14. // can be supplied.
  15. //
  16. static public void Execute(
  17. string language,
  18. string script,
  19. Dictionary< string, object > scopeVars )
  20. {
  21. ScriptEngine eng = s_runtime.GetEngine(language);
  22. ScriptScope scope = eng.CreateScope();
  24. // set up the scope
  25. if (scopeVars != null)
  26. {
  27. foreach (var kv in scopeVars)
  28. {
  29. scope.SetVariable(kv.Key, kv.Value);
  30. }
  31. }
  33. // execute the script
  34. if (script != null && script != "")
  35. {
  36. ScriptSource envSource =
  37. eng.CreateScriptSourceFromString(
  38. script, SourceCodeKind.Statements);
  39. envSource.Execute(scope);
  40. }
  41. }
  43. //
  44. // Evaluate an expression of the specified language. A script can be supplied
  45. // that is executed before the expression is evaluated. The script is intended to
  46. // be used to set up the environment (ie, local variables, loaded libraries, etc)
  47. // in which the expression is evaluated. A dictionary containing variables to
  48. // be injected into the scope before script execution can also be supplied.
  49. //
  50. static public object Evaluate(
  51. string language,
  52. string environmentScript,
  53. string expression,
  54. Dictionary< string, object > scopeVars )
  55. {
  56. ScriptEngine eng = s_runtime.GetEngine(language);
  57. ScriptScope scope = eng.CreateScope();
  59. // set up the scope
  60. if (scopeVars != null)
  61. {
  62. foreach (var kv in scopeVars)
  63. {
  64. scope.SetVariable(kv.Key, kv.Value);
  65. }
  66. }
  68. // execute the environment script
  69. if (environmentScript != null && environmentScript != "")
  70. {
  71. ScriptSource envSource =
  72. eng.CreateScriptSourceFromString(
  73. environmentScript, SourceCodeKind.Statements);
  74. envSource.Execute(scope);
  75. }
  77. // evaluate the expression
  78. ScriptSource source =
  79. eng.CreateScriptSourceFromString(
  80. expression, SourceCodeKind.Expression);
  81. return source.Execute(scope);
  82. }
  83. }

Using these utility methods, our original expressions of "a + b" could be written more concisely, like the following:

  1. // Alternative using our DlrUtils.
  2. Dictionary<string, object> scopeVars = new Dictionary<string,object>();
  3. scopeVars.Add( "a", 5 );
  4. scopeVars.Add( "b", 6 );
  5. object r1 = DlrUtils.Evaluate("python", null, "a + b", scopeVars);
  7. // Another alternative using our DlrUtils.
  8. object r2 = DlrUtils.Evaluate("python", "a = 5\nb = 6\n", "a + b", null);

In all of the above examples, you can simply specify "ruby" in place of "python" to execute Ruby scripts instead. As more DLR languages pop up, you can execute them too.

Now that we can host the DLR and execute scripts written in any DLR language, let's do something useful in XAML with it. See part 2.