Tuesday, August 31, 2010

How to make a C# class available to VB6

At my company, we have a legacy application written in VB6 that still has a lot of life left in it.  Converting this application to C# is a daunting task, and we simply don't have the resources to do all in one step.

What we've decided to do is use C# to write new class objects that we need and use these from VB6.  As we get time and find that we can make improvements to the VB6 versions of existing classes, we'll replace those objects with new objects written in C#.  We'll try to use C# User Controls for new GUI elements.  What this does is allow us to develop new components in C# and slowly migrate existing code into C#, all while still releasing new versions.  As we get more pieces written in C# we can later make the full transition and move away from VB6 completely.

For this to work, we need to be able to do two things:
  1. Create class objects in C# that have properties, methods, and events that we can use from VB6.
  2. Create user controls in C# that we can place on VB6 forms, with properties, methods, and events.

I found that there are tons of web pages that talk about COM Interop from .NET, but very few that have step-by-step examples of what you need to do to actually get it to work.

For my first entry, I'll go through the steps for making a COM object in C# that you can directly use from VB6, written in Visual Studio 2008.  If there's interest, I'll step through how we're creating UserControls in C# that we can put on VB6 forms.





Create the C# Project

  1. Open VisualStudio 2008 and create a new C# Class Library Project. Change the name to something meaningful. The project name, just as in VB6, will become the first part of the ProgID. In this example I’ll use CSharpVB6Test. Click Ok to create your project.




  1. Right-click on the Project note of the Solution Explorer, click “Properties”, click the “Build” tab, and check the “Register for COM interop” checkbox.
  1. Click on the Signing tab, check the “Sign the assembly” checkbox. Select “<New…>” from the dropdown list, put the project name (or any other string) in the key file name box, and optionally enter a password. If you choose not to use a password (I didn’t for this sample), uncheck that box.



  1. Click “OK” and close the Project Properties tab.
  2. Properties & Methods: Right-click the project in the Solution explorer, Click “Add”, “New Item”, select “Interface” as the type, and give it a name. For this example I’ll use “IMyInterface.cs”. Click the “Add” button.
  3. At the top of the new interface file, add the using statement for Interop services:

using System.Runtime.InteropServices;

  1. Above your interface declaration, add the following two lines:

[ComVisible(true)]
[Guid("########-####-####-####-############")]

  1. Run the guidgen.exe program (Start -> All Programs -> Microsoft Visual Studio 2008 -> Visual Studio Tools -> Visual Studio 2008 Command Prompt, then "guidgen" at the command prompt). Click New GUID, select the Registry Format, and click the Copy button. Replace the guid text with the text in your clipboard, removing the braces {} from the guid.
  2. Add the “public” keyword to the interface.

public interface IMyInterface

  1. Define your interface. For this example I’ll declare a method “MyMethod” that returns a string. For each method, you need to add a COM dispatch identifier. These can be sequential numbers:
[DispId(1)]
string MyMethod();

  1. The final interface then looks something like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Runtime.InteropServices;

namespace CSharpVB6Test
{
   [ComVisible(true)]
   [Guid("########-####-####-####-############")]
   public interface IMyInterface
   {
      [DispId(1)]
      string MyMethod();
   }
}

  1. Events: Right-click the project in the Solution explorer, Click “Add”, “New Item”, select “Interface” as the type, and give it a name. For this example I’ll use “IMyEvents.cs”. Click the “Add” button.
  2. At the top of the new interface file, add the using statement for Interop services:

using System.Runtime.InteropServices;

  1. Above your interface declaration, add the following three lines:

[ComVisible(true)]
[Guid("########-####-####-####-############")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]

  1. Run the guidgen.exe program (Start -> All Programs -> Microsoft Visual Studio 2008 -> Visual Studio Tools -> Visual Studio 2008 Command Prompt, then "guidgen" at the command prompt). Click New GUID, select the Registry Format, and click the Copy button. Replace the guid text with the text in your clipboard, removing the braces {} from the guid.
  2. Add the “public” keyword to the interface.

public interface IMyEvents

  1. Define your events. For this example I’ll declare a method “MyStatusEvent” that passes a string. For each event, you need to add a COM dispatch identifier. These can be sequential numbers:
[DispId(1)]
void MyStatusEvent(string status);

  1. The final event interface then looks something like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Runtime.InteropServices;


namespace CSharpVB6Test2
{
   [ComVisible(true)]
   [Guid("########-####-####-####-############")]
   [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
   public interface IMyEvents
   {
      [DispId(1)]
      void MyStatusEvent(string status);
   }
}

  1. Right-click the Class1.cs node in the Solution explorer, add rename it to something meaningful. For this example I’ll rename it to MyClass.cs. Double click the file to open the code window.
  2. At the top of the class file, add the using statement for Interop services:

using System.Runtime.InteropServices;

  1. Above your class declaration, add the following four lines:

[ComVisible(true)]
[Guid("########-####-####-####-############")]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(IMyEvents))]

  1. Run the guidgen.exe program (Start -> All Programs -> Microsoft Visual Studio 2008 -> Visual Studio Tools -> Visual Studio 2008 Command Prompt, then "guidgen" at the command prompt). Click New GUID, select the Registry Format, and click the Copy button. Replace the guid text with the text in your clipboard, removing the braces {} from the guid.
  2. Add a public event handler and a public event that matches your event declaration in the event interface class. You’ll use this event within your code to raise events to VB6.
public delegate void MyStatusEventHandler(string status);
public event MyStatusEventHandler MyStatusEvent;
  1. Add the properties/methods interface to the class definition:

public class MyClass : IMyInterface

  1. Hover over [IMyInterface] until you have the dropdown menu option, and select ‘Implement Interface IMyInterface:




  1. Replace the “throw new NotImplementedException()” to raise our event and return some text:

#region IMyInterface Members

public string MyMethod()
{
   if (MyStatusEvent != null)
   {
      MyStatusEvent("some status message");
   }
   return "it works!";
}

#endregion

  1. The final test class will look something like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Runtime.InteropServices;

namespace CSharpVB6Test2
{
   [ComVisible(true)]
   [Guid("########-####-####-####-############")]
   [ClassInterface(ClassInterfaceType.None)]
   [ComSourceInterfaces(typeof(IMyEvents))]
   public class MyClass : IMyInterface
   {
      public delegate void MyStatusEventHandler(string status);
      public event MyStatusEventHandler MyStatusEvent;

      #region IMyInterface Members

      public string MyMethod()
      {
         if (MyStatusEvent != null)
         {
            MyStatusEvent("some status message");
         }
         return "It works!";
      }

      #endregion

   }
}

  1. Click “File” -> “Save All”, specify a directory in which to save your solution, and click “Save”
  2. Build your solution: “Build” -> “Build Solution”
  3. Open a command prompt (Start -> Run -> cmd.exe) and change directory to the Release folder where your new dll is stored.




  1. Run the regasm.exe command, adding an option to generate a type library against your new dll.



    • regasm.exe CSharpVB6Test.dll /tlb
You should see something like this:

Microsoft (R) .NET Framework Assembly Registration Utility 2.0.50727.3053
Copyright (C) Microsoft Corporation 1998-2004. All rights reserved.

Types registered successfully
Assembly exported to 'C:\Documents and Settings\rphillips\My Documents\Visual Studio 2008\Projects\CSharpVB6Test\CSharpVB6Test\bin\Release\CSharpVB6Test.tlb', and the type library was registered successfully

  1. Open a new VB6 “Standard EXE” project.
  2. Open “Project” -> “References”, and navigate to your project name in the list. Check it and click OK.




  1. You can verify that the interfaces worked properly by looking at the object browser. You should see the methods, properties, and events declared in the interface classes.


  1. In your code, create a new instance of the C# class object with events. Add a MsgBox to the event and test the method.

Option Explicit

Dim WithEvents oCSharpObject As CSharpVBTest.MyClass

Private Sub Form_Load()
    Set oCSharpObject = New CSharpVBTest.MyClass
    Debug.Print "Method results: " & oCSharpObject.MyMethod
End Sub

Private Sub Form_Unload(Cancel As Integer)
    Set oCSharpObject = Nothing
End Sub

Private Sub oCSharpObject_MyStatusEvent(ByVal status As String)
    MsgBox status
End Sub

  1. Click run and see that your C# object works.

Some things to watch for

Making changes to your C# DLL

I’ve noticed that in some cases, new methods may not be added automatically or old methods may not be removed as they should. When making new versions of your C# DLL you should always unregister the COM properties prior to recompiling. This is done by adding the “/unregister” option to the regasm.exe call.
  • regasm /unregister CSharpVB6Test.dll /tlb:CSharpVB6Test.tlb
Change the C# code, rebuild, and register again. I saw this happening another time, and it seemed to fix itself when I increased the assembly build version. Good luck with this one ;)

Unable to copy file "obj\Release\???.dll" to "bin\Release\???.dll". The process cannot access the file because it is being used by another process.

When you’ve made a reference to the DLL from VB6, you won’t be able to recompile a new DLL until you close the VB6 IDE.

My methods and properties aren’t available through Intellisense.

Something didn’t go right in the C# class declarations. Go back and review each of the steps and make sure you didn’t miss something. This can be anything from not making the interface public to forgetting one of the class modifiers.

I see methods like “Equals”, “GetHashCode”, “GetType”, “MemberwiseClone”, or “ToString” and not the interface methods I declared.

See the above note.

What if I have implemented more than one interface in my class object?

This gives interesting results. The first interface that your class object implements will become its default interface. The other interface(s) will then be added to the COM wrapper as class definitions that can’t be created (similar to creating an ActiveX DLL class and setting the Instancing property to “2 – PublicNotCreatable”. You can then create the C# object using the default interface, and then cast it to the other interfaces. For instance, if I added a second interface to my C# project my VB6 code might look like this:
Option Explicit

Private Sub Form_Load()
   Dim o As New CSharpVB6Test.MyClass
   Dim o2 As CSharpVB6Test.IAnotherInterface
   Set o2 = o
   MsgBox o2.MyOtherMethod
End Sub


I’m getting an error: Automation error - The system cannot find the file specified.

I’ve usually encountered this when changing the C# class definition. Remove the reference, unregister the assembly, recompile, re-register, add the reference. Check that the Dispatch event name matches the name in the class object. This has only happened a few times and I can’t pinpoint a solution.. keep messing with it.

Cannot register assembly "..\CSharpVB6Test.dll" - access denied. Please make sure you're running the application as administrator. Access to the registry key 'HKEY_CLASSES_ROOT\CSharpVB6Test.MyClass' is denied.

User Access Control (UAC) prevents writing into this section of the registry unless you're the Administrator.  Run VS2008 as Administrator and rebuild.

1 comment: