在.net 中对 ArcObjects 的应用
在.net 中对 ArcObjects 的应用
一、与 COM 的互操作
在 .NET Framework 控制下运行的代码,称为托管代码; 相反,在 .NET Framework 之外执行的代码,称为非托管代码。 COM 是非托管代码的一个示例。 .NET 框架通过一种称为 【COM 互操作】的技术与 COM 交互。
为了使 COM 互操作起作用,公共语言运行时 (CLR) 需要所有 COM 类型的元数据。 这意味着,通常存储在类型库中的 COM 类型定义需要转换为 .NET 元数据。 这可以通过 .NET Framework SDK 附带的【类型库导入器实用程序 (tlbimp.exe)】 轻松完成。 此实用程序生成【互操作程序集】,其中包含类型库中所有 COM 定义的元数据。 一旦元数据可用,.NET 客户端就可以无缝地创建 COM 类型的实例并调用其方法,就好像它们是本机 .NET 实例一样。
二、PIAs
PIAs(Primary interop assemblies)是 COM 类型库的供应商提供的官方的 .NET 类型定义,用于与 COM 类型进行互操作。 COM 库发布者 强命名 PIAs,以保证其唯一性。 有关详细信息,请参阅 Microsoft Developer Network (MSDN) 网站的 Primary Interop Assemblies。
ESRI 为【所有使用 COM 实现的 ArcObjects 类型库】提供了 PIAs。在安装期间,如果检测到 .NET Framework 版本 3.5 sp1,ArcGIS .NET 开发人员应仅使用安装在GAC (全局程序集缓存) 中的 PIAs。 ESRI 仅支持随 ArcGIS 提供的互操作程序集(interop assemblies)。 您可以通过公钥 (8FC3CC631E44AD86) 识别有效的 ESRI 程序集。
ArcGIS 安装程序还安装 Microsoft Stdole.dll PIA,为某些 ESRI 库【在使用的OLE (对象链接和嵌入) 字体和图片类时】提供互操作。
三、COM 包装器
.NET 运行时提供包装类,以使托管和非托管客户端都相信它们正在与各自环境中的对象进行通信。 当托管客户端调用 COM 对象上的方法时,运行时会创建一个 RCW(runtime-callable wrapper,运行时可调用包装器),用于处理两个环境之间的封送处理。 同样,.NET 运行时为相反的情况创建 CCW(COM-callable wrappers,COM 可调用包装器),在这种情况下,COM 客户端与 .NET 组件进行通信。
下图概述了此过程:
四、RCW 类和 System.__COMObject
Runtime callable wrappers and the System.__COMObject type
在 .NET 中,每个类、接口、枚举等都由其类型(type)来描述。 Type 类是 .NET Framework 的一部分,它保存了相关数据类型的数据和函数成员的信息。 在 .NET 中,当您通过互操作创建一个 COM 对象时,您会获得一个对这个COM 对象的引用【包装在强类型 RCW 中】。在 .NET 应用程序内, 一个 RCW 保存了一个 COM 对象的引用。
在以下代码示例中,变量 sym 被声明为 ISimpleMarkerSymbol 接口类型,并被赋值为一个 SimpleMarkerSymbolClass 的实例;检出【变量 sym】类型变量,并将其写入调试窗口。 如果您运行此代码,会发现【变量 sym】的类型为 SimpleMarkerSymbolClass。该变量包含了一个对 ISimpleMarkerSymbol 接口的引用【一个 SimpleMarkerSymbolClass RCW 的引用,即一个包装了 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)
在不同的编码情况下,您可以从另一个属性或方法中,获取到对 RCW 的引用。 在以下代码示例中,检出一个 renderer (ISimpleRenderer 接口)的 Symbol 属性,它是一个 SimpleMarkerSymbol 的实例(意味着进行 as 操作之后不为null ):
[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)
尽管您可能希望获得与前述相同的输出(一个 SimpleMarkerSymbolClass RCW 的引用),但【变量 sym】的类型却是 System.__ComObject。
以下是前面两个代码示例的区别:
- 在第一个代码示例中,您使用了 New(或 new)关键字和 SimpleMarkerSymbolClass 类型。 因此,在编译时,编译器能够通过反射而发现变量的确切类型,有关该类型的元数据就会存储在编译后的代码中。 从而,当代码运行时,运行时拥有描述变量确切类型的所有信息(元数据)。
- 在第二个代码示例中,您从 ISimpleRenderer 接口的 Symbol 属性设置了 sym 变量。因此,在编译此代码时,编译器可以找到的唯一元数据是 : ISimpleRenderer 的 Symbol 属性返回 ISymbol 引用,编译器无法发现实际对象类的类型。尽管您可以执行强制转换,以获取 sym 变量的 ISimpleMarkerSymbol 接口,但 .NET 运行时在运行时仍是没有所需的元数据,来准确发现变量类型是什么。在这种情况下,当您访问 Symbol 属性 时,.NET 运行时会将 COM 对象引用包装在一个名为 System._ComObject 的一般性 RCW 中。System._ComObject,这是 .NET Framework 内部的一个类,可用于保存对任何类型 COM 对象的引用。其目的是充当未知类型 COM 对象的 RCW。
类型转化
问:在第二个代码示例中,之所以使用 一般性 RCW (System._ComObject),是由于编译时所需要的元数据不充分 ,那么,在代码中直接强制类型转换为已知的类型,是否可行?
答:不可行,因为:
在第二个代码示例中,即使开发人员知道引用的类的确切类型,并将代码写成下面的样子,.NET 运行时仍然没有所需的元数据(将变量强制转换为强类型 RCW 的元数据)。 这将引发【尝试转换为 SimpleMarkerSymbolClass 类型】失败:
[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:
问:既然上述转换为特定类型的尝试不可行,为何将类型转换为某接口却又可行的呢?
答:因为 System._ComObject 类专门用于 COM 对象。对于 COM对象 实现的任何 COM 接口,它始终可以对执行相应的 QI (query interface,查询接口)。 因此,转换到特定接口(只要在 COM 对象上实现了这些接口)是可行的,能成功的。 请参见以下代码示例:
[C#]
ESRI.ArcGIS.Display.ISimpleMarkerSymbol sym3=sym as
ESRI.ArcGIS.Display.ISimpleMarkerSymbol;
[VB.NET]
Dim sym3 As ESRI.ArcGIS.Display.ISimpleMarkerSymbol=sym
要转换单例对象,请参阅 与单例对象交互。【ArcObjects中的单例,均不可用上述2种方式实例化,需要使用Activator.CreateInstance(type) 来获取】
有关 RCW 和互操作的更多信息,请参阅 Adam Nathan 所著的《.NET and COM: The Complete Interoperability Guide》一书(Sams Publishing,2002 年)。
五、向 COM 公开 .NET 对象
创建供 COM 客户端使用的 .NET 组件时,请遵守以下准则以确保互操作性:
- 避免使用带参数的构造函数。
- 避免使用静态方法。
- 在托管代码中定义事件源接口。
- 在用户定义的Exception中包含 HRESULTs。
- 为需要它们的类型提供 GUID 。
- 期望继承差异。
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.
六、性能注意事项
COM 互操作,为应用程序,增加了一层新的开销,但总成本很小,而且通常不明显。 但不管怎么说,成本确实增加了。 因此,如果您怀疑【COM 互操作】是应用程序性能的瓶颈,请尝试创建一个 COM 工作类,将【所有繁琐的 COM 调用】都包装到【一个托管代码可以调用的】函数中。 这样做,限制两个环境之间的编组,提高了性能。 有关详细信息,请参阅 Performance of ArcObjects。
七、COM 到 .NET 类型转换
一般来说,类型库导入器在导入类型时,会使用它们(被导入的类型)在 COM 中的名称。 所有被导入的类型会被添加到具有以下命名约定的命名空间中:
- ESRI.ArcGIS.
例如,geometry 库的命名空间是 ESRI.ArcGIS.Geometry。 所有类型都由其完整的命名空间和类型名称标识。
1. 类、接口和成员
所有 COM coclass 都被转换为托管类,转换生成的托管类与【附加了 Class 的原始类】具有相同的名称。 例如,Point coclass 转为 PointClass
Point(COM原始类名称)+Class(附加Class)= PointClass(托管类名称)。
所有转换生成的托管类还会有一个与 coclass 同名的接口,对应于 coclass 的默认接口。 例如,PointClass 有一个 Point 接口(注意:Point 接口 与 IPoint 接口)。 类型库导入器添加此接口,以便客户端可以注册为事件接收器。 有关详细信息,请参阅本主题的后述内容 类接口 部分。
.NET 类中也会出现有 .NET 支持但 COM 不支持的类成员。COM 类实现的每个接口的每个成员,都作为托管类成员添加。 COM 类实现的任何属性或方法都可以直接从对应的托管类来访问,而不必强制将它们转换为特定接口而添加到托管类中去。 请参见以下代码示例:
[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())
由于 COM 类实现的每个接口的每个成员,都作为托管类成员添加,这必然在某些托管类中造成成员名称冲突。因此,需要一个合适的命名规则:当成员名称冲突时,与 coclass 一起列出的第一个接口保持不变;其他与之有名称冲突的成员,在其名称前添加前缀:接口名称及下划线。例如,MapClass 有名为 AreaOfInterest 和 IBasicMap_AreaOfInterest 的成员。
在C# 中,常规属性语法不支持具有引用或多个参数的属性。 在这些情况下,有必要使用辅助方法。 请参见以下代码示例:
[C#]
ILayer layer=mapControl.get_Layer(0);
MessageBox.Show(layer.Name);
2. 事件
【事件接口】转换到 .NET 时,类型库导入器会创建多个类型,以使【托管应用程序】能够接收到 COM 类触发的事件。
导入器创建的第一种类型:委托。
委托命名规则:在【原始事件接口】后加上一个下划线,后跟事件名称,然后加上 EventHandler。
例如,在 IActiveViewEvents 接口上定义的 SelectionChanged 事件定义了以下委托:
- IActiveViewEvents_SelectionChangedEventHandler
导入器创建的第二种类型:事件接口。命名规则:在【原始事件接口】名称后添加 _Event 后缀。
例如,IActiveViewEvents 生成 IActiveViewEvents_Event。
在托管应用程序,使用创建的事件接口来设置事件接收器。
3. 非 OLE 自动化兼容类型
不符合 OLE 自动化的 COM 类型,通常在 .NET 中不起作用。 ArcGIS 包含一些不兼容的方法,这些方法不能在 .NET 中使用。 但是,在大多数情况下,已经添加了对应的补充接口,以使违规成员得到合规地重写。 它们通常将 GEN 后缀添加到原始 COM 接口名称中。
例如,通过点数组定义包络时,不能使用 IEnvelope.DefineFromPoints; 相反,您必须使用 IEnvelopeGEN.DefineFromPoints。
请参见以下代码示例:
[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);
[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)
4. 类接口
创建【类接口】是为了帮助 VB 程序员过渡到 .NET。 它们(即类接口)也常用于由 VB.NET 升级向导,或 也常用于 VS.NET 中的【代码片段转换器】生成的代码中。
但是,您应该避免在 ESRI 互操作程序集中使用【类接口】,因为它们可能会在未来版本的 ArcGIS 中发生变化。 本节解释更多关于类接口的信息。
在 VB6 中,【默认接口】的细节对用户是隐藏的。程序员可以实例化一个变量,并访问其默认接口的成员,而无需对该接口执行特定的 QI。 但是,.NET 不提供这种相同的功能。 为了让 VB 开发人员更无缝地衔接到 .NET,.NET 中的【类型库导入器】向每个互操作程序集添加了“类接口”,从而允许在 .NET 中以相同的语法使用 COM 对象。
当导入对象库时,会为每个 COM 类创建一个【类接口 RCW】。 类接口的名称与 COM 类相同,例如 Envelope。 COM 类的默认接口的所有成员都添加到该类接口中。 如果COM类有源接口(是事件的源头),那么,这个类接口也将包含这个源接口的所有事件,这有助于程序员链接事件。
会有第二个 RCW 被创建,表示底层 COM 类;这个 RCW 的名称是 COM 类名加后缀 Class,例如 EnvelopeClass。【类接口】通过一个属性链接到 (这个第二 RCW)类(如,EnvelopeClass),该属性指示【类接口】所属的 COM 类。该属性会被 .NET 编译器识别,从而允许程序员通过使用【类接口】来实例化一个(这个第二 RCW)类。
例外情况是,具有 IUnknown 或 IDispatch 的默认接口的类,它们从未在 RCW 类上公开,因为这些成员是由 .NET Framework 运行时在内部调用的。在这种情况下,下一个实现的接口被暴露在【类接口】上。
大多数 ArcObjects 将 IUnknown 定义为其默认接口。
例如,esriGeometry 对象库中的 Point COM 类将 IPoint 接口列为其第一个实现的接口(即意味着 IPoint 接口 暴露在了 类接口 Point 上)。在.NET中,这个类是通过 Point 类接口来访问的,它继承了 IPoint 接口和 PointClass 类。
以下代码示例显示,通过将变量类型声明为 Point 类接口,该变量可用于从此类接口访问 IPoint.PutCoords 方法:
[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)
【类接口】所继承的接口,在不同 ArcGIS 版本之间可能发生变化; 因此,您应该避免使用前面的语法(即避免使用类接口)。 您可以在 VB .NET 对象浏览器中查看这些类型,如以下屏幕截图所示。 使用 VB .NET 时,PointClass 默认不显示,但可以通过选择 Show Hidden Members 选项使其可见。
在 C# 对象浏览器中,您可以更清楚地看到【类接口 Point】、其继承的接口 IPoint 以及类 PointClass,如以下屏幕截图所示: