冠冕堂皇

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 :: 管理 ::

COM中的可连接对象与连接点机制及其MFC程序实现

http://www.yesky.com/20020322/1603133.shtml

 本文首先论述可连接对象(Connectable Objects)和连接点机制的原理,然后通过一个示例说明怎样用MFC编程实现可连接对象和内嵌于客户的事件接收器. 

1、可连接对象和连接点机制的基本原理 

  为了在组件对象和客户之间提供更大的交互能力,COM也需要主动与客户进行通信。组件对象通过出接口(Outgoing Interface)与客户进行通信。如果一个组件对象定义了一个或者多个出接口则此组件对象叫做可连接对象。 

1.1 出接口(Outgoing Interface)与入接口(Incoming Interface)

  客户是主动的,而组件是被动的,组件通过自身暴露给客户的接口来监听客户请求,一旦接收到客户请求便宜做出反应,这样的接口称为“入接口”(Incoming Interface).

  与入接口相对应,对象也可以提供"出接口"(Outgoing Interface),对象通过这些出接口与客户进行通讯。出接口包含一组成员函数,每个函数代表一个通知。如果一个COM对象支持一个或多个接口,那么我们称这样的对象为可连接对象(Connectable Object).

  所谓出接口也是COM接口。每个出接口包含一组成员函数,每个成员函数代表了一个事件、一个通知或者一个请求。入接口是由COM本身实现的,出接口是由客户端(的事件接收器Sink)实现的。事件接收器也是COM对象比如OPC DA Client 要实现IOPCDataCallback和IOPCShutdown接口,都要使用IConnectionPoint接口定义实例,用IConnectionPointContainer的FindConnectionPoint方法获取实例给前述定义的接口,使用完毕后在用Marshal.ReleaseComObject(xx)释放非托管实例。 

  可连接对象必须实现一个IConnectionPointContainer接口用于管理所有的出接口【比如建立一个OPC DA Server,则DA Server必须继承此接口】。每个出接口对应一个连接点对象,连接点对象实现了IConnectionPoint接口。客户正是通过IConnectionPoint接口与可连接对象建立连接。每一个连接用CONNECTDATA结构描述【此结构一般用于OPC DA Server端进行连接管理,而OPC DA客户端一般不需要】。 

  CONNECTDATA包含两个成员:IUnknown* pUnk和DWORD dwCookie。pUnk对应于客户中事件接收器的IUnknown接口指针;dwCookie是由连接点对象生成的用于唯一标识此连接的32位整数。 

  通过一个由可连接对象实现的枚举器接口IEnumConnectionPoints【只在DA Server端实现】,客户可以访问可连接对象的所有连接点。但是要获得IEnumConnectionPoints接口指针,要通过IConnectionPointContainer::EnumConnectionPoints(IEnumConnectionPoints**)函数,此函数返回枚举器接口指针。 

  通过另一个有可连接对象实现的枚举器接口IEnumConnections,无论客户还是可连接对象都可以访问一个连接点上的所有连接。通过IConnectionPoint::EnumConnections(IEnumConnections**)函数可以获得IEnumConnections接口指针。 

  综上所述,一个可连接对象必须实现四个接口:IConnectionPointContainer、IConnectionPoint、IEnumConnectionPoints、IEnumConnections【DA Server都要实现】。这四个接口的定义请阅读MSDN文档:

Support for connectable objects requires support for four interfaces:

  

1.2 Architecture of Connectable Objects

The connectable object is only one piece of the overall architecture of connectable objects. This technology includes the following elements:

  • Connectable object. Implements the IConnectionPointContainer interface; creates at least one connection point object; defines an outgoing interface for the client.
  • Client. Queries the object for IConnectionPointContainer to determine whether the object is connectable; creates a sink object to implement the outgoing interface defined by the connectable object.
  • Sink object. Implements the outgoing interface; used to establish a connection to the connectable object.
  • Connection point object. Implements the IConnectionPoint interface and manages connection with the client's sink.

The relationships between client, connectable object, a connection point, and a sink are illustrated in the following diagram:

Before the connection point object calls methods in the sink interface in step 3 in the preceding diagram, it mustQueryInterface for the specific interface required, even if the pointer was already passed in the step 2 call to the Advisemethod.

Two enumerator objects are also involved in this architecture though not shown in the illustration. One is created by a method in IConnectionPointContainer to enumerate the connection points within the connectable object. The other is created by a method in IConnectionPoint to enumerate the connections currently established to that connection point. One connection point can support multiple connected sink interfaces, and it should iterate through the list of connections each time it makes a method call on that interface. This process is known as multicasting.

When working with connectable objects, it is important to understand that the connectable object, each connection point, each sink, and all enumerators are separate objects with separate IUnknown implementations, separate reference counts, and separate lifetimes. A client using these objects is always responsible for releasing all reference counts it owns.

Note   A connectable object can support more than one client and can support multiple sinks within a client. Likewise, a sink can be connected to more than one connectable object.

The steps for establishing a connection between a client and a connectable object are as follows:

  1. The client queries for IConnectionPointContainer on the object to determine whether the object is connectable. If this call is successful, the client holds a pointer to the IConnectionPointContainer interface on the connectable object and the connectable object reference counter has been incremented. Otherwise, the object is not connectable and does not support outgoing interfaces.
  2. If the object is connectable, the client next tries to obtain a pointer to the IConnectionPoint interface on a connection point within the connectable object. There are two methods for obtaining this pointer, both inIConnectionPointContainer::FindConnectionPoint and in IConnectionPointContainer::EnumConnectionPoints. There are a few additional steps needed if EnumConnectionPoints is used. (See Using IConnectionPointContainer for more information.) If successful, the connectable object and the client both support the same outgoing interface. The connectable object defines it and calls it, and the client implements it. The client can then communicate through the connection point within the connectable object.
  3. The client then calls Advise on the connection point to establish a connection between its sink interface and the object's connection point. After this call, the object's connection point holds a pointer to the outgoing interface on the sink.
  4. The code inside Advise calls QueryInterface on the interface pointer that is passed in, asking for the specific interface identifier to which it connects.
  5. The object calls methods on the sink's interface as needed, using the pointer held by its connection point.
  6. The client calls Unadvise to terminate the connection. Then the client calls IConnectionPoint::Release to free its hold on the connection point and, therefore, the main connectable object also. The client must also callIConnectionPointContainer::Release to free its hold on the main connectable object.

1.3 Events in COM and Connectable Objects

When a program detects something that has happened, it can notify its clients. For example, if a stock ticker program detects a change in the price of a stock, it can notify all clients of the change. This notification process is referred to asfiring an event.

With COM, server objects can use COM events to fire events without any information about what objects will be notified. Objects can also use connectable objects to maintain detailed information about clients who have requested notifications.

COM connectable objects provide outgoing interfaces to their clients in addition to their incoming interfaces. As a result, objects and their clients can engage in bidirectional communication. Incoming interfaces are implemented on an object and receive calls from external clients of an object, while outgoing interfaces are implemented on the client's sink and receive calls from the object. The object defines an interface it would like to use, and the client implements it.

An object defines its incoming interfaces and provides implementations of these interfaces. Incoming interfaces are available to clients through the object's IUnknown::QueryInterface method. Clients call the methods of an incoming interface on the object, and the object performs desired actions on behalf of the client.

Outgoing interfaces are also defined by an object, but the client provides the implementations of the outgoing interfaces on a sink object that the client creates. The object then calls methods of the outgoing interface on the sink object to notify the client of changes in the object, to trigger events in the client, to request something from the client, or, in fact, for any purpose the object creator comes up with.

An example of an outgoing interface is an IButtonSink interface defined by a push button control to notify its clients of its events. For example, the button object calls IButtonSink::OnClick on the client's sink object when the user clicks the button on the screen. The button control defines the outgoing interface. For a client of the button to handle the event, the client must implement that outgoing interface on a sink object and then connect that sink to the button control. Then, when events occur in the button, the button will call the sink, at which time the client can execute whatever action it wishes to assign to that button click.

Connectable objects provide a general mechanism for object-to-client communication. Any object that wishes to expose events or notifications of any kind can use this technology. In addition to the general connectable object technology, COM provides many special purpose sink and site interfaces used by objects to notify clients of specific events of interest to the client. For example, IAdviseSink may be used by objects to notify clients of data and view changes in the object.

现在结合后面的示例简单描述一下可连接对象和客户通信的过程。在后面的示例中,可连接对象ConnObject定义了出接口IEventSink,对应此出接口,实现了一个连接点对象SampleConnPoint(此对象实现了对应于出接口的连接点接口IConnectionPoint,接口ID为IID_IEventSink)【实现IConnectionPoint的类有Guid属性?】

2、编程实例 

  现在用MFC实现一个可连接对象,然后写一个极为简单的客户和时间接收器。 

  需要说明的是,MFC通过CCmdTarget类实现了IConnectionPointContainer和IEnumConnectionPoints接口,此外,通过CConnectionPoint类实现了IConnectionPoint接口 

    1).客户在获取了可连接对象的IUnknown接口指针m_pIUnknown后,调用m_pIUnknown->QueryInterface(IID_IConnectionPointContainer,(void**)&pConnPtCont);如果调用成功,pConnPtCont中将存放可连接对象的IConnectionPointContainer接口指针。如果调用不成功,则表明对象不是可连接对象。 

  2).调用pConnPtCont->FindConnectionPoint(IID_IEventSink,&pConnPt)。如果调用成功,pConnPt将存放对应于出接口IEventSink的连接点对象SampleConnPoint所实现的连接点接口IConnectionPoint指针;如果调用不成功,说明可连接对象不支持出接口IEventSink。 【在DA Server中,定义OPCDAServer的类和定义OPCGroup的类都要实现IConnectionPoint接口,因为OPCDAServer中有IOPCShutDown需要客户端实现,而OPCGroup是事实上的操作单元,有IOPCDataCallBack需要客户端实现。这样,客户端通过调用FindConnectionPoint来查找相应接口,从而订购接口的事件。即下面第3点,在客户端的OPCServer类订购IOPCShutDown事件,OPCGroup类订购IOPCDataCallBack事件,来分别执行OPC服务器关闭时客户端采取的行动和OPC数据项有变化时客户端应采取的行动。】

  3).调用pConnPt->Advise(pIEventSink,&m_dwCookie)以建立事件接收器(EventSink)与连接点的连接。其中pIEventSink是客户事件接收器IUnknown接口的指针,此指针通过此函数传递给了可连接对象以便可连接对象发起对客户的通信;m_dwCookie是连接标识,此值由可连接对象设置由客户保存,客户还要使用此值以断开连接。 

  4).可连接对象可以通过连接点调用客户事件接收器中的方法。在客户与连接点成功建立连接后,连接点中已经保存了客户事件接收器接口的指针并可以调用pConnPt->GetConnections()来获取。 


  5).客户调用pConnPt->Unadvise(m_dwCookie)来取消连接,同时调用pConnPt->Release()释放连接点对象。 

2.1 可连接对象ConnObject

  在这个对象中,实现一个一般的COM接口IEventServer,客户可以使用此接口的方法DoSomething()作一些事情,但主要的是对象将在此处触发事件。SampleConnPoint实现连接点对象。 

1、在GUIDs.h中写入:

// {EE888B01-EA9C-11d3-97B5-5254AB191930} 

static const IID CLSID_ConnObject = //组件ID 

{ 0xee888b01, 0xea9c, 0x11d3, { 0x97, 0xb5, 0x52, 0x54, 0xab, 0x19, 0x19, 0x30 } }; 

// {EE888B02-EA9C-11d3-97B5-5254AB191930} 

static const IID IID_IEventServer = //一般的COM接口,客户使用此接口的方法 

//DoSomething() 

{ 0xee888b02, 0xea9c, 0x11d3, { 0x97, 0xb5, 0x52, 0x54, 0xab, 0x19, 0x19, 0x30 } }; 


//// {EE888B03-EA9C-11d3-97B5-5254AB191930} 

static const IID IID_IEventSink = //连接点对象所实现的连接点接口ID 

{ 0xee888b03, 0xea9c, 0x11d3, { 0x97, 0xb5, 0x52, 0x54, 0xab, 0x19, 0x19, 0x30 } }; 
GUIDs.h

2. 在IConnObject.h中写入 

#include "GUIDs.h" 

//声明IEventServer接口 

DECLARE_INTERFACE_(IEventServer,IUnknown) 

{ 

STDMETHOD(DoSomething)()PURE; 

}; 

//声明出接口,此出接口将由客户的事件接收器实现 

DECLARE_INTERFACE_(IEventSink,IUnknown) 

{ 

STDMETHOD(EventHandle)()PURE; 

};
IConnObject.h

3.添加基类为CCmdTarget的类CConnObject.在类声明文件CConnObject1.h中加上#include “IConnObject.h”,在类声明中写入: 

protected: 

…… 

//声明实现IEventServer接口的嵌套类 

BEGIN_INTERFACE_PART(EventServer,IEventServer) 

STDMETHOD(DoSomething)(); 

END_INTERFACE_PART(EventServer) 

DECLARE_INTERFACE_MAP() 

//声明实现连接点的嵌套类 

BEGIN_CONNECTION_PART(CConnObject,SampleConnPoint) 

CONNECTION_IID(IID_IEventSink) 

END_CONNECTION_PART(SampleConnPoint) 

DECLARE_CONNECTION_MAP() 

DECLARE_OLECREATE(CConnObject)
CConnObject1.h

  说明:BEGIN_CONNECTION_PART和END_CONNECTION_PART宏声明了实现连接点的嵌套类SampleConnPoint,并且是基于CConnectionPoint类的,如果需要重载CConnectionPoint类的成员函数或者添加自己的成员函数,可以在这两个宏中声明.这里,CONNECTION_IID宏重载了CConnectionPoint::GetIID()函数.使用DECLARE_CONNECTION-MAP()宏声明连接点映射表. 

4.在类CConnObject的实现文件中写入 

IMPLEMENT_OLECREATE(CConnObject,"ConnObject", 

0xee888b01, 0xea9c, 0x11d3, 0x97, 0xb5, 0x52, 0x54, 0xab, 0x19, 0x19, 0x30); 

BEGIN_INTERFACE_MAP(CConnObject,CCmdTarget) 

INTERFACE_PART(CConnObject,IID_IEventServer,EventServer) 

INTERFACE_PART(CConnObject,IID_IConnectionPointContainer,ConnPtContainer) 

END_INTERFACE_MAP() 

BEGIN_CONNECTION_MAP(CConnObject,CCmdTarget) 

CONNECTION_PART(CConnObject,IID_IEventSink,SampleConnPoint) 

END_CONNECTION_MAP() 
CConnObject

 说明:

  A.必须在接口映射中写入INTERFACE_PART(CConnObject,IID_IConnectionPointContainer,ConnPtContainer)以实现IConnectionPointContainer接口.注意,CCmdTarget类内嵌有才ConnPtContainer类以实现IConnectionPointContainer接口,并用m_xConnPtContainer加以记录. 

  B.用BEGIN_CONNECTION_MAP和END_CONNECTION_MAP宏实现连接点映射.CONNECTION_PART定义了实现连接点的类.

5.在CConnObject::CConnObject()中写入:

EnableConnections();
CConnObject

6.实现IEventServer接口 
  IEventServer接口是基于IUnknown接口的,实现IUnknown接口的方法这里不在赘述.在实现文件中写入: 

STDMETHODIMP 

CConnObject::XEventServer::DoSomething() 

{ 

//DoSomething 

METHOD_PROLOGUE(CConnObject,EventServer) 

pThis->FireEvent(); 

return S_OK; 

} 
IEventServer

  DoSomething()方法可以为客户提供需要的服务.这里着重的是可连接对象在此处触发客户事件接收器的事件,FireEvent()函数是ConnObject类实现的专门触发事件的的函数,代码如下: 

void CConnObject::FireEvent() 

{ 

//获取连接点上的连接指针队列 

const CPtrArray* pConnections = m_xSampleConnPoint.GetConnections(); 

ASSERT(pConnections!=NULL); 

int cConnections = pConnections->GetSize(); 

IEventSink* pIEventSink; 

//对每一个连接触发事件 

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

{ 

//获取客户事件接收器接口指针 

pIEventSink = (IEventSink*)(pConnections->GetAt(i)); 

ASSERT(pIEventSink!=NULL); 

//调用客户事件接受器事件处理函数 

//此函数是出接口定义,由客户事件接收器实现的 

pIEventSink->EventHandle(); 

} 

}
FireEvent

2.2、客户事件接收器(Sink) 

  事件接收器也是COM对象,也可以用嵌套类来实现,但是它只是客户的一个内部对象,所以可以没有CLSID和类厂.下面示例是一个对话框程序,对话框有三个按钮:”连接”(IDC_CONNECT),”断开”(IDC_DISCONNECT),”事件”(IDC_EVENT). 

1.创建一个基于对话框的工程:ConnClient. 

2.在CConnClientDlg中首先加入#include “IConnObject.h”,然后在对话框类声明中声明事件接收器嵌套类: 

BEGIN_INTERFACE_PART(EventSink,IEventSink) 

STDMETHOD(EventHandle)(); 

END_INTERFACE_PART(EventSink) 

同时声明几个私有变量: 

private: 

LPCONNECTIONPOINTCONTAINER pConnPtCont;//记录组件对象 

//IConnectionPointContainer接口指针 

LPCONNECTIONPOINT pConnPt;//记录连接点接口指针 

DWORD m_dwCookie;//记录连接标识 

IUnknown* m_pIUnknown;//用以记录组件对象IUnknown接口指针
CConnClientDlg

3.实现事件接收器: 

STDMETHODIMP_(ULONG) 

CConnClientDlg::XEventSink::AddRef() 

{ 

return 1; 

} 

STDMETHODIMP_(ULONG) 

CConnClientDlg::XEventSink::Release() 

{ 

return 0; 

} 

STDMETHODIMP 

CConnClientDlg::XEventSink::QueryInterface(REFIID riid,void** ppvObj) 

{ 

METHOD_PROLOGUE(CConnClientDlg,EventSink) 

if(IsEqualIID(riid,IID_IUnknown)|| 

IsEqualIID(riid,IID_IEventSink)) 

{ 

*ppvObj = this; 

AddRef(); 

return S_OK; 

} 

else 

{ 

return E_NOINTERFACE; 

} 

} 

STDMETHODIMP 

CConnClientDlg::XEventSink::EventHandle() //此函数将被可连接对象调用 

{ 

::AfxMessageBox("源对象向事件接收器发出了的通知!"); 

return S_OK; 

}
XEventSink

4.初始化COM库并创建组件对象实例 
  在CConnClientDlg::OninitDialog()中写入: 

HRESULT hResult; 

hResult = ::CoInitialize(NULL); 

if(FAILED(hResult)) 

{ 

::AfxMessageBox("不能初始化COM库!"); 

return FALSE; 

} 

m_pIUnknown = NULL; 

hResult = ::CoCreateInstance(CLSID_ConnObject,NULL, 

CLSCTX_INPROC_SERVER,IID_IUnknown,(void**)&m_pIUnknown); 

if(FAILED(hResult)) 

{ 

m_pIUnknown = NULL; 

::AfxMessageBox("不能创建ConnObject对象!"); 

return FALSE; 

} 

m_dwCookie = 0;//预置连接标识为0
CConnClientDlg

5.在按钮”连接”(IDC_CONNECT)的CLICK事件处理函数void CConnClientDlg::OnConnect()中写入: 

void CConnClientDlg::OnConnect() 

{ 

if(m_dwCookie!=0) 

{ 

return; 

} 

if(m_pIUnknown!=NULL) 

{ 

HRESULT hResult; 

hResult = m_pIUnknown->QueryInterface(IID_IConnectionPointContainer, 

(void**)&pConnPtCont); 

if(FAILED(hResult)) 

{ 

::AfxMessageBox("不能获取对象的IConnectionPointContainer接口!"); 

return; 

} 

ASSERT(pConnPtCont!=NULL); 

hResult = pConnPtCont->FindConnectionPoint(IID_IEventSink,&pConnPt); 

if(FAILED(hResult)) 

{ 

pConnPtCont->Release(); 

::AfxMessageBox("不能获取对象的IEventSink连接点接口!"); 

return; 

} 

ASSERT(pConnPt!=NULL); 

//获取事件接收器指针 

IUnknown* pIEventSink; 

m_xEventSink.QueryInterface(IID_IUnknown,(void**)&pIEventSink); 

//通过连接点接口的Advise方法将事件接收器指针传给可连接对象 

if(SUCCEEDED(pConnPt->Advise(pIEventSink,&m_dwCookie))) 

{ 

::AfxMessageBox("与可连接对象ConnObject建立连接成功!"); 

} 

else 

{ 

::AfxMessageBox("不能与ConnObject建立连接!"); 

} 

pConnPt->Release(); 

pConnPtCont->Release(); 

return; 

} 

} 
OnConnect

  上述代码与可连接对象的连接点建立连接. 

6.编写按钮”断开”(IDC_DISCONNECT)的CLICK处理函数如下: 

void CConnClientDlg::OnDisconnect() 

{ 

if(m_dwCookie==0) 

{ 

return; 

} 

pConnPt->Unadvise(m_dwCookie); 

pConnPt->Release(); 

pConnPtCont->Release(); 

m_dwCookie = 0; 

}
OnDisconnect

 

7.编写按钮”事件”(IDC_EVENT)的CLICK处理函数: 

void CConnClientDlg::OnEvent() 

{ 

if(m_pIUnknown!=NULL) 

{ 

IEventServer* pIEventServer; 

HRESULT hResult; 

hResult = m_pIUnknown->QueryInterface(IID_IEventServer,(void**)&pIEventServer); 

if(FAILED(hResult)) 

{ 

::AfxMessageBox("不能获取IEventServer接口!"); 

return; 

} 

pIEventServer->DoSomething(); 

} 

}
OnEvent

  这里,客户调用组件提供的服务DoSomething(),而正如前面所看到的,组件对象将在这个函数中触发一个由客户事件接收器处理(CConnClientDlg::XEventSink::EventHandle())的事件. 

8.在退出应用时: 

void CConnClientDlg::OnCancel() 

{ 

m_pIUnknown->Release(); 

::CoUninitialize(); 

CDialog::OnCancel(); 

}
OnCancel

  运行程序后,首先点击”连接”,然后点击”事件”按钮,这时将弹出MessageBox,并提示” 源对象向事件接收器发出了的通知!”. 

3、小结 

  正是由于有了可连接对象这一机制,实现了客户与组件对象的双向通信,使组件对象具有了事件机制.这种类似于”服务器推送(Server push)”的技术在分布式应用系统中十分重要. 

  本文所举示例是用基于IUnknown接口实现的,其实,用自动化接口IDispatch作为出接口更为方便.需要说明的是,用ATL来写可连接对象更为简洁,MSDN文档中有一个示例.

 

posted on 2013-09-10 15:56  冠冕堂皇  阅读(803)  评论(0)    收藏  举报