Calling .NET Code from COM

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.

  1. Select StartProgram FilesVisual Studio .NETVisual Studio .NET ToolsVisual Studio .NET Command Prompt. This step will open a command prompt with the right environment variables , including path information to tlbexp.exe ( C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Bin ) and regasm.exe (C:\ WINNT\Microsoft.NET\Framework\v1.0.3705 ).

  2. Change directories to the bin folder containing your .NET assembly. (In our example, this is the folder containing TemperatureConverter.dll .)

  3. Type tlbexp assemblyname and press Enter. (For our example, the command is tlbexp TemperatureConverter.dll .) This will create a file with the same name and a .tlb extension. (The .tlb file contains the IDL, which you can explore with OleView.exe .) Note that you can skip the tlbexp.exe command (this step) if you use the /tlb switch with regasm.exe .

  4. Next run regasm assemblyname /tlb / codebase . (For our example, the command is regasm TemperatureConverter.dll /tlb /codebase .)

NOTE

When you export a type library and register that library for COM Interop, you will get the following warning:

RegAsm warning : Registering an unsigned assembly with /codebase can cause your assembly to interfere with other applications that may be installed on the same computer. The /codebase switch is intended to be used with signed assemblies. Please give your assembly a strong name and re-register it.

The /codebase switch tells regasm.exe to store path information that helps locate your registered assemblies. However, in this case you can ignore the error. Strong names are for registering public assemblies in the Global Assembly Cache (GAC). You should put assemblies in the GAC only if you want everyone to be able to use them. This is not necessarily the case with COM Interop assemblies; more likely you want only your application to use them, so don't put them in the GAC.

However, if you do want to make your assembly public and register it in the GAC, use the sn.exe utility to make a strong name key file. Associate that key with your assembly in the assemblyinfo.vb file using an assembly attribute (the attribute is already there) and use gacutil.exe to place the assembly into the GAC. Again, do this only if you want everyone to know about and share the assembly.

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

NOTE

If any open clients are using the type library and assembly, you may need to close them before unregistering, deleting, or modifying the assembly and type library. Remember, this is COM now. As a result we will need to unregister the assembly if we plan to make modifications to it in the same location from which it was registered. To unregister the assembly, type regasm TemperatureConverter.dll /unregister at the command prompt. Our assembly will now show up in developer tools like Visual Studio 6. For example, TemperatureConverter.tlb will be listed in the VB6 References dialog as TemperatureConverter .

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.

graphics/07fig05.gif

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.

  1. Create a new .NET class library project.

  2. Delete the default Class1.vb file.

  3. Select FileAdd New Item.

  4. In the templates list, select the COM Class template item (Figure 7.6).

    Figure 7.6. The COM Class template makes it easy to export a .NET source to COM.

    graphics/07fig06.gif

  5. Add a simple method that returns a string, or type in the ToFahrenheit function used earlier in this chapter.

  6. Build the code (shown in Listing 7.11).

  7. Open VB6. Add a reference to the type library with the same name as the .NET assembly. (In my example the solution was named SimpleComExample.sln and the assembly was named SimpleComExample.dll ; thus the type library is named SimpleComExample.tlb .)

  8. Declare an instance of the class using the library.class name syntax.

  9. You will see and be able to invoke the method defined in the COM Class template.

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.



Visual Basic. NET Power Coding
Visual Basic(R) .NET Power Coding
ISBN: 0672324075
EAN: 2147483647
Year: 2005
Pages: 215
Authors: Paul Kimmel

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net