Nigel Thompson
Microsoft Developer Network Technology Group
March 20, 1995
Click to open or copy the files in the HOUSE4 sample application for this technical article.
This technical article is the fourth 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 code reuse through object aggregation.
Note Before running the sample, you must register the location of 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.
If you have read the previous three articles in this "MFC/COM Objects" series, you may well have thought to yourself that there seems to be an awful lot of code required to implement a few simple interfaces, and also that there is a lot of code replication in my examples. Well, even if you haven't had those thoughts, I have. It has seemed to me for a while now that I should in some way be reusing some bits of the code rather than doing yet another cut and paste job.
In this article I want to look at a method of code reuse, supported by COM objects, called aggregation. After reading a lot of what has been written to date, I have to say that several people have really avoided the issue in practical terms. I will try very hard not to do that, too!
Before we go any further, let me answer one question that you might have about using multiple inheritance as a means of incorporating multiple interfaces into a single object. In response to the "Can we do this?" question, there are two answers: "Yes" and "No." On the "Yes" side, there are ways to use multiple inheritance, but they are a bit outside the scope of these articles. On the "No" side, I have to tell you that if you want to stick with the Microsoft® Foundation Class Library (MFC) and the wonders of the Visual C++™ AppWizard, you have to live without multiple inheritance because the MFC folks will not be using it. So when AppWizard becomes capable of creating and editing COM object code, it's going to do it through object aggregation and the macros you've seen so far.
I don't want to go into horrendous detail about exactly how object aggregation works because you can read about that in the OLE 2.0 Design Specification, in Kraig Brockschmidt's book Inside OLE 2 (MSDN Library, Books), and a few articles in the MSDN Library. Aggregation is a way of having a COM object support a given interface without implementing that interface directly itself. Instead, the COM object includes another object that does support that interface, and the containing object exports the interface from the contained object. Figure 1 shows the idea a bit more clearly.
Figure 1. Aggregation in use
To the user of the appliance object, it is as though the appliance supports the IDrawing interface even though in actual fact it is implemented in a different object. There is a lot more to the mechanism of aggregation than I've talked about here, but this is enough detail for now.
If you dig through the code of the various appliances I have built for the HOUSE sample, you'll find that they all support the IDrawing interface, and in fact, all implement it the same way with pretty much exactly the same piece of code. So here's a good candidate for code reuse. If you read the previous article ("MFC/COM Objects 3: Objects That Talk Back"), you might have thought that the INotifySrc interface would be a handy thing to have in all objects that can change state—I did, too. So my second candidate for the aggregation treatment is a notification list object that any appliance can use to keep track of who needs notification of changes of state.
Of these two examples, the notification list object is the easiest to envisage. After all, one list is much like another, and if the list object were to support adding a user, removing a user, and notifying all users, we'd have the design pretty much done. So I could alter my earlier definition of INotify a bit, crank out the code, and have an object ready for aggregation into one of the appliances.
The first example of reuse—support for the IDrawing interface—is a bit harder to envisage. You can't simply have a chunk of code that draws without knowing exactly what it needs to draw. In the case of the appliances, I always wanted to draw a bitmap. The bitmap sometimes changed when the appliance changed state, but in essence, I wanted to be able to say, "This is my bitmap. Draw it." So I could perhaps create a bitmap object (a COM object, that is) that supports the IDrawing interface, and the appliance could use one of these bitmap objects. But (and this is a big but) how will the appliance tell the bitmap object which bitmap to draw? Be careful here! You can't simply say, "Give it a pointer. . ." because we're now dealing with a COM object that can (in principal, at least) be in a different address space. Even if we say this bitmap object will always be in an InProc server, and so the addressing problem is moot, we still don't have a way to tell the bitmap object which bitmap to load. We could always define yet another interface—IBitmap perhaps—that has a Load function. Then the appliance object can create a bitmap object, call its IBitmap::Load function to load the image, and via object aggregation, export the bitmap's IDrawing interface to the outside world.
Does all this sound a bit complex to you? Does this seem like we're adding a thousand lines of code to save five? Well, it does to me, too, so no point going on with the discussion. It's time to do a little creative programming and see what all this boils down to in practice. The goals are (1) to create a bitmap object that supports IDrawing, (2) to use that in each of the appliances, and (3) to create a notification list object that is used by any object that can change state and might want to tell its user about that fact. We'll start with the notification list object.
Figure 2 shows the NotifySrcList object and its interfaces.
Figure 2. The NotifySrcList object
In moving the INotifySrc interface into this object, I decided to rename the interface functions so that they made a bit more sense. I also added a new member, NotifyAll. Here's the new interface definition:
// {A7B3F570-4E8D-11ce-9EED-00AA004231BF}
DEFINE_GUID(IID_INotifySrc,
0xa7b3f570, 0x4e8d, 0x11ce, 0x9e, 0xed, 0x0, 0xaa, 0x0, 0x42, 0x31, 0xbf);
class INotifySrc : public IUnknown
{
public:
// Standard IUnknown interface functions
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid,
LPVOID* ppvObj) = 0;
virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;
virtual ULONG STDMETHODCALLTYPE Release(void) = 0;
// This interface
virtual HRESULT STDMETHODCALLTYPE AddUser(INotify* pNotify,
LPVOID pUserInfo) = 0;
virtual HRESULT STDMETHODCALLTYPE RemoveUser(INotify* pNotify) = 0;
virtual HRESULT STDMETHODCALLTYPE NotifyAll(void) = 0;
};
The members we're primarily interested in are AddUser, RemoveUser, and NotifyAll. The NotifyAll member walks the list and calls each entry to notify it of some change that has occurred. Let's go through the process of creating this new object step by step and then see how it's put to use.
Use ClassWizard to create a new class derived from CCmdTarget. (Don't forget to check the OLE Automation and OLE Creatable boxes.) Add the interface map declaration, the interface definition, and any member variables to the header file. We also need to define a simple class to hold the user information:
class CNotifyListObject : public CCmdTarget
{
[...]
// Declare the interface map for this object.
DECLARE_INTERFACE_MAP()
BEGIN_INTERFACE_PART(NotifySrc, INotifySrc)
STDMETHOD(AddUser)(INotify* pNotify, LPVOID pUserInfo);
STDMETHOD(RemoveUser)(INotify* pNotify);
STDMETHOD(NotifyAll)();
END_INTERFACE_PART(NotifySrc)
CObList m_NotifyList;
};
// Notification info class
class CUserInfo
{
public:
INotify* pNotify;
LPVOID pUserInfo;
};
In the implementation file, we add the interface map:
BEGIN_INTERFACE_MAP(CNotifyListObject, CCmdTarget)
INTERFACE_PART(CNotifyListObject, IID_INotifySrc, NotifySrc)
END_INTERFACE_MAP()
In the constructor, we add a call to EnableAggregation() to (yes, you guessed it) enable object aggregation:
CNotifyListObject::CNotifyListObject()
{
EnableAutomation();
// Make this object aggregatable.
EnableAggregation();
// To keep the application running as long as an OLE automation
// object is active, the constructor calls AfxOleLockApp.
AfxOleLockApp();
}
By default, MFC's IUnknown implementation in CCmdTarget doesn't support object aggregation because there's a small amount of overhead required on every call to QueryInterface for aggregated objects.
When we are finished with this object, we need to be sure we release any interface pointers we might be hanging on to in our list. So a small piece of code is added to OnFinalRelease:
void CNotifyListObject::OnFinalRelease()
{
// Make sure the notification list has all its entries removed.
while (!m_NotifyList.IsEmpty()) {
CUserInfo* pInfo = (CUserInfo*)m_NotifyList.RemoveHead();
ASSERT(pInfo);
pInfo->pNotify->Release();
delete pInfo;
}
delete this;
}
Now we simply need to implement the INotifySrc functions. This is pretty trivial stuff, but for once, I'll include all the code here:
/////////////////////////////////////////////////////////
// INotifySrc interface
// IUnknown for INotifySrc
IMPLEMENT_IUNKNOWN(CNotifyListObject, NotifySrc)
// INotifySrc methods
STDMETHODIMP CNotifyListObject::XNotifySrc::AddUser(INotify* pNotify,
LPVOID pUserInfo)
{
METHOD_PROLOGUE(CNotifyListObject, 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;
}
STDMETHODIMP CNotifyListObject::XNotifySrc::RemoveUser(INotify* pNotify)
{
METHOD_PROLOGUE(CNotifyListObject, NotifySrc);
if (!pNotify) return E_INVALIDARG;
// Find this one in the list.
POSITION pos = pThis->m_NotifyList.GetHeadPosition();
POSITION pfnd = NULL;
CUserInfo* pInfo = NULL;
while (pos) {
pfnd = pos;
pInfo = (CUserInfo*)pThis->m_NotifyList.GetNext(pos);
if (pInfo->pNotify == pNotify) break;
}
if (!pfnd) return E_INVALIDARG; // Not found.
// Remove it from the list and delete the info object.
pThis->m_NotifyList.RemoveAt(pfnd);
delete pInfo;
// Say we're done with the user's interface now.
pNotify->Release();
return NOERROR;
}
STDMETHODIMP CNotifyListObject::XNotifySrc::NotifyAll()
{
METHOD_PROLOGUE(CNotifyListObject, NotifySrc);
// Walk the notification list.
POSITION pos = pThis->m_NotifyList.GetHeadPosition();
while(pos) {
CUserInfo* pInfo = (CUserInfo*)pThis->m_NotifyList.GetNext(pos);
ASSERT(pInfo && pInfo->pNotify);
// Report the change of state.
pInfo->pNotify->Change(pInfo->pUserInfo);
}
return NOERROR;
}
The only remaining things to do are add an entry in APPLIANCESID.H for the new object, modify the registry file for the new object, and run REGEDIT to install it. Now let's look at how the NotifyListSrc object can be used to replace the direct implementation of INotifySrc in the standard lamp object.
To try out the NotifyListSrc object, I first edited the standard lamp files to remove all the implementation of INotifySrc. Having cleaned out the old, we can start to add the new. In the header file, we add some member variables and a declaration for OnCreateAggregates:
class CStandardLamp : public CCmdTarget
{
[...]
// Notification list
IUnknown* m_punkNotifyList;
INotifySrc* m_pINotifySrc; // cached ptr
// Overrides for aggregation
virtual BOOL OnCreateAggregates();
// Helpers
void NotifyChange();
};
There are two interface pointers declared: m_punkNotifyList, which will point to the IUnknown interface on a NotifyListSrc object, and m_pINotifySrc, which will point to the object's INotifySrc interface. We don't need to have two pointers. We could live with just the m_punkNotifyList pointer and use QueryInterface to get the INotifySrc pointer every time we wanted it. However, we're likely to need the INotifySrc interface a lot, and calling QueryInterface frequently might slow things down. So we'll get an INotifySrc pointer and cache it here. OnCreateAggregates is a virtual function declared in CCmdTarget. NotifyChange is a local helper function.
Let's look at the implementation code next. The first thing to do is make the pointers NULL in the constructor:
CStandardLamp::CStandardLamp()
{
[...]
m_punkNotifyList = NULL;
m_pINotifySrc = NULL;
[...]
}
Then we add the interface map entry, which will make use of the aggregated object:
BEGIN_INTERFACE_MAP(CStandardLamp, CCmdTarget)
INTERFACE_PART(CStandardLamp, IID_IDrawing, Drawing)
INTERFACE_PART(CStandardLamp, IID_IOutlet, Outlet)
INTERFACE_PART(CStandardLamp, IID_ILight, Light)
INTERFACE_AGGREGATE(CStandardLamp, m_punkNotifyList)
END_INTERFACE_MAP()
Note that we have replaced the direct implementation of INotifySrc with an aggregated version supplied by the object whose IUnknown interface pointer will be held in the m_punkNotifyList variable.
We now add the OnCreateAggregates function:
BOOL CStandardLamp::OnCreateAggregates()
{
// Create the objects we want to aggregate.
HRESULT hr = ::CoCreateInstance(CLSID_NotifyListObject,
GetControllingUnknown(),
CLSCTX_INPROC_SERVER,
IID_IUnknown,
(LPVOID*)&m_punkNotifyList);
if (FAILED(hr)) {
TRACE("Failed to create object. SCODE: %8.8lXH (%lu)\n",
GetScode(hr),
GetScode(hr) & 0x0000FFFF);
m_punkNotifyList = NULL;
return FALSE;
}
ASSERT(m_punkNotifyList);
// Get a pointer to the object's INotifySrc interface,
// so we won't have to get this every time we want to use it.
if (m_punkNotifyList->QueryInterface(IID_INotifySrc, (LPVOID*)&m_pINotifySrc) != S_OK) {
TRACE("INotifySrc not supported");
return FALSE;
}
ASSERT(m_pINotifySrc);
// When we got the INotifySrc pointer, this caused our main object
// ref count to be incremented by one (because the notify src list
// object is aggregated). We don't want this extra ref count, so
// we manually remove it. (Yes, I know this doesn't look very kosher.)
m_dwRef--;
ASSERT(m_dwRef > 0);
return TRUE;
}
This function creates an instance of the NotifyListObject object and saves its IUnknown pointer in m_punkNotifyList for use by the interface map. Then it goes on to obtain a pointer to the object's INotifySrc interface and saves it in m_pINotifySrc. This bit requires a bit of rather hacky code to implement correctly. Because the notify list object is aggregated into the standard lamp object, when I get a pointer to any one of the notify lists interfaces, the reference count of the standard lamp object is incremented not the reference count of the list object. Since any object that has a reference count on itself can never be deleted, we have to do something here to set the balance straight. What we do is simply decrement the standard lamp's reference count by one. This is straight out of the OLE 2.0 Design Specification, so be kind to me and send your comments to them.
This is all that's needed for the standard lamp object to support INotifySrc. MFC's object aggregation support does everything else. The only other thing to do now is modify the standard lamp code to call the notify list object's NotifyAll member to report changes. I did this in two steps, using a helper function. Here's an example of where it's used:
STDMETHODIMP CStandardLamp::XLight::SetBrightness(BYTE bLevel)
{
METHOD_PROLOGUE(CStandardLamp, Light);
pThis->m_bLevel = bLevel;
pThis->NotifyChange();
return NOERROR;
}
And here's what the NotifyChange helper does:
void CStandardLamp::NotifyChange()
{
ASSERT(m_pINotifySrc);
m_pINotifySrc->NotifyAll();
}
I agree that you could probably just use m_pINotifySrc->NotifyAll(); inline instead of calling this function, but I wanted to guarantee the ASSERT would be there every time. And finally, we need to tidy up when OnFinalRelease is called:
void CStandardLamp::OnFinalRelease()
{
m_dwRef++;
ASSERT(m_pINotifySrc);
m_dwRef++;
m_pINotifySrc->Release();
ASSERT(m_punkNotifyList);
m_punkNotifyList->Release();
delete this;
}
This code releases the pointer we have on the list object interfaces. Because the INotifySrc pointer required us to do some direct modification of the lamp object reference count, we must be sure that we compensate for that here. So for every cached interface pointer we have created, we must increment the lamp object's reference count by one before releasing the interface pointer. Unfortunately, that's not quite all. We must also do an additional increment of the reference count to be sure that the destructor of the lamp object gets called only once. Again—go and read the OLE 2.0 Design Specification for a more detailed analysis of this problem.
Now we have a standard lamp that supports INotifySrc through object aggregation. That was so much fun to do I thought we'd move right along and do it all again for IDrawing <grin>.
Providing support for IDrawing in all the appliance objects through object aggregation is a little more tricky than it was to support INotifySrc because we're going to need to know what to draw. The object we'll create here is based on a bitmap (a device-independent bitmap actually) and has two interfaces. One interface allows us to set up the correct image; the other is the IDrawing interface.
Figure 3. The BitmapObject object
I'll just warn you here that this isn't going to be the definitive bitmap object you've been looking for—it's a cheap and cheerful demo of an aggregatable object—that's all. Since so much of the way we'll build this object is the same as we did for the notify list object, I'm going to skip a lot of the code and just give you the "to do" list.
Here are the steps I took to create the BitmapObject object:
The results of this are in BITMAPOB.H and BITMAPOB.CPP.
I decided to demonstrate use of the BitmapObject object in the radio because it has the simplest implementation of IDrawing. I'm going to give you the list of steps here, and let you review the code later.
Having done all of that, the radio now should draw itself exactly as it did before, but now it doesn't have to implement the IDrawing code itself.
Using object aggregation can help you reuse one piece of code in multiple COM objects that require a common interface. If you have time, perhaps you'd like to modify the rest of the appliances in the sample to use the bitmap object and support the INotifySrc interface, too.