Thanks to Microsoft's foresight you don't have to rewrite all of your
COM functionality in .NET in order to take advantage of .NET's capabilities.
In this article, the second of a two-part series, I will show you how to
consume .NET classes from COM. This article assumes you are familiar with
COM type libraries and method calling, at least at the conceptual level, and
know the basics of C# programming.
Consuming .NET Classes from COM
How hard can using .NET classes in COM be? Not too hard, actually. The
first thing you might ask is when or why you would want to expose a .NET
component to a COM client. After all, if you are writing new code, wouldn't
it just be easier to write the client code in .NET as well? Often, the
answer will be no. If you have a large-scale legacy application that you
need to maintain and continue to evolve, you may not have the luxury of
rewriting the whole thing, or even significant portions of it from scratch
in .NET. But if the customer is asking for new features, it doesn't make
sense to code those new features using legacy technologies and languages
either.
By coding the new capabilities as .NET components, you can plug those
capabilities into your legacy code base with only minor coding requirements
on the legacy side. You have new code that not only can be easily consumed
by the legacy application as a COM component, but the same code can be
reused and extended by new .NET applications that you build now and in the
future. Not only does this make your code more maintainable and extensible,
you can also capitalize on the vast capabilities in the .NET Framework class libraries and third-party frameworks and components being built for .NET, and create your new capabilities a lot faster and with a lot less code.
The first step in using a .NET object from a COM client is to declare
the class you want to expose. Almost any class in .NET can be exposed to a
COM client simply by exporting the information for its assembly and
registering the assembly with COM. The class must have a default
constructor, and the methods, properties, fields, and events you want to
expose must be public. To expose a class in a fashion that adheres to the
rules of COM requires a little more planning.
Remember that a COM component's interface must never change once published. The component coclass and the interfaces are identified with GUIDs (globally unique
identifiers) that should not be changed either. You also need to think about
what other architectural constraints there may be, such as what the
threading model should be, whether interfaces need to be custom, dispatch,
or dispatch only. Most of the architectural aspects you need to specify can
be set using .NET attributes. Table 1 shows a list of the most common
COM-related attributes and what they do for you. Most of these are covered
in more detail shortly.
Each .NET class that you expose to COM will have a default class
interface associated with it. The name of the interface is the name of the
class prefixed by an underscore, and by default it is a dispinterface
(dispatch only). You can make this explicit using the ClassInterface
attribute on your class as shown here.
[ ClassInterface( ClassInterfaceType.AutoDispatch ) ]
class MyClass {
...
}
You can also use the AutoDual type if you want an IDispatch dual
interface for the class interface, but this is not recommended for reasons
I'll discuss shortly. You cannot make the class interface IUnknown-based,
but you can make other interfaces either dispinterfaces, dual, or custom
using the InterfaceType attribute.
In .NET 1.0, the default ClassInterfaceType is AutoDispatch. When the
.NET type information is exported to a type library, the type information
from AutoDispatch interfaces is not placed in the type library. Since they
can only be called dynamically, this leaves it up to the runtime to locate
the method by name and keeps clients from caching the DISPID of the method,
which the runtime is free to change if it chooses.
Exposing your class functionality to COM clients on the class interface
is a really bad idea though, because it is almost guaranteed to break the
cardinal rule of COM an interface should never be changed after it is
published. The class interface is guaranteed to change every time you add a
new public member to the class or one of its parents. So exposing your
functionality on the class interface is not a very extensible solution since
it means you can no longer change your class after publishing it.
The right way to do things is to define public .NET interfaces that
contain the methods, properties, and events that you want to expose using
good component-based design principles. These principles include trying to
keep the number of methods on one interface to a reasonably small set, and
to make sure that all the methods on an interface are functionally related
to the intended purpose of the interface. Once you have defined the
interfaces, their methods, properties, and events, then you should implement
these interfaces in the .NET class you want to act as your COM coclass. The
interfaces must be marked as public, and will default to being IDispatch
derived in COM. You can use the InterfaceType attribute to change this.
When you define your classes in this fashion, they are not only more
usable from COM, they can be used in a good component-based fashion from
your other .NET classes as well. It also models the way components were
built in VC6 and VB6.
Besides not relying on the class interface to expose your functionality,
another good approach is to block it from use and set one of the other
interfaces as the default interface. You do this by setting the
ClassInterfaceType for the class to None. Then the first interface that your
class derives from (the first one after the colon in the class declaration)
will become the default interface.
So to make all this concrete, I'll show you how to implement a simple
component for a COM client using .NET and C#. Let's say you have a VB6
application to which you want to add the capability to convert an image file
from a JPEG to a GIF or vice versa. Doing this in VB6 or VC6 would require a
third-party library or a lot of messy, low-level C++ code. Doing this with
the .NET Framework is a piece of cake, so I will add this functionality
using a .NET class exposed to the VB6 app as a COM component. In this
example, for simplicity I just have the class save the converted image to
another image file. While you could marshal the converted bits back to an
unmanaged application through a Variant or other means in COM, the code to
do so gets a little tricky and is beyond the scope of this article.
The first step is to define the interface.
[Guid("920EDD21-B27D-4137-A40A-C9BC605C494C")]
public interface IConvertImages
{
void ConvertJpgToGif(string jpgFilePath,
string gifFilePath);
void ConvertGifToJpg(string gifFilePath,
string jpgFilePath);
}
Note that we use the Guid attribute to deterministically assign the
associated IID for this interface. This is an important step, because
otherwise the export tools will automatically assign one, and it will change
each time you modify the interface, compile, and export, which can clutter
up your registry horribly.
The class definition to implement this conversion functionality is shown
in Listing 1.
I use the ClassInterfaceType.None to block the generation of a default class interface, and I assign a Guid for the coclass CLSID for the same reasons mentioned for the IID above. If you are programming using VB.NET, there is also a ComClass attribute to
specify the CLSID, IID, and event interface IID. As you can see, thanks to
the Bitmap class in the .NET Framework, it only takes a couple of lines of
code to provide this functionality. Place all this in a file named
ImageConverter.cs, and add a namespace declaration around it all with the
namespace SimpleImageProcessor:
namespace SimpleImageProcessor
{
interface IConvertImages . . .
class ImageConverter . . .
}
To get this to compile, you will need to add the following three namespace
references to the top of the source file:
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
In order for COM to be able to get to the assembly at runtime and use it
as a COM object, the assembly has to be registered with COM, and it has to
be either placed in the same folder as the host executable, or registered in
the .NET global assembly cache (GAC) so that the runtime can find it. In
order to add the assembly to the GAC, you will need to digitally sign it. To
digitally sign it, you need a strong name key file, and will need to compile
that key file into your assembly.
So let's step through what it takes to accomplish all of that. First,
you create the strong name key file using the Strong Name Tool (sn.exe). You
run sn.exe as follows to create a key file:
sn k demoKey.snk
You could use sn.exe and the Assembly Linker tool (al.exe) to add the key
file to your assembly after you have compiled it, but a much easier way is
to use the AssemblyKeyFile attribute in your code to compile it in at
compile time. You can add this attribute to any of the source files that get
compiled into the assembly, as long as it is only declared once, and is at
assembly scope (i.e., outside the namespace declaration for your code). For
our example, add the following line to the top of the ImageConverter.cs file, immediately beneath all the "using" namespace statements and above the SimpleImageProcessor namespace declaration.
[assembly: System.Reflection.AssemblyKeyFile("demoKey.snk")]
Now we need to compile the code into a .NET assembly. From the command
line, use the following command:
csc /t:library ImageConverter.cs
The /t (or /type) switch tells the C# compiler to make the resulting
assembly a class library (or DLL) instead of using the default packaging of
an EXE.
The next step is to create the type library and register the assembly.
You can create the type library with the tlbexp.exe tool, then call
regasm.exe to do the registration, or you can do it all in one fell swoop
using the /tlb switch on regasm:
regasm /tlb:ImageConverter.tlb ImageConverter.dll
You could now fire up the OLE/COM Viewer and take a look at the type
library information if you want to see that it is all in there, just like
with any other COM component. For this example, the type library will end up
named ImageConverter, the same as the DLL. If you open the type library in
OLEView, you will see that in fact the interface and coclass are there, but
the _ImageConverter class interface is not, since we applied a
ClassInterfaceType of None (see Figure 1). You will also see that in fact
the IConvertImages interface is marked as the default interface since it was
the first interface that the ImageConverter class derived from.
Even though COM can find all the type information it needs to describe
the component now, there is one last step before you will be able to
actually instantiate the component from a client application. You must
either copy the .NET assembly (ImageConverter.dll) into the same folder as
the host executable that will be using it, or you must register the assembly
in the GAC. I will take the latter approach to show how it is done.
Deploying the assembly with the host executable is actually the preferred
method because it avoids cluttering up the GAC with unneeded shared
assemblies. But if your component might be used by more than one host
application, then the GAC is the way to go.
Registering the assembly in the GAC is as easy as running one more
command from the command prompt.
gacutil i ImageConverter.dll
You now have a full-fledged .NET COM component that can be consumed from
any COM client. Download the code from www.sys-con.com/dotnet/sourcec.cfm
for a sample VB6 client.
All this command-line stuff is fine and good, but shouldn't it be easier
in VS.NET? A little. VS.NET will automatically take care of the regasm step
for you if you go into the Project properties, under the Configuration
Properties\Build node in the tree, and set the Register for COM Interop
property to True. However, you will still need to use the command-line tools
to generate the key file and to put the assembly into the GAC.
These steps can be added to your Tools menu as an external tool to keep
you from having to fire up a command prompt. Unfortunately, VS.NET 2002 did
not include custom build steps for C#, but VS.NET 2003 does include custom
build steps with which you could register the assembly in the GAC or perform
other command-line options or run scripts.
One way around this is to add a C++ Makefile project to your solution
with a project type of Utility, and put the custom build steps in that
project as the pre- or post-build commands. Then you can set up the project
dependencies and build order between that and your primary project with the
.NET solution, and you achieve the same thing. See the download code
SimpleImageProcessor solution for an example of this approach.
The download code also contains a VB6 project called ImageConverterClient that shows how easy it is to then use the .NET component in a VB6 client. A VC6 client would be just as simple, no harder than using any other COM component.
Back in the Beta 1 days I wrote a couple articles and an e-book on COM
interop. In those, I dedicated an entire section to exposing .NET controls
as ActiveX controls to ActiveX control containers. Regrettably, Microsoft removed the capability to do that for good in Beta 2. There is no straightforward way to get a .NET control exposed as an ActiveX control in the release of .NET. There are workarounds and manual methods out there involving implementing all the required ActiveX control interfaces by hand, but they are not really practical for production development. So if
you want to develop your GUI controls in .NET, you are going to need a .NET
container application to host them. You can also create an unmanged C++ shim
control that hosts the .NET runtime and wraps .NET controls to expose them
as an ActiveX control, but that technique is quite advanced as well.
Conclusion
I've taken you through a whirlwind tour of creating interoperable code
between .NET applications and COM components, and COM clients and .NET
components. Using these capabilities, the doors open up considerably to
progressively migrate your existing applications to .NET without having to
trash everything you have done before and start over. There is a lot more
capability in .NET and the runtime to perform far more complex COM
interoperability than I have described in this series. If you are planning
on migrating an application with complex COM components that pass a variety
of data and types as method parameters, it behooves you to dig into the COM
interoperability sections of the MSDN documentation and see what all you
will want or need to do to get the most possible reuse out of your existing
code.
Author Bio
Brian Noyes is an independent software consultant with Software Insight (www.softinsight.com). He is an MCSD with over 11 years of programming, software
development, and project management experience. He is a contributing editor to numerous programming publications, author of .NET and COM: Working Together, and contributing author to Sams Teach Yourself DirectX 7 in 24 Hours.
brian@softinsight.com
Listing 1:
The ImageConverter Class implements the functionality exposed to COM clients.
[ClassInterface(ClassInterfaceType.None)]
[Guid("3957E444-EA85-4f49-AAD4-7DDAB6A12324")]
public class ImageConverter : IConvertImages
{
public ImageConverter() {}
public void ConvertJpgToGif(string jpgFilePath,
string gifFilePath)
{
Bitmap bmp = new Bitmap(jpgFilePath);
bmp.Save(gifFilePath, ImageFormat.Gif);
}
public void ConvertGifToJpg(string gifFilePath,
string jpgFilePath)
{
Bitmap bmp = new Bitmap(gifFilePath);
bmp.Save(jpgFilePath, ImageFormat.Jpeg);
}
}
All Rights Reserved
Copyright © 2004 SYS-CON Media, Inc.
E-mail:
info@sys-con.com