Nigel Thompson
Microsoft Developer Network Technology Group
March 20, 1995
Click to open or copy the files in the HOUSE3 sample application for this technical article.
This technical article is the third in a series that describes creating and using 32-bit Component Object Model (COM) objects with Visual C++™ and the Microsoft® Foundation Class Library (MFC). This article looks at objects that send notification events back to their users.
Note Before running the sample, you must register the location of the APPLIANCES.DLL in the registry. See the first article in this series ("MFC/COM Objects 1: Creating a Simple Object") for details on how to do this.
On many occasions the user of an object would like to know when the object changes state. For example, in the previous article ("MFC/COM Objects 2: Using Interfaces"), we were controlling various light bulbs through an interface that allowed us to turn them on and off and maybe set a brightness level. In order to see the effect of these changes of state, we redrew the entire scene, asking each bulb to draw itself in its current state. We knew that if one of the control dialog boxes changed the state of an object, we needed to redraw the scene to see the change.
You might have tried running two instances of the slider control for the standard lamp and noticed that when you moved one slider, the image of the lamp changed, but the other slider did not. In this article we're going to look at a better way for users of an object to find out when it changes state. By the time we're done, you'll be able to bring up two sliders for the standard lamp, move one of them, and see not only the image of the lamp change, but also the position of the other slider.
Figure 1 shows a picture of what we want to happen when a slider is used to update the state of the lamp.
Figure 1. Notification events being used to update state
In the scenario shown here, Slider 1 is moved by the user and uses the ILight interface of the standard lamp to set a new brightness level (1). The standard lamp object then sends out notification events (2, 3, and 4) to all users of the object that need to know about a change of the object's state. In response to these notifications, the sliders use the object's ILight interface to find its current brightness level and reset their own positions accordingly. The house simply uses the object's IDrawing interface to ask the object to draw itself in its current (new) state. No big deal, eh?
Although this may look simple, there's a fair bit of work required to set it up. Let's go through the steps one by one, starting with the moment the house first creates the lamp object. At this point, the house needs to tell the lamp that it (the house) would like to be notified of any change of state in the lamp. How does the house do this? First of all, we need to create some new interface that the lamp supports, that allows the lamp to be told that someone wants to be notified of something. I called this new interface INotifySrc because it deals with objects that are the source of some notification event. So if we make our lamp support INotifySrc, we have an interface the house can talk to to tell the lamp that the house wants change-of-state notification events. Note that because there is a potential for multiple users of a single object to need change-of-state notification, the object must keep a list of all interested parties.
So now the lamp has a list—but a list of what? What exactly is the lamp object going to store that it can use to tell its user that a change of state has taken place? Score one point if you said a pointer to something. Take a point off if you said a pointer to the CMainFrame C++ object. Take 10 points off (and see me after class) if you said a window handle. Score 10 zillion points if you said—A pointer to an interface. Why does it need an interface pointer and not some other pointer? Well, in the longer term, objects and users of objects might be on different machines or, at least, in different process address spaces. A regular Joe Soap pointer isn't going to be much use in a different address space, but an interface pointer can be marshaled by the wonders of remote procedure call (RPC) so that it still works. OK, so maybe it's a bit of a thin argument, but we're in here trying to learn about interfaces, and this is where we can add another one.
So for a user of the object to receive notification from the object, it (the user) must provide the object with a pointer to some interface of its own that the object can use to send the notification event. I called this new interface INotify. Figure 2 shows a nice pictorial summary.
Figure 2. The notification interfaces in use
I added the code to support notification in three steps. First, I added the INotify interface to the house code. Then I added INotifySrc support to the standard lamp object, and finally I added INotify support to the slider dialog. We'll look at these steps in turn.
The first step is to declare the interface map and the interface itself in MAINFRM.H:
class CMainFrame : public CFrameWnd
{
[...]
// 3:INotify support
// Declare the interface map for this object
DECLARE_INTERFACE_MAP()
// INotify interface
BEGIN_INTERFACE_PART(Notify, INotify)
STDMETHOD(Change)(LPVOID pUserInfo);
END_INTERFACE_PART(Notify)
};
Now, in MAINFRM.CPP, we implement the interface map:
BEGIN_INTERFACE_MAP(CMainFrame, CCmdTarget)
INTERFACE_PART(CMainFrame, IID_INotify, Notify)
END_INTERFACE_MAP()
Note that we don't need to use the OLECREATE. . . macros because we won't be creating any objects through this interface.
Next we modify CHouseApp::InitInstance and CHouseApp::ExitInstance in HOUSE.CPP to register and revoke the class factories. I haven't mentioned class factories before, and there isn't space to go into detail here, so if you want to know more, read Kraig Brockschmidt's book, Inside OLE2 (MSDN Library, Books).
BOOL CHouseApp::InitInstance()
{
[...]
// 3:Register our class factories.
COleObjectFactory::RegisterAll();
[...]
}
int CHouseApp::ExitInstance()
{
// 3:Revoke all class factories.
COleObjectFactory::RevokeAll();
[...]
}
The last step is to provide the implementation of the interface itself in MAINFRM.CPP:
IMPLEMENT_IUNKNOWN(CMainFrame, Notify)
STDMETHODIMP CMainFrame::XNotify::Change(LPVOID pUserInfo)
{
METHOD_PROLOGUE(CMainFrame, Notify);
// One of the objects has changed, so redraw to see the effect.
pThis->Invalidate();
return NOERROR;
}
Note that we aren't trying to be clever here. If we detect that an object has changed state (CMainFrame::XNotify::Change is called by an object), we simply redraw the entire scene. We could optimize this quite a bit by only redrawing the area around the changed object.
The application now has a complete implementation of INotify. All that it needs to do now is supply a pointer to this interface when it creates an object that supports the INotifySrc interface. We do this in the CMainFrame::CreateAppliance helper function:
void CMainFrame::CreateAppliance(REFIID riid,
IUnknown** ppIUnknown,
int x, int y,
CRect* pRect)
{
[...]
// 3:Get a pointer to the object's INotifySrc interface.
INotifySrc* pINotifySrc = NULL;
if ((*ppIUnknown)->QueryInterface(IID_INotifySrc,
(LPVOID*)&pINotifySrc) != S_OK) {
dprintf2("INotifySrc not supported");
} else {
// Give the COM object a pointer to our own INotify interface,
// and use the user info ptr to store the object's IUnknown ptr.
INotify* pINotify = NULL;
ExternalQueryInterface(&IID_INotify, (LPVOID*)&pINotify);
ASSERT(pINotify);
pINotifySrc->SetUser(pINotify, *ppIUnknown);
// Free the interface.
pINotifySrc->Release();
}
}
Note the use of ExternalQueryInterface to get a pointer to our own INotify interface. ExternalQueryInterface is part of MFC's IUnknown implementation in CCmdTarget.
Supporting INotifySrc in the standard lamp required two things: the creation of a simple C++ object to store information about each client, and the addition of code to implement the INotifySrc interface itself. Because there can be multiple clients wanting notification, the standard lamp actually keeps a list (a CObList object) of clients. Here are the additions to STANDARD.H:
class CStandardLamp : public CCmdTarget
{
[...]
// 3:INotifySrc interface
BEGIN_INTERFACE_PART(NotifySrc, INotifySrc)
STDMETHOD(SetUser)(INotify* pNotify, LPVOID pUserInfo);
STDMETHOD(UnsetUser)(INotify* pNotify);
END_INTERFACE_PART(NotifySrc)
// 3:Support fns
void CStandardLamp::NotifyChange();
[...]
// 3:Notification list
CObList m_NotifyList;
};
// 3:Notification info class
class CUserInfo
{
public:
INotify* pNotify;
LPVOID pUserInfo;
};
Don't forget to also add the INotify and INotifySrc header files to each module that uses them and also to GUIDS.CPP.
The implementation of INotifySrc is quite straightforward, so we'll look at only the essential parts here. Here's the SetUser function, which adds a new client to the notification list:
STDMETHODIMP CStandardLamp::XNotifySrc::SetUser(INotify* pNotify,
LPVOID pUserInfo)
{
METHOD_PROLOGUE(CStandardLamp, NotifySrc);
if (!pNotify) return E_INVALIDARG;
// Save the user's INotify interface and info.
pNotify->AddRef();
CUserInfo* pInfo = new CUserInfo;
ASSERT(pInfo);
pInfo->pNotify = pNotify;
pInfo->pUserInfo = pUserInfo;
pThis->m_NotifyList.AddTail((CObject*)pInfo);
return NOERROR;
}
Note that we are saving a pointer to the user's INotify interface and also a pointer to some information that users will need to know when they are notified of a change later. For example, a user may send a pointer to an internal data structure that describes how the object is used in the application. When the application gets a change notification, it can use the pointer to access whatever private data it uses to make updates, based on the change of state. So let's see what happens when the lamp changes state:
STDMETHODIMP CStandardLamp::XLight::SetBrightness(BYTE bLevel)
{
METHOD_PROLOGUE(CStandardLamp, Light);
pThis->m_bLevel = bLevel;
pThis->NotifyChange();
return NOERROR;
}
When the brightness is changed, an internal helper function (NotifyChange) is called to notify all the object's users. Here's the function:
void CStandardLamp::NotifyChange()
{
// Walk the notification list.
POSITION pos = m_NotifyList.GetHeadPosition();
while(pos) {
CUserInfo* pInfo = (CUserInfo*)m_NotifyList.GetNext(pos);
ASSERT(pInfo && pInfo->pNotify);
// Report the change of state.
pInfo->pNotify->Change(pInfo->pUserInfo);
}
}
Note how the user information pointer is sent back to the user as a parameter of INotify::Change.
The final step is to modify the slider dialog box to support the INotify interface and have it set up a notification request when the dialog box is created. As for the application, we declare the interface in the dialog's header:
class CLightDlg : public CDialog
{
[...]
// 3:INotify support
// Declare the interface map for this object.
DECLARE_INTERFACE_MAP()
// INotify interface
BEGIN_INTERFACE_PART(Notify, INotify)
STDMETHOD(Change)(LPVOID pUserInfo);
END_INTERFACE_PART(Notify)
INotify* m_pINotify;
};
Let's see what happens when the dialog box is displayed:
BOOL CLightDlg::OnInitDialog()
{
[...]
// 3:Tell it we'd like change notification.
INotifySrc* pINotifySrc = NULL;
if (m_pILight->QueryInterface(IID_INotifySrc,
(LPVOID*)&pINotifySrc) != S_OK) {
dprintf2("INotifySrc not supported");
} else {
// Give the COM object a pointer to our own INotify interface,
// and use the user info ptr to store the object's IUnknown ptr.
m_pINotify = NULL;
ExternalQueryInterface(&IID_INotify, (LPVOID*)&m_pINotify);
ASSERT(m_pINotify);
pINotifySrc->SetUser(m_pINotify, m_pILight);
// Free the interface.
pINotifySrc->Release();
}
[...]
}
As for the application, the object is given a pointer to the dialog's INotify interface. Finally, let's see what happens when the notification event is received:
STDMETHODIMP CLightDlg::XNotify::Change(LPVOID pUserInfo)
{
METHOD_PROLOGUE(CLightDlg, Notify);
// The object has changed, so redraw to see the effect.
BYTE b;
pThis->m_pILight->GetBrightness(&b);
pThis->m_wndBright.SetPos(255 - b);
return NOERROR;
}
Pretty simple really—the slider is set to the current brightness value of the lamp.
One point to note is that when the (human) user moves the slider, it's now unnecessary to tell the application to repaint because the application is also receiving notification events from the object as it changes state.
Using notification events from objects can make multiple use of a single object very easy to manage. Implementing the interfaces required for such notification support everywhere can require that a lot of code be repeated. In the next article ("MFC/COM Objects 4: Aggregation") we'll look at a technique called aggregation for reusing code in COM objects.