Events: From an ATL Server to a Visual Basic Sink

Robert Coleridge
MSDN Content Development Group

March 26, 1997

Click to copy the files for the ATLEVNTS sample discussed in this technical article

Introduction

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 the Project and Objects

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.

  1. To create a new project, select New from the File menu.

  2. Select ATL COM Wizard, fill in the Project name and, if necessary, the Location and Platforms text boxes, and click OK.

  3. Select the Server Type and click Finish.

  4. From the Workspace window, right-click on the Classes entry you just created, and then select New ATL Object. Select Simple Object and click Next.

  5. After filling in the class and object information, click the Attributes tab and select the Support Connection Points and Support ISupportErrorInfo options.

  6. Add methods or properties to the created object.

  7. Voilà! You have just created a complete ATL COM Server.

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.

Creating the Event Interfaces

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

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:

  1. On the Projects menu, point to Add To Project, and then click Components and Controls.

  2. From the list of available components, double-click Developer Studio Components, select ATL Proxy Generator, and then click Insert.

  3. The ATL Proxy generator requires the name of the type library containing the objects. You simply need to click on the button marked " . . . ", select our generated type library (you may have to browse for it), and click Open.

  4. You will then see a list of defined interfaces in the type library. One of them will be the dual interface we defined in our .idl file. Select the DIUrlReaderEvents interface. Set the proxy type to Connection Points, and then click Insert. Click Save and close all remaining dialog boxes.

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"

Altering the Templates and Macros

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;
}

Tying It All Together

The following list reiterates the 16 steps required to generate an ATL object that fires events to a Visual Basic sink object.

  1. Create an ATL COM Wizard project.

  2. Create an ATL object with support for connection points.

  3. Add methods and properties to the object(s).

  4. Run GuidGen.exe to get a GUID for the dispinterface event object.

  5. Add the dispinterface definition inside the library definition.

  6. Add void event methods to this interface.

  7. Run GuidGen.exe to get a GUID for the interface event object.

  8. Add the event interface.

  9. Add HRESULT event methods to this interface.

  10. Add dispinterface and interface to coclass.

  11. Add forward references to the top of the .idl file.

  12. Generate a typelib with the ATL Proxy Generator.

  13. Add #include for the generated file to the object .h file.

  14. Modify the class inheritance list.

  15. Add connection point entries to the connection point map.

  16. Add event firing to the appropriate code.

UrlReader: The Sample Code

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.

Conclusion

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.