Bernoulli Lizard
Bernoulli Lizard

Reputation: 579

Bind all Descriptions of an enum to a combobox

How can I bind the formatted string descriptions of an enumeration to a combobox? I want the combobox to show the descriptions, not the enum.

I have an enum like this:

Public Enum Sandwiches
<ComponentModel.Description("Ham Sandwich")> HamSandwich
<ComponentModel.Description("Reuben")> Reuben
<ComponentModel.Description("Po’ Boy")> PoBoy
<ComponentModel.Description("Grilled Cheese")> GrilledCheese
End Enum

I can use this function to convert the selected item back to an enum, after the description string has already been bound to a combobox:

        Public Function GetEnumFromDescriptionAttribute(Of T)(description As String) As T
        Dim type As Type = GetType(T)
        If Not type.IsEnum Then
            Throw New InvalidOperationException()
        End If
        For Each fi As Reflection.FieldInfo In type.GetFields()
            Dim descriptionAttribute As ComponentModel.DescriptionAttribute = TryCast(Attribute.GetCustomAttribute(fi, GetType(ComponentModel.DescriptionAttribute)), ComponentModel.DescriptionAttribute)
            If descriptionAttribute IsNot Nothing Then
                If descriptionAttribute.Description <> description Then
                    Continue For
                End If
                Return DirectCast(fi.GetValue(Nothing), T)
            End If
            If fi.Name <> description Then
                Continue For
            End If
            Return DirectCast(fi.GetValue(Nothing), T)
        Next
        Return Nothing
    End Function

But I cannot find a clean way to bind all of the enumerations to the combobox. I know that an often sugested solution is to convert the enums and enum descriptions to a dictionary, and then set those as the cbo DisplayMember and ValueMember. But I cannot figure out how to actually do that.,

I've ready through dozens of partial solutions on how to do this; The problem is that they are all written in C#, use implicit conversion with Option Strict turned OFF, and don't show the full implementation. So it's impossible for me to translate any of the solutions into .NET, since I have no idea what type variables have been defined as.

Upvotes: 1

Views: 646

Answers (2)

TnTinMn
TnTinMn

Reputation: 11801

It appears that your goal is override the string representation of a Enum value. The base representation displays Enum's name, where-as you want to display the Enum's Description attribute.

Assuming that this is a WinForm's combobox, the standard way would be to decorate the Enum with a custom TypeConverter and override the ConvertTo method. Using the EnumConverter Class and the basis for the custom converter minimizes the required code.

Imports System.ComponentModel
Imports System.Globalization
Imports System.Reflection

Public Class EnumDescriptionConverter(Of T As {Structure, IConvertible}) : Inherits EnumConverter

  Private enumDescriptions As New Dictionary(Of T, String)

  Public Sub New()
    MyBase.New(GetType(T))
    ' Since the ability to add an Enum constraint to the generic type does not exist in VB.
    ' this check is to ensure that it is an Enum.
    ' Note that normally, throwing an exception in a constructor is considered bad form, but
    ' there is no other option
    If GetType(T).IsEnum Then
      LoadEnumDescriptions()
    Else
      Throw New ArgumentException($"{GetType(T).Name} is not an Enum")
    End If
  End Sub

  Private Sub LoadEnumDescriptions()
    ' An Enum type comprises static (Shared) fields that represent the defined enum values and
    ' an instance field that holds thae actual value. so only retrieve the static fields
    ' to get the defined (named) enum members.
    For Each fi As FieldInfo In GetType(T).GetFields(BindingFlags.Public Or BindingFlags.Static)
      Dim description As String
      Dim descAttrib As DescriptionAttribute = fi.GetCustomAttribute(Of DescriptionAttribute)
      If descAttrib Is Nothing Then
        ' no description attribute so use the defined name
        description = fi.Name
      Else
        description = descAttrib.Description
      End If
      enumDescriptions.Add(CType(fi.GetValue(Nothing), T), description)
    Next
  End Sub

  Public Overrides Function ConvertTo(context As ITypeDescriptorContext, culture As CultureInfo, value As Object, destinationType As Type) As Object
    Dim ret As Object

    ' the purpose of this converter is to provide an enum's description attribute as the string representation
    ' instead of the field name as provided by the standard implemention.
    If destinationType Is GetType(String) AndAlso value IsNot Nothing Then
      Dim enumValue As T = DirectCast(value, T)
      If enumDescriptions.ContainsKey(enumValue) Then
        ret = enumDescriptions(enumValue)
      Else
        ret = enumValue.ToString(Nothing)
      End If
    Else
      ret = MyBase.ConvertTo(context, culture, value, destinationType)
    End If
    Return ret
  End Function
End Class

Modify your Enum definition to use this custom converter.

<TypeConverter(GetType(EnumDescriptionConverter(Of Sandwiches)))>
Public Enum Sandwiches
  <ComponentModel.Description("Ham Sandwich")> HamSandwich
  <ComponentModel.Description("Reuben")> Reuben
  <ComponentModel.Description("Po’ Boy")> PoBoy
  <ComponentModel.Description("Grilled Cheese")> GrilledCheese
End Enum

Now all you need to do is load the ComboBox.Items with Enum values.

ComboBox1.Items.AddRange(System.Enum.GetValues(GetType(Sandwiches)).Cast(Of Object).ToArray())

enter image description here

Use the ComboBox.SelectedItem to retrieve the selected value.

Upvotes: 1

jmcilhinney
jmcilhinney

Reputation: 54417

Here's a class that I wrote some time ago to represent an Enum value and its description:

Imports System.ComponentModel
Imports System.Reflection
 
''' <summary>
''' Contains an enumerated constant value and a friendly description of that value, if one exists.
''' </summary>
''' <typeparam name="T">
''' The enumerated type of the value.
''' </typeparam>
Public Class EnumDescriptor(Of T)
 
    ''' <summary>
    ''' The friendly description of the value.
    ''' </summary>
    Private _description As String
    ''' <summary>
    ''' The enumerated constant value.
    ''' </summary>
    Private _value As T
 
    ''' <summary>
    ''' Gets the friendly description of the value.
    ''' </summary>
    ''' <value>
    ''' A <b>String</b> containing the value's Description attribute value if one exists; otherwise, the value name.
    ''' </value>
    Public ReadOnly Property Description() As String
        Get
            Return Me._description
        End Get
    End Property
 
    ''' <summary>
    ''' Gets the enumerated constant value.
    ''' </summary>
    ''' <value>
    ''' An enumerated constant of the <b>EnumDescriptor's</b> generic parameter type.
    ''' </value>
    Public ReadOnly Property Value() As T
        Get
            Return Me._value
        End Get
    End Property
 
    ''' <summary>
    ''' Creates a new instance of the <b>EnumDescriptor</b> class.
    ''' </summary>
    ''' <param name="value">
    ''' The value to provide a description for.
    ''' </param>
    Public Sub New(ByVal value As T)
        Me._value = value
 
        'Get the Description attribute.
        Dim field As FieldInfo = value.GetType().GetField(value.ToString())
        Dim attributes As DescriptionAttribute() = DirectCast(field.GetCustomAttributes(GetType(DescriptionAttribute), _
                                                                                        False),  _
                                                              DescriptionAttribute())
 
        'Use the Description attribte if one exists, otherwise use the value itself as the description.
        Me._description = If(attributes.Length = 0, _
                             value.ToString(), _
                             attributes(0).Description)
    End Sub
 
    ''' <summary>
    ''' Overridden.  Creates a string representation of the object.
    ''' </summary>
    ''' <returns>
    ''' The friendly description of the value.
    ''' </returns>
    Public Overrides Function ToString() As String
        Return Me.Description
    End Function
 
End Class

Here's a simplified version I just wrote:

Imports System.ComponentModel
Imports System.Reflection

Public Class EnumDescriptor(Of T)

    Public ReadOnly Property Description As String

    Public ReadOnly Property Value As T

    Public Sub New(value As T)
        Me.Value = value

        'Get the Description attribute.
        Dim field = value.GetType().GetField(value.ToString())
        Dim attributes = DirectCast(field.GetCustomAttributes(GetType(DescriptionAttribute), False), DescriptionAttribute())

        'Use the Description attribute if one exists, otherwise use the value itself as the description.
        Description = If(attributes.Length = 0, value.ToString(), attributes(0).Description)
    End Sub

    Public Overrides Function ToString() As String
        Return Description
    End Function

End Class

Here's another class that can be used to store an instance of the previous class for each value in an Enum:

''' <summary>
''' A collection of EnumDescriptors for an enumerated type.
''' </summary>
''' <typeparam name="T">
''' The type of the enumeration for which the EnumDescriptors are created.
''' </typeparam>
Public Class EnumDescriptorCollection(Of T)
    Inherits ObjectModel.Collection(Of EnumDescriptor(Of T))
 
    ''' <summary>
    ''' Creates a new instance of the <b>EnumDescriptorCollection</b> class.
    ''' </summary>
    Public Sub New()
        'Populate the collection with an EnumDescriptor for each enumerated value.
        For Each value As T In [Enum].GetValues(GetType(T))
            Items.Add(New EnumDescriptor(Of T)(value))
        Next
    End Sub
 
End Class

Here's some example usage that binds a instance of the collection class to a ComboBox:

Public Class Form1
 
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        With ComboBox1
            .DisplayMember = "Description"
            .ValueMember = "Value"
            .DataSource = New EnumDescriptorCollection(Of Sandwiches)
        End With
    End Sub
 
    Private Sub ComboBox1_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ComboBox1.SelectedIndexChanged
        MessageBox.Show(ComboBox1.SelectedValue.ToString(), ComboBox1.Text)
    End Sub
 
End Class

Upvotes: 2

Related Questions