使用 ConditionalValueConverter 在 WPF 绑定表达式中评估比较
基于另一种类型的比较,输出一种类型的值。
引言
在 WPF 中,您将进行大量数据绑定。例如,您可以将菜单项的 IsChecked
绑定到产品中某个功能的 Enabled
状态,或者将 TextBlock
的 Foreground
画笔绑定到红色画笔的静态资源。
但是,如果您想将该文本的 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
。当您将文本框绑定到滑块时,该值将在 double
和 string
之间自动转换——您无需执行任何特殊操作。但是,对于自定义类型,可能没有自动转换,您必须编写自己的 ValueConverter
。这正是 ValueConverter
最初要解决的问题。
由于您可以为每个绑定指定要使用的 ValueConverter
,因此您可以开始对输入值进行一些技巧性操作。ValueConverter
所要做的就是接受特定类型(输入属性的类型)的输入值,并提供另一种类型(绑定目标的类型)的输出。只要遵循这些规则,任何操作都可以,并且开发人员已经非常有创意地重载了此机制,将用户界面呈现代码放入 Binding
和代码隐藏的组合中。
实现细节
您使用 Reference
值(用于与输入比较)、所需的输出 ValueType
以及需要返回的 TrueValue
和 FalseValue
(将强制转换为 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.Boolean
或 System.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”值。
高级用法
还有一些其他的进阶选项。因为 TrueValue
和 FalseValue
是 Type
对象的属性,所以您实际上可以使用 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 日。