Ivo Salmre
Microsoft Corporation
February 24, 1998
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.
This document describes how to design COM object libraries that take advantage of the Microsoft® Visual Basic® development system and scripting languages.
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).
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.
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").
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.
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."
Set objXL = CreateObject("Excel.Application")
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.
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.
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.
In order to make a CoClass creatable it must be registered. This involves two steps:
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.
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:
—or—
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.)
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.
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.
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
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* );
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.
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.
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:
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).
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.
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.
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.
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.
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.
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.
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.
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
{
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.
*.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 should have plain English names. If a class describes a car, call it Car, not ICar.
ProgID's are what are used by Visual Basic and scripting's CreateObject keyword.
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 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.
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:
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.
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 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.
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.
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.
An error has occurred inside your method. You wish to return descriptive error information. Do the following:
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.
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
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.
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
After building your Visual Basic– and script-friendly components it is easy to test them in Visual Basic.
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:
'Declare an Event.
Public Event Foo(ss As Integer)
'Declare a method.
Public Sub Bar()
End Sub
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."