Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Using IronPython in WPF to Evaluate Expressions

0.00/5 (No votes)
17 Jul 2009 1  
Using IronPython in WPF to evaluate expressions

Introduction

For those who follow my articles, you may have expected a new one in my MVVM series, don't worry that is on its way very soon, but there is always time for a little article. You see I have just read a little book on IronPython and thought it a pretty cool language. What I especially liked was the fact that it can be hosted in .NET as a script. This gave me an idea, wouldn't it be cool if we could use IronPython as a script to evaluate mathematical expressions for us and return a value.

Think JavaScript's eval function, which is missing from C# (at least it is right now, it may be part of C# 4.0, who knows). So with this thought in mind, I whipped up a small proof of concept and was actually very pleased with the results, so much so I have turned it into this article.

You may be asking why would I possibly want to use a mathematical expression in WPF. Well here is one common usage, you have a UI element that you want to be as wide as its container - some magic number, this is something you could do with a standard IValueConverter, but what if you want another expression, you would need another IValueConverter implementation. That ain't cool.

So let's have a look at what I came up with.

I have come up with 2 options, a MarkupExtension and a IMultiValueConverter implementation.

You Will Need

To download and install IronPython 2.0.1 which is what I have installed and used. IronPython can be installed using the MSI installer available here.

PyExpressionExtension Markup Extension

This PyExpressionExtension could be used to evaluate a static expression where the values are not dynamic (if not bound to live values). The idea is that the user can feed in the following to the PyExpressionExtension:

  • Expression: The expression to evaluate using IronPython
  • ReturnResult: The expected return Type, which I would love to have omitted, but it seemed to not Bind and threw an exception if the correct type of object was returned to the place where the PyExpressionExtension was used. Who knows some bright spark out there may solve this issue.
  • ExpressionParams: The expression parameters, separated by the same character as that specified using the ParamsSplitChar shown below
  • ParamsSplitChar: The parameter split character to use, which allows the ExpressionParams string to be split into a array to pass to the Expression used inside the PyExpressionExtension

Anyway here is how I might use this to evaluate the following static expression. In this example, I am using the PyExpressionExtension to provide a Width value for the TextBox using the Expression: (1 * 200) * 2.

<TextBox Height="20"
         Background="Cyan"
         VerticalAlignment="Top"
         Margin="10"
         Width="{local:PyExpressionExtension 
         Expression='([0] * [1]) * [2]', 
         ReturnResult=Double,
         ExpressionParams='1 200 2', 
         ParamsSplitChar=' ' }" />

So let's now have a look at the actual PyExpressionExtension implementation. It is fairly easy. This is the entire code which I think is commented enough to tell you what is going on.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.Windows.Markup;
using IronPython.Hosting;
using Microsoft.Scripting;
using Microsoft.Scripting.Hosting;

namespace PythonExpressions
{
    /// <summary>
    /// Use the expression provided by the Expression parameter and
    /// substitute the placeholders with the Parameters, where the
    /// Parameters are obtained by splitting the Parameters string
    /// using the ParamsSplitChar value. Then IronPython is used to run the
    /// Expression and return the Results based on what ReturnResultType
    /// Type was requested. Which is a pain to have to specify but it appeared
    /// that WPF crashed when I did not specify the correct return Type
    /// </summary>
    public class PyExpressionExtension : MarkupExtension
    {
        #region Public Properties
        /// <summary>
        /// The code expression to run
        /// </summary>
        public String Expression { get; set; }

        /// <summary>
        /// A string with the Expression params, which
        /// should be separated using the same character
        /// as specified by the ParamsSplitChar 
        /// </summary>
        public String ExpressionParams { get; set; }

        /// <summary>
        /// The parameter split character to use
        /// </summary>
        public String ParamsSplitChar { get; set; }

        /// <summary>
        /// Make sure we use a TypeConverter to allow the Enum
        /// to specified in XAML using a string. This property
        /// specified a return Type for the IronPython run code
        /// </summary>
        [TypeConverter(typeof(ReturnResultTypeConverter))]
        public ReturnResultType ReturnResult { get; set; }
        #endregion

        #region Overrides
        /// <summary>
        /// Use the expression provided by the Expression parameter and
        /// substitute the placeholders with the Parameters, where the
        /// Parameters are obtained by splitting the Parameters string
        /// using the ParamsSplitChar value. Then IronPython is used to run the
        /// Expression and return the Results based on what ReturnResultType
        /// Type was requested. Which is a pain to have to specify but it appeared
        /// that WPF crashed when I did not specify the correct return Type
        /// </summary>
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            try
            {
                Object[] parameters = ExpressionParams.Split(new string[] 
				{ ParamsSplitChar },
                            	StringSplitOptions.RemoveEmptyEntries);

                Expression = Expression.Replace("[", "{");
                Expression = Expression.Replace("]", "}");

                String code = String.Format(Expression, parameters);

                //load the IronPython scripting host, and create a source and compile it
                ScriptEngine engine = PythonSingleton.Instance.ScriptEngine;
                ScriptSource source =
                  engine.CreateScriptSourceFromString(code, SourceCodeKind.Expression);

                Object res = source.Execute();

                //work out what type of return Type to use to keep WPF happy
                switch (ReturnResult)
                {
                    case ReturnResultType.Double:
                        return Double.Parse(res.ToString());
                    case ReturnResultType.String:
                        return res.ToString();
                }
            }
            catch (Exception ex)
            {
                return Binding.DoNothing;
            }
            return Binding.DoNothing;
        }
        #endregion
    }
}

One thing to note is that the ReturnResultType enum values can be set directly in the XAML. To do that, you need a TypeConverter to help the XamlParser convert from a String to a Enum type. Here is that code:

using System;
using System.ComponentModel;
using System.Globalization;

namespace PythonExpressions
{
    /// <summary>
    /// The return Type that should be used for the
    /// IronPython script that is run to evaluate the
    /// code script
    /// </summary>
    public enum ReturnResultType { String, Double }

    /// <summary>
    /// Allows the XAML to use the ReturnResultType enum.
    /// Basically you need a TypeConverter to allow the XamlParser
    /// to know what to do with Enum string value
    /// </summary>
    public class ReturnResultTypeConverter : TypeConverter
    {
        #region ConvertTo
        /// <summary>
        /// True if value can convert destinationType is String
        /// </summary>
        public override bool CanConvertTo
		(ITypeDescriptorContext context, Type destinationType)
        {
            if (destinationType.Equals(typeof(string)))
            {
                return true;
            }
            else
            {
                return base.CanConvertTo(context, destinationType);
            }
        }

        /// <summary>
        /// Convert to ReturnResultType enum value to a String
        /// </summary>
        public override object ConvertTo
		(ITypeDescriptorContext context, CultureInfo culture,
            object value, Type destinationType)
        {
            if (destinationType.Equals(typeof(String)))
            {
                return value.ToString();
            }
            else
            {
                return base.ConvertTo(context, culture, value, destinationType);
            }
        }
        #endregion

        #region ConvertFrom
        /// <summary>
        /// True if value can convert sourceType is String
        /// </summary>
        public override bool CanConvertFrom
		(ITypeDescriptorContext context, Type sourceType)
        {
            if (sourceType.Equals(typeof(string)))
            {
                return true;
            }
            else
            {
                return base.CanConvertFrom(context, sourceType);
            }
        }

        /// <summary>
        /// Convert from a String to a  ReturnResultType enum value
        /// </summary>
        public override object ConvertFrom(ITypeDescriptorContext context, 
				CultureInfo culture, object value)
        {
            if (value.GetType().Equals(typeof(String)))
            {
                try
                {
                    return (ReturnResultType)Enum.Parse(typeof(ReturnResultType), 
							value.ToString(), true);
                }
                catch
                {
                    throw new InvalidCastException(
                        String.Format("Could not ConvertFrom value {0} 
					into ReturnResultType enum value", 
                            value.ToString()));
                }
            }
            else
            {
                return base.ConvertFrom(context, culture, value);
            }
        }
        #endregion
    }
}

The PyExpressionExtension may not actually be that useful as the parameter values supplied for the Expression must be static, but it may be useful to someone, so I have included it.

I think the more useful version will be the IMultiValueConverter which we will look at next.

PyMultiBindingConverter IMultiValueConverter

The PyMultiBindingConverter is really just a fancy IMultiValueConverter so you can use the binding values as the expression parameters, so it is dynamic, and will be able to perform calculations based on dynamically changing bound values.

The code is much the same as the PyExpressionExtension, but here is how you will need to use it in XAML.

<TextBox Height="20"
         x:Name="txt1"
         Background="Cyan"
         VerticalAlignment="Top"
         Margin="10">

    <TextBox.Text>
        <MultiBinding Converter="{StaticResource pyConv}"
                      ConverterParameter="([0] * [1]) * [2]">
            <Binding ElementName="txt1"
                     Path="Height" />
            <Binding ElementName="txt1"
                     Path="Height" />
            <Binding ElementName="txt1"
                     Path="Height" />
        </MultiBinding>
    </TextBox.Text>
</TextBox>

Where the PyMultiBindingConverter is specified as a resource as follows:

<Window.Resources>
    <local:PyMultiBindingConverter x:Key="pyConv" ReturnResult="String" />
</Window.Resources>

NOTE: I have not found a way to get rid of the need to specify a Return Type. Which of course forces you to have a different PyMultiBindingConverter for different Type you would like to work with, so you would need 1 for String and another for Double, etc. I don't think that is too bad though as ValueConverters are re-usable.

Anyways, here is the code for the PyMultiBindingConverter:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.Globalization;
using IronPython.Hosting;
using Microsoft.Scripting;
using Microsoft.Scripting.Hosting;

namespace PythonExpressions
{
    /// <summary>
    /// Use the expression provided by the IMultiValueConverter parameter and
    /// substitute the placeholders with the Parameters, where the
    /// Parameters are obtained from the Bindings. Then IronPython is used to run the
    /// Expression and return the Results based on what ReturnResultType
    /// Type was requested. Which is a pain to have to specify but it appeared
    /// that WPF crashed when I did not specify the correct return Type
    /// </summary>
    public class PyMultiBindingConverter : IMultiValueConverter
    {
        #region Properties
        /// <summary>
        /// The code expression to run
        /// </summary>
        private String Expression { get; set; }

        /// <summary>
        /// Make sure we use a TypeConverter to allow the Enum
        /// to specified in XAML using a string. This property
        /// specified a return Type for the IronPython run code
        /// </summary>
        [TypeConverter(typeof(ReturnResultTypeConverter))]
        public ReturnResultType ReturnResult { get; set; }
        #endregion

        #region IMultiValueConverter Members

        /// <summary>
        /// Use the expression provided by the IMultiValueConverter parameter and
        /// substitute the placeholders with the Parameters, where the
        /// Parameters are obtained from the Bindings. Then IronPython is used to run the
        /// Expression and return the Results based on what ReturnResultType
        /// Type was requested. Which is a pain to have to specify but it appeared
        /// that WPF crashed when I did not specify the correct return Type
        /// </summary>
        public object Convert(object[] values, 
		Type targetType, object parameter, CultureInfo culture)
        {
            try
            {
                if (parameter != null && parameter.GetType().Equals(typeof(String)))
                {
                    if (!String.IsNullOrEmpty(parameter.ToString()))
                    {
                        Expression = parameter.ToString();
                        Expression = Expression.Replace("[", "{");
                        Expression = Expression.Replace("]", "}");

                        String code = String.Format(Expression, values);

                        //load the IronPythng scripting host, 
		      //and create a source and compile it
                        ScriptEngine engine = PythonSingleton.Instance.ScriptEngine;
                        ScriptSource source =
                          engine.CreateScriptSourceFromString
				(code, SourceCodeKind.Expression);

                        Object res = source.Execute();


                        switch (ReturnResult)
                        {
                            case ReturnResultType.Double:
                                return Double.Parse(res.ToString());
                            case ReturnResultType.String:
                                return res.ToString();
                        }
                    }
                    else
                    {
                        return Binding.DoNothing;
                    }
                }
                else
                {
                    return Binding.DoNothing;
                }
            }
            catch (Exception ex)
            {
                return Binding.DoNothing;
            }

            return Binding.DoNothing;

        }

        public object[] ConvertBack(object value, Type[] targetTypes, 
				object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

Some Limitations

  • As both the PyExpressionExtension and the PyMultiBindingConverter use string substitutions to replace the [] chars in the Expressions, you must use these around your Expression parameters. Something like this is a valid expression ([0] * [1]) * [2].
  • Having to specify a return Type plainly sucks, but I could not get rid of it. Which of course forces you to have a different PyMultiBindingConverter for different Type you would like to work with, so you would need 1 for String and another for Double, etc. I don't think that is too bad though as ValueConverters are re-usable.

That's It. Hope You Liked It

That is actually all I wanted to say right now, but I will be back soon to continue my series on a little MVVM framework that I am calling Cinch, so watch out for that one too.

Thanks

As always votes / comments are welcome.

History

  • 17th July, 2009: Initial post

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here