通用WPF/Silverlight值转换器





5.00/5 (9投票s)
一个通用的WPF/Silverlight值转换器。
引言
我不认为人们应该做的事情之一是将WPF/Silverlight枚举(例如Visibility
)直接从ViewModel
传递到View
。所有与ViewModel
的双向绑定都应该在ViewModel
中定义为bool
,即使View
需要其他值来减少耦合。原因之一是,有时ViewModel
中的属性的用法可能与最初的意图不符,而最初使用布尔值bool
可以避免在ViewModel
中进行更改,或者避免创建IValueConverter
来将两个值转换为另外两个值。首选方式应始终是使用IValueConverter
来转换值(bool
或其他值)到WPF。创建一个通用的IValueConverter
来将ViewModel
中的bool
值转换为View
所需的枚举(或其他值)实际上非常容易。这可以是一个IValueConverter
,然后可以在View
的XAML中进行自定义,或者可以在资源字典中定义。这样的转换器还可以提供附加功能来帮助程序员查找错误。
注意
我几乎已经放弃了这个方法,因为我认为IValueConverter
有一个更优雅的解决方案。这个解决方案让我感到不满意的地方之一是我必须在使用它之前在XAML中声明它。我几乎现在总是从MarkupExtension
派生我的转换器,这样我就不必在使用它之前声明转换器。这个转换器与MarkupExtension
不兼容,因为需要设置TrueValue
和FalseValue
。我可以克服这一点,也许我将来会研究这个可能性。最终这可能是一个更好的解决方案。这是关于转换器的文章链接:https://codeproject.org.cn/Articles/1017358/WPF-Converter-Helper-Class。使用帮助类进行转换器时,TrueValue
和FalseValue
的传递方式是通过ConverterParameter
。
背景
令人惊讶的是,我在微软的一个Silverlight项目上工作,当时Visibility
是在ViewModel
中设置的,我实际上需要使用一个值转换器(我相信它是一个bool
)将这个Visibility
转换为bool
以用于其他目的。由于我能做的更改受到行政限制,我无法更改ViewModel
(我理解他们很可能要修复这个问题,但我在修复程序实现之前就离开了项目)。
我可能一开始直接从ViewModel
将WPF枚举传递给View,但我很快就创建了自定义值转换器来完成工作。在ViewModel
中拥有WPF枚举似乎是错误的,并且IValueConverter
接口提供了所需的自定义功能。在创建此代码时,我同时在诅咒微软没有在XAML中提供简单的逻辑来允许简单的转换或简单的方程。
我不喜欢为将布尔值转换为View所需值的每种转换创建一个自定义转换器,因此我编写了一个简单的值转换器,可以根据View的XAML中true和false相关的值进行自定义。
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的转换能力足以将这些string
值转换为枚举,因此转换器可以正常工作。转换器还可以用于设置颜色,因此可以将“Red”和“Black”的字符串值分配给TrueValue
和FalseValue
,然后该转换器可以用于设置Foreground
Brush
。
现在使用转换器就像使用任何转换器一样,正如在TextBox
的XAML中可以看到的那样。
我在编码中广泛使用了这个值转换器,并且考虑写一篇关于这个想法的文章已经有一段时间了,但我知道我可以做得更好。首先,我可以将字符串转换为实际类型,这样就不需要进行字符串转换,除了第一次,这应该会以少量的初始化成本提高性能。其次,我可以做一些错误检查。错误检查的主要目的是帮助程序员找到错误;有很多次我花费了太长时间来查找绑定中的问题,而这些问题实际上非常简单,只是WPF/Silverlight在帮助开发人员处理绑定问题方面做得非常糟糕。这两个目标实际上是相辅相成的,因为将字符串转换为所需值和为开发人员提供反馈都需要将字符串转换为WPF期望的类型。
创建实现的关键方面是字符串与值之间以及值与字符串之间的相互转换。
我经常在WPF中使用枚举,所以很熟悉将字符串转换为枚举以及将枚举转换为字符串值。此外,我主要使用上述值转换器处理枚举,特别是Visibility
,因此最初只考虑为枚举进行检查和转换。当我开始实现我改进的值转换器时,我完成了枚举部分,然后认为它也应该能够将字符串转换为其他对象类型,因为WPF和Silverlight会进行这种转换。弄清楚如何做到这一点有点棘手。
我最初尝试使用System.Convert.ChangeType
方法,但不起作用(我并不惊讶)。研究后发现了TypeConverter
类。通过使用属性,可以将该类与它为其进行翻译的关联类关联起来。
[TypeConverter(typeof(MyClassTypeConverter))]
public class MyClass
{
//Class implementation here
}
public class MyClassTypeConverter : TypeConverter
{
public override object ConvertTo(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture,
object value,
Type destinationType)
{
//Conversion code here, returning object;
}
}
微软为许多标准类提供了类型转换器。所有需要做的就是使用静态TypeDescriptor.GetConverter
方法,然后使用返回类的ConvertFrom
方法。
TypeConverter converter = TypeDescriptor.GetConverter(targetType);
return converter.ConvertFrom(value);
我最初有一个不同的枚举转换代码,因为我可以使用Enum.Parse
方法,但是TypeConverter
两者都适用,并且只使用TypeConverter
消除了代码。
实现
转换器的代码如下:
public class IfTrueValueConverter : IValueConverter
{
public object TrueValue { get; set; }
public object FalseValue { get; set; }
private bool _checkValues;
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (!_checkValues)
Initialize(targetType);
return (bool)value ? TrueValue : FalseValue;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (!_checkValues)
Initialize(value.GetType());
return (value.ToString() == TrueValue.ToString());
}
private void Initialize(Type targetType)
{
_checkValues = true;
if (TrueValue == null)
{
TrueValue = true;
FalseValue = false;
}
else
{
TrueValue = FixValue(targetType, TrueValue);
FalseValue = FixValue(targetType, FalseValue);
}
}
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;
}
}
[ConditionalAttribute("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));
}
}
公共方法几乎与上面的代码相同,只是创建了一个私有的Initialize
方法来从公共方法中移除检查。_checkValues
变量允许在初始化完成后跳过初始化代码。Initialize
方法检查TrueValue
是否已分配(就像上面更简单的实现一样),如果尚未分配,则将其设置为布尔值。接下来,对TrueValue
和FalseValue
变量中的每一个调用私有静态FixValue
函数。FixValue
方法首先检查是否需要转换:如果不需要转换,则返回相同的值(如果尝试转换两次,可能会引发异常)。否则,代码获取targetType
的TypeConverter
,然后进行转换。如果发生异常(无法转换为正确类型的值),代码会捕获异常并返回原始值(这可能不起作用,但WPF会让代码工作)。我已经为转换器编程,以便如果环境处于调试模式,则消息框将告知开发人员异常的原因。这是因为包含MessageBox.Show
方法的该方法是用“ConditionalAttribute("DEBUG")
”修饰的。在此条件方法中,检查目标类型是否为枚举,以便枚举的消息框内容可以包含有关可接受枚举值的额外信息。
示例
包含的示例展示了该转换器在TextBox
上的多个不同属性上的使用。这些属性通过检查CheckBox
来更改。其中一个复选框控制可见性,需要选中它才能看到其他更改。绑定的属性包括double(FontSize
)、Brush(BorderBrush
)、Visibility、反转的布尔值(IsReadOnly
)和Thickness(BorderThickness
)。这应该可以很好地展示值转换器的灵活性。
XAML如下:
<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="250" Width="450">
<Window.Resources>
<local:IfTrueValueConverter x:Key="VisibilityConverter"
TrueValue="Visible" FalseValue="Hidden"/>
<local:IfTrueValueConverter x:Key="BorderColorConverter"
TrueValue="Red" FalseValue="Blue"/>
<local:IfTrueValueConverter x:Key="ReverseBoolConverter"
TrueValue="false" FalseValue="true"/>
<local:IfTrueValueConverter x:Key="BorderThicknessConverter"
TrueValue="3,4,5,6" FalseValue="1,2,3,4"/>
<local:IfTrueValueConverter x:Key="FontSizeConverter"
TrueValue="10" FalseValue="12.5"/>
</Window.Resources>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<CheckBox Content="checked for visible, unchecked for invisible"
IsChecked="{Binding IsVisible,FallbackValue=true}"/>
<CheckBox Content="checked for Red border color, unchecked for Blue border color"
IsChecked="{Binding BorderColor,FallbackValue=true}"/>
<CheckBox Content="checked to enable editing (IsReadOnly = false)"
IsChecked="{Binding CanEdit,FallbackValue=true}"/>
<CheckBox Content="checked for thick border thickness, unchecked for thinner"
IsChecked="{Binding ThinBorderThickness,FallbackValue=true}"/>
<CheckBox Content="checked for font size 10, unchecked for font size 12.5"
IsChecked="{Binding FontSize,FallbackValue=true}"/>
<TextBlock Text="TestControl:" Margin="0,5,0,0" Foreground="DarkBlue"/>
<TextBox Name="TestControl" Text="Test Message" Margin="5" Height="30"
Visibility="{Binding IsVisible,
Converter={StaticResource VisibilityConverter}}"
BorderBrush="{Binding BorderColor,
Converter={StaticResource BorderColorConverter}}"
BorderThickness="{Binding ThinBorderThickness,
Converter={StaticResource BorderThicknessConverter}}"
FontSize="{Binding FontSize,
Converter={StaticResource FontSizeConverter}}"
IsReadOnly="{Binding CanEdit,Converter={StaticResource
ReverseBoolConverter}}"/>
</StackPanel>
</Window>
如您所见,使用转换器就像使用我上面展示的更简单的转换器一样。
您需要修改XAML以显示此代码提供的调试帮助。所有需要做的就是将TrueValue
或FalseValue
在XAML中定义转换器时分配一个无效值。如果Visibility
的TrueValue
更改为“illegal”之类的东西,那么会出现以下MessageBox
:
这些信息应该有助于修复枚举的绑定问题。对于非枚举,MessageBox
稍微简单一些,没有有效枚举值的列表。
好处之一是对话框仅在转换器第一次运行时显示。
还有其他提供翻译问题反馈的方法,包括写入Output
窗口,但我更喜欢向开发人员显示消息。
可能的改进和选项
我曾有过一个想法,是将转换器扩展为三状态,第三个状态为null。个人而言,我没有在我的代码中看到对这个第三个值的需求,但我可以看到需要第三个值的场景。
另一个想法是添加属性,允许值与其他非true和false的值进行比较。这会非常简单,并且在绑定到XAML中定义的控件属性时可能非常有用。
这将进一步扩展为支持两个或三个以上的值。这可以通过在XAML中定义一个分隔列表来实现,该列表将作为属性输入到转换器中。然后,转换器将解析列表以获取转换。
正如我上面所说,我认为应该可以使用MarkupExtension
类来实现,这样就不必在使用前声明此转换器。
结论
这个通用的布尔值转换器足够灵活,可以处理View
所需的所有bool
转换。在某些情况下,它也可以用于其他绑定,其中绑定的值是bool
。