WPF: Embedding DLR Scripts in XAML (Python, Ruby) - Part 6, A Very Dynamic Application

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

To conclude this series of posts, I will build a simple application using many of the DLR Scripting goodies presented in this series. I will use Python as the scripting language, but you could use Ruby, JScript or any other language that has bindings to the DLR.

The idea here is to write a minimalist WPF application in C#, pushing as much logic as possible into the XAML and DLR Scripts, and as you will see, we end up with very little logic in the C# code!

For this simple application, I want to create a Window that has a "Load XAML File" button and a TabControl. When the button is clicked, the user is presented with a file chooser and chooses a XAML file. The XAML file is parsed and the root object defined in the XAML file is added to a TabItem in the TabControl (it is assumed that the root object is indeed a UIElement that makes sense to stick in a TabItem). Using a TabControl lets me load multiple XAML files, each getting its own tab.

The application hosts the DLR as described in Part 1 and also defines all the DLR scripting XAML extensions from the remaining parts of the series. This allows our dynamically loaded XAML to contain DLR scripts.

Let's start with a fairly conventional application building approach. We can build the Window using XAML and code behind as follows. The XamlReader class is used to dynamically load the XAML file:

  1. <Window x:Class="EmbeddingDlrScriptInXaml.Window1"
  2. xmlns="<a href="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. ">http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4. </a> xmlns:x="<a href="http://schemas.microsoft.com/winfx/2006/xaml"
  5. ">http://schemas.microsoft.com/winfx/2006/xaml"
  6. </a> xmlns:local="clr-namespace:EmbeddingDlrScriptInXaml"
  7. Title="DLR Example" Height="600" Width="800">
  8. <DockPanel>
  9. <Button DockPanel.Dock="Top" Click="Button_Click">Load XAML File</Button>
  10. <TabControl Name="TabControl" />
  11. </DockPanel>
  12. </Window>
  1. namespace EmbeddingDlrScriptInXaml
  2. {
  3. public partial class Window1 : Window
  4. {
  5. public Window1()
  6. {
  7. InitializeComponent();
  8. }
  9.  
  10. private void Button_Click(object sender, RoutedEventArgs e)
  11. {
  12. System.Windows.Forms.OpenFileDialog dlg =
  13. new System.Windows.Forms.OpenFileDialog();
  14. dlg.Filter = "XAML FIles (*.xaml)|*.xaml";
  15.  
  16. if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
  17. {
  18. object root = System.Windows.Markup.XamlReader.Load(
  19. System.IO.File.OpenRead(dlg.FileName));
  20.  
  21. TabItem t = new TabItem();
  22. t.Header = System.IO.Path.GetFileName(dlg.FileName);
  23. t.Content = root;
  24. TabControl.Items.Add(t);
  25. TabControl.SelectedItem = t;
  26. }
  27. }
  28. }
  29. }

I can create a file called "Part_2.xaml" that shows off the concepts from Part 2 of this series:

  1. <StackPanel
  2. xmlns="<a href="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. ">http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4. </a> xmlns:x="<a href="http://schemas.microsoft.com/winfx/2006/xaml"
  5. ">http://schemas.microsoft.com/winfx/2006/xaml"
  6. </a> xmlns:system="clr-namespace:System;assembly=mscorlib"
  7. xmlns:local="clr-namespace:EmbeddingDlrScriptInXaml;assembly=EmbeddingDlrScriptInXaml">
  8.  
  9. <StackPanel Orientation="Horizontal">
  10. <TextBlock>5+6=</TextBlock>
  11. <TextBlock Text="{local:Script Language=Python, Expression=5+6}" />
  12. </StackPanel>
  13.  
  14. <TextBlock>
  15. <TextBlock.Text>
  16. <local:Script Language="Python">
  17. <local:Script.Expression>"Hello " + "World"</local:Script.Expression>
  18. </local:Script>
  19. </TextBlock.Text>
  20. </TextBlock>
  21.  
  22. <StackPanel Orientation="Horizontal">
  23. <TextBlock>factorial( 5 )=</TextBlock>
  24. <TextBlock>
  25. <TextBlock.Text>
  26. <local:Script Language="Python" Expression="factorial( 5 )">
  27. <local:Script.Script>
  28. <system:String xml:space="preserve">
  29. <![CDATA[
  30. def factorial(n):
  31. if n == 0:
  32. return 1
  33. else:
  34. return n * factorial(n-1)
  35. ]]>
  36. </system:String>
  37. </local:Script.Script>
  38. </local:Script>
  39. </TextBlock.Text>
  40. </TextBlock>
  41. </StackPanel>
  42. </StackPanel>

This file can then be loaded dynamically by our application, resulting in the following display:

There we have it - a dynamically loaded XAML file that contains embedded DRL scripts!

No, that's not the end of the story... there is still a lot that is hard coded in this application - let's start removing what little C# code we wrote and make this application truly closed for change, but open for extension (a principal that really makes you Think Bottom Up).

Let's begin by moving the button event handler code into the XAML file. This means that the code behind file in now empty:

  1. <Window x:Class="EmbeddingDlrScriptInXaml.Window1"
  2. xmlns="<a href="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. ">http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4. </a> xmlns:x="<a href="http://schemas.microsoft.com/winfx/2006/xaml"
  5. ">http://schemas.microsoft.com/winfx/2006/xaml"
  6. </a> xmlns:local="clr-namespace:EmbeddingDlrScriptInXaml"
  7. xmlns:system="clr-namespace:System;assembly=mscorlib"
  8. Title="DRL Example" Height="600" Width="800">
  9. <DockPanel>
  10. <Button DockPanel.Dock="Top" Content="Load XAML File"
  11. CommandParameter="{Binding ElementName=TabControl}">
  12. <Button.Command>
  13. <local:ScriptCommand Language="Python">
  14. <local:ScriptCommand.Script>
  15. <system:String xml:space="preserve">
  16. <![CDATA[
  17. import clr
  18.  
  19. clr.AddReference('mscorlib')
  20. clr.AddReference('System.Windows.Forms')
  21. clr.AddReference('PresentationFramework')
  22.  
  23. from System.Windows import Forms
  24. from System.Windows import Controls
  25. from System.Windows import Markup
  26. from System import IO
  27.  
  28. dlg = Forms.OpenFileDialog();
  29. dlg.Filter = "XAML FIles (*.xaml)|*.xaml";
  30. if dlg.ShowDialog() == Forms.DialogResult.OK:
  31. root = Markup.XamlReader.Load( IO.File.OpenRead(dlg.FileName))
  32. t = Controls.TabItem();
  33. t.Header = IO.Path.GetFileName( dlg.FileName );
  34. t.Content = root;
  35. parameter.Items.Add(t);
  36. parameter.SelectedItem = t;
  37. ]]>
  38. </system:String>
  39. </local:ScriptCommand.Script>
  40. </local:ScriptCommand>
  41. </Button.Command>
  42. </Button>
  43. <TabControl Name="TabControl" />
  44. </DockPanel>
  45. </Window>

What use is a code-behind file with no code? None. So lets get rid of it completely. By that I mean remove Window1.xaml and Window1.xaml.cs from the solution - now there will be no XAML compiled into the application other than the default App.xaml file.

But what will our application display when it starts up? The StartupUri specified in App.xaml determines what is displayed at startup time. We want to set this dynamically somehow, say, from a command line parameter or an app.config setting. The first thing to do is remove the StartupUri from App.xaml:

  1. <!-- original generated code
  2. <Application x:Class="EmbeddingDlrScriptInXaml.App"
  3. xmlns="<a href="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4. ">http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  5. </a> xmlns:x="<a href="http://schemas.microsoft.com/winfx/2006/xaml"
  6. ">http://schemas.microsoft.com/winfx/2006/xaml"
  7. </a> StartupUri="Window1.xaml">
  8. </Application>
  9. -->
  10.  
  11. <!-- new code -->
  12. <Application x:Class="EmbeddingDlrScriptInXaml.App"
  13. xmlns="<a href="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  14. ">http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  15. </a> xmlns:x="<a href="http://schemas.microsoft.com/winfx/2006/xaml">
  16. </Application>
">http://schemas.microsoft.com/winfx/2006/xaml"> </Application> [/geshifil...

We have an opportunity to set the StartupUri property of the Application in the overridden Application.OnStartup method (in App.xaml.cs). I will use a command line parameter in this example to set the StartupUri (note: WPF doesn't much like relative URIs, so I use System.IO.Path.GetFullPath to make an absolute URI):

  1. namespace EmbeddingDlrScriptInXaml
  2. {
  3. public partial class App : Application
  4. {
  5. protected override void OnStartup(StartupEventArgs e)
  6. {
  7. StartupUri = new Uri(
  8. "file://" + System.IO.Path.GetFullPath( e.Args[0] ), UriKind.Absolute );
  9. }
  10. }
  11. }

To make the old Window1.xaml file dynamically loadable, there are a couple of changes that need to be made. First, remove the x:Class attribute in the root element as there is no code behind file, and second, specify the assembly for the "local" xml namespace:

  1. <Window
  2. xmlns="<a href="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. ">http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4. </a> xmlns:x="<a href="http://schemas.microsoft.com/winfx/2006/xaml"
  5. ">http://schemas.microsoft.com/winfx/2006/xaml"
  6. </a> xmlns:local="clr-namespace:EmbeddingDlrScriptInXaml;assembly=EmbeddingDlrScriptInXaml"
  7. xmlns:system="clr-namespace:System;assembly=mscorlib"
  8. Title="DRL Example" Height="600" Width="800">
  9.  
  10. ... no changes here ...
  11.  
  12. </Window>

To run the application, specify the XAML file to load on the command line. Assuming you are using the code attached to this post, you can specify "../../XamlFiles/Window.xaml" as the command line in the Debug tab of the application's properties. When you run the app, you will see the same window displayed with all the same functionality, only the while thing is dynamically loaded.

So lets see where we have gotten to with this dynamic WPF application:

  • There is no compiled XAML other than App.xaml (which does nothing and could be removed from the solution with no effect on its execution).
  • The application merely loads a XAML file as specified on the command line - that XAML file could do just about anything.
  • All event/command handling is performed by DLR scripts (in this case Python scripts) embedded in the dynamically loaded XAML!
  • We have written a dynamically loaded XAML file that in turn dynamically loads other XAML files!!

Dynamically loaded XAML has just gotten one step closer to it compiled counterpart. While the examples posted here are not complete, nor efficient and lack error checking, it is clear that, as a proof of concept, DLR languages can be embedded in dynamically loaded XAML quite easily.

The VS2008 solution attached to this post contains all the code from this blog series. To run the example app, do the following:

  1. Unzip the zip file somwhere.
  2. I have put the IronPython 2.0 Beta 3 and DLR assemblies into the bin/Debug folder in preparation for building the debug build. Copy these to the Release folder if you wish to run the release build (you really should download IronPython so you get the documentation, etc)
  3. Set the command line for the app (App Properties/Debug/Command line arguments) to "../../XamlFiles/Window.xaml"
  4. Hit F5 and you should be in business.

The examples from this series can be found in XamlFiles/Part_n.xaml.

The code presented here is a cut down version of a more complete and robust library that we (ie, Think Bottom Up) use in commercial work. If there is sufficient interest, we will consider making this library available in some way. If you are interested, please contact me (daniel.paull at thinkbottomup.com.au) to discuss.

It's been fun - I hope it's also been useful!

AttachmentSize
Package icon EmbeddingDlrScriptInXaml.zip873.05 KB