Introduction
This article has the intention to help teams interested in migrating VB6 applications to .NET (C# or VB.NET) or improve VB6 applications with .NET modern resources. The full migration of a VB6 project to .NET can be costly and complex. The approach described here allows you to convert small pieces of code at time. Another possibility is not to convert any code, but just add the new requirements directly in .NET avoiding increase the legacy application.
Background
There are several articles helping to create .NET user controls to use in VB6 applications. This approach simplifies the process with a generic COM Assembly developed in C# that uses reflection to call any another DLL that we want. With this generic DLL, we just need one registration in the OS and encapsulate a lot of complex code. Not having the need to register several DLL in the OS is useful when you have an environment with several machines.
Using the Code
Creating a COM Assembly
In Visual Studio, create a new Class Library project. In the project Properties, click on the Application tab, click on the Assembly Information button, check the option Make assembly COM-Visible. In the Build tab, check the option Register for COM interop.
Add a new class CSharpInteropService.cs to the project.
The interface below exposes an event used to invoke an action in the VB6 application. It allows to open a VB6 form that hasn't been converted yet.
[ComVisible(true), Guid(LibraryInvoke.EventsId), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ILibraryInvokeEvent
{
[DispId(1)]
void MessageEvent(string message);
}
The interface below exposes the method used to call a DLL.
[ComVisible(true), Guid(LibraryInvoke.InterfaceId)]
public interface ILibraryInvoke
{
[DispId(1)]
object[] GenericInvoke(string dllFile, string className, string methodName, object[] parameters);
}
The class below is the implementation of the interfaces. GenericInvoke
receives the DLL full patch, class name and method name that will be invoked using reflection. You can send an array of parameters. At the end, it is going to return an array. GenericInvoke
expects a method called MessageEvent
in the DLL target to create a delegate with OnMessageEvent
. You need to declare MessageEvent
in the target only if you want to communicate back with VB6.
[ComVisible(true), Guid(LibraryInvoke.ClassId)]
[ComSourceInterfaces("CSharpInteropService.ILibraryInvokeEvent")]
[ComClass(LibraryInvoke.ClassId, LibraryInvoke.InterfaceId, LibraryInvoke.EventsId)]
public class LibraryInvoke : ILibraryInvoke
{
public const string ClassId = "3D853E7B-01DA-4944-8E65-5E36B501E889";
public const string InterfaceId = "CB344AD3-88B2-47D8-86F1-20EEFAF6BAE8";
public const string EventsId = "5E16F11C-2E1D-4B35-B190-E752B283260A";
public delegate void MessageHandler(string message);
public event MessageHandler MessageEvent;
public object[] GenericInvoke(string dllFile, string className,
string methodName, object[] parameters)
{
Assembly dll = Assembly.LoadFrom(dllFile);
Type classType = dll.GetType(className);
object classInstance = Activator.CreateInstance(classType);
MethodInfo classMethod = classType.GetMethod(methodName);
EventInfo eventMessageEvent = classType.GetEvent
("MessageEvent", BindingFlags.NonPublic | BindingFlags.Static);
if (eventMessageEvent != null)
{
Type typeMessageEvent = eventMessageEvent.EventHandlerType;
MethodInfo handler = typeof(LibraryInvoke).GetMethod
("OnMessageEvent", BindingFlags.NonPublic | BindingFlags.Instance);
Delegate del = Delegate.CreateDelegate(typeMessageEvent, this, handler);
MethodInfo addHandler = eventMessageEvent.GetAddMethod(true);
Object[] addHandlerArgs = { del };
addHandler.Invoke(classInstance, addHandlerArgs);
}
return (object[])classMethod.Invoke(classInstance, parameters);
}
private void OnMessageEvent(string message)
{
MessageEvent?.Invoke(message);
}
}
Registering the COM Assembly
After building the project, you will get a DLL and TLB file. You need to register this assembly using the RegAsm.exe tool (32 bits). This tool is located in your .NET Framework version in C:\Windows\Microsoft.NET\Framework.
C:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe /codebase
"C:\Temp\CSharpInterop\CSharpInteropService\CSharpInteropService\bin\Debug\CSharpInteropService.dll"
/tlb:"C:\Temp\CSharpInterop\CSharpInteropService\CSharpInteropService\bin\Debug\CSharpInteropService.tlb"
To unregister use /u
instead of /codebase
.
If you try registering CSharpInteropService.dll from a network location and receive an error, it may be necessary to include the following lines in the regasm.exe.config inside the configuration
key:
<runtime>
<loadFromRemoteSources enabled="true"/>
</runtime>
Invoking a DLL
With the assembly registered, you just need to select it at VB6 project reference. The code below shows how to make a basic call.
Dim param(0) As Variant
param(0) = Me.hWnd
Dim load As New LibraryInvoke
Set CSharpInteropServiceEvents = load
load.GenericInvoke "C:\Temp\CSharpInterop\ClassLibrary1\ClassLibrary1\bin\Debug\ClassLibrary1.dll",
"ClassLibrary1.Class1", "ShowFormParent", param
The C# DLL
[DllImport("user32.dll")]
public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
public void ShowFormParent(long parent)
{
Form1 form = new Form1();
form.Show();
IntPtr p = new IntPtr(parent);
SetParent(form.Handle, p);
}
private delegate void MessageHandler(string message);
private static event MessageHandler MessageEvent = delegate { };
public static void OnMessageEvent(string message)
{
MessageEvent(message);
}
Network Environment
If you try to load an assembly from a network location and receive an exception (System.IO.FileLoadException
), try to create a config file for the VB6 executable. In the case of my sample project, I create a Project1.exe.config for the Project1.exe with the following content:
="1.0"="utf-8"
<configuration>
<runtime>
<loadFromRemoteSources enabled="true"/>
</runtime>
</configuration>
Limitations
In the current stage, there is a limitation when a C# form is opened with ShowDialog()
. In this case, the event in VB6 is not invoked when the form is open, but before opening the form and after closing it, the communication works.