On my current gig I have had the privilege of working with a legacy VB6 application. We are integrating some .NET WinForms, which means we will need to do some COM Interop. Working with the Interop Toolkit is easy enough if you’re simply building a COM app and deploying it via an MSI. With that approach you have the luxury of registering the .NET Types exposed to COM.
We are deploying a COM app via ClickOnce. This, as we have come to discover, is a big problem. The end-users run an MSI the very first time (actually, they have an Admin run it), which installs the ClickOnce launcher and registers a few COM controls and types using regsvr32. Once that initial install has taken place, future installs are rolled out over the normal ClickOnce deployment. After working with the Interop Toolkit I soon realized that it requires the .NET Forms to be exposed as normal COM classes and registered on the host system (using regasm). Doing this is not possible via ClickOnce. Furthermore, since the users are non-Admin (as they should be), we cannot simply add some code to the ClickOnce launcher to invoke regasm calls for new types. Having the users run an MSI (or getting an Admin to do it) every time we want to roll out new .NET Forms integrated with the legacy application would obviously defeat the purpose of using ClickOnce. I know you can use an Interop UserControl and use Registration Free COM but that requires me to put the control on a VB6 form instance, which I do not want to do for a number of reasons – not the least of which is that it complicates the Model View Presenter pattern implementation that we are using.
The solution? Expose the .NET Forms to COM via a single Registration Free COM proxy class.
As I was using the Interop Toolkit I soon realized that it is, well, pointless. The InteropForm, InteropMethod, InteropProperty, etc. attributes that you place on your .NET Form are simply there for the code generation tool that comes with the tookit. When you run this tool it cranks out a wrapper class for you that handles a few of the ugly details required to interface with COM. After a lot of hacking I soon realized that I can completely eliminate the reference to the Microsoft.InteropTools assembly and make my own simple wrapper that behaves in the same manner as the one generated by the tool. Actually, I wanted a single wrapper type that is generic enough to be used for any .NET Form. Doing this is fairly easy. The complicated part comes in making the class usable from a COM application via Registration Free COM – a feature that is available with XP Pro SP2 systems and later. Doing this requires compiling a .NET assembly with some options that are not supported by Visual Studio. I have read numerous tutorials on how to do this and really did not have a lot of luck with them. After a lot of trial and error I was finally able to come up with a list of steps to successfully deploy a .NET assembly with registration free COM. I will attempt to provide you with a list of steps that actually work (and keep them as simple as possible).
STEP 1 – Create a new C# Class Library project named “SimpleRegFree”. Configure this project so that it has a single .cs file in it named All.cs (with no classes inside it) – this will make some of the subsequent steps a little easier.
STEP 2- Move the contents of the Properties\AssemblyInfo.cs to the All.cs file and then delete the Properties\AssemblyInfo.cs file. At this point you should have the following in the All.cs file (the only file in the project):
using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("SimpleRegFree")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("beaucrawford.net")]
[assembly: AssemblyProduct("SimpleRegFree")]
[assembly: AssemblyCopyright("Copyright © beaucrawford.net 2008")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(true)]
[assembly: Guid("9b40f101-b802-443f-888e-22e800d2f087")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
namespace SimpleRegFree
{
}
STEP 3 - Make sure to change the above assembly level ComVisible attribute to true. Also, you should take note of the value of the assembly level AssemblyVersion attribute. This value will need to be referenced later. Under normal circumstances, you will want to leave this as a static build number and not one that automatically increments. This build number will be referenced by two .manifest files – one of which is embedded into the SimpleRegFree.dll. In the world of Registration Free COM, the less you have to maintain, the better.
STEP 4 - Create a new class named “InteropFormWrapper” and add the ClassInterface, ComVisible, and Guid attributes (use Tools >> Create GUID to generate a new GUID if you need to). Also, add a method named “Test” to this class and have it return a string. This class should now look like:
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
[Guid("40c5cdf4-e412-4bb8-a26a-22427240c49b")]
public class InteropFormWrapper
{
public string Test()
{
return DateTime.Now.ToString();
}
}
NOTE: The value for the InteropFormWrapper’s Guid attribute is very important. This will end up being the CLSID for this class when it is exposed to COM.
STEP 5- Build the project. Then navigate to the bin\Debug output directory and create two files.
First File
Name: register.bat
Contents:
"%windir%\Microsoft.NET\Framework\v2.0.50727\regasm.exe" SimpleRegFree.dll /tlb:SimpleRegFree.tlb /codebase
pause
Second File
Name: unregister.bat
Contents:
"%windir%\Microsoft.NET\Framework\v2.0.50727\regasm.exe" /u SimpleRegFree.dll
pause
These files, as you might have guessed, are just some helper files to register and unregister the .NET Types in our assembly with COM.
STEP 6 – Run the register.bat file, which will create a file named SimpleRegFree.tlb in the same directory. Doing this will also register our InteropFormWrapper class with COM.
STEP 7 – Create a new VB6 project named “MyCOMApp” that contains a single form with a button on it. Next go to the “References” listing for the project and you should see “SimpleRegFree” listed. Check its box and then return to the form. Double-click the button and add the following code:
Private Sub Command1_Click()
Dim f As New SimpleRegFree.InteropFormWrapper
MsgBox (f.Test())
End Sub
STEP 8 – Run the application from within the VB6 IDE and make sure that you get the current date/time displayed when you click the button. Next select File >> Make and then build the project to the same directory the files from Step 5 (the bin\Debug directory from the .NET project'). The name of the VB6 executable should be “MyCOMApp.exe”.
STEP 9 – Browse to the output directory and run the newly created MyCOMApp.exe to ensure that it behaves as expected (you should again see the current date/time displayed each time you click the button). At this point you should be able to copy the .exe to any location on your system and successfully run it from there (leaving the SimpleRegFree.dll file in its original location) since the COM Types are globally registered. For our purposes, you should leave it where it is. Next, double-click the unregister.bat file and then run the .exe again. When you click the button this time you should get a nasty error message saying “Runtime Error 429: ActiveX component can’t create object”. This behavior validates that the COM Types are successfully unregistered. If you do not get this error message then you should stop and reread the previous steps. This is one of the few times in your software development career that you want to get this error message. It ensures that you do not end up with a false positive later on for the registration free part of this walkthrough.
STEP 10 – Create a new file named “MyCOMApp.exe.manifest” and then open it up in Notepad. Paste the following code into it and then save it:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity type="win32" name="MyCOMApp" version="1.0.0.0" processorArchitecture="x86" />
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="SimpleRegFree" version="1.0.0.0" />
</dependentAssembly>
</dependency>
</assembly>
STEP 11 – If you try to run the MyCOMApp.exe file now you will get a message saying “This application has failed to start because the application configuration is incorrect. Reinstalling the application may fix this problem.”. You get this error because the manifest file we just created is telling the COM application to also load the SimpleRegFree assembly. This fails though since we have not yet embedded a manifest into that assembly (which COM needs to resolve the types referenced from the MyCOMApp.exe application). That’s coming next.
STEP 12 – Navigate to the same directory that the SimpleRegFree.csproj file is in. Create a new directory named “Make” and then move into it.
STEP 13 – You will now need to create three files.
First File
Name: SimpleRegFree.manifest
Contents:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
manifestVersion="1.0">
<assemblyIdentity
type="win32"
name="SimpleRegFree"
version="1.0.0.0" />
<clrClass
clsid="{40c5cdf4-e412-4bb8-a26a-22427240c49b}"
progid="SimpleRegFree.InteropFormWrapper"
threadingModel="Both"
name="SimpleRegFree.InteropFormWrapper" >
</clrClass>
</assembly>
VERY IMPORTANT: The GUID in the above file must be the exact same one specified for the Guid attribute on your InteropFormWrapper class created in Step 4.
Second File
Name: SimpleRegFree.rc
Contents:
#define RT_MANIFEST 24
1 RT_MANIFEST SimpleRegFree.manifest
Third File
Name: compile.bat
Contents:
"%ProgramFiles%\Microsoft Visual Studio 8\VC\bin\rc.exe" SimpleRegFree.rc
%windir%\Microsoft.NET\Framework\v2.0.50727\Csc.exe /noconfig /nowarn:1701,1702 /win32res:SimpleRegFree.RES /errorreport:prompt /warn:4 /define:DEBUG;TRACE /reference:%windir%\Microsoft.NET\Framework\v2.0.50727\System.Data.dll /reference:%windir%\Microsoft.NET\Framework\v2.0.50727\System.dll /reference:%windir%\Microsoft.NET\Framework\v2.0.50727\System.Windows.Forms.dll /reference:%windir%\Microsoft.NET\Framework\v2.0.50727\System.Drawing.dll /reference:%windir%\Microsoft.NET\Framework\v2.0.50727\System.Xml.dll /debug+ /debug:full /optimize- /out:..\bin\Debug\SimpleRegFree.dll /target:library ..\All.cs
pause
STEP 14 – Double-click the “compile.bat” file. This will first create a .RES file that can be embedded into the assembly as a Win32 Resource. If you look in the above file you will see this file being added with the /win32res compiler switch. This option is not currently supported in Visual Studio, which is why we have to compile the assembly from the command line. Once the resource file is generated the script will then call the C# compiler to generate the assembly (outputting it to the bin\Debug directory). If you want to verify that an existing assembly has a manifest in it you can use Visual Studio to do so by going to File >> Open and then selecting the compiled .dll file. If the assembly has a manifest in it you will see a directory named “RT_MANIFEST”. This should normally indicate that the assembly contains a manifest. If you drop into that directory you should see a file. Double click on that file and you should see the manifest information from Step 13.
If you now navigate to the bin\Debug directory you should be able to run the MyCOMApp.exe file successfully. If so, then you have created your very first Registration Free COM assembly. You can now deploy it together with the SimpleRegFree.dll and the MyCOMApp.exe.manifest files. Users should be able to run it without having the .NET Types registered with COM on their system.
In future posts I will cover how to wrap an instance of a System.Windows.Forms.Form and expose its events. If you look at the wrapper class generated by the Interop Toolkit code generation tool you should be able to figure this out rather easily. I’m using this same approach and adding some additional functionality. However, instead of having a wrapper for each and every .NET Form in my assembly, I’m using a single wrapper and instantiating an underlying form by using Reflection. This makes the maintenance of the embedded .manifest file much easier.
One important thing to note is that you will still need to register the assembly with COM on your development box. If you don’t do this then the VB6 IDE will not be able to resolve the types. Registration Free COM only works with the compiled executable.