Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

.pdf
Скачиваний:
78
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

1302 APPENDIX A COM AND .NET INTEROPERABILITY

when a COM object exposes COM events, the interop assembly will contain additional CIL code that is used by the CLR to map COM events to .NET events (you’ll see them in action in just a bit).

Building Our C# Client Application

Given that the CLR will automatically create the necessary RCW at runtime, our C# application can program directly against the CoCar, CarType, Engine, and IDriveInfo types as if they were all implemented using managed code. Here is the complete implementation, with analysis to follow:

// Be sure to import the Vb6ComCarServer namespace. class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** CoCar Client App *****");

//Create the COM class using early binding.

CoCar myCar = new CoCar();

//Handle the BlewUp event.

myCar.BlewUp += new __CoCar_BlewUpEventHandler(myCar_BlewUp);

//Call the Create() method. myCar.Create(50, 10, CarType.BMW);

//Set name of driver.

IDriverInfo itf = (IDriverInfo)myCar; itf.DriverName = "Fred";

Console.WriteLine("Drive is named: {0}", itf.DriverName);

// Print type of car.

Console.WriteLine("Your car is a {0}.", myCar.CarMake); Console.WriteLine();

//Get the Engine and print name of the cylinders.

Engine eng = myCar.GetEngine(); Console.WriteLine("Your Cylinders are named:"); string[] names = (string[])eng.GetCylinders(); foreach (string s in names)

{

Console.WriteLine(s);

}

Console.WriteLine();

//Speed up car to trigger event.

for (int i = 0; i < 5; i++)

{

myCar.SpeedUp();

}

}

// Handler for the BlewUp event. static void myCar_BlewUp()

{

Console.WriteLine("Your car is toast!");

}

}

APPENDIX A COM AND .NET INTEROPERABILITY

1303

Notice that when we call GetCylinders(), we are casting the return value into an array of strings. The reason is the fact that COM arrays are (most often) represented by the SAFEARRAY COM type (which is always the case when building COM applications using VB6). The RCW will map SAFEARRAY types into a System.Array object, rather than automatically mapping SAFEARRAYs into an array represented with C# syntax. Thus, by casting the Array object into a string[], we can process the array more naturally.

Interacting with the CoCar Type

Recall that when we created the VB6 CoCar, we defined and implemented a custom COM interface named IDriverInfo, in addition to the automatically generated default interface (_CoCar) created by the VB6 compiler. When our Main() method creates an instance of CoCar, we only have direct access to the members of the _CoCar interface, which as you recall will be composed by each public member of the COM class:

// Here, you are really working with the [default] interface. myCar.Create(50, 10, CarType.BMW);

Given this fact, in order to invoke the DriverName property of the IDriverInfo interface, we must explicitly cast the CoCar object to an IDriverInfo interface as follows:

// Set name of driver.

IDriverInfo itf = (IDriverInfo)myCar; itf.DriverName = "Fred";

Console.WriteLine("Drive is named: {0}", itf.DriverName);

Recall, however, that when a type library is converted into an interop assembly, it will contain Class-suffixed types that expose every member of every interface. Therefore, if you so choose, you could simplify your programming if you create and make use of a CoCarClass object, rather than a CoCar object. For example, consider the following subroutine, which makes use of members of the default interface of CoCar as well as members of IDriverInfo:

static void UseCar()

{

//-Class suffix types expose all

//members from all interfaces.

CoCarClass c = new CoCarClass();

//This property is a member of IDriverInfo. c.DriverName = "Mary";

//This method is a member of _CoCar. c.SpeedUp();

}

If you are wondering exactly how this single type is exposing members of each implemented interface, check out the list of implemented interfaces and the base class of CoCarClass using the Visual Studio 2008 Object Browser (see Figure A-14).

1304 APPENDIX A COM AND .NET INTEROPERABILITY

Figure A-14. The composition of CoCarClass

As you can see, this type implements the _CoCar and _IDriverInfo interfaces and exposes them as “normal” public members.

Intercepting COM Events

In Chapter 11, you learned about the .NET event model. Recall that this architecture is based on delegating the flow of logic from one part of the application to another. The entity in charge of forwarding a request is a type deriving from System.MulticastDelegate, which we create indirectly in C# using the delegate keyword.

When the tlbimp.exe utility encounters event definitions in the COM server’s type library, it responds by creating a number of managed types that wrap the low-level COM connection point architecture. Using these types, you can pretend to add a member to a System.MulticastDelegate’s internal list of methods. Under the hood, of course, the proxy is mapping the incoming COM event to their managed equivalents. Table A-3 briefly describes these types.

Table A-3. COM Event Helper Types

Generated Type (Based on the

 

_CarEvents [source] Interface)

Meaning in Life

__CoCar_Event

This is a managed interface that defines the add and remove

 

members used to add (or remove) a method to (or from) the

 

System.MulticastDelegate’s linked list.

__CoCar_BlewUpEventHandler

This is the managed delegate (which derives from

 

System.MulticastDelegate).

__CoCar_SinkHelper

This generated class implements the outbound interface in a

 

.NET-aware sink object.

 

 

As you would hope, you are able to handle the incoming COM events in the same way you handle events based on the .NET delegation architecture:

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** CoCar Client App *****");

CoCar myCar = new CoCar();

APPENDIX A COM AND .NET INTEROPERABILITY

1305

// Handle the BlewUp event.

myCar.BlewUp += new __CoCar_BlewUpEventHandler(myCar_BlewUp);

...

}

// Handler for the BlewUp event. static void myCar_BlewUp()

{

Console.WriteLine("Your car is toast!");

}

}

It is also worth pointing out if your C# code base is able to make use of all of the event-centric notations (anonymous methods, method group conversion, lambda expressions, etc.) when intercepting events from COM objects.

Source Code The CSharpCarClient project is included under the Appendix A subdirectory.

That wraps up our investigation of how a .NET application can communicate with a legacy COM application. Now be aware that the techniques you have just learned would work for any COM server at all. This is important to remember, given that many COM servers might never be rewritten as native .NET applications. For example, the object model of Microsoft Outlook is currently exposed as a COM library. Thus, if you needed to build a .NET program that interacted with this product, the interoperability layer is (currently) mandatory.

Understanding COM to .NET Interoperability

The next topic of this appendix is to examine the process of a COM application communicating with a .NET type. This “direction” of interop allows legacy COM code bases (such as an existing VB6 project) to make use of functionality contained within newer .NET assemblies. As you might imagine, this situation is less likely to occur than .NET to COM interop; however, it is still worth exploring.

For a COM application to make use of a .NET type, we somehow need to fool the COM program into believing that the managed .NET type is in fact unmanaged. In essence, you need to allow the COM application to interact with the .NET type using the functionality required by the COM architecture. For example, the COM type should be able to obtain new interfaces through QueryInterface() calls, simulate unmanaged memory management using AddRef() and Release(), make use of the COM connection point protocol, and so on.

Beyond fooling the COM client, COM to .NET interoperability also involves fooling the COM runtime. A COM server is activated using the COM runtime rather than the CLR. For this to happen, the COM runtime must look up numerous bits of information in the system registry (ProgIDs, CLSIDs, IIDs, and so forth). The problem, of course, is that .NET assemblies are not registered in the registry in the first place!

Given these points, to make your .NET assemblies available to COM clients, you must take the following steps:

1.Register your .NET assembly in the system registry to allow the COM runtime to locate it.

2.Generate a COM type library (*.tlb) file (based on the .NET metadata) to allow the COM client to interact with the public types.

3.Deploy the assembly in the same directory as the COM client or (more typically) install it into the GAC.

1306 APPENDIX A COM AND .NET INTEROPERABILITY

As you will see, these steps can be performed using Visual Studio 2008 or at the command line using various tools that ship with the .NET Framework 3.5 SDK.

The Attributes of System.Runtime.InteropServices

In addition to performing these steps, you will typically also need to decorate your C# types with various .NET attributes, all of which are defined in the System.Runtime.InteropServices namespace. These attributes ultimately control how the COM type library is created and therefore control how the COM application is able to interact with your managed types. Table A-4 documents some (but not all) of the attributes you can use to control the generated COM type library.

Table A-4. Select Attributes of System.Runtime.InteropServices

.NET Interop Attribute

Meaning in Life

[ClassInterface]

Used to create a default COM interface for a .NET class type.

[ComClass]

This attribute is similar to [ClassInterface], except it also provides the

 

ability to establish the GUIDs used for the class ID (CLSID) and interface

 

IDs of the COM types within the type library.

[DispId]

Used to hard-code the DISPID values assigned to a member for purposes

 

of late binding.

[Guid]

Used to hard-code a GUID value in the COM type library.

[In]

Exposes a member parameter as an input parameter in COM IDL.

[InterfaceType]

Used to control how a .NET interface should be exposed to COM

 

(IDispatch-only, dual, or IUnknown-only).

[Out]

Exposes a member parameter as an output parameter in COM IDL.

 

 

Now do be aware that for simple COM to .NET interop scenarios, you are not required to adorn your .NET code with dozens of attributes in order to control how the underlying COM type library is defined. However, when you need to be very specific regarding how your .NET types will be exposed to COM, the more you understand COM IDL attributes the better, given that the attributes defined in System.Runtime.InteropServices are little more than managed definitions of these IDL keywords.

The Role of the CCW

Before we walk through the steps of exposing a .NET type to COM, let’s take a look at exactly how COM programs interact with .NET types using a COM Callable Wrapper, or CCW. As you have seen, when a .NET program communicates with a COM type, the CLR creates a Runtime Callable Wrapper. In a similar vein, when a COM client accesses a .NET type, the CLR makes use of an intervening proxy termed the COM Callable Wrapper to negotiate the COM to .NET conversion (see Figure A-15). Like any COM object, the CCW is a reference-counted entity. This should make sense, given

that the COM client is assuming that the CCW is a real COM type and thus must abide by the rules of AddRef() and Release(). When the COM client has issued the final release, the CCW releases its reference to the real .NET type, at which point it is ready to be garbage collected.

APPENDIX A COM AND .NET INTEROPERABILITY

1307

Figure A-15. COM types talk to .NET types using a CCW.

The CCW implements a number of COM interfaces automatically to further the illusion that the proxy represents a genuine coclass. In addition to the set of custom interfaces defined by the

.NET type (including an entity termed the class interface that you examine in just a moment), the CCW provides support for the standard COM behaviors described in Table A-5.

Table A-5. The CCW Supports Many Core COM Interfaces

CCW-Implemented Interface

Meaning in Life

IConnectionPoint

If the .NET type supports any events, they are represented as COM

IConnectionPointContainer

connection points.

IEnumVariant

If the .NET type supports the IEnumerable interface, it appears to

 

the COM client as a standard COM enumerator.

IErrorInfo

These interfaces allow coclasses to send COM error objects.

ISupportErrorInfo

 

ITypeInfo

These interfaces allow the COM client to pretend to manipulate an

IProvideClassInfo

assembly’s COM type information. In reality, the COM client is

 

interacting with .NET metadata.

IUnknown

These core COM interfaces provide support for early and late

IDispatch

binding to the .NET type.

IDispatchEx

 

 

 

The Role of the .NET Class Interface

In classic COM, the only way a COM client can communicate with a COM object is to use an interface reference. In contrast, .NET types do not need to support any interfaces whatsoever, which is clearly a problem for a COM caller. Given that classic COM clients cannot work with object references, another responsibility of the CCW is to expose a class interface to represent each member defined by the type’s public sector. As you can see, the CCW is taking the same approach as Visual Basic 6.0!

1308 APPENDIX A COM AND .NET INTEROPERABILITY

Defining a Class Interface

To define a class interface for your .NET types, you will need to apply the [ClassInterface] attribute on each public class you wish to expose to COM. Again, doing so will ensure that each public member of the class is exposed to a default autogenerated interface that follows the same exact naming convention as VB6 (_NameOfTheClass). Technically speaking, applying this attribute is optional; however, you will almost always wish to do so. If you do not, the only way the COM caller can communicate with the type is using late binding (which is far less type safe and typically results in slower performance).

The [ClassInterface] attribute supports a named property (ClassInterfaceType) that controls exactly how this default interface should appear in the COM type library. Table A-6 defines the possible settings.

Table A-6. Values of the ClassInterfaceType Enumeration

ClassInterfaceType Member Name

Meaning in Life

AutoDispatch

Indicates the autogenerated default interface will only

 

support late binding, and is equivalent to not applying the

 

[ClassInterface] attribute at all.

AutoDual

Indicates that the autogenerated default interface is a “dual

 

interface” and can therefore be interacted with using early

 

binding or late binding. This would be the same behavior

 

taken by VB6 when it defines a default COM interface.

None

Indicates that no interface will be generated for the class.

 

This can be helpful when you have defined your own

 

strongly typed .NET interfaces that will be exposed to COM,

 

and do not wish to have the “freebie” interface.

 

 

In the next example, you specify ClassInterfaceType.AutoDual as the class interface designation. In this way, late-binding clients such as VBScript can access the Add() and Subtract() methods using IDispatch, while early-bound clients (such as VB6 or C++) can use the class interface (named

_VbDotNetCalc).

Building Your .NET Types

To illustrate a COM type communicating with managed code, assume you have created a simple C# Class Library project named ComCallableDotNetServer, which defines a class named DotNetCalc. This class will define two simple methods named Add() and Subtract(). The implementation logic is trivial; however, notice the use of the [ClassInterface] attribute:

//We need this to obtain the necessary

//interop attributes.

using System.Runtime.InteropServices;

namespace ComCallableDotNetServer

{

[ClassInterface(ClassInterfaceType.AutoDual)] public class DotNetCalc

{

public int Add(int x, int y) { return x + y; }

APPENDIX A COM AND .NET INTEROPERABILITY

1309

public int Subtract(int x, int y) { return x - y; }

}

}

As mentioned earlier in this appendix, in the world of COM, just about everything is identified using a 128-bit number termed a GUID. These values are recorded into the system registry in order to define an identity of the COM type. Here, we have not specifically defined GUID values for our DotNetCalc class, and therefore the type library exporter tool (tlbexp.exe) will generate GUIDs on the fly. The problem with this approach, of course, is that each time you generate the type library (which we will do shortly), you receive unique GUID values, which can break existing COM clients.

To define specific GUID values, you may make use of the guidgen.exe utility, which is accessible from the Tools Create Guid menu item of Visual Studio 2008. Although this tool provides four GUID formats, the [Guid] attribute demands the GUID value be defined using the Registry Format option, as shown in Figure A-16.

Figure A-16. Obtaining a GUID value

Once you copy this value to your clipboard (via the Copy GUID button), you can then paste it in as an argument to the [Guid] attribute. Be aware that you must remove the curly brackets from the GUID value! This being said, here is our updated DotNetCalc class type (your GUID value will differ):

[ClassInterface(ClassInterfaceType.AutoDual)]

[Guid("4137CFAB-530B-4667-ADF2-8E2CD63CB462")] public class DotNetCalc

{

public int Add(int x, int y) { return x + y; }

public int Subtract(int x, int y) { return x - y; }

}

On a related note, click the Show All Files button on the Solution Explorer and open up the AssemblyInfo.cs file located under the Properties icon. By default, all Visual Studio 2008 project workspaces are provided with an assembly-level [Guid] attribute used to identify the GUID of the type library generated based on the .NET server (if exposed to COM).

1310 APPENDIX A COM AND .NET INTEROPERABILITY

// The following GUID is for the ID of the typelib if this project is exposed to COM

[Assembly: Guid("EB268C4F-EB36-464C-8A25-93212C00DC89")]

Defining a Strong Name

As a best practice, all .NET assemblies that are exposed to COM should be assigned a strong name and installed into the global assembly cache (the GAC). Technically speaking, this is not required; however, if you do not deploy the assembly to the GAC, you will need to copy this assembly into the same folder as the COM application making use of it.

Given that Chapter 15 already walked you through the details of defining a strongly named assembly, simply generate a new *.snk file for signing purposes using the Signing tab of the Properties editor. At this point, you can compile your assembly and install ComCallableDotNetServer.dll into the GAC using gacutil.exe (again, see Chapter 15 for details).

gacutil -i ComCallableDotNetServer.dll

Generating the Type Library and Registering the

.NET Types

At this point, we are ready to generate the necessary COM type library and register our .NET assembly into the system registry for use by COM. Do to so, you can take two possible approaches. Your first approach is to use a command-line tool named regasm.exe, which ships with the .NET Framework 3.5 SDK. This tool will add several listings to the system registry, and when you specify the /tlb flag, it will also generate the required type library, as shown here:

regasm ComCallableDotNetServer.dll /tlb

Note The .NET Framework 3.5 SDK also provides a tool named tlbexp.exe. Like regasm.exe, this tool will generate type libraries from a .NET assembly; however, it does not add the necessary registry entries. Given this, it is more common to simply use regasm.exe to perform each required step.

While regasm.exe provides the greatest level of flexibility regarding how the COM type library is to be generated, Visual Studio 2008 provides a handy alternative. Using the Properties editor, simply check the Register for COM interop option on the Compile tab, as shown in Figure A-17, and recompile your assembly.

Once you have run regasm.exe or enabled the Register for COM Interop option, you will find that your bin\Debug folder now contains a COM type library file (taking a *.tlb file extension).

Source Code ComCallableDotNetServer application is included under the Appendix A subdirectory.

APPENDIX A COM AND .NET INTEROPERABILITY

1311

Figure A-17. Registering an assembly for COM interop using Visual Studio 2008

Examining the Exported Type Information

Now that you have generated the corresponding COM type library, you can view its contents using the OLE View utility by loading the *.tlb file. If you load ComCallableDotNetServer.tlb (via the FileView Type Library menu option), you will find the COM type descriptions for each of your .NET class types. For example, the DotNetCalc class has been defined to support the default _DotNetClass interface due to the [ClassInterface] attribute, as well as an interface named (surprise, surprise) _Object. As you would guess, this is an unmanaged definition of the functionality defined by

System.Object:

[uuid(88737214-2E55-4D1B-A354-7A538BD9AB2D),

version(1.0), custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "ComCallableDotNetServer.DotNetCalc")]

coclass DotNetCalc {

[default] interface _DotNetCalc; interface _Object;

};

As specified by the [ClassInterface] attribute, the default interface has been configured as a dual interface, and can therefore be accessed using early or late binding:

[odl, uuid(AC807681-8C59-39A2-AD49-3072994C1EB1), hidden, dual, nonextensible, oleautomation, custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "ComCallableDotNetServer.DotNetCalc")]

interface _DotNetCalc : IDispatch { [id(00000000), propget,

custom({54FC8F55-38DE-4703-9C4E-250351302B1C}, "1")] HRESULT ToString([out, retval] BSTR* pRetVal); [id(0x60020001)]

HRESULT Equals( [in] VARIANT obj,

[out, retval] VARIANT_BOOL* pRetVal);

[id(0x60020002)]

HRESULT GetHashCode([out, retval] long* pRetVal); [id(0x60020003)]

HRESULT GetType([out, retval] _Type** pRetVal); [id(0x60020004)]

HRESULT Add([in] long x, [in] long y,