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

通用WPF/Silverlight值转换器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2011 年 11 月 24 日

CPOL

9分钟阅读

viewsIcon

37065

downloadIcon

328

一个通用的WPF/Silverlight值转换器。

引言

我不认为人们应该做的事情之一是将WPF/Silverlight枚举(例如Visibility)直接从ViewModel传递到View。所有与ViewModel的双向绑定都应该在ViewModel中定义为bool,即使View需要其他值来减少耦合。原因之一是,有时ViewModel中的属性的用法可能与最初的意图不符,而最初使用布尔值bool可以避免在ViewModel中进行更改,或者避免创建IValueConverter来将两个值转换为另外两个值。首选方式应始终是使用IValueConverter来转换值(bool或其他值)到WPF。创建一个通用的IValueConverter来将ViewModel中的bool值转换为View所需的枚举(或其他值)实际上非常容易。这可以是一个IValueConverter,然后可以在ViewXAML中进行自定义,或者可以在资源字典中定义。这样的转换器还可以提供附加功能来帮助程序员查找错误。

注意

我几乎已经放弃了这个方法,因为我认为IValueConverter有一个更优雅的解决方案。这个解决方案让我感到不满意的地方之一是我必须在使用它之前在XAML中声明它。我几乎现在总是从MarkupExtension派生我的转换器,这样我就不必在使用它之前声明转换器。这个转换器与MarkupExtension不兼容,因为需要设置TrueValueFalseValue。我可以克服这一点,也许我将来会研究这个可能性。最终这可能是一个更好的解决方案。这是关于转换器的文章链接:https://codeproject.org.cn/Articles/1017358/WPF-Converter-Helper-Class。使用帮助类进行转换器时,TrueValueFalseValue的传递方式是通过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元素中定义值转换器。这只是定义任何值转换器供使用,但有两个额外的参数:TrueValueFalseValue。所有需要做的就是为ViewModel的值为true和false时所需的枚举字符串值指定。WPF的转换能力足以将这些string值转换为枚举,因此转换器可以正常工作。转换器还可以用于设置颜色,因此可以将“Red”和“Black”的字符串值分配给TrueValueFalseValue,然后该转换器可以用于设置Foreground Brush

现在使用转换器就像使用任何转换器一样,正如在TextBoxXAML中可以看到的那样。

我在编码中广泛使用了这个值转换器,并且考虑写一篇关于这个想法的文章已经有一段时间了,但我知道我可以做得更好。首先,我可以将字符串转换为实际类型,这样就不需要进行字符串转换,除了第一次,这应该会以少量的初始化成本提高性能。其次,我可以做一些错误检查。错误检查的主要目的是帮助程序员找到错误;有很多次我花费了太长时间来查找绑定中的问题,而这些问题实际上非常简单,只是WPF/Silverlight在帮助开发人员处理绑定问题方面做得非常糟糕。这两个目标实际上是相辅相成的,因为将字符串转换为所需值和为开发人员提供反馈都需要将字符串转换为WPF期望的类型。

创建实现的关键方面是字符串与值之间以及值与字符串之间的相互转换。

我经常在WPF中使用枚举,所以很熟悉将字符串转换为枚举以及将枚举转换为字符串值。此外,我主要使用上述值转换器处理枚举,特别是Visibility,因此最初只考虑为枚举进行检查和转换。当我开始实现我改进的值转换器时,我完成了枚举部分,然后认为它也应该能够将字符串转换为其他对象类型,因为WPFSilverlight会进行这种转换。弄清楚如何做到这一点有点棘手。

我最初尝试使用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是否已分配(就像上面更简单的实现一样),如果尚未分配,则将其设置为布尔值。接下来,对TrueValueFalseValue变量中的每一个调用私有静态FixValue函数。FixValue方法首先检查是否需要转换:如果不需要转换,则返回相同的值(如果尝试转换两次,可能会引发异常)。否则,代码获取targetTypeTypeConverter,然后进行转换。如果发生异常(无法转换为正确类型的值),代码会捕获异常并返回原始值(这可能不起作用,但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以显示此代码提供的调试帮助。所有需要做的就是将TrueValueFalseValue在XAML中定义转换器时分配一个无效值。如果VisibilityTrueValue更改为“illegal”之类的东西,那么会出现以下MessageBox

这些信息应该有助于修复枚举的绑定问题。对于非枚举,MessageBox稍微简单一些,没有有效枚举值的列表。

好处之一是对话框仅在转换器第一次运行时显示。

还有其他提供翻译问题反馈的方法,包括写入Output窗口,但我更喜欢向开发人员显示消息。

可能的改进和选项

我曾有过一个想法,是将转换器扩展为三状态,第三个状态为null。个人而言,我没有在我的代码中看到对这个第三个值的需求,但我可以看到需要第三个值的场景。

另一个想法是添加属性,允许值与其他非true和false的值进行比较。这会非常简单,并且在绑定到XAML中定义的控件属性时可能非常有用。

这将进一步扩展为支持两个或三个以上的值。这可以通过在XAML中定义一个分隔列表来实现,该列表将作为属性输入到转换器中。然后,转换器将解析列表以获取转换。

正如我上面所说,我认为应该可以使用MarkupExtension类来实现,这样就不必在使用前声明此转换器。

结论

这个通用的布尔值转换器足够灵活,可以处理View所需的所有bool转换。在某些情况下,它也可以用于其他绑定,其中绑定的值是bool

© . All rights reserved.