繁体   English   中英

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

[英]WPF Binding only working when using a Converter

我正在努力使用可重用且相对简单的 UserControl。

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

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

我需要一个可重用的控件来编辑应用程序周围多个位置的数据。 这是我目前拥有的控件,当 SelectedValue 为空时,它只显示一个带加号的按钮,单击它会设置 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 是实际存储数据(浮点数)的类,它实现 IHasValue 并覆盖 ToString() 以便将浮点数显示为字符串。

问题是,将 Text 绑定到 SelectedValue 似乎仅在使用转换器时才有效。 在上面的代码中,MyDebugConverter 只是按原样返回值:

// 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;
    }
}

如果我移除转换器,绑定只会显示任何内容。 根据输出窗口跟踪,DataItem 为空,但使用实时属性浏览器我可以看到正确设置了 DataContext,而 SelectedValue 确实是一个 FloatPrimitive。

我尝试寻找解决方案,但到目前为止,我只找到了相反的情况(绑定不能转换器一起使用)。 我对 WPF 还很陌生,所以我不确定从哪里开始调试,因为这似乎是一种相当罕见的情况,目前在线资源也无济于事。

我试过的:

  • 通过在 LayoutRoot Grid 内添加一个简单的 TextBlock 绑定到 DataTemplate之外的 SelectedValue,但我得到了非常相同的结果(需要转换器);
  • 通过从控件外部绑定到它并通过在控件内部调用 NewValueClick() 方法来设置 SelectedValue;
  • 在 MyDebugConverter 中添加一个 Debugger.Break() 调用,它允许我检查该值是否为空并正确设置,但是再次使用转换器是允许它首先正确显示的原因;
  • 大量摆弄实时财产浏览器

这可能是因为我正在使用接口吗? 我对项目中的另一个用户控件有一个非常相似的设置,它工作得很好,但它使用抽象类 + 继承而不是接口 + 实现。

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

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


问题说明

WPF 在底层使用类型转换器来处理绑定更新期间类型之间的转换。

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

问题是:正如在这个答案中所解释的,当 WPF 处理数据绑定时,它不会检查正在传输的值的实际类型,它只查看源和目标属性的声明类型。 出于这个原因,您在FloatPrimitive声明什么转换运算符或是否覆盖ToString() FloatPrimitive WPF 只能看到IHasValue<T>因为这是SelectedItem属性的类型。

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

您实际上可以看到此转换失败。 如果您将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 只是不支持interfaceTypeConverter 这个问题显示了一个有类似问题的人(他们试图转换interface类型)。

为什么MyDebugConverter修复了问题

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

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


解决方案

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM