Silverlight: Embed IronRuby/DLR Scripting within XAML using IValueConverter and Custom UserControl

After I wrote the “Intro to IronRuby/DLR Scripting in C# Silverlight 4 Application” post, I came across an interesting series on embedding DLR scripts in XAML with WPF. This is an interesting series, although the code doesn’t run in Silverlight, due to the fact that Silverlight is only a subset of WPF and doesn’t support the System.Windows.Markup.MarkupExtension class. I test out a couple things in Silverlight, and I was able to get similar DLR scripting functionality working under Silverlight using a combination of a simple, custom IValueConverter and a custom UserControl class.
If you are unfamiliar with value converters and the IValueConverter interface (same for both Silverlight and WPF), you may want to look it up and learn how to create your own. Value converters are tremendously helpful when performing data binding. ## Embedded IronRuby within XAML
To start, here’s a few examples of IronRuby code embedded within a custom IValueConverter and custom UserControl. As you’ll see, this can be very effective to embed scripting to define how to display the values bound, or to manipulate the content of the custom user control. Although, you must keep in mind that this is not limited to simple tasks such as these. You have the full power of the DLR and .NET within the DLR scripts being executed, so there really is no limit to what could be coded within. <pre class="csharpcode">«/span>UserControl x:Class=”SLXamlEmbeddedScript.MainPage” xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation” xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml” xmlns:d=”http://schemas.microsoft.com/expression/blend/2008” xmlns:mc=”http://schemas.openxmlformats.org/markup-compatibility/2006”

<span class="attr">xmlns:system</span><span class="kwrd">="clr-namespace:System;assembly=mscorlib"</span>
<span class="attr">xmlns:local</span><span class="kwrd">="clr-namespace:SLXamlEmbeddedScript"</span>
         
<span class="attr">mc:Ignorable</span><span class="kwrd">="d"</span>
<span class="attr">d:DesignHeight</span><span class="kwrd">="300"</span> <span class="attr">d:DesignWidth</span><span class="kwrd">="400"</span>
<span class="attr">x:Name</span><span class="kwrd">="root"</span> <span class="attr">DataContext</span><span class="kwrd">="test"</span><span class="kwrd">></span>
<span class="kwrd"><</span><span class="html">UserControl.Resources</span><span class="kwrd">></span>
    <span class="rem"><!-- This converter uses a Ruby object's 'add' method</span> <span class="rem">        to add 5 plus 5 and return the results</span> <span class="rem">        --></span>
    <span class="kwrd"><</span><span class="html">local:DLRScriptValueConverter</span> <span class="attr">x:Key</span><span class="kwrd">="FivePlusFiveConverter"</span> <span class="attr">xml:space</span><span class="kwrd">="preserve"</span><span class="kwrd">></span>
        class AddFive
            def add
                5 + 5
            end
        end
        a = AddFive.new
        a.add
    <span class="kwrd"></</span><span class="html">local:DLRScriptValueConverter</span><span class="kwrd">></span>

    <span class="rem"><!-- This converter uses the value being bound via data binding,</span> <span class="rem">        passed through to IronRuby as 'ConverterValue', and generates a custom</span> <span class="rem">        value to return. In this case it concatenates the FirstName and LastName</span> <span class="rem">        properties --></span>
    <span class="kwrd"><</span><span class="html">local:DLRScriptValueConverter</span> <span class="attr">x:Key</span><span class="kwrd">="GetFullNameConverter"</span> <span class="attr">xml:space</span><span class="kwrd">="preserve"</span><span class="kwrd">></span>
        ConverterValue.FirstName + " " + ConverterValue.LastName
    <span class="kwrd"></</span><span class="html">local:DLRScriptValueConverter</span><span class="kwrd">></span>

<span class="kwrd"></</span><span class="html">UserControl.Resources</span><span class="kwrd">></span>

<span class="kwrd"><</span><span class="html">StackPanel</span> <span class="attr">x:Name</span><span class="kwrd">="LayoutRoot"</span> <span class="attr">Background</span><span class="kwrd">="White"</span><span class="kwrd">></span>
    
    <span class="rem"><!-- Bind using the FivePlusFiveConverter defined above --></span>
    <span class="kwrd"><</span><span class="html">TextBlock</span> <span class="attr">Text</span><span class="kwrd">="{Binding Converter={StaticResource FivePlusFiveConverter}}"</span><span class="kwrd">></</span><span class="html">TextBlock</span><span class="kwrd">></span>
    
    <span class="rem"><!-- Bind using the GetFullNameConverter defined above --></span>
    <span class="kwrd"><</span><span class="html">TextBlock</span> <span class="attr">Text</span><span class="kwrd">="{Binding ElementName=root, Converter={StaticResource GetFullNameConverter}}"</span><span class="kwrd">></</span><span class="html">TextBlock</span><span class="kwrd">></span>
    
    <span class="rem"><!-- Use DLRScriptUserControl (custom UserControl) to embed</span> <span class="rem">        IronRuby code within XAML and execute it on the Content of</span> <span class="rem">        the control. --></span>
    <span class="kwrd"><</span><span class="html">local:DLRScriptUserControl</span> <span class="attr">x:Name</span><span class="kwrd">="testusercontrol"</span><span class="kwrd">></span>
        <span class="kwrd"><</span><span class="html">local:DLRScriptUserControl.Script</span><span class="kwrd">></span>
            <span class="kwrd"><</span><span class="html">system:String</span> <span class="attr">xml:space</span><span class="kwrd">="preserve"</span><span class="kwrd">></span>
                Ctrl.FindName('txtName').Text = 'Hello from IronRuby'
            <span class="kwrd"></</span><span class="html">system:String</span><span class="kwrd">></span>
        <span class="kwrd"></</span><span class="html">local:DLRScriptUserControl.Script</span><span class="kwrd">></span>
        <span class="kwrd"><</span><span class="html">local:DLRScriptUserControl.Content</span><span class="kwrd">></span>
            <span class="kwrd"><</span><span class="html">TextBlock</span> <span class="attr">x:Name</span><span class="kwrd">="txtName"</span><span class="kwrd">></span>Default<span class="kwrd"></</span><span class="html">TextBlock</span><span class="kwrd">></span>
        <span class="kwrd"></</span><span class="html">local:DLRScriptUserControl.Content</span><span class="kwrd">></span>
    <span class="kwrd"></</span><span class="html">local:DLRScriptUserControl</span><span class="kwrd">></span>
    
<span class="kwrd"></</span><span class="html">StackPanel</span><span class="kwrd">></span> <span class="kwrd"></</span><span class="html">UserControl</span><span class="kwrd">></span></pre>

One thing to not about the above usage code, is that you must use xml:space=”preserve” when embeding the IronRuby/DLR scripts. This will ensure the line breaks are preserved in the resulting string. Without it the code will not run, since the IronRuby syntax depends on those line breaks.

DLRScriptValueConverter - IValueConverter

This value converter is really simple, and allows you to specify ConvertScript and ConvertBackScript to allow the value converter to convert in two way mode.

Also, as you’ll see from the code, you could make this value converter work with both IronRuby and IronPython, as the Language property suggests. Although, for this simple example, the only supported language is IronRuby. To support IronPython it’s just a matter of instantiating the appropriate ScriptEngine class to Execute the script with.

using System;
using System.Windows.Data;
using System.Windows.Markup;
using Microsoft.Scripting.Hosting;

namespace SLXamlEmbeddedScript
{
    [ContentProperty("ConvertScript")]
    public class DLRScriptValueConverter : IValueConverter
    {
        public DLRScriptValueConverter()
        {
            this.Language = "IronRuby";
        }

        public string Language { get; set; }
        public string ConvertScript { get; set; }
        public string ConvertBackScript { get; set; }

        #region IValueConverter Members

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (string.IsNullOrEmpty(this.Language))
            {
                throw new InvalidOperationException("DLRScriptValueConverter.Language property must be set");
            }
            
            if (string.IsNullOrEmpty(this.ConvertScript))
            {
                throw new InvalidOperationException("parameter or DLRScriptValueConverter.ConvertScript property must be contain a value");
            }

            if (this.Language.ToLowerInvariant() != "ironruby")
            {
                throw new InvalidOperationException(string.Format("Unsupported DLR Language ({0}). Currently only IronRuby is supported.", this.Language));
            }

            // Create Ruby ScriptEngine
            ScriptEngine engine = IronRuby.Ruby.CreateEngine();

            // Make the "value" to be converted available to the DLR
            engine.Runtime.Globals.SetVariable("ConverterValue", value);

            // Execute the script and return its result
            return engine.Execute(this.ConvertScript);
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (string.IsNullOrEmpty(this.Language))
            {
                throw new InvalidOperationException("DLRScriptValueConverter.Language property must be set");
            }

            if (string.IsNullOrEmpty(this.ConvertBackScript))
            {
                throw new InvalidOperationException("parameter or DLRScriptValueConverter.ConvertBackScript property must be contain a value");
            }

            if (this.Language.ToLowerInvariant() != "ironruby")
            {
                throw new InvalidOperationException(string.Format("Unsupported DLR Language ({0}). Currently only IronRuby is supported.", this.Language));
            }

            // Create Ruby ScriptEngine
            ScriptEngine engine = IronRuby.Ruby.CreateEngine();

            // Make the "value" to be converted available to the DLR
            engine.Runtime.Globals.SetVariable("ConverterValue", value);

            // Execute the script and return its result
            return engine.Execute(this.ConvertBackScript);
        }

        #endregion
    }
}

DLRScriptUserControl – Custom UserControl

Just like the value converter above, the DLRScriptUserControl control is really simple. It inherits from UserControl to allow you to set the controls Content property to the XAML you want it to display, and implements a Script property that allows you to define the IronRuby script to execute.

When the IronRuby script is executed during the controls Loaded event, the control passes IronRuby a global variable named ‘Ctrl’ that contains a reference to the DLRScriptUserControl object itself. Like the XAML example above, this allows you to execute any code you want against the control once it is Loaded.

using System.Windows;
using System.Windows.Controls;
using IronRuby;
using Microsoft.Scripting.Hosting;

namespace SLXamlEmbeddedScript
{
    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();
            
            // 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());
        }
    }
}

Conclusion

The above code is rather simple, and not meant for production use. It is just a basic prototype framework of how you might embed some DLR scripting within a Silverlight application. One thing I plan on looking into next (and I may or may not blog about it) is using the XamlReader class to dynamically load the XAML and its embedded DLR script at runtime. The combination of both would allow you to build some very simple plugin-like functionality into your applications.

Hope this helps someone.

**Continued Here: **Silverlight: Embed IronRuby within XAML Part 2