The first example application (InteropDebug.sln and ComPrintUtils.vbp) consists of a VB .NET Windows Form project calling a VB 6.0 component to perform a specific printing task. Rather than invent a COM component for demonstration purposes, I decided to use a real-life component that has proved to be useful in the past and would be a little tricky to port over to VB .NET because of its extensive use of Win32 API calls.
Figure 11-1 shows the very simple user interface for this application. The single button shown on the .NET Windows Form calls a public method, PrintPictureToFitPage , in the VB 6.0 dynamic link library (DLL). This method takes a window handle as a parameter and then calls several private methods to print a sized and rotated graphic of the specified window.
The VB 6.0 component was originally written to handle the well-known problems with VB.Classic's PrintForm method, which only prints the client area of a form, refuses to print any form graphics, and doesn't make sensible decisions about sizing or rotating form graphics. The component was later expanded to print any specified window, the currently active window, or the desktop itself.
First you should build the ComPrintUtils component, which is a VB 6.0 ActiveX DLL. When you first load the ComPrintUtils.vbp project, you'll see a message warning you that the binary compatibility component doesn't exist. This warning appears because you haven't created the component yet. Go to the File ’ Make ComPrintUtils.dll menu option. Click the Options button on the Make Project dialog window, and go to the Compile tab, as shown in Figure 11-2. You must choose the Compile to Native Code option because the Visual Studio debugger doesn't understand p-code . You should also select the No Optimization and Create Symbolic Debug Info compilation options. The former option allows the debugger to map the executing program back to the source code accurately, just as the same option works when compiling a VB .NET application. The latter option ensures that the VB 6.0 compiler produces a debug symbol file, without which the binary-to-source code mapping can't occur.
Compiling the VB 6.0 component automatically registers it as far as COM is concerned , thus enabling VB .NET to see the component and its public interfaces. If your own COM component is for some reason not registered on the machine on which you wish to use it, then you should register it using regsvr32.exe from the command line.
Once you've created the VB 6.0 component, you should copy the binary to a project subfolder and then set the project's binary compatibility option to point at this binary-compatible version. You can do this using the Component tab on the dialog window you access by selecting the Project ’ ComPrintUtils properties menu item. Doing this ensures that all of the COM registry entries stay the same when you change and recompile the component.
The first major gotcha awaiting you is the fact that you need to install VC7++ before you can use the native debugger required for VB.Classic debugging. This is an unfortunate requirement because the VC7++ package that comes with Visual Studio is rather large, but I've been unable to find any way around this.
Now you can load the InteropDebug solution. To use the VB 6.0 component that you've just built in this solution, go to the Project ’ Add Reference menu option and click the COM tab. Find the ComPrintUtils component in the top window of the tab, double-click it to add the component to the solution, and then click the OK button. To confirm that the COM component has been added to the solution, go to the Solution Explorer window and look for the References item underneath the InteropDebug project. Underneath the References item should be a reference to the ComPrintUtils component, as shown in Figure 11-3. There should also be a reference to stdole , the COM automation library required for using any COM component.
Visual Studio has created a .NET Interop assembly called Interop.ComPrintUtils.dll in the \bin and \obj subfolders of the .NET solution. The CLR will make this component accessible to any .NET application by creating a Runtime Callable Wrapper (RCW). The RCW fools the .NET application into thinking that it's calling a .NET component rather than a COM component.
If you change any code within a VB 6.0 component, you should remove its reference from the VB .NET application (by right-clicking the reference and choosing Remove) before you add the reference again. This is the best way to ensure that the VB .NET application always refers to the correct version of the VB 6.0 component. If you don't do this, you might end up with mismatched source, binary, and debug symbols for the VB 6.0 component. This is because simply recompiling the VB 6.0 component won't cause a new Interop assembly to be created or allow the .NET application to recognize that the VB 6.0 source code has changed.
The final debugging preparation step is to switch on unmanaged debugging for the VB .NET project by right-clicking the project in the Solution Explorer window and going to the Configuration Properties ’ Debugging window. There you should select the check box marked Unmanaged code debugging. If you forget to select this setting, you can tear out your hair for hours wondering why your VB 6.0 breakpoints aren't being hit.
Set a breakpoint on line 62 of the FormInterop.vb code window, where the call is made to DebugTest.PrintPictureToFitPage , and press F5 to start the application running. When you click the single command button provided, you should land on the breakpoint that you've just set. If you then press F8 to step into the VB 6.0 component, a new source code window should open showing the VB 6.0 code, and the debugger should step into the VB 6.0 CaptureForm function. Once again, it's imperative that you've enabled unmanaged debugging in your VB .NET project ”without this, the debugger will simply step over any call to a VB 6.0 method. At this point, you can use the Visual Studio debugger to step back and forth quite freely between the managed and unmanaged worlds .
When Visual Studio knows where the VB 6.0 source code and debug symbols reside, it opens the source code window automatically as just discussed. More commonly, however, you need to inform the debugger exactly where the VB 6.0 source code and debug symbols can be found. For VB 6.0 source code, go to the solution's Properties ’ Common Properties ’ Debug Source Files dialog page as shown in Figure 11-4. Here you can specify the path (or paths) that contains the source code files. In a similar fashion, you can use the solution's Properties ’ Common Properties ’ Debug Symbol Files dialog page to specify the path that contains the VB 6.0 debug symbol files. Like VB .NET debug symbol files, these have a .pdb suffix.
You may notice that debugging of unmanaged code is very slow, sometimes taking 2 or 3 seconds to step from one line to another, and also that this performance problem can appear when debugging managed code if unmanaged debugging has been selected. Unfortunately, this is currently an unavoidable overhead when you do unmanaged debugging, so you'll have to live with it until Microsoft addresses the performance issue in a new release of the .NET Framework. C# has the same performance issue when working with unmanaged code.
It's worth taking a minute at this stage to compare the different ways that VB 6.0 and VB .NET view the public methods of a VB 6.0 component. If within the VB 6.0 IDE you examine the Declarations drop-down in the top right corner of the Source window, you can see the six methods declared by the VB 6.0 component. In the Visual Studio IDE, right-click ComPrintUtils on line 61 of the FormInterop.vb code window and choose the Go To Definition menu option. This should show the Object Browser window where you can see that the same six methods form the ComPrinting class and the _ComPrinting public interface. If you look closely at the method definitions, you can see one interesting point.
Where the VB 6.0 component declares a method argument as Long , VB .NET sees the same argument as Integer . This is because in .NET, an Integer is actually an Int32 , which represents a 32-bit integer. In VB 6.0, an Integer is a 16-bit data type and the Long data type is used instead to represent a 32-bit integer.
Another point to note is the call to InteropServices.Marshal.ReleaseComObject() in the VB .NET code. This method call takes place after the application has finished using the VB 6.0 component, but before the COM object variable is set to Nothing . The method decrements the RCW's COM reference count and is used for explicit control of the lifetime of a COM object used from managed code. You should always call this method if the COM component holds references to resources that need to be released in a timely manner or when references must be released in a specific order. In the absence of this call, you can find yourself debugging strange and intermittent resource errors, which is really no fun at all.
You might be tempted to run the VB 6.0 component from within its IDE and then jump into the VB 6.0 IDE from the Visual Studio IDE, in a similar manner to the traditional VB 6.0 approach of running two IDE sessions side by side and transferring smoothly between the two IDEs. However, running the Visual Studio and VB 6.0 IDEs side by side doesn't work. It might appear to work for a short time, but as soon as control transfers back from the VB 6.0 IDE to the Visual Studio IDE (and sometimes before), you'll see a major error and the whole house of cards will come tumbling down. Therefore, you need to compile the VB 6.0 component before you can debug it from Visual Studio. This is similar to the VB 6.0 approach of using a project group containing multiple projects within a single IDE session, with the VB .NET solution being the equivalent of the VB 6.0 project group .
So what happens if you really do need to debug the VB 6.0 component from within the VB 6.0 IDE, perhaps to take advantage of VB 6.0's capability to edit code during a debugging session? I've found a way to do this, but it only works until the first time that control is returned to the .NET application. After the first call into VB 6.0 and transfer back to VB .NET, the CLR triggers an exception, and you won't be able to continue debugging. If you can live with this restriction, you can try the following method.
First, build InteropDebug.sln as discussed previously. Then switch to the VB 6.0 IDE and go to the Project ’ ComPrintUtils properties menu option. In the resulting Project Properties dialog window, choose the Debugging tab and select the Start program option. The program that you need to launch the VB 6.0 component is, of course, the InteropDebug.exe application, so browse to the \InteropDebug\bin folder and double-click the executable.
Now place a breakpoint on the first statement of the VB 6.0 method that will be the first to be called, in this case the CaptureForm function in the ComPrinting class module. Finally, press F5 in the VB 6.0 IDE to start the debugging session. The VB .NET Windows Form should appear, and you can click the single command button. Now your VB 6.0 breakpoint should be hit. From here you can step through the code in your VB 6.0 component, at least until control returns back to VB .NET application. At this point you should see the standard Windows Forms dialog window warning you about an unhandled exception. Click the Continue button to just ignore the exception. The VB .NET application is now likely to be unstable, so you should close it at the first opportunity.
As I stated previously, this isn't an ideal way to debug VB 6.0 COM Interop, but it does at least allow you to use the VB 6.0 IDE when you really need to do so.