Thursday, June 30, 2005

How to Use a Managed (.NET) Control in an Unmanaged Container

It seems that in the first beta of the Visual Studio .NET tools, you were able to create a user control (like a button or a form), and the mark it for COM compatibility to use it like an ActiveX control in your legacy Visual Studio 6 projects. Unfortunately, Microsoft decided to yank that feature prior to the official 1.0 release of the .NET SDK. It has yet to reappear.


Fortunately, some clever folks have figured out how to work-around this and get it to work. The first clue I came across was a Microsoft hosted page that suggested building an interop ActiveX control in VC7, and then using the interop ActiveX control from within the VC6 project. I went along this idea for a while, but was eventually advised against it by some of the other developers here as they had seen issues in mixing VC6 and VC7 libraries.


Then I hit the jackpot. I found two websites that, when combined, got me exactly what I wanted. Those websites were:



http://www.ondotnet.com/pub/a/dotnet/2003/01/20/winformshosting.html

http://www.codeproject.com/cs/miscctrl/exposingdotnetcontrols.asp



I had to add two functions to my control class. One function to add some extra Registry information to mark the component as an ActiveX control, and the other to remove those entries when the component is unregistered. I also had to mark the class with a ClassInterface and a GUID. Below is the class declaration info:



[Guid("0B98FF25-E354-4e9b-AD66-4351D1F3D95B")]
[ClassInterface(ClassInterfaceType.AutoDual)]
public class MyActiveXControl : System.Windows.Forms.Button

Then I used the following two functions for my extra registry settings.


#region COM Interop / ActiveX Functions

[ComRegisterFunction()]
public static void RegisterClass ( string key )
{
// Strip off HKEY_CLASSES_ROOT\ from the passed key as I don't need it
StringBuilder sb = new StringBuilder ( key ) ;
sb.Replace(
@"HKEY_CLASSES_ROOT\","") ;

// Open the CLSID\{guid} key for write access
RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(),true);

// And create the 'Control' key - this allows it to show up in
// the ActiveX control container
RegistryKey ctrl = k.CreateSubKey ( "Control" ) ;
ctrl.Close ( ) ;

// Next create the CodeBase entry - needed if not string named and GACced.
RegistryKey inprocServer32 = k.OpenSubKey ( "InprocServer32" , true ) ;
inprocServer32.SetValue (
"CodeBase" , Assembly.GetExecutingAssembly().CodeBase ) ;
inprocServer32.Close ( ) ;

// Create a miscellaneous status key to prevent flickering
RegistryKey miscStatus = k.CreateSubKey("MiscStatus");
miscStatus.SetValue(
"", "131457");

// Reference the type library
RegistryKey typeLib = k.CreateSubKey("TypeLib");
Guid libid = Marshal.GetTypeLibGuidForAssembly(Assembly.GetExecutingAssembly());
typeLib.SetValue(
"", libid.ToString("B"));

// Assign the version of the control
RegistryKey versionKey = k.CreateSubKey("Version");
Version ver = Assembly.GetExecutingAssembly().GetName().Version;
string version = string.Format("{0}.{1}", ver.Major, ver.Minor);

if( version == "0.0" )
{
version =
"1.0";
}
versionKey.SetValue(
"", version);

// Finally close the main key
k.Close ( ) ;
}

[ComUnregisterFunction()]
public static void UnregisterClass ( string key )
{
StringBuilder sb =
new StringBuilder ( key ) ;
sb.Replace(
@"HKEY_CLASSES_ROOT\","") ;

// Open HKCR\CLSID\{guid} for write access
RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(),true);

// Delete the 'Control' key, but don't throw an exception if it does not exist
k.DeleteSubKey ( "Control" , false ) ;

// Next open up InprocServer32
RegistryKey inprocServer32 = k.OpenSubKey ( "InprocServer32" , true ) ;

// And delete the CodeBase key, again not throwing if missing
k.DeleteSubKey ( "CodeBase" , false ) ;

// Finally close the main key
k.Close ( ) ;
}


#endregion


With that code inserted, I then compiled the .NET code. To prove that the object will work, go to the Tools menu and choose ActiveX Test Control Container. Here you can add a sample instance of your control to prove to yourself it is working. The next step is to actually embed the control in you Visual Studio 6.0 project. Open up your dialog editor in VC++ and right-click the dialog. Choose "Insert ActiveX Control" and pick your control out of the list. Next, right-click the form and choose "Class Wizard...". Go to the Member variables tab and double click the ResourceID for your control to add a member variable in your code to access the functionality of the object. This last step is only necessary if you need to call methods or set properties on your user control.


I'm not guaranteeing that this code will work for you, but it has been working quite well for me. Sometimes when I open the dialog editor, I get an error message that says that the ActiveX control could not be instantiated, but it always compiles and works at runtime. Your mileage may vary.

3 comments:

  1. its possible there may be a problem with the above code. Shouldn't you be using the inprocServer32 key to delete the CodeBase subkey?

    ReplyDelete
  2. Have you managed to get this working with events?

    ReplyDelete
  3. As far as passing events back to the unmanaged host, no. Within the control itself I can catch events thrown by the UI, but the even engines of the managed and unmanaged code do not appear to work well together. One area where this has caused problems is with the help request events. When a help request is generated (by hitting the F1 key), it appears that the unmanaged host is catching the event and does not pass it down to the unmanaged control.

    For my particular situation, I created a button control which was the initial entry point for the managed code. By creating the control this way, I am able to enforce the modality of the child dialog launched from the button click. If you find a way to perform event passing, please leave a comment here with a link.

    ReplyDelete