WPF IValueConverter/IMultiValueConverter 辅助类






4.17/5 (14投票s)
提供一个类,帮助创建灵活的值和多值转换器,这些转换器使用ConverterParameter通过?:运算符指定true和false结果的返回值。
引言
我发现 WPF 绑定中有很多情况,当我想要做一个评估,结果是 true
或 false
,并返回一个取决于该结果的值。这类似于 C# 的条件运算符。WPF 的一个常见需求是根据一个标志值使控件 Visible
或 Collapsed
。
有几种选择。一种是为每种情况构建一个 ValueConverter
,但这会不断添加转换器,这需要时间,并会向解决方案添加模块和代码。另一种是向 ViewModel
添加属性,但这会将视图特有的信息(例如 Visibility.Visible
和 Visibility.Collapsed
)添加到 ViewModel
中,并增加 ViewModel
的复杂性——我认为在 ViewModel
中有两个 bool
值,一个与另一个相反,或者将枚举转换为二进制值是一种糟糕的代码气味。
我创建了许多 ValueConverters
,它们基本上评估传递的布尔值,并返回一个取决于该结果的值;转换器通常仅设计用于特定的转换。有一些框架包含一个转换器,当绑定值为 true
时,将其转换为 Visibilbiy.Visible
,否则转换为 Visibility.Collapsed
。但是,在某些情况下,可见性会因控件而异,取决于属性的值,或者我们希望是 Visibility.Hidden
而不是 Collapsed
。这意味着要么必须创建多个转换器,要么需要在 ViewModel
中设置多个依赖属性。
为了尽量减少所需的转换器数量,我首先添加了通过 ConverterParameter
传递“Reverse”的能力来反转结果。这减少了所需的转换器数量;然而,这仍然意味着有很多转换器。所以我想到一个更好的主意——在 ConverterParameter
中传递所需的值。然后问题是如何格式化 ConverterParameter
中的两个值。这似乎很明显,应该借鉴 C#,所以我像“?:
”运算符一样使用了“:
”。除其他外,这与 **XAML** 配合得很好。虽然我当前的需求不需要条件,但后来我添加了对条件参数的支持。
起初,我将代码放在每个转换器中,但很快就决定我想要创建一个辅助类,这意味着每个转换器共享一个通用的代码库。这使得每个转换器都非常简单,并支持轻松的升级和错误修复。如果未包含 ConverterParameter
,则默认值为布尔 true
和 false
,但也保留了在 ConverterParameter
中指定“Reverse
”的功能,使其类似于在 ConverterParameter
中设置了“false:true
”。这很快就得到了回报。
起初,我只返回 string
,然后依赖于每个类的 TypeConverter
将 string
转换为所需的 Type
。然而,这对 **Telerik** 控件不起作用,所以不得不使用转换器中的 TypeConverter
来进行转换,以得到正确的类型。
我还添加了在表达式列表中为 null
值添加第三个值的能力,它被添加到 ConverterParameter
的末尾,并用“:
”将其与其余部分分隔开。
后来的一个添加是构建灵活的 IMultiValueConverters
。只有三个似乎是有意义的,那就是 IsEqualConverter
、IsFalseConverter
和 IsTrueConverter
。IsFalseConverter
和 IsTrueConverter
之间的区别在于,对于 IsFalseConverter
为 true
,所有值都必须为 false
……不是 null
,也不是其他什么,而是 false
。IsTrueConverter
则完全相反。这些转换器可以与每个绑定的转换器一起使用,这就是为什么 true
或 false
值不仅检查 bool
true
或 false
,还检查 string
"true
" 或 "false
"。这使得控件的 Visibility
或 Enabled
属性依赖于多个值变得非常容易,而无需在 **XAML** 中进行多重嵌套,或在 ViewModel
中设置特殊属性。
当用作 IMultiValueConverter
时,IsEqualConverter
仅当所有值都相同,并且等于 ConverterParameter
字符串(在 "?" 之前,如果 "?" 存在于 ConverterParameter
中)时才返回 true
。
我对 IsEqualConverter
的另一个增强是允许具有关联返回值的多个匹配值
ConverterParameter="matchValue1?returnValue1: matchValue2?returnValue2:returnValueDefault"
创建转换器
我个人喜欢使用 MarkupExtension
来创建我所有的 ValueConverters
,因为它意味着我不需要在 **XAML** 中创建 static
资源。我还包含默认构造函数,以便 **XAML** 中不会出现问题,因为我使用了 MarkupExtension
,并且由于某种原因,Resharper(我认为)不认为存在固有的默认构造函数,因此必须有一个来防止显示警告。我宁愿将警告集中在一个地方,而不是每次使用转换器时都显示。
转换器只需要一行代码,即调用辅助方法的代码。只有辅助方法的第二个参数不是直接从 Convert
方法调用中的参数传递的。第二个参数特定于转换器的目的,即条件函数委托 (Func<T, bool?>
),用于确定参数参数中指定的哪个 true
或 false
(或 null
)值应被返回。
public sealed class IsFalseConverter : MarkupExtension, IValueConverter
{
public IsFalseConverter() { }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return NullableBooleanConverterHelper.Result<bool?>
(value, i => !i, targetType, parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
使用 IValueConverter
由于我使用 MarkupExtension
,因此绑定使用的转换器参数指定了与转换器命名空间相关的 namespace
前缀以及转换器的类名。只需要像普通的绑定语法一样为一个带有转换器的绑定进行操作,并包含一个转换器参数,该参数指定 true
和 false
结果要返回的值。在这段代码中,一个转换器根据 ToggleButton
是否被选中来设置 ToggleButton
的标题。此外,还有一个转换器,如果 TextBox
没有内容,则禁用该按钮。在这种情况下,在 ConverterParameter
中使用“reverse
”来翻转结果。
<ToggleButton Name="ShowHideButton" HorizontalAlignment="Center"
IsEnabled="{Binding Text,
ElementName=ReasonTextBox,
Converter={local:IsNullOrWhiteSpaceConverter},
ConverterParameter=reverse}"
Content="{Binding IsChecked,
ElementName=ShowHideButton,
Converter={local:IsFalseConverter},
ConverterParameter=Show:Hide}"/>
由于我使用 MarkupExtension
,因此绑定使用的转换器参数指定了与转换器命名空间相关的 namespace
前缀以及转换器的类名。只需要像普通的绑定语法一样为一个带有转换器的绑定进行操作,并包含一个转换器参数,该参数指定 true
和 false
结果要返回的值。在这段代码中,一个转换器根据 ToggleButton
是否被选中来设置 ToggleButton
的标题。此外,还有一个转换器,如果 TextBox
没有内容,则禁用该按钮。在这种情况下,使用‘reverse
’在 ConverterParameter
中翻转结果。
IsFalseConverter & IsTrueConverter IMultivalueConverter
在几个转换器中,我包含了 IMultiValueConverter
的实现。对于 IsFalseConverter
的 IMultiValueConverter
实现,该实现要求所有值都解析为 false
。
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var isFalse = values.All(i => (i != null) &&
(i as bool? == false || i.ToString() == "false"));
return ConverterHelper.Result<bool?>(isFalse, i => i, targetType, parameter);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
从代码可以看出,这不像 IValueConverter
那样灵活,尽管可以使其更灵活,但好处可能很小,因为 IValueConverter
可以用于 MultiBinding
中的每个 Binding
。我更改了第一行的计算,以便首先检查 null
,避免了后续的检查。最初,我让 null
值传播,让 ToString()
方法上的 null
检查捕获 null
值,但我怀疑早期的 null
检查会提高性能。
以下行应加以解释
var isFalse = values.All(i => (i != null) &&
(i as bool? == false || i.ToString() == "false"));
通常,使用 (i as bool?) == false
没有问题,因为自动转换通常会转换为所需的 bool
类型,但是在 MultiBinding
中的 Binding
中使用转换器时,值会被转换为 string
,因此还需要 i.ToString() == "false"
。我已更改计算,以便首先检查 null
,避免了后续的检查。最初,我让 null
值传播,并让 ToString()
方法上的 null
检查捕获 null
值,但我怀疑早期的 null
检查会提高性能。
这是一个在 MultiBinding
中使用 IsTrueConverter
的示例
<RowDefinition.Height>
<MultiBinding Converter="{converterHelperSample:IsTrueConverter}"
ConverterParameter="*:0">
<Binding Converter="{converterHelperSample:IsTrueConverter}"
ElementName="ShowHideButton"
Path="IsChecked" />
<Binding Converter="{converterHelperSample:IsNullOrEmptyConverter}"
ConverterParameter="reverse"
ElementName="ReasonTextBox"
Path="Text" />
</MultiBinding>
</RowDefinition.Height>
IsEqualConverter 多值比较
添加的一个扩展是能够为特定的输入值返回一个特定的值
<TextBlock Grid.Row="4"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding ElementName=TestComboBox,
Path=Text,
Converter={converterHelperSample:IsEqualConverter},
ConverterParameter='a?a selected:b?b selected:other value selected'}" />
因此,在此示例中,当输入值为 'a
' 时,返回值是 'a selected
';当输入值为 'b
' 时,返回值是 'b selected
';否则,值为 'other value selected
'。
IsEqualConverter
代码如下
public sealed class IsEqualConverter : MarkupExtension, IValueConverter, IMultiValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null || parameter == null) return DependencyProperty.UnsetValue;
return ConverterHelper.ResultWithParameterValue(
p => String.Equals(value.ToString(), p, StringComparison.CurrentCultureIgnoreCase),
targetType, parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{ throw new NotImplementedException(); }
}
支持此功能的 ConverterHelper
类中的代码是
public static object ResultWithParameterValue(Func<string, bool?> comparer, Type targetType,
object parameter, object nullValue = null, object trueValue = null, object falseValue = null)
{
var parameterString = parameter.ToString();
var compareItems = parameterString.Split(':').Select(i => i.Split('?').ToArray()).ToArray();
if (compareItems.Length > 1)
{
for (int i = 0; i < compareItems.Length - 1; i++)
{
for (int j = 0; j < compareItems[i].Length - 1; j++)
{
// ":" used as false value because it cannot be in the result string
var returnValue = Result(comparer(compareItems[i][j]), typeof(string), compareItems[i][j],
null, compareItems[i][compareItems[i].Length - 1], ":");
if (returnValue.ToString().ToLower() != ":") return ConvertToType(returnValue, targetType);
}
}
return ConvertToType(compareItems.Last().First(), targetType);
}
return ConvertToType(parameterString, targetType);
}
基本上,发生的事情是创建一个 string
数组,使用“:
”和“?
”上的 Split
方法。然后,它会假设顶层数组的所有元素至少有两个元素,最后一个元素只有一个。然后,它可以迭代数组,查找匹配项。
在使用 Result
方法时遇到问题,修复方法是:如果 Result
方法没有指定 false
值,则返回 string
":
"。这在某些情况下可能会导致一些问题,但考虑到由于“:
”用作分隔符,无法将其用于字符串之一,因此可能性不大。ResultWithParameterValue
仅与 IsEqualConverter
一起使用。
未来
我只创建了几个使用此辅助类可能创建的转换器。我在示例中包含了以下转换器
IsCollectionNotEmptyConverter
:如果类型是IEnumerable
并且有任何值,则返回true
值。IsEqualConverter
:当值参数 (Path
) 的ToString()
等于参数参数 (ConveterParameter
) 中“?
”之前的字符串时,返回true
值。它也可以用作IMultiValueConverter
,在这种情况下,所有值都必须等于ConverterParameter
中指定的值,或者彼此相等。IsTrueConverter
:与IsFalseConverter
类似,但相反。IsNullConverter
:当值参数为null
时,返回true
值。IsNullOrEmptyConverter
:当值参数为null
时,或当ToString()
值对IsNullOrWhiteSpace
方法的新参数。SignConverter
:如果值为正,则返回ConverterParameter
中的第一个值;如果为负,则返回第二个值;如果为0
,则返回最后一个值。
我相信这个实现比使用许多转换器是一个巨大的进步,它更灵活,并且易于开发人员在代码中使用和理解,因为它使用了类似于 C# 语言中的内容。它也非常容易扩展。
理想情况下,我希望能够将 lambda 表达式作为 ConverterParameter
传递。在简单的情况下,这不会增加很多文本,因为我可以使用类似于当前实现的条件运算符,但这需要比我投入到这个实现中的更多时间来开发。通过 lambda 表达式可以做到的事情会强大得多,因为我可以使用它来进行计算。
我个人认为微软应该继续增强 **XAML** 基础设施,包括能够在使用 lambda 表达式进行绑定。这将多次消除使用 IValueConverter
和触发器的需要。实在太可惜了,因为我认为 **XAML** 技术非常棒,而且本可以更好。
集合项的转换器
我创建了一个提示,展示了如何使用这个辅助类和一个 Binding
到 ObservableCollection
的 Count
属性来确定集合是否包含项,然后可以使用它来控制 View
的元素——IValueConverter 确定集合是否包含项。
历史
- 2015/08/09:初始版本
- 2015/05/11:2015 版本
- 2016/07/20:Bug 修复
- 2016/07/29:改进
IsEqualConverter
以处理更多选项 - 2016/10/05:为多个转换器添加了 XML 注释
- 2017/08/11:包含对绑定到
ObservableCollection
Count
属性的提示的引用