如何连接 ArcObjects .NET 事件
如何连接 ArcObjects .NET 事件
概述
事件是 Windows 应用程序接收通知的方式。 在 Windows 应用程序中,许多事件发生在特定的时刻——例如,鼠标移动、鼠标移出和鼠标单击。 在 .NET 中,通过委托来挂钩事件,而委托是函数指针(即保存对函数的引用)【在事件触发时,通过函数指针,调用事件处理程序】。 本主题说明如何连接 ArcObjects .NET 事件。
【译注:以下“背景知识”由译者对来自: COM 中事件处理的基本原则 的内容进行总结而得来】
背景知识
COM 中事件处理的基本原则:
-
COM 是基于接口的编程。
-
因此,一个 COM 对象(客户端) 调用另一个 COM 对象(服务器)的方法, 在某种意义上就像是,服务器在监听客户端,等待客户端的调用。被调用的方法属于服务器的某一个接口(ISomeInterface),此时我们可以这样认为,ISomeInterface 接口是服务器开放给客户端的一个“接入接口”。
-
类似地,客户端也可能会有监听服务器的需求,等待服务器的回调。此时,所谓的“传出接口”(ITalkBackInterface,比如:IGlobeDisplayEvents),就应运而生了:“传出接口”放在服务器这边,但由客户端实现它其中的方法。支持一个或多个传出接口的 COM 对象(服务器)称为可连接对象。注意,这里用的是“支持”一词,服务器本身是不实现“传出接口”的,而是调用它。
“传出接口”与“接入接口”一起,构成了一个“双向的对话”。
-
使用这样的机制(可连接性),服务器就可以向其客户端触发事件。
-
-
事件与请求的区别:
-
事件,由服务器触发并且不需要来自客户端的响应;但通过调用“传出接口”,通知客户端发生了某事件。
-
请求,是服务器通过调用“传出接口”向客户端提出问题并期望得到响应的方式。
-
在客户端这边,那些实现了【服务器的“传出接口”】的部分,我们称之为“接收器”。请注意,“接收器”本身就是一个对象(指针),与客户端的其他部分紧密相关。
关于请求,从接收器的角度讲,服务器的“传出接口”是具有传入性的。
-
-
实现方式:
服务器持有它“传出接口”的“某一实现(在客户端)”的指针(接收器)。通过这个指针,服务器将向客户端发送事件和请求。指针所指的部分,就是接收器。
到了这里,问题就转变为了:如何让服务器拥有接收器的指针。换句话说,需要解决的问题是:一个 COM 对象如何连接到它的接收器。要回答这个问题,我们先认识两个接口及它们的对象:
连接点和连接点容器:
- 连接点:实现了 IConnectionPoint 接口的对象。 IConnectionPoint 接口包含两个方法:Advise()和Unadvise()。
- 连接点容器:实现了 IConnectionPointContainer 接口的对象。可连接的对象
必须
是连接点容器。
大概步骤:
-
服务器公开一个连接点;服务器为它所支持的每个传出接口维护一个单独的连接点。
-
客户端调用连接点的 Advise 方法,与服务器建立关于事件的通信。当然,也可以调用 Unadvise 方法而与服务器终止连接。通过对方法 Advise 的调用,客户端可以开始监听来自服务器的一组事件。
-
连接点的引用计数,被两方同时持有,以确保双向通信的生命周期。
-
作为可连接对象的服务器(实现了 IConnectionPointContainer 接口),包含众多连接点;客户端通过服务器的 IConnectionPointContainer 接口请求出适当连接点对象(IConnectionPointContainer 接口可以通过服务器的 QueryInterface 而轻松获取到),当它接收到适当的连接点对象时,客户端将相应的接收器指针传递给该连接点。
下面示例代码,演示了
客户端如何选择连接点,并建立连接
:/* 传入 ISomeInterface 对象(服务器)的指针 */ void Sink::SetupConnectionPoint(ISomeInterface* pISomeInterface) { /* 声明两个指针 */ IConnectionPointContainer* pIConnectionPointContainerTemp = NULL; IUnknown* pIUnknown = NULL; /* 客户端 QI 自身以获取自身的 IUnknown 指针,以备在调用连接点的方法 Advise 时使用 */ this -> QueryInterface(IID_IUnknown, (void**)&pIUnknown); if (pIUnknown) { /* 客户端对 pISomeInterface (服务器)进行 QI, 以获取服务器作为连接点容器时的指针 */ pISomeInterface -> QueryInterface (IID_IConnectionPointContainer, (void**)&pIConnectionPointContainerTemp); if (pIConnectionPointContainerTemp) { /* 从连接点容器中找到“传出接口 ISomeEventInterface 的”连接点 */ /* 将连接点放进 m_pIConnectionPoint */ pIConnectionPointContainerTemp -> FindConnectionPoint(__uuidof(ISomeEventInterface), &m_pIConnectionPoint); pIConnectionPointContainerTemp -> Release(); pIConnectionPointContainerTemp = NULL; } if (m_pIConnectionPoint) { /* 调用连接点的 Advise 方法,建立通信 */ /* 注意:客户端需持有这个连接点 m_pIConnectionPoint 以确保双向通信的生命周期 */ m_pIConnectionPoint -> Advise(pIUnknown, &m_dwEventCookie); } pIUnknown -> Release(); pIUnknown = NULL; } }
建立服务器和客户端的通信连接之后,还需要将接收器与连接点的某一方法挂钩(未在示例代码中展示 ,但)。这样,每当服务器向接收器触发事件时,都会调用传出接口ISomeEventInterface中相应的方法(即由接收器实现的方法)。
实现方式总结:
(1)服务器除了需要实现客户端所需要的 ISomeInterface 接口外,还需要同时实现 IConnectionPointContainer 接口。作为容器时,服务器需要包含一个或多个连接点(连接点将作为服务器的类成员);这些连接点,在实现 IConnectionPoint 接口的同时,还需要实现 ISomeEventInterface 接口。ISomeEventInterface 接口针对服务器自身的需求,开放具体的方法;所开放的方法,一方面用于外部的客户端来挂钩接收器,另一方面用于在服务器自身内部调用这个方法;这样,一旦服务器调用这个开放的方法,就实际在调用接收器的实现。
(2)客户端根据服务器的 ISomeInterface 接口,QI 找到 IConnectionPointContainer 接口;再通过 IConnectionPointContainer 接口,找到 实现了 ISomeEventInterface 连接点;将 接收器 连接到 连接点的某个方法(事件)上。
(3)客户端调用服务器的某方法,服务器执行被调用的方法,服务器触发事件,执行 ISomeEventInterface 接口 的方法,即接收器的实现。
处理事件
一个对象,做某个动作,然后发出消息,即是引发一个事件。 该动作可以由“用户交互”引起,例如鼠标点击,也可以由另一个程序逻辑触发。
- 引发(触发)事件的对象是“事件发送者”。
- 捕获事件并对其做出响应的对象是“事件接收器”。
在事件通信中,“事件发送者”不知道“哪个对象或方法”接收(处理)它引发的事件。 在“事件发送者”和“事件接收器”之间需要一个中介。.NET Framework 定义了一种提供函数指针功能的特殊类型(委托),就是这个中介。
定义委托
在 .NET 中,委托是一个特殊的类,一个持有“对方法的引用”的类。与其他类不同,委托类具有签名,并且只能保存对“与其签名匹配的方法”的引用。 委托相当于类型安全的函数指针或回调。
要在应用程序中使用事件,请提供“事件处理程序”(事件处理方法),该处理程序执行程序逻辑,以响应事件。“事件处理程序”必须与“事件委托”具有相同的签名。将事件处理程序注册到事件源(事件发送者),这个过程就是事件接线。
当你创建一个委托的实例时,你需要传入函数名(作为委托构造函数的参数),以使委托完成对函数的引用。 请参见以下代码示例:
[C#]
delegate int SomeDelegate(string s, bool b); //A delegate declaration.
[VB.NET]
Delegate Function SomeDelegate(ByVal s As String, ByVal b As Boolean) As Integer
'A delegate declaration.
这个委托有一个签名:它接受两个参数 :string 和 bool,并返回一个 int 类型。
在 ArcObjects .NET 中,事件接口(也称为出站接口)具有 _Event 后缀,因为类型库导入器(type library importer)会自动为它们添加 _Event 后缀。
监听 ArcObjects 事件
以下是监听 ArcObjects 事件所需的步骤:
[C#]
//1. 转换相关的事件接口
IGlobeDisplayEvents_Event globeDisplayEvents=(IGlobeDisplayEvents_Event)
m_globeDisplay;
//2.注册事件处理程序方法
globeDisplayEvents.AfterDraw += new IGlobeDisplayEvents_AfterDrawEventHandler
(OnAfterDraw);
//3.实现委托签名指定的事件处理方法
private void OnAfterDraw(ISceneViewer pViewer)
{
//Your event handler logic.
}
//4.当您不再需要收听事件时,将事件从您的应用程序中断开
((IGlobeDisplayEvents_Event)m_globeDisplay).AfterDraw -= new
IGlobeDisplayEvents_AfterDrawEventHandler(OnAfterDraw);
[VB.NET]
'1. 转换相关的事件接口
Dim globeDisplayEvents As IGlobeDisplayEvents_Event=CType(m_globeDisplay, IGlobeDisplayEvents_Event)
'2.注册事件处理程序方法
AddHandler globeDisplayEvents.AfterDraw, AddressOf OnAfterDraw
'3.实现委托签名指定的事件处理方法
Private Sub OnAfterDraw(ByVal pViewer As ISceneViewer)
'Your event handler logic.
End Sub
'4.当您不再需要收听事件时,将事件从您的应用程序中断开
RemoveHandler (CType(m_globeDisplay, IGlobeDisplayEvents_Event)).AfterDraw, AddressOf OnAfterDraw
成员对象的连线事件
在许多情况下,您连接事件时,被连接的对象是作为容器对象(服务器作为连接点容器时)的成员(连接点)存在的,并且是通过属性访问的方式进行对象访问的。例如,您为【GlobeControl 应用程序】写一个命令,监听 GlobeDisplayEvents 的事件之一,AfterDraw;此时,使用【辅助类 GlobeHookHelper】几乎是必然的选择,因为它允许您的命令在 ArcGlobe 中工作。 请参见以下代码示例:
【译注:通过 编写多线程 ArcObjects 代码 的相关内容,我们知道,使用辅助类,实际上是同步某一个 STA 线程中的某一 ArcObjects 对象,避免跨单元调用。此示例代码中,事件的引发者是一个 ArcObjects 对象,因此需要用到辅助类,即 GlobeHookHelper】
[C#]
//类成员
private IGlobeHookHelper m_globeHookHelper=null;
public override void OnCreate(object hook)
{
//初始化辅助类
if (m_globeHookHelper == null)
m_globeHookHelper=new GlobeHookHelper();
//hook 为 GlobeControl 应用程序,为事件连接的对象
m_globeHookHelper.Hook=hook;
//m_globeHookHelper.GlobeDisplay 是连接点
((IGlobeDisplayEvents_Event)m_globeHookHelper.GlobeDisplay).AfterDraw += new
IGlobeDisplayEvents_AfterDrawEventHandler(OnAfterDraw);
//...
}
[VB.NET]
'类成员
Private m_globeHookHelper As IGlobeHookHelper=Nothing
Public Overrides Sub OnCreate(ByVal hook As Object)
'初始化辅助类
If m_globeHookHelper Is Nothing Then
m_globeHookHelper=New GlobeHookHelper()
End If
'hook 为 GlobeControl 应用程序
m_globeHookHelper.Hook=hook
'm_globeHookHelper.GlobeDisplay 是连接点
AddHandler (CType(m_globeHookHelper.GlobeDisplay, IGlobeDisplayEvents_Events)).AfterDraw, AddressOf OnAfterDraw
...
End Sub
AfterDraw 事件的连接是针对 GlobeHookHelper.GlobeDisplay 属性完成的。
当您运行前面的代码示例时,最终,事件停止并且没有任何异常或警告。这是因为在内部,您通过 GlobeHookHelper 的 GlobeDisplay 属性来连接事件,.NET Framework 为连接点创建的对象是局部变量。一旦 OnCreate 方法结束,由 .NET Framework 创建的本地 IGlobeDisplay 变量,就会超出范围,并被垃圾收集器收集。发生这种情况时,事件将停止触发。
对这种情况,正确处理方法是,保留连接点,并防止垃圾收集器处理它。这样,您就可以保证您正在收听的事件在类的整个生命周期内继续触发事件。一旦您完成收听,请不要忘记取消连接该事件。
最终,您的代码类会似于以下代码示例:
[C#]
//类成员
private IGlobeHookHelper m_globeHookHelper=null;
private IGlobeDisplay m_globeDisplay=null;
public override void OnCreate(object hook)
{
//初始化辅助类
if (m_globeHookHelper == null)
m_globeHookHelper=new GlobeHookHelper();
//hook 为 GlobeControl 应用程序
m_globeHookHelper.Hook=hook;
//从辅助类中获取 GlobeDisplay。
m_globeDisplay=m_globeHookHelper.GlobeDisplay;
((IGlobeDisplayEvents_Event)m_globeDisplay).AfterDraw += new
IGlobeDisplayEvents_AfterDrawEventHandler(OnAfterDraw);
//...
}
[VB.NET]
'类成员
Private m_globeHookHelper As IGlobeHookHelper=Nothing
Private m_globeDisplay As IGlobeDisplay=Nothing
Public Overrides Sub OnCreate(ByVal hook As Object)
'初始化辅助类
If m_globeHookHelper Is Nothing Then
m_globeHookHelper=New GlobeHookHelper()
End If
'hook 为 GlobeControl 应用程序
m_globeHookHelper.Hook=hook
'从辅助类中获取 GlobeDisplay。
m_globeDisplay=m_globeHookHelper.GlobeDisplay
AddHandler (CType(m_globeDisplay, IGlobeDisplayEvents_Events)).AfterDraw, AddressOf OnAfterDraw
...
End Sub