在 WPF 中使用 IronPython 评估表达式






4.92/5 (16投票s)
在 WPF 中使用 IronPython 评估表达式。
引言
对于一直关注我文章的读者,你们可能期望我的 MVVM 系列有新文章,别担心,那马上就到,但总有时间写一篇小文章。你们看,我刚读完一本关于 IronPython 的小书,觉得它是一种相当酷的语言。我特别喜欢的一点是它可以在 .NET 中作为脚本托管。这给了我一个想法,如果我们能使用 IronPython 作为脚本来为我们求数学表达式的值并返回一个结果,那不是很酷吗?
想想 JavaScript 的 eval
函数,C# 中缺少这个函数(至少目前是这样,也许 C# 4.0 会加入,谁知道呢)。所以带着这个想法,我匆忙写了一个小的概念验证,并且对结果非常满意,以至于我把它写成了这篇文章。
你可能会问,为什么我会在 WPF 中使用数学表达式。这里有一个常见的用法,你有一个 UI 元素,你想让它的宽度与其容器一样宽 - 某个魔术数字,这是你可以用标准的 IValueConverter
来完成的,但如果你想要另一个表达式,你就需要另一个 IValueConverter
实现。这不够酷。
所以,让我们来看看我提出了什么。
我提出了 2 个选项:一个 MarkupExtension
和一个 IMultiValueConverter
实现。
你需要
下载并安装 IronPython 2.0.1,这是我安装和使用的版本。你可以通过 这里 提供的 MSI 安装程序来安装 IronPython。
PyExpressionExtension 标记扩展
这个 PyExpressionExtension
可以用来评估静态表达式,其中值不是动态的(如果未绑定到实时值)。想法是用户可以向 PyExpressionExtension
输入以下内容:
Expression
:使用 IronPython 求值的表达式ReturnResult
:预期的返回类型,我很想省略它,但它似乎无法绑定,如果返回的对象类型不正确,就会在PyExpressionExtension
使用的地方抛出异常。也许有一些聪明人能解决这个问题。ExpressionParams
:表达式参数,使用ParamsSplitChar
指定的相同字符分隔ParamsSplitChar
:要使用的参数分隔符,它允许将ExpressionParams
字符串拆分成一个数组,以便传递给PyExpressionExtension
内部使用的表达式
无论如何,这是我如何使用它来评估以下静态表达式。在此示例中,我使用 PyExpressionExtension
为 TextBox
提供 Width
值,表达式为:(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=' ' }" />
现在让我们看看实际的 PyExpressionExtension
实现。它相当简单。这是整个代码,我认为注释足够说明它在做什么。
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
}
}
有一点需要注意,ReturnResultType enum
的值可以直接在 XAML 中设置。要做到这一点,你需要一个 TypeConverter
来帮助 XamlParser
将 String
转换为 Enum
类型。这是相关的代码
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
}
}
PyExpressionExtension
可能不是那么有用,因为为表达式提供的参数值必须是 static
的,但它可能对某些人有用,所以我包含了它。
我认为更有用的版本是 IMultiValueConverter
,我们接下来会看。
PyMultiBindingConverter IMultiValueConverter
PyMultiBindingConverter
实际上只是一个花哨的 IMultiValueConverter
,因此你可以使用绑定值作为表达式参数,这样它是动态的,并且能够根据动态变化的绑定值执行计算。
代码与 PyExpressionExtension
大致相同,但这是你在 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>
其中 PyMultiBindingConverter
指定为资源,如下所示
<Window.Resources>
<local:PyMultiBindingConverter x:Key="pyConv" ReturnResult="String" />
</Window.Resources>
注意:我还没有找到一种方法可以摆脱指定返回类型。这当然会迫使你为想要使用的不同类型提供不同的 PyMultiBindingConverter
,所以你需要一个用于 String
,另一个用于 Double
,依此类推。我认为这也不是什么大问题,因为 Value Converters
是可重用的。
总之,这是 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
}
}
一些限制
- 由于
PyExpressionExtension
和PyMultiBindingConverter
都使用string
替换来替换表达式中的[]
字符,所以你必须在表达式参数周围使用它们。像这样是有效的表达式 ([0] * [1]) * [2]。 - 必须指定返回类型确实很糟糕,但我无法摆脱它。这当然会迫使你为想要使用的不同类型提供不同的
PyMultiBindingConverter
,所以你需要一个用于String
,另一个用于Double
,依此类推。我认为这也不是什么大问题,因为Value Converters
是可重用的。
就是这样。希望你喜欢!
这实际上是我现在想说的全部内容,但我很快就会回来继续我关于一个我称之为 Cinch 的小型 MVVM 框架的系列文章,所以也要留意那一篇。
谢谢
一如既往,欢迎投票/评论。
历史
- 2009 年 7 月 17 日:首次发布