| [Editor's Update - 10/31/2003: This article reflects features of the .NET Framework 1.0 Beta that were not included in the final release. You cannot register Windows Forms controls as ActiveX controls or create them using CoCreateInstance. In the .NET Framework 1.0 you can use Windows Forms controls in Internet Explorer but you cannot use them as ActiveX controls (see Host Secure, Lightweight Client-Side Controls in Microsoft Internet Explorer). In the .NET Framework 1.1 you can also use Windows Forms controls in MFC applications (see .NET Framework 1.1 Provides Expanded Namespace, Security, and Language Support for Your Projects).] |
.NET Interop: Get Ready for Microsoft .NET by Using Wrappers to Interact with COM-based Applications
| David S. Platt|
|This article assumes you're familiar with COM and Visual Basic|
| Level of Difficulty 1
| SUMMARY Very soon, the development of Microsoft .NET applications will require interaction between those apps and existing COM components on both the client and the server. The .NET Framework has made provisions for this interaction by implementing various wrappers for COM objects to allow exposure of their properties and methods to .NET components. These wrappers will make it easy to make the connection between COM and .NET. |
After discussing wrappers, this article discusses other ways for .NET components to take part in COM+ transactions. To top off the tutorial on the interoperation of COM and .NET, the article discusses how ActiveX containers can host .NET controls, and how .NET containers can host ActiveX controls.
| he demise of COM has been greatly exaggerated. With all the buzz about the Microsoft® .NET Framework, the press seems to be forgetting that it hasn't shipped yet, and won't for some time. When it does ship, .NET will be in the same position as color TV 40 years ago, Windows® 3.0 11 years ago, or HDTV today. It's cooler, it's better, it does more useful stuff than the existing universal framework; but the entire world is heavily invested in a different standard that they can't just jettison all at once. Microsoft platforms have relied on COM for the last eight years, an eternity in this business. The first .NET app that ships will find nine gazillion COM apps to talk to and, by definition, no other .NET apps. The second .NET app will find nine gazillion COM apps and one .NET app, and so on. Just as the first color TV sets also needed to receive the black and white broadcasts that predominated at the time and the first versions of Windows needed to run MS-DOS®-based programs or no one would have bought either one of them, so .NET needs to interoperate seamlessly with COM. And it does, quite well, both as a .NET client using a COM server, and vice versa. |
You can see the genesis and the first test bed of the design ideas in the .NET support for COM in Visual J++®, which is part of Visual Studio® 6.0. I wrote an article on that COM support three years ago, which I've incorporated into my book, The Essence of COM: A Programmers Workbook, 3rd Edition (Prentice Hall). I've put that chapter on my Web site as a free download at http://www.rollthunder.com/javachapter.htm for any readers who might be interested.
Note that the sample programs supplied with this article were built with Visual Studio .NET Beta 2, build 9148. There will be changes between the time of this writing and the time you read this article. Don't be surprised if you have to rebuild the applications from scratch.
Using COM Objects from .NET
Since it is more likely that new .NET code will have to interoperate with existing COM code than the reverse, I will describe this case first. A .NET client accesses a COM server through a runtime-callable wrapper (RCW), as shown in Figure 1. The RCW wraps the COM object and mediates between it and the .NET common language runtime (CLR) environment, making the COM object appear to .NET clients just as if it were a native .NET object and making the .NET client appear to the COM object just as if it were a standard COM client.
Figure 1 Client Access Through RCW
The developer of a .NET client generates the RCW in one of two ways. If you're using Visual Studio .NET, simply right-click on the References section of your project and select Add Reference from the context menu. You will see the dialog box that offers a choice of all the registered COM type libraries it finds on the system, and also allows you to select unregistered type libraries by browsing to them (see Figure 2). Select the COM object for which you want to generate the RCW, and Visual Studio .NET will create it and add it to your project.
Figure 2 Generating an RCW
If you're not using Visual Studio .NET, the .NET SDK contains a command-line tool called TlbImp.exe that performs the same task. The logic that reads the type library and generates the RCW code actually lives in a .NET runtime class called System.Runtime.InteropServices.TypeLibConverter. Both Visual Studio .NET and TlbImp.exe use this class internally, and you can too if you're writing a development tool or feeling masochistic.
Figure 3 .NET Client Using COM
Figure 3 shows a sample .NET client program that uses a COM object server. You can download the samples and follow along from the link at the top of this article. This sample contains a COM server, a COM client, and a .NET client so you can compare the two. The source code is shown in Figure 4.
After you generate the RCW, you will probably want to import its namespace into the client program using the Imports statement, allowing you to refer to the object using its short name. You create the RCW object simply by using the new operator, as you would for any other .NET object. When it's created, the RCW internally calls the native COM function CoCreateInstance, thereby creating the COM object that it wraps. Your .NET client program then calls methods on the RCW as if it were a native .NET object. The RCW automatically converts each call to the COM calling convention; for example, it converts .NET strings into the BSTR strings that COM requires, and forwards it to the object. The RCW converts the results returned from the COM object into native .NET types before returning them to the client.
When you run the sample COM client program, you'll notice (from dialog boxes that I placed in the code) that the object is created when you click the button, and then immediately destroyed. When you run the sample .NET client program, you'll find that the object is created when you click the Get Time button, but that the object isn't destroyed immediately. You would think it should be, as the wrapper object goes out of scope, but it isn't, not even if you explicitly set the object reference to nothing. This is the .NET way of lazy resource recovery, described in Jeffrey Richter's articles on .NET garbage collection in the November and December 2000 issues of MSDN Magazine. The RCW has gone out of scope and is no longer accessible to your program, but it doesn't actually release the COM object that it wraps until the RCW is garbage collected and destroyed, which may happen at any later time. This can be a problem, as most COM objects were not written with this lifecycle in mind and thus might retain expensive resources that should be released as soon as the client is finished.
You can solve this problem in one of two ways. The first, obviously, is by forcing an immediate garbage collection via the System.GC.Collect function. Calling this function will collect and reclaim all system resources that are no longer in use, including all the RCWs not currently in scope. The drawback to this approach is that the overhead of a full garbage collection can be high, and you may not want to pay that price immediately just to shred one object, just as Julia Child often wipes off her favorite paring knife without cleaning up her entire kitchen. If you would like to blow away one particular COM object without affecting the others, you can do so via the System.Runtime.InteropServices.Marshal.ReleaseComObject function.
The RCW mechanism described in the preceding paragraphs requires an object to be early-bound, meaning that the developer must have intimate knowledge of the object (provided by a type library) at development time in order to construct the wrapper class. That's not possible in all design scenarios. For example, scripting situations require late binding, where a client reads the ProgID of an object and the method to call on it from script code at runtime. Most COM objects support the IDispatch interface specifically to allow this type of late-bound access. Creating an RCW in advance is not possible in situations like this. How does .NET handle these scenarios?
Figure 5 Late Binding
The .NET Framework supports late binding to the IDispatch interface supported by most COM objects. A sample late binding program is shown in Figure 5, and its code in Figure 6. You create a .NET system type based on the object's ProgID via the static method Type.GetTypeFromProgID. The static method Type.GetTypeFromCLSID (not shown in Figure 6) does the same thing based on a CLSID, if you have that instead of a ProgID. You then create the COM object using the Activator.CreateInstance method and call a method via the Type.InvokeMember function, passing the method's parameters in an array. It's more worklate binding always isbut you can do it.
Using .NET Objects from COM
Suppose, on the other hand, that you have a client that already speaks COM and now you want to make it use a .NET object instead. This is a somewhat less common scenario than the reverse situation because it presupposes new COM development in a .NET world. But I can easily see it occurring if you have an existing COM client that uses 10 COM objects and you now want to add an additional set of functionality that exists only as a .NET object. The .NET Framework supports this situation as well, via a COM-callable wrapper (CCW), as shown in Figure 7.
Figure 7 COM-callable Wrapper
The CCW wraps the .NET object and mediates between it and the CLR environment, making the .NET object appear to COM clients just as if it were a native .NET object. To operate with a COM-callable wrapper, a .NET component's assembly must be signed with a strong name; otherwise the CLR runtime won't be able to definitively identify it. It must also follow standard .NET component location conventions, which means that it must reside in the global assembly cache (GAC), or, less commonly, in the client application's directory tree. Any .NET class that you want COM to create must provide a default constructor, meaning a constructor that requires no parameters. COM object creation functions don't know how to pass parameters to the objects that they create, so you need to make sure your class doesn't require this. Your .NET class can have as many parameterized constructors as you want for the use of .NET clients, as long as you have one that requires none for the use of COM clients.
In order for a COM client to find the .NET object, you need to make the registry entries that COM requires for a client to locate a server when creating an object. You do this with a utility program called RegAsm.exe that comes with the .NET SDK. This program reads the metadata in a .NET class and makes registry entries that point the COM client to it. The sample code provides a batch file that does this for you. The registry entries that it makes are shown in Figure 8. Note that the COM server for this is the intermediary DLL Mscoree.dll. The Class value of the InProcServer32 key tells this DLL which .NET class to create and wrap, and the Assembly entry tells it in which .NET assembly it will find this class.
You will find it difficult to register a .NET component that lives in the GAC because of the GAC's complex directory structure. It doesn't look complex from Explorer because Explorer contains a custom plug-in that simplifies it, but if you look at it from the command-line window you'll find that its internal directory structure contains many levels of nesting that are hard to deal with. You will probably find it most convenient to place the .NET component in a standard directory, run regasm.exe on it there, then use gacutil.exe to move the component into the GAC. While moving a COM component after registration would render it useless, that doesn't happen in .NET. In fact, DLL Hell is one of the problems that .NET's architecture was designed specifically to solve, and does. All shared components live in the GAC, using strong names to avoid any naming conflicts. The .NET loader checks for a requested component in the requester's directory tree, then checks the GAC if it can't find the desired assembly there.
A COM client accesses a .NET object as if it were a native COM object. When the client calls CoCreateInstance to create the object, the registry directs the request to the registered server, Mscoree.dll. This DLL inspects the requested CLSID (all DLL COM servers expose a global function called DllGetClassObject for this purpose), reads the registry to find the .NET class to create and the assembly containing that class, and rolls a CCW on the fly based on that .NET class. The CCW converts native COM types to their .NET equivalentsfor example, BSTRs to .NET stringsand forwards them to the .NET object. It also converts the results back from .NET into COM, including any errors. The sample code for this article contains a .NET component that provides the current time, and a COM client that accesses it.
A .NET developer could reasonably want some methods, interfaces, or classes to be available to COM clients and others not to be. Therefore, .NET provides a metadata attribute called System.Runtime.InteropServices.ComVisible. You can use this attribute on an assembly, a class, an interface, or an individual method. The CCW reads this metadata attribute at runtime and will fail any request from COM to access anything marked with this attribute set to False. Settings made lower in the hierarchy override those made higher up.
For example, if you set ComVisible to False on an interface but True on one of that interface's methods, clients will be able to call that method on that interface but not have access to any of the other methods on the inteface. In the sample program, I set this attribute to True on my class (so you'll notice it when you look at the code), as shown in the code fragment that follows, thereby making it visible to COM.
The current CLR default setting is True, so if this attribute is not specifically set to False, the item will be visible to COM. However, Visual Studio .NET's current default behavior for assemblies is to set this attribute to False in a project's AssemblyInfo.vb file, which means that nothing in the assembly will be visible to COM unless you go out of your way to make it so.
Public Class Class1
I strongly disagree with this practice of a development tool overriding system defaults to their exact opposites. It violates the Principle of Least Astonishment, which states simply that astonishing the user is generally a bad thing to do, so you want to do it as little as possible. Any programmer reading system documentation will be told that the default is for ComVisible to be True, but when she builds a simple component with Visual Studio, COM won't be able to find it and the programmer won't know why. I'm not saying that either default is particularly right or particularly wrong; they each have advantages and disadvantages and I'd consider either choice to be reasonable. But having the operating system do one thing and the primary development environment do another will cause Microsoft to get many expensive phone support calls if they don't change one or the other (not both, same problem again) before the product ships.
COM and .NET each has its own internal way of handling errors. The developers of COM servers return special values from their methods to indicate an error condition, sometimes providing additional information in a separate error information object in thread local storage. C++ programmers do this directly in their code. Visual Basic® abstracts this mechanism away by providing the Err object and its Raise method, but under the hood it's doing the same thing. A COM client examines the error code and looks in thread local storage for the additional error information object in order to determine the presence and cause of an error.
.NET, on the other hand, handles errors by catching and throwing exceptions, in a manner similar to that used in the Java language. A server method wanting to signal an error creates an exception object describing the error and throws it to the CLR. A client programmer wanting to be notified of errors places an error handler on the stack by means of a Try...Catch block of code. When the server throws an error, the CLR looks for these handlers and transfers control to them if it finds one.
When .NET needs to talk to COM, each side is expecting to see only its own error handling methods. So the wrapper layersthe CCW or RCW, depending on which way you're goingneed to properly catch their server's type of error and convert it into the type that their clients know how to handle. In the example in which a .NET client is calling a COM server (shown previously), the COM server signals a standard COM error using the code shown at the top of Figure 9. The RCW detects this error and converts it into a .NET exception, which can be caught by the client code (shown at the bottom of Figure 9). I show the handler treating the COM server's exception as a generic System.Exception. If you want to, you can differentiate COM-based exceptions from other types by having your handler catch only the System.Runtime.InteropServices.COMException exception class.
Going the other way, the sample .NET server shown previously signals an error by creating and throwing a .NET exception, as shown at the top of Figure 10. The COM client written in Visual Basic handles it using its standard (yuck) On Error GoTo mechanism, as shown at the bottom of Figure 10. Each side does its normal everyday thing, and the wrappers handle the conversions back and forth.
Transactions in .NET
COM+ and its ancestor, Microsoft Transaction Services (MTS), provided automatic support that made it easy for programmers to write objects that participated in transactions. A programmer marked his objects administratively as requiring a transaction. COM+ then automatically created one when the object was activated. To make changes to a database the object used COM+ Resource Managers, which are programs such as SQL Server, that supported the COM+ way of performing transactions. The object then told COM+ whether it was happy with the results. If all the objects participating in a transaction were happy, COM+ committed the transaction, saving all their changes. If any object was unhappy, COM+ aborted the transaction, discarding the results of all objects' operations and rolling back the state of the system to its original values. To learn more about the COM+ implementation of transactions, read my book, Understanding COM+ (Microsoft Press, 1999), or my article in the December 2000 issue of MSDN Magazine, available at http://msdn.microsoft.com/msdnmag/issues/1200/comtips/comtips.asp.
Native .NET objects can also participate in transactions. Since the existing Microsoft transaction processing system is based on COM, .NET objects do this by using their COM interoperation capability, described in the first part of this article. You register a .NET object as a COM server. You then use the COM+ Explorer to install that component into a COM+ application and set its transaction requirements exactly as if it were a native COM component. You can also use a .NET SDK command-line tool called Regsvcs.exe to perform registration and set up a COM+ application in one step.
Alternatively, as a native COM component sometimes specifies its transaction requirements in its type library, you can specify a .NET object's transaction requirements in its metadata. The following code fragment shows a .NET object written in Visual Basic containing an attribute that specifies that it requires a transaction.
If you were writing the object in C#, you'd do exactly the same thing except you'd use square brackets instead of angle brackets. An ASP .NET page indicates the transaction requirements of the code on it by adding attributes, as shown in this code fragment.
Public Class <TransactionAttribute(TransactionOption.Required)>Class1
A Web Service indicates its transaction requirements by marking individual methods with attributes, as you can see in the following code fragment.
<%@page Transaction="Required " %>
See my article in the February 2001 issue of MSDN Magazine, available at http://msdn.microsoft.com/msdnmag/issues/01/02/Webcomp/Webcomp.asp, for more on Web Services.
Public Function <WebMethod(),
HelloWorld() As String
A .NET object that participates in a transaction needs to vote on that transaction's outcome. You can accomplish this in one of two ways. In COM+ and MTS, an object fetched its context object by calling the API function CoGetObjectContext or a wrapper for it named GetObjectContext, then called a method on the context object to indicate its transaction vote. A .NET object will find its context on the system-provided object System.EnterpriseServices.ContextUtil. This object provides the commonly used SetAbort and SetComplete methods and their somewhat less common siblings, EnableCommit and DisableCommit. These methods set your object's happiness and doneness bits in exactly the same manner as they did in COM+. The context also contains the properties DeactivateOnReturn and MyTransactionVote, which allow you to read and set these bits individually. Alternatively, you can make your transaction vote "fire and forget" by marking your .NET object with an attribute called AutoComplete. If you do this, a normal return from the object will automatically call SetComplete, but leaving the object by throwing an exception will automatically call SetAbort.
.NET Controls in ActiveX Containers
Most user interface programming in the Windows environment makes use of prepackaged controls. The notion of distributing reusable software containing user interface elements, methods, properties, and events in a convenient package that plugs into a smart environment for rapid development has been fantastically successful in the software marketplace. It started with 16-bit VBX controls and moved to ActiveX® controls when Windows moved to 32 bits. The pages of this magazine and others are crammed with ads for ActiveX controls that provide everything from spreadsheets and spelling checkers to strip chart recorders and electrocardiograms. They've multiplied like rabbits, competing with marsupials in Australia. For the success of .NET, this marketplace must continue to prosper.
The user interface portion of .NET, called Windows Forms, provides support for developing your own Windows Forms controls. Think of them as conceptually doing the same thing as ActiveX controls, except they're written for .NET. This is far too large a topic for this article to cover in any depth, but I did write a quick sample to demonstrate the ease with which you can write your own controls. A Windows Forms client hosting the control is shown in Figure 11 and the control's code is shown in Figure 12.
Figure 11 Windows Forms Client
You write a Windows Forms control by deriving a class from the base class System.Windows.Forms.UserControl. Creating a Windows Control Library project in Visual Studio .NET does this for you. C++ developers who wrote ActiveX controls by deriving from the MFC base class COleControl will find this approach familiar. This base class contains the functionality that is common to all Windows Forms controls, from simple properties such as a background color, to sophisticated negotiations with its container, such as those required for floating and docking. Your control overrides the Windows Forms event handlers whose behavior you want to replacein this case the OnPaint notification, in which I paint my control's rectangle in my control's background color and draw the current time on it using my control's default font. That's all there was to writing this control. Put the control in the Visual Studio toolbox by right-clicking on the toolbox, choose Customize Toolbox from the shortcut menu, and select the control (see Figure 13).
Figure 13 Placing .NET Controls in a Windows Toolbox
But if I write a Windows Forms control, doesn't that mean that only .NET applications can use it? There aren't many of them out there, at least not yet, so shouldn't I wait for a critical mass before I invest my development dollars? To help you answer "No" to these questions, the UserControl base class contains all the functionality that it needs to be accessible to ActiveX control hosts such as Visual Basic 6.0. This will allow a developer of a .NET control to take advantage of the enormous installed base and make a lot of money.
You have to write one relatively small piece of code to make your .NET control accessible to ActiveX hosts. ActiveX controls make several registry entries that standard COM servers don't, so you have to add this functionality to your .NET control. The CLR contains prefabricated functions that will make and remove these entries. These go by the names Control.ActiveXRegister and Control.ActiveXUnregister. You need to provide two external functions in your control class marked with attributes that tell the .NET COM registration utility to call them during the registration process. These functions need to delegate to ActiveXRegister and ActiveXUnregister (see Figure 14). That's the only extra piece of code that you have to write today, and I wouldn't be surprised if it moved into the base class in some future version.
Once you've done this, using your .NET control as an ActiveX control is simply a matter of registering it as you would any other .NET class wanting to be a COM server, as discussed previously in this article. You must build it with the ComVisible attribute set to True so that it knows that COM clients are allowed to see it. The CLR must be installed on the client machine. You generate a type library and register it with the RegAsm.exe utility. You then have to put your control DLL someplace where the client application (in this case, Visual Basic) can see it, usually the GAC but sometimes you can put it in the VB98 directory where its other binary files live. The Visual Basic control selection dialog box will then offer your .NET control as an option, as shown in Figure 15.
Figure 15 A .NET Control in Visual Basic 6.0
ActiveX Controls in .NET Containers
If .NET couldn't use ActiveX controls, many desktop developers couldn't use .NET for writing client applications. Then developers of ActiveX controls wouldn't have a large enough market to merit converting their controls, and the whole concept would be stillborn. Therefore, the developers of .NET Windows Forms wisely decided to include support for hosting ActiveX controls.
A Windows Forms application does not inherently know how to use an ActiveX control. It only understands controls written with its own native .NET architecture. In order for a Windows Forms application to host an ActiveX control, you need to generate a wrapper class that will contain the ActiveX control, mediate between its COM-based world view and the .NET world view of its container, and present it to Windows Forms as if it were a native Windows Forms control. You essentially need a monster runtime-callable wrapper, as described earlier in this article, that consumes all the COM interfaces that an ActiveX control provides, while it provides the COM interfaces that an ActiveX control requires from its host. This architecture is shown in Figure 16. If you think that sounds like a heck of a lot of work, you're right, but don't worry because the CLR provides a prefabricated class that does it all for you, called System.Windows.Forms.AxHost.
Figure 16 Windows Forms
You need to derive a separate wrapper class from AxHost for each class of ActiveX control that your Windows Forms application wants to host. This class will contain the class ID or program ID used to create the ActiveX control and will expose the properties, methods, and events of the internal ActiveX control in a native .NET format. This will feel very familiar to anyone who has ever imported an ActiveX control into Visual C++® or Visual J++. You generate this wrapper class using a command-line utility called AxImp.exe that comes with the .NET SDK. If you are using Visual Studio .NET, you can simply right-click on the toolbox, then select Customize Toolbox from the shortcut menu, and you will see the dialog box shown in Figure 17, which offers the list that Microsoft now calls COM Controls. (So long "ActiveX" name, and good riddance).
Figure 17 Visual Studio .NET Importing a Reference to an ActiveX Control
When you select a control from this list, Visual Studio .NET runs AxImp.exe internally and generates this wrapper class for you. It's built into a separate DLL as part of your project. You can't see the source code directly, at least not currently, but you can view its methods and properties in the Object Browser, shown in Figure 18. The new control will appear on your toolbox, and you can use it in the familiar manner.
Figure 19 Web Browser Control Inside a Web Form
I've written a sample Windows Forms program that uses the Microsoft Web Browser ActiveX control. Figure 19 shows a screen shot of it, displaying the MSDN Magazine Web page. I imported the ActiveX control into Visual Studio .NET as described previously. I then placed the ActiveX control on my Windows form and wrote the following code:
When the user clicks the Fetch button, I call the wrapper class's Navigate method, passing the URL that the user has entered. The wrapper class transforms this call into a COM call and passes it to the wrapped ActiveX control.
Protected Sub Command1Click(ByVal eventSender As System.Object, _
ByVal eventArgs As System.EventArgs) Handles Command1.Click _
Microsoft .NET needs to interoperate with COM if it is to be successful in the marketplace. The .NET architecture provides this interoperation via wrapper classes that mediate between .NET and COM, both as client and server. .NET objects using this interoperability feature can participate in COM+ transactions. The COM and .NET interoperation capability allows existing ActiveX containers to host .NET controls, and allows .NET Windows Forms applications to host ActiveX controls.
| For related articles see: |
Understanding COM+ (Microsoft Press, 1999)
"COM+ and Windows 2000: Ten Tips and Tricks for Maximizing COM+ Performance"
| David S. Platt is president and founder of Rolling Thunder Computing Inc. He teaches COM and COM+ at Harvard University and at companies all over the world. He publishes a free e-mail newsletter on COM+, available at http://www.rollthunder.com. David is the author of Understanding COM+ (Microsoft Press, 1999). This article is adapted from his recently published book,
Introducing Microsoft .NET (Microsoft Press, 2001). |