ListBox Programming
[Home] [Writing samples] [LinkedIn]

By Robert Delwood

Introduction

For presenting large amounts of information at one time, lists are the best. For .NET, these are listboxes and datagridviews. Listboxes are well known and are generally single column scrollable lists. Datagridviews tend to be multiple columns, presenting different information about the same subject for each row. For example, they may display a person's first and last name, address, age, and perhaps employee ID number. In both cases, they can be double clickable, taking an action on the selected row.

Programming these are similar and with a little extra work, can be extremely versatile with fast operations. This article addresses only listboxes but datagridviews are similar. For the project, start a VB.NET Windows Form application (in Visual Studio, click File|New Project|Visual Basic|Windows|Windows Form Application). On the form, drag a Button, and a ListBox. Resize the listbox to fit the form. I recommend setting one of the listbox's properties, Anchor, to Top, Left, Bottom, and Right. This adjusts the list box as the form is resized. Double click Button1 to show the code behind, and paste the following complete code. This will form the basis of the project.

Public Class Form1
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        TestListBox1()
    End Sub

    Private Sub TestListBox1()
    End Sub
End Class

The simplest way of formatting a listbox is by brute force, adding each item explicitly. Display the list by clicking Button1, defined by the TestListBox1 method.

Public Class Form1
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        TestListBox1()
    End Sub

    Private Sub TestListBox1()
        ListBox1.Items.Clear() 'Makes sure the list box is cleared each time.

        ListBox1.Items.Add("William Rehnquist")
        ListBox1.Items.Add("Warren E. Burger")
        ListBox1.Items.Add("Earl Warren")
        ListBox1.Items.Add("Fred M. Vinson")
    End Sub
End Class

Run the application (click Start), and click Button1, the listbox displays the four names. It's important to note that the four lines here represents the ability to collect the data, but that's not the focus of this article. The source could be a database, a file, the user entering data, or a programmatic algorithm to generate the information. Obviously it is impractical and unreasonable to use this individual line approach for large or extremely large amounts of information. For my examples, I'll use several ways based on the need of the example, but it is up to you to determine the information's source. How ever the information is gathered, it is added to the listbox with the ListBox1.Items.Add method.

A more versatile approach uses loops. I added a Label item to list the number of lines in the listbox. Drag a Label onto the windows form.

Public Class Form1
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        TestListBox1()
    End Sub

    Private Sub TestListBox1()
        ListBox1.Items.Clear() 'Makes sure the list box is cleared each time.

        For i As Integer = 0 To 1000
            ListBox1.Items.Add(CInt(Math.Ceiling(Rnd() * 12)) + 1)
        Next

        Label1.Text = String.Format("Items in the list: {0}", ListBox1.Items.Count)
    End Sub
End Class

This looping approach is more efficient but notice three points about it.

First, it requires a systematic source of information, here by generating a random number.

Second, there is a noticeable lag in completing the operation. If it's not noticeable, change the loop value from 1000 to perhaps 10,000, 100,000, or more.

Third, it's not very flexible. That is, you can't sort, or select information. There is a listbox Sort property that automatically lists the information in alphabetic order. This is useful for the simple operations, just as listing names, but I'm not going to use it.

A Better Way

The flexibility is a problem. In these examples, application is designed for the data. That is, the application does nothing more than displays the data. Real applications work the other way around. The data is the important factor and the application responds to it in different ways, such as adding or deleting entries, sorting, or analyzing them. For example, you may have a database of employee information, or a list of stock merchandise, whose information will be used different ways, including, but not limited solely to, displaying the information. For this, we will use an in memory database, such as an array or list collection.

Public Class Form1
    Dim gChiefsInformation As List(Of String) = New List(Of String)()

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        gChiefsInformation.Add("William Rehnquist")
        gChiefsInformation.Add("Warren E. Burger")
        gChiefsInformation.Add("Earl Warren")
        gChiefsInformation.Add("Fred M. Vinson")

        TestListBox1()
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        TestListBox1()
    End Sub

    Private Sub TestListBox1()
        ListBox1.Items.Clear()

        For Each item As String In gChiefsInformation
            ListBox1.Items.Add(item)
        Next

        Label1.Text = String.Format("Items in the list: {0}", ListBox1.Items.Count)
    End Sub
End Class 

Here, I've added a List collection, adding the four names to it. This is more representative of the situation, since the information can be used conveniently throughout the application. The following displays the list. I've also added the call in the form's load routine so it runs automatically now.

The Key: Using The DataSource Property

This is where we depart from the conventions. Using loops to enter information is slow and, now, is unneeded. Instead, there is a property called DataSource that takes a list or array and automatically applies that to the listbox. The same application the new way:

Public Class Form1
    Dim gChiefsInformation As List(Of String) = New List(Of String)()

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        gChiefsInformation.Add("William Rehnquist")
        gChiefsInformation.Add("Warren E. Burger")
        gChiefsInformation.Add("Earl Warren")
        gChiefsInformation.Add("Fred M. Vinson")

        TestListBox1()
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        TestListBox1()
    End Sub

    Private Sub TestListBox1()
        ListBox1.DataSource = gChiefsInformation

        Label1.Text = String.Format("Items in the list: {0}", ListBox1.Items.Count)
    End Sub
End Class

There are several implications. The first is this runs faster. Adding information is generally 16 times faster than using a loop. This may be trivial for up to 1000 items, but becomes significant at 100,000 with 1.8 seconds to load versus 27 seconds. In some instances it's possible to list a million points (for example, in scientific applications, such as recording the coefficients of an electrical function or points in a space, you can easily have millions or even billions of data points).

The second implication is that it gives you versatility in sorting the information. By using a collection, such as a list or array, you can use built in functions, like the Sort method, which is not the same as the Listbox's Sort property.

Public Class Form1
    Dim gChiefsInformation As List(Of String) = New List(Of String)()

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        gChiefsInformation.Add("William Rehnquist")
        gChiefsInformation.Add("Warren Burger")
        gChiefsInformation.Add("Earl Warren")
        gChiefsInformation.Add("John Marshall")
        gChiefsInformation.Add("Harlan Stone")
        gChiefsInformation.Add("John Jay")
        gChiefsInformation.Add("Fred Vinson")

        TestListBox1()
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        TestListBox1()
    End Sub

    Private Sub TestListBox1()
        gChiefsInformation.Sort()
        'gChiefsInformation.Reverse()
        ListBox1.DataSource = gChiefsInformation

        Label1.Text = String.Format("Items in the list: {0}", ListBox1.Items.Count)
    End Sub
End Class

Adding LINQ

Uncommenting the Reverse line reverses the order. However, the real power comes with LINQ. LINQ is .NET's query language, but it goes beyond just a SQL query. Microsoft has four LINQ types (called LINQ providers): LINQ to SQL, LINQ to XML, LINQ to DataSet, and LINQ to Objects. We're interested in the last provider. LINQ to Objects enables you to query in-memory collections and arrays. It is available by default in all Visual Studio projects but separately as the System.Linq namespace. You could do the same sort with LINQ. Replace TestListBox1 with this:

Private Sub TestListBox1()
	ListBox1.DataSource = gChiefsInformation.OrderBy(Function(x) x.Substring(0, x.Length)).ToList
	
	Label1.Text = String.Format("Items in the list: {0}", ListBox1.Items.Count)
End Sub

Or with a little more work, sort by last name. Replace the one line with this one:

ListBox1.DataSource = gChiefsInformation.OrderBy(Function(x) (x.Split(" "))(x.Split(" ").Length - 1)).ToList

Or even the number of letters in their name:

ListBox1.DataSource = gChiefsInformation.OrderBy(Function(x) x.Length).ToList

The last one may be interesting but hardly useful. The boring fact is there isn't much you can do with having just their names. To see the real power of LINQ, we need to have more information. And for that, we have to introduce objects. These represent their full name, last name, and the year appointed to the seat.

Public Class Form1
    Dim gChiefsInformation As List(Of JusticesNames) = New List(Of JusticesNames)()

    Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown
        gChiefsInformation.Add(New JusticesNames("William Rehnquist", "Rehnquist", 1986))
        gChiefsInformation.Add(New JusticesNames("Warren Burger", "Burger", 1969))
        gChiefsInformation.Add(New JusticesNames("Earl Warren", "Warren", 1953))
        gChiefsInformation.Add(New JusticesNames("John Marshall", " Marshall ", 1801))
        gChiefsInformation.Add(New JusticesNames("Harlan Stone", "Stone", 1941))
        gChiefsInformation.Add(New JusticesNames("John Jay", "Jay", 1789))
        gChiefsInformation.Add(New JusticesNames("Fred Vinson", "Vinson", 1946))

        ListBox1.DisplayMember = "FirstName"

        TestListBox1()
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        TestListBox1()
    End Sub

    Private Sub TestListBox1()
        ListBox1.DataSource = gChiefsInformation

        Label1.Text = String.Format("The list contains: {0}", ListBox1.Items.Count)
    End Sub

    Class JusticesNames
        Public Property FirstName As String
        Public Property LastName As String
        Public Property YearInOffice As Integer

        Public Sub New(first As String, last As String, year As Integer)
            Me.FirstName = first
            Me.LastName = last
            Me.YearInOffice = year
        End Sub
    End Class
End Class

Besides using objects, an important addition is the line. With multiple fields for each object, you must define the field to display:

ListBox1.DisplayMember = "FirstName"

Now we can use LINQ. Replace the method with this one.

ListBox1.DataSource = gChiefsInformation.OrderBy(Function(x) x.YearInOffice).ToList

You get the names listed in ascending order that they took office. This is for descending order:

ListBox1.DataSource = gChiefsInformation.OrderByDescending(Function(x) x.YearInOffice).ToList

There are two factors at work here. First, the LINQ statement creates a new collection, based on the original list:

ChiefsInformation.OrderByDescending(Function(x) x.YearInOffice).ToList

Then we assign that to the DataSource. In fact, it has to be explicitly cast as a list (by the ToList method). Second, the original list is kept intact. That means we can keep making all the LINQ calls we need without endangering the original list. That makes for good programming practice.

The more complete form of a LINQ call, and may be easier to work with, is the expanded form. This form looks more like the start of a conventional SQL call:

Dim queryResults = From justices In gChiefsInformation

The justices name is only a local variable and can be called anything. A more complete example:

Private Sub TestListBox1()
	Dim queryResults = From justices In gChiefsInformation
						Where justices.YearInOffice > 1961
						Select justices Order By justices.YearInOffice
	ListBox1.DataSource = queryResults.ToList

	Label1.Text = ListBox1.Items.Count
End Sub
	

This returns those justices starting after 1961 and from among those, are in ascending order. Many programmers are familiar with this format from SQL programming and may be more natural to figure out. It may also be tempting to list all the information, such as name, date confirmed, and the number of years in office, but remember this is a listbox, intended to present a small amount of information, such as only the name. Fully listing additional information is what a datagridview tool does. The procedures are identical for using a DataSource property, but the set up and overhead is slightly different. The datagridview tool is not covered here.

The Final Example

To make this display interactive, add three radio buttons. Each radio button controls the display of the list. You could also label the radio buttons' Text property in the design time's property list. I've also added a field, the length in the office.

Public Class Form1
    Dim gChiefsInformation As List(Of JusticesNames) = New List(Of JusticesNames)()

    Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown
        gChiefsInformation.Add(New JusticesNames("John Roberts", "Roberts", 2005, 11.5))
        gChiefsInformation.Add(New JusticesNames("William Rehnquist", "Rehnquist", 1986, 18.7))
        gChiefsInformation.Add(New JusticesNames("Warren Burger", "Burger", 1969, 17.2))
        gChiefsInformation.Add(New JusticesNames("Earl Warren", "Warren", 1954, 15.5))
        gChiefsInformation.Add(New JusticesNames("John Marshall", " Marshall ", 1801, 34))
        gChiefsInformation.Add(New JusticesNames("Harlan Stone", "Stone", 1941, 4.9))
        gChiefsInformation.Add(New JusticesNames("John Jay", "Jay", 1789, 5))
        gChiefsInformation.Add(New JusticesNames("Fred Vinson", "Vinson", 1946, 7.3))

        RadioButton1.Text = "Sort: By Name"
        RadioButton2.Text = "By Date In Office"
        RadioButton3.Text = "No Sort"

        ListBox1.DisplayMember = "FirstName"

        RadioButton3.PerformClick()
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        TestListBox1()
    End Sub

    Private Sub TestListBox1()
        Dim queryResults = (From justices In gChiefsInformation
                            Where justices.YearInOffice > 1961 And justices.YearsTenure > 10
                            Select justices Order By justices.YearsTenure).ToList
        ListBox1.DataSource = queryResults
        'ListBox1.DataSource = gChiefsInformation.OrderByDescending(Function(x) x.YearsTenure).ToList
    End Sub

    Class JusticesNames
        Public Property FirstName As String
        Public Property LastName As String
        Public Property YearInOffice As Integer
        Public Property YearsTenure As Integer

        Public Sub New(first As String, last As String, year As Integer, tenure As Integer)
            Me.FirstName = first
            Me.LastName = last
            Me.YearInOffice = year
            Me.YearsTenure = tenure
        End Sub
    End Class

    Private Sub RadioButton1_CheckedChanged(sender As Object, e As EventArgs) Handles RadioButton1.CheckedChanged
        Dim queryResults = (From justices In gChiefsInformation
                            Select justices Order By justices.LastName).ToList
        ListBox1.DataSource = queryResults
    End Sub

    Private Sub RadioButton2_CheckedChanged(sender As Object, e As EventArgs) Handles RadioButton2.CheckedChanged
        Dim queryResults = (From justices In gChiefsInformation
                            Where justices.YearsTenure > 8
                            Select justices Order By justices.YearInOffice).ToList
        ListBox1.DataSource = queryResults
    End Sub

    Private Sub RadioButton3_CheckedChanged(sender As Object, e As EventArgs) Handles RadioButton3.CheckedChanged
        ListBox1.DataSource = gChiefsInformation
    End Sub

    Private Sub ListBox1_DataSourceChanged(sender As Object, e As EventArgs) Handles ListBox1.DataSourceChanged
        Application.DoEvents()
        Label1.Text = String.Format("The list contains: {0}", 
            CType(ListBox1.DataSource, List(Of JusticesNames)).Count)
    End Sub
End Class

The last thing changed is that the list box count now appears in an event handler. Each object has a set of predefined handlers that are triggered by certain events, such as a mouse click, a mouse double click, the form being resized and so on. If the event code is completed, whenever it occurs the code executes. For example, if you wanted to take an action based on double clicking a justice's name, you would add a MouseDoubleClick event for the list box. In this case, the event is DataSourceChanged. As you might guess, this is trigged by assigning a list to the DataSource property. Here, the application updates the display of the list count without us having to explicitly make that call anymore.