August 1999

Improve ActiveX Control Performance

Master binding techniques to build leaner, faster ActiveX controls.

by Francesco Balena

Reprinted with permission from Visual Basic Programmer's Journal, August 1999, Volume 9, Issue 8, Copyright 1999, Fawcette Technical Publications, Palo Alto, CA, USA. To subscribe, call 1-800-848-5523, 650-833-7100, visit www.vbpj.com, or visit The Development Exchange.

The ability to create ActiveX controls proves enormously useful. For example, you can leverage this ability to create specialized versions of the controls you use most often, including the textbox and listbox controls. Visual Basic makes creating an ActiveX control a simple and visual process, and features wizards that write most of the code on your behalf. However, it's easy to end up with controls that exhibit less-than-optimal performance.

What you need:
Visual Basic 5.0 or 6.0
Contrary to what many developers believe, this poor performance often has nothing to do with the efficiency of VB's compiled code. Rather, it often depends on how VB accesses the properties and methods of the ActiveX control, regardless of the language used to author it. In this article, I'll demonstrate several techniques for creating ActiveX controls that can outperform both built-in controls and many third-party controls. I'll also explain how to employ existing binding techniques to your best advantage, as well as how to force VB to use vtable binding rather than the less efficient DispID binding VB uses automatically when working with external controls. As an example, I'll demonstrate how to implement this technique in a SuperListBox control, which works as a virtual listbox control that contains only the elements visible at a given moment. I'll discuss the mechanism that underpins the binding aspect of the control in detail, and you can find the full source for the control here.

The first step to writing more efficient ActiveX controls is to understand what happens when you invoke a property or method of an ActiveX control placed on a form's surface. Start by adding a reference to the Microsoft Windowless Control Library (MSWLESS.ocx) that ships with Visual Basic 6.0. You can find the library at this location on the VB CD: ...\COMMON\TOOLS\VB\WINLESS\. Note that VB doesn't install this control on your hard drive as part of its basic installation; you must install it manually. This library contains lightweight versions of the TextBox, ListBox, ComboBox, CheckBox, OptionButton, CommandButton, Frame, and ScrollBar controls. You can use this library to compare windowless control performance to the corresponding performance of built-in controls. For example, place a regular TextBox control and a WLText control on the same form, then run this simple benchmark:

Dim t As Single, i As Long, s As String
t = Timer
For i = 1 To 100000
   s = Text1.Text
Next
Print "TextBox: " & Timer - t
    
t = Timer
For i = 1 To 100000
   s = WLText1.Text
Next
Print "WLText: " & Timer - t

This benchmark confirms the result you would expect. The first loop takes 4.3 seconds on my system, a Pentium II 333 MHz. The second takes only 1.6 seconds, about 2.5 times faster. This comes as no surprise: The regular TextBox control merely wraps the Windows' Edit control; retrieving the control's Text property requires an internal API call. WLText is a windowless control, takes fewer resources, and never has to call Windows API functions.

However, working with string properties is often misleading, because strings are among VB's least efficient data types. So, try another test that uses a numeric quantity. Repeating this benchmark with the ForeColor property yields interesting results. On my machine, the standard textbox control takes 0.11 seconds to complete this task—about 15 times faster than the WLText control's 1.76 seconds. This contradicts the previous benchmark and seems illogical: Why should reading a number stored inside an ActiveX control be slower than performing the same action on a built-in control?

 
Figure 1 How VB Decides Binding Click here

More to Binding Than Early, Late
Solving this mystery requires learning some little-known details of the binding process. Binding is the translation of the name of a method or property into the address of the compiled routine that performs that method—or, more precisely, into the offset in the vtable that contains the address of the compiled routine (see Figure 1). Most experienced VB programmers know you should use early binding when referring to an object's methods or properties. You do this using a specific object variable:

Dim w As Word.Application
Set w = New Word.Application
w.Visible = True      ' early binding

Early binding ensures the translation is performed at compile time, enabling the program to execute at the highest possible speed. The compiler stores the vtable offset right in the EXE file; invoking the compiled code requires only a few Assembly statements. This means early-bound calls to ActiveX DLLs are virtually as fast as calls to routines included in the application's executable file. This form of binding is also known as vtable binding.

VB resorts to late binding when the compiler doesn't have enough information to translate a method's or property's name into the corresponding offset in the vtable. This means the translation process occurs as the program executes. Late binding occurs when you use a generic object variable:

Dim x As Object
Set x = New Word.Application
x.Visible = True      ' late binding

VBScript and VB3 use late binding because they don't offer specific object variables. Late binding is approximately 500 times slower than early binding. Worse, the compiler can't even ensure a method or property with the specified name exists; you should add extensive error trapping code to avoid error 438: Object doesn't support this property or method.

Some developers mistakenly associate early binding with the New keyword and late binding with the CreateObject method. However, binding has nothing to do with the method used to create the object; only the type of the variable used to reference an object affects how it binds. For example, you can assign a generic variable to a specific variable, then access the same object using either early or late binding:

' Continuing the previous example ...
Dim z As Word.Application
Set z = x            
' x uses late binding
z.Visible = True      
' z uses early binding

You can tell whether an object is accessed through early binding by typing a dot after the variable's name: If a list of available methods and properties appears, then the object is accessed through early binding. If IntelliSense is enabled but no list appears, the object is accessed through late binding.

Analyzing what VB does when it executes a method of a late-bound object yields interesting results. First, the VB runtime calls the GetIDsOfNames method of IDispatch, an interface supported by any object stored in a variable declared with As Object. These calls enable VB to retrieve the DispID, a number associated with the particular method you want to execute. This number is passed as an argument to the IDispatch's Invoke method, which carries out the property or method call.

Say Hello to DispID
vtable binding and late binding don't tell the whole story, however. Sometimes VB uses a third binding method: DispID binding. In this case, VB retrieves the DispID of a method or property during the compilation step and stores it in the EXE file. VB uses the DispID value as an argument to the IDispatch's Invoke method at run time—exactly what happens in late binding—but DispID binding doesn't require a call to the GetIDsOfNames method while the program executes. DispID binding is noticeably slower than vtable binding, but an order of magnitude faster than late binding. Plus, IntelliSense works with DispID-bound objects, and syntax errors are trapped at compile time, so you don't need any additional error-trapping code. For this reason, many developers refer to DispID binding as a form of early binding, although I've seen some place it in the late binding category.

Using DispID binding is not as simple as choosing how you refer to an object: VB lets you decide between early binding and late binding, but not between DispID and vtable binding. All ActiveX DLL and EXE components are accessed through vtable binding automatically—provided you use a specific object variable. It turns out that VB uses DispID binding in at least two cases: when accessing the properties or the methods of an ActiveX control, and when receiving events from any ActiveX object, whether a control or a component. In other words, events are inherently slower than direct calls because they use less efficient DispID binding. Fortunately, you can eliminate this overhead; the SuperListBox control illustrates how.

This control works as a virtual listbox control that contains only the elements visible at a given moment. The SuperListBox control comprises two constituent controls: a standard listbox and a vertical scrollbar. When you load the control with more elements than can be visible at a given moment, the scrollbar becomes active and the user can "scroll" the listbox to see the elements not currently visible. The control empties the listbox and fills it with a new set of elements whenever the user acts on the scrollbar.

The SuperListBox control exposes an AddItem method that works similarly to the standard method, with one exception: It loads the values in a private array of strings. The control loads each string into the listbox control only if—and when—the user asks to display it. This architecture makes a big performance difference: The SuperListBox control is nearly 10 times faster than a standard listbox control at loading elements. For example, the SuperListBox loads 30,000 elements in 0.7 seconds on my system, while a standard listbox takes 6.7 seconds.

 
Figure 2 Access Secondary Interfaces Click here

Use vtable Binding Instead
You might believe that a tenfold speed improvement is enough for most purposes. As I noted previously, all calls to the members of an ActiveX control use DispID binding rather than the more efficient vtable binding. This inefficiency is caused by the Extender object VB wraps around an ActiveX control to add properties, methods, and events such as TabStop, Move, GotFocus, and Validate (see Figure 2). This intermediate Extender object prevents the compiler from deriving the actual offsets in the vtable. Instead, the compiler can store in the EXE file the DispIDs of the properties and methods referenced in the client. This means that calling a routine inside a UserControl module is slower than calling the same routine located in an ActiveX DLL because the former call uses DispID binding rather than vtable binding.

Now, the good news: It is possible to call code in an OCX project using the faster vtable binding. In fact, you can use either of two undocumented techniques to do so.

The first method derives from the fact that only the members in the main UserControl module are accessed through DispID binding. Conversely, VB invokes all methods and properties exposed by a regular class module in the ActiveX control's project through more efficient vtable binding. Assume you want to build a treeview control clone with dependent Node objects. The properties of the main TreeView object are DispID-bound in such a control, while all the properties of the Node object are accessed by means of vtable binding.

 
Figure 3 Achieve vtable Binding With an Auxiliary Class Click here

Now assume you want to provide clients of your ActiveX control with the ability to execute fast method calls. Do this by creating a PublicNotCreatable class module in the same UserControl project and making the members of this class delegate to the members with the same name in the main UserControl module (see Figure 3). For example, look at how you provide vtable binding access to the AddItem method of the SuperListBox control. Start by defining the auxiliary class module:

' The SuperListBoxDirect class module
Dim slb As SuperListBox

' This method can only be invoked from
' within the ActiveX control project
Friend Sub Init(uc As SuperListBox)
   Set slb = uc
End Sub

Public Sub AddItem(Item As String, _
   Optional Index As Variant)
      slb.AddItem Item, Index
End Sub

Next, add a read-only property or a function to the main UserControl module that enables the client application to retrieve the companion SuperListBoxDirect object related to a particular instance of the SuperListBox control:

' in the SuperListBox.ctl module
Property Get ObjectDirect() As _
   SuperListBoxDirect
      Static obj As SuperListBoxDirect
      ' Create an instance if necessary
      If obj Is Nothing Then
         Set slbDirect = New _
            SuperListBoxDirect
         obj.Init Me
      End If
      Set ObjectDirect = obj
End Property

The client application can execute the fast version of the AddItem method by retrieving a reference to this companion SuperListBoxDirect object, then using this object's methods rather than the methods of the main ActiveX control:

' In the client application
Dim slbDir As SuperListBoxDirect
Set slbDir = _
   SuperListBox1.ObjectDirect
For i = 1 To 30000
   slbDir.AddItem "Item " & i
Next

 
Figure 4 Create a Faster Listbox Control. Click here

Note that the ObjectDirect method uses DispID binding, but it's invoked only once, so it doesn't add any noticeable overhead. You might be surprised to learn that—despite the extra call from the class to the UserControl module—the optimized AddItem method is about 3.5 times faster than the "native" AddItem method exposed by the main UserControl module (see Figure 4).

Expose a Secondary Interface
The second method for giving client applications the ability to access the members of your ActiveX control through vtable binding rather than DispID binding proves easier than the first. Your ActiveX control must expose a secondary interface that includes all the members for which you want to expose an optimized version. Calls to secondary interfaces are always vtable-bound, so this gives you another easy way to avoid the inefficient DispID binding.

Adopting this technique requires defining a secondary interface that contains all the properties and methods that you want to make vtable-bound. The sample program contains only one, AddItem:

' The ISuperListBoxFast class module
Public Sub AddItem(Item As String, _
   Optional ByVal Index As Variant)

   ' no code here
End Sub

The main UserControl module must implement this interface. This step requires only a minimal amount of code because each interface member has only to delegate to the method with the same name in the primary interface:

' in the SuperListBox.ctl module
Implements ISuperListBoxFast

Private Sub ISuperListBoxFast_AddItem _
   (Item As String, _
   Optional Index As Variant)
      ' call the "real" method in the
      ' primary interface
      AddItem Item, Index
End Sub

A client application that wants to invoke the optimized version of the AddItem method needs a reference to the SuperListBox control's ISuperListBoxFast interface. Unfortunately, this code raises a Type Mismatch error:

' in the client application
Dim slb As ISuperListBoxFast
Set slb = SuperListBox1  
' doesn't work!

This error seems nonsensical—until you remember that the SuperListBox object seen by the client application isn't the real ActiveX control, but the Extender object created by VB. Such an Extender object inherits all the properties, methods, and events of the original ActiveX control, but it doesn't inherit any secondary interfaces. Assigning the control's reference to a variable of type ISuperListBoxFast successfully requires accessing the real ActiveX control. You do this using the Object property, which is itself an Extender property (see Figure 2). This code does the trick:

' in the client application
Dim slbFast As ISuperListBoxFast
Set slbFast = SuperListBox1.Object
For i = 1 To 30000
   slbFast.AddItem "Item " & i
Next

This loop executes in exactly the same amount of time as the code based on the auxiliary SuperListBoxDirect class; this proves indirectly that members of secondary interfaces are accessed through the more efficient vtable binding. Using an auxiliary class or a secondary interface is mostly a matter of programming style because the amount of code you have to write for either is about the same.

I've noted that all events use DispID binding implicitly. This means they are less efficient than direct calls to methods. You can't do much to avoid this overhead when you use a third-party ActiveX control, but you do have a choice when you build your own VB controls. Your ActiveX control should send notifications to the container form through a secondary interface, rather than raise events. You define such an interface in the ActiveX control project and implement it in the parent form module.

I wanted to see this concept in action, so I modified the SuperListBox ActiveX control to support on-demand loading of elements. Rather than preload all the elements with a series of AddItem methods, the client code tells the control how many elements it contains and waits for the control to request the elements to be shown in the listbox at any given moment. This approach dramatically reduces the time needed to display the listbox initially; it also reduces memory overhead because you don't need to store the data both in the client program and the ActiveX control. This technique proves effective when displaying data stored in a database table.

Activate On-Demand Loading
You activate on-demand loading with the SuperListBox control by assigning a value to the ListCount property, then invoking the control's Refresh method:

' in the client application
Private Sub cmdFill_Click()
   SuperListBox1.ListCount = 10000
   SuperListBox1.Refresh
End Sub

' This event fires when the control
' has to display an item in the list
Private Sub SuperListBox1_GetItem( _
   ByVal Index A s Long, Item As _   
   String)
      ' List the contents of an array
      Item = arrItems(Index)
End Sub

The Refresh method fires a number of GetItem events equal to the number of visible elements in the listbox. Such events occur so quickly that the listbox appears instantaneously.

Substituting this standard event mechanism with an interface through which the control communicates with its client application proves straightforward. Start by defining the interface; do this by adding a PublicNotCreatable class module to the SuperListBox project:

' the ISuperListBoxEvent class module
Function GetItem(ByVal Index As Long, _
   ByVal ID As Long) As String
   ' this method must return the 
   ' indexed element in the listbox
End Function 

The GetItem method is a function, so you don't need to pass an argument by reference to make the app return a value to the SuperListBox control, as you must with the GetItem event. On the other hand, you do need a second argument (ID) in case you have multiple SuperListBox controls on the same form. Take advantage of this alternate notification mechanism by implementing the ISuperListBoxEvent interface in the parent form:

' in the parent form
Implements ISuperListBoxEvent

Function ISuperListBoxEvent_GetItem
   (ByVal Index As Long, _
   ByVal ID As Long) As String
      ' List the contents of an array
      ISuperListBoxEvent_Item = _
         arrItems(Index)
End Function 

Finally, the application must inform the SuperListBox control that notifications should be sent through the ISuperListBoxEvent interface rather than the standard event mechanism. Do this using the SuperListBox control's SetOwner method:

' If you have only one control on the
' form, you can pass anything to ID
SuperListBox1.SetOwner Me, 0

Or you can test the Parent property inside the ActiveX control module and activate notifications through the ISuperListBoxEvent interface if the parent form implements that interface:

Private Sub UserControl_Resize()
   If TypeOf Parent Is _
      ISuperListBoxEvent Then
         ' send notifications through
         ' the secondary interface
   End If
   ...
End Sub

Using an explicit SetOwner method requires an additional line of code in the client application, but adds flexibility (see Listing 1). The routine can use a private array, a mechanism based on events, or a notification system based on callback functions. For example, you might have the SuperListBox control filled by any other class in the application, not just the parent form.

The event mechanism is so efficient that there is no point in implementing an alternate notification method based on a secondary interface. This holds true for most of the notifications related to user interface events, because DispID binding typically proves fast enough to keep up with end users' actions.

However, you sometimes encounter cases where a secondary interface for notifications can speed up your code noticeably. For example, the SuperListBox control exposes a Search method that lets the client application search all the elements in the list for a matching value. A single call to the Search method can require the SuperListBox control to fire thousands of events in the client application until a match is found or all the elements have been compared. The control can scan 30,000 elements in about 0.7 seconds when it uses the standard event mechanism. Switching to the ISuperListBoxEvent interface enables the control to reach the same result in 0.5 seconds, about 30 percent faster. You can experience an even greater speed improvement in many cases.

Examining the sample control's complete source code can help you master more of the details. You can use the SuperListBox control in your applications, altering it to match your specific needs. For example, you might add support for multiselection, sorting, and columns. Or you might add the ability to load elements from a DAO, RDO, or ADO Recordset, which would probably deliver better performance than the DBList and DataList controls, thanks to the on-demand loading mechanism. In any case, you can use the techniques discussed in this article to give a performance boost to the ActiveX controls you create in VB.


Francesco Balena is publisher and editor-in-chief of Visual Basic Journal, VBPJ's Italian licensee. He is the coauthor of Platinum Edition Using Visual Basic 5 (Que), author of Programming Microsoft Visual Basic 6 (Microsoft Press) and a frequent speaker at VBITS conferences. Contact Francesco at fbalena@infomedia.it or visit his Web site at http://www.vb2themax.com/.