Understanding COM Event Handling
源码下载:http://pan.baidu.com/s/1geg2hxh 密码: gkr3
Change of Article Title
Please note that I have changed the title of this article from "TEventHandler - A C++ COM Event Handler For IDispatch-Based Events" to the current title "Understanding COM Event Handling." The latter, I believe, is a better title which gives a more accurate picture of the intended theme of this article, i.e. to expound carefully the internal mechanisms behind COM event handling.
Introduction
If you have ever done any development work involving the use of COM objects, chances are you would have encountered the need for COM object event handling. Visual Basic users will know how simple it is to connect with the event interface(s) of the COM (or ActiveX) objects. The VB IDE lays out the event handling function codes nicely for the user. All the user has to do is to fill in the details of the event handling functions.
For Visual C++ users, this is not always so straight forward. If your COM object happens to be an ActiveX control and you are using MFC, then yes, the Visual C++ IDE provides Wizards that can help you generate event handler function stubs. All the necessary codes (e.g. inserting the event sink map and event entry macros) are done automatically for you.
But what if your COM object is not an ActiveX control? What if you are using straight COM objects which also fire events and you need to handle those events?
If you are an MFC user, you may want to tinkle with the various MFC macros to see if you can fit in an event handler function into your code either manually or via the Wizard. I personally believe this is possible. But you need to be armed with an intimate knowledge of MFC and its generated macros.
If you do not use MFC, you may want to experiment with ATL codes (e.g. IDispEventImpl
, BEGIN_SINK_MAP
,SINK_ENTRY_EX
, etc.) to perform event handling. The ATL macro codes are certainly not simple but they are well-documented in MSDN and they do provide standard handling mechanisms.
In this article, I will go back to basics and seek to explain the fundamental principles of how event handling is done in COM. I will also provide a C+ class which serves as a basic and simple (at least in terms of code overhead) facilitator for COM Object Event Handling.
I do this via a special custom-developed template class named TEventHandler
which I have used in many projects. This class uses COM first principles and primitives and avoids the use of complicated macros. The sections following expound this class in detail. I assume that the reader is suitably conversant with C++, ATL and the concepts of template classes. However, before we start discussing the TEventHandler
class, let us explore the fundamental principles of Event Handling in COM.
Event Handling in COM
Incoming Interfaces
When we develop our usual COM objects, we provide implementations for interfaces (defined in an IDL file) which we write ourselves or have been supplied to us. Such implementations facilitate what are known as "incoming" interfaces. By "incoming," we imply that the object "listens" to its client. That is, the client calls methods of the interface and, in this way, "talks" to the object.
Referring to the diagram below, we can say that ISomeInterface
is an "incoming" interface provided by the COM object on the right.
Outgoing Interfaces
As Kraig Brockschmidt puts it so well in his book "Inside OLE," many COM objects themselves have useful things to say to their clients. And clients may want to listen to COM objects too. If such a two-way dialog is desired, something known as an "outgoing" interface is required. The term "outgoing" is used in the context of the COM object. It is outgoing in the perspective of the COM object. Imagine a situation in which the role of "talker" and "listener" is reversed as shown in the diagram below:
Referring to the diagram above, we can say that ITalkBackInterface
is an "outgoing" interface supported by the COM object on the right. The COM object invokes the methods of ITalkBackInterface
and it is the Clientthat implements the ITalkBackInterface
methods. A COM object that supports one or more outgoing interfaces is known as a Connectable Object or a Source. A connectable object can support as many outgoing interfaces as it likes. Each method of the outgoing interface represents a single event or request. All COM objects, regardless of whether they are non-visual COM objects or ActiveX controls (generated manually or via MFC or ATL), use the same mechanism (connectability) for firing events to their clients.
Events and Requests
Events are used to tell a client that something of interest has occurred in the object - a property has changed or the user has clicked a button. Events are particularly important for COM controls. Events are fired by COM objects and no response from the client is expected. In other words, they are simple notifications. Requests, on the other hand, is how a COM object asks the client a question and expects a response in return. Events and requests are similar to Windows messages, some of which inform a window of an event (e.g. WM_MOVE
) and some will ask for information from the window (e.g. WM_QUERYENDSESSION
).
Sinks
In both cases, the client of the COM object must listen to what the object has to say and then use that information appropriately. It is the client, therefore, that implements the outgoing interfaces which are also known as sinks (I really dislike this name but it has become common and ubiquitous in the world of COM and .NET). From a sink's perspective, this outgoing interface is actually incoming. The sink listens to the COM object through it. In this context, the connectable COM object plays the role of a client.
How Things are Tied Up Together
Let us take a helicopter view of the entire communications situation. There are three participants:
- The COM object itself
- The Client of the COM object
- The Sink
The Client communicates with the COM object as usual via the object's incoming interface(s). In order for the COM object to communicate back to the client in the other direction, the COM object must somehow obtain a pointer to an outgoing interface implemented somewhere in the client. Through this pointer, the COM object will send events and requests to the client. This somewhere is the Sink. Let us illustrate the above with a simple diagram:
Note that the Sink is an object by itself. The Sink provides the implementation for one or more outgoing interfaces. It is also usually strongly tied to the other parts of the client's code because the whole idea of implementing a sink is for the client code to be able to react to an event and/or to respond to some request from the COM object.
How Does a COM Object Connect to a Sink?
But how does a COM object connect to a Sink in the first place? This is where the notion of Connection Points and Connection Point Containers come in. We will explore this in detail in the next section.
Connection Points and Connection Point Containers
For each outgoing interface that a COM object supports (note the use of the word "support;" the COM object itself does not implement this interface, but rather, invokes it), the COM object exposes a small object called aconnection point. This connection point object implements the IConnectionPoint
interface.
It is through this IConnectionPoint
interface that the client passes its Sink's outgoing interface implementation to the COM object. Reference counts of these IConnectionPoint
objects are kept by both the client and the COM object itself to ensure the lifespan of the two-way communications.
The method to call (from the client side) to establish event communication with the COM object is theIConnectionPoint::Advise()
method. The converse of Advise()
is IConnectionPoint::Unadvise()
which terminates a connection. Please refer to MSDN documentation for more details of these methods.
Hence, via IConnectionPoint
interface method calls, a client can start listening to a set of events from the COM object. Note also that because a COM object maintains a separate IConnectionPoint
interface for every outgoing interface it supports, a client must be able to use the correct connection point object for every sink it implements.
How then, does the Client choose the appropriate connection point for a sink? In comes Connection Point Containers. An object which is connectable must also be a Connection Point Container. That is, it must implement the IConnectionPointContainer
interface. Through this interface, a Client requests for the appropriate Connection Point object of an outgoing interface.
When a client wants to connect a Sink to a Connection Point, it asks the Connection Point Container for the Connection Point object for the outgoing interface implemented by that Sink. When it receives the appropriate connection point object, the Client passes the Sink's interface pointer to that connection point. TheIConnectionPointContainer
interface pointer itself can be obtained easily via QueryInterface()
on the COM object itself. Nothing speaks better than an example code which is listed below:
void Sink::SetupConnectionPoint(ISomeInterface* pISomeInterface)
{
IConnectionPointContainer* pIConnectionPointContainerTemp = NULL;
IUnknown* pIUnknown = NULL;
/*QI this object itself for its IUnknown pointer which will be used */
/*later to connect to the Connection Point of the ISomeInterface object.*/
this -> QueryInterface(IID_IUnknown, (void**)&pIUnknown);
if (pIUnknown)
{
/* QI pISomeInterface for its connection point.*/
pISomeInterface -> QueryInterface (IID_IConnectionPointContainer,
(void**)&pIConnectionPointContainerTemp);
if (pIConnectionPointContainerTemp)
{
pIConnectionPointContainerTemp ->
FindConnectionPoint(__uuidof(ISomeEventInterface),
&m_pIConnectionPoint);
pIConnectionPointContainerTemp -> Release();
pIConnectionPointContainerTemp = NULL;
}
if (m_pIConnectionPoint)
{
m_pIConnectionPoint -> Advise(pIUnknown, &m_dwEventCookie);
}
pIUnknown -> Release();
pIUnknown = NULL;
}
}
The sample function above describes the SetupConnectionPoint()
method of a Sink
class. The Sink
class implements the methods of the outgoing interface ISomeEventInterface
. The SetupConnectionPoint()
method takes a parameter which is pointer to an interface named ISomeInterface
. The COM object behindISomeInterface
is assumed to be a Connection Point Container. The following is an outline of the function's logic:
- We first
QueryInterface()
theSink
object itself for itsIUnknown
interface pointer. ThisIUnknown
pointer will be used later in the call toIConnectionPoint::Advise()
. - Having successfully obtained the
IUnknown
pointer, we nextQueryInterface()
the object behindpISomeInterface
for itsIConnectionPointContainer
interface. - Having successfully obtained the
IConnectionPointContainer
interface pointer, we use it to find the appropriate Connection Point object for the outgoing interfaceISomeEventInterface
. - If we are able to obtain this Connection Point object (it will be represented by
m_pIConnectionPoint
), we will proceed to call itsAdvise()
method. - From here onwards, whenever the COM object behind
pISomeInterface
fires an event to the sink (by calling one of the methods ofISomeEventInterface
), the corresponding method implementation in theSink
object will be invoked.
I certainly hope that the above introductory sections on Event Handling in COM, Connection Points and Connection Point Containers will have served to provide the reader with a clear understanding of the basics of event handling. It is worth re-mentioning that the above principles are used whatever the type of COM object is involved (e.g. straight COM object, ActiveX controls, etc). With the basics explained thoroughly, we shall proceed to expound on the sample source codes, especially the TEventHandler
template class.
The Sample Source Code
I will attempt to explain TEventHandler
by running through some example codes. Please refer to the source files which are contained in the TEventHandler_src.zip file. In the set of sample codes, I have provided two sets of projects:
- EventFiringObject
- TestClient
EventFiringObject
The EventFiringObject project contains the code for a simple COM object which implements an interface namedIEventFiringObject
. This COM object is also a Connection Point Container which recognizes the_IEventFiringObjectEvents
connection point. The relevant IDL constructs for this COM object is listed below for discussion purposes:
[
object,
uuid(8E396CC0-A266-481E-B6B4-0CB564DAA3BC),
dual,
helpstring("IEventFiringObject Interface"),
pointer_default(unique)
]
interface IEventFiringObject : IDispatch
{
[id(1), helpstring("method TestFunction")]
HRESULT TestFunction([in] long lValue);
};
[
uuid(32F2B52C-1C07-43BC-879B-04C70A7FA148),
helpstring("_IEventFiringObjectEvents Interface")
]
dispinterface _IEventFiringObjectEvents
{
properties:
methods:
[id(1), helpstring("method Event1")] HRESULT Event1([in] long lValue);
};
[
uuid(A17BC235-A924-4FFE-8D96-22068CEA9959),
helpstring("EventFiringObject Class")
]
coclass EventFiringObject
{
[default] interface IEventFiringObject;
[default, source] dispinterface _IEventFiringObjectEvents;
};
In the EventFiringObject project, we implement a C++ ATL class named CEventFiringObject
which implements the specifications of coclass
EventFiringObject
. CEventFiringObject
provides a simple implementation of the TestFunction()
method. It simply fires Event1
which is specified in_IEventFiringObjectEvents
.
STDMETHODIMP CEventFiringObject::TestFunction(long lValue)
{
/* TODO: Add your implementation code here */
Fire_Event1(lValue);
return S_OK;
}
TestClient
TestClient is a simple test application: an MFC dialog-based application which instantiates theEventFiringObject
COM object. It also attempts to handle the Event1
event fired fromEventFiringObject
. I will walk through the TestClient code more thoroughly to explain the process of event handling. The client code centers around the CTestClientDlg
class which is derived from CDialog
. InTestClientDlg.h, notice that we declare an instance of a smart pointer object which will be tied to the COM object which implements IEventFiringObject
:
/* ***** Declare an instance of a IEventFiringObject smart pointer. ***** */
IEventFiringObjectPtr m_spIEventFiringObject;
Then, in the CTestClientDlg::OnInitDialog()
function, we instantiate m_spIEventFiringObject
:
/* ***** Create an instance of an object
which implements IEventFiringObject. ***** */
m_spIEventFiringObject.CreateInstance(__uuidof(EventFiringObject));
We also create a button in our simple dialog box labeled "Call Test Function." In the click handler for this button, we invoke the TestFunction()
of our m_spIEventFiringObject
:
/* ***** Call the IEventFiringObject.TestFunction(). ***** */
/* ***** This will cause the object which implements ***** */
/* ***** IEventFiringObject to fire Event1. ***** */
m_spIEventFiringObject -> TestFunction(456);
Thus far, we have dealt with mostly typical COM client code. The fun begins when we invoke TestFunction()
. We know that TestFunction()
will cause the m_spIEventFiringObject
COM object to fire the Event1
event. This is where the real action starts.
The TEventHandler Class
General Design Goals and Example Use Case
The TEventHandler
class is supplied in TEventHandler.h. It works according to the following design:
- It serves the role of a Sink for a client.
- It generically handles one event interface which must be dispinterface-based (i.e. derived from
IDispatch
). - After receiving an event fired from the COM object,
TEventHandler
will call a predefined method of its client. This is howTEventHandler
clients get notified of events fired from the COM object.
The predefined method of the client must be defined using the following signature:
typedef HRESULT (event_handler_class::*parent_on_invoke)
(
TEventHandler<event_handler_class, device_interface,
device_event_interface>* pthis,
DISPID dispidMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS* pdispparams,
VARIANT* pvarResult,
EXCEPINFO* pexcepinfo,
UINT* puArgErr
);
Notice that the predefined method's parameters match those of IDispatch::Invoke()
except for the addition of a parameter (first in the list) which is a pointer to the TEventHandler
instance itself. This parameter is supplied as it may be useful to client code. In the context of our example code, our usage of TEventHandler
can be represented by the following diagram:
What TEventHandler is Not Designed to Do
Why do we make an apparent rehash of the IDispatch::Invoke()
method? What value-add couldTEventHandler
have if your callback function still has to handle all the parameters of theIDispatch::Invoke()
call?
The answer is that the TEventHandler
class is not primarily designed to simplify the handling of the parameters of the event methods (although this might be possible, see my comments on this later in this section).
It is designed to be a sink object. It is meant to readily make available (for a client) a sink which can be hooked up to receive the dispinterface-based event of a COM object. And then have the sink automatically call the mirrorInvoke()
method of the client.
Note that using C++ templates alone, it will not be possible to anticipate in advance the return values and parameter lists of event methods. This would require the work of Wizards which can read all these information from the type libraries associated with the connectable COM objects and then generating function codes which match the signatures of event methods.
TEventHandler
is not a wizard. It is a C++ template (which makes it sort of a bona-fide code generator, but it can't do everything, e.g. read a type library and generate code according to information found therein...). It is also meant to be a Sink for one event interface which must be derived from IDispatch
, and hence TEventHandler
implements IDispatch
(see the next section "Why Is TEventHandler Dispinterface-based?" for more information on the reasons behind this).
Why is TEventHandler Dispinterface-based?
In order for TEventHandler
to be a Sink for ISomeEventInterface
, it must be derived fromISomeEventInterface
and it must implement the methods of that interface. I have chosen to makeTEventHandler
derive from IDispatch
, thereby making it a Sink for an event interface that also derives fromIDispatch
.
Interfaces that derive from IDispatch
are also known as dispinterfaces (for dispatch interfaces). Interfaces, in general (not just event interfaces), that are dispinterfaces are very common. They are common because of the long-time need for COM to support Visual Basic.
The IDispatch
interface is the basis behind Visual Basic's achievement of something known as late bindingwhich means the act of programmatically constructing a function call (together with the inclusion of parameters and the receipt of return values) at runtime. It is perhaps the most generic and flexible of all COM interfaces.
Take note that I'm using the term late binding in a generic way (I'm not referring to the C++ concept of vtables which is another implementation of late binding). IDispatch
can be used to define a virtual interface the methods of which are called via the IDispatch::Invoke()
method. This system of runtime function invocation is known as automation (formerly OLE-automation).
To distinguish one IDispatch
virtual interface from another, something known as a Dispatch Interface ID is used (DIID). This is programmatically no different from a normal Interface ID (IID). Take a look at the following code fragment:
pIConnectionPointContainerTemp -> FindConnectionPoint
(
__uuidof(ISomeEventInterface),
&m_pIConnectionPoint
);
Here, we are asking a connection point container to return to us a connection point that supports the outgoing interface that is identified by the DIID which is equivalent to the result of __uuidof(ISomeEventInterface)
.
Visual Basic applications can only handle ActiveX object events through sinks which are dispinterface-based. This is natural both because of the fact that Visual Basic cannot handle non-dispinterface-based events, and also because of the need to handle events fired from any and all ActiveX objects.
The central point behind this is that while event interfaces need not be dispinterface-based (their methods can be of any signature, the only mandate is that the event interface must also derive from IUnknown
), Visual Basic is not able to internally anticipate the design of these custom event interfaces and to generate Sinks for them.
Furthermore, the types of method return values and parameters must be confined to those that Visual Basic is able to understand and internally process. Hence, the only way to standardize the handling of event interfaces is to require that they be derived from IDispatch
, and that the return and parameter types be from a wide but limited ranged set. This set of types is known as the automation-compatible-types.
Like Visual Basic, it is not possible for us to design the TEventHandler
template class to be a Sink for custom event interfaces. Remember that we must actually implement the methods of the event interface thatTEventHandler
is to be a Sink for. Hence, I decided that TEventHandler
should handle only dispinterface-based events. And there are plenty of COM object sources which support them.
Code Details
The TEventHandler
template class is defined (in summary) as follows:
template <class event_handler_class, typename device_interface,
typename device_event_interface>
class TEventHandler : IDispatch
{
...
...
...
}
Note that TEventHandler
is derived from IDispatch
. However, note, it need not implement all of the methods of IDispatch
. Only the basic AddRef()
, Release()
, QueryInterface()
and the Invoke()
methods are required at minimum. TEventHandler
takes in three template parameters which are:
event_handler_class
device_interface
device_event_interface
The event_handler_class
parameter indicates to TEventHandler
the name of the class which will contain the predefined method to be invoked when the COM object fires an event of the outgoing interface. In out example use case, this parameter will be the CTestClientDlg
class. The CTestClientDlg
class will contain the invocation function:
HRESULT CTestClientDlg::OnEventFiringObjectInvoke ( IEventFiringObjectEventHandler* pEventHandler, DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr )
It is in this function that CTestClientDlg
will handle events fired from the EventFiringObject
COM object. The device_interface
parameter refers to the interface type of the COM object (whose event we are trying to receive). In our example use case, this will be IEventFiringObject
.
The last template parameter is device_event_interface
and this indicates the interface type of the outgoing interface supported by the COM object. This will be the interface that must be implemented by the Sink object. In our example use case, this will be _IEventFiringObjectEvents
. And note that because_IEventFiringObjectEvents
is essentially derived from IDispatch
, our Sink object (which isTEventHandler
) is also derived from IDispatch
.
All the above template parameters are used in order that the VC++ compiler be able to generate a C++ class which has been tailored to contain methods, properties and parameter types which match those ofCTestClientDlg
, IEventFiringObject
and _IEventFiringObjectEvents
. Hence, a customized class will eventually be created for further use in the code.
Usage
Usage of the TEventHandler
class is simple and straightforward. Let us walk through some example codes from TestClient:
- We need to define a specific class type based on
TEventHandler
:Hide Copy Code// ***** Declare an event handling class using the TEventHandler template. ***** typedef TEventHandler<CTestClientDlg, IEventFiringObject, _IEventFiringObjectEvents> IEventFiringObjectEventHandler;
Note that we are not instantiating an object here! We are merely defining a C++ class via the use of
TEventHandler
. We will call this new C++ classIEventFiringObjectEventHandler
.IEventFiringObjectEventHandler
is the customized class that we mentioned earlier. ThisIEventFiringObjectEventHandler
is the Sink for the outgoing interface_IEventFiringObjectEvents
supported by theEventFiringObject
COM object. - In our
CTestClientDlg
class, we will now define a pointer to an instance of theIEventFiringObjectEventHandler
class:Hide Copy Code/* Declare a pointer to a TEventHandler class */ /* which is specially tailored */ /* to receiving events from the _IEventFiringObjectEvents */ /* events of an IEventFiringObject object. */ IEventFiringObjectEventHandler* m_pIEventFiringObjectEventHandler;
- We need to define the invoke method which will be called by the
IEventFiringObjectEventHandler
class object when theEventFiringObject
fires an event based on the_IEventFiringObjectEvents
outgoing interface. We have seen this method before and it is defined as:Hide Copy CodeHRESULT CTestClientDlg::OnEventFiringObjectInvoke ( IEventFiringObjectEventHandler* pEventHandler, DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr );
Please note that the reader must be familiar with the
IDispatch::Invoke()
method in order to be able to interpret the values contained in the various parameters of this method. See the example code inOnEventFiringObjectInvoke()
itself and refer to MSDN documentation for further details. - In the
CTestClientDlg::OnInitDialog()
function, we instantiatem_pIEventFiringObjectEventHandler
:Hide Copy Code/* Instantiate an IEventFiringObjectEventHandler object. */ m_pIEventFiringObjectEventHandler = new IEventFiringObjectEventHandler (*this, m_spIEventFiringObject, &CTestClientDlg::OnEventFiringObjectInvoke );
Here, we instantiate with constructor parameters according to those defined for
TEventHandler
. The first parameter is a reference to theCTestClientDlg
object. The second is the smart pointer objectm_spIEventFiringObject
. This will be cast to anIEventFiringObject
interface pointer and so the inner pointer contained insidem_spIEventFiringObject
will be supplied to the constructor.The last parameter is a pointer to a method of the
CTEstClientDlg
class which conforms to theparent_on_invoke
method signature. You will note in theTEventHandler
constructor that once an instance ofIEventFiringObjectEventHandler
is created, theSetupConnectionPoint()
method is called. This method will duly perform all the required connection point protocols to establish event connectivity with theEventFiringObject
COM object. - When we no longer want to maintain event connection with the
EventFiringObject
COM object, we shutdown the connection point as in theCTestClientDlg::OnDestroy()
method:Hide Copy Codevoid CTestClientDlg::OnDestroy() { CDialog::OnDestroy(); /* When the program is terminating, make sure that we instruct our */ /* Event Handler to disconnect from the connection point of the */ /* object which implemented the IEventFiringObject interface. */ /* We also needs to Release() it (instead of deleting it). */ if (m_pIEventFiringObjectEventHandler) { m_pIEventFiringObjectEventHandler -> ShutdownConnectionPoint(); m_pIEventFiringObjectEventHandler -> Release(); m_pIEventFiringObjectEventHandler = NULL; } }
- To invoke the event handling code, I have included a button in the
CTestClientDlg
dialog box, and the handler to this button will call theEventFiringObject
'sTestFunction()
method which will internally fireEvent1
. This will lead toCTestClientDlg::OnEventFiringObjectInvoke()
being called by theIEventFiringObjectEventHandler
class object:Hide Copy CodeHRESULT CTestClientDlg::OnEventFiringObjectInvoke ( IEventFiringObjectEventHandler* pEventHandler, DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr ) { if (dispidMember == 0x01) // Event1 event. { // 1st param : [in] long lValue. VARIANT varlValue; long lValue = 0; VariantInit(&varlValue); VariantClear(&varlValue); varlValue = (pdispparams -> rgvarg)[0]; lValue = V_I4(&varlValue); TCHAR szMessage[256]; sprintf (szMessage, "Event 1 is fired with value : %d.", lValue); ::MessageBox (NULL, szMessage, "Event", MB_OK); } return S_OK; }
In Conclusion
I certainly do hope you will find the TEventHandler
class useful. C++ templates are really superb in generating generic code. There is already talk of C# providing template features. I really can't wait to get my hands on this. If you have any comments on TEventHandler
, on how to improve it further, please drop me an email anytime.