WPF 使用转换器参数的已计算值绑定






4.53/5 (10投票s)
WPF 的值绑定转换器。
引言
当我刚开始接触 WPF 时,我记得我曾因为无法在控件使用绑定值之前对其进行修改而感到沮丧。当时我正在进行自定义控件的开发,并将尺寸/位置绑定到祖先值。我只需要一点点调整,并且能够对绑定值进行简单的数学运算就已经很棒了。
最近我做了很多关于值转换器的研究,并最终着手解决这个问题。我知道值转换器是一个很好的解决方案,可以使用 ConverterParameter
来传递用于处理绑定值的字符串。问题在于我需要一个合适的计算引擎来处理方程式。有很多处理数字的计算引擎,但只能处理数字会限制转换器的灵活性,因为它甚至无法处理枚举。至少,我希望有一个能够处理数字和字符串的计算引擎。
当我终于有时间时,我开始在互联网上搜索一个能用于这个值转换器的计算引擎(我真的不想自己构建一个)。实际上有很多好的选择摆在我面前
- 使用 JavaScript
Eval
函数 - “使用 JavaScript 的Eval()
函数从 C# 计算表达式”: https://codeproject.org.cn/Articles/46350/Evaluate-Expressions-from-C-using-JavaScript-s-Eva。 - 动态编译使用 Microsoft 编译器 - “通过在运行时编译 C# 代码来计算数学表达式”: https://codeproject.org.cn/Articles/3988/Evaluating-Mathematical-Expressions-by-Compiling-C。
- 使用 C# 程序计算函数,例如“表达式求值器重访(100% 托管 .NET 中的
Eval
函数)”中的函数: https://codeproject.org.cn/Articles/13779/The-expression-evaluator-revisited-Eval-function-i。可能还有更多,但我需要一个能处理字符串的。 - 还有一个非常吸引我的想法是在“C# 公式计算器”中发布的: http://www.jarloo.com/c-formula-evaluator/。它使用了正则表达式。显然性能会很差,而且我必须扩展它来处理字符串,但它非常简洁。
我选择使用 JavaScript 解决方案,因为它最简单,功能强大(具有所有 JavaScript 功能),并且应该没有内存泄漏问题;我听说使用 Microsoft 编译器可能会有问题,尽管对于这种情况来说可能不是大问题,因为方程很少改变。要使用 C# 编译的代码,最好在一个字典中维护一个编译后的版本。其优点是它将提供 C# 的所有功能以及与 WPF 的兼容性。
最后一个选项意味着我使用了大量他人的代码,并且没有 JavaScript 或 C# 的强大功能。
在我开始这个项目时,我意识到我可以反转 Convert
和 ConvertBy
,并有一个转换器可以快速添加功能,让用户输入数字作为方程。由于我对多个值转换器实现有一些疑问,所以我先完成了那个项目,并在 CodeProject 上发布了它(用于计算用户方程输入的“值转换器”)。 JavaScript 对此功能肯定不如 C# 好,但我喜欢它的简单性;使用 Microsoft 编译器处理许多不同的方程可能会导致内存泄漏问题。
实现
要使用 JavaScript eval()
函数,必须创建一个 JavaScript 函数
package JavascriptEvaluator
{
class JavascriptEvaluator
{
public function Evaluate(expr : String) : String
{
return eval(expr, "unsafe");
}
}
}
我已经将此代码放入一个名为“JavascriptEvaluator.js”的文件中。
接下来,必须将其编译成一个程序集。该程序集将通过在 Visual Studio 命令提示符中执行以下命令在“JavascriptEvaluator.dll”文件中创建
jsc /target:library JavascriptEvaluator.js
我已经将此命令放入一个名为“JavascriptEvaluatorCompile.bat”的批处理文件中。
在使用此功能的项目中,必须将对“JavascriptEvaluator.js”和“Microsoft.Jscript
”的引用添加进去。您必须浏览到“JavascriptEvaluator.js”文件的位置来添加它,而“Microsoft.Jscript”程序集包含在 Framework 程序集中。
我实际上实现了两种不同的值转换器,一种是使用 IValueConverter
接口的基本单参数转换器,另一种是处理从 IMultiValueConverter
派生的更多参数的转换器。
现在我们有了提供 JavaScript eval()
函数访问权限的程序集引用,IValueConverter
的实现现在很简单
partial class EquationValueConverter : IValueConverter
{
private readonly JavascriptEvaluator.JavascriptEvaluator evaluator =
new JavascriptEvaluator.JavascriptEvaluator();
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (parameter == null || parameter.ToString() == string.Empty)
return value;
string newValue = string.Format(parameter.ToString(), value);
try
{
return evaluator.Evaluate(newValue);
}
catch (Exception)
{
return newValue;
}
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
可以看到,只实现了 Convert
,因为反向转换会很困难。
在 Convert
方法中,首先检查参数参数,确保它有一个非空值可以操作。如果没有,那么最多只能返回一个空值,或者可能抛出异常。然后使用 string.Format
方法,并将参数和值参数作为参数。这意味着,要创建一个方程式,只需将方程式放在字符串中,并使用“{0}”占位符表示值应该插入的位置。接下来,执行 JavaScript eval()
函数,并将此字符串作为参数。它被放在一个 try
-catch
块中,因为如果方程式无效,将会生成一个异常。
此代码实际上处理了两种情况:一种是 string.Format
函数返回一个可以被 JavaScript eval()
函数成功执行的函数,另一种是 string.Format
返回一个对目标有效但无法被 JavaScript eval()
函数成功执行的内容。string.Format
实际上非常强大,并且在许多用途中可以单独用作转换器(稍后我将展示一个多值转换器的用法)。事实上,您可能想使用两种不同的转换器,一种带 Eval()
函数,一种不带。创建一个单独的转换器,它只执行 string.Format
方法,其优点是不会在格式化字符串不是 Eval()
函数的有效方程式时导致异常,并可能简化参数参数,以便 Eval()
不会成功执行 string.Format
函数的结果。创建能够产生所需效果的参数可能会很困难。
创建两个单独的转换器也将意味着可以更好地进行错误捕获以获取调试信息。由于我将两者结合起来,因此无法像我希望的那样添加更多程序员帮助。我还有一个很好的示例可以玩,但可能会在用户尝试输入示例时导致许多错误。您可以在文章中看到我为另一个值转换器使用的一些调试信息:通用 WPF/Silverlight 值转换器。
IMultiValueConverter
的实现实际上要困难得多。原因是 IMultiValueConverter
的工作方式与 IValueConverter
不太一样。虽然 IValueConverter
根据 targetType
自动执行转换,但 IMultiValueConverter
似乎并非如此。并非解决方案本身很困难,而是弄清楚为什么一个有效而另一个无效。
partial class EquationValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (parameter == null || parameter.ToString() == string.Empty)
return null;
string newValue = string.Format(parameter.ToString(), values);
try
{
return FixValue(targetType, evaluator.Evaluate(newValue));
}
catch (Exception)
{
return FixValue(targetType, newValue);
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
private static object FixValue(Type targetType, object value)
{
if (value.GetType() == targetType)
return value;
try
{
var converter = TypeDescriptor.GetConverter(targetType);
return converter.ConvertFrom(value);
}
catch
{
return value;
}
}
}
为了处理转换问题,我使用了文章 通用 WPF/Silverlight 值转换器 中的一个方法。Microsoft 提供了一种创建类的方法,该类将为另一个类提供转换器。通过使用属性,可以将此类与它所翻译的关联类相关联
[TypeConverter(typeof(MyClassTypeConverter))]
public class MyClass
{
//Class implementation here
}
public class MyClassTypeConverter : TypeConverter
{
public override object ConvertTo(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture,
object value,
Type destinationType)
{
//Conversion code here, returning object;
}
}
在 FixValue
方法中,我使用 TypeDescriptor.GetConverter
方法来获取 targetType
的转换器,然后使用此转换器生成所需类型的实例。如果发生任何错误,可能是因为没有该类的转换器,或者字符串未被识别,则只返回传递给方法的该值。
ConverterParameter
ConverterParameter
必须是 JavaScript eval()
函数在参数被 string.Format
方法处理后能够识别的内容,或者它必须独立存在(在被 string.Format
方法处理后),并且不被 JavaScript eval()
函数识别为有效。因为它嵌入在 XML 中,任何在 XAML 参数中无效的字符都必须用字符实体引用替换(例如,将引号字符替换为“"”)。
如果 ConverterParameter
是定义绑定的属性的一部分,那么需要处理更多问题。首先是花括号 (“{“) 的转义序列。例如,要编写加五的方程式,参数将是
ConverterParameter ='{}{0} + 5'
需要单引号是因为 ConverterParameter
参数中有空格。
以下是在绑定属性值中使用 ConverterParameter
参数的注意事项
- 引号字符 (“"”) 被替换为 "e;。
- 要输入花括号 (“{“),则需要将“{}”转义序列放在开花括号之前。
- 如果参数中有空格,请将参数用单引号 {“'”} 括起来。
示例
在包含的示例中,我提供了几个使用方程式的示例。
第一个非常基本,但允许进行实验。基本上,有两个文本框,一个用于输入,另一个用于显示结果。ConverterParameter
中的方程式是
ConverterParameter={}{0}+5}
基本上,第一个文本框中的任何内容都会加上 5。它显而易见地支持数字,但如果第一个文本框中输入了带引号的文本,则加法会变成字符串追加。
由于 ConverterParameter
值中没有空格,所需的就是为参数(“{0}”)的花括号使用转义序列“{}”。
第二个示例是第二个文本框下方的滑块:如果此滑块向右,背景颜色为粉红色,向右为蓝色。ConverterParameter
是
ConverterParameter='{}{0} > 0 ? "Pink" : "LightBlue"'
在这里,需要所有特殊处理,因为有参数(“{0}”)需要花括号的转义字符,有空格(需要将方程式放在单引号内),以及定义两种颜色的字符串的双引号(需要 XML)。这里方程式使用了 JavaScript 条件运算符的强大功能(这也是我使用 JavaScript eval()
函数的原因之一)。
第三个示例与第二个类似,只是方程式实际上定义为一个资源。这里边框的颜色会根据滑块的位置而变化,但这次是彩虹的颜色而不是只有两种。ConverterParameter
只使用 StaticResource
。
ConverterParameter={StaticResource strParam}
将字符串作为静态资源放置的优点之一是,编写方程式要容易得多
<sys:String x:Key="strParam">{0} > 3 ? "Violet" : {0} > 1 ? "Blue"
: {0} > 0 ? "Green" : {0} > -1 ? "Yellow" : {0} > -2 ? "Orange"
: {0} > -3 ? "Red" : "Violet"</sys:String>
我故意在这个方程式中使用了“>”,因为元素的值可以包含此符号,但不能包含“<”(必须替换为“>”)。很容易看出将参数定义为资源的好处。
第四个示例是一个多转换器,其中三个滑块控制窗体的背景颜色。ConverterParameter
是
ConverterParameter="#ff{0:X2}{1:X2}{2:X2}"
ConverterParameter
在一个多绑定中,这需要绑定在自己的元素中定义,因此 ConverterParameter
有自己的属性。这意味着不需要花括号的转义序列。在这种情况下,ConverterParameter
只使用了 string.Format
方法的功能,并且实际上与 JavaScript 不兼容。执行时,当尝试使用 JavaScript 执行时会抛出异常,而 catch
语句会返回 string.Format
函数返回的字符串。如果稍微更改方程式,JavaScript eval()
函数将成功执行方程式(但实际上什么也不做)。这只需要将方程式放在引号内,或者在这种情况下,是引号所需的字符序列
ConverterParameter=""#ff{0:X2}{1:X2}{2:X2}""
为了让这个 string.Format
工作(因为格式是十六进制的,与浮点数不兼容),必须将滑块的值绑定到整数属性,以强制将值视为整数。直接绑定到滑块的尝试将导致 string.Format
抛出异常,因为尝试将非整数转换为十六进制数。
这也是我发现值转换到正确的目标类型不作为转换的一部分,而必须显式完成的示例。Window
底部的 TextBlock
的 Text
属性曾经被用作绑定值,以尝试找出颜色为何没有改变。当那奏效后,它给了我一个重要的线索,说明了我为什么在背景颜色方面遇到麻烦。
结论
这个转换器可能是我创建的最强大的转换器。有了它,您将不再需要在 ViewModel 中进行计算,暴露不应该在 ViewModel 中的额外属性。它可能允许移除代码隐藏中的代码。如果使用 C# 编译器而不是 JavaScript,它应该更强大。我已成功处理了数字和字符串。我没有在试图弄清楚如何处理 DateTime
值时感到沮丧。很容易看到能够编写完整的 C# 代码的好处,但我是一名 C# 开发人员,只在 JavaScript 中做过一点工作。