首页 > 解决方案 > 与自定义转换器绑定时,枚举被强制转换为字符串

问题描述

问题概述

我有一个习惯IValueConverterEnumDisplayConverter. 它应该接受一个Enum值并返回名称以便显示。不知何故,即使这个转换器被用于Enum类型属性之间的绑定,转换器也被传递了一个String.Empty. 这当然会导致错误,因为Stringis not an Enum,更不用说它真的出乎意料。

重现代码

以下代码可用于重现错误。之后是重现的步骤和对代码用途的解释。

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:VBTest"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">

    <DockPanel>
        <ListBox Name="LB_Foos" DockPanel.Dock="Left" ItemsSource="{Binding FooOptions}" SelectionChanged="ListBox_SelectionChanged"/>
        
        <ComboBox ItemsSource="{x:Static local:MainWindow.SelectableThings}" SelectedItem="{Binding OpenFoo.SelectableChosenThing}" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <ContentControl>
                        <ContentControl.Style>
                            <Style TargetType="ContentControl">
                                <Setter Property="Content" Value="{Binding Converter={x:Static local:EnumDisplayConverter.Instance}}"/>

                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding}" Value="-1">
                                        <Setter Property="Content" Value="None"/>
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </ContentControl.Style>
                    </ContentControl>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
    </DockPanel>    
</Window>
Imports System.Collections.ObjectModel
Imports System.Globalization

Class MainWindow

    Shared Sub New()
        Dim Things = (From v As Thing In [Enum].GetValues(GetType(Thing))).ToList
        Things.Insert(0, -1)
        SelectableThings = New ReadOnlyCollection(Of Thing)(Things)
    End Sub

    Public Shared ReadOnly Property SelectableThings As IReadOnlyList(Of Thing)

    Public ReadOnly Property FooOptions As New ReadOnlyCollection(Of Integer)({1, 2, 3, 4})

    'This is a placeholder method meant to set OpenFoo to a new instance of Foo when a selection is made.
    'In the actual application, this is done with data binding and involves async database calls.
    Private Sub ListBox_SelectionChanged(sender As Object, e As SelectionChangedEventArgs)
        OpenFoo = Nothing

        Select Case LB_Foos.SelectedItem
            Case 1
                OpenFoo = New Foo With {.ChosenThing = Nothing}
            Case 2
                OpenFoo = New Foo With {.ChosenThing = Thing.A}
            Case 3
                OpenFoo = New Foo With {.ChosenThing = Thing.B}
            Case 4
                OpenFoo = New Foo With {.ChosenThing = Thing.C}
        End Select
    End Sub

    Public Property OpenFoo As Foo
        Get
            Return GetValue(OpenFooProperty)
        End Get
        Set(ByVal value As Foo)
            SetValue(OpenFooProperty, value)
        End Set
    End Property
    Public Shared ReadOnly OpenFooProperty As DependencyProperty =
                           DependencyProperty.Register("OpenFoo",
                           GetType(Foo), GetType(MainWindow))
End Class

Public Enum Thing
    A
    B
    C
End Enum

Public Class Foo

    Public Property ChosenThing As Thing?

    Public Property SelectableChosenThing As Thing
        Get
            Return If(_ChosenThing, -1)
        End Get
        Set(value As Thing)
            Dim v As Thing? = If(value = -1, New Thing?, value)
            ChosenThing = v
        End Set
    End Property

End Class

Public Class EnumDisplayConverter
    Implements IValueConverter

    Public Shared ReadOnly Property Instance As New EnumDisplayConverter

    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
        If value Is Nothing Then Return Nothing
        Return [Enum].GetName(value.GetType, value)
    End Function

    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
        Return Binding.DoNothing
    End Function
End Class

重现步骤

  1. MainWindow
  2. ListBox从左侧选择任何项目
  3. ListBox
  4. 观察未处理的异常

代码说明

如果不清楚代码应该做什么,我会解释一下。

Foo表示用户正在通过 编辑的数据对象MainWindow。每个Foo人都有一个选项Thing没有aThing也是一种选择,这就是为什么ChosenThing是 a Thing?(ie Nullable(Of Thing))。

Null数据项在 a 中不起作用ComboBox,因为Null意味着“没有选择”。为了解决这个问题,我在-1我的可选值列表中添加了一个Thing值。在Foo.SelectableChosenThing中,我检查-1并将其转换Null为 的实际值Foo.ChosenThing。这让我可以ComboBox正确绑定。

问题详情

该错误似乎仅在被赋予新值之前OpenFoo设置为时发生。Nothing如果我取出线路OpenFoo = Nothing,一切正常。但是,在实际应用程序中,我在加载选择时设置OpenFoo-Nothing此外,它并没有解释为什么会发生这种情况。

当所涉及的属性是预期类型时,为什么EnumDisplayConverter传递一个value类型?StringThing

标签: wpfvb.netdata-bindingenumsivalueconverter

解决方案


事实证明,经过一番调查,问题出在ComboBox控件上。当被选中的项目为 时ComboBoxnull将显示值替换nullString.Empty这是来自.NET 参考源ComboBox.UpdateSelectionBoxItem的片段:

...

// display a null item by an empty string
if (item == null)
{
    item = String.Empty;
    itemTemplate = ContentPresenter.StringContentTemplate;
}
 
SelectionBoxItem = item;
SelectionBoxItemTemplate = itemTemplate;

...

这发生在DataTemplate考虑任何 s 之前,所以当我DataTemplate的 get 被调用时,它被赋予了一个值String.Emptyto display 而不是null.

解决方案是

  • DataTriggerContentControl's中添加一个在这种情况下Style检查String.Empty并且不使用转换器的。
  • 修改EnumDisplayConverter以检查非Enum值并返回DependencyProperty.UnsetValue

推荐阅读