3
votes

I have bound a ComboBox to an enum like so

Enum Foo
    Bar
    Baz
End Enum

Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    FooComboBox.DataSource = [Enum].GetValues(GetType(Foo))
End Sub

I would like to change the selected value at a given time

Private Sub FooBar()
    FooComboBox.SelectedValue = Foo.Bar
End Sub

But this throws an exception:

An unhandled exception of type 'System.InvalidOperationException' occurred in System.Windows.Forms.dll

Additional information: Cannot set the SelectedValue in a ListControl with an empty ValueMember.

And inspecting the ComboBox I can indeed see that the ValueMember property is empty. But SelectedValue is equal to the value expected for the currently selected item when I read it, which would indicate that the ValueMember is set to the enum value as was expected.

A) If the ValueMember property is not set, where is the value in SelectedValue coming from?

B) If the binding is causing SelectedValue to contain the correct value, why the flip doesn't setting it work in just the same way?

C) What else do I need to do to make this bloody .Net work?

Note that there are indeed quite a few C# & WPF questions on this, none really explain this as far as I have seen and I haven't been able to work out what the correct vb syntax would be from the answers suggested them.

1
I can use SelectedIndex to achieve the desired effect, but that's because, by coincidence, the index of the items match the enum values. What happens when the enum values are different to the index?Toby
FooComboBox.SelectedItem = Foo.Bar When they are not simple {0,1,2} values, use a Type to set both the Value and DisplaymemberŇɏssa Pøngjǣrdenlarp
@Plutonix, how does one "use a Type to set the Value and DisplayMember"?Toby
There is Assigning a value to ComboBox Items using Enum.Names and ENum.Values; and also Set DisplayMember and ValueMember on ComboBox without DataSource which makes it more complex than it needs to be with the "without datasource" requirementŇɏssa Pøngjǣrdenlarp
Ah you mean use a collection of complex types and bind to that... Bit much for a simple short, constant, list of numbers, no? God I hate VB.Toby

1 Answers

3
votes

Particularly when the values are not sequential, you need to provide a way for the control to "map" the Name to the related value. Once you post Enum.GetValues or the Names to a CBO, they have become detached.

You can use something like a KeyValuesPair(of String, Int32) using the names as TKey and the values as TValue. The generic can make it seem more complex than it is. Since the key will always be String, and the value is usually an Int32 I tend to use a simple NameValuePair class for these:

Public Class NameValuePair
    Public Property Name As String
    Public Property Value As Int32

    Public Sub New(n As String, v As Int32)
        Name = n
        Value = v
    End Sub

    Public Overrides Function ToString() As String
        Return String.Format("{0}", Name)
    End Function
End Class

That will associate any name with any value. The main thing is that you control what displays for ToString(). In this case, both the name and value come from an Enum; a simple method to create a List or Array of them:

Private Enum Stooges
    Moe = 9
    Larry = 99
    Curly = 45
    Shemp = 65
    CurlyJoe = 8
End Enum

' method to convert any Enum to a collection of Named-Value pairs
Private Function EnumToPairsList(e As Type) As List(Of NameValuePair)
    Dim ret As New List(Of NameValuePair)

    Dim vals = [Enum].GetValues(e)
    Dim names = [Enum].GetNames(e).ToArray

    For n As Int32 = 0 To names.Count - 1
        ret.Add(New NameValuePair(names(n), CType(vals.GetValue(n), Int32)))
    Next

    Return ret
End Function

EnumsToPairsList could return an array, or use KeyValuePair as desired. It can be expanded to use a Description in place of the name when present. Using it:

cbox1.DataSource = EnumToPairsList(GetType(Stooges))
cbox1.DisplayMember = "Name"   ' use "Key" for a KVP
cbox1.ValueMember = "Value"

' set a value:
cbox1.SelectedValue = Convert.ToInt32(Stooges.Shemp)

Using It

Since you have "wrapped" the enum in the NVP class, that is what each SelectedItem will be (enclosed in an Object). When using a DataSource such as this, you would typically act in the SelectedValueChanged event and examine the SelectedValue. This is the main purpose of it: show the names to the user, but return the enum value to you in code.

The only "trick" is that it needs to be cast back to your enum:

Private Sub cbox1_SelectedValueChanged(...
    Dim eItem As Stooges = CType(cbox1.SelectedValue, Stooges)
    Console.WriteLine(eItem)
    Console.WriteLine(eItem.ToString)        

45
Curly

If you insist on using SelectedItem, you will have to cast from Object to NameValuePair, get the Value, then cast that to your Enum Type.


It is often useful to hold onto a copy of the data source so it resides somewhere other than only as a control datasource:

Private StoogesDS As List(Of NameValuePair)
...
StoogesDS = EnumsToPairsList(GetType(Stooges))
cbox1.DataSource = StoogesDS

This allows your code to still use the collection even when the form is not around. As noted above, each item is now a NameValuePair object.

cbox1.SelectedItem = StoogesDS.FirstOrDefault(Function(z) z.Name = Stooges.Shemp.ToString())