Download presentation
Presentation is loading. Please wait.
Published byJohannes Haaland Modified 6年之前
1
十二. 可连接对象 概念与模型 轮询 通知 出接口 连接点机制 IConnectionPointContainer接口
十二. 可连接对象 概念与模型 轮询 通知 出接口 连接点机制 IConnectionPointContainer接口 IConnectionPoint接口 接收器的实现 连接过程 事件的激发与处理 IDiapatch出接口 MFC对连接的支持 宏 事件激发 例子:用MFC实现源对象 例子:用MFC实现接收器
2
1 模型与概念 1.1 轮询 假设有一个这样的接口IWaiter.
1 模型与概念 1.1 轮询 假设有一个这样的接口IWaiter. [ object,uuid(2756E11C-A F-969C-14153E1D1601)] interface IWaiter: IUnknown { HRESULT BeginWork(void); //这是一项很费时的任务, 如果是同步执行,客户必须等待它完成. 我们假设它是异步地执行的,客户发出指令后,即刻返回,对象有可能另开辟新的线程进行处理.比如是一个数据库的处理或者是一个科学计算任务.完成以后,客户也得不到任何信息. HRESULT IsOK([out, retval] BOOL *yon); // 刚才吩咐的任务完成了吗? } 在这样的设计模式下,客户的使用方法: IWaiter *pIW; hr=CoCreateInstance(CLSID_Waiter, IID_IWaiter, &pIW); pIW->BiginWork(); //下达命令 BOOL Done=false; while(!Done) { Sleep(10000); //无奈地等待 pIW->IsOK(&Done); //再问一次
3
1.2 通知 更有效的做法是,由客户提供一个接口,这个接口可以供Waiter使用. (留下一个电话号码,做完了通知我!)
[ object,uuid(2756E11C-A F-969C-14153E1D1602)] interface INotify: IUnknown { HRESULT OnWorkIsOk(void);} 我们希望Waiter在完成任务以后能够及时地告知客户,而不是让客户一遍遍的轮询. 现在的问题是客户要有一种方法把这个接口告诉waiter.修改IWaiter接口如下: [ object,uuid(2756E11C-A F-969C-14153E1D1601)] interface IWaiter: IUnknown { HRESULT BeginWork(void); HRESULT Advise([in] INotify* pIN), [out] DWORD *pdwCookie); //客户通过Advise方法提供与此接口任务相关的接口INotify, 而Waiter则返回一个代表这种关联的DWORD值.以后,这个值将被用来解除这种关联使用. HRESULT UnAdvise([in] dwCookie); //客户使用UnAdvise通知Waiter解除与INotify的关联. (嗨,别再烦我了!) }
4
再来看CWaiter对IWaiter的实现方法, 首先它定义一个成员变量
INotify * m_pIN; HRESULT CWaiter::Advise(INotify* pIN), DWORD *pdwCookie) { if( m_pIN!=NULL) return E_UNEXPECTED; //已经跟别的对象关联上了. m_pIN=pIN; //保存对通知对象的引用,通知对象在客户端. Waiter作为客户端的通知对象的客户! m_pIN->AddRef(); //添加引用计数 *pdwCookie=DWORD(m_pIN); //记录下这种关联 return S_OK; } HRESULT CWaiter::UnAdvise(DWORD dwCookie) { if(DWORD(m_pIN)!=dwCookie) return E_UNEXPECTED // 核对一下 如果不是当前关联的对象. m_pIN->Release(); //减少引用计数 m_pIN=NULL; //再清空
5
HRESULT CWaiter::BeginWork(void)
{ assert(m_pIN) //确保已经有了关联 ….// 费时的任务 m_pIN->OnWorkIsOk(); //作完了,通知客户,免得它等得心焦 } 而客户此时的使用则是: INotify * m_pIN= new CNotify; IWaiter *pIW; hr=CoCreateInstance(CLSID_Waiter, IID_IWaiter, &pIW); pIW->Advise(m_pIN, &dwcookie); pIW->BiginWork(); //下达命令 异步的通知: CNotify::OnWorkIsOk() { MessageBox(“ Wake up !, Your work is OK!”); //当pIW完成工作以后通过通告接口调用此函数以通知客户. …… 比前面的轮询的方式好得多.
6
1.3 出接口 概念: 入接口:客户调用,组件响应。单向通讯 IWaiter
出接口:组件对象主动与客户通讯。组件调用,客户响应。对象通过出接口与客户通讯 INotify 支持出接口的对象称为可连接对象,或源对象。 出接口也是COM接口。包含一组成员函数。每个函数代表一个事件(Event)、通知(Notification)、或请求(Request)。 出接口的含义:在客户方实现,并把接口指针交给对象,对象利用此指针与客户进行通讯。 接收器:实现出接口的对象称为接收器(sink)
7
客户可以与可连接对象形成一对多或多对一的连接关系
接收器 客户把接收器的 接口指针传给对象 可连接对象调用 接收器的接口成员 客户与可连接对象模型 客户可以与可连接对象形成一对多或多对一的连接关系
8
2.连接点机制 以上例子中,IWaiter接口的功能意义实际上有了扩展,它不仅能实现BeginWork的功能,而且能够通过INotify接口向其他的个体发出信息. 实现IWaiter接口的可连接对象可以把这种特性公开来,向外界宣称,“如果你想得到完成任务的通知,那么,请实现出接口INotify,并且与我建立连接,把你的电话号码告诉我 . (要收费的哦)” 而出接口则声明“我是对WorkIsOk事件感兴趣的!”. 实际上,在实际的业务逻辑中,可能存在更加复杂的关系,一个源对象有可能还实现了接口IWaitress, 她提供另一种服务,并且使用另一个出接口来完成类似的通告任务. 同时,客户的出接口也有可能与多个可连接对象建立起了连接关系. 为了描述这种多对多的连接关系,COM使用一种更为通用的机制这就是连接点(connection point)机制: 一个可连接对象必须实现IConnectionPointContainer接口,用来管理所有的出接口. 对于每一个出接口,源对象又管理一个连接点对象, 每个连接点对象都实现一个IConnectionPoint接口. 每个连接点对象可以管理多个连接. 如下图:
9
IConnectionPointContainer
可连接对象 接收器 IConnectionPointContainer 连接点对象 枚举器 IConnectionPoint 一个可连接对象可支持多个出接口,在该对象的IConnectionPointContainer接口中使用一个枚举器暴露它所支持的所有出接口。 对于每一个出接口的连接点对象,在它的IConnetionPoint接口中使用一个枚举器管理它连接的所有的接收器。 枚举器(Enumerator)也是一个COM对象,它可以来访问一组数据单元。它只暴露枚举接口。
10
2.1 IConnectionPointContainer接口
IConnectionPointContainer : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE EnumConnectionPoints( IEnumConnectionPoints **ppEnum) = 0; virtual HRESULT STDMETHODCALLTYPE FindConnectionPoint( REFIID riid, IConnectionPoint **ppCP) = 0; }; EnumConnectionPoints返回连接点枚举器,客户可以使用此枚举器访问所有的连接点。 FindConnectionPoint根据客户指定的出接口IID,返回相应的连接点。
11
2.2 IConnectionPoint接口 每个连接点对象对应一个出接口,它只实现IConnetionPoint接口。
IConnectionPoint : public IUnknown { public: virtual HRESULT GetConnectionInterface( IID *pIID) = 0; // 返回对应出接口的IID virtual HRESULTGetConnectionPointContainer( IConnectionPointContainer * *ppCPC) = 0; // 返回源对象IConnectionPointContainer的指针 virtual HRESULT Advise( IUnknown *pUnkSink, DWORD *pdwCookie) = 0; // 被客户用来建立接收器与源对象的连接。 virtual HRESULT Unadvise(DWORD dwCookie) = 0; // 被客户用来取消接收器与源对象的连接。 virtual HRESULT EnumConnections( IEnumConnections **ppEnum) = 0; };// 返回一个连接枚举器接口指针,被客户用来访问所有建立在此连接点对象上的连接。 连接使用CONNECTDATA数据来描述。CONNECTDATA包含两个数据成员,IUnkown类型的pUnk和DWORD类型的dwCookie,pUnk对应于接收器对象的接口指针,而dwCookie是连接点对象生成的一个整数。Unadvise将利用此整数找到相应的连接并取消它。
12
2.3 接收器的实现 class CSomeEventSet :public ISomeEventSet
2.3 接收器的实现 class CSomeEventSet :public ISomeEventSet { private : int m_Ref; public : DWORD m_dwCookie; // 连接键 CSomeEventSet(); ~CSomeEventSet(); virtual HRESULT _stdcall QueryInterface( REFIID riid, void **ppv) ; virtual ULONG _stdcall AddRef( void) ; virtual ULONG _stdcall Release( void) ; virtual HRESULT SomeEventFunction(......); ......} 接收器完全是客户的内部对象,不需要类厂来创建,也不需要CLSID。当然,它的接口ISomeEventSet是有明确的IID的。 客户可以直接使用new操作符创建接收器。
13
3 连接过程 3.1 连接过程 ISomeEventSet *pSomeEventSet; IUnkown *pUnk;
3 连接过程 3.1 连接过程 ISomeEventSet *pSomeEventSet; IUnkown *pUnk; IConnectionPointerContainer* pConnectinoPointContainer; IConnectionPoint *pConnectionPoint; //各种接口指针的声明 .... CSomeEventSet *pSink=new CSomeEventSet; //1 创建接收器对象 pSink->QueryInterface(IID_ISomeEventSet,&pSomeEventSet); // 2 返回接收器对象的接口指针 CoCreateInstance(CLSID_ISourceObj,NULL,CLSCTX_INPROC_SERVER,IID_IUnkown,&pUnk); //3 创建源对象,返回IUnkown接口指针 pUnk->QueryInterface(IID_IConnectionPointContainer, &pConnectPointContainer); // 4向源对象查询IConnectionPointContainer接口 pConnectionPointContainer->FindConnectionPoint(IID_ISomeEventSet, &pConnectionPoint); //5向源对象查询支持出接口的连接点对象 pConnectionPoint->Advise(pSomeEventSet,&pSink->m_dwCookie);//6 建立连接 ......// 7在此期间,源对象可以利用pSomeEventSet调用接收器对象的方法。见下 pConnectionPoint->Unadvise(pSink->m_dwCookie);//8断开连接 ......
14
3.2事件的激发与处理 接收器和源对象连接建立起来后,源对象可以激发事件或向客户发起请求,事件可以由:1.源对象的入接口成员函数激发。2.用户的操作激发。3.其他对象或客户调用激发。 BOOL CSourceObj::FireSomeEvent() { IEnumConnections * pEnum; IConnectionPoint *pConnectionPoint; CONNECTDATA connectionData; ISomeEventSet * pSomeEventSet; FindConnectionPoint(IID_ISomeEventSet,&pConnectionPoint); //寻找与此出接口对应的连接点对象 pConnectionPoint->EnumConnections(&pEnum);//得到连接枚举器 //对此连接点上连接的所有接收器发出请求 while(pEnum->Next(1,&connectionData,NULL)==NOERROR) { if(SUCCEEDED(connectionData.pUnk-> QueryInterface(IID_ISomeEventSet,(void**)&pSomeEventSet))) //由连接数据中取出接收器对象指针,此指针在Advise时由客户传入 //由此接口指针再查询出接口指针。 { pSomeEventSet->SomeEventFunction(); //调用出接口的函数。 pSomeEventSet->Release();} pEnum->Release(); return TRUE;}
15
4 IDispatch出接口 在编译时刻,客户不一定知道源对象支持什么出接口。客户只知道源对象实现了IID_ISourceObj入接口,然后通过查询IConnectionPointContainer知道它是支持出接口的。当然客户可以调用IConnectionPointerContainer的EnumConnectionPoint函数得到连接点枚举器,以此枚举器逐个得到连接点,然后调用连接点的IConnectionPoint::GetConnectionInterface函数得到它所支持的出接口IID。 然后,客户必须获得出接口的成员函数信息。然后动态地创建接收器对象。然而需要高度的编程技巧。
16
以IDispatch接口作为出接口。 IDispatch接口把所有的调用都通过Invoke函数来实现,并且提供了管理属性和方法的分发ID机制,以及一套描述参数和返回值的方法,所以使得运行时刻动态绑定属性和方法成为可能。 用IDispatch接口作为出接口可以解决接收器的动态创建过程。利用IDispatch作为源对象的出接口,由源对象提供出接口的类型信息,客户程序根据这些信息,在invoke函数中调用相应的事件控制函数。 过程: 1。从IDispatch接口派生新的接口作为出接口,把方法和属性加到派生接口中,并为之赋予分发ID。源对象通过类型库或IProvideClassInfo接口暴露出接口的类型信息。以后,源对象调用请求时,使用IDispatch::Invoke即可。 2。客户按照源对象提供的出接口类型信息实现接收器对象。接收器只需要实现IUnkown的成员函数和Invoke成员函数即可。由于出接口时源对象定义的,它当然知道接口的每个方法和属性以及其分发ID,所以接收器对象不需要实现其他的几个函数。
17
5 MFC对连接的支持。 5.1 宏 MFC实现了连接点类 CConnetionPoint
5.1 宏 MFC实现了连接点类 CConnetionPoint CConnectionPoint实现了IConnectionPoint接口。(见P189 ) CCmdTarget提供了一组宏来支持连接点对象。 1。使用DECLARE_CONNECTION_MAP()来定义连接点映射表以及以及有关表的操作函数。 #define DECLARE_CONNECTION_MAP() \ private: \ static const AFX_CONNECTIONMAP_ENTRY _connectionEntries[]; \ protected: \ static AFX_DATA const AFX_CONNECTIONMAP connectionMap; \ static const AFX_CONNECTIONMAP* PASCAL _GetBaseConnectionMap(); \ virtual const AFX_CONNECTIONMAP* GetConnectionMap() const; \
18
2。使用BEGIN_CONNECTION_MAP, CONNECTION_PART 和END_CONNECTION_MAP()来对连接映射表赋值。
#define BEGIN_CONNECTION_MAP(theClass, theBase) \ const AFX_CONNECTIONMAP* PASCAL theClass::_GetBaseConnectionMap() \ { return &theBase::connectionMap; } \ const AFX_CONNECTIONMAP* theClass::GetConnectionMap() const \ { return &theClass::connectionMap; } \ AFX_COMDAT const AFX_DATADEF AFX_CONNECTIONMAP theClass::connectionMap = \ { &theClass::_GetBaseConnectionMap, &theClass::_connectionEntries[0], }; \ AFX_COMDAT const AFX_DATADEF AFX_CONNECTIONMAP_ENTRY theClass::_connectionEntries[] = \ { \ #define CONNECTION_PART(theClass, iid, localClass) \ { &iid, offsetof(theClass, m_x##localClass) }, \ #define END_CONNECTION_MAP() \ { NULL, (size_t)-1 } \ }; \ 其中也使用了offset宏来计算类的嵌套类成员到父类的偏移。
19
3。使用BEGIN_CONNECTION_PART ,CONNECTION_IID,和END _CONNECTION_PART 来定义内嵌的嵌套类连接点对象。
#define BEGIN_CONNECTION_PART(theClass, localClass) \ class X##localClass : public CConnectionPoint \ { \ public: \ X##localClass() \ { m_nOffset = offsetof(theClass, m_x##localClass); } #define CONNECTION_IID(iid) \ REFIID GetIID() { return iid; } #define END_CONNECTION_PART(localClass) \ } m_x##localClass; \ friend class X##localClass; 我们看到连接点对象从CConnectionPoint派生。宏CONNECTION_IID指定CConnectionPoin的虚函数成员返回出接口的IID。宏END_CONNECTION_PART定义了一个内嵌的成员m_x*.
20
CCmdTarget有一个内嵌的结构成员m_xConnPtContainer用于存放IConnectionPointContainer的虚表和偏移。
我们需要在接口映射表中加入 INTERFACE_PART(CSourceObj, IID_IConnectionPointContainer, ConnPtContainer)以使得源对象支持IConnectionPointContainer接口,我们早就谈到,这是客户判断一个对象是否源对象的标志。
21
5.2 事件激发 由于使用了IDispatch作为出接口,所以激发函数就是调用接收器的invoke函数。MFC提供了一个封装类COleDispatchDriver。它主要用于IDispatch接口的客户方调用操作。利用COleDispatchDriver的成员函数,客户可以创建自动化对象,也可以把COleDispatchDriver对象与某个自动化对象联系起来。更重要的是,它使得调用invoke函数的参数处理简单一些。
22
void CSourceObj::FirePropChanged (long nInt)
{ COleDispatchDriver driver; POSITION pos = m_xEventSetConnPt.GetStartPosition(); LPDISPATCH pDispatch; while (pos != NULL) {pDispatch = (LPDISPATCH) m_xEventSetConnPt.GetNextConnection(pos); ASSERT(pDispatch != NULL);//得到连接点对象所对应的出接口 driver.AttachDispatch(pDispatch, FALSE); //出接口和driver联系在一起 TRY driver.InvokeHelper(0/, DISPATCH_METHOD, VT_EMPTY, NULL,(BYTE *)(VTS_I4), nInt); //通过driver来调用invoke函数 END_TRY driver.DetachDispatch(); }
23
6 例子:用MFC实现源对象 新建MFC DLL工程SourceComp,选中automation
添加新类CSourceObj派生自CCmdTarget指定Create By type id 定义出接口IEventSet编辑odl文件.使用GUIDGen产生一个GUID 指定对象为源对象,且支持IEventSet coclass SourceObj { [default] dispinterface ISourceObj; [default,source] dispinterface IEventSet; }; 在CSourceObj的头文件中,加入连接点申明和连接点对象定义。 BEGIN_CONNECTION_PART(CSourceObj, EventSetConnPt) virtual REFIID GetIID(); END_CONNECTION_PART(EventSetConnPt) DECLARE_CONNECTION_MAP() 在CSourceObj的实现文件中构造函数中加入EnableConnections(); 加入IEventSet的IID定义 static const IID IID_IEventSet = { 0xb77c2985, 0x56dd, 0x11cf, { 0xb3, 0x55, 0x0, 0x10, 0x4b, 0x8, 0xcc, 0x22 } };
24
接口映射表中加入IConnectionPointContainer表项
INTERFACE_PART(CSourceObj, IID_IConnectionPointContainer, ConnPtContainer) 加入连接映射表的赋值 BEGIN_CONNECTION_MAP(CSourceObj, CCmdTarget) CONNECTION_PART(CSourceObj, IID_IEventSet, EventSetConnPt) END_CONNECTION_MAP() 实现虚函数GetIID REFIID CSourceObj::XEventSetConnPt::GetIID(void) { return IID_IEventSet; } XEventSetConnPt类继承自类CConnectionPoint,后者有一个纯虚的函数GetIID,所以必须给出实现。它返回它所支持的出接口的IID。
25
11。事件激发函数: void CSourceObj::FirePropChanged (long nInt) { COleDispatchDriver driver; POSITION pos = m_xEventSetConnPt.GetStartPosition(); LPDISPATCH pDispatch; while (pos != NULL) {pDispatch = (LPDISPATCH) m_xEventSetConnPt.GetNextConnection(pos); ASSERT(pDispatch != NULL);//得到连接点对象所对应的出接口 driver.AttachDispatch(pDispatch, FALSE);//出接口和driver联系在一起 TRY driver.InvokeHelper(0/, DISPATCH_METHOD, VT_EMPTY, NULL,(BYTE *)(VTS_I4), nInt); //通过driver来调用invoke函数,向所有连接点的接收器激发分发ID为0的事件。 END_TRY driver.DetachDispatch(); }} void CSourceObj::SetMyProperty(long nNewValue) { mProperty = nNewValue; FirePropChanged (mProperty);} 编译。注册。regsvr32
26
7 例子:用MFC实现接收器 1。新建MFC exe 基于dialoag box 2。AfxOleInit()初始化自动化。
3。加入成员变量IDispatch * m_pDispatch 以保存源对象的接口指针DWORD m_dwCookie以保存接收器与源对象的连接标识 4.对话框类也派生自CCmdTarget,给对话框类加入内嵌的接收器对象。也即对话框类也要实现IEventSet接口。 BEGIN_INTERFACE_PART(EventSink, IDispatch) INIT_INTERFACE_PART(CTestCtrlDlg, EventSink) STDMETHOD(GetTypeInfoCount)(unsigned int*); STDMETHOD(GetTypeInfo)(unsigned int, LCID, ITypeInfo**); STDMETHOD(GetIDsOfNames)(REFIID, LPOLESTR*, unsigned int, LCID, DISPID*); STDMETHOD(Invoke)(DISPID, REFIID, LCID, unsigned short, DISPPARAMS*, VARIANT*, EXCEPINFO*, unsigned int*); END_INTERFACE_PART(EventSink)
27
5。接收器对象对IUnkown的成员函数的实现和对IDispatch的除invoke外的成员函数的实现都可以简化处理。因为没有人会调用它们。
STDMETHODIMP CTestCtrlDlg::XEventSink::Invoke( DISPID dispid, REFIID, LCID, unsigned short wFlags, DISPPARAMS* pDispParams, VARIANT* pvarResult, EXCEPINFO* pExcepInfo, unsigned int* puArgError) { if (dispid == 0) AfxMessageBox("The Property has been changed!"); else AfxMessageBox("I don't known the event!"); return S_OK; } 对于分发ID为0的事件,invoke响应一个对话框。
28
7。创建源对象 GUID sourceobjCLSID; HRESULT hResult = ::CLSIDFromProgID(L"SourceComp.SourceObj", &sourceobjCLSID); if (FAILED(hResult)) return FALSE; hResult = CoCreateInstance(sourceobjCLSID, NULL, CLSCTX_INPROC_SERVER, IID_IDispatch, (void **)&m_pDispatch); //创建源对象,获取源对象的IDispatch接口指针。 注意源对象 coclass SourceObj { [default] dispinterface ISourceObj; [default,source] dispinterface IEventSet; }; ISourceObj和IEventSet都是自动化接口。 此接口指针将要用来查询IConnectionPointContainer。
29
8。连接: static const IID IID_IEventSet ={……} int CTestCtrlDlg::Connection() { BOOL RetValue = 0; if (m_dwCookie != 0) return 2; LPCONNECTIONPOINTCONTAINER pConnPtCont; if ((m_pDispatch != NULL) &&SUCCEEDED(m_pDispatch->QueryInterface( IID_IConnectionPointContainer,(LPVOID*)&pConnPtCont))) // 查询IConnectionPointContainer接口。 { LPCONNECTIONPOINT pConnPt = NULL; DWORD dwCookie = 0; // 查询IID_IEventSet连接点对象 if (SUCCEEDED(pConnPtCont->FindConnectionPoint (IID_IEventSet, &pConnPt))) {pConnPt->Advise(&m_xEventSink, &dwCookie); // 进行连接,把自己的接收器对象传入。源对象将利用此指针激发事件。 m_dwCookie = dwCookie;//连接标志保存下来,供断开使用 RetValue = 1;pConnPt->Release(); } pConnPtCont->Release(); m_dwCookie = dwCookie; } return RetValue; }
30
9 断开: // 查询IConnectionPointContainer接口。 if ((m_pDispatch != NULL) &&SUCCEEDED(m_pDispatch->QueryInterface( IID_IConnectionPointContainer,(LPVOID*)&pConnPtCont))) {LPCONNECTIONPOINT pConnPt = NULL; // 查询IID_IEventSet连接点对象 if (SUCCEEDED(pConnPtCont->FindConnectionPoint(IID_IEventSet, &pConnPt))) {pConnPt->Unadvise(m_dwCookie); //使用连接标志断开。 pConnPt->Release(); m_dwCookie = 0; RetValue = 1; }
31
10 用户的设置属性操作。 void CTestCtrlDlg::OnSetproperty() { COleDispatchDriver driver; //由于源对象也是自动化对象,这里也使用COleDispatchDriver来操作自动化接口。将源对象的自动化接口与driver连起来。 driver.AttachDispatch(m_pDispatch, FALSE); TRY driver.SetProperty(0x1, VT_I4, m_Property); // 使用driver来调用自动化接口的属性。 END_TRY driver.DetachDispatch(); }
Similar presentations