Dr. Dobb's Journal September, 2004
The company I work for sells micro-fluidic instrumentation intended for use in pharmaceutical and biotechnology laboratories. Typically, there are a large number of machines and instruments that reside in these labs, with differing models from various companies, each performing a different set of functions. Integrating these disparate functionalities into a cohesive process and methodology falls under the banner of what is known as "lab automation." As you'd expect, each machine or instrument runs some form of control software, and the industry has gradually settled on ActiveX controls as the preferred component technology that serves as the glue that connects all the pieces together. The dominant software architecture is a master scheduling application that is in control of the entire process, which invokes methods and gets/sets properties on the individual ActiveX "driver" components. This master application more often than not takes the form of a Visual Basic 6 (VB6) executable, as in Figure 1.
As the industry has settled on the VB6/ActiveX paradigm of distributed lab automation, it is simply not feasible from a business point of view to completely jettison the COM-centric paradigm of component-oriented development and embrace .NET controls lock, stock, and barrel. Since many of our customers use VB6, we must componentize our control software such that a VB6 client can talk to it. After all, the marketing department isn't going to be pleased if they find out a potential sale is lost because engineering has completely transitioned to .NET and, thus, a potential VB6 customer has no means of integrating our instrumentation software into their production line. Fortunately, Microsoft has given some thought to backwards compatibility, and through the machinations of a .NET technology called ".NET Interop," it is indeed possibleand even advantageousto develop COM objects in .NET's paradigmatic language, C#.
Much of our control software requires unimpeded access to the underlying hardware. As such, our existing ActiveX controls are implemented using (unmanaged) C++. As anyone who has coded ActiveX controls using the Active Template Library (ATL) knows, developing these controls using the wizards provided by Visual Studio can be a laborious and arduous task, involving the modification of a multitude of source files, such as .cpp, .h, and Interface Description Language (IDL) files just to implement simple controls, like the one that I describe here. I wanted to leverage the power of .NET in our software development strategy, but until recently lacked the expertise (and, more importantly, the impetus) to make that jump. That impetus was handed to me when I was faced with the daunting prospect of developing several lab instrumentation ActiveX controls, on an extremely tight deadline, for a line of products my company had acquired through a merger. I then explored the possibility of using .NET interop to implement ActiveX controls in C# as a means of expediting this development. What I found was somewhat of a revelation to methat, in fact, developing ActiveX controls and COM objects in general is straightforward in C#, and by properly utilizing .NET Interop, you can develop ActiveX controls that can be used by a VB6 client or any other COM-compatible language, provided the .NET Framework is available on the client machine. Moreover, in my estimation, the removal of ATL style "wizard-driven" coding (because hand coding such an implementation in C++ without the wizard is extremely time consuming to say the least), leads to an advantage when developing ActiveX controls in C#. The code is far more concise and easier to digest and maintain.
In this article, I first present an ActiveX control and VB6 client application, then examine the process of implementing this control using both C++ (ATL7) and C# in Visual Studio .NET 2003.
The COM object I use here is about as simple as I could conceive. It publishes a single method, Add(), which accepts two double input parameters and returns the sum of those two inputs. The sample client application is a VB6 executable and is equally simple (Figure 2). The form has text edit boxes corresponding to the two input parameters and the result of the Add() method is placed in another edit box when the button in the form is clicked.
This particular implementation uses Unmanaged C++ and ATL Version 7. An alternative method of crafting ActiveX controls is to utilize the Microsoft Foundation Classes (MFC) framework, but Microsoft recommends ATL if size and speed are the main criteria. The ActiveX controls discussed here are of the in-process variety (as opposed to out-of-process), meaning that they reside within DLLs.
The first step in creating the ActiveX control is to create a new ATL project in Visual Studio (Figure 3). The next step is to use the wizard to provide the skeleton for the control. Switch to the class view of the project (View|Class View), and add an "ATL Simple Object" class to the project (Project|Add Class); see Figure 4.
After selecting the option to add an ATL simple object to the project, the wizard prompts for a variety of options. Provide a class name and use the default options. The act of adding this ATL object to the project inserts a rather copious amount of code into the numerous C++ source files and generates Globally Unique Identifiers (GUID) for the interface and object classes. At this point, the wizard has constructed a fully functional ActiveX control (admittedly a useless one that has neither methods nor properties). Now you can insert the Add() method into the control. Again from the class view of the solution, select the interface class whose name begins with a capital "I" and is followed by whatever object name you selected earlier when you inserted the ATL object. Bring up the Add Method Wizard via Project|Add Method, and you can configure the signature of the new method. Figure 5 shows the contents of the Add Method Wizard after creating the signature for the Add() method. The class method that implements Add() returns an HRESULT and C++ clients, when invoking any COM method, can expect an HRESULT return value and should use the FAILED macro to test for success or failure. Clients coded in higher level languages, like Visual Basic or C#, can expect the ActiveX methods to return the parameter decorated with the [out,retval] attribute (which, in this case, is a double and is the summation of the two inputs).
The act of using the Add Method Wizard hides much of the gory details of implementing ActiveX methods in C++ from you. In essence, what is happening is that the wizard adds the method signature to the IDL portion of the project, adds a method declaration to the interface class, adds a similar method declaration to the implementation class, and finally adds a shell of the new method to the implementation class's .cpp file, which of course is left for you to fill in. When all is said and done, the C++ project for this trivial ActiveX control consists of 10 source files, not including transient source files that the MIDL compiler generates.
The VB6 client application has already been introduced. After creating the form in the VB UI designer, a reference to the new component must be added to the Visual Basic project. Add a reference to the control from within the Visual Basic IDE via Project|Reference, then browse to the location of the DLL that contains the new ActiveX control. Double clicking on the ActiveX DLL inserts the reference into the Visual Basic project.
Listing One implements this simple application. What you have done to this point is nothing new, rather it could easily have been accomplished in almost the same way using Visual Studio 6 in 1999. What is new is to implement this same control using C# and .NET Interop.
Using a COM object from .NET is straightforward. Once you add a reference to the COM object, Visual Studio automatically generates a Runtime-Callable Wrapper (RCW), which provides a thin veneer around the COM object and performs data-specific marshaling to/from the legacy COM domain and new .NET domain. What I implement is the converse of this, and the task is made substantially easier as the ActiveX control described here does not have a GUI aspect to it. If that were the case, then you could not use the steps described here as the MSDN documentation emphatically states that you "cannot register Windows Forms controls as ActiveX controls or create them using CoCreateInstance."
In contrast to the C++ version, the C# ActiveX implementation is much less wizard-driven. However, the result is far more concise, and easier to read and understand. The initial step is to create a C# class library project by selecting File|New|Project, choosing Visual C# Projects and the Class Library Template (see Figure 6). There is no ActiveX template and you'll be creating the control from scratch.
Listing Two is the C# version of the control and is probably a tenth of the size of the C++ version in terms of lines of code. The C++ ATL object was named SimpleActiveX, so the interface class is ISimpleActiveX. The corresponding C# interface class is named ICSharp_ActiveX and decorated with two attributes. .NET attributes let you extend the metadata that accompanies an assembly by annotating the source code with relevant fields. In this case, the first attribute is the GUID associated with the interface object. .NET Interop and, of course, the COM runtime need these GUIDs to perform their magic. The ATL wizard took care of generating GUIDs (and sprinkling them throughout various source files), while here I manually generate a GUID and link it to the event class using an attribute. GUIDs can be generated using either the command-line tool guidgen.exe or via the Create GUID item under the Visual Studio .NET Tools menu. The second attribute decorates the Add() interface method and is reminiscent of the IDL code inserted into the C++ project by the ATL wizard. In fact, it is merely embedded IDL and the same syntax is followed (for example, a help string could be associated with the method via the IDL helpstring keyword).
Likewise, the class object that implements this interface requires a GUID and is decorated with a GUID attribute. The ClassInterface attribute controls whether a separate class interface is generated for the attributed class (in this case CSharp_ActiveX_Class). While this option can be useful for debugging, the recommended value for this attribute is ClassInterfaceType.None, as it mitigates versioning problems. The Add() method is implemented and, except for a few postbuild steps, the COM object has been fully implemented. All of the attributes used in this example reside in the System.Runtime.InteropServices namespace. By adding the using declaration at the top of the source file, you don't need to use the fully qualified name for all of the attributes.
Before this assembly can be used as a COM object into a pre-.NET language, the object must be registered for COM Interop (Project|Properties, select Build under the Configuration folder and set Register for COM Interop to True). What this does is generate a COM-callable wrapper (CCW), which wraps the .NET class library we have just implemented. The COM client (in this case, the sample VB6 application) interacts with the CCW, which in turn forwards method invocations and performs data marshaling between itself and the underlying .NET class library.
For the CCW to work correctly, the .NET assembly must be strongly named or the .NET runtime can't identify it. To sign the assembly with a strong name, I use the sn.exe utility, which is in the .NET SDK. By adding a postbuild step to the C# project, you can seamlessly integrate this postprocessing into the build process. The steps required to accomplish this are:
The VB6 client code (available electronically; see "Resource Center," page 5) is virtually identical to the previous VB6 listing, sans the variable names pertaining to the COM object being changed. The only other change concerns how to import the reference to the COM object. Recall that with the C++ ActiveX control, we added a reference to the ActiveX control by pointing Visual Basic to the DLL that contained the control. For the C# ActiveX control, when adding the reference, import the type library (.tlb) file instead, which is one of the outputs of the C# build process.
The advantages of using C# to develop COM objects are two-fold. C# lets you leverage the power of the .NET Framework and provides a bridge between older technology and the future. For example, in C/C++, dealing with BSTRs and the like can be cumbersome, even though ATL helps alleviate some of the issues. Many of these issues melt away in the C# domain. Also, the source code is easier to read and digest than the corresponding C++ version. Wizard-generated code, with all of its mysterious macros, transient and temporary files, registration scripts and such lend an air of black magic to the code. The C# implementation is cleaner, and by extension easier to maintain.
One downside to using .NET interop is the overhead involved with data marshaling between the managed and unmanaged domains. However, through careful design, this performance hit can be mitigated. In practice, I have found the benefits to the implementation strategy described in this article to be significantas long as the component is not shuttling voluminous amounts of data, the gains in readability should offset any potential performance issues.
DDJ
'Declaration of the ActiveX control
Dim CppControl As Cpp_ActiveX.CSimpleActiveX
Private Sub btnAdd_Click()
' grab data from input text boxes
Dim a, b As Integer
a = CDbl(txtA.Text)
b = CDbl(txtB.Text)
txtResult.Text = CStr(CppControl.Add(a, b))
End Sub
Private Sub Form_Load()
' instantiate control
Set CppControl = New Cpp_ActiveX.CSimpleActiveX
End Sub
Back to articleusing System;
using System.Runtime.InteropServices; // for DispId & Guid attributes
namespace CSharp_ActiveX
{
/// <summary>
/// COM object Interface
/// </summary>
[Guid("3D6E75CD-C44F-46fd-9723-F833B366129F")]
public interface ICSharp_ActiveX
{
[DispId(1)] double Add(double a, double b);
}
/// <summary>
/// The object that implements the above COM interface
/// </summary>
[
Guid("1DBB9AEB-9333-408f-925C-4DE11599DEEF"),
ClassInterface(ClassInterfaceType.None)
]
public class CSharp_ActiveX_Class : ICSharp_ActiveX
{
public double Add(double a, double b)
{
return a+b;
}
}
}
Back to article