您的位置:首页 > 其它

OLE技术专题——COM连接点事件(下)

2005-09-02 23:53 411 查看

Understanding COM Event Handling
By Lim Bio Liong

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 Client that 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, it invokes it), the COM object exposes a small object called a connection 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 the
IConnectionPoint::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.
The
IConnectionPointContainer
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 behind
ISomeInterface
is assumed to be a Connection Point Container. The following is an outline of the function's logic:
We first
QueryInterface()
the
Sink
object itself for its
IUnknown
interface pointer. This
IUnknown
pointer will be used later in the call to
IConnectionPoint::Advise()
.
Having successfully obtained the
IUnknown
pointer, we next
QueryInterface()
the object behind
pISomeInterface
for its
IConnectionPointContainer
interface.
Having successfully obtained the
IConnectionPointContainer
interface pointer, we use it to find the appropriate Connection Point object for the outgoing interface
ISomeEventInterface
.
If we are able to obtain this Connection Point object (it will be represented by
m_pIConnectionPoint
), we will proceed to call its
Advise()
method.
From here onwards, whenever the COM object behind
pISomeInterface
fires an event to the sink (by calling one of the methods of
ISomeEventInterface
), the corresponding method implementation in the
Sink
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 Codes

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 named
IEventFiringObject
. 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 the
EventFiringObject
COM object. It also attempts to handle the
Event1
event fired from
EventFiringObject
. 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
. In TestClientDlg.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 how
TEventHandler
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 could
TEventHandler
have if your callback function still has to handle all the parameters of the
IDispatch::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 mirror
Invoke()
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 from
ISomeEventInterface
and it must implement the methods of that interface. I have chosen to make
TEventHandler
derive from
IDispatch
, thereby making it a Sink for an event interface that also derives from
IDispatch
.
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 binding which 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 that
TEventHandler
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 is
TEventHandler
) 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 of
CTestClientDlg
,
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
:
// ***** 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++ class
IEventFiringObjectEventHandler
.
IEventFiringObjectEventHandler
is the customized class that we mentioned earlier.
This
IEventFiringObjectEventHandler
is the Sink for the outgoing interface
_IEventFiringObjectEvents
supported by the
EventFiringObject
COM object.

In our
CTestClientDlg
class, we will now define a pointer to an instance of the
IEventFiringObjectEventHandler
class:
/* 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 the
EventFiringObject
fires an event based on the
_IEventFiringObjectEvents
outgoing interface. We have seen this method before and it is defined as:
 HRESULT 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 in
OnEventFiringObjectInvoke()
itself and refer to MSDN documentation for further details.

In the
CTestClientDlg::OnInitDialog()
function, we instantiate
m_pIEventFiringObjectEventHandler
:
/* 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 the
CTestClientDlg
object. The second is the smart pointer object
m_spIEventFiringObject
. This will be cast to an
IEventFiringObject
interface pointer and so the inner pointer contained inside
m_spIEventFiringObject
will be supplied to the constructor.
The last parameter is a pointer to a method of the
CTEstClientDlg
class which conforms to the
parent_on_invoke
method signature.
You will note in the
TEventHandler
constructor that once an instance of
IEventFiringObjectEventHandler
is created, the
SetupConnectionPoint()
method is called. This method will duly perform all the required connection point protocols to establish event connectivity with the
EventFiringObject
COM object.

When we no longer want to maintain event connection with the
EventFiringObject
COM object, we shutdown the connection point as in the
CTestClientDlg::OnDestroy()
method:
void 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 the
EventFiringObject
's
TestFunction()
method which will internally fire
Event1
. This will lead to
CTestClientDlg::OnEventFiringObjectInvoke()
being called by the
IEventFiringObjectEventHandler
class object:
HRESULT 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.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: