65.9K
CodeProject 正在变化。 阅读更多。
Home

在 WPF 中使用 IronPython 评估表达式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (16投票s)

2009 年 7 月 17 日

CPOL

4分钟阅读

viewsIcon

60467

downloadIcon

569

在 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 内部使用的表达式

无论如何,这是我如何使用它来评估以下静态表达式。在此示例中,我使用 PyExpressionExtensionTextBox 提供 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 来帮助 XamlParserString 转换为 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
    }
}

一些限制

  • 由于 PyExpressionExtensionPyMultiBindingConverter 都使用 string 替换来替换表达式中的 [] 字符,所以你必须在表达式参数周围使用它们。像这样是有效的表达式 ([0] * [1]) * [2]。
  • 必须指定返回类型确实很糟糕,但我无法摆脱它。这当然会迫使你为想要使用的不同类型提供不同的 PyMultiBindingConverter,所以你需要一个用于 String,另一个用于 Double,依此类推。我认为这也不是什么大问题,因为 Value Converters 是可重用的。

就是这样。希望你喜欢!

这实际上是我现在想说的全部内容,但我很快就会回来继续我关于一个我称之为 Cinch 的小型 MVVM 框架的系列文章,所以也要留意那一篇。

谢谢

一如既往,欢迎投票/评论。

历史

  • 2009 年 7 月 17 日:首次发布
© . All rights reserved.