Problem
When I create a new application, I create the application in .NET. However, I do have many legacy applications that are still in production that were written with COM based languages (for example, VB6). To complicate matters further, many of the COM based applications were written as ActiveX EXE servers. I have multiple ActiveX EXE servers that talk to each other through their exposed COM interfaces. My goal is to migrate these applications to the .NET framework.
With .NET COM interoperability, it’s very easy to expose methods and properties to any COM compliant language such as VBScript or Visual Basic 6. In fact, with VB.NET applications and Visual Studio 2005, it becomes trivial: simply add a COM class to the VB.NET project and you’re done.
C# takes a little more effort to expose the assembly to COM – you need to create your own GUIDs and implement your own interfaces (you also need to use REGASM.exe to create a type library). Both .NET Class Libraries (DLLs) and Windows Applications (EXEs) can be exposed to COM. This article does not explain in detail how to expose methods and properties from a .NET assembly to COM. There are many great articles on CodeProject that explain this process in detail.
There is a problem that occurs when it comes time to invoke a .NET assembly through the exposed COM interface. It’s easy to invoke the assembly through the COM interface (for example, just use CreateObject(“…”)
in VBScript or use the “New
” keyword in VB6), however, you will quickly notice that there are problems:
- The COM application does not use the running instance of the .NET assemby.
Additionally, the COM application loads the .NET assembly in its same address space. In the case of VBScript, every time you execute the script that calls the exposed COM interface, a new instance of the .NET assembly is loaded. When the script ends, so does the .NET assembly.
Problem Summary
How do we expose a .NET EXE assembly to a COM compliant client application (such as VB6 or VBScript) and force the client application to use the running instance of the .NET EXE assembly? If there is not a running instance of the .NET EXE assembly, then the client application should invoke a new instance of the .NET assembly.
Essentially, we are asking the .NET EXE assembly to behave like an ActiveX EXE server (out-of-process server) that we can late/early bind to from any COM compliant language.
Background
You may be asking yourself, “Why would we need a .NET assembly to mimic the behavior of an ActiveX EXE server?” The answer: The need to force a .NET EXE assembly to behave like an ActiveX EXE server is useful for upgrading legacy ActiveX EXE server applications to .NET assemblies in phases. I believe that all VB6 Windows applications should be upgraded to .NET. However, many of us do not have the luxury of upgrading every legacy application to .NET at the same time; the upgrades have to occur in phases. Also, some of your legacy applications may be written so they are exposed to a scripting language (VBScript or JavaScript). How can we continue to execute these scripts against the new/upgraded .NET framework assemblies?
To emphasis my point, consider the following example: Application A and Application B are enterprise applications that are installed on thousands of computers. Each application was written as a VB6 ActiveX EXE server application and each application “talks” to the other application through their exposed COM interfaces. Application B will be upgraded to .NET first (and put into production) followed by the upgrade of Application A. The communication between Application A and Application B cannot be broken during the upgrade process:
Solutions
Again, we are faced with the problem of upgrading a legacy VB6 ActiveX EXE server application to .NET and avoiding breaking the communication channels exposed to other legacy VB6 ActiveX EXE server applications. There are a number of solutions that can be employed to solve this issue. One potential solution would involve using TCP/IP listeners in the .NET assembly to listen on a port for incoming requests from client applications. A VB6 client application could then use WinSockets to “talk” to the .NET assembly. However, this solution would require changes in all the legacy VB6 applications that need to communicate with the upgraded .NET EXE assembly (not to mention re-testing and re-deployment of the legacy applications to thousands of computers).
In this article, I will explore creating an ActiveX EXE wrapper around the .NET EXE assembly. The benefit of using a wrapper around the new .NET EXE assembly is that none of the other legacy VB6 ActiveX EXE applications need to be modified (if the other VB6 application doesn’t use late binding, you have to make sure the COM interfaces in the wrapper are the same as the legacy ActiveX EXE that is being replaced).
The ActiveX EXE wrapper will be responsible for:
- Launching the .NET EXE assembly. The .NET EXE assembly will be loaded in the same address space as the ActiveX EXE wrapper.
- The ActiveX EXE wrapper will communicate with the .NET EXE assembly through exposed COM interfaces in the assembly.
- The ActiveX wrapper will also have exposed properties and methods that can be called by other COM applications.
Essentially, the ActiveX EXE wrapper will “bubble” COM requests it receives to the .NET EXE assembly.
Step 1: Create the VB.NET application that exposes COM methods
To illustrate the concept of ActiveX EXE wrappers, here’s a simple example. The VB.NET Windows Application is called “Message Net” and its interface appears as follows:
This is a very simple application that receives string messages and displays them in a list box. The application is composed of a form (frmMain
) and two classes (App
and ComExpose
).
The App
class simply holds a static/shared reference to frmMain
:
Public Class App
Public Shared MainForm As frmMain
End Class
The frmMain
class obviously contains the code that creates the user interface. Additionally, frmMain
contains the following code:
Private Sub frmMain_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
If App.MainForm Is Nothing Then App.MainForm = Me
End Sub
And the button click code:
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
ListBox1.Items.Insert(0, DateTime.Now.ToLongTimeString() & _
" - Hello World! This was added internally")
End Sub
That’s it; this is a complete (although not a very useful) application. Next, we need to expose the list box through a COM interface. As previously mentioned, VB.NET projects in Visual Studio 2005 allow you to add “COM Classes” (in the Solution Explorer, right click on the project, “Add, New Item….” and select “COM Class”. An empty “COM Class” will be added to your project:
<ComClass(ComClass1.ClassId, ComClass1.InterfaceId, ComClass1.EventsId)> _
Public Class ComClass1
#Region "COM GUIDs"
Public Const ClassId As String = _
"3d9e2e6f-5bf2-4144-8790-4e5b0af104e9"
Public Const InterfaceId As String = _
"b89d94d5-2306-4afc-a3b5-e86a1958fb9d"
Public Const EventsId As String = _
"1e3a740b-f7a9-4d31-9a7d-db11210f0c01"
#End Region
Public Sub New()
MyBase.New()
End Sub
End Class
Any public methods and properties added to this class will be exposed to COM clients. Add the StartApplication()
and DisplayMessages()
methods to this new class
Public Sub StartApplication()
Dim frm As frmMain
frm = New frmMain()
App.MainForm = frm
frm.ListBox1.Items.Add(".Net application starting")
frm.ShowDialog()
End Sub
Public Sub DisplayMessage(ByVal Message As String)
If Not App.MainForm Is Nothing Then
App.MainForm.ListBox1.Items.Insert(0, _
DateTime.Now.ToLongTimeString() & " - " & Message)
End If
End Sub
One final step to finish our application and fully expose it to COM: Create a type library. The .NET framework comes with a utility called REGASM.exe that allows you to register assemblies that are exposed to COM. The REGASM utility also allows you to create a type library (TLB) that can be used in many programming IDEs (including VB6). To use REGASM, just run the Create.Bat file (you may need to update the paths in the bat file). Notice that MsgNet.tlb has been created.
Now that we have a .NET application that exposes COM methods, let's invoke the .NET assembly with VBScript (the script can be found in the file TestMsgNet.vbs).
Dim MyMsgNet
Set MyMsgNet = CreateObject("MsgNet.ComExpose")
MyMsgNet.StartApplication
MyMsgNet.DisplayMessage ("Hello from VBScript")
Set MyMsgNet = Nothing
As you can see from the test, it doesn’t work. The main interface and the initial message ".NET application starting" are displayed, however, the text “Hello from VBScript” is never displayed. Also note that every time you execute the above VBScript, the .NET application is reloaded.
This creates a problem for migrating applications to .NET. You could migrate every single ActiveX EXE server application to .NET at the same time, and utilize .NET Remoting as the communication tool between the various AppDomains, however, this approach is not realistic. Hence, the ActiveX EXE wrapper phased approach.
Step 2: Wrap the VB.NET project in an ActiveX server
The above solution is not very helpful. We want the .NET EXE assembly to behave like an ActiveX EXE server. That is, we want all client applications to utilize the running instance if available. To accomplish this goal, we need to wrap the .NET assembly with an ActiveX server. I understand that this is an additional step, however, it is a necessary step (otherwise, you need to change every ActiveX EXE server application, recompile, and deploy to utilize TCP/IP, for example). To complete this exercise, you need to use a programming language that will allow you to create an ActiveX EXE server. I will illustrate this using VB6.
-
Create a new ActiveX EXE project in VB6:
- Add a reference to the “MsgNet.tlb” type library that you created with the REGASM utility.
-
Create a module in the VB6 application and define a global variable called MyMsgNet
. The module should also contain a “Sub Main
” procedure to start the ActiveX EXE server:
Option Explicit
Public MyMsgNet As MsgNet.ComExpose
Public Sub Main()
Set MyMsgNet = New MsgNet.ComExpose
MyMsgNet.StartApplication
End Sub
Note: you will have to set the “Startup Object” in the VB6 project to “Sub Main”.
Also note, you will need to set the “Start Mode” to “Standalone”.
- Finally, to complete the ActiveX EXE wrapper for the .NET assembly, you need to create a VB6 class that will simply call the corresponding methods in the .NET assembly.
-
Create a class in the VB6 project called “ComExpose
” and add the following code:
Option Explicit
Public Sub DisplayMessage(ByVal Message As String)
MyMsgNet.DisplayMessage Message
End Sub
All done; the ActiveX EXE COM wrapper is complete. Compile the application. Notice that when you execute the VB6 ActiveX EXE wrapper, the .NET application is invoked.
Also note:
Step 3: Testing the ActiveX EXE COM wrapper
Previously, we used a VBScript to test our .NET EXE assembly and found that the results were not good. The messages were never displayed in the .NET assembly. To test the new VB6 ActiveX EXE wrapper, we only need to make a small modification to our VBScript:
Dim MyMsgNet
Set MyMsgNet = CreateObject("MsgNet_Com.ComExpose")
MyMsgNet.DisplayMessage ("Hello from VBScript")
Set MyMsgNet = Nothing
We changed CreateObject(“MsgNet.ComExpose”)
to CreateObject(“MsgNet_Com.ComExpose”)
to switch from the COM interface in the .NET EXE assembly to the ActiveX EXE wrapper.
Notice that when you execute the VBScript now, it will utilize the running instance of the .NET EXE assembly. The goal has been accomplished: the .NET EXE assembly is “acting” like an ActiveX EXE server.
Conclusion
Using an ActiveX EXE wrapper, you can make a .NET assembly behave like an ActiveX EXE server. This is useful for upgrading legacy VB6 applications to .NET in phases or allowing the .NET assemblies to be extended with VBScript.
I can be reached at donaldsnowdy@hotmail.com.