Integrating .NET Components into COM

I l @ ve RuBoard

Integrating .NET Components into COM

In addition to calling COM components from managed space, you can expose .NET components to unmanaged code using COM Interop. To do this, you must create a COM Callable Wrapper (CCW) for your managed component. Like an RCW, the CCW hides the differences between the .NET model and COM, making the .NET component appear as a regular COM component to COM clients . A COM client activates and accesses a .NET component through a CCW in the same way as it would use any other COM component.

A CCW is a nontrivial piece of code. It has to create and manage a .NET component on behalf of a COM client, keeping track of the number of references and arranging for the .NET component to be garbage collected when the last reference disappears. In other words, it provides the functionality of the IUnknown interface. The CCW might also need to implement other interfaces, such as IDispatch and IConnectionPoint , which will return information obtained using reflection to query the metadata of the .NET component. In addition to these interfaces, your .NET component might expose its own custom interfaces, and the CCW must map these to COM as well.

Designing .NET Components for COM Interop

The tools supplied by Microsoft can create CCWs from most .NET components, but it's good practice to follow a few guidelines when you design classes to be exposed to COM:

  • Stick to automation-compliant data types. Do not create methods that take nonautomation types as parameters or pass them back as return values. Not all COM clients will be able to consume COM interfaces exporting types that are not automation-compliant.

  • Try to use isomorphic types. This is less crucial than the previous point and might not always be possible, but it will help improve marshaling performance. You should provide marshaling information using MarshalAsAttribute for nonisomorphic types that are passed to COM clients.

  • Do not create static methods.

  • Do not define parameterized constructors.

  • Define and implement interfaces explicitly. This is good practice and means that the CCW will not have to generate its own interfaces for accessing your classes.

  • Implement event sink interfaces to provide access to managed events from COM. (See the following example.)

For example, consider the Cake class (in Cake.jsl in the CakeComponent project). This class implements three interfaces called ICake , IBaker , and IBakerEvents . The ICake interface defines four properties: Size , Shape , Filling (the usual suspects ), and Message (such as "Happy Anniversary"). The IBaker interface contains the method BakeCake , which takes a parameter indicating how long the cake should be baked and then raises the CakeBaked event when this period has expired . The CakeBaked event is defined through the event sink interface IBakerEvents .

Starting with the ICake interface, setting ComVisibleAttribute to true makes this interface visible to COM. In fact, interfaces and classes will be visible to COM by default, but it is still worthwhile to be explicit and use this attribute, if only for documentation purposes. Each interface must be tagged with a unique GUID (the COM IID), using GuidAttribute . Use the GuidGen utility that's supplied with Visual Studio .NET (and that's available in the Tools menu as the Create GUID command) to generate unique GUIDs.

Each property defined by the ICake interface is implemented as a pair of getter and setter methods. Remember that with J#, you must explicitly call these methods set_<XXX> and get_<XXX> and provide the @property directive. This interface is converted into a COM dual interface, and you can optionally specify dispatch IDs for each method and property using DispIdAttribute . The parameters of each setter method are input parameters and are tagged with InAttribute (the default). For methods that return output values, you should tag each output parameter with OutAttribute . The set_Message method takes a String parameter, and a MarshalAsAttribute is applied to ensure that it is marshaled correctly as a COM BSTR . The ICake interface is reproduced here:

 /**@attributeComVisibleAttribute(true)*/ /**@attributeGuidAttribute("EDEDB0B2-82CB-4120-B6F6-0633913C46B8")*/ publicinterfaceICake { /**@property*/ publicvoidset_Size(/**@attributeInAttribute()*/shortsize); /**@property*/ publicshortget_Size(); /**@property*/ publicvoidset_Shape(/**@attributeInAttribute()*/shortshape); /**@property*/ publicshortget_Shape(); /**@property*/ publicvoidset_Filling(/**@attributeInAttribute()*/shortfilling); /**@property*/ publicshortget_Filling(); /**@property*/ publicvoidset_Message(/**@attributeInAttribute()*/ /**@attributeMarshalAsAttribute(UnmanagedType.BStr)*/Stringmsg); /**@property*/ publicStringget_Message(); } 

The IBaker interface is likewise tagged with ComVisibleAttribute and Guid ­Attribute . Its single method, BakeCake , takes one input parameter:

 /**@attributeComVisibleAttribute(true)*/ /**@attributeGuidAttribute("9A0C6B4B-2D1F-4d42-983D-E9E84AFD7868")*/ publicinterfaceIBaker { publicvoidBakeCake(/**@attributeInAttribute()*/shorthowLongFor); } 

The IBakerEvents interface is the COM event sink interface, which effectively defines the COM connection points that clients can use to subscribe to events. Each event should define one method, providing the connection point for that event. When a COM client subscribes to an event, it attaches to this connection point and supplies an implementation of the named method (known as the event sink ). In this example, the BakeCake method of the IBaker class raises the CakeBaked event when the cake is baked. The IBakerEvents event sink interface therefore defines a method called CakeBaked . How this method is actually hooked up to the corresponding event will be revealed shortly. Methods that define event sinks can take parameters, although they should not return values. In addition, the event sink interface must be tagged with InterfaceTypeAttribute , specifying a parameter of ComInterfaceType.InterfaceIsIDispatch ” an event sink interface must be exposed to COM as an IDispatch interface.

 /**@attributeComVisibleAttribute(true)*/ /**@attributeGuidAttribute("6DF0B938-37D5-490b-8AF6-C285720F1DB2")*/ /**@attributeInterfaceTypeAttribute(ComInterfaceType.InterfaceIsIDispatch)*/ publicinterfaceIBakerEvents { //Eventsinkmethod publicvoidCakeBaked(); } 

The Cake class contains the code that implements these three interfaces. It is tagged with ComVisibleAttribute and is given a COM CLSID using GuidAttribute . The class is also given a human-readable ProgID using ProgIdAttribute . The ClassInterfaceAttribute specifies the type of interface the class exposes to COM. You can generate an IDispatch interface only, using the setting ClassInterfaceType.AutoDispatch , or a dual interface using the setting ClassInterfaceType.AutoDual . However, the recommended setting is ClassInterfaceType.None . This prevents an interface being automatically generated for the class and ensures that the class is accessible only through any interfaces it explicitly implements.

In this example, the Cake class implements ICake and IBaker . No class interface is generated, so the class can be accessed only through the ICake and IBaker interfaces. Also note that the order in which the interfaces are specified is significant: the first interface listed ( ICake ) will be the default interface for the class.

 /**@attributeComVisibleAttribute(true)*/ /**@attributeGuidAttribute("DA8ADFF6-22C4-4513-9E82-64B7CE3305C1")*/ /**@attributeProgIdAttribute("CakeComponent.Cake")*/ /**@attributeClassInterfaceAttribute(ClassInterfaceType.None)*/ //ICakeisthedefaultinterface publicclassCakeimplementsICake,IBaker { } 

"But," you scream, "what about the IBakerEvents interface?" Event interfaces are indicated by using ComSourceInterfacesAttribute . You list the name of the interface and the assembly for each event interface the class implements ”in this case, the CakeComponent.IBakerEvents interface in the CakeComponent assembly (the name of the assembly created by the project):

 /**@attributeComSourceInterfacesAttribute("CakeComponent.IBakerEvents, CakeComponent")*/ 

Much of the Cake class is reasonably straightforward ”it implements the properties of the ICake interface. The BakeCake method of the IBaker interface is a little more interesting. When invoked, this method uses a thread from the thread pool (see Chapter 8 for details) and uses it to execute the private method CakeBaker , passing the time to wait as a parameter:

 publicvoidBakeCake(shorthowLongFor) { //Spawnathreadthatwaitsforthespecifiedperiodoftime //andthenraisestheCakeBakedevent ThreadPool.QueueUserWorkItem(newWaitCallback(CakeBaker), newShort(howLongFor)); } 

We'll come back to the CakeBaker method shortly. First, let's look at how the event mechanism works.

You should remember that an event in J# is implemented using a delegate ( marked with the @delegate directive) and a pair of add_<XXX> and remove_<XXX> methods (marked with the @event directive) to allow a client to subscribe to and unsubscribe from the event. For the COM event sink mechanism to work with .NET, the name of the event must be the same as the name of the method in the event sink interface. In this example, the event sink method is called CakeBaked , so the name of the event is also CakeBaked , which is published in J# through a pair of methods called add_CakeBaked and remove_CakeBaked . The delegate type itself can be called almost anything ”here it's CakeBakedEventHandler ” but its signature must match that of the event sink method (no parameters, void return value in this case). The Cake class creates a private delegate variable called bakedEventHandler to hold the subscriptions to the event.

 /**@delegate*/ privatedelegatevoidCakeBakedEventHandler(); privateCakeBakedEventHandlerbakedEventHandler=null; /**@event*/ publicvoidadd_CakeBaked(CakeBakedEventHandlerhandler) { bakedEventHandler= (CakeBakedEventHandler)Delegate.Combine(bakedEventHandler,handler); } /**@event*/ publicvoidremove_CakeBaked(CakeBakedEventHandlerhandler) { bakedEventHandler= (CakeBakedEventHandler)Delegate.Remove(bakedEventHandler,handler); } 

Returning to the CakeBaker method, you can see that it sleeps for the designated period of time. (The current implementation actually waits for seconds rather than minutes, but you can change it if you want to wait 20 minutes for an event when you test the code!) When the method wakes up, it raises the CakeBaked event by calling Invoke on the delegate variable ( bakedEventHandler ). At this point, any clients acting as sinks will have their sink methods executed:

 privatevoidCakeBaker(ObjectbakeTimeObject) { ShortbakeTimeSeconds=(Short)bakeTimeObject; intbakeTimeMilliseconds=bakeTimeSeconds.shortValue()*1000; //Use60000forminutes //Waitforthecaketobake System.Threading.Thread.Sleep(bakeTimeMilliseconds); //RaisetheCakeBakedevent bakedEventHandler.Invoke(); } 

One final point: .NET classes that are exposed as COM servers must be placed where a COM client can find them. You can either deploy the COM server in the same folder as the client or you can place it in the GAC. If you want to go for the GAC option, you must sign the assembly ”by generating a key pair using the Sn utility and applying AssemblyKeyFileAttribute to the assembly.

The complete code for the Cake.jsl sample file can be found in the downloadable CakeComponent project.

Creating a COM Callable Wrapper

The Cake class is implemented as a class library, and the project is compiled into the file CakeComponent.dll. To make the Cake class available to COM, you must create the CCW. You can do this in two ways: by using the type library export utility (Tlbexp.exe) to create a COM type library or by using the assembly registration utility (Regasm.exe), which can also create a type library but also registers the component in the Windows Registry. The following command registers the component and creates the type library CakeComponent.tlb:

 RegasmCakeComponent.dll/tlb:CakeComponent.tlb 

You should then either copy CakeComponent.dll to the folder containing your COM client executable or place it in the GAC.

Note

If your COM clients are built using Visual Basic 6.0 or earlier, you should place the component DLL in the same folder as the Visual Basic runtime (\Program Files\Microsoft Visual Studio\VB98).


It is instructive to examine what the CCW actually produces for your component: You can place the CakeComponent.dll assembly in the GAC and use the OLE/COM Object Viewer from the Tools menu of Visual Studio .NET. If you expand the All Objects folder, you can look for the component with the ProgID CakeComponent.Cake and expand it. You'll see the various interfaces implemented through the CCW, as depicted in Figure 13-8.

Figure 13-8. The OLE/COM Object Viewer showing the CCW for CakeComponent.dll

You should have been expecting the IBaker and ICake interfaces because you defined these yourself. You can also see that the CCW created the standard COM IUnknown , IConnectionPointContainer , IProvideClassInfo , ISupportErrorInfo , and IMarshal interfaces. The IManagedObject interface, which is also created automatically as part of the CCW, provides what's called the Managed Object Interface. This interface defines two methods: GetSerializedBuffer , which you can use to obtain a serialized version of the object (as a BSTR ) if the object supports serialization, and GetObjectIdentity , which returns a three-part unique identifier (a GUID, the ID of the application domain, and the ID of the CCW) for the object if you need to perform a managed object equality test in your COM code. The IDL definition for IManagedObject is shown here:

 [ odl, uuid(C3FCC19E-A970-11D2-8B5A-00A0C9B7C9C4), helpstring("ManagedObjectInterface"), oleautomation ] interfaceIManagedObject:IUnknown{ HRESULT_stdcallGetSerializedBuffer([out]BSTR*pBSTR); HRESULT_stdcallGetObjectIdentity([out]BSTR*pBSTRGUID, [out]int*AppDomainID, [out]int*pCCW); }; 

The _Object interface provides access to the System.Object methods available to all managed objects ” ToString , Equals , GetHashCode , and GetType , as shown below. (The ReferenceEquals method is omitted because it does not translate to COM very well ”if you need to determine whether two references refer to the same managed COM object, use the GetObjectIdentity method of the IManagedObject interface.) The java.lang.Object methods are not exposed. A COM client can use this interface to execute System.Object methods against a managed COM component.

 [ odl, uuid(65074F7F-63C0-304E-AF0A-D51741CB4A8D), hidden, dual, nonextensible, oleautomation, custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9,System.Object) ] interface_Object:IDispatch{ [id(00000000),propget, custom(54FC8F55-38DE-4703-9C4E-250351302B1C,1)] HRESULTToString([out,retval]BSTR*pRetVal); [id(0x60020001)] HRESULTEquals([in]VARIANTobj, [out,retval]VARIANT_BOOL*pRetVal); [id(0x60020002)] HRESULTGetHashCode([out,retval]long*pRetVal); [id(0x60020003)] HRESULTGetType([out,retval]_Type**pRetVal); }; 

Finally, the two interfaces ICake and IBaker contain the methods and properties you implemented in the Cake class. Figure 13-9 shows the ICake interface in the OLE/COM Object Viewer. You'll be comforted to see that the J# properties have been translated into COM. (The get_ and set_ prefixes of the J# methods have disappeared, and instead the propget and propput IDL attributes have been applied, together with paired DispIDs.)

Figure 13-9. The IDL implementation of the ICake interface

For completeness, Figure 13-10 shows the IBaker interface, which comprises the single method BakeCake .

Figure 13-10. The IDL implementation of the IBaker interface

Testing the CCW

To test that the CCW works as expected, the CakeClient Visual Basic 6.0 project contains a form that instantiates a Cake object, sets its properties, calls the BakeCake method through the IBaker interface, and registers an event handler for the CakeBaked event. The Cake object is defined using the Visual Basic WithEvents keyword. (This allows an object to use the COM connection point mechanism without having to manually query the various interfaces and connect them together; the object instead defines methods of the form object_event that the Visual Basic runtime will automatically connect as COM event sinks.) The CakeBaked event is handled by the CakeObject_CakeBaked method.

To use the CakeComponent , you must set a reference to the type library in the References dialog box. (Choose References from the Project menu, scroll down to CakeComponent, and select it, as shown in Figure 13-11.)

Figure 13-11. The Visual Basic 6.0 Project References dialog box

If you compile and run the program, the Cake Client form will appear. You can set the size of the cake, its shape, and the filling, and you can type a message to be iced on the cake. The Order button invokes the cmdOrder_Click method, which instantiates a new Cake object, sets its properties, and then retrieves them again to display the results in a message box, as shown in Figure 13-12.

Figure 13-12. The Cake Client program after you create a new cake

At this point, you can set the time required to bake the cake and click the Bake button. (These controls are disabled until the cake is created.) After the designated number of minutes (or seconds, if you left the Cake class unchanged earlier), the sleep operation in the Cake component will expire and raise the CakeBaked event, triggering the CakeObject_CakeBaked method in the Visual Basic code and displaying a message box (as shown in Figure 13-13).

Figure 13-13. The cake has been baked.

I l @ ve RuBoard


Microsoft Visual J# .NET (Core Reference)
Microsoft Visual J# .NET (Core Reference) (Pro-Developer)
ISBN: 0735615500
EAN: 2147483647
Year: 2002
Pages: 128

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