AddThis Social Bookmark Button

Print

Versioning VB 6 Components with VB .NET: An Excuse to Use VB .NET Today

by Jose Mojica
08/22/2001

Many of us live in a world where we are not allowed to use beta versions for our production code. In fact, it may be sometime after a final version is released before management allows us to start migrating code. By this time you have probably heard of VB .NET and you may have downloaded the Visual Studio .NET beta, perhaps with the justification that you intend to evaluate it for future releases. You may also be looking for any excuse to use it now.

Related Reading

COM+ Programming with Visual Basic
Developing COM+ Servers with COM, COM+, and .NET
By Jose Mojica

It turns out that there is at least one compelling reason for using VB .NET today: to solve an existing VB 6 problem--versioning. This article shows you the problems with versioning VB 6 code and how to solve them by creating custom type libraries with VB .NET. A great thing about this technique is that the type library you will generate will not be dependent on the .NET SDK. Therefore, you can use it without shipping beta code to your clients.

Versioning Components in VB 6

The problem with versioning components in VB 6 boils down to one thing: VB 6 does not give you full control over your interface's GUIDs. The basic scenario is that you create a component and compile it, then write a client program to use the component. Sometime later you make modifications to your component and you recompile it. Suddenly, the client program no longer works. It usually fails with error 429--ActiveX can't create object. Most developers solve this problem by recompiling the client program. Thus, teams normally end up rebuilding every component and every client to ensure that they are all compatible.

The reason client programs stop working is because of numbers called GUIDs (globally unique identifiers). GUIDs are 128-bit numbers that are assigned (in COM) to different aspects of your component. An example of a GUID is {FAE3A31F-693C-4ca3-B0EC-0BD471042D52}. A typical VB ActiveX DLL or ActiveX EXE project has GUIDs assigned to three different parts of the project: to the type library, to each class, and to each interface.


Visit vb.oreilly.com for a complete list of O'Reilly books about Visual Basic.

Information about every public class in the project is grouped into a binary file called a type library. The type library file is embedded in the image of your COM server (ActiveX DLL or ActiveX EXE) as a Windows resource. To distinguish your type library from other type libraries, the VB compiler assigns a GUID, following the COM specifications. A GUID assigned to the type library is called a LIBID.

Every class in a project also receives a GUID, called a CLSID. Public classes in VB 6 have default interfaces. This is a table of all the public functions in the class. Each interface receives a GUID as its unique name. According to COM rules, if you modify the public methods in some way, you must assign a new GUID to the interface. Because VB 6 follows these same rules, if you modify a method in any way (add, remove, or change a method), it assigns a new GUID to the interface. However, what happens if you keep every method the same? It turns out that if you are not careful VB 6 will also change the GUIDs each time you compile, even if you have not changed a method.

Why is it a problem if VB changes a GUID for the interface? It's a problem if you create a client program to use the component. The client program builds a dependency on the component's interface GUID and on the class's GUID. If VB changes various GUIDs in the component, then the client program stops working.

If you have not made any changes to the component's interface, you must tell VB not to change the GUIDs of the default interface when you compile. You do this by setting the version-compatibility property of your VB project to binary compatibility. When you turn on this setting, each time you compile your server code, the clients continue to work fine. At some point, however, you will need to change, delete, or add a method. VB 6 has a mechanism for letting you add methods without breaking compatibility. That means your client code is safe as long as you only add methods. It is a different story if you remove or change a method. When you do so, COM says that you must change the GUIDs of your interface, and VB 6 obeys this rule by forcing you to switch to project compatibility.

The only problem is that VB does something unusual--it changes the GUIDs of every interface in the project, even the ones you haven't changed. So, if you have four components in one ActiveX DLL or ActiveX EXE and four client programs using these components, you have to recompile every single client, even if the component they are using is not the one you changed. This may be OK once in a while, but what about when you are developing and testing? What if you are deploying the application in a company where it would be difficult to change every single client?

Languages like C++ enable you to take full control over the interface GUIDs. Although that means you have to know when to change them, it also means you do not have to change all the GUIDs each time you make a change to a single interface. In C++, project interfaces are defined in IDL (Interface Definition Language), a language similar to C++, for defining interfaces. In the past, the only way to take full control of your GUIDs in VB was to learn IDL and use it to define your interfaces, then compile the IDL into a type library and use it in your VB project.

Enter VB .NET

VB .NET does not build COM components. It builds a new type of component that can run in .NET's common language runtime (CLR). Think of CLR as a virtual machine. In actuality, it may work very differently from the Java Virtual Machine, but in concept it is a lot like Java. The approach in .NET is that VB .NET compiles your code to a high-level assembly language Microsoft developed called Intermediate Language (IL). The compiler turns your code to IL and wraps it inside a DLL or an EXE. Then, when you execute the EXE or load the DLL, the just-in-time compiler (JIT compiler) transforms your code to x86 machine code that the processor can run.


Check out O'Reilly's .NET Resource Center for the latest articles and books covering Microsoft's .NET technology.

Internally, your COM classes look little like .NET classes. However, Microsoft provides a tool that enables you to use .NET classes through COM. The way this tool works is that it produces type libraries that can be used from VB 6. If all you have in a VB .NET project is the definition of the interfaces, then the type library produced will not have any dependency on the compiled .NET code. That means you can use the type library and compile it into your code without your clients needing to install the .NET SDK. What's more, the type library produced is just like any other COM type library produced today, so it is very safe to use. To illustrate this technique, let's build a VB 6 project without using the technique, then build a VB .NET project, and then change the original VB 6 project so that it uses this technique.

Original VB 6 Project

Let's suppose that you have a banking application. In this application you have two classes: a checking class and a savings class. A good design for such an application would be to separate the methods these two classes have in common into a single interface called IAccount. The following code shows the definition of the IAccount interface:

'VB 6 IAccount interface
Public Sub MakeDeposit(ByVal Amount As Currency)
End Sub
Public Property Get Balance() As Currency
End Property

In VB 6, interfaces are declared inside class modules. In this case, the class module would be called IAccount. Interface methods do not have code, just the definition of the methods. Also, the class's instancing property is normally set to 2--PublicNotCreatable, to let client programs know that the class is not a creatable entity. Interfaces serve as a way to communicate with the functionality of a class. After defining the interface, you would implement it in a concrete class such as Checking and Savings. The following code shows what the Checking class may look like:

'VB 6 Checking class
Implements IAccount
Private m_Balance As Currency

Public Sub IAccount_MakeDeposit(ByVal Amount As Currency)
    m_Balance = m_Balance + Amount
End Sub

Public Property Get IAccount_Balance() As Currency
    IAccount_Balance = m_Balance
End Property

The Checking class uses the Implements command to adopt the IAccount interface. To use the interface, a client program would declare a variable as IAccount and use New to create an instance of Checking as follows:

Dim Acct As IAccount
Set Acct = New Checking
Call Acct.MakeDeposit(5000)
Msgbox Acct.Balance

Notice that the client program uses the Checking class through the IAccount interface. The client code above has a dependency on the IAccount interface and on the Checking class. If your server project has a number of interfaces, then each client program would have dependencies on one or more of those interfaces, and on one or more of the concrete classes that implement them. If you were to change one of the methods in any of the interfaces, then VB 6 would change the GUIDs to all the interfaces and every client program would stop working. Let's see how VB .NET can help manage those GUIDs more efficiently.

VB .NET Project

As a replacement for IDL, you can use VB .NET to get more control over your interfaces. Plus, because VB .NET is similar to VB 6, you do not have to learn a different syntax. Let's begin a VB .NET project; you should see the dialog box in Figure 1.

Figure 1. New project dialog box
Figure 1. New project dialog box

Choose the class library project. You can name the new project BankInterfaces. When you create a new library project, VB will create a class file named class1.vb. This file contains some default code for the definition of a class: Class1. You can delete all the default code and define your IAccount interface as follows:

'VB .NET Interface
Public Interface IAccount
    Sub MakeDeposit(ByVal Amount As Decimal)
    ReadOnly Property Balance() As Decimal
End Interface

Rename the class1.vb file to Account.vb. The next step is to assign GUIDs to the interface and to the project. This is done using the GuidAttribute. An attribute is a class that can be used to add information to an assembly, a module, an interface, a class, a field, a method, or a parameter in a method. The following code shows the interface code with attributes:

Imports System.Runtime.InteropServices

<Guid("1393732E-8D27-431a-A180-8EDA0E4499E2")> _
Public Interface IAccount
    Sub MakeDeposit(<MarshalAs(UnmanagedType.Currency)> 
        ByVal Amount As Decimal)
    ReadOnly Property Balance() As 
        <MarshalAs(UnmanagedType.Currency)> Decimal
End Interface

As you can see, the code above declares the IAccount interface as it did before but uses two attributes throughout the definition. The first attribute is the Guid attribute before the declaration. The namespace-qualified name (or the full name) of this attribute is System.Runtime.InteropServices.GuidAttribute. What enables you to omit the namespace name of the attribute is the statement Imports System.Runtime.InteropServices at the beginning of the file; what enables you to use the name Guid instead of GuidAttribute is the fact that when you use an attribute class name you can omit the "Attribute" part of the name. Thus, the class name GuidAttribute becomes Guid when used as an attribute.

The Guid attribute, at the interface level, will produce an interface ID (IID) when the type library is generated. The GUID for the interface was not constructed by hand. To come up with the numbers you must use a tool called guidgen.exe. This tool is automatically installed when you install Visual Studio 6. Figure 2 shows the guidgen.exe program interface.

Figure 2. guidgen.exe
Figure 2. guidgen.exe

There is a second instance of the Guid attribute in the code. However, it was the wizard that added the second instance automatically. It is in the file AssemblyInfo.vb. If you examine the code in that file you will find a line like the following:

<Assembly: Guid("8680B180-6BF8-4CCE-A4FC-E1A30ADA35FF")> 

A full discussion of the term assembly is beyond the scope of this article, but for now think of the assembly as the project. Putting the Guid attribute at the assembly level enables you to assign the LIBID for the type library you will generate.

The second attribute that you see in the definition of the interface is the MarshalAs attribute. The MarshalAs attribute is necessary because the datatype Decimal has a number of possible interpretations in the COM world. (By default Decimal gets converted to a wide character string.) The MarshalAs attribute enables you to specify the correct type conversion. If you are wondering how in the world one figures out when to use MarshalAs and when not to, it is not as hard as it first seems. Instead of declaring the interface in VB .NET and exporting it for VB 6, declare the interface in VB 6 and import it into VB .NET. There will be more information about this later in the article.

Once you have the source code in place you can build the project choosing Build from the Project menu. The resulting DLL will be called BankInterface.DLL and you can find it in the Project\Bin directory.

Creating the Type Library

When you build a VB .NET library project, you create what is known in the run-time as an assembly. Assemblies are not COM servers, and that means that they do not have embedded type libraries. However, the Microsoft .NET SDK ships with a tool called tlbexp.exe. This tool can read an assembly and create a type library from the definitions in the assembly. The best way to do this is to locate the DLL in a command prompt and run the tlbexp.exe program using BankInterfaces.DLL as the command-line parameter. From the command line enter the following:

tlbexp BankInterfaces.dll

Figure 3 shows the output generated from running tlbexp.

Figure 3. Running tlbexp to generate a type 
library
Figure 3. Running tlbexp to generate a type library

If you look at the files in the directory you will see that there is a new file called BankInterfaces.tlb. To use this file in your VB 6 project you must first register the type library with a command available in Visual Studio 6 called regtlib.exe. From the command line enter the following:

regtlib BankInterfaces.tlb

Once you register the type library file, you can incorporate it in your VB 6 project using the Project Reference dialog. In VB 6 choose References from the Project menu, then select BankInterfaces from the list as shown in Figure 4.

Figure 4. Project References dialog
Figure 4. Project References dialog

The tlb file contains a declaration of the IAccount interface. With the BankInterfaces reference in place, you can get rid of the IAccount class in the VB project. The Checking and Savings classes can be used as before. You can then build the VB 6 COM server and modify the client project slightly.

Reversing the Process

Earlier I mentioned that certain .NET datatypes have different interpretations in the COM world, and that to specify the conversion type you must use the MarshalAs attribute. I also mentioned that an easy way to know what datatypes required this attribute was to reverse the process. Reversing the process means taking an existing type library and generating an assembly that can be used in .NET. With this mechanism you can examine how VB types translate to .NET and which types require special handling.


For more in-depth coverage of Microsoft's .NET Framework, visit O'Reilly's .NET DevCenter.

Reversing the process is done with a tool called tlbimp.exe, also included in the .NET SDK. Tlbimp takes a type library file, or a DLL that contains a type library, and generates an assembly that can be used from .NET. Using that approach, I've created a VB type library that contains nearly every type that a developer may use in defining an interface, and created a .NET assembly from it. The results are described below:

VB 6 Type   .NET Type Integer   Short Single   Single Byte   Byte Variant   MarshalAs(UnmanagedType.Struct) Object Long   Integer Double   Double Currency   MarshalAs(UnmanagedType.Currency) Decimal String   MarshalAs(UnmanagedType.BStr) String Boolean   Boolean Date   Date Object   MarshalAs(UnmanagedType.IDispatch) Object Array*   <MarshalAs(UnmanagedType.SafeArray,
SafeArraySubType:=UnmanagedType.*enter type here*)>

*An example of an Array declaration is the following: Sub MyMethod(ByRef strs() As String). The strs parameter in the MyMethod declaration is of type SafeArray(Bstr). A SafeArray(Bstr) is an array of strings. To specify a SafeArray parameter you use the MarshalAs attribute passing SafeArray as the type, then adding SafeArraySubType:= the type of theSafeArray. In the case of the MyMethod declaration, the second parameter to the attribute would be SafeArraySubType:=UnmanagedType.Bstr.

New Client Version

The VB 6 client code can remain mostly as is, however, you must reference the new type library as well, like you did in the server project. Also, to completely get rid of any dependencies the client project has on the VB 6 generated GUIDs, you must change the New command in the code to use CreateObject instead, as seen below. (Changing from New to CreateObject eliminates the dependency on the CLSIDs.)

Dim Acct As IAccount
Set Acct = CreateObject("BankServer.Checking")
Call Acct.MakeDeposit(5000)
Msgbox Acct.Balance

With the VB .NET-generated type library and the CreateObject command you now have full control over GUIDs. That means you no longer have to worry about how you set your version compatibility project--no more worrying about Binary Compatibility, Project Compatibility, or No Compatibility on the server side. It does mean, however, that you must follow one important versioning guideline.

Versioning GUIDs

With full control comes more responsibility. While you are developing, it is not necessary to follow COM rules to the letter. You may change, add, or delete a method and keep the same GUID. Once you release the server and client to the outside world, it is a different story--you must follow the COM versioning golden rule:

Any time you need to change a method in an interface you should create a new interface and assign to the new interface a new GUID.

For example, let's suppose the requirements change and your company now needs an extra parameter in the MakeDeposit method, like AmountAvailable. Instead of modifying the existing method (which would break existing clients) you should add a new interface to the VB .NET project called IAccount2 and then implement both IAccount and IAccount2 in the Checking class. The following code shows the definition of the IAccount2 interface in the VB .NET project:

Imports System.Runtime.InteropServices

<Guid("1393732E-8D27-431a-A180-8EDA0E4499E2")>  _
Public Interface IAccount
    Sub MakeDeposit(<MarshalAs(UnmanagedType.Currency)> 
        ByVal Amount As Decimal)
    ReadOnly Property Balance() As 
        <MarshalAs(UnmanagedType.Currency)>  Decimal
End Interface

<Guid("1393732E-8D27-431a-A180-8EDA0E4499E2")> _
Public Interface IAccount2
    Sub MakeDeposit(<MarshalAs(UnmanagedType.Currency)> 
        ByVal Amount As Decimal, 
        <MarshalAs(UnmanagedType.Currency)> 
        ByVal AmountAvailable As Decimal)
    ReadOnly Property Balance() As 
        <MarshalAs(UnmanagedType.Currency)>  Decimal
End Interface

Notice that the code uses a different GUID for the IAccount2 interface. Of course you would have to save the project, rebuild it and re-export the type library with tlbexp.exe. Then you would modify the server code as follows:

'VB 6 Checking class
Implements IAccount
Implements IAccount2

Private m_Balance As Currency
Private m_Available As Currency

Public Sub IAccount_MakeDeposit(ByVal Amount As Currency)
    m_Balance = m_Balance + Amount
End Sub

Public Property Get IAccount_Balance() As Currency
    Balance = m_Balance
End Property


Public Sub IAccount2_MakeDeposit(ByVal Amount As Currency, ByVal 
AmountAvailable As Currency)
    m_Balance = m_Balance + Amount
    m_Available = m_Available + AmountAvailable
End Sub

Public Property Get IAccount2_Balance() As Currency
    IAccount2_Balance = IAccount_Balance
End Property

Notice that the code above implements both the IAccount interface and the IAccount2 interface. As a result you must implement the methods of each interface. Even though it looks like you are duplicating code, this approach will guarantee that your existing client code will continue to run smoothly. What's more, this approach will give you a really good excuse to start using VB .NET to solve a very serious VB 6 problem.

To learn more about versioning components and transitioning to .NET, see COM+ Programming with Visual Basic.


Jose Mojica is an instructor and researcher at DevelopMentor, a company that's gained an international reputation for its experience with COM and COM+. He teaches various courses that focus on enterprise development in COM+, IIS, .NET, and Visual Basic. Before joining DevelopMentor, Jose was a consultant at IBM, writing DCOM servers that performed speech recognition, and creating ActiveX controls in ATL for the ViaVoice SDK. He has worked with Visual Basic since Version 1.0. Jose is the author of Building ActiveX Controls with Visual Basic 5.0 and coauthor of Programming Internet Controls and Distributed Applications for Visual C++ 6.0 MCSD Training Kit.


O'Reilly & Associates recently released (June 2001) COM+ Programming with Visual Basic.