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

WPF/Silverlight 实现 IValueConverter 的通用方法

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2012年6月18日

CPOL

6分钟阅读

viewsIcon

32710

downloadIcon

263

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>

正如您所见,首先我们需要在XAMLWindow.Resources元素中定义值转换器。这只是定义任何值转换器以供使用,只是多了两个额外的参数:TrueValueFalseValue。只需放入所需的枚举的字符串值,以便在ViewModel的值为 true 和 false 时使用。WPF的转换功能可以将这些字符串值转换为枚举,因此转换器可以正常工作。该转换器还可以用于设置颜色,因此可以将“Red”和“Black”的字符串值分配给TrueValueFalseValue,然后可以使用该转换器来设置前景色画笔。

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

类型转换

在我改进这个转换器的时候,我想到可以通过在第一次时将TrueValueFalseValue属性转换为正确的类型来提高性能,从而节省值转换器转换期间自动转换所需的时间。为此,我使用了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 为许多标准类提供了类型转换器。只需要使用staticTypeDescriptor.GetConverter方法,然后使用返回类的ConvertFrom方法。

TypeConverter converter = TypeDescriptor.GetConverter(targetType);
return converter.ConvertFrom(value);

我对TrueValueFalseValue属性都使用了这种转换,这两个属性都是 object 类型。这意味着只需要进行简单的强制转换即可使用这些属性。Convert方法中的targetType参数用于在Convert方法是第一个执行的方法时确定属性要转换为的类型(ConvertBack方法也会初始化值,但我认为这可能有点多余)。

增加灵活性的更多属性

为了满足转换器的即时需求,我添加了最初称为Compare(后来更改为CompareTrue)的属性。其思想是比较Convert方法中传递的值参数与该值,如果它们相同,则值转换器的Convert方法将返回TrueValue。现在,由于我可以检查值参数的类型,所以我也可以在此属性上使用TypeConverter

仅此CompareTrue属性在不考虑ConvertBack方法的情况下工作得很好。对于我使用此转换器的大多数情况,我从未真正使用过ConvertBack方法,但我应该考虑它。因此,我添加了一个CompareFalse属性。

不幸的是,我不得不考虑我可能正在处理想要比较 null 值的类型。这增加了相当多的复杂性,因为我希望当Convert方法中的值参数的类型为bool时,默认值为truefalse,但对于大多数类型来说,null是有效的。在初始化期间,除非类型是布尔值用于比较,否则CompareTrueCompareFalse属性将保持为null,在这种情况下,它们分别更改为booltruefalse。如果CompareTrueCompareFalse属性不为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));
    }

示例

示例显示了转换器用于更改某些控件的ForegroundBackground。下面的内容显示了每次选择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中定义转换器时,将TrueValueFalseValue之类的属性之一赋值为无效值。如果VisibilityTrueValue更改为“illegal”之类的值,则会出现以下MessageBox

https://codeproject.org.cn/KB/WPF/GenericValueConverter/image002.png

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

https://codeproject.org.cn/KB/WPF/GenericValueConverter/image003.png

一个好处是,对话框只在转换器首次运行时显示。

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

结论

IValueConverter可以处理简单的bool比较,但这对于大多数应用程序来说已经足够了。请注意,您可以使用此转换器处理的许多地方都可以通过WPF中的触发器来处理;Silverlight目前不支持触发器。但是,我发现使用此转换器然后转换为触发器可以使事情更容易,因为我可以在转换器中使用调试器,而在触发器中则不能。

我发现使用此转换器进行调试的一个问题是,它的用途远不止一个简单的转换器,有时调试会很困难。我使用ConverterParameter来帮助我调试。

© . All rights reserved.