可绑定的转换器、转换器参数和 StringFormat






4.87/5 (12投票s)
如何绕过 Binding 的 Converter、ConvertParameter 和 StringFormat 无法指定为动态 Bindings 的限制。
引言
在本文中,我们开发了克服 WPF 和 Silverlight 5 中 ConverterParameter
无法指定为动态 Binding
的限制的方法。这些解决方案是对文章 Bindingable Converter Parameter 以及其他博客和论坛中描述的解决方案的替代方案。本文提出的解决方案的特点是:
- 辅助代码不多
- 现有实现
IValueConverter
的转换器无需修改即可使用 - 在数据模板中有效
- 支持 RelativeSource
- 每个元素每种可绑定
ConvertParameter
的绑定数量没有限制。 - 支持双向绑定
- 不仅支持绑定到
ConverterParameter
,也支持绑定到Converter
。 - 支持绑定到 StringFormat (2.0 版新增)
- Silverlight 5 支持(请参阅最后一节)
背景
对于 WPF 和 Windows 中的 Binding,可以使用转换器来自定义外观。为了将额外信息传递给转换器,可以使用 ConverterParameter
。但是,Converter
和 ConverterParameter
都不能指定为 Binding
,许多开发人员认为这是一个限制。一些博客和论坛主题建议创建一个派生自 DependencyObject
的转换器。然后,转换器本身可以具有在 XAML 中分配给绑定的依赖属性。但是,这在动态上下文和数据模板中不起作用。
正如在一些地方指出的,MultiBinding
在许多情况下可能是一个不错的选择。在本文中,我们将从这个角度入手,开发一些辅助工具使其更加容易。
步骤 1:在 WPF 中使用 MultiBinding
正如在一些博客和论坛中所述,我们应该首先考虑简单地使用 MultiBinding
,将转换器参数绑定指定为第二个绑定,如下所示:
<TextBox>
<TextBox.Text>
<MultiBinding Converter="{StaticResource WrappedTestValueConverter}" >
<Binding Path="FirstName" />
<Binding Path="ConcatSign" />
</MultiBinding>
</TextBox.Text>
</TextBox>
在这种情况下,我们还必须更改 Converter
以实现 IMultiValueConverter
接口。最简单的方法是将 IMultiValueConverter
实现添加到原始转换器中,如下所示:
#region IMultiValueConverter Members
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return Convert(values[0], targetType, values[1], culture);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new object[] { ConvertBack(value, targetTypes[0], parameter, culture) };
}
#endregion
上面的代码调用原始 IValueConverter
的 Convert
和 ConvertBack
方法。
如果您不想或无法更改原始转换器,可以使用通用的“适配器”IMultiValueConverter
,它只需将转换委托给原始 IValueConverter
实现。下面是一个该适配器的通用实现:
[ContentProperty("Converter")]
public class MultiValueConverterAdapter : IMultiValueConverter
{
public IValueConverter Converter { get; set; }
private object lastParameter;
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (Converter == null) return values[0]; // Required for VS design-time
if (values.Length > 1) lastParameter = values[1];
return Converter.Convert(values[0], targetType, lastParameter, culture);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
if (Converter == null) return new object[] { value }; // Required for VS design-time
return new object[] { Converter.ConvertBack(value, targetTypes[0], lastParameter, culture) };
}
}
要使用适配器,您可以在资源部分简单地将原始 IValueConverter
实例声明包装起来,如下所示:
<local:MultiValueConverterAdapter x:Key="WrappedTestValueConverter">
<local:TestValueConverter />
</local:MultiValueConverterAdapter>
通过此解决方案,我们无需在 XAML 代码中使用任何自定义绑定,只需标准的 MultiBinding
,它也可以在模板和样式中使用。所有必需的更改都在转换器端完成。使用通用适配器,我们根本不会修改原始转换器。
此方法的一个限制是它不能完全支持双向绑定。问题在于作为第二个值传递给 Convert
方法的参数在调用 ConvertBack
时将不可用。如果参数未在反向转换中使用,这不会成为问题。为了部分规避此问题,可以存储 Convert
中的参数并在 ConvertBack
中重用,如上面 MultiValueConverterAdapter
实现所示。但是,这在同一转换器实例用于多个 XAML 元素的情况下不起作用,例如在数据模板化的列表中。有关解决方案,请继续阅读。
步骤 2:一个新的标记扩展
为了解决共享转换器实例在双向场景中导致的问题,我们可以创建一个新的简单标记扩展。这样,我们还可以获得更易于理解的 XAML 代码,并且 XAML 作者不必像上面那样包装值转换器资源声明。
自定义标记扩展的完整实现如下所示:
public class ConverterBindableBinding : MarkupExtension
{
public Binding Binding { get; set; }
public IValueConverter Converter { get; set; }
public Binding ConverterParameterBinding { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
MultiBinding multiBinding = new MultiBinding();
multiBinding.Bindings.Add(Binding);
multiBinding.Bindings.Add(ConverterParameterBinding);
MultiValueConverterAdapter adapter = new MultiValueConverterAdapter();
adapter.Converter = Converter;
multiBinding.Converter = adapter;
return multiBinding.ProvideValue(serviceProvider);
}
}
对于标记扩展,每当要将扩展应用于 XAML 元素时,都会调用 ProvideValue
。在此方法中,我们所做的只是以编程方式创建一个新的 MultiBinding
,将原始绑定和转换器参数作为子绑定,并创建一个新的、干净的 MultiValueConverterAdapter
实例。您会看到这与我们上面在 XAML 中所做的非常相似,但不同之处在于,每次使用绑定时,我们都会获得适配器的新实例。
现在,我们可以用这个标记扩展替换上面的 MultiBinding
XAML 代码,如下所示:
<TextBox>
<TextBox.Text>
<local:ConverterBindableBinding Binding="{Binding FirstName}"
Converter="{StaticResource TestValueConverter}"
ConverterParameterBinding="{Binding ConcatSign}" />
</TextBox.Text>
</TextBox>
您可以看到,我们可以使用原始的 TestValueConverter
,而无需对其源代码或资源定义进行任何更改。此外,通过此解决方案,与上面的 MultiBinding
解决方案相比,更容易理解 ConcatSign
绑定用作 ConverterParameter
的源。因此,即使您没有双向绑定问题,您很可能也想使用此解决方案。
在使用此标记扩展的 Style
上下文中仍然存在一个限制,因为在这种情况下,将在应用样式的所有位置使用在 ProvideValue
方法中创建的 MultiBinding
和 MultiValueConverterAdapter
的相同实例。因此,如果您需要使用 ConvertBack
实现中的转换器参数的双向绑定,请不要在样式中使用它。
可绑定的转换器
上述解决方案现在可以轻松扩展,以支持绑定到 Converter
,而不仅仅是 ConverterParameter。我们只需在 ConverterBindableBinding
中添加一个新的属性 ConverterBinding
,并将其作为第三个绑定传递给
。有关详细信息,请参阅提交的源代码。MultiConverte
rValueAdapter
支持 StringFormat(2.0 版新增)
在新版本(2.0)中,我还添加了对 StringFormat
的支持。您甚至可以使用 StringFormatBinding
属性动态设置字符串格式。为此,我创建了一个新的 BindableBinding
类,其用法如下:
<TextBox>
<TextBox.Text>
<local:BindableBinding Path="FirstName"
Converter="{StaticResource TestValueConverter}"
ConverterParameterBinding="{Binding ConcatSign}"
StringFormatBinding="{Binding CurrentStringFormat}"/>
</TextBox.Text>
</TextBox>
Silverlight 5 解决方案
上述解决方案适用于 WPF。对于 Silverlight 5,您可以使用我在文章“Silverlight 5 中的 MultiBinding”中提出的解决方案。这个 MultiBinding
实现开箱即用地支持可绑定的 ConverterParameter
和 Converter
。与 WPF 的 MultiBinding
不同,它还直接支持 IValueConverter
的使用,因此不需要适配器转换器。在这种情况下,XAML 代码将如下所示:
<TextBox Text="{z:MultiBinding Converter={StaticResource TestValueConverter}
ConverterParameter={Binding ConcatSign} Binding1={Binding FirstName} }" />
此 Silverlight 解决方案也可以在 Style
中使用,而不会在进行双向绑定时出现共享资源的麻烦。
历史
- 2012 年 9 月 17 日 - 提交第一个版本。
- 2014 年 9 月 19 日 - 提交 2.0 版,支持 StringFormat。