October 25, 1999
See also: "Dr. GUI and COM Events, Part 1"
Download the sample code for this article (zipped, 89.8K)
Dr. GUI's Bits and Bytes
ATL COM Objects That Fire Events
Give It a Shot
Where We've Been; Where We're Going
The good doctor is back, even though a bit colder. (It was a bit chilly on much of the trip to camp in an unheated vehicle, which forced the good doctor to discover these buildings called motels.)
Remember the John Waters movie Polyester starring the incomparable Divine (rest in peace)? When you went to see the movie, they gave you a numbered scratch-and-sniff card. At various points in the film, a number was flashed on the screen, and you were supposed to scratch the corresponding number on your card to smell what was happening on-screen. Good taste dictates that Dr. GUI not reveal what the scents were.
Soon, if things go as a company called DigiScents plans, you'll be able to have a "Smell-O-Vision" attachment for your PC that will allow you to use your sense of smell to navigate the Web. It will use a set of scented oils to generate smells.
You can check it out on the Web at http://www.zdnet.com/zdnn/stories/news/0,4586,2354314,00.html, or take a look at a recent Wired issue that even includes a scratch-and-sniff cover.
What will they think of next?
As you may have read, the Mars Climate Orbiter was lost recently. What happened?
NASA's investigation revealed that one team working on the orbiter used English units (feet, inches, miles, pounds), while another used metric measurements. Because the units were not translated properly, the spacecraft was put in an incorrect orbit and was lost. You can read more about this at http://mars.jpl.nasa.gov/msp98/news/mco990930.html.
The moral: Be sure the meanings of your parameters are well-defined, including units. If you can use type-checking features of your language to enforce units, that may be well worthwhile.
There are more and more developer centers on MSDN -- and the latest is for Microsoft Exchange developers (http://msdn.microsoft.com/exchange/default.asp). It features information on the upcoming Exchange 2000, including how to get a beta copy. Even if you don't do Exchange development now, check it out every now and then just so you know what you're missing.
You'll recall from last time that events were just a tiny bit complicated.
First off, most event interfaces are dispatch interfaces because it's far simpler for a client to create an object that implements IDispatch and figures out the parameters than it would be to figure out a way to be called via an arbitrary custom interface. The object firing the events -- the source object -- defines the dispatch interface as a source interface and makes it available to the client object via a type library. Note that because events are defined via an interface, there can be more than one event method in the same interface.
A client can check to see whether an object fires events by querying for IConnectionPointContainer. If the object implements IConnectionPointContainer, that means it implements one or more event interfaces. The client can discover what event interfaces the object implements by calling IConnectionPointContainer::EnumConnectionPoints, or the client can ask for a specific connection point by calling IConnectionPointContainer::FindConnectionPoint.
A connection point is a miniature COM object implemented by the source object that's responsible for keeping track of all the connections for that event interface. It normally implements only IUnknown and IConnectionPoint.
Once the client has an interface pointer to one of the source's connection points, it establishes the event interface connection by creating a miniature COM object to receive the events and then passing an interface pointer to that miniature COM object to IConnectionPoint::Advise. The event recipient miniature object normally implements only IUnknown and IDispatch.
After the connection is made, the source object can fire events by calling methods on the interface pointer passed by the client. Since more than one client can call IConnectionPoint::Advise, the source object needs to fire the event for all of them by executing a loop that calls the appropriate method on the interface pointer each client passed to Advise.
When a client no longer wants to receive events, it must call IConnectionPoint::Unadvise to break the connection. It is very important to do this before the client exits.
As you can see, there's some mildly complicated (and bug-prone) code involved here: The connection point object has to maintain a list of connections, and the event firing process involves enumerating that list of connections and calling the appropriate method for each one. Oh -- the good doctor almost forgot -- you also have to make a dispatch interface method call by creating an array of VARIANT parameters and passing it to the many-parametered IDispatch::Invoke method. All in all, there's a lot of work to do. Luckily, ATL is good at this kind of work.
As you've seen from the description of connection points, you have to do a lot to be able to fire events: implement four interfaces (IConnectionPointContainer, IEnumConnectionPoints, IConnectionPoint, and IEnumConnections) in four different objects (the main source object, the connection point enumerator, the connection point, and the connection enumerator, respectively); maintain two collections (connection points and connections); and implement code to call the proper methods on the interface pointers for all of the connections when you fire an event.
Isn't there an easier way? The Microsoft Foundation Classes (MFC) makes it easy -- but at the cost of large objects. Or you can use our hero, Active Template Libraries (ATL), which makes it almost as easy.
Just as ATL provides stock template complete-and-debugged-but-about-as-efficient-as-possible implementations of important interfaces such as IUnknown, IDispatch, and IClassFactory, it also provides the implementations we need to deal with events.
Most of these implementations are provided via templated classes, but the implementation of the event firing loop is in a special "proxy" class that's generated for us by the Implement Connection Point wizard. (This wizard used to be a separate program with a very catchy name -- the ATL Proxy Generator. With a name like that, it's no wonder people pull their hair out when thinking about events.)
First off, you'll need the Interface Definition Language (IDL) code for the source interface. This was described in-depth in the previous article, but the good doctor is showing it to you here for your convenience. Note that you need both the IDL for the source interface itself . . .
[
uuid(F2F660CF-3ED7-11D3-9C8C-000039714C10),
helpstring("_IAAAFireLimitEvents Interface")
]
dispinterface _IAAAFireLimitEvents
{
properties:
methods:
[id(1), helpstring("method Changed")]
HRESULT Changed(IDispatch *Obj,
CURRENCY OldValue);
[id(2), helpstring("method SignChanged")]
HRESULT SignChanged(IDispatch *Obj,
CURRENCY OldValue);
};
. . . and the declaration of the interface as a source interface in the coclass section of the object's IDL (shown below in bold).
coclass AAAFireLimit
{
[default] interface IAAAFireLimit;
[default, source] dispinterface _IAAAFireLimitEvents;
};
If you use ATL's wizards correctly, the above code will be written for you.
ATL provides stock template implementations of IConnectionPointContainer and IConnectionPoint called IConnectionPointContainerImpl and IConnectionPointImpl. We will end up using both of them, but we're going to use IConnectionPointImpl indirectly.
To implement IConnectionPointContainer in our main source object (whose class name is CAAAFireLimit), we do two things.
First, we add
public IConnectionPointContainerImpl<CAAAFireLimit>,
to our inheritance list. The good doctor called the project "AAAFireLimit," so the class name is CAAAFireLimit. The "AAA" at the front is for no good reason except to ensure that this object will end up at the top of Visual Basic's list of object references (Project.References). If we're at the top of the list, it's easier to reference and un-reference the object.
Second, we add an entry to the object's COM map for IConnectionPointContainer:
COM_INTERFACE_ENTRY(IConnectionPointContainer)
The connection point container relies on a connection point map with one entry per connection point (i.e., one entry per source interface). This map goes inside the CAAAFireLimit class declaration (not shown here):
BEGIN_CONNECTION_POINT_MAP(CAAAFireLimit)
CONNECTION_POINT_ENTRY(DIID__IAAAFireLimitEvents)
END_CONNECTION_POINT_MAP()
This simple map tells the implementation of IConnectionPointContainer that it has only one connection point for the interface with the ID DIID__IAAAFireLimitEvents (in other words, the interface IAAAFireLimitEvents). If we implemented more source interfaces, we'd have more CONNECTION_POINT_ENTRY macros.
So IConnectionPoint is solved. How do we get actual connection points and the loops to fire events?
By the way, if you use ATL's object wizard correctly, this code will be added for you automatically. But it's important to know what code was added in case you have to debug events or add the code to an existing object yourself.
If you're guessing that we're also going to inherit from IConnectionPointImpl, you're right. But we're going to do it indirectly, thereby killing our two remaining birds with one stone. Instead of inheriting directly from IConnectionPointImpl, we have Visual Studio create a templated "proxy" class for us that derives from IConnectionPointImpl and implements an additional member function called Fire_[event] per event method. Our interface has two methods, Changed and SignChanged, so our connection point proxy class implements Fire_Changed and Fire_SignChanged.
The implementation of each of these methods contains the loop necessary to make the call for every interface pointer for which IConnectionPoint::Advise was called.
We then inherit from our templated proxy class. This class, including the Fire_ methods, is generated by a program called the Implement Connection Points wizard (formerly known as the ATL Proxy Generator). We'll talk about exactly how to run this wizard later, but for now let's note that it needs a type library as input, so we'll have to be sure to compile the IDL file before we generate the proxy.
The Implement Connection Points wizard also adds the following to CAAAFireLimit's inheritance list:
public CProxy_IAAAFireLimitEvents< CAAAFireLimit >,
CProxy_IAAAFireLimitEvents is the name of the class generated by the wizard. Finally, the Wizard adds a connection point entry to the connection point map:
BEGIN_CONNECTION_POINT_MAP(CAAAFireLimit)
CONNECTION_POINT_ENTRY(DIID__IAAAFireLimitEvents)
END_CONNECTION_POINT_MAP()
If you have more than one connection point (more than one source interface), you'll derive from a proxy class for each. You might think this is odd -- after all, you can't multiply inherit from the same class more than once in the same inheritance list. But it's okay; since the same template with different parameters generates a different class, we're not inheriting from the exact same class multiple times -- each one is slightly (but sufficiently) different.
Here's a listing of this class with one of the Fire_ methods (Fire_SignChanged) omitted. Note that our proxy class derives from IConnectionPointImpl. Look at the code for the Fire_ method -- aren't you glad you don't have to write it?
template <class T>
class CProxy_IAAAFireLimitEvents :
public IConnectionPointImpl<T,
&DIID__IAAAFireLimitEvents, CComDynamicUnkArray>
{
//Warning this class may be recreated by the wizard.
public:
HRESULT Fire_Changed(IDispatch * Obj, CY OldValue)
{
CComVariant varResult;
T* pT = static_cast<T*>(this);
int nConnectionIndex;
CComVariant* pvars = new CComVariant[2];
int nConnections = m_vec.GetSize();
for (nConnectionIndex = 0;
nConnectionIndex < nConnections;
nConnectionIndex++)
{
pT->Lock();
CComPtr<IUnknown> sp =
m_vec.GetAt(nConnectionIndex);
pT->Unlock();
IDispatch* pDispatch =
reinterpret_cast<IDispatch*>(sp.p);
if (pDispatch != NULL)
{
VariantClear(&varResult);
pvars[1] = Obj;
pvars[0] = OldValue;
DISPPARAMS disp = { pvars, NULL, 2, 0 };
pDispatch->Invoke(0x1,
IID_NULL, LOCALE_USER_DEFAULT,
DISPATCH_METHOD, &disp,
&varResult, NULL, NULL);
}
}
delete[] pvars;
return varResult.scode;
}
//... Fire_SignChanged omitted, similar to Fire_Changed
};
You might have noticed that we do not include a COM_INTERFACE_ENTRY for IConnectionPoint. Why?
Well, our main object does not implement IConnectionPoint. Rather, IConnectionPointContainer::FindConnectionPoint returns a pointer to a separate object that implements IUnknown and IConnectionPoint.
The good doctor was imagining that this object would be created by using new during the constructor of IConnectionPointImpl. But the designers of ATL are much more devious (and efficient) than that. Instead, through the miracles of multiple inheritance, templates, and a little bit of nasty (but valid) pointer manipulation, the connection point object (or objects) are built into your main object, along with the data (for the collection of connections) each will need. Each has a separate COM identity, so they're logically separate objects -- just implemented as part of your object. This is perfectly legal because COM doesn't dictate implementation—only behavior. As long as you obey COM's rules, you can implement your objects any way you like.
The neat trick ATL uses is a little much to go into in this column, but you can learn the gory details from ATL Internals (by Rector and Sells, published by Addison-Wesley) on pages 390-396 -- or by grokking ATL's source code yourself.
Last, but not least: It's polite to tell clients how to get to your type library and access your default source interface. The right way to do this is by implementing IProvideClassInfo2 -- which ATL can do for you. Just add this to your main class's inheritance list:
public IProvideClassInfo2Impl<&CLSID_AAAFireLimit,
&DIID__IAAAFireLimitEvents,
&LIBID_AAAFIRELIMITMODLib,
LIBRARY_MAJOR, LIBRARY_MINOR>
You'll have to define the two macros, LIBRARY_MAJOR and LIBRARY_MINOR, to correspond to the version number of your type library. Do this near the beginning of your header file. For now, I've defined these as follows. You'll update them as you release new versions of your object.
// version number of type library
#define LIBRARY_MAJOR 1
#define LIBRARY_MINOR 0
Don't forget to add your entries to your COM map. Since your object will implement IProvideClassInfo as well as IProvideClassInfo2, you'll need to add two entries:
COM_INTERFACE_ENTRY(IProvideClassInfo2)
COM_INTERFACE_ENTRY(IProvideClassInfo)
ATL does the rest for you.
The good doctor mentioned before that if you create a new thread, you cannot fire events from it. Although it's tempting to simply call Fire_Changed from the new thread, this is dead wrong.
The problem is that the thread that stores the event interface pointer (when it calls IConnectionPoint::Advise) isn't the same thread as the thread using the stored pointer (when it calls Fire_Changed). The code in Fire_Changed is using the stored interface pointer directly -- the code is:
CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);
This violates COM's rule that you can't pass an unmarshaled (raw) interface pointer across threads. This is easy to ignore because the interface pointers are pretty well hidden in ATL and the Fire_ method. But you still have to do it right. After all, you have no way of knowing whether your client is capable of handling calls on different threads -- i.e., whether the client is capable of multi-threaded operation. Many aren't.
To fix this, you'll have to marshal the interface pointer in IConnectionPoint::Advise (while you're running on the original thread) and unmarshal it in Fire_Changed (while you're running on the new thread). Don't forget to call CoInitializeEx from the new thread; this is necessary for all threads that use COM.
This is a little too complicated to show in code this time, but those of you who need a solution can work one up from the information here (and some additional COM smarts). The good doctor is hoping to tackle this in a future column.
Although ATL's implementations do a lot for you, the descriptions above may seem a tad daunting. Fear not: ATL has some nice wizards and checkboxes that will help you implement most of the code above. About all you have to do is define the source interface and fire the events at the right times.
The object we're creating is very simple: It contains a value that you can get, set, and add to. An event is fired when the value changes and when the value changes sign.
As for all COM objects, we first create (if we don't have one) a module to contain our object. There are no special things to do in this step; choose whatever options in the ATL COM AppWizard that are appropriate for your project. Dr. GUI starts his name with "AAA" (so the object will alphabetize early) and ends it with "Mod" (so the module is distinct from the object).
Once you've entered a name, just choose all the default options. For directions on this, see Dr. GUI's first article on ATL objects.
You do this the usual way: Use Insert.New ATL Object (on the Insert menu) and pick Simple Object. Then enter the name (same name as in the previous step less the "Mod" suffix). DON'T click OK. You must set attributes for this object, so click on the Attributes tab and check the Support Connection Points box. Before you click OK, your dialog box should look like this:
Figure 1. ATL Object Wizard with Support Connection Points checked
Be sure to check Support Connection Points.
Note For some reason, Dr. GUI's installation of Visual Studio® was missing the Insert.New ATL Object command. Installing Service Pack 3 fixed the problem. Try installing the Service Pack if you run into a similar problem.
As you look through the code generated by the ATL Object Wizard, you'll find some code you've not seen before:
The next step is to use the usual Visual Studio mechanisms (right-click on the source interface name in Class View, then select Add Method) to add our two methods. You can see the IDL for those two methods above.
The Changed method has the following prototype:
HRESULT Changed(IDispatch *Obj, CURRENCY OldValue);
Note that we're passing an interface pointer to our object as well as the old value when the value changes. Passing our interface pointer allows our client to access all of our methods and properties -- pretty powerful stuff.
Note also that we used the COM CURRENCY type, as described in Dr. GUI's column on COM automation data types.
The SignChanged method has a similar prototype -- see the IDL code (above).
Once you've got the methods declared, you have to compile the IDL file. The quickest way is to switch to File View, find and right-click on the IDL file, and select Compile. (If you want to waste some time, you can build the entire project.)
Once you've got the IDL compiled, switch back to Class View. Then right-click on your object's class name (in our case, CAAAFireLimit) and select Implement Connection Point. If you compiled your type library properly, you'll get a dialog like the following:
Figure 2. Implement Connection Point box with event interface(s) checked
Be sure to check the interface(s) for which you want to implement connection points. (By the way, it's this wizard that's an updated version of the previous ATL Proxy Generator.)
This will make the following changes to your code:
Since we're inheriting from this class, we can call its Fire_ methods easily. Note that if we make any changes to the event interface, we'll have to do this whole step (recompile the IDL and implement the connection point) again. That will regenerate the proxy class from scratch, so don't modify the code in that file.
It's polite to provide an implementation of IClassInfo and IClassInfo2. We'll do this by adding the code described in the corresponding section above.
Finally, now that we're ready to fire events, let's add the usual properties and methods.
Recall that the idea of this object is simple -- it'll hold a currency value and fire events when the value changes and when the value changes sign. So we'll have a property for the value (and we'll make that property the default by setting its dispatch ID to 0). We'll also add an Add method to allow adding a number to the object. The IDL for our object's interface looks like this:
interface IAAAFireLimit : IDispatch
{
[propget, id(0), helpstring("property Value")]
HRESULT Value([out, retval] CURRENCY *pVal);
[propput, id(0), helpstring("property Value")]
HRESULT Value([in] CURRENCY newVal);
[id(1), helpstring("method Add")]
HRESULT Add(CURRENCY cyAddend);
};
The implementations of the methods are almost exactly as you'd expect, with the exception that any method that might change the value of the object calls a helper method, CheckAndFire, which fires any necessary events.
STDMETHODIMP CAAAFireLimit::get_Value(CURRENCY *pVal)
{
*pVal = m_cyValue; // can't change value
return S_OK;
}
STDMETHODIMP CAAAFireLimit::put_Value(CURRENCY newVal)
{
CURRENCY oldVal = m_cyValue;
m_cyValue = newVal;
CheckAndFire(oldVal);
return S_OK;
}
STDMETHODIMP CAAAFireLimit::Add(CURRENCY cyAddend)
{
CURRENCY cyOld = m_cyValue;
VarCyAdd(m_cyValue, cyAddend, &m_cyValue);
CheckAndFire(cyOld);
return S_OK;
}
The data member, m_cyValue, is a class member of type CURRENCY. Note that we used COM's VarCyAdd function to perform the addition, as described in the last column on automation data types.
We are also keeping the old value around so we can check to see if we need to fire an event. (That means that you can set the value to the existing value without firing an event -- so Changed really means changed.)
The CheckAndFire method is a little complicated. The case for the Changed event is simple enough, but the case for the SignChanged event is a little trickier, because we don't want to fire an event unless the value goes from positive to negative or vice versa -- going to zero doesn't count. On the other hand, we do want to fire an event if the number goes from positive to zero to negative, or vice versa. The logic here handles those cases nicely, although it takes another instance variable (hrOldSign, initialized to VARCMP_EQ in the constructor) to track the sign.
Note that we're using COM's VarCyCmp function to do the comparison. We also created a global variable, cyZero, which is initialized to zero in the proper format:
CURRENCY cyZero = { 0i64 };
Here's the CheckAndFire method:
void CAAAFireLimit::CheckAndFire(CURRENCY cyOld)
{
// Fire event if value changed
HRESULT hrCmpRes = VarCyCmp(m_cyValue, cyOld);
if (hrCmpRes != VARCMP_EQ)
Fire_Changed(this, cyOld);
// Fire event if sign changed
HRESULT hrCmpZero = VarCyCmp(m_cyValue, cyZero);
if (hrCmpZero != VARCMP_EQ) { // not equal to zero
if (hrCmpZero != hrOldSign && hrOldSign != VARCMP_EQ) {
Fire_SignChanged(this, cyOld);
}
hrOldSign = hrCmpZero;
}
}
When we fire an event, we pass the this pointer (so the client can call our methods) as well as the old value (for the client's information).
Lastly, in addition to initializing hrOldSign in the constructor, we also initialize m_cyValue to cyZero.
That's about it. Now all you have to do is build (and debug) your object and run it in a client that can respond to events. We'll talk about clients that receive events next time.
If you're adding events to an existing object, be sure to add all the code that would have been added had you checked the Support Connection Points checkbox. That's the code described in Step two (above). You can still use the wizard to generate your proxy class -- the good doctor recommends this to avoid the pain of generating it yourself.
Dr. GUI knows full well that you won't know whether you really know what we've talked about until you try it -- so give it a shot!
Write your own COM object that fires events as a result of a method call. (Don't start a thread and fire an event from it unless you understand how to marshal interface pointers across threads.) If you know how to write a visual ATL control, you can fire an event when something happens to it (for instance, someone clicks on the control).
Then write a Visual Basic client to test it -- or use a Web page and script. There's a Visual Basic client provided in the sample code.
This time, we talked about how to add events to an ATL COM object. There are a number of steps necessary to add events, but the more complicated ones are made easier by wizards and proxy generators.
Next time, we'll discuss how to receive events in a Visual Basic application and an ATL COM object.