Introduction
This tutorial is an extension to my article on building VC clients for VB ActiveX DLLs. Having read such an article, the question about handling events naturally comes to the reader's mind.
Here, I'm going to show you how to handle custom events generated in VB ActiveX components in a Visual C++ client. First, we are going to build an MFC client and then turn our attention to creating an ATL client. Doing all of this is not tough at all, as you shall see. A framework like MFC makes it very easy for the programmer to receive event notifications from an ActiveX code component. A word before we move on, I assume that the reader is conversant with VB ActiveX technology, Automation and MFC COM and IDL (Interface Definition Language).
VB ActiveX Components
A VB ActiveX component is a unit of code that follows the COM specification for providing objects. It exposes much of its functionality through one or more interfaces. These software components have a high reusability factor.
As the component needs to communicate with the client, they can be implemented as an in-process (read DLL) component and an out-process (read EXE) component.
In an in-process component (ActiveX DLL components), the communication between the server and the client is implemented in the address space of the client application. Though this makes them faster than ActiveX EXE components, who need to be loaded in their own address space, the biggest drawback is that a faulty DLL will crash the client, and in turn, the object. That tends to bring everybody down.:-)
VB Events
An event can be simply defined as something that occurs during the application's lifetime. Events allow a class to communicate to other classes and programs. A component raises an event to notify the client about the completion of some task. This event can be caught by the client, and the client can respond to the event as it sees fit.
Custom events provide event-handling capabilities to classes and components. The object that generates the event is known as the event source and the object that responds to an event is known as the event sink. First, we are going to fire a custom event from a VB ActiveX DLL and handle the notification in an MFC client. In other words, we have to build an event sink in an MFC client that responds to events generated by a VB ActiveX component, which acts as the event source. The code in the event sink is executed when the event is fired.
To declare a custom event called evtTaskDone
in VB, use the Event
keyword in the General Declarations section of a class module like:
Public Event evtTaskDone()
This custom event can then be fired by using the RaiseEvent
keyword like:
RaiseEvent evtTaskDone
With all that in mind, we now roll up our sleeves and dig into some code. First, we are going to build a VB ActiveX DLL that is the source of the event.
Building the Event Source
Fire up VB and in the New Project dialog, choose ActiveX DLL and click Open. VB creates a new DLL component project called Project1
, having a single class Class1
. Go to Project->Properties and set the Project Name as VBEvents
. In the Project Explorer View, right-click on Class1
and choose to remove it from the project.
Note: We could also have chosen to use this class, but then we wouldn't have seen the VB Class Builder.
Now right-click again on the Project Explorer View and add a single Class Module to the project. In the Add Class Module dialog, choose VB Class Builder and click on Open.
In Class Builder, go to File->New->Class and add a new class called clsEventSrc
Accept the default values and click on OK. Next, go to File->New->Event and add a single event to this class called evtNotify
. Update all the changes to the project and close the Class Builder window.
Next, click on Tools->Add Procedure and add a new procedure to the clsEventSrc
class called prcFireEvent
to the class you just created like:
Public Sub prcFireEvent()
RaiseEvent evtNotify
End Sub
The procedure simply fires our event. Save everything and go to File->Make to build VBEvents.dll and register the component.
Building the MFC Client
Our MFC client is a plain old Appwizard generated Dialog-based application with additional Automation support. As usual, open VC++ 6.0 and create a new MFC Appwizard
EXE project called MFCClient
. Hit Build to build the project, and take a break from all that hard work!
The OLE/COM Object Viewer is a nifty little tool that is shipped along with Visual C++ 6.0. It will help us generate the IDL file for the DLL component. Go to Tools->OLE/COM Object Viewer and open this tool. Next, in OLE/COM Object Viewer, click on File->View Typelib and navigate to the VBEvent.dll file that we have previously built. Ready for some magic? Click on Open and open up ITypeLib Viewer. Can you can view the IDL file? Whoa! Save the file through File->Save As, as VBEvents.IDL and close the tool. We have no need for it at present.
Next in our VC++ project, add this IDL file to the project. In FileView
, right click on the IDL file and choose Settings. In the MIDL tab, set the Output header file name to VBEvents.h and the UUID filename to VBEvents_i.c. Also deselect the MkTyplib compatible option.
Save everything and in FileView
, right-click on the VBEvents.IDL file and choose Compile. This will build the typelibrary
and generate the necessary files.
Examine the MIDL generated VBEvents_i.c file. It contains all the UUID definitions that the client can use to build a sink object. In VBEvents.h, notice the dual interface _clsEventSrc
. The component's dispinterface
__clsEventSrc
is identified by DIID___clsEvent
. This is the event source for out custom event.
The next step is to add an sink object to connect to the source event. Fortunately, for us, MFC makes the task of building an event sink as easy as 1-2-3 (and well,4-5-6) With a couple of MFC macros, you can delegate much of the intricacy involved behind building a sink to MFC. First, add a new CCmdTarget
derived class to the project called MFCSink
. In the ClassWizard
, choose to select the Automation option. This is our sink object with Automation support.
Then import the server's typelib
in the client with #import
. If you haven't read my previous article, read it here. Otherwise, go right ahead and use code like:
#import "VBEvents.dll" rename_namespace("MFCClient")
using namespace MFCClient;
There's nothing new to this code. While you are there in stdafx.h, also #include
the afxctl.h file
Next, open MFCSink.cpp and modify the INTERFACE_PART
macro so that the second parameter (IID
) is the IID of the event source, in our case DIID___clsEventSrc
. Your interface map should look like:
BEGIN_INTERFACE_MAP(MFCSink, CCmdTarget)
INTERFACE_PART(MFCSink, DIID___clsEventSrc, Dispatch)
END_INTERFACE_MAP()
Next, in the DISPATCH
Map of the MFCSink
class, add a DISP_FUNCTION_ID
macro for each of the events defined in the source interface that you want to handle. My DISPATCH
Map looks like:
BEGIN_DISPATCH_MAP(MFCSink, CCmdTarget)
DISP_FUNCTION_ID(MFCSink, "evtNotify",1,evtNotify, VT_EMPTY, VTS_NONE)
END_DISPATCH_MAP()
In Classview
, right-click on the IMFCSink
interface and add a single method evtNotify()
. Notice, that as per our DISPATCH
Map, this method takes no parameters and returns a void
. Our implementation of this method displays a simple MessageBox
and looks like:
void MFCSink::evtNotify()
{
AfxMessageBox("Event notification handled in MFC client");
}
All that remains for us to do is hook up and terminate the connection appropriately in the client code. MFC makes this job very easy with AfxConnectionAdvise()
and correspondingly AfxConnectionUnadvise()
. If you are not familiar with these functions, now would be a good time to look up their documentation.
Moving on, declare three variables in the dialog class header as:
_clsEventSrc *m_pSrc;
MFCSink *m_pSink;
DWORD m_dwCookie;
The first is a pointer to the interface through which we shall fire the event. The second is a pointer to the sink object. Lastly, the m_dwCookie
variable is a cookie that stores the number of connections that has been established. We'll need this when we want to disconnect from the event source. In our case, we set this to 1
in the dialog class constructor. Don't forget to #include
the VBEvents_i.c file. The code to establish the connection in the dialog's OnInitDialog()
member function looks like:
CoInitialize(NULL);
m_pSink=new MFCSink;
HRESULT hr=CoCreateInstance(CLSID_clsEventSrc,NULL,CLSCTX_INPROC_SERVER,IID_IDispatch,(void**) &m_pSrc);
if(SUCCEEDED(hr))
LPUNKNOWN m_pUnk=m_pSink->GetIDispatch(FALSE);
if(SUCCEEDED(hr))
{
if (AfxConnectionAdvise(m_pSrc,DIID___clsEventSrc,m_pUnk,FALSE,&m_dwCookie))
return TRUE;
else
return FALSE;
}
else
return FALSE;
As we have setup a connection, we also need to disconnect when the dialog is destroyed. We can do that in the dialog's OnDestroy()
. In ClassView
, right-click the dialog class and Add a Windows message handler to handle WM_DESTROY
messages. In the handler, add the following code to successfully disconnect.
LPUNKNOWN m_pUnk=m_pSink->GetIDispatch(FALSE);
AfxConnectionUnadvise(m_pSrc,DIID___clsEventSrc,m_pUnk,FALSE,m_dwCookie);
if(m_pSink!=NULL)
{
delete m_pSink;
m_pSink=NULL;
m_pSrc=NULL;
}
With everything in place, we now need to fire the event. Simply call:
m_pSrc->prcFireEvent();
anywhere in your code where you want to fire the event.
ATL Client
Building a pure ATL client means a little more typing than the MFC client. But it's a lot easier than creating connectable objects in raw C++. Remember that the sink has to support IDispatch
. So that means at a minimum, implementing 7 methods (3 for IUnknown
and 4 for IDispatch
). To our relief, ATL provides the IDispEventSimpleImpl<>
and IDispEventImpl<>
template classes that help us in quickly creating dispinterface
sink objects. These is a host of information and code available for creating ATL sinks for dispinterface
based source objects that you might want to lookup. Relevant Microsoft KB Articles Q:181277, Q:181845 and Q:194179
Back to the task at hand, to make our client very efficient, we'll use an IDispEventSimpleImpl
derived class. First, create a new ATL/COM AppWizard
generated EXE project called ATLClient
. To this, add a dialog called ATLClientDlg
. The dialog has two buttons, one to setup the connection and the other to fire the event. Next, import the server's typelib
with #import
as described in the MFC client section above. Moving on to the sink object, the declaration looks like:
#define IDC_SRCOBJ 1
static _ATL_FUNC_INFO OnEventInfo = {CC_STDCALL, VT_EMPTY, VT_NULL};
class CSinkObj : public IDispEventSimpleImpl<IDC_SRCOBJ, CSinkObj, &__uuidof(__clsEventSrc)>
{
public:
HWND m_hWndList;
CSinkObj(HWND hWnd = NULL) : m_hWndList(hWnd)
{
}
BEGIN_SINK_MAP(CSinkObj)
SINK_ENTRY_INFO(IDC_SRCOBJ, __uuidof(__clsEventSrc), 1, evtNotify, &OnEventInfo)
END_SINK_MAP()
HRESULT __stdcall evtNotify()
{
TCHAR buf[80];
wsprintf(buf, "Sink : Notification Event Received");
AtlTrace("\n%s",buf);
return S_OK;
}
};
All I have done is add a sink map to the IDispEventSimpleImpl
-derived class and then add a sink entry corresponding to each event of a source interface that I would like to handle. The ATL_FUNC_INFO
structure helps us pass parameters to event handlers. In our event handler however, we do nothing fancy. Just a simple debug message would do.
In the dialog class, add variables:
private:
CSinkObj* m_pSink;
_clsEventSrc *pEvent;
The dialog class's OnConnect()
looks like:
LRESULT OnConnect(UINT,WORD,HWND hWndCtrl,BOOL& bHandled)
{
m_pSink=new CSinkObj(hWndCtrl);
HRESULT hr=CoCreateInstance(CLSID_clsEventSrc,NULL,CLSCTX_INPROC_SERVER,
__uuidof(_clsEventSrc),(void**)&pEvent);
if(SUCCEEDED(hr))
{
m_pSink->DispEventAdvise(pEvent);
}
return hr;
}
As before, call:
pEvent->prcFireEvent();
when you want to fire the event. Don't forget to use DispEventUnadvise()
to disconnect when the dialog is destroyed.
That's it! We have built both an MFC and an ATL client that responds to events generated by a VB ActiveX DLL code component. The code and project files were built with Visual C++ 6.0 SP3 under Win95.
I have included another project VBTimer
consisting of a VB ActiveX DLL and respective ATL client project files. This project does something a little more sophisticated than our first VB DLL, which fires an event without any parameters. The ActiveX DLL implements a VB Timer that fires an event with an single parameter (timer count) after every 1 second interval. This event is caught by the ATL client that displays the timer count in the output window.
References
- NIIT Technical Reference
- Microsoft KB Articles Q181845,Q181277 and Q194179
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.