August 23, 1999
The Doc Is Back…
Dr. GUI's Bits and Bytes
Where We've Been, Where We're Going
Next Time: ATL Makes Events Easier!
Where We've Been, Where We're Going
After a rainy June, summer has finally arrived in Seattle. But summertime makes it oh-so-hard to get work done. That, combined with the more serious delays caused by preparing for, going to, and recovering from Tech Ed (see Dr. GUI about three-fourths of the way through Paul Maritz's keynote speech at http://events.microsoft.com/isapi/events/pages.asp?s=49100&p=612) and being pulled away for an emergency project, caused this column to be, well, uh, late.
However, the most serious delay was that Dr. GUI wanted to use a multi-threaded design for the demo program in this column. Now, COM is perfectly capable of dealing with threads, as is Dr. GUI -- but it takes considerable extra work -- more than enough to confuse the basic issues about events.
Dr. GUI thought he had a single-threaded timer object working using the Windows timer SetTimer API. But it had some problems, when the client shut down before the timer expired, that could not satisfactorily be resolved. And that approach was also beginning to overwhelm the concepts of events with complications of messages and timers and queues (oh, my!).
Instead, we have a simpler object that does a fine job of showing you what events can do. It even demonstrates a powerful programming technique you can use to reduce the amount of redundant code you write. And yes, Dr. GUI intends to come back to the issues of multi-threading in COM in a later column.
It's still not too late to sign up for Dev Days '99, coming to a city near you on September 15.
As Dr. GUI has been saying, Windows 2000 is just around the corner. The good doctor means it this time. And the best way you can spend a day (besides taking the day off) is to learn all about what's new in Windows 2000.
Windows 2000 is chock-full of features that will make your apps (and the systems they run on) more reliable, more robust, and more powerful. And there are tons of new system services that give you easy, standard ways to do complicated things, such as setup, transactions, and queues.
So check out DevDays '99 at http://msdn.microsoft.com/events/devdays/. It'll cost you US$150, but that's cheap compared with a day at a PDC. Dr. GUI's sure it'll be time and money well spent -- and if you're in Seattle, you might just see the good doctor there (incognito, of course).
Dr. GUI couldn't resist this: A Web server on a chip at http://www-ccs.cs.umass.edu/~shri/iPic.html. Cool, huh? It almost makes the good doctor wish he'd studied Electrical Engineering instead of Computer Science. Almost.
There's also an article from the Seattle Times about this at http://archives.seattletimes.com/cgi-bin/texis.mummy/web/vortex/display?storyID=37b741c61e.
Nowadays, it seems everyone's giving away PCs. MSN even has two offers -- one through Micro Center (http://www.microcenter.com) and one through ePCdirect (http://www.epcdirect.com). Other companies have deals, too; a good article about them is at http://www.seattletimes.com/news/technology/html98/free_19990718.html.
The Association of Support Professionals gave their annual awards for the best "Web Support Sites" recently, and Dr. GUI was pleased to see MSDN listed as one of the 10 winners. (For the complete list, see http://asponline.com/awards.html.) The good doctor is even more pleased to tell you that a lot of improvements are in the works to make MSDN Online even better.
If you have a Windows CE device, you'll absolutely want to get ActiveSync 3.0 (http://www.microsoft.com/windowsce/products/highlights/activesync.asp). It replaces almost all older versions of Windows CE synchronization software (except for the original H/PCs with Windows CE 1.0). And it is amazing: It just works.
Now it's not perfect (what software is?), but it's such a vast improvement over previous versions that you're silly if you don't upgrade. And the price is right: free.
Did you go out and buy yourself one of the cool Windows CE H/PC Pro devices? If you did, and if you have a Windows NT Terminal Server, you'll want to download the Terminal Server client at http://www.microsoft.com/windowsce/products/download/term-serv.asp. With it installed (and the appropriate license), you'll be able to use almost any Windows NT application on your H/PC Pro.
Even if you don't have an H/PC Pro or other Windows NT Terminal Server client hardware, you may want to use Terminal Server with a Windows 95/98/NT machine as a client. Why, you ask? Because it allows you to use machines with hardware, OS, or application configurations different from yours. For instance, the Developer Support group at Microsoft has Windows NT Terminal Servers set up with old versions of Visual Studio®. When a customer calls with a question about an older development environment, the support engineer can hook up to the appropriate Windows NT Terminal Server box and reproduce the problem the customer is experiencing -- without having to bother installing the older version on their machine. You could do the same for projects that were built with older versions of Visual Studio. Simply set up a Windows NT Terminal Server with the project and the appropriate version of Visual Studio (or any other development environment), and anyone with the appropriate license can maintain the project from his or her desk.
Dr. GUI is so glad that his new laptop has DVD-ROM. Using the MSDN Library is a dream when you don't have to switch CDs. In fact, the only thing that the good doctor lacks when using his laptop away from work is a fast Internet connection. (Maybe it's time for DSL now that the price is down to $19.95/month, plus ISP charges.)
But, the good doctor is even happier that you, too, will be able to take advantage of DVD-ROM: July's MSDN Library was provided on DVD-ROM free of charge to every subscriber. What better incentive could there be to get an inexpensive DVD-ROM drive? (By the way, Encarta Reference Suite on DVD-ROM is tres cool, too.)
Starting in January 2000, only a year before the new millennium, MSDN subscribers will be able to choose to get everything, not just the Library, on DVD-ROM. Check it out, especially if you're not a current subscriber, at http://msdn.microsoft.com/subscriptions/.
Get your start on Windows 2000 Device Drivers. The RC1 (Release Candidate 1) version of the Windows 2000 DDK is available as a free download at http://www.microsoft.com/ddk/.
So you think that Microsoft's Web servers aren't very scalable? PC Week's scalability benchmarks say otherwise. They concluded that the Microsoft Web platform is "fast enough for any business on earth."
The good doctor has been doing some interesting, non-computer reading of late. On the off chance that you might enjoy the same books, he'd like to share a few titles with you.
The first of the books is Andrew Tobias's The Only Investment Guide You'll Ever Need. (Dr. GUI has had to be concerned with personal finance lately.) It's amazing that anyone can make personal finance downright fun and funny -- but Andy can. This book is informative and really fun to read. And his Web site has a daily column that's quick-to-read, fun, a tad quirky, and informative (http://www.andrewtobias.com).
You know the shaver ad where the guy says he liked the shaver so much he bought the company? Well, Andrew Tobias isn't for sale -- but many of his books are still in print. Dr. GUI's bought and read them all, and recommends them. He especially enjoyed the Modern Library edition of The Best Little Boy in the World. They're all written in a lucent, fun-to-read style, and each has something important and interesting to say. Check them out at his Web site (above).
Finally, Dr. GUI picked up something he's been secretly wanting for a long time: Miss Manners' Guide to Excruciatingly Correct Behavior. It's wonderful. You can learn such things as how to politely decline invitations you'd really much rather avoid. And, Miss Manners, like a good programmer, adds comments about why she's telling you what she's telling you, so you learn more than just the etiquette rules.
You might have a hard time finding it, though: Amazon lists it as out-of-print and Barnes and Noble says it takes several weeks to ship. Dr. GUI was fortunate to find his copy for an excellent price at Costco. Just look for the large book with the pink slipcover.
Once the good doctor's done with Miss Manners, it'll be time for Matthew Fox's The Reinvention of Work. And maybe something about those computer thingies. For instance, there's a new book on building ATL COM objects. The good doctor hopes to report on that one next time.
As you may have noticed, we've finished COM Automation. Finally! Yes!
Three columns were far more than the good doctor expected, but it still came out shorter than Brockschmidt (and, certainly shorter than the book Kraig at one time planned to write covering it). With so many "you'll only find it here" points to cover, it's no wonder it took a little longer.
This time, we'll talk about COM events. Events, thankfully, are pretty well documented. But they're not trivial, so hang on! As we'll discuss, events are sometimes under-used, so it's good to feel comfortable with them. This time, we'll talk about how to fire events and the theory of receiving them.
Next time, we'll also show how to implement events in ATL and how to write a Visual Basic client that receives events. And we'll talk about how your designs can be leaner by using events.
At long last, we get to events. But what is an event?
Recall, if you will, that the communications we've seen so far in COM have been very one-sided: The client called methods on the object. That's it.
Okay, it's true that the object could pass back return values, but only when the client asked. "Speak only when spoken to."
"Speak only when spoken to" works well for a wide variety of objects, including "dumber" objects that simply respond to commands.
Figure 1. One-way communication between a client and its object using the IFoo interface. Recall that the unlabeled interface is IUnknown.
But what if your object needs to inform the client that something interesting has happened? For instance, a visual control, such as a button, would want to tell the client when it's been clicked. An object that implements a business rule might need to tell the client that the rule has been violated. Or an object might be doing something in the background and want to inform the client when it's done. (We won't be covering the background case in this article.)
You could, of course, implement a method on the object called HasButtonBeenClicked or HasRuleBeenViolated or ArentYouDoneYET. The object would keep a flag describing its state and would return it in response to the method call. But it would be the client's responsibility to poll the method continually. This is inefficient and hard to program -- not a good thing.
Instead, what if the object could call a method on the client? Then the object could call the method as soon as the conditions warranted it: When the button was clicked, when the rule was violated, or when it was finished. And the client would get speedy (not necessarily instant, not necessarily asynchronous) notification that the event had occurred. Such a scheme would look something like this.
Figure 2. Two-way communication between client and object using the IFoo interface to communicate from the client to the object, and the IFooEvents interface to communicate from the object to the client.
Note that the client now has to implement an interface that the object makes calls on. However, the object still specifies the interface. Since the object is the source of the calls on this outgoing interface, this interface is called a source interface. Think of the object as being the source of events and it'll be easy to remember.
The client is the sink for calls on this interface. We'll use the terms source and sink from now on to indicate the object (source) and client (sink). What the good doctor calls a "source object" is called a "connectable object" by other folks. The two are, for our purposes, one and the same.
So COM events are basically simple: They're a way for the object (source) to call methods on the client (sink). This being COM, the event method will be called via an interface pointer. That implies three things:
In addition to these basics, COM's event scheme supports a number of interesting capabilities:
Another name for what we've been calling the event interface is the source interface. It's called the source interface because it's declared in the event-firing object that is the source of calls made on that interface.
Our source interface for this sample is very simple; here's the Interface Definition Language (IDL):
[
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);
};
The Changed event will be fired every time the value of the object changes, and the SignChanged event will be fired every time the sign of the value of the object changes.
The first thing you'll notice is that the type of interface for our events is the dreaded dispinterface -- a dispatch interface. Why, you might ask? Particularly after Dr. GUI made it clear in previous columns just how inefficient and tedious dispatch interfaces are for everyone involved?
Well, despite the fact that dispatch interfaces are slower and are a pain to program, they do have one major advantage: It's relatively easy to write code that will correctly interpret the calls from an arbitrary dispatch interface. All you have to do is implement IDispatch, especially Invoke. The parameters are passed as an array of variants -- far easier to parse than digging through the stack to find the parameters as they're actually passed. The client object will have to use the type library to figure out what methods exist (and, if it wants, what their parameters are).
There's a second, more subtle (but even more important), reason: With a dispatch interface, the object receiving the calls doesn't have to provide implementations of all the methods in the interface. Recall that when you implement a regular custom (or, for that matter, dual) interface, you must implement all of the methods on that interface (at least to return E_NOTIMPL).
For a dispatch interface, you have to implement all of the methods of IDispatch, but your implementation of Invoke doesn't have to implement every method in the dispatch interface -- it can just return an error (DISP_E_MEMBERNOTFOUND) for methods it doesn't support (in other words, events it doesn't care to handle).
So, because it's easier for a language such as Visual Basic or Visual Basic for Applications (VBA) to get event calls on a dispatch interface rather than on a custom interface, that's all they support. If you're writing your own sink in C++ and you don't care about other clients, you can use a custom interface for improved performance. But in most cases, events are relatively rare, so usually the performance isn't a big issue. It doesn't make sense for a source interface to be a dual interface; the dualness of dual interfaces is that their methods can be called either directly or via IDispatch::Invoke; in other words, dual interfaces are about receiving calls, not making them. If you want to have an event interface that can be high performance and compatible with Visual Basic, implement two equivalent source interfaces (one custom and one dispatch) with different interface IDs (IIDs), and make the dispatch interface the default so Visual Basic will be happy.
The other thing you'll notice is that the interface name begins with an underscore. This is a Visual Basic convention: If the interface name begins with an underscore, it won't be displayed by the Visual Basic environment.
Aside from that, the IDL code looks really familiar.
But there's something missing! How do we know that the interface is a source interface in our particular object? We've never marked them before, because, by default, interfaces are incoming interfaces.
There's nothing in the IDL for the interface itself that tells us whether the interface is a source or sink interface. It can be either, depending on how it's used. Whether it's a source interface is determined by our particular object, so we specify it in the coclass section of our IDL:
coclass AAAFireLimit
{
[default] interface IAAAFireLimit;
[default, source] dispinterface _IAAAFireLimitEvents;
};
Note that we've specified that the _IAAAFireLimitEvents interface is both an outgoing (source) interface and the default source interface. Visual Basic and scripting languages deal with only one source interface per COM object, so it's best to use the default attribute (and essential if you have multiple source interfaces).
Now that we've got the interface described, we have to get an interface pointer to it. (We'll assume that the client implements it correctly.) What has to happen next is that the client has to create a sink object that implements the source interface and pass its interface pointer to the source object.
But, of course, it's not that simple.
The designers of COM sometimes add in extra functionality that ends up not being used much. For instance, you might recall that some of the IDispatch methods had an interface ID (IID) parameter. This was to allow support for multiple dispatch interfaces, but that feature was never implemented nor made part of the COM standard. So, dispatch interfaces assume one interface per COM object.
In the case of events, the designers of COM wanted to support the features described above (multiple outgoing interfaces per object, and so forth) and also wanted to allow you to change event interfaces without changing the main code for your object.
COM's solution to this problem is to have your source object implement a miniature COM object, called a connection point. Your source object will have exactly one connection point object for each outgoing interface. Each connection point will serve exactly one outgoing interface.
Connection points are separate COM objects, but they're created by your object, not by CoCreateInstance. (We'll talk about that shortly.) Connection points typically implement only two interfaces: IUnknown and IConnectionPoint.
The easiest way to understand what a connection point does is to look at the methods of the IConnectionPoint interface.
The most important are Advise and Unadvise. The client (sink) calls IConnectionPoint::Advise on the connection point object to establish the connection. (Don't worry about how the client gets an interface pointer to the connection point object for right now.) The parameter to Advise is the IUnknown pointer to the sink object that implements the event interface. The implementation of Advise stores this interface pointer in the connection point object. After that, the source object can fire an event by getting the interface pointer from the connection point object and calling a method on that pointer.
Advise returns a cookie -- an integer with a unique value that represents this connection. To break the connection, the client calls Unadvise with the same cookie. This causes the connection point object to delete the associated interface pointer. It's vital for your client to do this when it no longer wants to receive events -- for instance, when it shuts down.
Since multicasting is supported, it's possible for more than one connection to be made using the same connection point. When the event is fired, the source object has to call the correct method on each connection. If twenty objects have called Advise, then when you fire the Changed event, twenty calls to Changed will be made -- one for each interface pointer passed to Advise. (As mentioned before, the connection point is responsible for storing all these interface pointers.)
The next method in IConnectionPoint is more for the source object's convenience: EnumConnections returns a pointer to an object (yet another mini-object) that implements IEnumConnections. This allows the source object to get the interface pointers it needs to make the method calls. (You may recall that we discussed IEnum... interfaces briefly in the last article.)
The final two methods can be used by either the client or source, and they do just what their names say: GetConnectionInterface returns the IID of the interface this connection point serves, and GetConnectionPointContainer returns the source object's IConnectionPointContainer pointer. (We'll explain what that is anon.)
To review: For the client, the connection point's function is to provide a way to create a connection (Advise) and break a connection (Unadvise). If you understand that, you can figure out the rest.
We now know how to set up a connection once we have a connection point: We simply pass our sink mini-object's IUnknown pointer to the connection point's Advise method.
But how do we get a pointer to the connection point in the first place?
We mentioned before that COM objects can support more than one connection point. In order to do this, source object must implement the IConnectionPointContainer interface. This interface allows the client object to obtain a connection point.
IConnectionPointContainer is a simple interface, with only two methods: FindConnectionPoint returns a pointer to the connection point specified by the IID passed by the client object, and EnumConnectionPoints returns an IEnumConnectionPoints enumerator that allows the holder to walk through all of the connection points supported by the source object.
So, the source object implements IConnectionPointContainer. The object also maintains a collection of connection points that can be searched and enumerated via the methods in IConnectionPointContainer. Each connection point maintains a list of active connections, set up by calls to the connection point's Advise method and ended by calls to the connection point's Unadvise method. These connections can be enumerated, too. (In addition to the connection point object, you need to be able to create an object that implements IEnumConnectionPoints for IConnectionPointContainer::EnumConnectionPoint, and an object that implements IEnumConnections for IConnectionPoint::EnumConnections -- a total of three different miniature COM objects.)
It's complicated, but it's what you need to support multiple event interfaces and multicasting.
Here's a diagram of how all the objects look when everything's connected:
Figure 3. Setting up a connection point the COM way. ICP stands for IConnectionPoint. Dr. GUI has omitted the enumeration objects.
Now that we know what all the objects and interfaces are, we can derive the sequence of steps necessary for the client to receive events:
Once the connection is made, firing events is relatively simple -- the source object enumerates the connections and calls the appropriate method on each one.
Since most event interfaces are dispatch interfaces, the appropriate method is IDispatch::Invoke with the correct parameters identifying which dispatch method to call with what parameters. (See? You knew that all that dispatch interface stuff from previous columns would come in handy!)
The sink mini-object then receives the events and handles them as it chooses. That's the relatively easy part.
The good doctor alluded to problems he was having with events and threads earlier.
Here's the basic rule: Unless you do something special (more on this later), you always have to fire your events from the same thread that called IConnectionPoint::Advise. You may not normally start another thread and fire events from that thread. Why? Well, to fire an event means that you call into another object using an interface pointer.
But, in COM, you are never allowed to pass interface pointers between threads; you must marshall them instead. This is not terribly difficult to do, even though it involves calling the API with the world's longest name: CoMarshalInterThreadInterfaceInStream. (Perhaps it's named for a small town in Wales.)
You pass the stream pointer to the other thread (guaranteed to work in this special case) and call CoGetInterfaceAndReleaseStream in the new thread to get a marshalled interface pointer. Unfortunately, ATL's event firing implementation doesn't marshall the interface pointers, so it'll take some work to fire events from another thread.
In two common cases, this isn't an issue. If you're firing the event in response to a Windows message, such as a mouse button down message, you'll be okay. You'll be running on the same thread you were created on when you received the message.
Or, in our example, we fire an event in response to a method call. Again, this is safe, because the method will be called on the same thread you were created on.
It's only when you explicitly create a thread yourself (such as to sleep, or do some background processing) that firing events from the newly created thread becomes difficult.
COM+ will bring some major changes to the world of events. Of course, the COM events we're talking about here will continue to be supported. But COM+ will also support new easier ways of firing and sinking events, including persistent events using a publish/subscribe scheme.
But we're not talking about those just yet. For now, we're sticking to the "classic" COM events as understood by languages such as Visual Basic and scripting languages.
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. That's a lot just to fire a little event!
Isn't there an easier way? Microsoft Foundation Classes (MFC) makes it easy -- at the cost of large objects. Or, you can use our hero, Active Template Libraries (ATL), which makes it almost as easy. We'll talk about exactly how to do that next time.
This time we talked about COM events, including connection points and so much more. But we didn't yet write an actual code -- that'll be next time. Even though the article explaining the code won't be posted until next time, you can take a look at the code now by downloading it from http://msdn.microsoft.com/voices/drgui0899.zip.
After that, we'll discuss persistence. Or maybe events and threads.