WPF 中管道式值转换器






4.90/5 (47投票s)
演示如何将 WPF 数据绑定中使用的值转换器链接在一起。
引言
本文探讨了如何在 Windows Presentation Foundation 的数据绑定环境中,将多个物理值转换器组合成一个逻辑值转换器。预计读者已经熟悉 WPF 的数据绑定以及使用 XAML 声明用户界面元素。本文不解释 WPF 数据绑定的基础知识,但如果需要,读者可以参考 Windows SDK 中的这篇文章以获得 WPF 数据绑定的全面概述。
本文提供的代码是在 .NET Framework 3.0 的 2006 年 6 月 CTP 版本上编译和测试的。
背景
WPF 的数据绑定基础结构非常灵活。促成这种灵活性的一个主要因素是,可以在两个绑定对象(即数据源和目标)之间注入自定义值转换器。值转换器可以被看作是一个黑盒子,它接收一个值,并输出另一个值。
值转换器是任何实现了 IValueConverter 接口的对象。该接口暴露了两个方法:Convert
和 ConvertBack
。当绑定值从数据源传递到目标时,会调用 Convert
;反之亦然。如果值转换器确定无法根据输入值返回有意义的输出值,它可以返回 Binding.DoNothing
,这将通知数据绑定引擎不要将输出值推送到绑定操作的相应目标。
问题
WPF 的数据绑定支持允许 Binding
对象拥有一个值转换器,该转换器可以被赋值给它的 Converter
属性。为绑定操作使用单个转换器是有限制的,因为它迫使你的自定义值转换器类变得非常具体。例如,如果你需要根据数字 XML 属性的值来确定用户界面中文本的颜色,你可能会倾向于创建一个值转换器,该转换器将 XML 属性值转换为数字,然后将该数字映射到枚举值,然后将该枚举值映射到 Color
,最后从该颜色创建 Brush
。这种技术可以工作,但该值转换器在许多场景下将无法重用。
最好是能够创建一个模块化的值转换器库,然后以某种方式将它们管道连接起来,就像大多数命令行环境允许将一个命令的输出作为另一个命令的输入一样。
解决方案
为了解决这个问题,我创建了一个名为 ValueConverterGroup
的类。ValueConverterGroup
类本身也是一个值转换器(它实现了 IValueConverter
),允许你将多个值转换器组合成一个集合。当调用 ValueConverterGroup
的 Convert
方法时,它会将调用委托给它所包含的每个值转换器的 Convert
方法。添加到组中的第一个转换器先调用,最后一个转换器后调用。调用 ConvertBack
方法时,情况则相反。
一个转换器在组中的输出成为下一个转换器的输入,而最后一个转换器的输出被返回给 WPF 数据绑定引擎,作为整个组的输出值。为了方便起见,我使得可以直接在 XAML 文件中声明 ValueConverterGroup
及其子值转换器。
Using the Code
以下是一个简短的演示,展示了如何使用 ValueConverterGroup
类。整个演示可以在本文的顶部下载。
以下是演示中使用的简单 XML 数据
<?xml version="1.0" encoding="utf-8" ?>
<Tasks>
<Task Name="Paint the living room" Status="0" />
<Task Name="Wash the floor" Status="-1" />
<Task Name="Study WPF" Status="1" />
</Tasks>
Status XML 属性将映射到此枚举类型的值
public enum ProcessingState
{
[Description("The task is being performed.")]
Active,
[Description( "The task is finished." )]
Complete,
[Description( "The task is yet to be performed." )]
Pending,
[Description( "" )]
Unknown
}
以下是一些自定义值转换器,它们将被管道连接起来,将 Status 值转换为 SolidColorBrush
[ValueConversion( typeof( string ), typeof( ProcessingState ) )]
public class IntegerStringToProcessingStateConverter : IValueConverter
{
object IValueConverter.Convert(
object value, Type targetType, object parameter, CultureInfo culture )
{
int state;
bool numeric = Int32.TryParse( value as string, out state );
Debug.Assert( numeric, "value should be a String which contains a number" );
Debug.Assert( targetType.IsAssignableFrom( typeof( ProcessingState ) ),
"targetType should be ProcessingState" );
switch( state )
{
case -1:
return ProcessingState.Complete;
case 0:
return ProcessingState.Pending;
case +1:
return ProcessingState.Active;
}
return ProcessingState.Unknown;
}
object IValueConverter.ConvertBack(
object value, Type targetType, object parameter, CultureInfo culture )
{
throw new NotSupportedException( "ConvertBack not supported." );
}
}
// *************************************************************
[ValueConversion( typeof( ProcessingState ), typeof( Color ) )]
public class ProcessingStateToColorConverter : IValueConverter
{
object IValueConverter.Convert(
object value, Type targetType, object parameter, CultureInfo culture )
{
Debug.Assert(value is ProcessingState, "value should be a ProcessingState");
Debug.Assert( targetType == typeof( Color ), "targetType should be Color" );
switch( (ProcessingState)value )
{
case ProcessingState.Pending:
return Colors.Red;
case ProcessingState.Complete:
return Colors.Gold;
case ProcessingState.Active:
return Colors.Green;
}
return Colors.Transparent;
}
object IValueConverter.ConvertBack(
object value, Type targetType, object parameter, CultureInfo culture )
{
throw new NotSupportedException( "ConvertBack not supported." );
}
}
// *************************************************************
[ValueConversion( typeof( Color ), typeof( SolidColorBrush ) )]
public class ColorToSolidColorBrushConverter : IValueConverter
{
object IValueConverter.Convert(
object value, Type targetType, object parameter, CultureInfo culture )
{
Debug.Assert( value is Color, "value should be a Color" );
Debug.Assert( typeof( Brush ).IsAssignableFrom( targetType ),
"targetType should be Brush or derived from Brush" );
return new SolidColorBrush( (Color)value );
}
object IValueConverter.ConvertBack( object value, Type targetType,
object parameter, CultureInfo culture )
{
Debug.Assert(value is SolidColorBrush, "value should be a SolidColorBrush");
Debug.Assert( targetType == typeof( Color ), "targetType should be Color" );
return (value as SolidColorBrush).Color;
}
}
最后是 Window 的 XAML(粗体部分为相关内容)
<Window x:Class="PipedConverters.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:PipedConverters" Title="PipedConverters" Height="300" Width="300" FontSize="18" > <Window.Resources> <!-- Loads the Tasks XML data. --> <XmlDataProvider x:Key="xmlData" Source="..\..\data.xml" XPath="Tasks/Task" /> <!-- Converts the Status attribute text to the display name for that processing state. --> <local:ValueConverterGroup x:Key="statusDisplayNameGroup"> <local:IntegerStringToProcessingStateConverter /> <local:EnumToDisplayNameConverter /> </local:ValueConverterGroup><!-- Converts the Status attribute text to a SolidColorBrush used to draw the output of statusDisplayNameGroup. --> <local:ValueConverterGroup x:Key="statusForegroundGroup"> <local:IntegerStringToProcessingStateConverter /> <local:ProcessingStateToColorConverter /> <local:ColorToSolidColorBrushConverter /> </local:ValueConverterGroup>
<!-- Converts the Status attribute to the tooltip message for that processing state. --> <local:ValueConverterGroup x:Key="statusDescriptionGroup"> <local:XmlAttributeToStringStateConverter /> <local:IntegerStringToProcessingStateConverter /> <local:EnumToDescriptionConverter /> </local:ValueConverterGroup>
<DataTemplate x:Key="taskItemTemplate"> <StackPanel Margin="2" Orientation="Horizontal" ToolTip="{Binding XPath=@Status, Converter={StaticResource statusDescriptionGroup}}" > <TextBlock Text="{Binding XPath=@Name}" /> <TextBlock Text=" (" xml:space="preserve" /> <TextBlock Text="{Binding XPath=@Status, Converter={StaticResource statusDisplayNameGroup}}" Foreground="{Binding XPath=@Status, Converter={StaticResource statusForegroundGroup}}" /> <TextBlock Text=")" /> </StackPanel> </DataTemplate> </Window.Resources> <Grid > <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition /> </Grid.RowDefinitions> <TextBlock Background="Black" Foreground="White" HorizontalAlignment="Stretch" Text="Tasks" TextAlignment="Center" /> <ItemsControl Grid.Row="1" DataContext="{StaticResource xmlData}" ItemsSource="{Binding}" ItemTemplate="{StaticResource taskItemTemplate}" /> </Grid> </Window>
上面声明的窗口如下所示(注意任务状态文本的颜色取决于状态值)
如上所示的 XAML,演示项目创建了几个 ValueConverterGroup
。其中决定状态文本前景的组包含三个子值转换器。第一个转换器将 Status 属性值从数字转换为 ProcessingState
枚举值。下一个转换器将枚举值映射到用于图形表示该处理状态的 Color
。组中的最后一个转换器从前一个转换器输出的颜色创建 SolidColorBrush
。
上述聚合值转换方法使得值转换器能够保持简单并具有明确定义的用途。这个巨大的优势付出的代价很小。用于 ValueConverterGroup
的值转换器有一个要求。值转换器类必须且仅必须用 System.Windows.Data.ValueConversionAttribute
属性进行装饰。 该属性用于指定转换器期望输入值的类型,以及它将输出的对象类型。
要理解此限制为何存在,有必要深入了解 ValueConverterGroup
类的工作原理以及它必须满足的要求。
工作原理
本文的其余部分讨论了 ValueConverterGroup
类的工作原理。阅读此部分对于使用该类并非必需。
ValueConverterGroup
类相对简单。它只是将其 Convert
和 ConvertBack
方法的调用委托给它所包含的值转换器。创建这个类有两个方面需要额外规划才能正确处理。首先让我们看看它对 IValueConverter.Convert
的实现。
object IValueConverter.Convert(
object value, Type targetType, object parameter, CultureInfo culture )
{
object output = value;
for( int i = 0; i < this.Converters.Count; ++i )
{
IValueConverter converter = this.Converters[i];
Type currentTargetType = this.GetTargetType( i, targetType, true );
output = converter.Convert( output, currentTargetType, parameter, culture );
// If the converter returns 'DoNothing'
// then the binding operation should terminate.
if( output == Binding.DoNothing )
break;
}
return output;
}
将输入值转换为输出值的过程需要我们调用组中的每个值转换器。这很简单。问题是每个值转换器对 targetType
参数都有特定的期望。整个转换过程可能需要输出值为 Brush
,但组中的中间转换器可能对它们应该输出的对象类型有完全不同的期望。
例如,本节前面部分所示的演示将字符串(包含一个整数值)转换为 SolidColorBrush
。在此过程中,它将整数转换为 ProcessingState
枚举值,然后将该值转换为 Color
,最后将 Color
转换为 SolidColorBrush
。只有组中的最后一个转换器期望输出的是 Brush
,因此只有该转换器应该接收传递到 ValueConverterGroup
的 Convert
方法的原始目标类型值。
解决此问题的方法是要求添加到组中的所有值转换器都用 ValueConversionAttribute
进行装饰。这是强制执行此要求的代码。
/* In the ValueConverterGroup class */
// Fields
private readonly ObservableCollection<IValueConverter> converters =
new ObservableCollection<IValueConverter>();
private readonly Dictionary<IValueConverter,ValueConversionAttribute>
cachedAttributes = new Dictionary<IValueConverter,ValueConversionAttribute>();
// Constructor
public ValueConverterGroup()
{
this.converters.CollectionChanged +=
this.OnConvertersCollectionChanged;
}
// Callback
void OnConvertersCollectionChanged(
object sender, NotifyCollectionChangedEventArgs e )
{
// The 'Converters' collection has been modified, so validate that each
// value converter it now contains is decorated with ValueConversionAttribute
// and then cache the attribute value.
IList convertersToProcess = null;
if( e.Action == NotifyCollectionChangedAction.Add ||
e.Action == NotifyCollectionChangedAction.Replace )
{
convertersToProcess = e.NewItems;
}
else if( e.Action == NotifyCollectionChangedAction.Remove )
{
foreach( IValueConverter converter in e.OldItems )
this.cachedAttributes.Remove( converter );
}
else if( e.Action == NotifyCollectionChangedAction.Reset )
{
this.cachedAttributes.Clear();
convertersToProcess = this.converters;
}
if( convertersToProcess != null && convertersToProcess.Count > 0 )
{
foreach( IValueConverter converter in convertersToProcess )
{
object[] attributes = converter.GetType().GetCustomAttributes(
typeof( ValueConversionAttribute ), false );
if( attributes.Length != 1 )
throw new InvalidOperationException( "All value converters added to a " +
"ValueConverterGroup must be decorated with the " +
"ValueConversionAttribute attribute exactly once." );
this.cachedAttributes.Add(
converter, attributes[0] as ValueConversionAttribute );
}
}
}
当一个值转换器被添加到 Converters
属性时(此处未显示),将执行 OnConvertersCollectionChanged
方法,如果任何转换器没有用 ValueConversionAttribute
进行装饰,它将抛出异常。出于性能原因,一旦检索到与值转换器关联的属性实例,就会将其缓存。
由于组中的每个值转换器都保证解释了它期望处理的类型,因此 Convert
方法可以确定每个转换器的目标类型。如上面的 Convert
方法所示,在执行值转换器之前调用以下方法:
protected virtual Type GetTargetType(
int converterIndex, Type finalTargetType, bool convert )
{
// If the current converter is not the last/first in the list,
// get a reference to the next/previous converter.
IValueConverter nextConverter = null;
if( convert )
{
if( converterIndex < this.Converters.Count - 1 )
{
nextConverter = this.Converters[converterIndex + 1];
}
}
else
{
if( converterIndex > 0 )
{
nextConverter = this.Converters[converterIndex - 1];
}
}
if( nextConverter != null )
{
ValueConversionAttribute attr = cachedAttributes[nextConverter];
// If the Convert method is going to be called,
// we need to use the SourceType of the next
// converter in the list. If ConvertBack is called, use the TargetType.
return convert ? attr.SourceType : attr.TargetType;
}
// If the current converter is the last one to be executed return the target
// type passed into the conversion method.
return finalTargetType;
}
该方法只是检查即将执行的转换器是否是组中的最后一个或第一个。如果正在调用 Convert
方法,并且当前转换器不是组中的最后一个,则目标类型值从与列表中下一个转换器关联的 ValueConversionAttribute
实例的 SourceType
属性中检索(调用 ConvertBack
时,转换器执行顺序相反)。如果正在执行 ConvertBack
,则前一个转换器的 TargetType
成为当前转换器的目标类型。请注意,处理 ConvertBack
时,源和目标的语义会交换。
创建这个类第二个让我没立即想到的是如何方便地在 XAML 中将值转换器添加到组中。这并不是一段很难写的代码,只是我花了一段时间才找到方法 ;)
[System.Windows.Markup.ContentProperty("Converters")]
public class ValueConverterGroup : IValueConverter
{
}
基本上,该属性会通知 WPF 基础结构,此类中的 Converters
属性是在 XAML 中添加子对象时要添加到的属性。将 ContentPropertyAttribute
添加到该类后,就可以像这样在 XAML 中使用 ValueConverterGroup
类:
<local:ValueConverterGroup x:Key="someConverterGroup">
<local:MyCustomConverter />
<local:YourCustomConverter />
</local:ValueConverterGroup>
结论
通过管道连接值转换器,可以更轻松地在许多方面使它们可重用。
添加到 ValueConverterGroup
的每个值转换器都必须且仅一次用 ValueConversionAttribute
进行装饰。
当发生 Convert
操作时,组中的值转换器将按照它们在 Converters
集合中的顺序执行。当调用 ConvertBack
时,它们的执行顺序是最后一个到第一个。