lassic VB ListBoxes held a simple array of strings used for the list, and a parallel ItemData array that held a list of Long numeric values. Using the two in tandem, you could populate a ListBox with a list of strings to display and simultaneously populate the ItemData property with some type of ID or primary key field value. When a user selected an item (or items) you could retrieve the ItemData value and use it to obtain the associated object or use the value as a lookup value for a database query.
However, in VB.NET, when you drag a ListBox onto a form and then try to write the same loop to populate the ListBox, adding a text value and an ItemData numeric value for each item, you’ll get a compile-time error. ListBoxes in .NET don’t have an ItemData property. Hmm. It does seem that the ubiquitous VB ListBox lost some backward compatibility. But in doing so, it also gained functionality. Rather than holding simple String values, the ListBox’s Items collection now holds Objects?which can be anything. In addition, you can set a DisplayMember property for the ListBox (by default, it calls the ToString() method) of the item. Before displaying the item, the ListBox invokes the item member named by the DisplayMember property.
In other words, rather than storing a single set of strings and associated ID values and then having to retrieve the appropriate data when a user clicked on the item, you can now store the entire set of objects right in the Items property.
Still, despite considerable effort by VB.NET experts to convince people of the advantages of VB.NET’s Items collection, many people aren’t happy with the new ListBox implementation. One reason is that the consumers of a class aren’t always the creators of the class?and they may not be satisfied with the class creator’s selections.
At the Mercy of the Class Creator
Suppose you’re told to use a Person class (created by a coworker) that has four properties: ID (Long), LastName, FirstName, and Status (see Listing 1). The Person object has an overloaded constructor so you can assign all the values when you create the object.
You want to fill a ListBox with Person objects. So you create a Form and drag a ListBox onto it. You want to fill the ListBox when the user clicks a button, so you add a Fill List button to do that (see Figure 1).
VB.NET makes it very easy to display items in a ListBox, because you can set the ListBox’s DataSource property to any collection that implements the IList interface (although you can still write a loop to add items to the ListBox if you wish). The ArrayList class implements the IList interface, so, you create an ArrayList member variable for the form, and fill it with Person objects during the Form_Load event.
' define an ArrayList at class level Private people As New ArrayList() Private Sub Form1_Load(ByVal sender As _ System.Object, ByVal e As System.EventArgs) _ Handles MyBase.Load Dim p As Person ListBox1.DisplayMember = "ToString" ListBox1.ValueMember = "ID" p = New Person(1, "Twain", "Mark", "MT") people.Add(p) p = New Person(2, "Austen", "Jane", "JA") people.Add(p) p = New Person(2, "Fowles", "John", "JF") people.Add(p) End Sub
Now when a user clicks the Fill List button, by setting the ListBox’s DataSource property to the people ArrayList, the ListBox displays the names automatically.
Private Sub Button1_Click(ByVal sender As _ System.Object, ByVal e As System.EventArgs) _ Handles Button1.Click ListBox1.Sorted = True ListBox1.DataSource = people End Sub
Unfortunately, you find that the class creator didn’t override the ToString implementation or provide any additional LastFirst method to provide the strings for the ListBox. So the result is that the ListBox calls the default Person.ToString implementation, which returns the class name, ListBoxExample.Person. The result looks like Figure 2.
OK, no problem. What about using the DisplayMember property? Add the following line to the end of the Button1_Click method.
ListBox1.DisplayMember = "LastName"
Run the project again. This time, the result is a little closer (see Figure 3).
Now you’re stuck. The only good solution is to get the class creator to add a LastFirst property; otherwise, you’ll have to go to a good deal of trouble to get the list to display both names. Pretend the class creator actually helps you out, and adds a LastFirst property to the Person class.
Public ReadOnly Property LastFirst() As String Get Return p.LastName & " " & p.FirstName End Get End Property
Now you can change the ListBox.DisplayMember property, and the form works as expected (see Figure 4).
ListBox1.DisplayMember = LastFirst
Just as you get the form working, your manager walks in, and says, “Oh, by the way, the client wants to be able to change the list from Last/First to First/Last?both sorted, of course.” Now what? You could get the class creator to change the class again, but surely there’s a better solution.
You could inherit the class, and add a FirstLast method, but then you’d have two classes to maintain. You could create a new wrapper class that exposes the people ArrayList collection, and that also implements FirstLast and LastFirst properties. But what if the clients change their minds again? You’d have to keep adding methods to the class, or bite the bullet and beg the class creator for yet more changes. Also, do you really want to create a wrapper for every class you want to display in a ListBox?
This is when you begin to miss the classic VB ListBox’s ItemData property. If you could assign the Person.ID as the ItemData value, you could concatenate the names yourself, add them to the ListBox, and then look up the Person based on the ID when a user selects an item from the ListBox. But ItemData is gone.
You could inherit the ListBox class and add an ItemData property. But if you just add an ItemData property, the property is isolated from the ListBox Items collection; if the collection changes, you will have to intercept the change messages and alter the ItemData values appropriately. Unfortunately, that’s a lot more work than you would normally want to do. All these are onerous choices. Things would be a lot easier if you could just control the Person class.
At this point, change personas and put on a control creator’s hat. Here’s a different approach to displaying custom strings based on some object.
Unless there’s a good reason not to do so, when you create a class, you typically want the class *consumer* to have as much control over the instantiated objects as possible. One way to increase the class consumers’ power is to give them control over the method that the ListBox (or other code) calls to get a string representation of your object. In other words, rather than pre-define multiple display methods within your class, you could provide a public Delegate type and then add a private member variable and a public property to your class that accept that delegate type.
' Public Delegate type definition Public Delegate Function displayPersonDelegate _ (ByVal p as Person) as String ' Private member variable Private mDisplayMethod as displayPersonDelegate ' Public Property Public Property DisplayMethod() as displayPersonDelegate Get Return mDisplayMethod End Get Set (ByVal Value as displayPersonDelegate) mDisplayMethod=Value End Set End Property
In the code above, the displayPersonDelegate accepts a Person object and returns a string. The class consumer will create a displayPersonDelegate object and assign it to the public DisplayMethod property.
Next, override the ToString method so that it returns the delegate result value. For example:
Public Overloads Overrides Function ToString() _ as String ' Check to see if a delegate exists If Me.DisplayMethod is Nothing ' use the default MyBase.ToString, or ' any other default ToString implementation ' you wish Return MyBase.ToString Else ' Try to invoke the assigned delegate Try Return Me.DisplayMethod(Me) Catch ' again, use the default MyBase.ToString, or ' any other default ToString implementation ' you wish Return MyBase.ToString() End Try End Property
The advantage of this scheme is that the object consumer gets the best of both worlds?a default ToString implementation assignable by the class creator and the ability to call a custom ToString method by assigning the delegate. And the class creator doesn’t need to worry about all the possible ways that a user may wish to display an object. Finally, it gives the object consumer the ability to set different custom ToString methods for every instance of the Person class.
The simplest way to use the Person class is to assign a collection of them to some collection object, setting the DisplayMethod for each Person to a function matching displayPersonDelegate signature. For example, to create an ArrayList containing the Person objects, you would first write the display functions:
Public Function DisplayPersonFirstLast _ (byVal p as Person) as String Return p.FirstName & " " & p.LastName End Function Public Function DisplayPersonLastFirst _ (byVal p as Person) as String Return p.LastName & ", " & p.FirstName End Function
Next, when you create the collection, you assign the DisplayMethod for each Person object.
' define an ArrayList at class level Private people As New ArrayList() ' create Person objects and add them ' to the people ArrayList Dim p as person p = New Person(1, "Twain", "Mark", "MT") ' create a displayPersonDelegate for the ' DisplayPersonLastFirst method p.DisplayMethod = New Person.displayPersonDelegate _ (AddressOf DisplayPersonLastFirst) people.Add(p) ' repeat as necessary p = New Person(2, "Austen", "Jane", "JA") p.DisplayMethod = New Person.displayPersonDelegate _ (AddressOf DisplayPersonLastFirst) people.Add(p) p = New Person(2, "Fowles", "John", "JF") p.DisplayMethod = New Person.displayPersonDelegate _ (AddressOf DisplayPersonLastFirst) people.Add(p)
Using the DisplayMethod delegate property, Person object consumers can create custom methods to display the object’s data in any format they prefer. But because the scheme defaults to the .NET standard ToString method, you haven’t changed the base functionality of ToString in any other way. In fact, the only reason to override the ToString method at all is because that’s what the ListBox calls by default. But you could just as easily write a Display method and have the class consumers call the Display method explicitly (in this case, by setting the ListBox DisplayMember property to “Display”), and leave ToString out of the equation altogether.
Who Needs ItemData?
This solution accomplishes one other thing that, until now, was impossible without writing customized code: You can set a different DisplayMethod for each instance of a class. The Custom button illustrates this capability by setting the Status property of the “Jane Austen” Person object to a custom string.
Private Sub Button4_Click(ByVal sender As _ System.Object, ByVal e As System.EventArgs) _ Handles Button4.Click Dim p As Person ' retrieve Jane Austen p = CType(people(1), Person) ' set a custom Status string p.Status = "Not at home. Whew!" ' change only this Person objects' DisplayMethod p.DisplayMethod = New Person. _ displayPersonDelegate (AddressOf _ DisplayPersonStatus) ' display the results ListBox1.DataSource = Nothing ListBox1.DataSource = people End Sub
When you click the button, the results look like Figure 5.
Finally, giving class consumers the ability to create customized display strings for your classes goes a long way toward making the missing ItemData truly unnecessary. When you click on an item in the ListBox, it displays a MessageBox that shows the selected item and its ID, proving that associating an ID with an item by using objects works just as well as the older ItemData array?and doesn’t require the class consumer to write any code.
One small downside of this method is that if you want to post two ListBoxes side by side, both containing the same objects, but one displaying (for example) LastName/FirstName and the other displaying FirstName/LastName, you need to implement a Clone method so you can set different display methods for the objects in each list. In this particular case, writing a wrapper object to handle the class display may be a better design.