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

使用 ConditionalValueConverter 在 WPF 绑定表达式中评估比较

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (4投票s)

2009 年 2 月 8 日

公共领域

5分钟阅读

viewsIcon

45249

downloadIcon

384

基于另一种类型的比较,输出一种类型的值。

引言

在 WPF 中,您将进行大量数据绑定。例如,您可以将菜单项的 IsChecked 绑定到产品中某个功能的 Enabled 状态,或者将 TextBlockForeground 画笔绑定到红色画笔的静态资源。

但是,如果您想将该文本的 Foreground 画笔绑定为负数为 Red,正数为 Black 呢?或者,如果您想将 IsChecked 绑定为 "!Enabled" 呢?好吧,您必须编写一个 TypeConverter。即使是像否定布尔值这样简单的事情,您也必须编写自定义代码并在 XAML 文件中引用它。

问题所在

在编写了几个这样的类型转换器后,我很快意识到其中 90% 归结为同一件事:“如果输入具有特定值,则返回一个输出值,否则返回另一个输出值。”输出值通常是已知的,并且通常与输入值的类型不同。如果可以将这种行为编写一次,并在整个应用程序中重用,那不是很棒吗?这将减少代码开发和测试时间,并减少代码错误。

解决方案

引入 ConditionalValueConverter。它接受一个引用值(表示为字符串,称为 Reference),以及两个输出值(表示为字符串,具有指定的 ValueType)。如果输入值等于引用值,它将返回其中一个值(TrueValue),否则返回另一个值(FalseValue)。在您的 XAML 中,您可以这样绑定它:

<Button Foreground="{Bind Path=Some.Path.To.A.Boolean, TypeConverter= 
    {local:ConditionalValueConverter Reference=true, ValueType=Brush, 
        TrueValue=Green, FalseValue=Red}}"
    Name="theButton" Click="OnClick">Click Me</Button>

为了使此功能正常工作,需要将“local”XML 命名空间定义为 ConditionalValueConverter 所在的命名空间。但是,如果您已经在使用自定义绑定,您可能已经知道了。

现在,它是如何工作的?

当评估绑定时,该值可以选择性地通过 ValueConverter。当您将文本框绑定到滑块时,该值将在 doublestring 之间自动转换——您无需执行任何特殊操作。但是,对于自定义类型,可能没有自动转换,您必须编写自己的 ValueConverter。这正是 ValueConverter 最初要解决的问题。

由于您可以为每个绑定指定要使用的 ValueConverter,因此您可以开始对输入值进行一些技巧性操作。ValueConverter 所要做的就是接受特定类型(输入属性的类型)的输入值,并提供另一种类型(绑定目标的类型)的输出。只要遵循这些规则,任何操作都可以,并且开发人员已经非常有创意地重载了此机制,将用户界面呈现代码放入 Binding 和代码隐藏的组合中。

实现细节

您使用 Reference 值(用于与输入比较)、所需的输出 ValueType 以及需要返回的 TrueValueFalseValue(将强制转换为 ValueType)来配置 ValueConverter。所有这些都作为基本 C# 属性实现。

public string Reference { get; set; } 
object trueValue_;
object setTrueValue_;
public object TrueValue { get { return trueValue_; } 
                          set { setTrueValue_ = value; MakeTrue(); } }
object falseValue_;
object setFalseValue_;
public object FalseValue { get { return falseValue_; } 
                           set { setFalseValue_ = value; MakeFalse(); } }
Type valueType_;

public string ValueType { get { return valueType_.Name; } 
                          set { valueType_ = GetValueType(value); 
                                MakeTrue(); MakeFalse(); } }

输出类型是什么?

您会注意到,在更改这些属性之一时,会调用一些辅助函数来设置正确的值和类型。由于属性设置的顺序不确定,您必须在更改实际值(作为字符串)和更改期望值类型时都尝试转换这些值。让我们从类型开始。我们只需获取字符串,然后返回该字符串的类型。不幸的是,Type.GetType(string) 不识别基本类型,如 System.BooleanSystem.Double,因此我们必须首先显式地检查这些类型。

Type GetValueType(string name) 
{
  if (name == "float" || name == "System.Single")
    return typeof(float);
  if (name == "double" || name == "System.Double")
    return typeof(double);
  if (name == "int" || name == "System.Int32")
    return typeof(int);
  if (name == "string" || name == "System.String")
    return typeof(string);
  if (name == "bool" || name == "System.Boolean")
    return typeof(bool);
  return Type.GetType(name);
}

配置返回值(对于真和假)

然后,当我们设置 TrueValue(或 FalseValue)属性时,我们必须将值转换为期望的类型。这需要在实际进行转换之前进行一些初步检查。

void MakeTrue() 
{
  if (setTrueValue_ == null || valueType_ == null)
    return;
  if (setTrueValue_.GetType() == valueType_)
  {
    trueValue_ = setTrueValue_;
    return;
  }
  if (setTrueValue_.GetType() != typeof(string))
  {
    throw new InvalidOperationException(
        String.Format("Set type must be ValueType ({0}) or string " + 
                      "for ConditionalValueConverter.TrueValue. Got type {1}.",
                      valueType_.Name, setTrueValue_.GetType().Name));
  }
  trueValue_ = TypeDescriptor.GetConverter(valueType_).
                  ConvertFromInvariantString((string)setTrueValue_);
}

我们直到同时拥有值(通常是 string)和类型后才进行转换。此外,如果设置的值(在 setTrueValue_ 中)已经是正确的类型,我们就不需要转换;只需记住这是要使用的值(存储在 trueValue_ 中)。最后,如果设置的值不是字符串,也不是期望的类型,我们就认为用户做错了,并通过异常报告。之后,我们使用 TypeDescriptor.GetConverter() 函数查找合适的类型转换器,并将字符串转换为值。由于 C#(和 XAML)中的编程通常使用“不变”区域性(其中小数使用句点等),因此我们使用 ConvertFromInvariantString() 函数。

实际转换类型

最后,类型转换器的实际工作。

public object Convert(object value, Type targetType, 
              object parameter, System.Globalization.CultureInfo culture)
{
  if (targetType != valueType_)
    throw new System.NotSupportedException();
  if (value == null || Reference == null)
    return ((object)value == (object)Reference) ? trueValue_ : falseValue_;
  object r = Reference;
  if (value.GetType() != Reference.GetType())
    r = TypeDescriptor.GetConverter(value).ConvertFrom(r);
  if (value.Equals(r))
    return trueValue_;
  return falseValue_;
}

同样,这里有一些健全性检查。例如,我们不能转换为配置类型以外的任何类型。如果引用或要转换的值是 null,则仅当两者都为 null 时,我们才返回 true。我们可能还需要将引用(它是 string)转换为我们正在转换的值的类型——例如,如果输入属性是一个布尔值。一旦所有这些完成,我们就将其与(转换后的)引用值进行比较,并根据结果返回“true”值或“false”值。

高级用法

还有一些其他的进阶选项。因为 TrueValueFalseValueType 对象的属性,所以您实际上可以使用 StaticResource 绑定来设置它们,而不是字符串值。这就是为什么我们在 MakeTrue()(和 MakeFalse())函数内部测试属性类型的原因。

此外,您可以将 ConditionalValueConverter 实例化为 Resource,并在为 Binding 对象指定 ValueConverter 时使用 StaticResource 引用。这允许您跨多个绑定重用相同的对象(例如,一个按符号为文本着色的对象),从而节省应用程序的解析类型和内存。

<Window.Resources> 
  <local:ConditionalValueConverter Reference="true" TrueValue="Black" 
      FalseValue="Red" ValueType="Brush" x:Key=redOrBlack />
...
  <Button Background="{Bind Path=Some.Boolean, 
                       ValueConverter={StaticResource redOrBlack}}" />

好了,就这些了。这篇文章的写作时间可能比代码还长,但考虑到我没有在其他地方找到相同的代码,我认为其他人可能会发现它和我一样有用!

版本历史

  • 版本 1.0 — 2009 年 2 月 9 日。
© . All rights reserved.