WPF 绑定仅在使用转换器时有效

WPF Binding only working when using a Converter

我正在为可重用且相对简单的 UserControl 而苦苦挣扎。

我的数据存储在实现简单 IHasValue 接口的 classes 中:

public interface IHasValue<T>
{
    T Value { get; }
    ValueType Type { get; }
}

我需要一个可重复使用的控件来编辑应用程序周围多个位置的数据。这是我目前拥有的控件,当 SelectedValue 为 null 时,它只显示一个带加号的按钮,单击它设置 SelectedValue(稍后我将添加用户输入)并显示值而不是按钮:

<UserControl x:Class="DotsCompanion.Controls.HasValueFloatSelectorControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:DotsCompanion.Controls"
             xmlns:conv="clr-namespace:DotsCompanion.Converters"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">


    <UserControl.Resources>
        <conv:NullToTrueFalseConverter x:Key="NullToTrueFalseConverter" />
        <conv:MyDebugConverter x:Key="MyDebugConverter" />

        <DataTemplate x:Key="EmptyValueTemplate" DataType="{x:Type ContentControl}">
            <Button Click="NewValueClick">
                <TextBlock Text="+"></TextBlock>
            </Button>
        </DataTemplate>
        <DataTemplate x:Key="HasValueTemplate" DataType="{x:Type ContentControl}">
            <StackPanel DataContext="{Binding Path=DataContext, RelativeSource={RelativeSource AncestorType={x:Type ContentControl}}}">
                <!-- THIS IS THE PROBLEMATIC BINDING -->
                <TextBlock Text="{Binding Path=SelectedValue, Converter={StaticResource MyDebugConverter}}"></TextBlock>
            </StackPanel>
        </DataTemplate>
    </UserControl.Resources>


    <Grid Name="LayoutRoot">
        <ContentControl>
            <ContentControl.Style>
                <Style TargetType="ContentControl">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding SelectedValue, Converter={StaticResource NullToTrueFalseConverter}}" Value="true">
                            <Setter Property="ContentTemplate" Value="{DynamicResource HasValueTemplate}"/>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding SelectedValue, Converter={StaticResource NullToTrueFalseConverter}}" Value="false">
                            <Setter Property="ContentTemplate" Value="{DynamicResource EmptyValueTemplate}"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ContentControl.Style>
        </ContentControl>
    </Grid>
</UserControl>

这是控件的极其简单的代码隐藏:

public partial class HasValueFloatSelectorControl : UserControl
{
    public IHasValue<float> SelectedValue
    {
        get { return (IHasValue<float>)GetValue(SelectedValueProperty); }
        set { SetValue(SelectedValueProperty, value); }
    }

    public static readonly DependencyProperty SelectedValueProperty =
        DependencyProperty.Register("SelectedValue", typeof(IHasValue<float>), typeof(HasValueFloatSelectorControl), new PropertyMetadata(default(IHasValue<float>)));


    public HasValueFloatSelectorControl()
    {
        InitializeComponent();
        LayoutRoot.DataContext = this;
    }

    private void NewValueClick(object sender, EventArgs e)
    {
        SelectedValue = new FloatPrimitive(2.0f);
    }
}

FloatPrimitive 是 class 实际存储数据(浮点数)的地方,它实现 IHasValue 并覆盖 ToString() 以便浮点数显示为字符串。

问题是,将 Text 绑定到 SelectedValue 似乎只在使用 Converter 时有效。在上面的代码中,MyDebugConverter 只是 returns 原样的值:

// This should be literally useless AFAIK
public class MyDebugConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value;
    }
}

如果我删除转换器,绑定将不会显示任何内容。根据输出 window 跟踪,DataItem 为空,但使用实时 属性 资源管理器我可以看到 DataContext 已正确设置并且 SelectedValue 实际上是一个 FloatPrimitive。

我尝试寻找解决方案,但到目前为止我只发现了相反的情况(绑定无法正常工作 转换器)。我是 WPF 的新手,所以我不确定从哪里开始调试,因为这似乎是一种相当罕见的情况,而且在线资源目前没有帮助。

我尝试了什么:

会不会是我用的是接口导致的?我为项目中的另一个用户控件设置了一个非常相似的设置,它工作得很好,但它使用抽象 class + 继承而不是接口 + 实现。

显然,问题是您的 属性 SelectedValueinterface 类型。

如果将 SelectedItem 的类型更改为 FloatPrimitive,一切都会按预期工作(不是解决方案,只是对我的观点的测试)。我还通过测试和研究确认问题出在 interface 方面,而不是它是泛型的事实。


问题的解释

WPF 在后台使用 Type Converters 在绑定更新期间处理类型之间的转换。

  • 对于 IntegerString 等类型,有一堆默认的 TypeConverters
  • 您可以通过将 TypeConverterAttribute 应用于要转换的 class 来为您自己的 class 定义 TypeConverter
  • 当 WPF 转换为 String 时,如果所有其他方法都失败,它将调用 Object.ToString()

问题所在:如 this answer 中所述,当 WPF 处理数据绑定时,它不检查正在传输的值的实际类型,它只查看源和目标的声明类型特性。因此,在 FloatPrimitive 上声明什么转换运算符或是否覆盖 ToString() 并不重要; WPF 只能看到 IHasValue<T>,因为那是 SelectedItem 属性 的类型。

IHasValue<T> 显然没有默认值 TypeConverter 而且您还没有声明自己的。由于 TextBlock.TextString,WPF 通常会尝试调用 Object.ToString(),但 IHasValue<T> 不是 Object.实现 IHasValue<T> 的对象肯定是 Object 的后代,但 WPF 不知道这一点,它只知道它是一个 IHasValue<T>。因此转换失败,结果 null 被 return 编辑。

您实际上可以看到此转换失败。如果将 PresentationTraceSources.TraceLevel=High 添加到 {Binding SelectedItem ...}(并保留 Converter=...),输出将显示:

System.Windows.Data Warning: 101 : BindingExpression (hash=16490914): GetValue at level 0 from HasValueFloatSelectorControl (hash=72766) using DependencyProperty(SelectedValue): FloatPrimitive (hash=14200498)
System.Windows.Data Warning: 80 : BindingExpression (hash=16490914): TransferValue - got raw value FloatPrimitive (hash=14200498)
System.Windows.Data Warning: 84 : BindingExpression (hash=16490914): TransferValue - implicit converter produced <null>
System.Windows.Data Warning: 89 : BindingExpression (hash=16490914): TransferValue - using final value <null>

绑定正确获取 FloatPrimitive,尝试转换它,但以 null 结束,这解释了为什么 TextBlock 为空。

明显的解决方案是为调用 ToString()IHasValue<T> 声明您自己的 TypeConverter。不幸的是,这不起作用(我自己尝试过)。似乎 WPF 只是不支持 interfaces 的 TypeConverters。 This question 显示有类似问题的人(他们试图将 转换为 一个 interface 类型)。

为什么 MyDebugConverter 修复问题

当您使用 Binding.Converter 时,WPF 将在尝试任何其他操作之前调用提供的 IValueConverter。但是 IValueConverter 不需要 return 目标的确切类型 属性,因此 WPF 需要检查输出值的类型。如果 IValueConverter 输出 不是 目标 属性 的确切类型,WPF 将尝试使用 TypeConverters 来完成工作。

问题是,这需要 WPF 实际检查输出值的类型。因此,即使 MyDebugConverter 什么都不做,WPF 假设输入是 IHasValue<T>,但 实际上查看了 输出并发现它现在是 FloatPrimitive .所以现在它试图将其转换为后者而不是前者。这允许它使用适用的默认值 TypeConverter 来调用 Object.ToString().


解决方案

如上所述,声明您自己的 TypeConverter 不适用于 interface 类型,这给您留下了几个选择:

  • SelectedItem 的类型更改为某个基数 class,而不是 interface。 (根据您的要求,这可能不是一个选项)
  • Text(或类似名称的)属性 添加到您的 IHasValue<T> 实现(并且可能添加到 interface 本身),这将 return ToString()。绑定到 属性 而不是对象。
    例如,如果 FloatPrimitive 有一个 public string Text => ToString();,那么您可以绑定到 SelectedItem.Text
  • 不要绑定 String 属性。如果您将 TextBlock 替换为 ContentControl 并改为绑定 ContentControl.Content,则一切都会按预期显示(内部 ContentPresenter 知道调用 ToString())。这也为您提供了添加 DataTemplate 的选项,以防您想要显示的不仅仅是文本。示例:<ContentControl Content="{Binding SelectedValue}"/>
  • 绑定到 String 类型或其他类型的属性时,使用带有 IValueConverterBinding.Converter