Ivo Salmre
Microsoft Corporation
February 1998
Click to copy the sample files for this technical article.
Abstract
Goals of Versioned Component Development
OLE Automation Calling Conventions
Overview of Languages
The Visual Basic Public Class
Building Robust Components
Insuring Broad Component Usability
Insuring Broad Component Interoperability
Maintaining Your Components
The Sample
Tips and Common Mistakes
Summary
Other References
Microsoft® Visual Basic® makes it easy to build Component Object Model (COM) components. Without writing a line of code, a programmer can create a new Microsoft ActiveX® project and compile it into a server. Adding properties and methods is also similarly easy. Properly maintaining and versioning components is a more complex endeavor. Doing so is extremely important, because it will save the time and great frustration that result when misversioned components break existing applications. To do this, all of the programmer's existing Visual Basic skills can be leveraged, but a few new guidelines and strategies must also be learned. This document is an effort to lay out these additional strategies.
What does it mean to develop, version, and maintain components? Developing, versioning, and maintaining components means writing, debugging, deploying, and maintaining components that run in mission-critical or line-of-business applications. These applications are designed to automate, simplify, or make possible tasks of great value to a business or across businesses. Failure or redesign of these components is costly.
Components derive their strength from their ability to be used widely. This means ensuring that components you and your team build work well, whether they are used from Visual Basic, Java, C/C++, Visual Basic Scripting Edition (VBScript), JavaScript, or any other language.
Large systems are built out of many components that interact with each other. It is important to design components in these systems in such a way that this interaction is highly usable and robust—that is, that components you and your team build work well with each other.
As business needs evolve and grow, it is important that business systems evolve with them. Much is needed today, and more will be required tomorrow. To this end, the components you build need to be able to be upgraded with as little disruption to the larger business systems as possible. (For example, upgrading a single component should not require recompiling and redeploying all 60 components in the larger system; it should be upgradable by itself.)
In order to understand how to design components that work well in both scripting and precompiled languages, it is important to understand how different languages bind to COM objects and access properties and methods on them.
Early binding (also known as VTable binding) is compile-time binding. When an application that consumes components is compiled, the calls to functions on those objects (v-table offsets) and the signatures of those functions (number, type, and order of the parameters) are resolved and compiled into the application.
Because the function calls are resolved at compile time, work does not need to be done at run time.
Because the parameter types are known, error checking can be done at compile time.
Adding a parameter or changing the order of the functions in the object's v-table will result in breaking binary compatibility. The user must choose between breaking compatibility and making these changes.
Late binding is run-time binding. Method calls are resolved at run time.
If all of your components are accessing objects from a late-bound perspective (and this is an important "if"), you have a great deal of liberty to add parameters and modify parameter types.
The purpose of building components is good encapsulation and code reuse. COM makes possible broad reuse of components by different languages. To gain the maximum benefit of building components, you should design your components to the needs of the consumers of those components.
Technically "everything is possible in C/C++," meaning that you can call any interface from C/C++, whether it's early bound or late bound, uses variants or not, and so on. However, just because it is possible doesn't mean that it is convenient, useful, or prudent. C++ (and C) is a strongly typed language that works best with strongly typed calling conventions, such as early-binding and strongly typed variables.
The following partial code shows how to make a late-bound call from C++:
//MISSING: Code to package the parameters in an array of variants (this code removed in the interest of saving some space).
//Look up the memid of our call
hRes = pDisp->GetIDsOfNames(IID_NULL, &pbstrMethd, 1, NULL, m_lngSyncCallDispID);
ON_FAIL_ABORT(hRes);
ON_FALSE_ABORT(m_lngSyncCallDispID != -1);
//Init the return variant structure
VariantInit(&vntRetVal);
//Clear the last result before we get going here
hRes = VariantClear(&m_vntRetVal_SyncCall);
//Since we may be expecting our invoke to cause an error,
//we do not want to propagate an error upward if one occurs.
//instead we want to see if we're expecting an error...
m_hRes_SyncCall = m_pDisp_SyncCall->Invoke(
m_lngSyncCallDispID, //ID of method we're calling
GUID_NULL, //Must be NULL
0, //lcid: Don't care...
DISPATCH_METHOD | DISPATCH_PROPERTYGET,
&m_dispparams, //struct containing the parameters we're passing
&vntRetVal, //return value
&m_SyncMethod_ExcepInfo,//struct to store expection info in
&uArgErr); //Argument that caused Invoke to fail (i.e. arg type incorrect)
The following shows the code to make an early-bound call from C++:
hRes = m_SomeObject->SomeMethod(firstParameter, secondParameter);
As you will probably agree, early binding is considerably easier to use from C or C++.
Java, much like C++, is a strongly typed language, generally binding to objects and interfaces at compile time. Its variables also are generally strongly typed.
Java takes only by value parameters for functions. You may not return values via function parameters.
Note Can functions with by-ref parameters be called in Java? Yes, but it is awkward to do so. The by-reference variables must be declared as one-element arrays in Java in order to pass them as by reference to functions that require by-reference parameters. You should avoid by-reference parameters if you want to make using your objects simple for Visual J++™ programmers.
These comments refer to using Visual J++ version 6.0 and later. COM/Java interactions using Java compilers other than the Microsoft Visual J++ compiler will have different levels of COM support.
Microsoft Visual Basic was designed with COM and, specifically, Automation in mind. Not surprisingly, it offers the richest possible experience for calling components. Visual Basic is capable of easily binding to objects at compile time or run time:
The following code shows early binding to an object:
Dim objXL as Excel.Application 'Interface bound to the Excel.Application interface
Set objXL = CreateObject("Excel.Application") 'Create the object & bind it to the Excel.Application interface.
Dim objWB as Excel.Workbook 'Interface bound to the Excel.Workbook interface
Set objWB = objXL.Workbooks.Add 'Create a workbook object & bind to the interface
Note All object calls are resolved at compile time.
The following code shows late binding to an object:
Dim objXL as Object 'Late Binding interface
Set objXL = CreateObject("Excel.Application") 'Create the object & bind it to the Excel.Application interface.
Dim objWB as Object 'Late Binding interface
Set objWB = objXL.Workbooks.Add 'Create a workbook object & bind to the interface
Note All object calls are resolved at run time.
What is the moral of the story? The user code for late-bound and early-bound calls is almost identical in Visual Basic.
Polymorphism is the ability of an object to support multiple interfaces. Visual Basic makes it easy to bind to multiple interfaces in an object. Binding to an interface is done via the Visual Basic Set keyword.
For example:
Sub Foo (obj as Object)
Dim myCar as Car
Dim myTruck as Truck
'Takes the object (unknown type) and gets the Car inteface
Set myCar = obj
'Querys the object for the Truck interface
Set myTruck = myCar
'Querys the object for the Truck interface (same as above)
Set myTruck = obj
End Sub
.
As demonstrated above, the Set statement queries an object (that is, the variable on the right side of the Set statement) for the interface required by the destination variable (that is, the variable on the left side of the Set statement) interface. This is done through the COM QueryInterface function. If the object being queried does not support the requested interface, a run-time error is generated. This run-time error can be trapped; in this way, you can dynamically determine whether an object supports a given interface. Polymorphism is a powerful, but advanced behavior.
Scripting languages (for example, VBScript and JavaScript) use untyped variables. Because the variables have no type, there is no way for the compiler to resolve their properties or methods until the calls are made (late bound).
Note JavaScript does not support passing parameters by reference. JavaScript will make copies of these variables and pass them by value when calling methods that take by-reference parameters. Thus, you cannot get a return value via a method parameter when using JavaScript.
It is worth taking a moment to understand a Visual Basic class from a COM perspective.
Note See Visual Basic documentation for a more in-depth discussion about Visual Basic classes.
When a class is marked "public" (in the classes property sheet), it means that:
Createable means that the object can be created from outside the project; that is, someone external to the project can say, "Hey, give me one of those!" Your server will oblige by creating a new instance of the class and returning it to the requesting party.
All createable classes are public.
Each createable class has a unique CLSID (for example, {2A3631E2-35F3-11D1-AD18-0020781019CA}) associated with it. When the component is registered on a machine, the CLSID is written out to the system registry. The CLSID is used to request that COM create an instance of your class. When a project (for example, Client A) that references your project (Server B) is compiled, the CLSIDs of your classes are stored in the compiled application (Client A). When a New <YourObjectName>
statement is executed in the application, the stored CLSID (in Client A) is used to request that COM create the object (in Server B).
Example:
Dim objXL as Excel.Application
Set objXL = New Excel.Application 'Asks the OS to create the object with the CLSID {00024500-0000-0000-C000-000000000046}
Each createable class has a ProgID (for example, Excel.Application) associated with it. When the component is registered on a machine, the ProgID is written out to the system registry. The ProgID in the system registry allows the object to be created via the CreateObject() function.
Example:
Dim objXL as Excel.Application
Set objXL = CreateObject("Excel.Application")
The difference between creating objects using New/CLSID and CreateObject/ProgID is that with the latter, the CLSID need not be known at compile time—this allows the CLSID to change.
Every public member (any member not marked as private or friend) of a class is part of that class's default interface.
Important things to note about default interfaces:
Dim x as Object
) and class a method off that variable, late binding is used.Points 1 and 2 are actually stating the same thing (because VBScript and JavaScript languages use variants and only support late binding).
Public members of a class are members of the default interface. The default interface supports early and late binding by consumers of the class.
The Implements keyword in Visual Basic is used to indicate that, in addition to the default interface, your class implements additional interfaces. This concept is referred to as polymorphism.
The Implements keyword allows you to use externally declared interfaces in your classes. This is a very powerful concept in large systems with many interdependent components. (See the sample code associated with this article for an example of how to use this in multicomponent systems).
Implements, when properly used, is your friend.
With Visual Basic, any public createable class modules are translated into CoClasses in the type library (TLB) of the COM component. Each CoClass will implement one or more interfaces. Each CoClass will implement the default interface, which contains any public methods or properties of the class module. In addition, the CoClass will contain any interface explicitly implemented with the Implements statement. Figure 1 shows a diagram of how Class1 in a COM component is translated into TLB information.
Figure 1: Representation of a Visual Basic class module in a TLB
Notice that the Visual Basic class module Class1 becomes CoClass Class1. Interface _Class1 is created to contain the Public method Test(). The CoClass also contains interface _Class2, because we implement the default interface of Class2. Finally, because an event was added, Visual Basic creates an event interface, __Class2, which contains the events, and the CoClass contains that interface as a source.
The same class module looks like this in Interface Description Language (IDL):
[helpcontext(0), dual, oleautomation, nonextensible, version(1.0), uuid(52987C33-D96D-11D1-9D66-006097DBED14)]
interface _Class1 : stdole.IDispatch {
HRESULT STDMETHODCALLTYPE Test(void) ;
}
[helpcontext(0), dual, oleautomation, nonextensible, version(1.0), uuid(52987C31-D96D-11D1-9D66-006097DBED14)]
interface _Class2 : stdole.IDispatch {
HRESULT STDMETHODCALLTYPE Test(void) ;
}
[helpcontext(0), version(1.0), uuid(52987C34-D96D-11D1-9D66-006097DBED14)]
coclass Class1 {
[default] dispinterface _Class1 ;
dispinterface _Class2 ;
[default, source] dispinterface __Class1 ;
}
Note This discussion refers to Visual Basic version 5.0.
There are three compatibility options in Visual Basic. These options can be used by specifying a previous version of a Visual Basic-generated COM component with which you wish to be compatible. Depending on the level of compatibility, and the changes made to a project, Visual Basic will compile an appropriate new server.
This means exactly what it sounds like; no compatibility with a previously compiled server is desired. A new type library globally unique identifier (GUID), CoClass GUIDs, and interface GUIDs will be generated. A server compiled with no compatibility will not work with any existing compiled client applications.
Project compatibility is most useful when working with multiple projects under development; that is, running in the Visual Basic integrated development environment (IDE). It is not meant to assure compatibility with compiled projects.
Project compatibility allows other instances of the Visual Basic development environment to maintain references to your server, while allowing the modification of the interfaces of a COM server component (for example, deletion of functions, modification of parameters, and so on). It accomplishes this by simply keeping the type library GUID the same. In Visual Basic 5.0, the CoClass GUIDs and Interface GUIDs are reset when a project is recompiled as a project-compatible server.
The following outlines a typical use of project compatibility:
In short, project compatibility is primarily a debugging convenience feature.
Binary compatibility goes further than project compatibility by allowing a server to be recompiled, and any existing compiled client application should still work with the new binary-compatible server.
For a typical use of binary compatibility in debugging, let's suppose we wish to debug ServerB and produce a version with bug fixes:
Binary compatibility is useful when we want to compile versions of components that are compatible with previously compiled versions of these components.
The compatibility options you choose will determine how the type library (TLB) compiled into your COM component is modified. Binary compatibility will also modify the list of interfaces that your server supports (that is, the interfaces your server responds to a QueryInterface by another server). Table 1 summarizes the changes that Visual Basic 5.0 will make to the type library depending on the compatibility setting.
Table 1. Changes in the Type Library Depending on Compatibility Settings
No compatibility | Project compatibility | Binary compatibility | |
TLB version | Reset to 1.0 | Major version incremented | Minor version incremented |
TLB GUID | New GUID | Same | Same |
CoClass version | Reset to 1.0 | Same | Same if unchanged, otherwise minor version incremented |
CoClass GUID (CLSID) | New GUID | New GUID | Same |
Interface version | Reset to 1.0 | Reset to 1.0 | Same if unchanged, otherwise minor version incremented |
Interface GUID (IID) | New GUID | New GUID | Same if unchanged, otherwise new GUID given |
VTable offsets of methods/properties in interface | Random order | Random order | All methods in new interface that are in the old interface must get same VTable offsets |
DispIDs of methods/properties in the dispInterface | Random order | Random order | All methods in new dispInterface must get same DispID |
Enum GUID | New GUID | New GUID | Same (additional Enum members are allowed, but removal is not allowed) |
Note Project compatibility only keeps the TLB GUID the same. Any compiled client applications will not work, but a client application in the debugger will maintain its reference.
Binary compatibility exhibits the most sophisticated behavior. In the cases where Binary compatibility is preserved, the TLB GUID remains the same and the CoClass GUIDs (CLSIDs) remain the same. If no new methods have been added, Visual Basic 5.0 will keep the interface GUIDs the same. The Vtable offsets and the DispIDs of methods and properties are maintained if binary compatibility is set.
Question: Why does Visual Basic sometimes generate new Interface GUIDs (IIDs) with binary compatibility set?
Answer: In the case where new methods have been added to an interface, Visual Basic generates a new interface GUID for that interface. This behavior is in accordance with COM rules (some scenarios would be problematic, potentially causing a crash, if this were not done).
For example:
Class1:
Public Sub Method1()
End Sub
Class2:
Public Sub Test(obj As Class1)
Obj.Method1
End Sub
Now consider the situation where we had a client application that implements Class1 and instantiates Class2 in the DLL server, and passes over its own implementation of this interface. All this would work fine to begin with. However, suppose the DLL server is changed as follows:
Class1:
Public Sub Method1()
End Sub
Public Sub Method2()
End Sub
Class2:
Public Sub Test(obj As Class1)
Obj.Method1
Obj.method2
End Sub
If Visual Basic kept the same interface GUID for Class1, and the old client application was run, the server application would crash when it attempted to call Method2 (because Method2 did not exit in this class).
Whenever possible, function parameters should be strongly typed (that is, they should not be variants). This allows languages that support early binding to generate compile errors for conditions that would otherwise only be detected at run time.
Note Languages that do not support strongly typed variables (such as VBScript and Java Script) will suffer no penalty calling these methods/properties. Strongly typing the parameters will give the consumers of the components a better idea of what is expected by the method call.
This may seem like an obvious suggestion, but it cannot be overstated. Scalability is something that must be thought of throughout the development cycle. Measure concurrency needs early, measure them often, and modify your designs accordingly. There is no substitute for empirical evidence.
For maximum penetration, it is desirable to build components that work well when used from heterogeneous programming environments.
Visual Basic, C++, and VBScript support passing parameters by reference. Java and JavaScript do not. Therefore, to make your components palatable to the largest set of users, you should avoid requiring by-reference parameters to your methods and properties.
Note In Visual Basic and VBScript, if a method parameter is strongly typed and is called late bound, passing in a variant, a run-time type-mismatch error will occur. Only strongly typed parameters can be passed to this kind of method. This is yet an additional reason to use by-value parameters if possible.
Common sense advice: If you want to insure that your components work in heterogeneous programming environments, test your components early from these environments. Better to modify your design during prototyping rather than have to redesign late in the development process.
Large enterprise systems are typically composed of many components that need to interoperate.
Often, it is useful to define a set of common interfaces and to implement these in objects throughout an enterprise. Many COM core interfaces are used in this way (for example, IPersistPropertyBag, IDataSource, IErrorSource, and IOleObject). An interface connotes a behavior, such as "I am a purchase order" or "I am a bank transaction." Visual Basic can bind to Automation-compatible interfaces that are defined inside a type library. By centrally defining the interfaces used by your system in a type library, you make it easy for your components to implement these interfaces and interact with objects that implement them.
Note The interfaces I listed above all began with the letter "I." This is typical for core COM interfaces, but do not feel compelled to do this. In fact, you may want to avoid the prefix "I." Not using "I" in the prefix of interface names you define makes them more readable and easier to browse in object browsers. For example, ActiveX Data Objects (ADO) and Data Access Object (DAO) avoid using "I" in the public interfaces that Visual Basic binds to.
Note The sample accompanying this article uses this approach and is instructive on how to use external type libraries as a central location for interface definitions.
Often in large systems, you may wish to have multiple implementations of objects that share interface definitions. Use the Visual Basic Implements keywords to do this.
Note A class can implement multiple interfaces.
You may wish to have a network of servers that can communicate with each other via early-bound interfaces. (For example, ServerA creates strongly typed objects on ServerB. ServerB gets strongly typed objects passed in from ServerA on a method call.) This can be difficult because Visual Basic does not allow circular references between projects; in this case, one of the components would have to late bind to the objects in the other. This problem can be overcome by defining your business interfaces centrally and having your components implement interfaces in these type libraries.
The member functions of implemented interfaces are marked private by default and, as such, are not part of the objects default interface. This means that they are not callable from scripting languages (which can only call methods in an object's default interface). The best way to solve this dilemma is to declare public functions that delegate to your private implemented functions. For example:
Implements Bicycle 'This object implements the Bicycle interface
'--------------------------------------------------
'Implementation for Bicycle::RingBell()
'
' Note: Since this function is Private, it is not callable from script
'--------------------------------------------------
Private Sub Bicycle_RingBell()
Beep
End Sub
'--------------------------------------------------
'Function in the default interface that delegates to the function in
'the Bicycle interface.
'--------------------------------------------------
Public Sub RingBell()
Bicycle_RingBell
End Sub
The primary use of this approach is to build a component that is used by many clients, but which lacks any dependencies on these clients—for example, an ActiveX control (.ocx) developed for resale. Many clients will bind to the component that contains the control. Yet the control needs to know little about the components and objects that bind to it (that is, it has no need to bind to any of its clients' interfaces).
Keep a separate, binary-compatible version of your component around. It's important to differentiate between development and deployment.
The binary component you choose to be compatible with should not change often. It should only change once per deployment of you application (once for your version 1.0 public release, once for your version 2.0 public release, and so forth). The message to take away here is that you should not be overwriting the component that you are basing your component's compatibility on every time you build an .exe, .dll, or .ocx.
Explanation
When you add a method to an interface, you are creating a superset interface of a previous interface. If you have binary compatibility turned on, this means that the new interface you have defined is compatible with, but not identical to, the previous interface. This will cause a new interface identifier (IID) to be added to the list of interfaces that your class supports. Doing this more than once per release will cause two problems:
The moral of the story: Carefully consider your compatibility target.
Scenario: Build an ActiveX control (ControlA) that can be consumed by several clients (ClientB, ClientC). Be able to version this control, fix bugs, and add methods.
Initial design, version 1
Step 1: Start a new ActiveX control project. Give the project a name and give the control a name.
Step 2: Add a test project (TestProjectD) to the group project containing ControlA. Place an instance of ControlA on a form in TestProjectD.
Step 3: Design your control. Add public methods, events, and so on.
Step 4: Run and debug your control in TestProjectD. Make needed changes to the design of your ActiveX control object.
Note Up to this point, your ActiveX control project (ControlA) has been set to project compatibility. This leaves you with the maximum amount of freedom to add, remove, and modify the members of the control project. Projects outside the group project that bind to the ActiveX control will fail often as the CLSID and IIDs of the ActiveX control are in flux. You may compile .ocx files from your project, but again, nothing is locked down.
Initial deployment, version 1
Step 5: Compile ControlA into an .ocx (ControlA.ocx). Make a copy of ControlA.ocx (ControlA_version1.ocx). Set the binary compatibility of ControlA's project to point to ControlA_version1.ocx.
Note Because you now have set binary compatibility to ControlA_version1.ocx, any builds you make of ControlA.ocx should be plug and play with the original ControlA.ocx.
Step 6: Use ControlA.ocx in ClientB and ClientC. You now have a fully functioning control.
Fixing bugs, version 1
Note Now that your control is deployed, it is very likely that you will discover bugs that you want to fix (that is, not object model changes, simply bugs in your code).
Step 7: Make fixes in ControlA (remember, ControlA has its binary compatibility set to ControlA_version1.ocx). Recompile and deploy ControlA.ocx.
Note If you want to change the signature of existing methods (for example, add parameters to methods), you will break binary compatibility. In this case, you should turn off binary compatibility and go back to Step 4. This will break applications that are bound to your current ControlA.ocx. This is the cost of making binary incompatible changes.
Initial development, version 2
Note Now that you have successfully deployed version 1 of ControlA, you wish to make updates to ControlA. You wish to add properties and methods to your control without breaking existing applications bound to ControlA.
Step 8: Keeping the binary compatibility set to ControlA_version1.ocx, add properties and methods to your application. Test these additions using TestProjectD (in the group project containing ControlA) to host your control.
Initial deployment, version 2
Step 9: Compile ControlA into an .ocx (ControlA.ocx). Make a copy of ControlA.ocx (ControlA_version2.ocx). Set the binary compatibility of ControlA's project to point to ControlA_version2.ocx.
Note Because you have now set binary compatibility to ControlA_version2.ocx, any builds you make of ControlA.ocx should now be plug and play with the original ControlA.ocx. Because you have added methods and properties to what was in ControlA_version1.ocx, your ActiveX control now supports three programmatic interfaces (two IIDs). The initial interface is the one defined when you built and deployed the version 1 of your control. The second new interface is the one you just defined.
Fixing bugs, version 2
Note Now that your control is deployed, it is very likely that you will discover bugs that you want to fix (again, not object model changes, simply bugs in your code). Let's take a look at doing that.
Step 10: Make fixes in ControlA (remember, ControlA has its binary compatibility set to ControlA_version2.ocx). Recompile and deploy ControlA.ocx.
Note If you want to change the signature of existing methods (for example, add parameters to methods), you will break binary compatibility. In this case you should turn binary off compatibility and go back to Step 4. This will break applications that are bound to your current ControlA.ocx. This is the cost of making binary incompatible changes.
Initial development, version 3
Note Now that you have successfully deployed versions 1 and 2 of ControlA, you wish to make updates to ControlA. You wish to add properties and methods to your control without breaking existing applications bound to ControlA.
Step 11: Keeping the binary compatibility set to ControlA_version2.ocx, add properties and methods to your application. Test these additions using TestProjectD (in the group project containing ControlA) to host your control.
Initial deployment, version 3
Step 12: Compile ControlA into an .ocx (ControlA.ocx). Make a copy of ControlA.ocx (ControlA_version3.ocx). Set the binary compatibility of ControlA's project to point to ControlA_version3.ocx.
Note Because you have now set binary compatibility to ControlA_version3.ocx, any builds you make of ControlA.ocx should now be plug and play with the previous two released versions of ControlA.ocx. Because you have added methods and properties to what was in ControlA_version2.ocx, your ActiveX control now supports three programmatic interfaces (three IIDs). The initial interface is the one defined when you built and deployed the version 1 of your control. The second interface is the one defined when you built and deployed the version 2 of your control. The third new interface was the one you just defined.
And so on. Again, the idea here is to be careful and methodical.
The primary use of this approach is to build systems of multiple components that need to communicate with each other. The larger number of discrete components in your system, the more it will benefit from centrally defining and maintaining the interfaces that are critical to your system.
Note As the number of components in your system grows, the burden of "one more file" (the type library with all the interface definitions) becomes minuscule.
Do not use a Visual Basic built .exe, .dll, or .ocx as your centrally defined type library. This seems very tempting at first.
Wrong. Declare all of your interfaces in Visual Basic classes in one Visual Basic project (simply declaring the interfaces, adding no code). Compile that project into an .exe or .dll. Have all the other projects reference the type library in this compiled component in order to get their interface definitions. No fooling with IDL, no mess, you're done!
This works well until you need to change something (which you most likey will have to do at some point). At this point, because you do not have direct control of CLSIDs, IIDs, and so on, you are once again at the mercy of the compiler and may paint yourself into a corner. A far better approach exists.
Right. Declare all of your interfaces in Visual Basic classes in one Visual Basic project (simply declaring the interfaces, adding no code). Compile that project into an .exe or .dll. Use the "OLE COM Object Viewer" to spit out the IDL representation of these interfaces. Remove the CoClasses, and modify the interfaces (names, hidden attributes, and so on.). Compile, using Microsoft Interface Definition Language (MIDL), the IDL file into a stand-alone type library and use this type library as your central interface definition.
This allows you to leverage the simplicity and rapid application development (RAD)-ness of declaring interfaces in Visual Basic and provides the power and flexibility gained by having a stand-alone type library over whose contents you have full control.
Separating the interface objects that early bind from the interface that objects late bind has several advantages. It allows you to maximize the flexibility of late binding (for example, adding optional parameters) and the speed and type safety of early binding.
The IDL, is used to describe classes and interfaces. When compiled using MIDL (which ships with Microsoft Visual C++®), it produces type libraries.
Type libraries are binary files that describe the functionality and layout of these classes and interfaces. Type libraries can be referenced by programming tools such as Visual Basic, Visual J++, and Visual C++.
When a type library is referenced by Visual Basic, its classes can implement the interfaces described in the libraries of Visual Basic classes.
Using CreateObject("<ProjectName>.<ClassName>") in conjunction with using Implements for all of your programmatic interfaces will give you the greatest degree of flexibility while maintaining the type safety and efficiency of early binding. The object is created and then bound to the desired interface.
Going this extra step means that none of your projects need to reference each other; they only need to reference your central type library with the interface definitions.
Note CreateObject is slower than New because it needs to look up the CLSID in the system registry at run time. However, the performance of method calls in both cases is the same (because you are early bound to the objects interface, you get the maximum call speed). If your ratio of method calls to object creations is high, this overhead should be minor. If you are creating many objects and performing relatively few method calls on these objects, this overhead may be more noticeable. I advise using CreateObject() to create objects, and in those cases where object creation speed is critical, add the project reference and use the New keyword. As in every other case, there is no substitute for empirical evidence. Measure and tune.
Question: I am using a language that does not have CreateObject(). What can I do to create an object using the ProgID instead of the CLSID?
Answer: Visual Basic and certain run-time environments (for example, Internet Information Server's Active Server Pages) implement CreateObject(); many other languages and run-time environments do not (yet). In these environments, there are two basic strategies you can follow to gain the CreateObject functionality:
Class ActiveXHelper:
Function CreateObjectFromProgID(ProgId as String) as Object
Set CreateObjectFromProgID = CreateObject(ProgId)
End Function
You can then call this class function from your non–Visual Basic code to create late-bound objects. Because this code is simple and does not need to be recompiled, you can be sure that the CLSID, IID, and so on will not change. (Thus, you create this object as can early bound.)
Of the two methods, I prefer the second, because it is simpler. (It takes minutes to build and the code requires no real debugging.) This is a great example of using COM to solve problems in the best-suited language and leveraging this solution from other languages.
Scenario: Build a system of components (ComponentA, ComponentB, and ComponentC) that interact with each other via well-defined interfaces.
Initial design, version 1
Step 1: Start a new .idl file (MySystem.idl), and define public interfaces for the system using the IDL language. Use guidgen.exe (which ships with Visual C++) to generate new unique IDs for your type library and interfaces.
Example of an interface:
[uuid(63789A11-E480-11d1-860D-0000F875B12F), odl, oleautomation, nonextensible]
interface Car: IDispatch
{
[id(2))] HRESULT LicensePlate([out, retval] BSTR *pstr);
}
Step 2: Compile the .idl file into a type library (MySystem.tlb) using the midl.exe compiler (which ships with Visual C++). Note that the MIDL compiler can also produce header files for use by C++.
Step 3: Start Visual Basic projects that contain your objects (ComponentA, ComponentB, and ComponentC). Create classes for your interfaces and have them implement the interfaces defined in your type libraries.
Step 4: If you want your components to be accessed by scripting languages, declare public methods in your classes that defer to the private implemented interface methods.
Step 5: Debug your components. Because you have not deployed your interfaces yet, you can make changes as needed, add and remove methods, change parameters, and so on. (You will have to recompile your components that implement or consume these if they are affected by the interface changes you made.)
Initial deployment, version 1
Step 6: Deploy your components and type library. Consumers of your components should bind to your type library (not the individual components).
Note 1 Obtain maximum flexibility by having the consumers of your components create instances of your object using the classes ProgID (for example Visual Basic's CreateObject()) instead of using CLSID (for example, Visual Basic's New). In this way, they are not bound to the classes CLSID.
Note 2 Once you have published your type library and components, you are obligated to maintain compatibility for those client applications that have bound to your components. If you have full knowledge of all the clients that bind to your components, you may make surgical interfaces modifications that you know will not affect clients bound to your components, but this is definitely advanced behavior.
Fixing bugs, version 1
Step 7: Doubtless, there will be bug fixes that need to be made and components redeployed. As long as you do not change the interface definitions that your clients are bound to, you can simply make these fixes and redeploy your components.
Initial design, version 2
Step 8: You have successfully deployed version 1 of your system. You wish to add methods and properties to version 2 of your interfaces. Start with your exiting IDL file (MySystem.idl), make a copy of the interface you wish to extend, give it a unique interface ID, and add the methods to the updated interface.
Example of version1 and version2 interfaces:
[uuid(63789A11-E480-11d1-860D-0000F875B12F
), odl, oleautomation, nonextensible]
interface Car: IDispatch
{
[id(2))] HRESULT LicensePlate([out, retval] BSTR *pstr);
}
[uuid(63789A12-E480-11d1-860D-0000F875B12F
), odl, oleautomation, nonextensible]
interface Car2: IDispatch
{
[id(2))] HRESULT LicensePlate([out, retval] BSTR *pstr);
[id(3))] HRESULT CarModel([out, retval] BSTR *pstr);
}
Step 9: Update your components to support both interfaces. Typically, members of one interface will delegate its implementation to members of another interface. Example Visual Basic code showing this follows:
Implements Car 'This class implements the CAR interface
Implements Car2 'This class implements the CAR2 interface
Private Function Car_LicencePlate() as String
Car_LicencePlate = "IOU 123"
End Function
'Function that delegates to another implementation
Private Function Car2_LicencePlate() as String
Car_LicencePlate = Car_LicencePlate()
End Function
Private Function Car2_CarModel() as String
Car2_CarModel = "Sports Coupe"
End Function
Deployment, version 2
Step 10: Deploy your components and type library. Consumers of your components can now bind to the new (and old) interfaces. When binding to the new interfaces, they will be able to call the new properties and methods you have added to your objects.
Benefits
Drawbacks:
Note If you create your objects in a late-bound fashion (for example, using CreateObject()) and bind them to interfaces defined in an external type library, you can dispense with Visual Basic Version Compatibility options altogether. In fact, it's a good idea to set the Version Compatibility option to No Compatibility in these cases to make absolutely sure no CLSID or type library ID conflicts occur in any of your projects. This is powerful stuff and gives you great flexibility to handle situations Version Compatibility was not designed to handle. But at a cost—you must manage your type library and interfaces yourself.
The sample associated with this article shows a system of several servers using a centrally defined interface library. They exemplify the design patterns offered in Approach 2 (described earlier).
These objects interact with each other robustly because they are strongly typed and location independent.
Objects in this system are created late bound (via CreateObject) and then bound to strongly typed interfaces.
None of these projects references another project. Instead, they all reference the centrally defined interface library.
The type library of the system is built by running midl.exe and SimpleCarInterfaces.idl.
It is tempting to throw out all of your old built component sources and interface definitions and simply keep the latest and greatest pieces around. This is always a bad idea. Choose milestones in your products, whether time- (such as weekly or monthly) or feature-driven, make a copy of your development tree, and store it somewhere safe. Organize this process! You will be glad you did. Someday you will find yourself needing to roll back to an older copy of an interface or of a binary-compatible component. Having known versions of these components available will be well worth the small amount of time it takes to make a copy.
Note Source code control systems (such as Microsoft Visual SourceSafe™) are great for this task.
If you are using Approach 2, you will need to construct a type library. The OLE COM Object Viewer that ships with Visual C++ 5.0 is a great tool for taking a compiled type library and reverse engineering it to see what the IDL code for the type library looks like. If you are interested in using an external type library in your applications, this is a nice way to jump-start your development. This method is not foolproof, but using it properly will likely save time in constructing the needed *.idl file.
The .idl file for the component will be displayed in the viewer. You can cut and paste this into Notepad. This .idl file should be massaged before implementing it as interface definitions from Visual Basic. Do the following:
You should now have a type library that you can reference from Visual Basic and that contains interfaces you can implement.
Building a maintainable and versionable component is not hard, but it does take planning. Building components that are not maintainable or verisionable and deploying them into mission-critical situations is a costly venture, both in terms of long-term maintenance costs and system performance and reliability. Robustness and maintainability take planning and good design. For single components (such as ActiveX controls or servers without interdependencies), Visual Basic server compatibility (Approach 1) is a good one. For more complex systems of components, or where absolute control is required (most enterprise systems), external explicitly defined type libraries are preferable. Following the guidelines outlined in this document should help you achieve this goal and gain the full promise offered by component-based development.
Too many people were involved in reviewing this document and suggesting improvements to fully list here, so I will not attempt it. Thanks everyone for your time and thoughts, they are much appreciated. Specifically, Brian Haslam is to be thanked for contributing important parts to the contents of this document.
For details on declaring interfaces, naming guidelines, and the finer technical aspects of building usable interfaces, see my article "Building COM Components that Take Full Advantage of Visual Basic and Scripting."
Note This document is not intended to replace the documentation shipping with Visual Basic. The reader is encouraged to seek detailed knowledge on topics such as Binary-Compatibility and Implements in the Visual Basic documentation. Reading this before, after, or concurrently with the product documentation will doubtless be of benefit.