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

WPF IValueConverter/IMultiValueConverter 辅助类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.17/5 (14投票s)

2015年8月9日

CPOL

10分钟阅读

viewsIcon

25127

downloadIcon

258

提供一个类,帮助创建灵活的值和多值转换器,这些转换器使用ConverterParameter通过?:运算符指定true和false结果的返回值。

引言

我发现 WPF 绑定中有很多情况,当我想要做一个评估,结果是 truefalse,并返回一个取决于该结果的值。这类似于 C# 的条件运算符。WPF 的一个常见需求是根据一个标志值使控件 VisibleCollapsed

有几种选择。一种是为每种情况构建一个 ValueConverter,但这会不断添加转换器,这需要时间,并会向解决方案添加模块和代码。另一种是向 ViewModel 添加属性,但这会将视图特有的信息(例如 Visibility.VisibleVisibility.Collapsed)添加到 ViewModel 中,并增加 ViewModel 的复杂性——我认为在 ViewModel 中有两个 bool 值,一个与另一个相反,或者将枚举转换为二进制值是一种糟糕的代码气味。

我创建了许多 ValueConverters,它们基本上评估传递的布尔值,并返回一个取决于该结果的值;转换器通常仅设计用于特定的转换。有一些框架包含一个转换器,当绑定值为 true 时,将其转换为 Visibilbiy.Visible,否则转换为 Visibility.Collapsed。但是,在某些情况下,可见性会因控件而异,取决于属性的值,或者我们希望是 Visibility.Hidden 而不是 Collapsed。这意味着要么必须创建多个转换器,要么需要在 ViewModel 中设置多个依赖属性。

为了尽量减少所需的转换器数量,我首先添加了通过 ConverterParameter 传递“Reverse”的能力来反转结果。这减少了所需的转换器数量;然而,这仍然意味着有很多转换器。所以我想到一个更好的主意——在 ConverterParameter 中传递所需的值。然后问题是如何格式化 ConverterParameter 中的两个值。这似乎很明显,应该借鉴 C#,所以我像“?:”运算符一样使用了“:”。除其他外,这与 **XAML** 配合得很好。虽然我当前的需求不需要条件,但后来我添加了对条件参数的支持。

起初,我将代码放在每个转换器中,但很快就决定我想要创建一个辅助类,这意味着每个转换器共享一个通用的代码库。这使得每个转换器都非常简单,并支持轻松的升级和错误修复。如果未包含 ConverterParameter,则默认值为布尔 truefalse,但也保留了在 ConverterParameter 中指定“Reverse”的功能,使其类似于在 ConverterParameter 中设置了“false:true”。这很快就得到了回报。

起初,我只返回 string,然后依赖于每个类的 TypeConverterstring 转换为所需的 Type。然而,这对 **Telerik** 控件不起作用,所以不得不使用转换器中的 TypeConverter 来进行转换,以得到正确的类型。

我还添加了在表达式列表中为 null 值添加第三个值的能力,它被添加到 ConverterParameter 的末尾,并用“:”将其与其余部分分隔开。

后来的一个添加是构建灵活的 IMultiValueConverters。只有三个似乎是有意义的,那就是 IsEqualConverterIsFalseConverterIsTrueConverterIsFalseConverterIsTrueConverter 之间的区别在于,对于 IsFalseConvertertrue,所有值都必须为 false……不是 null,也不是其他什么,而是 falseIsTrueConverter 则完全相反。这些转换器可以与每个绑定的转换器一起使用,这就是为什么 truefalse 值不仅检查 bool truefalse,还检查 string "true" 或 "false"。这使得控件的 VisibilityEnabled 属性依赖于多个值变得非常容易,而无需在 **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?>),用于确定参数参数中指定的哪个 truefalse(或 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 前缀以及转换器的类名。只需要像普通的绑定语法一样为一个带有转换器的绑定进行操作,并包含一个转换器参数,该参数指定 truefalse 结果要返回的值。在这段代码中,一个转换器根据 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 前缀以及转换器的类名。只需要像普通的绑定语法一样为一个带有转换器的绑定进行操作,并包含一个转换器参数,该参数指定 truefalse 结果要返回的值。在这段代码中,一个转换器根据 ToggleButton 是否被选中来设置 ToggleButton 的标题。此外,还有一个转换器,如果 TextBox 没有内容,则禁用该按钮。在这种情况下,使用‘reverse’在 ConverterParameter 中翻转结果。

IsFalseConverter & IsTrueConverter IMultivalueConverter

在几个转换器中,我包含了 IMultiValueConverter 的实现。对于 IsFalseConverterIMultiValueConverter 实现,该实现要求所有值都解析为 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** 技术非常棒,而且本可以更好。

集合项的转换器

我创建了一个提示,展示了如何使用这个辅助类和一个 BindingObservableCollectionCount 属性来确定集合是否包含项,然后可以使用它来控制 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 属性的提示的引用
© . All rights reserved.