It is likely that companies and programmers will eventually have enough code that COM-based clients will be calling .NET servers. I can imagine scenarios where individual programmers sneak in .NET parts as a bridge to the future for companies slow to switch to .NET or so they can use highly prized third-party solutions available only in .NET. Calling .NET from COM is supported by COM Interop. In this section, to be thorough, I included an example of .NET to COM Interop relationship, including the preparatory steps necessary to make the mechanics work.
Recall that metadata was incorporated in .NET assemblies to mitigate "DLL hell." This metadata consists of an assembly manifest that provides information about types and member names , playing the same role as COM type libraries. Using the assembly manifest information, .NET can generate a COM callable wrapper (CCW) that marshals calls from COM to .NET, quietly converting COM types to .NET types. The steps involved are roughly analogous to the steps of consuming a COM binary in .NET, but in reverse. To perform the reverse operation we use the tlbexp.exe tool to create the CCW and regasm.exe to make appropriate entries in the registry, allowing the COM clients to find the .NET servers.
The mechanics are the most important part of our example, so I will use the temperature conversion codethis time implemented in .NETto allow us to focus on the process rather than the code. (Note: If you want to export some .NET code to COM as quickly as possible, skip to the Exporting to COM Made Easy subsection.)
Creating a Test .NET Class Library
We can implement TemperatureConverter.sln as a class library in .NET. We are purposefully creating an assembly that contains a class that can be consumed by a COM client application. This is not the most thorough way to create the class library for this purpose; later we will add polish with some adornments.
COM doesn't know anything about parameterized constructors. Thus, we can have any constructors we like, but we must have a default constructor. Additionally, any properties, methods , fields, or events that we want exposed to COM must be public. By default, public members will be exposed to COM. Listing 7.7 offers a .NET implementation of the temperature converter containing the public constructor and the two conversion methods.
Listing 7.7 A .NET Implementation of the Converter Class for Use in a COM Client
1: Public Class Converter 2: 3: Public Sub New() 4: 5: End Sub 6: 7: Public Function ToFahrenheit( _ 8: ByVal Temperature As Double) As Double 9: 10: Return Temperature * (9 / 5) + 32 11: 12: End Function 13: 14: Public Function ToCelsius( _ 15: ByVal Temperature As Double) As Double 16: 17: Return (Temperature - 32) * 5 / 9 18: 19: End Function 20: 21: End Class
This class is simple enough to not require any explanation except that the empty default constructor is not explicitly required. Recall that VB .NET will create an empty default constructor for us if we fail to define one. The next thing we need to do is tell COM about our .NET assembly.
Exposing .NET Types to COM
The manifest information in a .NET assembly needs to be converted into IDL in a type library. The generated .tlb file plays the role of CCW. In addition, we have to tell the registry where to find the type library and .NET assembly. (Remember, we are going backward into DLL hell.) This is all accomplished with the command-line tools tlbexp.exe and regasm.exe , which both ship with .NET.
The tlbexp.exe utility reads the assembly manifest information of an assembly and generates the IDL. The regasm.exe tool adds entries in the registry for the assembly and the type library. You can automate these steps together in a batch file or .cmd file by running the tlbexp.exe utility first, following by regasm.exe . The following steps will make things easier.
Thus for our example we can run regasm TemperatureConverter. dll /tlb /codebase to create the type library and register the assembly in one step. The /tlb switch generates the type library, and the /codebase switch tells the registry about the physical file location of the .NET assembly. If we don't use the /codebase switch, COM won't be able to find our assembly. By default the COM client will look in the GAC, and we haven't taken any steps to put the assembly there. (That is, TemperatureConverter.dll isn't in the GAC, so we need to use the /codebase switch.)
Consuming .NET Assemblies in VB6
Consuming TemperatureConverter in VB6 is accomplished by adding a reference to TemperatureConverter.tlb in the References dialog (Figure 7.5) and writing code to instantiate instances of the Converter class. Keep in mind that the CCW makes the assembly look like a COM object; thus we use TemperatureConverter as if it were a COM object. Listing 7.8 contains some simple client code that consumes the .NET Converter class.
Listing 7.8 Using the .NET Server in VB6
Private Converter As TemperatureConverter.Converter Private Sub CommandToCelsius_Click() On Error GoTo Handler Text1.Text = Converter.ToCelsius(CDbl(Text1.Text)) Exit Sub Handler: MsgBox Err.Description End Sub Private Sub CommandToFahrenheit_Click() On Error GoTo Handler Text1.Text = Converter.ToFahrenheit(CDbl(Text1.Text)) Exit Sub Handler: MsgBox Err.Description End Sub Private Sub Form_Load() Set Converter = New TemperatureConverter.Converter End Sub
Figure 7.5. The type library for our .NET assembly in the VB6 References dialog.
From the listing you can see that the TemperatureConverter.Converter class is declared and used in the usual way.
Exposing .NET Delegates to COM
Visual Basic 6 takes care of a lot of housekeeping for us. We generally don't worry about IDL, type libraries, or interfaces like IDispatch , IUnknown , ISinkEvents , and IConnectionPoints . Visual Basic 6 takes care of these low-level elements of COM on our behalf . However, when we want to raise events in .NET sources and handle those events in COM sinks, we have to convert .NET delegates (events in .NET) to COM connection points (events in COM). This is managed by specifically declaring an interface that maps method signatures in an interface to event members in a class. We combine the code with attributes that tell tlbexp.exe what kind of IDL to generate.
Listing 7.9 provides an example of a .NET source that exposes delegates to COM by simulating connection points in .NET. After a description of this code, Listing 7.10 provides an example of a VB6 client consuming the .NET source.
Listing 7.9 Exposing .NET Events to COM by Simulating Connection Points
1: Imports System.Runtime.InteropServices 2: 3: Public Delegate Sub ConvertDelegate( _ 4: ByVal Temperature As Double) 5: 6: 7: <InterfaceType( _ 8: ComInterfaceType.InterfaceIsIDispatch)> _ 9: Public Interface TemperatureEvents 10: Sub ConvertToFahrenheit(ByVal Temperature As Double) 11: Sub ConvertToCelsius(ByVal Temperature As Double) 12: End Interface 13: 14: 15: <ComSourceInterfaces( _ 16: "TemperatureConverter.TemperatureEvents")> _ 17: Public Class Converter 18: 19: Public Event ConvertToCelsius As ConvertDelegate 20: Public Event ConvertToFahrenheit As ConvertDelegate 21: 22: Public Function ToFahrenheit( _ 23: ByVal Temperature As Double) As Double 24: 25: OnConvertToFahrenheit(Temperature) 26: Return Temperature * (9 / 5) + 32 27: 28: End Function 29: 30: Public Function ToCelsius( _ 31: ByVal Temperature As Double) As Double 32: 33: OnConvertToCelsius(Temperature) 34: Return (Temperature - 32) * 5 / 9 35: 36: End Function 37: 38: Private Sub OnConvertToFahrenheit( _ 39: ByVal Temperature As Double) 40: RaiseEvent ConvertToFahrenheit(Temperature) 41: End Sub 42: 43: Private Sub OnConvertToCelsius( _ 44: ByVal Temperature As Double) 45: RaiseEvent ConvertToCelsius(Temperature) 46: End Sub 47: 48: End Class
Line 1 imports System.Runtime.InteropServices , which introduces Interop classes like the attributes we'll be using in the listing. Lines 3 and 4 declare a new delegate, ConvertDelegate , which accepts a Temperature value. We want to expose to VB6 clients those events that match the signature of ConvertDelegate ; thus we need to expose the events in a dispinterface that simulates COM connection points in IDL. This is accomplished by defining an interface in .NET, adding method signatures that match the events, and adorning the interface with the InterfaceTypeAttribute . The interface containing the two method signatures that become our event connection points is defined in lines 7 through 12. The InterfaceTypeAttribute is applied in lines 7 and 8; ComInterfaceType.InterfaceIsDispatch indicates that the COM interface needs to be exposed as a dispinterface.
Lines 15 through 48 implement the Converter class with the new events. We have two events: ConvertToCelsius and ConvertToFahrenheit . When ToCelsius is called, the ConvertToCelsius event is raised. ConvertToFahrenheit is raised when ToFahrenheit is called. The actual events are raised in the associated private methods (following a .NET convention).
The ComSourceInterfacesAttribute (lines 15 and 16) associates the event source interface TemperatureEvents in line 9 with the Converter class and the events in lines 19 and 20.
When we are ready to implement the COM client, the CCW takes care of mapping the .NET delegates ( ConvertToFahrenheit and ConvertToCelsius ) to the COM connection points on our behalf. Consequently the VB6 code is written as if we were using an event source that originated from COM. Listing 7.10 contains code that consumes the .NET event source by using the WithEvents statement as you'd expect.
Listing 7.10 A COM Client Consuming .NET Events
1: Private WithEvents Converter As TemperatureConverter.Converter 2: 3: Private Sub CommandToCelsius_Click() 4: On Error GoTo Handler 5: Text1.Text = Converter.ToCelsius(CDbl(Text1.Text)) 6: Exit Sub 7: Handler: 8: MsgBox Err.Description 9: 10: End Sub 11: 12: Private Sub CommandToFahrenheit_Click() 13: On Error GoTo Handler 14: Text1.Text = Converter.ToFahrenheit(CDbl(Text1.Text)) 15: Exit Sub 16: 17: Handler: 18: MsgBox Err.Description 19: 20: End Sub 21: 22: Private Sub Converter_ConvertToCelsius( _ 23: ByVal Temperature As Double) 24: 25: MsgBox "Converting " & Temperature & " to Celsius" 26: 27: End Sub 28: 29: Private Sub Converter_ConvertToFahrenheit( _ 30: ByVal Temperature As Double) 31: 32: MsgBox "Converting " & Temperature & " to Fahrenheit" 33: 34: End Sub 35: 36: Private Sub Form_Load() 37: Set Converter = New TemperatureConverter.Converter 38: End Sub
Recall that in VB6 we use a WithEvents statement when declaring a COM object to have the IDE expose the events in the Code Editor. If we pick the object from the Object drop-down list, the Code Editor will update the Procedure drop-down list to contain the event methods for us. If we select an event method, the Code Editor will stub out the event handler. The two event handlers are shown in Listing 7.10 in lines 22 through 34. The remaining code you have seen before.
If we were using a language like C++, we'd have to do more of the infrastructure work, but VB6 manages things like the connection points for us, and the CCW handles marshaling the data back and forth.
Applying Interop Attributes
You can employ several Interop attributes to manage how COM Interop behaves. I have included a few of them here to whet your appetite. Check the Visual Studio .NET help documentation for an exhaustive list.
The ComRegisterFunctionAttribute and ComUnregisterFunctionAttribute can be applied to a pair of shared methods that run code during the registration and unregistration processes. If you define one or the other method, you must also provide the symmetric operation. These two methods work in pairs, and regasm.exe will honk at youfailing to registerif you define a register function but no unregister function.
The ComVisibilityAttribute provides a way for you to conceal things from COM. By default public members are visible to COM. Initialize the ComVisibilityAttribute with False to make a public member invisible to COM. For example, lines 3 and 4 of Listing 7.9 define a delegate. .NET clients can use delegates directly, but the COM client won't be able to do anything with it. Thus we could apply the <ComVisible(False)> attribute to the delegate declaration to make it invisible to COM.
Finally there is the ClassInterfaceAttribute , which will generate an interface for your .NET classes based on the public managed types you define and the inherited types. <ClassInterface(ClassInterfaceType.AutoDual)> is good for testing since it saves you the time of explicitly declaring an interface for your class, but it can lead to versioning problems. When ClassInterfaceType.AutoDual is used, the tlbexp.exe utility generates an interface and an interface identifier (IID) for the interface. This permits clients to bind to a specific layout that will change as the class changes. Managed code won't be affected by the change because managed clients are talking directly to the class; unmanaged clients are talking to the class through the interface. If the underlying layout changes, the wrong behaviors may be invoked inadvertently. It is better to avoid using the ClassInterfaceAttribute and instead define a literal interface and implement that interface in the class you are exposing to COM.
Exporting to COM Made Easy
If you read the preceding subsections, you now know a lot about the manual process of COM-to-.NET Interop. If you skipped ahead because you are in a hurry, you have come to the right place. Earlier in the chapter I said that you could use tlbimp.exe to import a COM source or you could let the .NET IDE do it for you. The same is true for exporting a .NET source for use by COM. If you create a class library project, select FileAdd New Item, and pick COM Class from the list of templates, .NET will create a class that uses the ComClassAttribute . The .NET IDE will manage exporting the type library and registering the .NET source for use with COM when you compile your class library. All you have to do is focus on writing the code.
To try this shortcut follow these steps.
Listing 7.11 Using the COM Class Template to Expose a .NET Source to COM
1: <ComClass(ComClass1.ClassId, _ 2: ComClass1.InterfaceId, ComClass1.EventsId)> _ 3: Public Class ComClass1 4: 5: #Region "COM GUIDs" 6: ' These GUIDs provide the COM identity for this class 7: ' and its COM interfaces. If you change them, existing 8: ' clients will no longer be able to access the class. 9: Public Const ClassId As String = _ 10. "BD0327C9-6680-423C-9503-F6C90C97C301" 11: Public Const InterfaceId As String = _ 12: "DD916514-BE48-40C9-8E5E-486EB6522C47" 13: Public Const EventsId As String = _ 14: "3C7B3C24-3986-4C92-9A64-699D96067106" 15: #End Region 16: ' A creatable COM class must have a Public Sub New() 17: ' with no parameters, otherwise, the class will not be 18: ' registered in the COM registry and cannot be created 19: ' via CreateObject. 20: Public Sub New() 21: MyBase.New() 22: End Sub 23: 24: Public Function ToFahrenheit( _ 25: ByVal Temperature As Double) As Double 26: 27: Return Temperature * 9 / 5 + 32 28: End Function 29: 30: End Class
The only code I added was the function ToFahrenheit in lines 24 through 28. After building the class library containing this code, the .tlb file was generated and the necessary registry entries were created to make ComClass1 available to VB6. Pretty easy.
While a big application is likely to need a lot of manual tweaking, you can try to get as much mileage out of automated approaches as possible. I would use the COM Class template unless something specific prevented me from doing so.