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 的新手,所以我不确定从哪里开始调试,因为这似乎是一种相当罕见的情况,而且在线资源目前没有帮助。
我尝试了什么:
- 通过在 LayoutRoot 网格中添加一个简单的 TextBlock,绑定到 DataTemplate 外部 的 SelectedValue,但我得到了完全相同的结果(需要转换器);
- 通过从控件外部绑定到 SelectedValue 以及在控件内部调用 NewValueClick() 方法来设置 SelectedValue;
- 在 MyDebugConverter 中添加一个 Debugger.Break() 调用,这允许我检查该值是否不为 null 并且设置正确,但是再次使用转换器是允许它正确显示在第一名;
- 大量摆弄实时 属性 浏览器
会不会是我用的是接口导致的?我为项目中的另一个用户控件设置了一个非常相似的设置,它工作得很好,但它使用抽象 class + 继承而不是接口 + 实现。
显然,问题是您的 属性 SelectedValue
是 interface
类型。
如果将 SelectedItem
的类型更改为 FloatPrimitive
,一切都会按预期工作(不是解决方案,只是对我的观点的测试)。我还通过测试和研究确认问题出在 interface
方面,而不是它是泛型的事实。
问题的解释
WPF 在后台使用 Type Converters 在绑定更新期间处理类型之间的转换。
- 对于
Integer
、String
等类型,有一堆默认的 TypeConverter
s
- 您可以通过将
TypeConverterAttribute
应用于要转换的 class 来为您自己的 class 定义 TypeConverter
。
- 当 WPF 转换为
String
时,如果所有其他方法都失败,它将调用 Object.ToString()
。
问题所在:如 this answer 中所述,当 WPF 处理数据绑定时,它不检查正在传输的值的实际类型,它只查看源和目标的声明类型特性。因此,在 FloatPrimitive
上声明什么转换运算符或是否覆盖 ToString()
并不重要; WPF 只能看到 IHasValue<T>
,因为那是 SelectedItem
属性 的类型。
IHasValue<T>
显然没有默认值 TypeConverter
而且您还没有声明自己的。由于 TextBlock.Text
是 String
,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 只是不支持 interface
s 的 TypeConverter
s。 This question 显示有类似问题的人(他们试图将 转换为 一个 interface
类型)。
为什么 MyDebugConverter
修复问题
当您使用 Binding.Converter
时,WPF 将在尝试任何其他操作之前调用提供的 IValueConverter
。但是 IValueConverter
不需要 return 目标的确切类型 属性,因此 WPF 需要检查输出值的类型。如果 IValueConverter
输出 不是 目标 属性 的确切类型,WPF 将尝试使用 TypeConverter
s 来完成工作。
问题是,这需要 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
类型或其他类型的属性时,使用带有 IValueConverter
的 Binding.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 的新手,所以我不确定从哪里开始调试,因为这似乎是一种相当罕见的情况,而且在线资源目前没有帮助。
我尝试了什么:
- 通过在 LayoutRoot 网格中添加一个简单的 TextBlock,绑定到 DataTemplate 外部 的 SelectedValue,但我得到了完全相同的结果(需要转换器);
- 通过从控件外部绑定到 SelectedValue 以及在控件内部调用 NewValueClick() 方法来设置 SelectedValue;
- 在 MyDebugConverter 中添加一个 Debugger.Break() 调用,这允许我检查该值是否不为 null 并且设置正确,但是再次使用转换器是允许它正确显示在第一名;
- 大量摆弄实时 属性 浏览器
会不会是我用的是接口导致的?我为项目中的另一个用户控件设置了一个非常相似的设置,它工作得很好,但它使用抽象 class + 继承而不是接口 + 实现。
显然,问题是您的 属性 SelectedValue
是 interface
类型。
如果将 SelectedItem
的类型更改为 FloatPrimitive
,一切都会按预期工作(不是解决方案,只是对我的观点的测试)。我还通过测试和研究确认问题出在 interface
方面,而不是它是泛型的事实。
问题的解释
WPF 在后台使用 Type Converters 在绑定更新期间处理类型之间的转换。
- 对于
Integer
、String
等类型,有一堆默认的TypeConverter
s - 您可以通过将
TypeConverterAttribute
应用于要转换的 class 来为您自己的 class 定义TypeConverter
。 - 当 WPF 转换为
String
时,如果所有其他方法都失败,它将调用Object.ToString()
。
问题所在:如 this answer 中所述,当 WPF 处理数据绑定时,它不检查正在传输的值的实际类型,它只查看源和目标的声明类型特性。因此,在 FloatPrimitive
上声明什么转换运算符或是否覆盖 ToString()
并不重要; WPF 只能看到 IHasValue<T>
,因为那是 SelectedItem
属性 的类型。
IHasValue<T>
显然没有默认值 TypeConverter
而且您还没有声明自己的。由于 TextBlock.Text
是 String
,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 只是不支持 interface
s 的 TypeConverter
s。 This question 显示有类似问题的人(他们试图将 转换为 一个 interface
类型)。
为什么 MyDebugConverter
修复问题
当您使用 Binding.Converter
时,WPF 将在尝试任何其他操作之前调用提供的 IValueConverter
。但是 IValueConverter
不需要 return 目标的确切类型 属性,因此 WPF 需要检查输出值的类型。如果 IValueConverter
输出 不是 目标 属性 的确切类型,WPF 将尝试使用 TypeConverter
s 来完成工作。
问题是,这需要 WPF 实际检查输出值的类型。因此,即使 MyDebugConverter
什么都不做,WPF 假设输入是 IHasValue<T>
,但 实际上查看了 输出并发现它现在是 FloatPrimitive
.所以现在它试图将其转换为后者而不是前者。这允许它使用适用的默认值 TypeConverter
来调用 Object.ToString()
.
解决方案
如上所述,声明您自己的 TypeConverter
不适用于 interface
类型,这给您留下了几个选择:
- 将
SelectedItem
的类型更改为某个基数class
,而不是interface
。 (根据您的要求,这可能不是一个选项) - 将
Text
(或类似名称的)属性 添加到您的IHasValue<T>
实现(并且可能添加到interface
本身),这将 returnToString()
。绑定到 属性 而不是对象。
例如,如果FloatPrimitive
有一个public string Text => ToString();
,那么您可以绑定到SelectedItem.Text
。 - 不要绑定
String
属性。如果您将TextBlock
替换为ContentControl
并改为绑定ContentControl.Content
,则一切都会按预期显示(内部ContentPresenter
知道调用ToString()
)。这也为您提供了添加DataTemplate
的选项,以防您想要显示的不仅仅是文本。示例:<ContentControl Content="{Binding SelectedValue}"/>
- 绑定到
String
类型或其他类型的属性时,使用带有IValueConverter
的Binding.Converter
。