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

最后一个 IValueConverter

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (19投票s)

2008 年 9 月 24 日

CPOL

7分钟阅读

viewsIcon

104803

downloadIcon

502

使用此脚本驱动的 IValueConverter, 以少量工作即可将任何内容绑定到任何内容

更新 (2008/09/25)

添加了 ScriptExtension 并提到了 LambdaExtension

引言

当我开始接触 WPF 时,我认为 数据绑定很棒,但有时我就是无法轻易地从值中获取我需要的信息。

在这里,我想描述一个简单的虚构问题。假设我想显示一个整数列表,背景颜色取决于数字是奇数还是偶数。

ExpressionConverter

XAML 可能看起来像这样

<ListBox ItemsSource="{Binding List, ElementName=root}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding}"
                       Background="{?? what shall I put here ??}"/>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

初步调查

最初,我想编写自己的 MarkupExtension,它会有一个表达式属性并运行某种脚本。

结果发现

  1. 你做不到。Binding 扩展程序可以做一些你做不到的事情。例如,你不能总是知道属性何时更改。你不能按名称查找对象(如 Binding 扩展程序的 ElementName 属性),等等。
  2. 有更好的方法。Binding 扩展程序已经完成了所有工作,你只需要一个智能的 IValueConverter 来设置给 Binding.Converter 属性。

什么是 IValueConverter?

当你在 WPF 中进行数据绑定时,框架会自动为你进行一些转换,以便可以使用提供的值来设置目标属性。但是,如果你想稍微修改一下行为,并自己决定如何进行转换呢?你可以通过将 BindingConverter 属性设置为 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# 文件!smile_embaressed

我想要做的是只是把表达式放在转换器里,就像 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,你可以在他的博客上查看关于 WPFix2WPFix3 的内容。哇,那些都是真正的热门宝贝。我甚至可能会放弃我的扩展而使用他的!但我必须花更多时间来玩玩它(目前有点超级忙)。

另外,Daniel Paull 有一些非常有趣的 博客文章(这里这里这里)介绍了如何使用 DLR 来实现类似的功能。我真的很喜欢这些文章,因为它们是 DLR 的一个简单入门。

历史

  • 2008年9月30日:更新了“附加信息”部分
  • 2008年9月25日:文章已更新
  • 已进行第一次修复!
    • 我更新了代码,以提供更好的设计器支持。此外,我的(当前实验性的)关于 LambdaExtension 的工作也在这个程序集中(来自 fikrimvar)。
  • 2008年9月24日:首次发布!
© . All rights reserved.