Read an Excerpt
By A. Russell Jones
John Wiley & SonsISBN: 0-7821-4253-2
Chapter OneWindows Forms Solutions
SOLUTION 1 ListBox ItemData Is Gone!
SOLUTION 2 Create Owner-Drawn ListBoxes and Combo Boxes
SOLUTION 3 Upgrade Your INI Files to XML
SOLUTION 4 Build Your Own XML-Enabled Windows Forms TreeView Control
ListBox ItemData Is Gone!
PROBLEM Classic VB ListBoxes had an ItemData property that let you associate an item in a ListBox with something else, such as an ID value for a row in a database table, or an index for an array of items. But .NET ListBoxes don't have an ItemData property. How can I make that association now?
SOLUTION Place your items in a class. When you do that, you often don't need an index or ID number, because the items are directly available from the ListBox's Items collection.
The look of those familiar VB ListBoxes and ComboBoxes hasn't changed, but the way they work has changed dramatically. For those of you just getting started with .NET, dealing with ListBoxes and ComboBoxes is often one of the first sources of serious frustration. But don't worry. In 10 minutes you can absorb the basic workings of the new .NET ListBoxes and ComboBoxes, and you'll never miss ItemData again.
For the rest of this solution, I'll limit the discussion to ListBoxes, but all the information in this solution works with both ComboBoxes and ListBoxes.
The data model for classic VB ListBoxes consisted of the List property, which held a simple array of strings, and a parallel ItemData array that held Long numeric values. It was convenient to use the two lists in tandem; for example, you might populate a ListBox with a list of strings from a database table, while simultaneously populating the ItemData property with a unique numeric value from that table, such as an AutoNumber. 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. Table 1 shows the classic VB ListBox data model with three items in the List array, and three Long integer values in the ItemData array.
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 having two separate arrays limited to Strings and Longs, the .NET ListBox has only one collection, called Items, which holds objects-meaning you can store any type of object as an item in a ListBox, and not just simple strings and numbers. However, the ListBox still needs a string to display for each item. That's easy. By default, the ListBox calls the ToString method to display each item in the Items collection.
But wait! What if the ToString method doesn't display what you need? That's easy too. List- Boxes now have a DisplayMember property. If the DisplayMember property is set, the ListBox invokes the item number named by the DisplayMember property before displaying the item.
In other words, rather than storing a single set of strings and associated ID values, and then having to do extra work of retrieving the appropriate data when a user clicks on an item, you can now store the entire set of objects-right in the Items property.
Still, despite the best efforts of VB.NET experts to convince them otherwise, people aren't always happy with the current 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. So first, I'll show you how to re-create the functionality of the classic VB ListBox control, and then I'll show you how to move far beyond it-and even beyond the probable intent of the .NET designers-to create an extremely flexible strategy for displaying items in .NET ListBoxes.
Mimicking a Classic VB ListBox
What you're about to do may feel awkward at first, but you'll soon find that as your thinking patterns switch from managing raw data to handling classes, it will become a natural behavior. Because you're trying to mimic an ItemData property that doesn't exist, your first inclination might be to subclass the .NET ListBox control and add your own parallel array of Integer values, accessed via an added ItemData property. But that carries baggage you don't need, because you'd have to manage the new array in code-which becomes very difficult with a control that can sort items. You'd then have to make sure the arrays stay synchronized across sorts when users modify the Item collection-it can be a mess.
Populating a ListBox
Here's an easier way. Rather than adding the ItemData property to the control itself, add the ItemData value to the items you put into the Items collection. When you do that, you don't have to subclass the control or write any special sorting or list modification code. For example, suppose you have a list of employee names and ID numbers. When a user clicks on an employee name in the ListBox, you want to show a MessageBox with that user's ID number and name. Assume you have the names in a string array called names, and the IDs in a Long array called IDs. In classic VB, you would write code like this:
Dim i As Long For i = 0 To UBound(names) List1.AddItem names(i) List1.ItemData(List1.NewIndex) = ids(i) Next
In .NET, however, you create a simple class with two properties, Text and ItemData, and a constructor to make it easy to assign the two properties when you create the class. Listing 1 shows the code for such a class, named ListItem.
Assuming you have the names and IDs arrays already populated, you can create instances of your ListItem class and assign them to the ListBox's Items collection using a simple loop:
Dim i As Integer
For i = 0 To names.Length - 1 Me.ListBox1.Items.Add(New ListItem(names(i), ids(i))) Next
But if you run this code, you'll find that the ListBox displays a list of items that look like [Projectname].ListItem rather than the list of names you were expecting. That's because, by default, the ListBox calls the ToString method for each item to get a displayable string. In this case, however, you don't want to use the default; you want the ListBox to display the Text property. So, add this line before the loop that populates the ListBox:
Me.ListBox1.DisplayMember = "Text"
That tells the ListBox to display the Text property for each item rather than the results of ToString.
You must assign a property member to the ListBox.DisplayMember property-using a public field or a function doesn't work. That's because the display functionality works through reflection-the ListBox dynamically queries the item at runtime for a property with the name you assign to the ListBox.DisplayMember property.
Of course, it's your class, and you can eliminate the DisplayMember assignment by overriding the ToString method to show whatever you like. In this case, you want to show the Text property. So, add this code to the ListItem class:
Public Overrides Function ToString() As String Return Me.Text End Function
Now you can remove the DisplayMember assignment and the ListBox will still display the results of the Text property.
Getting the Data Back
As you've seen, you can use this simple ListItem class to work with exactly the same data you used in classic VB ListBox code. Getting the data back is just as simple. When a user clicks an item, the .NET ListBox fires a SelectedItemChanged event. That happens to be the default event for the ListBox, so if you double-click on it in design mode, Visual Studio will insert a stub event handler for you. Fill in the event-handling code as follows:
Private Sub ListBox1_SelectedIndexChanged( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles ListBox1.SelectedIndexChanged
Dim li As ListItem If Me.ListBox1.SelectedIndex >= 0 Then li = DirectCast(Me.ListBox1.SelectedItem, ListItem) Debug.WriteLine("Selected Item Text: " & _ li.Text & System.Environment.NewLine & _ "Selected ItemData: " & li.ItemData) End If End Sub
First, test to ensure that an item is selected. If so, even though you know that it's a ListItem, the ListBox.Items collection doesn't-it's a collection of objects. Therefore, you need to cast the selected item to the correct type, using either the CType or DirectCast method (Direct- Cast is faster when you know the cast will succeed).
Now that you've seen a way to re-create VB6 ListBox behavior, I'll concentrate on other ways to use the list controls in .NET, including binding the control to a collection type.
The Class Creator Has Control
Suppose you're told to use a Person class (created by a co-worker) that has four properties: ID (Long), LastName, FirstName, and Status (see Listing 2). The Person object has an overloaded constructor so you can assign all the values when you create the object. I've included the complete, finished code for the Person class in Listing 2, even though we're assuming your co-worker didn't give you the class in quite this shape. I've highlighted the portions that you'll add in the next section of this solution. The Person class has ID, LastName, FirstName, and Status properties. Although it exposes LastFirst and FirstLast methods, the interesting parts are the DisplayPersonDelegate, the DisplayMethod property, and the overridden ToString method.
You want to fill a ListBox with Person objects. So you create a Form and drag a ListBox onto it. You want the ListBox to fill when the user clicks a button, so you add a Fill List button to do that (see Figure 1).
VB.NET makes it easy to display items in a ListBox, because you can set the ListBox's DataSource property (binding the list) to any collection that implements the IList interface, which represents a collection of objects that you can access individually by index. Note that you don't have to populate the list through binding; you can still write a loop to add items to the ListBox, as you've already seen in the "Populating a ListBox" section of this solution. However, binding is convenient, as long as you understand exactly what the framework does when it displays the list.
The ArrayList class implements the IList interface, so you can create an ArrayList member variable for the form, called people, and fill it with Person objects during the Form_Load event.
' define an ArrayList at class level Private people As New ArrayList()
Private Sub Form2_Load( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles MyBase.Load
Dim p As Person Me.ListBox1.Sorted = True ListBox1.DisplayMember = "ToString" ListBox1.ValueMember = "ID" p = New Person(1, "Twain", "Mark", "") people.Add(p) p = New Person(2, "Austen", "Jane", "") people.Add(p) p = New Person(3, "Fowles", "John", "") people.Add(p) End Sub
Now, when a user clicks the Fill List button, the ListBox displays items automatically because the code sets the ListBox's DataSource property to the people ArrayList:
Private Sub btnFillList_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) + Handles btnFillList.Click
ListBox1.DataSource = Nothing ListBox1.DataSource = people End Sub
Unfortunately, you find that the class creator didn't override the ToString implementation or include 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, Solution1.Person. The result looks like Figure 2.
OK, no problem. What about using the DisplayMember property? Just add the following line to the end of the Button1_Click method:
ListBox1.DisplayMember = "LastName"
Now, run the project again. This time, the result is a little closer to what you want (see Figure 3). Setting the ListBox's DisplayMember property to the string "LastName" causes the ListBox to invoke the LastName method. Unfortunately, this displays only the last names, not the last and first names.
Now you're stuck. Unless you can get the class creator to add a LastFirst property, you'll have to go to a good deal of trouble to get the list to display both names. (At this point, you have to pretend the class creator actually helps and adds a LastFirst property to the Person class.)
Public ReadOnly Property LastFirst() As String Get Return Me.LastName & ", " & Me.FirstName End Get End Property
Now you can change the ListBox.DisplayMember property, and the form will work 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 clients want 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, as well as 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 have 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 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. Of course, you can mimic it, as you've seen, but that seems like a lot of trouble when you already have a class that you could store directly into the ListBox.
All these possibilities are onerous choices. Things would be a lot easier if you could just control the Person class. What's the answer?
Delegate, Delegate, Delegate
At this point, you need to change roles-take off your reader hat and put on your control creator hat. Here's a completely 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 as possible over the instantiated objects. One way to increase 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 predefining multiple display methods within your class, you provide a public Delegate type, and then add a private member variable and a public property to your class that accept the delegate type. For example:
' 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
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 Try Return Me.DisplayMethod(Me) Catch Return MyBase.ToString() End Try End Function
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 have 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.
Excerpted from .NET Programming by A. Russell Jones Excerpted by permission.
All rights reserved. No part of this excerpt may be reproduced or reprinted without permission in writing from the publisher.
Excerpts are provided by Dial-A-Book Inc. solely for the personal use of visitors to this web site.