Using A .NET Component From A COM Client


So far you have seen how to access a COM component from a .NET client. Equally interesting is to find a solution for accessing .NET components in an old COM client that is using Visual Basic 6, MFC, or ATL.

COM Callable Wrapper

If you want to access a COM component with a .NET client, you have to work with an RCW. To access a.NET component from a COM client application, you must use a COM Callable Wrapper (CCW). Figure 33-13 shows a CCW that wraps a .NET class, and offers COM interfaces that a COM client expects to use. The CCW offers interfaces such as IUnknown, IDispatch, ISupportErrorInfo, and others. It also offers interfaces such as IConnectionPointContainer and IConnectionPoint for events. A COM client gets what it expects from a COM object — although a .NET component is behind the scenes.

image from book
Figure 33-13

The wrapper deals with methods such as AddRef(), Release(), and QueryInterface() from the IUnknown interface, whereas in the .NET object you can count on the garbage collector without the need to deal with reference counts.

Creating a .NET Component

In the following example, you build the same functionality into a .NET class that you have previously built into a COM component. Start by creating a C# class library, and name it DotNetComponent. Then add the interfaces IWelcome and IMath, and the class NetComponent that implements these interfaces:

 using System; using System.Runtime.InteropServices; namespace Wrox.ProCSharp.COMInterop.Server { public interface IWelcome { string Greeting(string name); } public interface IMath { int Add(int val1, int val2); int Sub(int val1, int val2); } public class DotnetComponent : IWelcome, IMath { public DotnetComponent() { } public string Greeting(string name) { return "Hello " + name; } public int Add(int val1, int val2) { return val1 + val2; } public int Sub(int val1, int val2) { return val1 - val2; } } } 

After building the project, you can create a type library.

Creating a Type Library

A type library can be created by using the command-line utility tlbexp. The command

 tlbexp DotnetComponent.dll 

creates the type library DotnetComponent.tlb. You can view the type library with the utility OLE/ COM Object Viewer. To access this utility in Visual Studio, select Tools OLE/COM Object Viewer. Next, select File View TypeLib to open the type library. Now you can see the interface definition shown in the following code. The unique ids will differ.

The name of the type library is created from the name of the assembly. The header of the type library also defines the full name of the assembly in a custom attribute, and all the interfaces are forward- declared before they are defined:

 // Generated .IDL file (by the OLE/COM Object Viewer) //  // typelib filename: <could not determine filename> [ uuid(), version(1.0), custom(, DotNetComponent,  Version=1.0.1321.19165, Culture=neutral, PublicKeyToken=null) ] library DotnetComponent { // TLib : Common Language Runtime Library :  // {} importlib("mscorlib.tlb"); // TLib : OLE Automation : {} importlib("stdole2.tlb"); // Forward declare all types defined in this typelib interface IWelcome; interface IMath; interface _Settings; interface _DotnetComponent; 

In the following generated code, you can see that the interfaces IWelcome and IMath are defined as COM dual interfaces. You can see all methods that have been declared in the C# code are listed here in the type library definition. The parameters changed: the .NET types are mapped to COM types (such as the String class to the BSTR type), and the signature is changed, so that a HRESULT is returned. Because the interfaces are dual, dispatch ids are also generated:

 [ odl, uuid(), version(1.0), dual, oleautomation, custom(,  Wrox.ProCSharp.COMInterop.Server.IWelcome) ] interface IWelcome : IDispatch { [id(0x60020000)] HRESULT Greeting([in] BSTR name, [out, retval] BSTR* pRetVal); }; [ odl, uuid(), version(1.0), dual, oleautomation, custom(,  Wrox.ProCSharp.COMInterop.Server.IMath) ] interface IMath : IDispatch { [id(0x60020000)] HRESULT Add([in] long val1, [in] long val2,  [out, retval] long* pRetVal); [id(0x60020001)] HRESULT Sub([in] long val1, [in] long val2,  [out, retval] long* pRetVal); }; 

The coclass section marks the COM object itself. The uuid in the header is the CLSID used to instantiate the object. The class DotnetComponent supports the interfaces _DotnetComonent, _Object, IWelcome, and IMath. _Object is defined in the file mscorlib.tlb included in an earlier code section and offers the methods of the base class Object. The default interface of the component is _DotnetComponent, which is defined after the coclass section as a dispatch interface. In the interface declaration it is marked as dual, but because no methods are included, it is a dispatch interface. With this interface it is possible to access all methods of the component using late binding:

 [ uuid(), version(1.0), custom(, Wrox.ProCSharp.COMInterop.Server.DotnetComponent) ] coclass DotnetComponent { [default] interface _DotnetComponent; interface _Object; interface IWelcome; interface IMath; }; [ odl, uuid(), hidden, dual, oleautomation, custom(, Wrox.ProCSharp.COMInterop.Server.DotnetComponent) ] interface _DotnetComponent : IDispatch { }; }; 

There are quite a few defaults for generating the type library. However, often it is advantageous to change some of the default .NET to COM mappings. This can be done with several attributes in the System.Runtime.InteropServices namespaces.

COM Interop Attributes

Applying attributes from the namespace System.Runtime.InteropServices to classes, interfaces, or methods allows you to change the implementation of the CCW. The following table lists these attributes and a description.

Attribute

Description

Guid

This attribute can be assigned to the assembly, interfaces, and classes. Using the Guid as an assembly attribute defines the type-library id, applying it to interfaces defines the interface id (IID), and setting the attribute to a class defines the class id (CLSID).

The unique ids needed to be defined with this attribute can be created with the utility guidgen.

The CLSID and type-library ids are changed automatically with every build. If you don't want to change it with every build, you can fix it by using this attribute. The IID is only changed if the signature of the interface changes, for example, a method is added or removed, or some parameters changed. Because with COM the IID should change with every new version of this interface, this is a very good default behavior, and usually there's no need to apply the IID with the Guid attribute. The only time you want to apply a fixed IID for an interface is when the .NET interface is an exact representa- tion of an existing COM interface, and the COM client already expects this identifier.

ProgId

This attribute can be applied to a class to specify what name should be used when the object is configured into the registry.

ComVisible

This attribute enables you to hide classes, interfaces, and delegates from COM when set to false. This prevents a COM representation from being created.

InterfaceType

This attribute, if set to a ComInterfaceType enumeration value, enables you to modify the default dual interface type that is created for .NET interfaces. ComInterfaceType has the values InterfaceIsDual, InterfaceIsIDispatch, and InterfaceIsIUnknown. If you want to apply a custom interface type to a .NET interface, set the attribute like this: [InterfaceType(ComInterfaceType.InterfaceIsIUnkwnown)]

ClassInterface

This attribute enables you to modify the default dispatch interface that is created for a class. ClassInterface accepts an argument of a ClassInterfaceType enumeration. The possible values are AutoDispatch, AutoDual, and None. In the previous example you have seen that the default is AutoDispatch, since a dispatch interface is created. If the class should only be accessible by the defined interfaces, apply the attribute [ClassInterface(ClassInterfaceType.None)] to the class.

DispId

This attribute can be used with dual and dispatch interfaces to define the dispid of methods and properties.

In

COM allows specifying attributes to parameter types if the parameter

Out

should be sent to the component [In], from the component to the client [Out], or in both directions [In, Out].

Optional

Parameters of COM methods may be optional. Parameters that should be optional can be marked with the Optional attribute.

Now you can change the C# code to specify a dual interface type for the IWelcome interface and a custom interface type for the IMath interface. With the class DotnetComponent the attribute ClassInterface with the argument ClassInterfaceType.None defines that no separate COM interface will be generated. The attributes ProgId and Guid specifiy a prog id and a guid:

 [InterfaceType(ComInterfaceType.InterfaceIsDual)]  public interface IWelcome  { [DispId(60040)] string Greeting(string name); } [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]  public interface IMath  {   int Add(int val1, int val2);   int Sub(int val1, int val2); } [ClassInterface(ClassInterfaceType.None)] [ProgId("Wrox.DotnetComponent")] [Guid("")]  public class DotnetComponent : IWelcome, IMath  {     public DotnetComponent()    {    }

Rebuilding the class library and the type library changes the interface definition. You can verify this with OleView.exe. As you can see in the following IDL code, the interface IWelcome is still a dual interface, whereas the IMath interface now is a custom interface that derives from IUnknown instead of IDispatch. In the coclass section, the interface _DotnetComponent is removed, and now the IWelcome is the new default interface, because it was the first interface in the inheritance list of the class DotnetComponent:

 // Generated .IDL file (by the OLE/COM Object Viewer) //  // typelib filename: <could not determine filename> [  uuid(),  version(1.0),  custom(, DotNetComponent,  Version=1.0.1321.28677, Culture=neutral, PublicKeyToken=null) ] library DotnetComponent { // TLib : Common Language Runtime Library :  // {} importlib("mscorlib.tlb"); // TLib : OLE Automation : {} importlib("stdole2.tlb"); // Forward declare all types defined in this typelib interface IWelcome; interface IMath; interface _Settings; [ odl, uuid(), version(1.0), dual, oleautomation, custom(, Wrox.ProCSharp.COMInterop.Server.IWelcome) ] interface IWelcome : IDispatch { [id(0x0000ea88)] HRESULT Greeting([in] BSTR name, [out, retval] BSTR* pRetVal); }; [ odl, uuid(), version(1.0), oleautomation, custom(, Wrox.ProCSharp.COMInterop.Server.IMath) ] interface IMath : IUnknown { HRESULT _stdcall Add([in] long val1, [in] long val2, [out, retval] long* pRetVal); HRESULT _stdcall Sub([in] long val1, [in] long val2, [out, retval] long* pRetVal); }; [ uuid(), version(1.0), custom(, Wrox.ProCSharp.COMInterop.Server.DotnetComponent) ] coclass DotnetComponent { interface _Object; [default] interface IWelcome; interface IMath; }; };

COM Registration

Before the .NET component can be used as a COM object, it is necessary to configure it in the registry. Also, if you don't want to copy the assembly into the same directory as the client application, it is necessary to install the assembly in the global assembly cache. The global assembly cache itself is discussed in Chapter 15.

For installing the assembly into the global assembly cache, you must sign it with a strong name (using Visual Studio 2005 you can define a strong name within properties of the solution). Then you can register the assembly in the global assembly cache:

 gacutil –i dotnetcomponent.dll 

Now you can use the regasm utility to configure the component inside the registry. The option /tlb extracts the type library, and also configures the type library in the registry:

 regasm dotnetcomponent.dll /tlb 

The information for the .NET component that is written to the registry is as follows. All COM configuration is in the hive HKEY_CLASSES_ROOT (HKCR). The key of the prog id (in the case of this example, it is Wrox.DotnetComponent) is written directly to this hive, along with the CLSID.

The key HKCR\CLSID\{CLSID}\InProcServer32 has the following entries:

  • mscoree.dll: mscoree.dll represents the CCW. This is a real COM object that is responsible for hosting the .NET component. This COM object accesses the .NET component to offer COM behavior for the client. The file mscoree.dll is loaded and instantiated from the client via the normal COM instantiation mechanism.

  • ThreadingModel=Both: This is an attribute of the mscoree.dll COM object. This component is programmed in a way to offer support both for STA and MTA.

  • Assembly=DotnetComponent, Version=1.0.1321.33886, Culture=neutral, PublicKeyToken= 5cd57c93b4d9c41a: The value of the Assembly stores the assembly full name including the version number and the public key token, so that the assembly can be uniquely identified. The assembly registered here will be loaded by mscoree.dll.

  • Class=Wrox.ProCSharp.COMInterop.Server.DotnetComponent: The name of the class will also be used by mscoree.dll. This is the class that will be instantiated.

  • RuntimeVersion=v2.0.50110: The registry entry RuntimeVersion specifies the version of the .NET runtime that will be used to host the .NET assembly.

In addition to the configurations shown here, all the interfaces and the type library are configured with their identifiers, too.

Creating a COM Client

Now it's time to create a COM client. Start by creating a simple C++ Win32 Console Application Project, and name it COMClient. You can leave the default options selected, and click Finish in the project wizard.

In the beginning of the file COMClient.cpp, add a preprocessor command to include the <iostream> header file and to import the type library that you created for the .NET component. The import statement creates a "smart pointer" class that makes it easier to deal with COM objects. During a build process, the import statement creates .tlh and .tli files that you can find in the debug directory of your project, which includes the smart pointer class. Then add using namespace directives to open the namespace std that will be used for writing output messages to the console, and the namespace DotnetComponent that is created inside the smart pointer class:

 // COMClient.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <iostream> #import "../DotNetComponent/bin/debug/DotnetComponent.tlb" using namespace std; using namespace DotnetComponent; 

In the _tmain() method, the first thing to do before any other COM call is initialization of COM with the API call CoInitialize(). CoIntialize() creates and enters an STA for the thread. The variable spWelcome is of type IWelcomePtr, which is a smart pointer. The smart pointer method CreateInstance() accepts the prog id as an argument to create the COM object by using the COM API CoCreateInstance(). The operator -> is overridden with the smart pointer, so that you can invoke the methods of the COM object such as Greeting():

 int _tmain(int argc, _TCHAR* argv[]) { HRESULT hr; hr = CoInitialize(NULL); try { IWelcomePtr spWelcome; hr = spWelcome.CreateInstance("Wrox.DotnetComponent");   // CoCreateInstance() cout << spWelcome->Greeting("Bill") << endl; 

The second interface supported by your .NET component is IMath, and there is also a smart pointer that wraps the COM interface: IMathPtr. You can directly assign one smart pointer to another as in spMath = spWelcome;. In the implementation of the smart pointer (the = operator is overridden), the QueryInterface() method is called. With a reference to the IMath interface you can call the Add() method.

 IMathPtr spMath; spMath = spWelcome;// QueryInterface() long result = spMath->Add(4, 5); cout << "result:" << result << endl;  } 

In case an HRESULT error value is returned by the COM object (this is done by the CCW that returns HRESULT errors if the .NET component generates exceptions), the smart pointer wraps the HRESULT errors and generates _com_error exceptions instead. Errors are handled in the catch block. At the end of the program, the COM DLLs are closed and unloaded using CoUninitialize():

 catch (_com_error& e) { cout << e.ErrorMessage() << endl; } CoUninitialize(); return 0; } 

Now you can run the application, and you will get outputs from the Greeting() and the Add() methods to the console. You can also try to debug into the smart pointer class, where you can see the COM API calls directly.

Important

In case you get an exception that the component cannot be found, check if the same version of the assembly that is configured in the registry is installed in the global assembly cache.

Adding Connection Points

Adding support for COM events to the .NET components requires some changes to the implementation of your .NET class. Offering COM events is not a simple usage of the event and delegate keywords, it is necessary to add some more COM interop attributes.

First, you have to add an interface to the .NET project: IMathEvents. This interface is the source or outgoing interface for the component, and will be implemented by the sink object in the client. A source interface must be either a dispatch or a custom interface. A scripting client supports only dispatch interfaces. Dispatch interfaces are usually preferred as source interfaces:

 [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] public interface IMathEvents { [DispId(46200)] void CalculationCompleted(); } 

Next, you have to add a delegate. The delegate must have the same signature and return type as the method in the outgoing interface. If you have multiple methods in your source interface, for each one that differs with the arguments, you have to specify a separate delegate. Because the COM client does not have to access this delegate directly, the delegate can be marked with the attribute [ComVisible(false)]:

 [ComVisible(false)]  public delegate void CalculationCompletedDelegate(); 

With the class DotnetComponent, a source interface must be specified. This can be done with the attribute [ComSourceInterfaces]. Add the attribute [ComSourceInterfaces] and specify the outgoing interface declared earlier. You can add more than one source interface with different constructors of the attribute class; however, the only client language that supports more than one source interface is C++. Visual Basic 6 clients only support one source interface.

[ClassInterface(ClassInterfaceType.None)] [ProgId("Wrox.DotnetComponent")] [Guid("")] [ComSourceInterfaces(typeof(IMathEvents))] public class DotnetComponent : IWelcome, IMath {    public DotnetComponent()    {    } 

Inside the class DotnetComponent, you have to declare an event for every method of the source interface. The type of the method must be the name of the delegate, and the name of the event must be exactly the name of the method inside the source interface. You can add the event calls to the Add() and Sub() methods. This step is the normal .NET way to invoke events, as discussed in Chapter 6.

 public event CalculationCompletedDelegate CalculationCompleted;    public int Add(int val1, int val2)    { int result = val1 + val2; if (CalculationCompleted != null) CalculationCompleted(); return result;    }    public int Sub(int val1, int val2)    { int result = val1 - val2; if (CalculationCompleted != null) CalculationCompleted(); return result;    } }



Professional C# 2005
Pro Visual C++ 2005 for C# Developers
ISBN: 1590596080
EAN: 2147483647
Year: 2005
Pages: 351
Authors: Dean C. Wills

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net