Introduction
This article will describe how to utilise Windows Forms controls outside of
.NET. In a recent MSDN magazine article on .NET Interop available here,
various ways of exposing .NET objects to 'legacy' environments are discussed,
including the exposure of Windows Forms controls as ActiveX controls.
The problem is that the goalposts have
moved since the article was written as Beta 2 is now available, and
unfortunately this support has been removed - see this posting
on the .NET list at http://discuss.develop.com.
The following image shows a control, written purely within .NET, hosted
within an ActiveX control container - in this instance tstcon32.exe.
As Beta1 supported this facility, and being somewhat inquisitive, I decided
to see if I could find a way to expose controls anyway. The attached project
creates the 'Prisoner' control, which won't set the world on fire but does show
the main things you need to do in order to get a .NET control up & running
within VB6.
CAVEAT: As this support has been dropped from Beta2 of .NET, don't
blame me if it fries your PC or toasts the cat.
Now that's out of the way, how's it done?.
Writing the control
- Create a new control project from within Visual Studio
- my examples are all in C# but VB.NET could also be used.
- Add controls etc to the form, put in the code etc.
- Add in the following using clauses...
using System.Runtime.InteropServices;
using System.Text;
using System.Reflection;
using Microsoft.Win32;
- Attribute your class so that it gets a ProgID. This
isn't strictly necessary as one will be generated, but it's almost always best
to be explicit.
[ProgId("Prisoner.PrisonerControl")]
[ClassInterface(ClassInterfaceType.AutoDual)]
This assigns the ProgID, and also defines that the interface exposed should
be 'AutoDual' - this crufts up a default interface for you from all public,
non-static members of the class. If this isn't what you want, use one of the
other options.
- Update the project properties so that your assembly is
registered for COM interop.
If you're using VB.NET, you also need a strong named assembly. Curiously in
C# you don't - and it seems to be a feature of the environment rather than a
feature of the compiler or CLR.
- Add the following two methods into your class.
[ComRegisterFunction()]
public static void RegisterClass ( string key )
{
StringBuilder sb = new StringBuilder ( key ) ;
sb.Replace(@"HKEY_CLASSES_ROOT\","") ;
RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(),true);
RegistryKey ctrl = k.CreateSubKey ( "Control" ) ;
ctrl.Close ( ) ;
RegistryKey inprocServer32 = k.OpenSubKey ( "InprocServer32" , true ) ;
inprocServer32.SetValue ( "CodeBase" , Assembly.GetExecutingAssembly().CodeBase ) ;
inprocServer32.Close ( ) ;
k.Close ( ) ;
}
The RegisterClass function is attributed with ComRegisterFunction - this static
method will be called when the assembly is registered for COM Interop. All I do
here is add the 'Control' keyword to the registry, plus add in the
CodeBase entry.
CodeBase is interesting - not only for .NET controls. It defines a URL path
to where the code can be found, which could be an assembly on disk as in this
instance, or a remote assembly on a web server somewhere. When the runtime
attempts to create the control, it will probe this URL and download the control
as necessary. This is very useful when testing .NET components, as the usual
caveat of residing in the same directory (etc) as the .EXE does not apply.
[ComUnregisterFunction()]
public static void UnregisterClass ( string key )
{
StringBuilder sb = new StringBuilder ( key ) ;
sb.Replace(@"HKEY_CLASSES_ROOT\","") ;
RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(),true);
k.DeleteSubKey ( "Control" , false ) ;
RegistryKey inprocServer32 = k.OpenSubKey ( "InprocServer32" , true ) ;
k.DeleteSubKey ( "CodeBase" , false ) ;
k.Close ( ) ;
}
The second function will remove the registry entries added when (if) the
class is unregistered - it's always a good suggestion to tidy up as you go.
Now
you are ready to compile & test your control.
Testing the Control
For
this example I have chosen tstcon32.exe, which is available with the
installation of .NET. The main reason I've used this rather than VB6 is that I
don't have VB6 anymore.
Inserting the Control
First up you need to
insert your control, so to do that choose Edit -> Insert New Control,
and choose your control from the dropdown...
This will result in a display as shown at the top of the article, if you're
following along with my example code.
Testing Methods
The example control only includes one method, 'Question'. To test this within
TstCon32, choose Control -> InvokeMethods from the menu, and select
the method you want to call. Note that because I defined the interface as
AutoDual, I get gazillions of methods. If you implement an interface and expose
this as the default interface then the list of methods will be more manageable.
Click on the 'Invoke' button will execute the method, which in this instance
displays the obligatory message box.
Wrap Up
Dropping support for creating ActiveX controls from Windows Forms controls is
a pain, and one decision I wish Microsoft had not made.
This article presents one way of exposing .NET controls as ActiveX controls,
and seems to work OK. Having said that, I've not exhaustively tested this and
who knows what bugs might be lurking in there. I haven't delved into events yet,
nor property change notifications, so there's some fun to be had there if you
like that sort of thing.
The .NET framework truly is the best thing since sliced bread, but the lack
of support for creating ActiveX controls from Windows Forms controls is inconvenient. There are many applications out there
(ours included) which can be extended with ActiveX controls. It would be nice given
the rest of the support in the framework to be able to expose Windows Forms
controls to ActiveX containers, and maybe someday the support will be available.