Building COM Components That Take Full Advantage of Visual Basic and Scripting

Ivo Salmre
Microsoft Corporation

February 24, 1998

Contents

Abstract
Introduction
A Sample Type Library For Reference
Defining Usable Type Libraries
Defining Creatable Classes
Defining Classes That Are Not Creatable
Defining Interfaces
Using Events
Defining Properties and Methods
Using Collection Objects Where Appropriate
Including Help in Your Application
Naming Conventions
Returning Errors
Quick Examples
Trying Out Your COM Components from Visual Basic
When in Doubt

Click to copy the sample files associated with this technical article.

Abstract

This document describes how to design COM object libraries that take advantage of the Microsoft® Visual Basic® development system and scripting languages.

Introduction

It is a simple matter to build components that take full advantage of both Visual Basic and scripting languages, if certain guidelines are followed. These components will provide the maximum design and run-time facilities for developers and accentuate the value of the investment made in developing these components. Following these conventions will enable using the maximum potential of Visual Basic, Microsoft Office, other Visual Basic for Applications (VBA) licensees, and scripting languages.

Note   While these guidelines focus specifically on Visual Basic and scripting languages, the advice contained applies equally well to other development languages (Microsoft Visual C++®, Visual Basic for Applications, Java, and others).

A Sample Type Library for Reference

The following library uses the rules of thumb outlined in this document to produce a Type Library that takes advantage of scripting languages and rapid application development (RAD) tools like Visual Basic. It depicts two fictitious objects:

Type Library Source: ThreadServer IDL

Type Library Compiled: ThreadServer TLB

Note   The IDL file is liberally sprinkled with comments to explain what is being done and why.

Click to open or copy the files in the ThreadServer sample application for this technical article.

Defining Usable Type Libraries

A type library is a binary file that describes your applications' object model. Type libraries are typically embedded as a resource inside an ActiveX® EXE or DLL. Type libraries have a type library name (for example, "Excel").

Make Sure to Import stdole2.tlb

Example

library RDO
{
  // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
  importlib("STDOLE2.TLB");

StdOle2.TLB contains definitions of all of the base interfaces you will need to construct Visual Basic– and script-friendly components. It should always be imported into your type libraries.

Defining Creatable Classes

What is a Creatable Object?

A "creatable" object is an object that can be instantiated by itself. Creatable classes have public Class Factories that generate instances of these objects on demand.

An example would be the object "Excel.Application."

Two examples

  1. To look up the CLSID in the registry (at run time) and create the object:
       Set objXL = CreateObject("Excel.Application")
    
  2. Use the <TypelibName>.<CoClassName> to get the CLSID at compile time:
       Set objXL = New Excel.Application
    

This differs from objects that are "not creatable" and must obtained via method calls to other objects. An example of a "not creatable" object would be a Cell object, which must be obtained from an Excel Worksheet object.

Note   In the enclosed sample, the ThreadServer is the creatable class. Thread is not a creatable class—instances of Thread objects must be obtained via a ThreadServer object.

Declaring a Creatable Class

Each creatable class needs to have a CoClass in the object library's IDL file.

At a minimum, a CoClass will contain a "default" interface. This is the main interface to the properties and methods exposed by the object. This interface is generally thought of as the object.

An object can also have nondefault interfaces. These interfaces can be accessed by Visual Basic but not scripting languages and thus should be avoided unless there is a good reason for using one. You should strive to have only one programmatic interface on your classes; multiple interfaces are confusing to class consumers.

Note   Multiple programmatic interfaces on an object are used to implement polymorphism. For example, a hotel reservation object could have two interfaces. One that the front end user interface (UI) uses to set reservation attributes (such as name, date, and credit card number). A second interface could be used by the back end to process the reservation (for example, AcceptReservation(), or ValidateCreditCard()). Polymorphism has its place, but it is an advanced behavior that generally should be avoided, because it is much harder to understand from an end-user perspective.

Event sources

A CoClass can also declare sources (also default and not default). A source interface describes the events that the class can generate. Users can write code to consume (also known as "sink") events generated these classes. See the following events description for more information.

Registering a Creatable Class

In order to make a CoClass creatable it must be registered. This involves two steps:

  1. Registering the CoClass' CLSID (class id) in the registry under HKEY_CLASSES_ROOT\CLSID\<Your Clsid>\. A number of subkeys need to be set here (for example, InProcServer32, Threading Model, and so on). See the Automation documentation for more details.

  2. Registering the ProgID of the CoClass. In order for CreateObject("Excel.Application") to work there needs to be under HKEY_CLASSES_ROOT a key named Excel.Application that contains the CLSID of the object. Similarly, your object needs to have a ProgID. By convention this ProgID is named <TypeLib Name>.<CoClass Name> (for example, Excel.Application).

How and when is this registration performed?

In a dynamic-link library (DLL) or OCX these registry keys should be written in the library entry point when DLLRegisterServer (which you must implement) is called. DLLRegisterServer is called when regsvr32.exe <your component> is called. (This should be done by setup.)

In an EXE, your component should perform this registration when <EXENAME>/REGSERVER is called. By convention, your .exe should silently exit after this registration is performed (so setup programs can call this).

Note   For more information on registering Automation (ActiveX) Servers, please refer to the Automation documentation.

Defining Classes That Are Not Creatable

Some objects must be obtained through other objects (for example, the Thread object in the sample IDL file). The methods that return instances of these classes should be strongly typed methods (that is, they should return the exact type of the object instead of returning Variant or Idispatch*—see below).

If the class is an event source but is not creatable, it should still be declared in a CoClass. In this case, do not register the CLSIDs for these classes in the registry (that would publish them as creatable classes). These CoClasses should be marked as noncreatable.

Example

[
   uuid(39AE4333-AD6C-11D1-85E3-0000F875B12F),
   version(1.0),
   noncreatable,
    custom({50867B00-BB69-11D0-A8FF-00A0C9110059}, "-1")
  ]
  coclass Class1 {
    [default] interface _Class1;
    [default, source] dispinterface __Class1;
  };

Note   If the object that you are describing is noncreatable and has no events, you do not have to declare a CoClass for it. You have two options in this case:

Defining Interfaces

An interface is a collection of methods and (Automation) properties. This is what a language binds to and what the user generally thinks of as "the object" he/she is working with. Objects generally (but not always) have one "programmatic" interface that the user programs against, but may support many other interfaces for persistence, marshalling, and so on.

Interfaces Should Be Marked Dual

Dual interfaces are interfaces that support both IDispatch (also known as "late") as well as v-table binding. This means that when they are used inside strongly typed languages the calls are faster (because they are straight v-table calls). Dual interfaces should be used in all cases unless there is good reason not to. Dual interfaces are just as callable from scripting engines as standard IDispatch interfaces are so there is no need to avoid using them because of scripting. They are also very easy to implement using C++ classes (or Visual Basic itself) as should be the standard mechanism for all automatable interfaces that are exposed by all object models.

Interfaces Must Be Marked as "oleautomation"

If for some reason you choose not to mark your interface as dual (which implies "oleautomation"), you must mark it as "oleautomation." Doing this insures that the interface calling conventions and datatypes conform to those that are available in Visual Basic and scripting languages. This is essential.

When to Mark an Interface as "hidden"

If an interface is being used as the default interface of a CoClass it should be marked "hidden." The interface's properties and methods will appear under the CoClass' name in the development environment's object browser.

If the interface is not the default interface in a CoClass then it should not be marked as hidden. The interface's properties and methods will appear under its name in the object browser.

Q: Why not declare all interfaces public?

A: Doing so creates clutter and confusion for your clients. Users will see two definitions for an interface that is also exposed as a default interface in a CoClass.

Interfaces Should Generally Be Marked as "nonextensible"

Nonextensible means that, "The only properties/methods in this interface are the ones listed here." This allows a compiler that supports early binding (for example, Visual Basic/VBA) to generate compile-time errors when an invalid method call (such as a misspelled method name) is discovered. An interface not marked nonextensible indicates that, "You may early bind to the methods listed in this interface, but I will expose additional late-binding methods at run time that may also be called." In general use, this is rarely the case.

Note   Other interfaces can still inherit from nonextensible interfaces.

Using Events

Your classes can expose events that consumers of your objects can sink. This allows you to publish descriptions of the events that your objects can generate and allows programmers to bind to these interfaces. These events are declared in interfaces in your IDL file.

Event Interfaces Must Be Declared as "dispinterface"

Visual Basic does not permit v-table binding to an event interface. If you want your events to be sinkable by Visual Basic script, you must declare them in a "dispinterface." (See sample IDL file.)

Figure 1 shows an example of a Visual Basic class sinking an event declared in the sample type library.

Figure 1. A Visual Basic class sinking an event.

Note   The syntax for dispinterfaces is somewhat different than standard interfaces (see sample IDL file for example). Details on IDL can be found in the Platform SDK in the MSDN™ Library, under IDL, DispInterface, and so on.

Defining Properties and Methods

Use Strongly Typed Variables

Instead of returning a variant or IDispatch interface, return strongly typed objects or enumerations where possible. This allows user errors to be discovered at compile time as opposed to run time. It also gives the programmer much more information.

The use of variants should be avoided. Statement completion is driven off parameter types and the tool tip supplied by the development tool (see screen shots later in this document) is useless unless parameter types are known. (Knowing that a parameter takes an integer provides significant self-documentation over it taking a variant.).

Note   Strongly typing your variables allows hyperlinks to these strong types in the Object Browser (variants, which are by definition typeless, cannot offer this facility).

Optional parameters

Type libraries allow the use of strongly typed "optional parameters" with default values. Strongly typed optional parameters should be used whenever possible instead of optional variant parameters.

Q: If I use strongly typed variables will this cause any problems in scripting languages that do not support strongly typed variables?

A: No. The scripting languages were specifically designed to be able to coerce their untyped variables into the appropriate types. Automation's IDispatch implementation takes care of all of this. In fact, using strong types makes things easier for you and your clients. You do not have to worry about extracting the correct values from variants. Your clients will be able to see what kind of data types you are expecting.

Q: When should variants be used?

A: Variants have their uses; specifically, in cases where a data type is unknown at compile time. (For example, ADO Recordset.Fields(<SomeField>) = ????. Due to a RecordSet's generic nature, it is not known what data type is returned.) Variants have their use, but should only be used when necessary.

Use Enumerations

Instead of using a long as a parameter to a function, use an enumeration. The enumeration enables users to see exactly what their options are.

Note   If you have a function that takes a set of predefined string values, you can declare these string values as constants (const) inside the type library. An example in Visual Basic version 5.0 would be the vbCrLF string constant, which declares a string consisting of a carriage return and a line feed.

The development environment will use enumerations to drive its autostatement completion. Functions that use enumerations greatly aid the user in understanding what the functions do.

Q: Are enumeration symbols available in scripting languages?

A: No, not currently, but it is still a good idea to use them. An explicit string can always be passed into a parameter that takes a string enumeration. An explicit long can always be passed into a parameter that takes a long enumeration. Scriptwriters will still gain the browsing information that enumeration parameters provide.

Note   If you so desire you can provide an object that has methods with these enumeration values (that is, make the enumeration items properties on the object). Doing this gives the scriptwriter the ability to use these values instead of hard-coding explicit values. (However, use of this object will be less efficient than explicit values since it involves a property access.)

Choosing between Properties and Methods

Properties are usually values that can be retrieved and modified. Properties can also be used to get/set indexed properties. For example, strText = ListItems(12), ListItems(12) = strText.

Methods are used to perform actions.

When to use properties

Use properties when you want to expose something to a script or a Visual Basic programmer as a get/set pair. Example:

   Form1.BackColor = RGB(12, 14, 72)    'Sets a scalar value
   lngColor = Form1.BackColor         'Gets a scalar value
   set MyControl.DataProvider = objSomeDataProvider   'Sets an object reference
   set objSomeDataProvider = MyControl.DataProvider   'Gets an object reference

Note   A word on Set. Properties that allow you to set object references (as opposed to scalar values) use the IDL property attribute "putref." Since Automation objects can have default values (for example, strTextBaxValue = TextBox1 gets the default property associated with the object TextBox1, in this case, the "text" property of the text box). Set exists to remove an ambiguity. Set means set the object reference, not set the default property of the object (for example, set objTextBox = TextBox1 sets objTextBox to the object TextBox1),

Getting the value of a property should not affect the state of your object. They should be order independent—calling one property should not affect the values of any other properties.

Note   Object property values will be displayed in the "locals" window when debugging.

When to use methods

Use methods when you want to expose a function that perform actions and may return values. Examples:

   'Create a chart object.
Set objSomeGraph = objChartMaker.MakeGraph(xAxis:=12, yAxist:=14, arrPoints())
  
   'Validate a credit card.
   boolIsValid = objCreditCard.Validate(strCardNumber, ctCardType)
   'Process an order.
      objOrder.Process

Default Property on an Object

Automation supports the concept of a "default property." A default property is the property that is accessed when the object is referred to without any explicit property or method call. For example:

strText = TextBox1   'Gets the default property of the textbox (it's text contents)

A default property is declared with a property ID of 0. For example:

[id(00000000), propget]
    HRESULT Foo([out, retval] LONG* );

Valid Kinds of Parameters for Properties and Methods

All function parameters must be [in] or [in, out]

Visual Basic and scripting support "by value" parameters. These are signified as "[in]" in the IDL function declarations.

Visual Basic and scripting support "by reference" parameters. These are signified as "[in, out]" in the IDL function declarations.

Visual Basic and scripting do not support passing [out] parameters to/from functions.

All function return values must be [out, retval]

A return value differs from a function parameter in that it must be marked [out, retval] in the IDL declaration.

Note   See the OLE/COM documentation for details on [in], [out] and [in, out] parameters.

Valid Datatypes of Parameters for Properties and Methods

The following datatypes are supported by Visual Basic and script:

Integer (16 Bit, Signed, VT_I2)
Long (32 Bit, Signed, VT_I4)
Date (VT_DATE)
Currency (VT_CY)
Object (* VT_DISPATCH)
Strings (VT_BSTR)
Boolean (VT_BOOL)
Currency (VT_DECIMAL)
Single (VT_R4)
Double (VT_R8)
Decimal (VT_DECIMAL)
Byte (VT_UI1)
Variants  

Also:

Using Collection Objects Where Appropriate

Collections are objects that are useful in representing sets of objects or variants over which users can iterate. They are useful in expressing the concept of "sets of things that should be grouped together." Typical examples if collections include Microsoft Excel Workbooks collections (Application.Workbooks() is a collection object that contains the set of all the workbooks currently loaded into Excel) and Visual Basic user-defined collections (for example, a collection of customer objects).

Count Property

Collection objects have a Count property that allows users to determine the number of items in a collection. You should provide your collection object with this property.

Item(Index) Property

Collection objects have an Item(Index) property. The Index parameter is typically a variant that supports identifying a collection member either by numerical index (such as collection.Item(4)) or by named key (such as collection.Item("Workbook1")).

The Item property should be the collection object's default property (that is, collection("Workbook1") is the same as collection.Item("Workbook1")).

You should provide your collection with this property.

Add and Remove Methods

Collections will also typically have Add and Remove methods, allowing the object's consumers to add and remove collection items. These methods should exist if the user should explicitly be able to add or remove items to collections.

Note   See Visual Basic's Collection object for an example of a full-featured collection.

Enabling Collection Iteration

As noted, collections support iterating over the items in the set. This is done via Visual Basic's "For Each" language construct. Example:

"This loop will iterate through all the workbooks in the Workbooks collection and display each of their titles
For each objWorkbook in Workbooks
  MsgBox objWorkbook.Name
Next

In order to support this iteration behavior, a collection object needs to have a hidden method that is defined in IDL as follows:

[id(0xfffffffc)] HRESULT _NewEnum([out, retval] IUnknown** ppunk);

The ID must be: '0xfffffffc'

The method name must be '_NewEnum'

Note   An underscore (_) preceding any method name indicates that the method is hidden. It will not be displayed to users in the Object Browser or in Auto Statement Completion.

When this method is called, your object should return a new object enumerating the elements in the collection. The interface of this new object must be IEnumVARIANT.

Note   See the Platform SDK documentation in the MSDN Library for IEnumVARIANT for details on how to implement this object. A "Threads" collection object is defined in the sample type library for this article.

Including Help in Your application

Help strings are trivial to add. Integrating your help files with your object models type library is also a simple task. You should do both.

Specify "help strings" for Everything

Help strings on your type library, CoClasses, interfaces, enumerations, methods, and properties allow users to get help information while they are developing. These strings are displayed in the Visual Basic Object Browser and are very useful for users attempting to learn your object model.

This is an extremely inexpensive way to provide the user a great deal of quick help. Help strings should absolutely be provided for all nonhidden items in a type library.

Figure 2. Displaying the help string "Windows Thread."

Note   The help string "Windows Thread" is displayed when the user selects the method CreateThread. Events are also displayed here (note the lightning bolt icon).

The help string should describe the purpose of the object library, CoClass, interface, method, enumeration or property.

Example:

helpstring("This is a helpstring")

Note   These help strings can be localized, but this is a somewhat more involved process. See the Platform SDK documentation in the MSDN Library for "helpstringcontext" and ITypeLib2::GetDocumentation2 for more information on how to do this.

Specify a Help File for Your Object Library

If you have a help file associated with your object library, specify the help file name in the type library header. Example:

[
 uuid(EE008642-64A8-11CE-920F-08002B369A33),
 version(2.0),
 helpstring("Microsoft Remote Data Object 2.0"),
 helpfile("rdo98.chm"),
 helpcontext(0x0004bc28)
]
library RDO
{

Specify "helpcontext" for everything

If you have included a help file with your object library, you should specify help context IDs for all nonhidden interfaces, enumerations, methods, and properties. Having these present allows the development environment to navigate the user to the specific help pages that pertain to the item in question.

Naming Conventions

File Extensions

*.OCX. If your COM Server is a visible ActiveX control (for example, something you can embed in a Visual Basic form) it should have a .ocx extension.

*.DLL. If your COM Server is an in-process server that is not a visible ActiveX control (such as a set of objects that users can program, that do not provide a UI embedded in a window), it should have a .dll extension.

CoClasses

CoClasses should have plain English names. If a class describes a car, call it Car, not ICar.

ProgIDs

ProgID's are what are used by Visual Basic and scripting's CreateObject keyword.

Example:

Set objXL = CreateObject("Excel.Application")

By convention, the ProgID is named "<TypeLibName>.<CoClass Name>". Doing this insures that Visual Basic's New keyword (which uses the CoClass name) looks and acts the same as CreateObject().

Note   ProgIDs are not listed in the Type Library. They are registered by your component in the system registry under HKEY_CLASSES_ROOT/<PROGID>.

Interfaces

Interfaces that are not marked "hidden" should have "plain English" names. If an interface describes a car, call it Car, not ICar.

Underscores should not be used in public Interface names. Using underscores will cause trouble for people using Visual Basic's implements keyword or sinking events in that interface.

Note   Private interface names should not start with a leading underscore. This was done in very early object models in order to hide the interface name, but it is no longer needed, given the Hidden attribute. Doing so just makes the interface unusable in cases where developers may want access to the hidden interface.

Enumerations

An Enumeration should have a plain English name that describes its type, such as RecordSetType, or FileLockType.

Enumeration items are typically prefixed with a two- or three-letter prefix (lowercase). This serves two purposes:

Casing

Each word should start with an uppercase character, such as "CheckBox" or "StringConstants."

Prefixes to enumeration items should be lower case. For example:

lsFancy = 3 (List Style "Fancy" = 3)

lsPlain = 4 (List Style "Plain" = 4)

ttApartment = 2 (Thread Type Apartment = 2)

Public interfaces, functions, methods, properties, parameters, and CoClasses should start with an uppercase character.

Casing of constants

Constants should follow the same conventions, mixed case with the first letter in each word capitalized.

Note   This is in contrast to previous convention where constants were all uppercase. Moving to mixed case brings us in line with COM+ naming conventions.

Parameters

Parameters are displayed in the Object Browser with the names you give them. Since the objects are displayed along with their data types (for example, "Color as Long") it is the highly recommended convention not to decorate the parameter names with the datatypes (that is, do not use "lngColor as Long").

Exception   The parameter names of function return types (that is, parameters marked "retval") are not displayed in the Object Browser, so the names given these are not particularly important.

Returning Errors

Giving meaningful errors is essential to aiding users in debugging their code. In every error condition that occurs, a unique error number and descriptive error text should be returned.

Returning descriptive error information allows you to give the consumer of your component the following information:

If a host (script, Visual Basic for Applications, and so on) calls one of your methods that returns an error (a failing HRESULT), the host will query your object to see if you support rich error information. If your interface supports rich error information, the host will call the system API GetErrorInfo to get the detailed error information.

Your Objects Should Support the Interface ISupportErrorInfo

ISupportErrorInfo contains only one method, InterfaceSupportsErrorInfo(). When called, this method indicates if it supports rich error information.

When InterfaceSupportsErrorInfo() is called (passing in the IID of an interface), it should return "S_OK" if your object supports rich error information.

Note   Search in the Platform SDK documentation  in the MSDN Library for IErrorInfo, ISupportsErrorInfo, SetErrorInfo, or GetErrorInfo for more detailed information.

Returning Error Information

An error has occurred inside your method. You wish to return descriptive error information. Do the following:

  1. Call CreateErrorInfo to create an error object.

  2. Using the ICreateErrorInfo interface on the object, set the rich error information (description, source, help file, and so on).

  3. Query the object for its IErrorInfo interface.

  4. Call SetErrorInfo, passing in the pointer to IErrorInfo.

Note   Bubbling up errors from objects called by your objects is simple! If you wish to bubble up the error produced by an object called by your object, return an error HRESULT. The clients consuming your object will see that an error occurred. They will query your object to see if you support rich error information. You say "yes." They call GetErrorInfo to get the error information. This rich error information they get is the error information that was bubbled up.

Quick Examples

The Visual Basic Object Browser

The Visual Basic Object Browser is where Visual Basic users go to get information on objects they are programming.

Notice the following things:

Following the guidelines above for constructing your interfaces enables users to maximize this facility.

Figure 3. The Visual Basic Object Browser

Inline Function Information

Visual Basic uses TypeLib information to display inline function definitions to users. (Note the strongly typed return type, as opposed to the generic Object.)

Figure 4. Using TypeLib information to display inline function definitions.

Auto Statement Completion

Using enumerations instead of Long values allows the Visual Basic development environment to offer "auto statement" completion for the user, making development much quicker and bug free.

Figure 5. Automatic statement completion

Trying Out Your COM Components from Visual Basic

After building your Visual Basic– and script-friendly components it is easy to test them in Visual Basic.

  1. Register the component on your system (regsvr32 <YourDLL> or <YourEXE> /REGISTER).

  2. Start Visual Basic and create a new project.

  3. Add a reference from your project to the DLL or Exe containing the type library. You can do this by opening the References dialog box from the Project menu and selecting your DLL/EXE.

  4. Go to the object browser and look at your object model. The object browser can be selected from the View menu.

  5. Try using your objects in code. Double-clicking on a form window will bring you to the code window beneath the form. You can place code that uses your objects here in the Form_Load method. Pressing F5 will run the code.

  6. Test the events. Make sure you can sink them from Visual Basic and that code attached to them fires when the event is triggered.

  7. Let us know if you have any questions. It's important to us that Visual Basic and other languages interoperate well with COM components.

When in Doubt

If you want to see how Visual Basic builds CoClasses and interfaces (which are accessible from Visual Basic and script) to get an idea of how the IDL file looks:

  1. Start Visual Basic and create a new project, either an ActiveX EXE or an ActiveX DLL.

  2. Add the appropriate properties, methods, and events:
    'Declare an Event.
    Public Event Foo(ss As Integer)
    'Declare a method.
    Public Sub Bar()
    End Sub
    
  3. Compile the component into an EXE or DLL.

  4. Start the OLE-COM Object Viewer (which ships with Visual C++).

  5. Under the File menu, select View TypeLib... and open the binary file you just built.

Result: The IDL for the Type Library will be displayed.

Note   Visual Basic type libraries add additional information into them as "custom" attributes. These should be ignored unless you understand what they do. Also, Visual Basic event interfaces are implemented as dispinterfaces, a subset of "dual." If at all possible, all of your interfaces should be marked "dual."