Export (0) Print
Expand All
4 out of 6 rated this helpful - Rate this topic

Building, Versioning, and Maintaining Visual Basic Components

Visual Studio 6.0
 

Ivo Salmre
Microsoft Corporation

February 1998

Contents

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

Abstract

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.

Goals of Versioned Component Development

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.

Broad Component Usability

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.

Robust Component Interoperability

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.

Maintainability

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.)

OLE Automation Calling Conventions

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 (Compile-Time Binding)

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.

Advantages

  • Fastest possible code.

    Because the function calls are resolved at compile time, work does not need to be done at run time.

  • Strongest type checking.

    Because the parameter types are known, error checking can be done at compile time.

Disadvantages

  • Not resilient to function signature changes.

    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.

  • Scripting languages are not capable of early binding.

Late Binding (Run-Time Binding)

Late binding is run-time binding. Method calls are resolved at run time.

Advantages

  • Most resilient to function signature changes.

    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.

Disadvantages

  • Cumbersome to call from C++/Java.
  • No compile-time type checking (errors are found at run time).

Overview of Languages

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.

C and C++

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++.

J++: Strongly Typed

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 : Jack of All Trades

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:

Early and late binding using Visual Basic

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 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: Late Bound and Flexible

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.

The Visual Basic Public Class

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.

Making a Class Public

When a class is marked "public" (in the classes property sheet), it means that:

  1. Its default interface (explained below) is published in the servers type library (compiled into the servers .exe, .dll, or .ocx file).
  2. The object can be passed outside of the project. All objects that you intend to pass around cross-project need to be marked "public."

Making a Class Createable

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.

Class identifiers (CLSIDs)

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}

Programmatic identifiers (ProgIDs)

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.

The Default Interface

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:

  1. The default interface of a class is the only interface that scripting languages can access.
  2. The default interface of a class is the only one that can be late bound to. If you assign a reference of your object to a variable that is a variant or object (for example, 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

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.

Translation of Visual Basic Class Modules to CoClasses and Interfaces

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 ;
}

Compatibility Options in Visual Basic

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.

No compatibility

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

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:

  • ServerA is under development. Frequent recompiles are taking place to build the component (ServerA.exe). Many of these compiles change interfaces in a binary-incompatible way.
  • ClientB is also under development. ClientB references ServerA.exe. ClientB is being run and debugged in the Visual Basic development environment (that is, not as a built .exe or .dll).
  • ServerA keeps project compatibility set, allowing the Visual Basic environment to keep track of any changes. Every time ClientB recompiles, it will sync to the class and interface changes in ServerA.

In short, project compatibility is primarily a debugging convenience feature.

Binary compatibility

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.

  • This feature is surprisingly useful (although has limitations that can be overcome by methods discussed in the next section). It would be very painful to have to recompile and distribute every client application, just because of a bug fix in a server. This is especially problematic from a distribution standpoint. When a server is on a second tier, there may be tens, hundreds, or thousands of client applications that use that server. Having to recompile them would be a nightmare. Binary compatibility solves this problem.
  • Binary compatibility can also be set while debugging (not many people are aware of this). Binary compatibility can be used to enable the debugging of servers when the client is compiled.

For a typical use of binary compatibility in debugging, let's suppose we wish to debug ServerB and produce a version with bug fixes:

  • A client (ClientA.exe) is early bound to the server (ServerB.dll). The source for the client is not readily available.
  • ServerB is loaded into the Visual Basic development environment. Binary compatibility with the compiled ServerB.dll is turned on. The server is placed into run mode.
  • ClientA.exe is run. It creates objects in ServerB (running in the Visual Basic debugger). Because binary compatibility (in ServerB) has been turned on, ClientA is able to create and bind to objects in ServerB.
  • The problem is debugged. A new ServerB.dll is compiled (no public interface changes were made, just bug fixes). Because binary compatibility (in ServerB) has been turned on, a new version of ServerB is produced that can be bound to by clients of the old ServerB. No recompile in the clients is required.

Binary compatibility is useful when we want to compile versions of components that are compatible with previously compiled versions of these components.

Modification of the Type Library with Compatibility Settings

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:

  • Suppose there is a Visual Basic COM dynamic-link library (DLL) server with two class modules—Class1 and Class2. Suppose Class1 has one method, and Class2 has a method that takes a parameter of Class1:

    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).

  • A similar situation exists when DCOM is involved between components. Suppose in the example above the old client and the old server are on Machine1, and the new DLL server is on Machine2. If the client instantiated Class1 on Machine1 and then attempted to call method Test in Class2 on another machine, the server on Machine2 would crash if the client passed over an old interface.

Building Robust Components

Method/Property Parameters Should Be Strongly Typed

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.

Measure while You Develop

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.

Insuring Broad Component Usability

For maximum penetration, it is desirable to build components that work well when used from heterogeneous programming environments.

Functions/Properties Should Take By-Value Parameters

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.

There Is No Substitute for Empirical Evidence

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.

Insuring Broad Component Interoperability

Large enterprise systems are typically composed of many components that need to interoperate.

Sharing Early-Bound Interfaces Between Objects

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.

Declare your interfaces centrally and have servers implement these interfaces.

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.

Breaking the circular references dilemma

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.

Using objects that use Implements from script

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

Maintaining Your Components

Approach 1: Visual Basic Binary-Compatible Servers

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).

Benefits

  • Ease of use for simple cases.
  • Component can ship with a minimum number of files (that is, the Visual Basic run time and your compiled component—.dll, .exe, or .ocx).

Drawbacks

  • Strict versioning. Incompatible changes (which arise often in the development cycle) may cause the CLSIDs and IIDs in your project to be reset. This can be time consuming to manage.
  • You do not have final and absolute control over the CLSIDs, TLB GUIDs, IIDs, and so on. Rather, you have entrusted Visual Basic to maintain these for you and apply the appropriate versioning rules.

Tips for using this approach

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:

  • Eventual bloat: Your compiled server will grow slightly larger (~ 100 bytes) every time a class supports a new interface. Doing this over and over again will force your classes to support a larger number of interfaces than necessary, thus growing the size of your component. Because most of these compatible interfaces are meaningless (no deployed applications are targeting them), it's best to avoid accumulating them.
  • Painting yourself into a corner: You have released version 1.0 of your application (MyApp.exe). It consists of one class (Class1) with one method (Sub Foo()). You are now starting on version 2.0 of your application, in which you intend to add a method to the class. You compile a new version of MyApp.exe, adding a new method (Sub Bar()). You are still in development mode. You accidentally (ignoring the advice above) set the binary-compatible server to the new MyApp.exe you have just built. While testing, you realize that you really should have a parameter to your new method (Sub Bar(Optional X as Integer)). If you recompile now, you will be breaking binary compatibility with the second version of MyApp.exe and you will receive compile errors telling you that you are not binary compatible. You have painted yourself into a corner. Had you kept your binary compatibility targeted at the first version of MyApp.exe, you would not have this problem (because you would still be a superset of the first interface that you released).

The moral of the story: Carefully consider your compatibility target.

Using binary compatibility

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.

Approach 2: Declare Your Interfaces Centrally and Have Servers Implement These Interfaces.

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.

Benefits

  • You have final and absolute control over the IIDs (interface IDs) and interface definitions in your project.
  • One central location for all interface definitions. This avoids the complications of many projects referencing a myriad of different projects, circular references (which are disallowed), and so on.
  • Work around bugs in binary and project compatibility. Visual Basic does a very good job enforcing these rules and warning the user when binary incompatible changes are being made. This allows the user to produce binary-compatible components of existing components. As the complexity of your systems increase, this task becomes much more complicated because bugs do exist in Visual Basic handling of complex cases. Explicitly declaring your interfaces in an external type library allows you to circumnavigate Visual Basic's binary compatibility.
  • C++ and Visual J++ friendliness. Because you can insure that the interface IDs remain static, it is easier to build C++ and Java components that bind to your objects.

Drawbacks

  • An additional type library that needs to be installed with the systems you design.
    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.
  • You do not have final and absolute control over the CLSIDs, TLB GUIDs, IIDs, and so forth. Rather, you have entrusted Visual Basic to maintain these.

Tips for using this approach

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.

Separate the late-bound interface from the early-bound interface.

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.

Use IDL to define your early-bound interface.

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.

Create objects via late-bound mechanisms and bind the returned objects to early-bound interfaces.

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:

  1. Implement CreateObject yourself. This involves writing a function that looks up the ProgId in the registry (for example, Excel.Application), finds the CLSID associated with it (for example, {00024500-0000-0000-C000-000000000046}) and calls CoCreateInstance (or CoCreateInstanceEx) with the CLSID to create the object.
  2. Use Visual Basic to create a simple COM DLL (for best performance, make sure the COM DLL is marked for Apartment Threading [also known as Unattended Execution]), which exposes the Visual Basic CreateObject as a class member. Example:
    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.

Using external type libraries

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.

Summary of Approach 2

Benefits

  • Full control. You are in full control of the interface ID (IID), the signatures of the functions, and their layout in the interface. This gives you an enormous amount of flexibility in your development process.
  • Sharing of interfaces between components. Abstractly declaring your interfaces allows easy sharing of interfaces between components. For example you can declare a Car interface and have multiple objects (such as Convertible, Truck, or Coupe) implement these interfaces.
  • Visual Basic Version Compatibility no longer needed. Visual Basic Version Compatibility does a great job of making sure that binary-incompatible changes are detected and not compiled into your application. This saves the user from many dangerous situations where they could otherwise compile code that had invalid (crashing) calls present. The downside is that for large systems of dependent projects, managing all of the components and their version compatibility rules can be a nightmare (Which project referenced which? When do I need to compile? What order?). Managing these versioning issues by placing all of the systems interfaces into an external type library allows you to overcome these difficulties and makes large systems more manageable.

Drawbacks:

  • Extra work. Two separate interface declarations and the need to use IDL to declare interfaces.
  • Responsibility. Being in full control of the interface layout and versioning places a burden on the developer. He or she is responsible for making sure proper versioning semantics are maintained between releases of their components.
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

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.

Tips and Common Mistakes

Always keep copies of your milestone builds of components, interfaces, and sources.

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.

The OLE COM Object Viewer can be helpful in generating IDL.

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.

  1. Use Visual Basic to create a COM .dll or .exe that contains the properties and functions you want in your interface.
  2. Compile the component.
  3. Run the OLE COM Object Viewer. Choose the View TypeLib... menu option and select the component that you just built.

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:

  1. Remove all of the CoClass definitions. (You do not need them and having them around will just be confusing.)
  2. Change all the GUIDs (use guidgen.exe to generate new GUIDs) for the type library, interfaces, and so on.
  3. Rename the type library and interface names to something more appropriate.
  4. Make sure to remove the underscore (_) from the front of the interface names and remove the hidden attributes from the interface attributes. Removing these will make the interfaces visible from the Visual Basic object browser.
  5. Save the file as <Filename>.id. Use midl.exe to compile the .idl file. (Note that the "/h" switch will produce a C/C++ header file as well.)
  6. Fix any syntax errors that come up during the compile.

You should now have a type library that you can reference from Visual Basic and that contains interfaces you can implement.

Summary

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.

Credits

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.

Other References

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.
Did you find this helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft. All rights reserved.