Silverlight: Embed IronRuby within XAML Part 2

In the previous post I explored the possibility of embedding IronRuby / DLR Script within XAML using both an IValueConverter and Custom UserControl. I spent a little more time experimenting with the DLRScriptUserControl I posted, and came up with some small modifications to it that allow UI event handlers to be wired up using a DLR language (such as IronRuby. ## Loading Assemblies Into the ScriptEngine.Runtime Context
Something nice with how the ScriptEngine class allows you to execute a script of IronRuby (or other DLR language) is that it requires the implementer of the ScriptEngine (not the DLR script writer) to call “ScriptEngine.Runtime.LoadAssembly” in order to make a certain CLR assembly accessible within the DLR script that gets executed. This allows the scripts to be run in a sort of sandboxed environment.
There are two ways of doing this: <pre class="csharpcode">// One: Load the assemblies of known types you want to // make available within the script. // Load System.Windows engine.Runtime.LoadAssembly(typeof(UserControl).Assembly); // Load System.Windows.Browser engine.Runtime.LoadAssembly(typeof(HtmlPage).Assembly);

// Two: Load assemblies using thier Full Name engine.Runtime.LoadAssembly( Assembly.Load(“System.Windows, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e”) ); engine.Runtime.LoadAssembly( Assembly.Load(“System.Windows.Browser, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e”) );</pre>

The first method is easiest when the assembly you wish to load is already referenced by the host application. It also allows you to easily support being able to migrate your application to a newer version of the framework since it will load the version of the assembly that your application is compiled for, rather than the specific version stated in the full assembly name.

Loading Assemblies within DLRScriptUserControl

Here’s a modified version of DLRScriptUserControl from the previous post that includes the above snippets to load both “System.Windows” and “System.Windows.Browser” assemblies into the ScriptEngine.Runtime context.

public class DLRScriptUserControl : UserControl
{
    public object Script { get; set; }

    public DLRScriptUserControl()
    {
        this.Loaded += new RoutedEventHandler(DLRScriptUserControl_Loaded);
    }

    void DLRScriptUserControl_Loaded(object sender, RoutedEventArgs e)
    {
        // Create IronRuby Engine
        ScriptEngine engine = Ruby.CreateEngine();

        // Load necessary assemblies for event handling, etc.
        // Load System.Windows
        engine.Runtime.LoadAssembly(typeof(UserControl).Assembly);
        // Load System.Windows.Browser
        engine.Runtime.LoadAssembly(typeof(HtmlPage).Assembly);
            
        // Give IronRuby access to this control via a variable named 'Ctrl'
        engine.Runtime.Globals.SetVariable("Ctrl", this);
            
        // Execute the code
        engine.Execute(this.Script as string, engine.CreateScope());
    }
}

Handle UI Event within Embedded IronRuby using DLRScriptUserControl

Now that the “System.Windows” and “System.Windows.Browser” assemblies are loaded and accessible to the DLR script that’s executed (in this case IronRuby) we can use their namespaces within code.

The below example demonstrates how to:

  1. Get references to the controls created within the xaml code for the control
  2. Set the Text property of the TextBlock in the control
  3. Attach an event handler to the Button control’s Click event

I have also included a second example (commented out) of using IronRuby to add an event handler to the Click event in case you are interested in seeing two methods of accomplishing this.

<local:DLRScriptUserControl x:Name="testusercontrol">
    <local:DLRScriptUserControl.Script>
        <system:String xml:space="preserve">
            # init variables that point directly to the controls
            # within the control
            txtName = Ctrl.FindName('txtName')
            btnRubyButton = Ctrl.FindName('btnRubyButton')
                    
            # Set some text to the txtName TextBlock control
            txtName.Text = 'Hello from IronRuby'
                    
            # Add Click event handler to the button
            btnRubyButton.click do |s, e|
                System::Windows::Browser::HtmlPage.Window.Alert('Hello from IronRuby')
            end
                    
                    
            # This method of handling the event works, but the previous is easier to read
            # def btnRubyButton_Click(s,e)
            #     System::Windows::Browser::HtmlPage.Window.Alert('Hello from IronRuby!')
            # end
            # Ctrl.FindName('btnRubyButton').click.add method(:btnRubyButton_Click)
        </system:String>
    </local:DLRScriptUserControl.Script>
    <local:DLRScriptUserControl.Content>
        <StackPanel>
            <TextBlock x:Name="txtName">Default</TextBlock>
            <Button x:Name="btnRubyButton" Content="IronRuby Button"></Button>
        </StackPanel>
    </local:DLRScriptUserControl.Content>
</local:DLRScriptUserControl>

As you can see, by including these assemblies (or any others you want/need) you can easily turn the DLR script used for the control (as in the previous article) from providing very basic business logic, to being a full code behind for the control.

Dynamically Load the Entire Control (XAML + Script) at Runtime

Being able to embed IronRuby (or any DLR language) within XAML is nice, but what’s the benefit if it is still compiled as a resource within the assembly. In most cases you could use C# (or VB.NET) to accomplish the same results since everything ends up compiled within the same assembly.

To make things much more dynamic, by allowing the XAML it’s DLR script to be loaded at runtime, you can use the XamlReader to parse/load the entire thing at runtime. This would allow you to store your customized XAML + IronRuby within a database or some other file and load it at runtime. By loading this all at runtime, it would allow you to make your application scriptable and each script could be modified individually without requiring you to recompile, test and deploy the entire application if all you wanted to change was one simple little script.

To modify the previous example of DLRScriptUserControl to load it from a xaml file at runtime, we’ll follow these steps:

  1. Add a ContentPresenter to the Silverlight application where the DLRScriptUserControl will be displayed.
  2. Save the XAML + Script for the DLRScriptUserControl to a file that is stored within the website hosting the Silverlight application
  3. Add code to the application (within the MainPage.Load event in this case) to download the XAML and load it into the ContentPresenter using the XamlReader class.

Step 1: Heres the ContentPresenter to add to the application

<ContentPresenter x:Name="parseXamlTest"></ContentPresenter>

Step 2: Here’s the XAML + Script to save in a file within the website

In this case we are saving it as “DLRControl.xaml.” Also it is important to note that the root element within the XAML must have namespace declarations for all the namespaces used within the XAML file.

<local:DLRScriptUserControl
            xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
            xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
            xmlns:system='clr-namespace:System;assembly=mscorlib'
            xmlns:local='clr-namespace:SLXamlEmbeddedScript;assembly=SLXamlEmbeddedScript'
            x:Name='testusercontrol'>
    <local:DLRScriptUserControl.Script>
        <system:String xml:space='preserve'>
            # init variables that point directly to the controls
            # within the control
            txtName = Ctrl.FindName('txtName')
            btnRubyButton = Ctrl.FindName('btnRubyButton')
                    
            # Set some text to the txtName TextBlock control
            txtName.Text = 'Hello from IronRuby'
                    
            # Add Click event handler to the button
            btnRubyButton.click do |s, e|
                System::Windows::Browser::HtmlPage.Window.Alert('Hello from IronRuby')
            end
                    
            # This method of handling the event works, but the previous is better
            # def btnRubyButton_Click(s,e)
            #     System::Windows::Browser::HtmlPage.Window.Alert('Hello from IronRuby!')
            # end
            # Ctrl.FindName('btnRubyButton').click.add method(:btnRubyButton_Click)
        </system:String>
    </local:DLRScriptUserControl.Script>
    <local:DLRScriptUserControl.Content>
        <StackPanel>
            <TextBlock x:Name='txtName'>Default</TextBlock>
            <Button x:Name='btnRubyButton' Content='IronRuby Button'></Button>
        </StackPanel>
    </local:DLRScriptUserControl.Content>
</local:DLRScriptUserControl>

Step 3: Download and Load the XAML at Runtime

You could place this code anywhere in the app, but for this example I just placed it within the MainPage.Load event handler in the application.

void MainPage_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
    var client = new WebClient();
    client.DownloadStringCompleted += (s, args) => {
        // Load XAML once downloaded, and set the resulting
        // UI control to the content of the ContentPresenter
        parseXamlTest.Content = XamlReader.Load(args.Result);
    };
    // Download the XAML file
    client.DownloadStringAsync(
        new System.Uri("http://localhost:9028/DLRControl.xaml")
        );
}

Conclusion

My initial goal when experimenting with the DLR was to figure out some techniques that could be used to make an application easily scriptable and allow functionality to be built as plugins that are just loaded and executed without the need to be compiled to an assembly first. While these examples provide a good start to making an application scriptable, there is still much to do yet in order to allow the script plugins to interact with each other.

**Download the Code: **