Visual Basic

Adopting flexible parameter types for inputs to a procedure

Another good practice to adopt is using more flexible parameter types for inputs to a procedure. In tutorial 4, "Using Variants Instead of Simple Data Types" If you take that advice, you should be careful to validate the parameters and display helpful errors. In a simple application, you can easily locate the cause of an error; but if the error occurs in a compiled ActiveX control, it might be a different story. The sample code here is the procedure declaration for a subroutine that fills a list box from an array:

Public Sub FillList(ByVal lst As ListBox, anArray() As Integer)

The function might work fine, but it's restrictive. Imagine you have another type of list box control that has some added functionality. You won't be able to pass it into this function. It's also possible that someone might want to use this routine with a combo box. The code will be similar, so this is a feasible request. However, you won't be able to use the procedure above with a combo box. If the routine is part of the application, you can rewrite it; more than likely, however, you'll write another routine instead. If the routine is in a DLL file, rewriting it might not be so easy. In the following code, the procedure header is changed to make it more generic and the rest of the code is added as well:

Public Sub FillList(ByVal ctl As Control, anArray() As Integer)
      Dim nIndex  As Integer
      For nIndex = LBound(anArray) To UBound(anArray)
          ctl.AddItem anArray(nIndex)
      Next nIndex
  End Sub

Notice the potential problem now in this routine, however. If any control that doesn't have an AddItem method is passed to the routine, it will fail. It might be some time later, when another programmer calls the routine, that the error is detected; and if the routine is in a DLL, it might take some time to debug. What we need is some defensive programming. Always try to code as if the procedure is part of an external DLL in which other programmers cannot access the source code. In this example, you can use defensive coding in two ways: by using Debug.Assert or by raising an error.

The Debug.Assert method, introduced in Visual Basic 5, evaluates an expression that you supply and, if the expression is false, executes a break. C programmers use these assertions in their code all the time. This method is intended to trap development-type errors that you don't expect to occur once the system is complete. You should never use assertions in a built executable; therefore, the method has been added to the Debug object. In a built executable, Debug.Assert is ignored, just as with the Debug.Print method. You could use an assertion here like this:

Public Sub FillList(ByVal ctl As Control, anArray() As Integer)
      Dim nIndex  As Integer
      ' Assert - This subroutine handles only ListBox and ComboBox.
      Debug.Assert TypeOf ctl Is ListBox Or _
          TypeOf ctl Is ComboBox
      For nIndex = LBound(anArray) To UBound(anArray)
      .
      .
      .

This will now trap the error if the routine is running in design mode. Because the debugger will break on the assert line, it's always best to put a comment around the assert so that another programmer triggering the assert can easily identify the problem.

With our example, the assert is not a good method to use for defensive programming because we might put this routine into a DLL, in which case the assert would be ignored and the user would get an error. A better way would be to raise an error. When you raise an error, the code that calls this function will have to deal with the problem. Think of the Open procedure in Visual Basic. If you try to open a file that doesn't exist, the Open procedure raises an error: "File not found." We can do the same with our routine:

Public Sub FillList(ByVal ctl As Control, anArray() As Integer)
      Dim nIndex  As Integer
      Const ERR_INVALID_CONTROL = 3000
      If Not(TypeOf ctl Is ListBox) And _
          Not(TypeOf ctl Is ComboBox) Then
          Err.Number = ERR_INVALID_CONTROL
          Err.Description = "An invalid control " & ctl.Name & _
              " was passed to sub 'FillList' - "
          Err.Raise Err.Number
      End If
      For nIndex = LBound(anArray) To UBound(anArray)
      .
      .
      .

This method will work in any situation, but it has two problems. The first problem is not really a problem in this instance because the caller won't be expecting an error. If the caller were anticipating an error, however, we might want to check the error number and perform a specific action. Visual Basic 4 allowed type libraries in which you could declare constants and declarations to include in a project. The main problem with these was that you couldn't create a type library within Visual Basic. It also meant that any client project would need to include the type library, thus increasing dependencies.

Enumerated constants is a feature introduced in Visual Basic 5. Let's see how the code looks before we explain what's happening:

' General declarations
  Public Enum CustomErrors
      ERR_INVALID_CONTROL = 3000
      ERR_ANOTHER_ERROR
      .
      .
      .
  End Enum
  Public Sub FillList(ByVal ctl As Control, anArray() As Integer)
      Dim nIndex  As Integer
      If Not(TypeOf ctl Is ListBox) And _
          Not(TypeOf ctl Is ComboBox) Then
          Err.Number = CustomErrors.ERR_INVALID_CONTROL
          Err.Description = "An invalid control " & ctl.Name & _
              " was passed to sub 'FillList' - " &
          .
          .
          .

The constants are declared between the EnumEnd Enum, just as in a user-defined type. The Enum name can be used to explicitly scope to the correct constant if you have duplicates. Notice that the second constant in the example doesn't have a value assigned. With enumerated constants, if you specify a value, it will be used. If you don't specify a value, one is assigned, starting from 0 or the previous constant plus 1. Enumerated constants can contain only long integers. The big advantage in using enumerated constants is that they can be public. For example, if you create a class, any client of that class can access the constants. Now you don't have to have constants with global scope, and you don't need to create type libraries. In effect, the module becomes more encapsulated.

The second potential problem with the function is that the array might be empty-but not the kind of empty that you can check with the IsEmpty function. If our sample code were to be passed an array that didn't contain any elements (for example, it might have been cleared using Erase), you would get a "Subscript out of range" error as soon as you used LBound on it. A much better way of passing arrays is to use a Variant array. A Variant array is simply a variable declared as type Variant that you ReDim. If the array has no elements, IsEmpty will return True. You can also check that an array as opposed to, say, a string has been passed. The code looks something like this:

Public Sub FillList(ctl As Control, vArray As Variant)
      Dim nIndex  As Integer
      ' Exit if array is empty.
      If IsEmpty(vArray) Then Exit Sub
      ' Exit if not an Integer array.
      If VarType(vArray) <> vbArray Or _
          VarType(vArray) <> vbInteger Then
          ' Error

The techniques described all help you to achieve the following benefits:

  • Create reusable and generic code by creating loosely coupled routines and components
  • Help others to reuse your code
  • Protect your code from errors caused by client code

Group Functionality

You might be surprised at how many applications contain functions that fall into a particular category but are fragmented across the application. Such fragmentation often occurs when many programmers are working on the same application and the module names don't clearly identify the type of functionality the modules contain. The ownership aspect also comes into play: some programmers don't like to amend another programmer's code. Access conflict might also be an issue. It is good practice to familiarize yourself with other modules in the application so that you can group functionality and also identify functionality that you can use. Grouping reusable functionality has the added benefit that it can be extracted in separate DLLs at a later stage.

Document Your Code

Visual Basic now has an excellent Object Browser. You can include detailed documentation about your functions that will prevent others from constantly having to ask questions about routines you have written. Unfortunately, for many programmers, writing documentation is like going to the dentist. It is also a task that never gets included in project plans, so it almost never gets done. It is vital that you increase your estimates to allow time for these tasks.

Refine Your Habits

We've made only a few suggestions here-there are lots more. If you decide on a set of good habits and constantly practice and refine them, your organization will benefit because of the development time you'll save, and you will benefit because you'll need to do much less debugging. Each time you complete a project, review it and identify areas that you could have improved. Reusability is an issue that has to be supported by the business, designed by the designers, and coded by the programmers. Unless all sides commit to the goal of reusability, you will be doomed to continually rewrite most of your code.