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
ParamsSp
litChar
: 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
{
public class PyExpressionExtension : MarkupExtension
{
#region Public Properties
public String Expression { get; set; }
public String ExpressionParams { get; set; }
public String ParamsSplitChar { get; set; }
[TypeConverter(typeof(ReturnResultTypeConverter))]
public ReturnResultType ReturnResult { get; set; }
#endregion
#region Overrides
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);
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();
}
}
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
{
public enum ReturnResultType { String, Double }
public class ReturnResultTypeConverter : TypeConverter
{
#region ConvertTo
public override bool CanConvertTo
(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType.Equals(typeof(string)))
{
return true;
}
else
{
return base.CanConvertTo(context, destinationType);
}
}
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
public override bool CanConvertFrom
(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType.Equals(typeof(string)))
{
return true;
}
else
{
return base.CanConvertFrom(context, sourceType);
}
}
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
{
public class PyMultiBindingConverter : IMultiValueConverter
{
#region Properties
private String Expression { get; set; }
[TypeConverter(typeof(ReturnResultTypeConverter))]
public ReturnResultType ReturnResult { get; set; }
#endregion
#region IMultiValueConverter Members
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);
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 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