Your Listbox-Filling Options

<< Click to Display Table of Contents >>

Navigation:  Combo Boxes and List Boxes >

Your Listbox-Filling Options

200112_AB3 Andy Baron        

Powerful list and combo boxes have always been an important part of Access. With Access 2002/03, they acquire even more power, as Andy Baron shows in this article.

Combo and list boxes have always been very full-featured in Access. Access developers take for granted capabilities like multiple columns and having control over combo box list width, which are unavailable in other development environments (unless you buy third-party controls or perform fancy API tricks). Not much has changed in the Access versions of these controls over the years, other than the addition of the Dropdown method for combo boxes—until now, that is! Access 2002 offers important new ways to populate your list controls.

The list-filling techniques discussed in this article are all alternatives to using queries based on persistent connections to your data. By reducing persistent connections, you improve your application's scalability. However, increasing scalability, as in these examples, is only one reason to use these techniques. They're also handy if you want to list items, like files or printer names, that aren't stored in a database. Without these techniques, you'd need to dump the data into a table and use a query based on that table as the row source of your list control, or use complex callback functions.

The sample application

The sample application that accompanies this article contains a form with several examples of combo and list boxes that demonstrate the list-filling capabilities of Access 2002 (see Figure 1).

200112_AB1 Figure 1

 

The sample application gets its data by opening ADO recordsets against the SQL Server Northwind database. To establish a connection to the database, the code uses the accompanying Data Link file, Northwind.UDL. You can adjust the connection properties by double-clicking on this file and using the dialog box shown in Figure 2. If necessary, you can even change the connection to use the Jet provider and to point to Northwind.mdb, instead of using SQL Server.

200112_AB2 Figure 2

Here's the code that opens a connection to the database when one is needed:

Public Function ConnectToNorthwind( _

 cnn As ADODB.Connection) As Boolean

Dim blnState As Boolean

  

If cnn Is Nothing Then

   Set cnn = New ADODB.Connection

End If

  

On Error Resume Next

If cnn.State = adStateOpen Then

   blnState = True

Else

   cnn.ConnectionString = _

     "File Name=" & CurrentProject.Path & _

     "\Northwind.UDL"

        

   cnn.Open

   If cnn.State = adStateOpen Then

      blnState = True

   Else

      blnState = False

   End If

End If

ConnectToNorthwind = blnState

End Function

The recordsets in the sample code use client-side cursors, and their connections to the database are closed immediately after the data is retrieved. The sample application doesn't maintain any persistent connections to the database, increasing scalability.

These "disconnected" list-filling techniques are useful in n-tier applications where data is supplied by middle-tier objects. If that data is packaged as ADO recordsets, then you can use code very similar to the sample code in this application. You'll also see an example of using XML data to create an ADO recordset. You're not required to rely on recordsets—you could use the MSXML parser, for example, to fill lists from XML data without using ADO recordsets at all. To do that, you'd use a value list as your row source type. Value lists are much improved in Access 2002.

Expanded value list capacity

As in prior versions of Access, you can populate combo and list boxes programmatically or in design view by setting the RowSourceType to "Value List" and assigning a delimited list of string values to the RowSource property. For example, you might assign "Male;Female" to the row source of a combo box used for entering a person's gender.

However, in previous versions of Access, this technique could only be used for very short lists, because the RowSource property was limited to strings containing a maximum of 2,048 characters. In Access 2002, that maximum has been increased to 32,768 (32K). This makes value lists much more practical, and, as a side benefit, it also enables you to use more complex SQL strings as row sources.

To create multicolumn controls based on value lists, set the number of columns in the control's Column Count property, and add a semicolon or comma delimiter between data for each column. For example, if you want to use a code of 1 for Male and 2 for Female, enter the values shown in Table 1 for the control's properties.

Table 1. Sample values for a value list.

Property name

Value

Column Count

2

Row Source Type

Value List

Row Source

1;Male;2;Female

You can use either semicolons or commas to delimit the list, or a combination of the two. If you use commas in the property window, Access will convert them to semicolons. However, when assigning a value list row source in code, you can use either or both characters.

So, what do you do if the values in your list contain commas or semicolons? For example, suppose you want a list of employees to appear as LastName, FirstName. You can accomplish this by enclosing your values in quotes. For example, the row source of a two-column value list containing employee IDs and names might look like this:

1,"Baron, Andy",2,"Chipman, Mary"

The sample application uses the ADO GetString method to transform a recordset into a semicolon-delimited list, with quotes surrounding each of the values. The Employees combo box shown in Figure 1 has a row source type of Value List, and it has this code in its Enter event procedure:

Private Sub cboEmployeeVL_Enter()

Dim cnn As ADODB.Connection

Dim rst As ADODB.Recordset

Dim strEmployees As String

    

On Error GoTo HandleErr

  

If ConnectToNorthwind(cnn) Then

  Set rst = New ADODB.Recordset

  rst.CursorLocation = adUseClient

  rst.Open _

    Source:="SELECT EmployeeID," _

    & " LastName + ', ' + FirstName" _

    & " FROM Employees" _

    & " Order By LastName, FirstName", _

    ActiveConnection:=cnn, _

    CursorType:=adOpenStatic, _

    Options:=adCmdText

  Set rst.ActiveConnection = Nothing

  CloseAndReleaseConnection cnn

  strEmployees = rst.GetString(adClipString, _

    ColumnDelimeter:=""";""", RowDelimeter:=""";""")

    strEmployees = """" & _

    Left(strEmployees, Len(strEmployees) - 2)

  Me!cboEmployeeVL.RowSource = strEmployees

    

  rst.Close

Else 

  MsgBox "Couldn't connect to Northwind"

End If

ExitHere:

  CloseAndReleaseConnection cnn

  Set rst = Nothing

  Exit Sub

  

HandleErr:

  MsgBox Err & ": " & Err.Description, , _

    "Error in cboEmployeeVL"

  Resume ExitHere

End Sub

The only tricky part of the code is the doubling up of quotes inside of quotes. The value list string that this code produces begins like this:

"5";"Buchanan, Steven";"8";"Callahan, Laura";

The first column of the combo box, containing employee IDs, has a width of zero, so it's hidden. When you select an employee, the Orders list box, which is also shown in Figure 1, gets populated with a list of the orders taken by that employee, showing the order ID and order date. The code for this is a little simpler because there's no need to worry about the order IDs or dates including commas or semicolons. Without that worry, there's no need to enclose the values in quotes:

Private Sub cboEmployeeVL_AfterUpdate()

Dim cnn As ADODB.Connection

Dim rst As ADODB.Recordset

Dim strSQL As String

Dim strOrders As String

  

On Error GoTo HandleErr

If ConnectToNorthwind(cnn) Then

  strSQL = "SELECT OrderID, OrderDate " & _

   "FROM Orders " & _

   "WHERE EmployeeID = " & Me!cboEmployeeVL & _

   " ORDER BY OrderDate DESC"

 

  Set rst = New ADODB.Recordset

  rst.CursorLocation = adUseClient

  rst.Open _

    Source:=strSQL, _

    ActiveConnection:=cnn, _

    CursorType:=adOpenStatic, _

    Options:=adCmdText

  Set rst.ActiveConnection = Nothing

  CloseAndReleaseConnection cnn

  If rst.EOF Then

    rst.Close

    GoTo ExitHere

  End If

    

  strOrders = rst.GetString(adClipString, _

     ColumnDelimeter:=";", RowDelimeter:=";")

  Me!lboOrdersVL.RowSource = strOrders

  rst.Close

Else 

  MsgBox "Couldn't connect to Northwind"

End If

ExitHere:

  CloseAndReleaseConnection cnn

  Set rst = Nothing

  Exit Sub

  

HandleErr:

  MsgBox Err & ": " & Err.Description, , _

    "Error in cboEmployeeVL_AfterUpdate"

  Resume ExitHere

  Resume

End Sub

Here's the beginning of the string that this code produces:

11075;5/6/1998;11068;5/4/1998;

Both of these event procedures, as well as all of the others in this application, call a public sub to close and release the ADO connection:

Public Sub CloseAndReleaseConnection( _

 cnn As ADODB.Connection)

  If Not cnn Is Nothing Then

    If cnn.State = adStateOpen Then

      cnn.Close

      Set cnn = Nothing

    Else

      Set cnn = Nothing

    End If

  End If

End Sub

Using GetString is, of course, only one of many possible ways to construct a value list. If you're working with XML data, you could even create an XSL stylesheet that would transform an XML document into a delimited string suitable for use as a value list. In addition, Access 2002 provides two new methods for adding and removing items from value lists.

Editing value lists

If you've used Visual Basic forms or the forms package that's used with VBA in Word and Excel, then you've probably used the AddItem method to populate combo and list boxes. Access 2002 introduces AddItem and RemoveItem methods that are implemented as wrappers around value lists. To use these methods in Access, your combo or list box must have a Row Source Type of Value List, and these new methods simply append or remove items from the value list string.

The sample application includes two controls very similar to the ones shown in Figure 1, except that their code uses AddItem rather than GetString to construct the value lists. Here's the section of code that populates the employees combo box using the new AddItem method:

With Me.cboEmployeesAI

  Do Until rst.EOF

    .AddItem """" & rst!EmployeeID & """;""" _

       & rst!FullName & """"

    rst.MoveNext

  Loop

End With

You still need to add extra quotes if your values might contain commas or semicolons, and you need to add a delimited set of values for each item if you need multiple columns. In other words, all AddItem does is append a string to the existing value list string.

The RemoveItem method isn't used in the sample application. RemoveItem enables you to delete a section of your value list by specifying the zero-based row index or the value in the bound column of that row. This is a bit handier than AddItem, because the code to do this manually would be a bit trickier to write.

There's no Clear method to go along with AddItem and RemoveItem. If you want to clear the contents of a value list, you must assign an empty string to the row source property. The code for the AddItem version of the Orders list box does this:

With Me.lboOrdersAI

  .RowSource = ""

  Do Until rst.EOF

   .AddItem rst!OrderID & ";" _

      & rst!OrderDate

   rst.MoveNext

  Loop

End With

If you've worked with the VB or VBA list controls, you may also have used their Column and List properties, which enable you to fill a control by assigning an array of values to the property. These properties weren't added to the Access controls, although the familiar Access Column property still enables you to read values stored in your combo box. The sample application includes examples of using the MS Forms combo and list boxes (the ones used in Word and Excel) on the Access form. In the examples, the ADO GetRows method transforms a recordset into an array, and the code assigns this array to the Column property of the control. This is a very efficient technique that isn't available in Access controls:

varList = rst.GetRows

cboMSF.Column = varList

Unfortunately, the missing Clear method, List property, and read/write Column property aren't the only shortcomings to the implementation of AddItem and RemoveItem in Access 2002. Performance is seriously flawed. Perhaps this will be fixed in a service pack, but using the original release of Access 2002 to run the sample application results in very noticeable delays when the controls are being filled by using AddItem. For this reason, be sure to test your application carefully before relying on these new methods in production code.

Despite the poor performance of AddItem for populating lists, however, the expansion of the size of value lists in Access 2002 is a very welcome feature. With just a little effort, you can easily write your own list-filling code for value lists, using GetString, XML techniques, or simple string concatenation, and you now have a very viable alternative to relying on database objects as row sources. Although the controls in the sample application are unbound, you can use value lists with bound controls (ones that have control sources) just as effectively.

Assigning recordsets to controls

In Access 2000, Microsoft made it possible for you to assign recordsets to forms, instead of setting the form's record source. Now in Access 2002, you can assign a recordset to a combo or list box, instead of setting the control's row source (you can also now assign properly shaped recordsets to Access reports).

For the types of list-filling needs illustrated in the sample application, this is clearly the most suitable new technique to use, and you'll find that it performs very well. Why bother with value lists when you can assign your recordset directly to the control? With this new capability, the only time you need to consider using a value list is if your data doesn't lend itself easily to being gathered in a recordset.

The sample application contains a generic procedure, LoadControlRst, that accepts a control, a source string, and a command type as parameters. The code opens a recordset against Northwind using the source string and the command type. In the sample form, the source strings are SQL statements, and the command type is adCmdText; but you could also use the name of a stored procedure with adcmdStoredProc as the command type. Once the recordset is created, it's assigned to the control

The LoadControlRst procedure was written to work with lookup data that doesn't change often—for example, lists of products or employees. Instead of going to the database each time the procedure needs to fill a list control, it first looks for a locally saved XML file. If it finds the file, it loads a recordset from the file. If it doesn't find the file, it connects to the database, opens a client-side recordset, disconnects, saves the recordset as XML, and assigns the recordset to the control. To refresh the XML files, you call a procedure that deletes the local XML file and reloads the control. Here's the opening of the LoadControlRst procedure:

Public Sub LoadControlRst( _

  ByRef ctl As Control, _

  strSource As String, _

  lngRowSourceCommandType As ADODB.CommandTypeEnum)

    

On Error GoTo Handle_Err

Const conProcName As String = _

    "LoadControlRst"

Dim strFile As String

Dim cnn As ADODB.Connection

Dim rst As ADODB.Recordset

This code generates the name that will be used for the XML file:

  strFile = CurrentProject.Path & "\" _

  & CurrentProject.Name & "_" & ctl.Name & ".XML"

With the preliminary work done, the code then creates a client-side recordset and either opens the XML or creates it:

    Set rst = New ADODB.Recordset

    rst.CursorLocation = adUseClient

    rst.CursorType = adOpenStatic

    rst.LockType = adLockReadOnly

    If Len(Dir(strFile)) > 0 Then

      rst.Open Source:=strFile, Options:=adCmdFile

    Else

      Set rst.ActiveConnection = cnn

      rst.Open Source:=strRowSource, _

       Options:=lngRowSourceCommandType

      rst.Save strFile, adPersistXML

      Set rst.ActiveConnection = Nothing

      CloseAndReleaseConnection cnn

    End If

In the previous code, if the XML file is found, the recordset is loaded from the XML file. If the file isn't found, the data is retrieved, and the resulting recordset is saved as an XML file. With the recordset now created, it's assigned to the control passed to the routine:

    Set ctl.Recordset = rst

The error handler for this routine has to handle finding the file already present when it saves the recordset (this could happen if someone saves it between the time we checked for it and when we saved it):

Handle_Err:

  Select Case Err.Number

    Case 58   ' File already exists.

      Kill strFile

      Resume

    Case Else

      Err.Raise Err.Number, _

        conProcName, Err.Description

  End Select

Figure 3 shows the part of the sample form that works with recordsets and XML. When you click the Refresh button next to the employees combo box, it runs this code:

Call RefreshControlRst( _

    Me.cboEmployeesXML, _

    "SELECT EmployeeID, " _

    & "LastName + ', ' + FirstName" _

    & " AS FullName" _

    & " FROM Employees" _

    & " Order By LastName, FirstName", _

    adCmdText)

 

200112_AB3 Figure 3

Here's the RefreshConrolRst procedure that's called by the Refresh button click event procedure:

Public Sub RefreshControlRst(ctl As Control, _

 strRowSource As String, _

 lngRowSourceCommandType As ADODB.CommandTypeEnum)

 

Const conProcName = "RefreshControlRst"

On Error Resume Next

Kill CurrentProject.Path & "\" _

    & CurrentProject.Name & "_" & ctl.Name & ".xml"

Err.Clear

On Error GoTo Handle_Err

Call LoadControlRst(ctl, strRowSource, _

   lngRowSourceCommandType)

This example is somewhat complex, because of the use of XML files to store lookup data locally without making repeated calls to the database. But the part of the code that works with the new Recordset property of list controls is very straightforward—you simply assign your recordset to the property:

Set ctl.Recordset = rst

You can use disconnected recordsets, as the sample code does. However, you can't use recordsets that you create from scratch yourself. ADO enables you to create a new recordset, add field definitions to it, and then add rows of data, without ever connecting to a data source. This type of recordset can't successfully be assigned to an Access list control. If you're composing list data in code without ever making a connection to a data source, that would be a good place to use a value list.

List controls have always been among the most useful features in Access, and in Access 2002 they get even better. Value lists now can hold up to 32K characters, not the measly 2K that was supported previously. New AddItem and RemoveItem methods make it easier to edit value lists, but test their performance in your application carefully before you rely on them. You can also now fill a list control simply by assigning a recordset to the control's new Recordset property. One application of these new techniques is to build forms that don't require persistent connections to a database in order to display lookup data.

Your download file is called 112BARON.zip in the file SA2001-12down.zip

This can be purchased with all the other downloads on this page

 

Other Pages at the Site Of A Similar Nature

Tricks With Combo Boxes

Taming the Treeview Control

Tip: The IN Crowd