Enum getting cast to string when bound with custom converter

Viewed 192

Problem Overview

I have a custom IValueConverter called EnumDisplayConverter. It's supposed to take an Enum value and return the name so it can be displayed. Somehow, even though this converter is being used on a binding between properties of an Enum type, the converter is being passed a value of String.Empty. This of course causes an error as String is not an Enum, not to mention it's just really unexpected.

Code to Reproduce

The following code can be used to reproduce the error. The steps to reproduce and an explanation of what the code is meant to do come after.

<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

Steps to Reproduce

  1. Run MainWindow
  2. Select any item from the ListBox on the left
  3. Select a different item from the ListBox
  4. Observe unhanded exception

Code Explanation

In case it's not clear what the code is supposed to do, I'll explain a bit.

Foo represents a data object that is being edited by the user via MainWindow. Every Foo has the option of a Thing. Not having a Thing is also an option, which is why ChosenThing is a Thing? (i.e. Nullable(Of Thing)).

Null data items don't work in a ComboBox, since Null means "there is no selection". To get around this, I add a value of -1 to my list of selectable Thing values. In Foo.SelectableChosenThing, I check for -1 and convert it to Null for the actual value of Foo.ChosenThing. This lets me bind to the ComboBox correctly.

Problem Details

The error only seems to occur when OpenFoo is set to Nothing before being given a new value. If I take out the line OpenFoo = Nothing, everything works. However, in the real application I want to set OpenFoo to Nothing while the selection is being loaded- besides, it doesn't explain why this is happening in the first place.

Why is EnumDisplayConverter being passed a value of type String, when the properties involved are of the expected type Thing?

1 Answers

Turns out, after some investigation, that the problem comes from the ComboBox control. When the selected item of the ComboBox is null, it replaces the display value of null with String.Empty. Here's a snippet from the .NET Reference Source for ComboBox.UpdateSelectionBoxItem:

...

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

...

This happens before any DataTemplates are considered, so by the time my DataTemplate get's called, it's being given a value of String.Empty to display instead of null.

The solution is to either

  • Add a DataTrigger to the ContentControl's Style which checks for String.Empty and doesn't use the converter in such a case.
  • Modify EnumDisplayConverter to check for non-Enum values and return DependencyProperty.UnsetValue.
Related