WPF/Silverlight 实现 IValueConverter 的通用方法





5.00/5 (11投票s)
IValueConverter 的通用实现。
引言
我最初创建了一个更简单的值转换器,用于将bool
属性转换为自定义值,特别是将布尔值转换为Visibility
。这已在CodeProject上发布,文章名为“Generic WPF/Silverlight IValueConverter
”(https://codeproject.org.cn/Articles/289495/Generic-WPF-Silverlight-Value-Converter)。我厌倦了为细微的值更改而重复造轮子,以及在我的应用程序中充斥着大量的 值转换器。最近,我正在开发一个新项目,真的不想为每个特殊状态添加一个bool
属性。这几乎和为Visibility
添加一个特殊属性一样糟糕。我想到在我的转换器中添加第三个可自定义的属性。所以现在我有一个更灵活的值转换器。
背景
我最初创建了以下非常基本的转换器,它允许我绑定到一个bool
属性并返回由IValueConverter
上的两个属性定义的其他内容。
public class IfTrueValueConverter : IValueConverter
{
public object TrueValue { get; set; }
public object FalseValue { get; set; }
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (TrueValue == null)
{
TrueValue = true;
FalseValue = false;
}
return (bool)value ? TrueValue : FalseValue;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (TrueValue == null)
{
TrueValue = true;
FalseValue = false;
}
return (value.ToString() == TrueValue.ToString());
}
}
用于此转换器的XAML非常类似于使用简单转换器,不同之处在于 true 和 false 值是在资源中定义转换器时定义的。
<Window x:Class="GenericValueConverter.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:GenericValueConverter"
Title="IfTrueValueConverter example" Height="350" Width="525">
<Window.Resources>
<local:IfTrueValueConverter x:Key="VisibilityConverter"
TrueValue="Visible" FalseValue="Hidden"/>
</Window.Resources>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBox Name="TestControl" Text="Test Message" Margin="5"
Visibility="{Binding IsVisible,
Converter={StaticResource VisibilityConverter}}"/>
</StackPanel>
</Window>
正如您所见,首先我们需要在XAML的Window.Resources
元素中定义值转换器。这只是定义任何值转换器以供使用,只是多了两个额外的参数:TrueValue
和FalseValue
。只需放入所需的枚举的字符串值,以便在ViewModel
的值为 true 和 false 时使用。WPF的转换功能可以将这些字符串值转换为枚举,因此转换器可以正常工作。该转换器还可以用于设置颜色,因此可以将“Red”和“Black”的字符串值分配给TrueValue
和FalseValue
,然后可以使用该转换器来设置前景色画笔。
现在使用转换器就像使用任何转换器一样,正如在TextBox
的XAML中可以看到的。
类型转换
在我改进这个转换器的时候,我想到可以通过在第一次时将TrueValue
和FalseValue
属性转换为正确的类型来提高性能,从而节省值转换器转换期间自动转换所需的时间。为此,我使用了TypeConverter
类。该类可以通过使用属性与它所转换的关联类相关联。
[TypeConverter(typeof(MyClassTypeConverter))]
public class MyClass
{
//Class implementation here
}
public class MyClassTypeConverter : TypeConverter
{
public override object ConvertBack(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture,
object value,
Type destinationType)
{
//Conversion code here, returning object;
}
}
Microsoft 为许多标准类提供了类型转换器。只需要使用static
TypeDescriptor.GetConverter
方法,然后使用返回类的ConvertFrom
方法。
TypeConverter converter = TypeDescriptor.GetConverter(targetType);
return converter.ConvertFrom(value);
我对TrueValue
和FalseValue
属性都使用了这种转换,这两个属性都是 object 类型。这意味着只需要进行简单的强制转换即可使用这些属性。Convert
方法中的targetType
参数用于在Convert
方法是第一个执行的方法时确定属性要转换为的类型(ConvertBack
方法也会初始化值,但我认为这可能有点多余)。
增加灵活性的更多属性
为了满足转换器的即时需求,我添加了最初称为Compare
(后来更改为CompareTrue
)的属性。其思想是比较Convert
方法中传递的值参数与该值,如果它们相同,则值转换器的Convert
方法将返回TrueValue
。现在,由于我可以检查值参数的类型,所以我也可以在此属性上使用TypeConverter
。
仅此CompareTrue
属性在不考虑ConvertBack
方法的情况下工作得很好。对于我使用此转换器的大多数情况,我从未真正使用过ConvertBack
方法,但我应该考虑它。因此,我添加了一个CompareFalse
属性。
不幸的是,我不得不考虑我可能正在处理想要比较 null 值的类型。这增加了相当多的复杂性,因为我希望当Convert
方法中的值参数的类型为bool
时,默认值为true
和false
,但对于大多数类型来说,null
是有效的。在初始化期间,除非类型是布尔值用于比较,否则CompareTrue
和CompareFalse
属性将保持为null
,在这种情况下,它们分别更改为bool
值true
和false
。如果CompareTrue
和CompareFalse
属性不为null
,则使用类型的 TypeConverter 来固定这些值。
我添加了最后一个额外的属性,因为我有一个特定的用途,即作为默认值,那就是 null 值。我必须承认,对于一个通用转换器来说,它增加了比其价值更多的复杂性,但所有值类型都可以是Nullable
的,对象也可以是null
。
实现
转换器的代码如下
public class IfTrueValueConverter : IValueConverter
{
// Default for comparison is true and false, otherwise need to have values set in
// XAML definition for value converter
public object TrueValue { get; set; }
public object FalseValue { get; set; }
public object CompareTrue { get; set; }
//Note: CompareFalse is only significant for ConvertBack
public object CompareFalse { get; set; }
public object NullValue { get; set; }
private bool _checkValues;
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (!_checkValues && value != null)
Initialize(targetType, value.GetType());
if (value == null) return
CompareTrue == null ? TrueValue:
(FalseValue == null ? FalseValue : NullValue);
return CompareTrue.Equals(value) ? TrueValue :
(CompareFalse == null ? FalseValue :
(CompareFalse.Equals(value) ? FalseValue : NullValue));
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (!_checkValues && value != null)
Initialize(value.GetType(), targetType);
return TrueValue.Equals(value) ? CompareTrue : CompareFalse;
}
private void Initialize(Type targetType, Type compareType)
{
_checkValues = true;
TrueValue = (TrueValue != null) ? FixValue(targetType, TrueValue) : true;
FalseValue = (FalseValue != null) ? FixValue(targetType, FalseValue) : false;
NullValue = (NullValue != null) ? NullValue = FixValue(targetType, NullValue)
: null;
CompareTrue = (CompareTrue != null) ? FixValue(compareType, CompareTrue) : true;
CompareFalse = (CompareFalse != null) ? FixValue(compareType, CompareFalse) :
((compareType.FullName == (typeof(bool)).FullName) ? (object)false : null);
}
private static object FixValue(Type targetType, object value)
{
if (value.GetType() == targetType)
return value;
try
{
TypeConverter converter = TypeDescriptor.GetConverter(targetType);
return converter.ConvertFrom(value);
}
catch
{
DisplayIssue(targetType, value);
return value;
}
}
[Conditional("DEBUG")]
private static void DisplayIssue(Type targetType, object invalidValue)
{
if (targetType.IsEnum)
{
var enumNames = string.Join(", ", Enum.GetNames(targetType));
MessageBox.Show(string.Format(
"Enumeration value '{0}' not recognized for enumeration type '{1}'. Valid values are {2}.",
invalidValue, targetType, enumNames));
}
else
MessageBox.Show(string.Format(
"The value '{0}' not recognized for target type '{1}'.",
invalidValue, targetType));
}
示例
示例显示了转换器用于更改某些控件的Foreground
和Background
。下面的内容显示了每次选择ComboBox
时的更改。
这的XAML如下
<Window x:Class="ValueConverterExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:local="clr-namespace:ValueConverterExample"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ValueConverter Example"
Height="150"
Width="300">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Window.Resources>
<local:IfTrueValueConverter x:Key="converterOne"
TrueValue="DarkGreen"
FalseValue="DarkGoldenrod"
NullValue="DarkRed"
CompareTrue="Green Color" />
<local:IfTrueValueConverter x:Key="converterTwo"
TrueValue="LightGreen"
FalseValue="LightYellow"
NullValue="Pink"
CompareTrue="Green Color"
CompareFalse="Yellow Color" />
</Window.Resources>
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal"
Background="{Binding SelectedItem,Converter={StaticResource converterTwo}}">
<Label Content="Select a value"
Foreground="{Binding SelectedItem,
Converter={StaticResource converterOne}}" />
<ComboBox Name="comboBox"
Width="100"
SelectedItem="{Binding SelectedItem}"
ItemsSource="{Binding ItemsSource}"
Background="Transparent" />
</StackPanel>
</Window>
我展示了两种情况,一种是未设置CompareFalse
,另一种是设置了CompareFalse
。对于将在Convert
方法中使用的属性(当值既不等于CompareTrue
也不等于CompareFalse
时),有一个明显的论点,但我还没有遇到过这样的需求,所以没有实现这样的属性。此外,这还会引发关于如何处理ConvertBack
返回值的某些问题。
内置调试辅助
您需要修改XAML以显示代码提供的调试辅助功能。所有需要做的就是在XAML中定义转换器时,将TrueValue
或FalseValue
之类的属性之一赋值为无效值。如果Visibility
的TrueValue
更改为“illegal”之类的值,则会出现以下MessageBox
。
这些信息应该有助于修复枚举的绑定问题。对于非枚举,MessageBox
在没有有效枚举值列表的情况下会稍微简单一些。
一个好处是,对话框只在转换器首次运行时显示。
还有其他提供翻译问题反馈的方法,包括写入输出窗口,但我更喜欢向开发人员显示消息。
结论
此IValueConverter
可以处理简单的bool
比较,但这对于大多数应用程序来说已经足够了。请注意,您可以使用此转换器处理的许多地方都可以通过WPF中的触发器来处理;Silverlight目前不支持触发器。但是,我发现使用此转换器然后转换为触发器可以使事情更容易,因为我可以在转换器中使用调试器,而在触发器中则不能。
我发现使用此转换器进行调试的一个问题是,它的用途远不止一个简单的转换器,有时调试会很困难。我使用ConverterParameter
来帮助我调试。