最后一个 IValueConverter






4.92/5 (19投票s)
使用此脚本驱动的 IValueConverter,
更新 (2008/09/25)
添加了 ScriptExtension
并提到了 LambdaExtension。
引言
当我开始接触 WPF 时,我认为 数据绑定很棒,但有时我就是无法轻易地从值中获取我需要的信息。
在这里,我想描述一个简单的虚构问题。假设我想显示一个整数列表,背景颜色取决于数字是奇数还是偶数。

XAML 可能看起来像这样
<ListBox ItemsSource="{Binding List, ElementName=root}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"
Background="{?? what shall I put here ??}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
初步调查
最初,我想编写自己的 MarkupExtension,它会有一个表达式属性并运行某种脚本。
结果发现
- 你做不到。
Binding
扩展程序可以做一些你做不到的事情。例如,你不能总是知道属性何时更改。你不能按名称查找对象(如Binding
扩展程序的ElementName
属性),等等。 - 有更好的方法。
Binding
扩展程序已经完成了所有工作,你只需要一个智能的IValueConverter
来设置给Binding.Converter
属性。
什么是 IValueConverter?
当你在 WPF 中进行数据绑定时,框架会自动为你进行一些转换,以便可以使用提供的值来设置目标属性。但是,如果你想稍微修改一下行为,并自己决定如何进行转换呢?你可以通过将 Binding
的 Converter
属性设置为 IValueConverter
来实现!
public class MyConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
现在,代码可以看起来像这样
Window1.xaml
<Window.Resources>
<local:ColorConverter x:Key="int2color"/>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{Binding List, ElementName=root}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"
Background="{Binding Converter={StaticResource int2color}}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
ColorConverter.cs
public class ColorConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (!(value is int))
return null;
int i = (int)value;
return i % 2 == 0 ? Brushes.Red : Brushes.Green;
}
}
现在,每当我遇到这样的问题时,我都可以编写一个新的 IValueConverter
。等等,不oooooooo!!!!……。
说真的,我不能只写一些内置的(在 XAML 中)快速代码吗?
脚本调查
最初,我找到了 Ken Boggart 的转换器(抱歉,没有链接,我的谷歌技能随着年龄增长显然下降了)。他们甚至还有一个基于脚本的转换器。我不喜欢它们,因为它们使用了快速粗糙的脚本引擎,并且与值转换器紧密耦合。
我还考虑使用 IronPython 作为脚本引擎。虽然这是一个伟大而成熟的项目,但 Python 有一个让我抓狂的地方。代码块由缩进定义!说真的……而且,这不仅仅是美学问题。想象一下:在你的 XAML 中嵌入一些 Python 脚本,“不小心”重新格式化文档(CTRL+E,D),然后你就破坏了你的 Python 脚本!嘶……太糟糕了。
我想编写自己的简单解释器。我最初的计划是使用 ANTLR。虽然它是一个很棒的项目,拥有很棒的 IDE,但在 C# 中测试/开发语法有点繁琐,因为所有工具都面向 Java。然后,我发现了 Irony。我还没有怎么用过它(与 ANTLR 相比),但我的第一印象是
- 编写语法更容易,看起来更好,而且绝对比 ANTLR 更适合 C#。
- 下一阶段,解释器或 AST 访问更加繁琐。
- 此外,ANTLR 可以非常轻松地将操作直接嵌入到解析器中,从而实现一个集解析/解释阶段于一体的方案,例如。
但后来,我发现我不需要编写自己的 Irony 驱动的解释器/脚本引擎,已经有一个了:Script.NET。
ScriptConverter
Script.NET 是一个快速简单的脚本引擎,它可以像所有自重的 .NET 脚本引擎一样,调用所有 .NET 方法和类。它可以轻松地嵌入到你的应用程序中;而且语法非常简单,你可以在 21 分钟内学会它!太好了,正是我想要的。当然,它使用花括号来定义代码块!
Script.NET 的当前版本(截至 2008 年 9 月)不支持最新的 Irony。它本身包含三个 DLL(加上过时的 Irony),并且依赖于 Windows Forms。我决定“修复”所有这些问题,因此在压缩的源代码中提供的版本不是原始的 Script.NET 源代码,而是自定义版本,只有一个 DLL,没有 WinForms 依赖项。
在我的转换器中,我显然必须添加一个表达式属性(用于脚本)。不太明显,但同样关键的是,我必须添加一个已知类型属性,以便脚本可以调用一个 static
属性或某个类构造函数。
它是如何工作的
好吧,首先,你只需一行代码即可创建一个脚本解释器
Script script = Script.Compile("my script string");
然后,你可以定义一些全局变量,或添加已知类型,如下所示
script.Context.SetItem("value", ContextItem.Variable, value);
script.Context.SetItem("Brushes", ContextItem.Type, typeof(Brushes));
然后,你也可以在一行中对其进行求值
return script.Execute();
一个让我停了几分钟的小问题是,我如何从脚本中返回东西?
好吧,一个简单值是一个表达式,最后计算的表达式就是返回值。例如,“1
” 是一个有效的脚本,它返回 1
。
Script.NET 不支持 (bool_expr ? true_expr : false_expr
) 运算符,但你可以这样表示
if (bool_expr)
true_expr;
else
false_expr;
解决方案一
ScriptConverter.cs
[ContentPropertyAttribute("Expression")]
public class ScriptConverter : IValueConverter
{
Script script;
TypeDictionary knownTypes = new TypeDictionary();
string expression;
public string Expression
{
get { return expression; }
set
{
expression = value;
script = Script.Compile(expression);
SetTypes(script, knownTypes);
}
}
public class TypeDictionary : Dictionary<string, Type> { }
public TypeDictionary KnownTypes
{
get { return knownTypes; }
set
{
knownTypes = value;
SetTypes(script, knownTypes);
}
}
static void SetTypes(Script ascript, TypeDictionary types)
{
foreach (var item in types)
ascript.Context.SetItem(item.Key, ContextItem.Type, item.Value);
}
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
script.Context.SetItem("value", ContextItem.Variable, value);
script.Context.SetItem("param", ContextItem.Variable, parameter);
return script.Execute();
}
}
现在,有了这个超酷的转换器,我的 XAML 就可以大放异彩了!
你会注意到我初始化了 Script,并提供了一个它需要知道的类型列表来计算表达式。
Window1.xaml
<Window.Resources>
<local:ScriptConverter x:Key="int2color">
<local:ScriptConverter.KnownTypes>
<x:Type x:Key="Brushes" TypeName="Brushes"/>
</local:ScriptConverter.KnownTypes>
if( value % 2 == 0 )
Brushes.Red;
else
Brushes.Green;
</local:ScriptConverter>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{Binding List, ElementName=root}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"
Background="{Binding Converter={StaticResource int2color}}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
进一步简化
好吧,现在我在 XAML 中完成了所有工作,而且效果很好。但我用一个臃肿的 XAML 替换了一个没用的 C# 文件!
我想要做的是只是把表达式放在转换器里,就像 lambda 表达式一样。
这时 MarkupExtension 将会派上用场。基本上,扩展是一种使用在 XAML 中运行的代码来创建对象的自定义方式。{x:Type …}
, {x:Static …}
, {Binding …}
都是 MarkupExtensions
。
创建自己的扩展很容易。好吧,让我们写出我真正想要看到的简化 XAML,然后写出扩展
<ListBox ItemsSource="{Binding List, ElementName=root}">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel Orientation="Horizontal">
<TextBlock Text="{Binding}"
Background="{Binding
Converter={local:Script
'if(value%2==0) Brushes.Red; else Brushes.Green;',
{x:Type Brushes}}}"/>
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
好了,这是一个更简单的 XAML 代码!在绑定的 Converter
参数中,我传递了一个直接创建我的转换器的扩展。这个扩展将接受以下参数:一个脚本 string
和一个脚本运行所需的类型列表。
最初我打算使用一个像这样的扩展构造函数
public ScriptExtension(string code, params Type[] types)
{
//....
}
但这行不通,因为 XAML 不会识别“params Type[]
”结构,它会期望 2 个且仅 2 个参数,第二个参数是类型数组。此外,标记扩展参数还有一些其他问题(XAML 仅根据参数数量来选择它们,它不做任何类型匹配)。最后我用了这段代码
public class ScriptExtension : MarkupExtension
{
public ScriptExtension(string code) { Init(code); }
public ScriptExtension(string code, Type type) { Init(code, type); }
public ScriptExtension(string code, Type type, Type t2) { Init(code, type, t2); }
protected ScriptExtension(string code, params Type[] types) { Init(code, types); }
void Init(string code, params Type[] types)
{
Code = code;
TypeDictionary dict = new TypeDictionary();
foreach (var aType in types)
dict[aType.Name] = aType;
KnownTypes = dict;
}
public string Code { get; private set; }
public TypeDictionary KnownTypes { get; private set; }
// return the converter here
public override object ProvideValue(IServiceProvider isp)
{
return new ScriptConverter(Code) { KnownTypes = KnownTypes };
}
}
设计器问题
它在运行时工作得很好。但是设计器有个问题。它根本无法处理这些多个构造函数。最后,我创建了多个 ScriptExtension
子类,称为 Script0Extension
, Script1Extension
, Script2Extension
等等……取决于类型参数的数量。
因此,最终的 XAML 看起来像这样
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel Orientation="Horizontal">
<TextBlock Text="{Binding}"
Background="{Binding
Converter={local:Script1
'if(value%2==0) Brushes.Red; else Brushes.Green;',
{x:Type Brushes}}}"/>
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
替代解决方案
在这个阶段,我们为许多问题找到了一个相当不错的解决方案。但是后来我调查了这个 LambdaExtension。它真的很酷,我已经把它添加到我的个人工具箱和这个 ExpressionExplorer
项目中了。
它允许你用 Lambda 表达式定义转换器,例如
<TextBlock Text='{Binding Source={x:Static s:DateTime.Now},
Converter={fix:Lambda "dt=>dt.ToShortTimeString()"}}'>
这相当不错,我想象 Lambda 表达式会比我的脚本表达式快得多。然而,它存在一些问题
- 你无法使用转换器方法的其他参数(参数和区域性)。
- 你无法引用任何类型,也无法返回
Brush
,也就是说,它无法解决我的简单问题!
但我认为,即使存在这些缺点,它仍然可以在 DataTriggers 中作为优秀的转换器。
附加信息
以下是一些人在编写此代码时启发了我,或者是我参考的页面
之后,我收到了更多非常有价值的反馈。
首先,Orcun Topdagi 进一步改进了他的 LambdaExtension,你可以在他的博客上查看关于 WPFix2 和 WPFix3 的内容。哇,那些都是真正的热门宝贝。我甚至可能会放弃我的扩展而使用他的!但我必须花更多时间来玩玩它(目前有点超级忙)。
另外,Daniel Paull 有一些非常有趣的 博客文章(这里、这里和这里)介绍了如何使用 DLR 来实现类似的功能。我真的很喜欢这些文章,因为它们是 DLR 的一个简单入门。
历史
- 2008年9月30日:更新了“附加信息”部分
- 2008年9月25日:文章已更新
- 添加了
ScriptExtension
并提到了 LambdaExtension。
- 添加了
- 已进行第一次修复!
- 我更新了代码,以提供更好的设计器支持。此外,我的(当前实验性的)关于
LambdaExtension
的工作也在这个程序集中(来自 fikrimvar)。
- 我更新了代码,以提供更好的设计器支持。此外,我的(当前实验性的)关于
- 2008年9月24日:首次发布!
- 如果需要,我可能只会更新 我的博客页面。