Robert Coleridge
MSDN Content Development Group
March 26, 1997
Click to copy the files for the ATLEVNTS sample discussed in this technical article
This article demonstrates how to create an Active Template Library (ATL) Component Object Model (COM) server that triggers events in a Microsoft® Visual Basic® sink object. Although this may sound like a straightforward COM coding task, there are several idiosyncrasies that must be taken into account. These idiosyncrasies will be demonstrated by writing a simple ATL COM server that reads a Uniform Resource Locator (URL) that has been requested by a Visual Basic client. Once the URL is read, the server raises an event that is picked up by the Visual Basic client.
Most of us who have used Visual Basic are familiar with the terms VBX and OCX. These are generally third-party controls that, when added to a project, give it increased functionality. The ATL COM server that this article will help you to build functions just like a VBX or OCX control.
In order to compile and run the ATLEVNTS sample, you must be running Microsoft Visual C++® version 5.0 and Visual Basic version 5.0. These tools are available either separately or as part of the Microsoft Visual Studio™ 97 package.
Creating a project and an object with the new Visual C++ 5.0 ATL COM Wizard is a very straightforward process, as described in the following steps.
That is all it takes to create a generic ATL COM Server. For further details, see the Visual C++ 5.0 documentation. Now let us examine how to add events to make the object work with a Visual Basic sink.
In order to generate the event interface, we must first generate a dispinterface for the event object. This is because Visual Basic does not generate vtables for its event sinks. The events must be fired through an IDispatch object. Therefore, source and interface attributes are not allowed because ATL assumes that a vtable is supported.
Let's generate an interface for our event methods. First, generate a GUID for the interface with the GuidGen.exe utility. Then, add the event methods to the .idl file, as follows:
[
object,
dual,
uuid(723B5B11-9EEF-11d0-BD88-00A0C90F282F),
helpstring("IUrlReaderEvent Interface"),
version(1.0)
]
interface IUrlReaderEvents : IDispatch {
HRESULT ReadUrlFinished(long lFlags);
HRESULT ReadUrlError(long lErrorCode);
};
Note that since events are in essence subroutines, they must be defined as HRESULT methods that do not return any value.
Once this is done, we need to generate the dispinterface for the .idl file as well. Use the GuidGen.exe utility to generate another GUID, and then place the dispinterface definition inside the library definition, as follows:
[
uuid(511A66F3-9EEE-11D0-BD88-00A0C90F282F),
version(1.0),
helpstring("ReadUrl 1.0 Type Library")
]
library READURLLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(723B5B10-9EEF-11d0-BD88-00A0C90F282F),
nonextensible,
helpstring("DIUrlReaderEvents Interface")
]
dispinterface DIUrlReaderEvents
{
properties:
methods:
[id(1)] void ReadUrlFinished(long lFlags);
[id(2)] void ReadUrlError(long lErrorCode);
};
. . .
};
Note that the event methods use a void return. This is due to the fact, as stated above, that the events are treated as subroutines and do not return values. The difference in syntax is due to the different requirements of an interface object and a dispinterface object.
The final step in creating the event interfaces is to modify the coclass definition so that the interfaces are exposed and COM knows which interface is the source of events. The sample .idl file looks like the following:
[
uuid(511A66F3-9EEE-11D0-BD88-00A0C90F282F),
version(1.0),
helpstring("ReadUrl 1.0 Type Library")
]
library READURLLib
{
. . .
[
uuid(511A6703-9EEE-11D0-BD88-00A0C90F282F),
helpstring("UrlReader Class")
]
coclass UrlReader
{
[default] interface IUrlReader;
interface IUrlReaderEvents;
[default, source] dispinterface DIUrlReaderEvents;
};
};
The "interface IUrlReaderEvents;" line informs COM of the event interface and the "[default, source] dispinterface DIUrlReaderEvents;" line informs COM which interface is the source of events. This source must be the dispinterface in order to work with a Visual Basic sink.
The ATL Proxy Generator (ATLPG) is an add-in with Visual C++ 5.0 that generates the code necessary for the proxy stub. This code is also the code we will use to cause the firing of the Visual Basic event.
To have the ATLPG generate the event code, do the following:
That is all it takes to have the ATLPG generate the event code. The ATLPG will examine the type library, find our events, and generate code titled Fire_xxxx, where xxxx is the event we defined in the .idl file. To fire a specified event, we simply invoke one of these Fire_xxxx functions.
The last step we need to take with the ATLPG-generated code is to add an #include line in the object implementation file. The above "walk-through" generated a file called cprdurl.h. Our object is the UrlReader, so we need to add the following line to the urlrdr.h file:
#include "cprdurl.h"
This is the next-to-last step in modifying the generated ATL code. What is necessary here is to modify the inheritance list for the object that fires events. The generated intermittence list for the CUrlReader class looks like the following:
class ATL_NO_VTABLE CUrlReader :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CUrlReader, &CLSID_UrlReader>,
public ISupportErrorInfo,
public IConnectionPointContainerImpl<CUrlReader>,
public IDispatchImpl<IUrlReader, &IID_IUrlReader, &LIBID_READURLLib>
{
Note that this list does not contain anything we don't need. The two CComxxxx templates are used to derive the base COM object and the base object for our CUrlReader class. The ISupportErrorInfo class and IConnectionPointContainerImpl template are simply a result of our selecting the Supports Connection Points and the Support ISupportErrorInfo options. The IDispatchImpl template is required because we selected a dual interface object. We need to add the template for the ATLPG-generated template, like so:
class ATL_NO_VTABLE CUrlReader :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CUrlReader, &CLSID_UrlReader>,
public ISupportErrorInfo,
public IConnectionPointContainerImpl<CUrlReader>,
public CProxyDIUrlReaderEvents<CUrlReader>,
public IDispatchImpl<IUrlReader, &IID_IUrlReader, &LIBID_READURLLib>
{
Once this template is added to the inheritance list, we need to add an entry to the connection point map. This is done by making the following change to the connection point map:
BEGIN_CONNECTION_POINT_MAP(CUrlReader)
CONNECTION_POINT_ENTRY(DIID_DIUrlReaderEvents)
END_CONNECTION_POINT_MAP()
The final urlrdr.h file should look something like the following (our additions are in bold face):
// urlrdr.h : Declaration of the CUrlReader
. . .
#include "resource.h" // main symbols
#include "cprdurl.h"
#include <ComDef.h> // for _bstr_t class
/////////////////////////////////////////////////////////////////////////////
// CUrlReader
class ATL_NO_VTABLE CUrlReader :
. . .
public CProxyDIUrlReaderEvents<CUrlReader>,
. . .
{
. . .
BEGIN_CONNECTION_POINT_MAP(CUrlReader)
CONNECTION_POINT_ENTRY(DIID_DIUrlReaderEvents)
END_CONNECTION_POINT_MAP()
. . .
#endif //__URLREADER_H_
That's it! We now have a fully functional ATL COM Server that can fire events to a Visual Basic sink object. All that is left is to fire the event whenever it is logically necessary. In the ATLEVNTS sample, the firing code is launched on a separate thread to allow asynchronous processing. The thread creation code looks like the following:
// Create URL reading thread, passing in address of the thread code.
// The "this" pointer is passed in so that the thread code has
// access to the calling object.
m_hThread = ::CreateThread(
NULL,
0,
(LPTHREAD_START_ROUTINE)&CUrlReader::ProcessUrl,
this,
0,
&m_dwThreadId);
The following sample code fires the event:
DWORD WINAPI CUrlReader::ProcessUrl(void * pParam)
{
// The "pParam" is a "this" pointer to the calling object,
// so cast appropriately.
CUrlReader * pCaller = (CUrlReader *)pParam;
...
// Raise event notification
pCaller->Fire_ReadUrlFinished(0);
return 0;
}
The following list reiterates the 16 steps required to generate an ATL object that fires events to a Visual Basic sink object.
The sample Visual Basic client is quite simple. All it does is provide two editboxes—one to enter the desired URL and the other for the name of the output file. Once the user enters the desired URL and the output file specification and clicks the Read URL button, the client will request the HTML page from the COM server and then immediately return to the client application. (Since this is a multithreaded application, the client can do further processing until the event notification is fired from the server.)
The COM server will establish a link to the web and attempt to read the specified URL. In order to allow the Visual Basic code to return immediately, the COM server creates a separate thread to do the Web work. If the Web work fails, then the COM server fires the UrlReader_ReadUrlError event. If the URL is read successfully, then the UrlReader_ReadUrlFinished event is fired.
For a technical discussion of the Visual Basic client and its event syntax, please refer to the Visual Basic documentation. See entries on WithEvents and classes.
As you can see from this article, adding the requisite components to an ATL object is not difficult at all. It is simply a series of sequential steps. With this knowledge in mind, you can now combine the power of ATL, the Visual C++ 5.0 wizardry, and what you've learned from this article to create a multitude of useful objects.