Toby Toby - 3 months ago 22
Vb.net Question

How to set SelectedValue for enum bound ComboBox

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.

Answer

Particularly when the values are not sequential, need to provide a way for the control to be able 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 makes it look a bit 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

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)

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. Bound to the list, the items in the CBO are now NameValuePair (or KeyValuePair(of Tkey, TValue)) so referencing and Item will be that Type (class).

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

The same is true in CBO events, SelectedItem will be a Type (as Object), not just a string. SelectedValue however will be 9, 45 etc.