In this topic
- About interoperating with COM
- Primary interop assemblies
- COM wrappers
- Runtime callable wrappers and the System.__COMObject type
- Exposing .NET objects to COM
- Performance considerations
- COM to .NET type conversion
About interoperating with COM
Code running under the control of the .NET Framework is called managed code; conversely, code executing outside the .NET Framework is called unmanaged code. COM is one example of unmanaged code. The .NET Framework interacts with COM via a technology known as COM interop.
For COM interop to work, the Common Language Runtime (CLR) requires metadata for all the COM types. This means that the COM type definitions normally stored in the type libraries need to be converted to .NET metadata. This is easily accomplished with the Type Library Importer utility (tlbimp.exe) that ships with the .NET Framework SDK. This utility generates interop assemblies containing the metadata for all the COM definitions in a type library. Once metadata is available, .NET clients can seamlessly create instances of COM types and call its methods as though they were native .NET instances.
Primary interop assemblies
PIAs are the official, vendor-supplied .NET type definitions for interoperating with underlying COM types. PIAs are strongly named by the COM library publisher to guarantee uniqueness. For more information, see the Microsoft Developer Network (MSDN) Web site's topic, Primary Interop Assemblies.
ESRI provides PIAs for all ArcObjects type libraries that are implemented with COM. ArcGIS .NET developers should only use the PIAs that are installed in the Global Assembly Cache (GAC) during the installation if version 3.5 sp1 of the .NET Framework is detected. ESRI only supports the interop assemblies that ship with ArcGIS. You can identify a valid ESRI assembly by its public key (8FC3CC631E44AD86).
The ArcGIS installation program also installs the Microsoft Stdole.dll PIA, providing interop for Object Linking and Embedding (OLE) font and picture classes that are used by some ESRI libraries. For more information about the Microsoft PIA, see the Microsoft Help and Support Web site's topic, Microsoft Office XP primary interop assemblies (PIAs) are available for download (Article ID: 328912).
COM wrappers
The .NET runtime provides wrapper classes to make both managed and unmanaged clients believe they are communicating with objects within their respective environment. When managed clients call a method on a COM object, the runtime creates a runtime-callable wrapper (RCW) that handles the marshaling between the two environments. Similarly, the .NET runtime creates COM-callable wrappers (CCWs) for the reverse case, in which COM clients communicate with .NET components. The following illustration outlines this process:
Runtime callable wrappers and the System.__COMObject type
In .NET, each class, interface, enumeration, and so on, is described by its type. The Type class, which is part of the .NET Framework, holds information about the data and function members of a data type. When you create a COM object in .NET via interop, you get a reference to your object that is wrapped in a strongly typed RCW. An RCW can hold a reference to a COM object inside a .NET application.
In the following code example, the sym variable is declared as the ISimpleMarkerSymbol interface type and is set to a new SimpleMarkerSymbolClass. The sym type variable is retrieved and written to the debug window. If you run this code, the sym type is SimpleMarkerSymbolClass—the variable holds a reference to the ISimpleMarkerSymbol interface of the SimpleMarkerSymbolClass RCW.
[C#]
ESRI.ArcGIS.Display.ISimpleMarkerSymbol sym = new
ESRI.ArcGIS.Display.SimpleMarkerSymbolClass();
Debug.WriteLine(sym.GetType().FullName);
[VB.NET]
Dim sym As ESRI.ArcGIS.Display.ISimpleMarkerSymbol = New ESRI.ArcGIS.Display.SimpleMarkerSymbolClass
Debug.WriteLine(CType(sym, Object).GetType.FullName)
In a different coding situation, you could get a reference to an RCW from another property or method. In the following code example, the Symbol property of a renderer (ISimpleRenderer interface) is retrieved, where the renderer uses a single SimpleMarkerSymbol to draw:
[C#]
ESRI.ArcGIS.Display.ISimpleMarkerSymbol sym = rend.Symbol as
ESRI.ArcGIS.Display.ISimpleMarkerSymbol;
Debug.WriteLine(sym.GetType().FullName);
[VB.NET]
Dim sym As ESRI.ArcGIS.Display.ISimpleMarkerSymbol = rend.Symbol
Debug.WriteLine(CType(sym, Object).GetType.FullName)
Although you might expect to get the same output as before, the reported type of sym is System.__ComObject. The following is the difference between the two previous code examples:
- In the first code example, you create the symbol using the New (or new) keyword and the type SimpleMarkerSymbolClass. When the code is compiled, the exact type of the variable is discovered by the compiler using Reflection, and metadata about that type is stored in the compiled code. When the code runs, the runtime has all the information (the metadata) that describes the exact type of the variable.
- However, in the second code example, you set the sym variable from the Symbol property of the ISimpleRenderer interface. When this code is compiled, the only metadata the compiler can find is that the Symbol property returns an ISymbol reference. The type of the actual class of object cannot be discovered. Although you can perform a cast to get the ISimpleMarkerSymbol interface of the sym variable (or any other interface that the symbol implements), the .NET runtime does not have the required metadata at run time to discover exactly what the variable type is. In this case, when you access the Symbol property, the .NET runtime wraps the COM object reference in a generic RCW called System.__ComObject. This is a class internal to the .NET Framework that can be used to hold a reference to any kind of COM object. Its purpose is to act as the RCW for an unknown type of COM object.
Casting
In the second code example, even if you know the exact type of class to which you have a reference, the .NET runtime still does not have the required metadata to cast the variable to a strongly typed RCW. This is shown in the following code example, as attempting a cast to the SimpleMarkerSymbolClass type fails:
[C#]
// The following line results in sym2 being null, as the cast fails.
ESRI.ArcGIS.Display.SimpleMarkerSymbolClass sym2 = sym as
ESRI.ArcGIS.Display.SimpleMarkerSymbolClass;
[VB.NET]
' The following line results in a runtime error, as the implicit cast fails.
Dim sym2 As ESRI.ArcGIS.Display.SimpleMarkerSymbol = sym
However, because the System.__ComObject class specifically works with COM objects, it can always perform a query interface (QI) to any COM interfaces that are implemented by an object. Therefore, casting to specific interfaces (as long as they are implemented on the object) will be successful. See the following code example:
[C#]
ESRI.ArcGIS.Display.ISimpleMarkerSymbol sym3 = sym as
ESRI.ArcGIS.Display.ISimpleMarkerSymbol;
[VB.NET]
Dim sym3 As ESRI.ArcGIS.Display.ISimpleMarkerSymbol = sym
To cast a singleton object, see Interacting with singleton objects.
For more information about RCWs and interop, refer to the book, .NET and COM: The Complete Interoperability Guide, by Adam Nathan (Sams Publishing, 2002).
Exposing .NET objects to COM
When creating .NET components for COM clients to use, observe the following guidelines to ensure interoperability:
- Avoid using parameterized constructors.
- Avoid using static methods.
- Define event source interfaces in managed code.
- Include HRESULTs in user-defined exceptions.
- Supply globally unique identifiers (GUIDs) for types that require them.
- Expect inheritance differences.
For more information, see How to register COM components and Registering classes in COM component categories. Also review the MSDN Web site's topic, Interoperating with Unmanaged Code.
Performance considerations
COM interop adds a new layer of overhead to applications, but the overall cost of interoperating between COM and .NET is small and often unnoticeable. However, the cost of creating wrappers and having them marshal between environments does add up. If you suspect COM interop is the bottleneck in your application's performance, try creating a COM worker class that wraps all the chatty COM calls into one function that managed code can invoke. This improves performance by limiting the marshaling between the two environments. For more information, see Performance of ArcObjects.
COM to .NET type conversion
Generally speaking, the type library importer imports types with the name they originally had in COM. All imported types are also added to a namespace that has the following naming convention:
- ESRI.ArcGIS.<name of the library>
For example, the namespace for the Geometry library is ESRI.ArcGIS.Geometry. All types are identified by their complete namespace and type name.
Classes, interfaces, and members
All COM coclasses are converted to managed classes. Managed classes have the same name as the original with Class appended. For example, the runtime-callable wrapper for the Point coclass is called PointClass.
All classes also have an interface with the same name as the coclass that corresponds to the default interface for the coclass. For example, the PointClass has a Point interface. The type library importer adds this interface so clients can register as event sinks. For more information, see the Class interfaces section in this topic.
The .NET classes also have class members that .NET supports, but COM does not. Each member of each interface the class implements is added as a class member. Any property or method a class implements can be accessed directly from the class rather than having to cast to a specific interface. See the following code example:
[C#]
ESRI.ArcGIS.Geometry.PointClass thePt = new ESRI.ArcGIS.Geometry.PointClass();
thePt.PutCoords(10.0F, 8.0F);
ESRI.ArcGIS.Geometry.IGeometry geom = thePt.Buffer(2.0F);
MessageBox.Show(geom.Dimension.ToString());
[VB.NET]
Dim thePoint As New PointClass
thePoint.PutCoords(10.0, 11.9)
Dim geom As IGeometry = thePoint.Buffer(2.0F)
MessageBox.Show(geom.Dimension.ToString())
Since interface member names are not unique, name conflicts are resolved by prefixing the interface name and an underscore to the name of each conflicting member. When member names conflict, the first interface listed with the coclass remains unchanged. For example, the MapClass has members called AreaOfInterest and IBasicMap_AreaOfInterest.
Properties in C# that have a by reference or multiple parameters are not supported with the regular property syntax. In these cases, it is necessary to use the accessory methods instead. See the following code example:
[C#]
ILayer layer = mapControl.get_Layer(0);
MessageBox.Show(layer.Name);
Events
The type library importer creates several types that enable managed applications to sink to events fired by COM classes. The first type is a delegate named after the event interface plus an underscore followed by the event name, then the word EventHandler. For example, the SelectionChanged event defined on the IActiveViewEvents interface has the following delegate defined:
- IActiveViewEvents_SelectionChangedEventHandler
The importer also creates an event interface with an _Event suffix added to the original interface name. For example, IActiveViewEvents generates IActiveViewEvents_Event. Use the event interfaces to set up event sinks.
Non-OLE automation-compliant types
COM types that are not OLE automation compliant generally do not work in .NET. ArcGIS contains a few noncompliant methods, and these cannot be used in .NET. However, in most cases, supplemental interfaces have been added that have the offending members rewritten compliantly. They usually have the GEN suffix added to the original COM interface name. For example, when defining an envelope via a point array, you cannot use IEnvelope.DefineFromPoints; instead, you must use IEnvelopeGEN.DefineFromPoints. See the following code example:
[VB.NET]
Dim pointArray(1) As IPoint
pointArray(0) = New PointClass
pointArray(1) = New PointClass
pointArray(0).PutCoords(0, 0)
pointArray(1).PutCoords(100, 100)
Dim env As IEnvelope
Dim envGEN As IEnvelopeGEN
env = New EnvelopeClass
envGEN = New EnvelopeClass
'Will not compile.
env.DefineFromPoints(2, pointArray)
'Does not work.
env.DefineFromPoints(2, pointArray(0))
'Works.
envGEN.DefineFromPoints(pointArray)
[C#]
IPoint[] pointArray = new IPoint[2];
pointArray[0] = new PointClass();
pointArray[1] = new PointClass();
pointArray[0].PutCoords(0, 0);
pointArray[1].PutCoords(100, 100);
IEnvelope env = new EnvelopeClass();
IEnvelopeGEN envGEN = new EnvelopeClass();
//Will not compile.
env.DefineFromPoints(3, ref pointArray);
//Does not work.
env.DefineFromPoints(3, ref pointArray[0]);
//Works.
envGEN.DefineFromPoints(ref pointArray);
Class interfaces
Class interfaces are created to help Visual Basic (VB) programmers transition to .NET. They are also commonly used in code produced by the Visual Basic .NET Upgrade wizard or the code snippet converter in Visual Studio .NET.
However, you should avoid using the class interfaces in the ESRI interop assemblies, as they might change in future versions of ArcGIS. This section explains more about class interfaces.
In VB6, the details of default interfaces were hidden from the user, and a programmer could instantiate a variable and access the members of its default interface without performing a specific QI for that interface. However, .NET does not provide this same ability. To allow VB developers a more seamless introduction to .NET, the type library importer in .NET adds "class interfaces" to each interop assembly, allowing COM objects to be used with this same syntax inside .NET. When an object library is imported, a class interface RCW is created for each COM class. The name of the class interface is the same as the COM class, for example, Envelope. All members of the default interface of the COM class are added to this class interface. If the COM class has a source interface (is the source of events), the class interface also includes all the events of this interface, which helps a programmer to link up events.
A second RCW is created that represents the underlying COM class; the name of this is the same as the COM class with a suffix of Class, for example, EnvelopeClass. The class interface is linked to the class by an attribute that indicates the class to which it belongs. This attribute is recognized by the .NET compilers, which allows a programmer to instantiate a class by using its class interface.
The exception is classes that have a default interface of IUnknown or IDispatch that are never exposed on RCW classes because the members are called internally by the .NET Framework runtime. In this case, the next implemented interface is exposed on the class interface instead. As most ArcObjects define IUnknown as their default interface, this affects most ArcObjects classes. For example, the Point COM class in the esriGeometry object library lists the IPoint interface as its first implemented interface. In .NET, this class is accessed by using the Point class interface, which inherits the IPoint interface and the PointClass class.
The following code example shows that by declaring a variable type as a Point class interface, that variable can be used to access the IPoint.PutCoords method from this class interface:
[C#]
ESRI.ArcGIS.Geometry.Point thePt = new ESRI.ArcGIS.Geometry.Point();
thePt.PutCoords(10, 8);
[VB.NET]
Dim thePt As ESRI.ArcGIS.Geometry.Point = New ESRI.ArcGIS.Geometry.Point()
thePt.PutCoords(10, 8)
The inherited interface of a class interface is not guaranteed to remain the same between versions of ArcGIS; therefore, you should avoid using the previous syntax. You can view these types in the VB .NET Object Browser as shown in the following screen shot. When using VB .NET, PointClass is not shown by default but can be made visible by selecting the Show Hidden Members option.
In the C# Object Browser, you can see more clearly the class interface Point, its inherited interface IPoint, and the class PointClass as shown in the following screen shot: