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.
|
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 |
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 |
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 |
' 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 |
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/.