Chris Pietschmann

husband, father, hacker, entrepreneur, futurist, innovator, autodidact

NAVIGATION - SEARCH

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.

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

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

    </UserControl.Resources>

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

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

blog comments powered by Disqus