Writing MMC Snapins is a Snap with ATL 3.0

Ray F. Djajadinata

Microsoft is encouraging the use of the Microsoft Management Console-MMC-for all applications without standard user interfaces, such as servers. You write your server, and then you write a "snapin" for MMC that will control your server or report on its recent activities. Ray explains the basic mechanism of MMC and the basics of writing its snapins. He also shows you how to remove ATL's limitation in handling the context menu and fixes a nasty bug in a macro.

I think that one of the main reasons behind Microsoft's success is that it always tries its best to make doing everything easier. For example, Microsoft designs its products and technologies so that knowledge and experiences gained from dealing with one are quite often directly applicable in dealing with the others. The integration between Visual Basic and Office is one example of this. At another level, using COM for nearly anything from 3D API to database access is another good example. Still another example is having Internet Explorer integrated into the Windows shell, making everything look and feel instantaneously familiar to the users.

MMC is the product of that design philosophy, which aims to make doing various tasks even easier. Using MMC for hosting administrative tools and applications means that users and administrators no longer have to learn a different user interface for each application. They only have to be familiar with the MMC user interface instead, which is quite similar to the Windows Explorer user interface. That is, you have a tree control on the left pane (also known as the scope pane) and the result you'll get by selecting and expanding the tree on the right (a.k.a. the result pane).

For us, the good thing about MMC is that it provides a single programming model for writing management and administrative applications. By specifying the interaction between MMC applications (better known as snapins) and MMC itself through-what else-"well-defined COM interfaces," the programming model becomes clear and easier to understand. ATL 3.0 adds a layer of abstraction on top of that model, simplifying MMC snapin programming greatly. Familiarizing yourself with snapin development and ATL snapin support will get you ready for Windows NT 5.0-oops, Windows 2000-which will have its management/administrative applications run inside MMC.

What makes a snapin a snapin?

A snapin is nothing but a COM component. But what characteristics must the component possess to become a snapin? First of all, its host must be an in-process server. Second, it must make certain entries in the Registry, under the following keys:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MMC\NodeTypes

 

and

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MMC\SnapIns

besides its regular COM Registry entries. The easiest way of doing this is to add them to the Registry script and let Registrar do the rest. The ATL Snapin Wizard does this automatically when the project is generated, but you can-and sometimes need to-add additional entries, which can be done very easily by scripting.

Externally, that's about all a COM object needs to be a snapin. Inside, however, a snapin must implement certain interfaces if he (not that I have enough evidence that a snapin is a he) really wants to be a real snapin. There are quite a lot of them, but the most important ones are the IComponentData and IComponent interfaces. To implement them, you'll need to make calls to the other group of MMC interfaces-those implemented by MMC itself. The most important of these are IConsole and IConsoleNameSpace, which are used to manage the scope pane and the result pane.

Ah, Doc/View!

The MMC architecture is in some ways rather similar to something we're already familiar with: the Document/View architecture. A snapin can offer multiple views of the same data. To put it into SAT-compliant form: IComponentData is to Document as IComponent is to View. Every snapin instance can have any number of IComponent components, usually one for each MDI child window (MMC is an MFC MDI application, and it hosts snapins in its child windows), but only one IComponentData (every time you add a snapin from Add/Remove dialog box, you get an instance of that snapin). IComponentData methods deal with items in the scope pane, enumerate them, provide information about them, and so on. IComponent methods handle the result pane. They determine what kind of view should be presented to the user, enumerate items in the result pane, and so forth.

The Wizard

Now we'll see what the Snapin Wizard has to offer. First of all, generate a regular ATL project. Remember, choose Dynamic Link Library for your server type. Click Finish. Next, choose Insert, New ATL Object, and you'll get the familiar ATL Object Wizard dialog box. Choose Object for your category and MMC Snapin for the object type.
A dialog box like the one shown in Figure 1 will appear. I called the sample snapin VCDSnap1. Now move to the MMC Snapin tab, clear the Supports Persistence check box, and check the IExtendContextMenu check box. Click OK, and the Wizard will conjure up four nice classes for his master (that's you). (See Figure 2.)

Snapin? What about it?

The simplest class of the four is CVCDSnap1About. This class inherits from ISnapinAbout and is an externally creatable COM object by itself. Implementing the members is very straightforward, as their names clearly represent each of their purposes. I added an icon to be used as the snapin image and changed the default appearance of the snapin's static folder. Change the code under GetSnapinImage to use the icon:

   STDMETHOD(GetSnapinImage)(HICON *hAppIcon) {
      if(hAppIcon == NULL) {
         return E_POINTER;
      }
      *hAppIcon = 
         static_cast<HICON>(
            ::LoadImage(_Module.GetResourceInstance(), 
               MAKEINTRESOURCE(IDI_SNAPINIMAGE), 
               IMAGE_ICON, 0, 0,
               LR_DEFAULTCOLOR));
      return S_OK;
   }

And change the static folder's appearance:

STDMETHOD(GetStaticFolderImage)(HBITMAP *hSmallImage,
      HBITMAP *hSmallImageOpen,
      HBITMAP *hLargeImage,
      COLORREF *cMask)
   {
      *hSmallImage = LoadBitmap(
               _Module.GetResourceInstance(), 
               MAKEINTRESOURCE(IDB_VCDSNAP1_16));
      *hSmallImageOpen = LoadBitmap(
               _Module.GetResourceInstance(),  
               MAKEINTRESOURCE(IDB_VCDSNAP1_16OPEN));
      *hLargeImage = LoadBitmap(
              _Module.GetResourceInstance(), 
               MAKEINTRESOURCE(IDB_VCDSNAP1_32));
      return S_OK;
   }

The information you provide to MMC through these methods will be displayed when you highlight the snapin on the Add/Remove Snapin dialog box and click the About button (this button will only be enabled if you support the ISnapinAbout interface). (See Figure 3.)

Yet another CoClass

If you open the .idl file of the project-VCDSnapin1.idl-you'll notice that there are actually only two externally creatable classes in the project, CVCDSnap1About and CVCDSnap1. This is true regardless of whether you're using ATL 3.0 or not-you only need to make two of your classes externally creatable. One of them is an optional class supporting ISnapinAbout, and the other is the main class, which supports IComponentData.

So in this project, CVCDSnap1 is our central class, which inherits from IComponentDataImpl (the ATL implementation of IComponentData interface) and, in this sample snapin, IExtendContextMenu. For such an important class, however, it only exposes few member functions. This is because IComponentDataImpl has implemented most of IComponentData methods. ObjectMain() is just a simple static member that registers MMC-specific clipboard format for use with IDataObject::GetDataHere() later.

Get a quick view of the view

As I said earlier, in MMC, our "view class" is the class that implements IComponent. In my project, this class is CVCDSnap1Component, which is derived from IComponentImpl. Again, IComponentImpl has done most of the hard work for you. The only overridable provided by the Wizard is the Notify() method, though of course you can always override other methods manually.

ATL enhancements to the programming model

One big problem with writing MMC snapins at the SDK level is that it forces you to handle things in different places, which makes your code harder to read and more error-prone. This is because MMC interfaces weren't designed with the concept of an item in mind.

Take an item in the scope pane, for example. It has several "properties," like the text caption, node type (a GUID that identifies a specific kind of node), IDataObject pointer, context menus, and so forth. Some are visible, some aren't. These properties can't be accessed in a single, consistent way. To manage items, you must scatter your code to several different methods. Most of these methods take a special argument that uniquely identifies an item (a cookie), which can be used to determine which item is currently being handled by your code.

For example, to provide MMC with an IDataObject pointer for an item, you implement QueryDataObject(), which accepts a cookie, and return a class that implements IDataObject interface (especially the GetDataHere() method) for that item. To set its caption, you implement GetDisplayInfo(), which, again, accepts a structure that contains a cookie. How about handling events? Handling messages for a window is really straightforward, done in its own window procedure. But for MMC items or nodes, the handler can be found in two places: IComponentData::Notify() and IComponent::Notify(). So now we have to handle schizophrenic items, as if handling perfectly normal and healthy items isn't difficult enough!

ATL solves this by putting the whole mess of handling an item into a C++ class. As usual, there's the abstract base class <X>, and <X>Impl class, which provides default implementations for X's member functions. These classes are called CSnapInItem and CSnapInItemImpl, respectively. Using classes derived from CSnapInItemImpl enables you to put notification handlers in the item class's "window procedure," store information pertaining to an item in member variables, and so on. In the sample project, this class is named CVCDSnap1Data and represents the snapin's root node.

Now, the trick behind this magic is actually quite simple. As I said previously, the code for handling each item is scattered to several interfaces with several methods each. What ATL does is forward calls to these methods to the item itself. Let's take a look at one example of forwarding a call to a method of IComponentData interface. This method accepts one argument, a pointer to a SCOPEDATAITEM structure:

STDMETHOD(GetDisplayInfo)(SCOPEDATAITEM *pScopeDataItem);

In MMC, there are two types of items: scope pane items and result pane items. A scope pane item is represented by a structure, SCOPEDATAITEM, whose members hold information specific to that item. The structure for a result pane item is RESULTDATAITEM, which does the same for result pane items. Each structure has an lParam member that stores the cookie for the item it represents. The essence of ATL's magic is to use a pointer to the object representing the item as the cookie for the item itself. Let's take a look at what ATL does in this method (taken from ATLSNAP.H, with some modifications) in Listing 1.

Listing 1. The GetDisplayInfo method.

STDMETHOD(GetDisplayInfo)(SCOPEDATAITEM *pScopeDataItem)
   {
      // Some debugging code here   
      if (pScopeDataItem == NULL)
         // an assert
      else
      {
         // Cast the cookie back to a pointer
         CSnapInItem* pItem= (CSnapInItem*) pScopeDataItem->lParam;
         if (pItem == NULL)
            pItem = m_pNode;
               .            
               .
         if (pItem != NULL)
            hr = pItem->GetScopePaneInfo(pScopeDataItem);
      }
      return hr;

   }

You see, instead of determining what to do based on the cookie passed, ATL delegates the responsibility back to the item by calling the CSnapInItem's method through the casted cookie. If you take a closer look at the code in ATLSNAP.H, you'll see that many of the methods simply forward the work to CSnapInItem. This enables you to gather code that was previously scattered in several interfaces to one location: your CSnapInItemImpl-derived class.

Enumerating processes

I wrote a sample snapin that lists all the running processes in the system, along with the DLLs they use. Since I expect that most of you who read this article will develop under Windows NT, I did it the NT way (I also put calls to Unicode version of some API, so it won't run on 95 anyway). To compile this code, you'll need PSAPI.H and PSAPI.LIB, which can be found in your Visual C++ CD. You'll also need PSAPI.DLL, which is in C:\WINNT\system32, if you installed Windows NT 4.0 in C:\WINNT, of course.

Adding node types to your snapin

The sample snapin displays processes and, under each process, the DLLs that the process is using. So at least it needs two additional types of node. I took the simplest approach to adding these new node types:

1.Copy the item class's declaration (in my project, it's CVCDSnap1Data) and paste it onto the same file, just below the original class.

2.Replace all occurrences of CVCDSnap1Data with your class name. I chose to rename the class as CDllNode.

3.Now look at the .cpp file (in this project it's VCDSnap1.cpp). There are some static initializations at the top of the file. Copy them, and simply change the name to a new one. Of course, don't forget to change the GUID too, because this is now a different node. Mine looks like Listing 2.

Listing 2. Static initializations.

static const GUID CDllNodeGUID_NODETYPE = 
{ 0x5af13f00, 0x6144, 0x11d2, { 0xa8, 0xd4, 0xe8, 0xba, 0x5d, 0x0, 0x0, 0x0 } };
const GUID*  CDllNode::m_NODETYPE = &CDllNodeGUID_NODETYPE;
const OLECHAR* CDllNode::m_SZNODETYPE = OLESTR("5AF13F00-6144-11d2-A8D4-E8BA5D000000");
const OLECHAR* CDllNode::m_SZDISPLAY_NAME = OLESTR("VCDSnap1");

const CLSID* CDllNode::m_SNAPIN_CLASSID = &CLSID_VCDSnap1;

4.Copy CVCDSnap1Data member functions into the .cpp file, and replace the original class name with the new name.

5.Do the same for the second node, which will represent running processes. I chose to name it CProcessNode.

Don't forget to modify the .rgs file to put your new node types into the Registry. You need to add your new nodes' GUID below the previous entry made by the Wizard. Add these entries under NodeTypes and NoRemove NodeTypes. They should look like this after you add your entries:

NodeTypes
{
   <Entry made by the Wizard>
   // Below are the entries you add
   {9F5493AB-5C5D-11D2-A8C5-2450A8000000} 
   {5AF13F00-6144-11d2-A8D4-E8BA5D000000} 
}

and

NoRemove NodeTypes
{
   <Entry made by the Wizard>
   // Below are the entries you add
   ForceRemove {9F5493AB-5C5D-11D2-A8C5-2450A8000000}
   {
   }
   ForceRemove {5AF13F00-6144-11d2-A8D4-E8BA5D000000}
   {
   }
}

Each node class in the sample snapin has private members that contain information pertaining to the entity it represents. CVCDSnap1Data, for example, has a std::vector member that holds smart pointers (I used std::auto_ptr) to CProcessNode, one for each process. Each CProcessNode, in turn, has a vector that holds std::auto_ptr<CDllNode>s; again, one for each loaded DLL.

Wizard's play

You know, a Wizard is something you can never really trust. Just like this ATL Snapin Wizard. Some of you might have noticed that something is missing from the .rgs file. Where's the registration code for the VCDSnap1About coclass? It turned out that the Wizard used a  DECLARE_REGISTRY() macro for VCDSnap1About, instead of the usual DECLARE_REGISTRY_RESOURCEID(), as shown in Listing 3.

Listing 3. Registry declaration (wrong).

DECLARE_REGISTRY(CVCDSnap1About, _T("VCDSnap1About.1"), _T("VCDSnap1About.1"), IDS_VCDSNAP1_DESC, THREADFLAGS_BOTH);

What's so important about this? What's so important-and extremely annoying-about this is that using the DECLARE_REGISTRY() macro might cause the debugger to skip your breakpoints in your debugging session, leaving you banging your head, wondering how MMC can call a method without causing the debugger to stop at your breakpoint there. (Now who said the headbanging days are over? Yesterday you had Megadeth, today you have MMC snapins!) This is one heck of a bug that can cost you hours, because this bug only appears if your project path contains long filenames. So when I tried to debug a test project named mmctest, which I put in C:\test, everything worked fine. You can find more information concerning this bug at http://support.microsoft.com/support/kb/articles/q193/5/13.asp.

Always count your children

Let's get this thing working now. The sample snapin will add running processes as children of the snapin root node and loaded DLLs as children of each respective process. To do this, I handle MMCN_EXPAND notification for both CVCDSnap1Data item and CProcessNode items. CProcessNode::Notify() inserts one item for each DLL found in m_vDlls member of CprocessNode, using InsertItem() from the IConsoleNameSpace interface. CVCDSnap1Data is very similar, except that I replaced m_vDlls with m_vProcesses. Listing 4 has the details.

Listing 4. Handling MMCN_EXPAND.

case MMCN_EXPAND:
   {
      CComQIPtr<IConsoleNameSpace, &IID_IConsoleNameSpace> 
         spConsoleNameSpace(spConsole);
      // TODO : Enumerate scope pane items
      // If item need to be expanded
      if(arg) {
         for(int ctr = 0; ctr < m_vDlls.size(); ctr++) {
            SCOPEDATAITEM sdi;
            ::ZeroMemory(&sdi, sizeof(SCOPEDATAITEM));
            sdi.mask = SDI_STR | SDI_PARAM | 
               SDI_IMAGE | SDI_OPENIMAGE | 
               SDI_PARENT;
            m_vDlls[ctr]->GetScopePaneInfo(&sdi);
            sdi.displayname = MMC_CALLBACK;
            sdi.relativeID = param;
            hr = spConsoleNameSpace->InsertItem(&sdi);
            assert(SUCCEEDED(S_OK));
         }
      }
      break;

   }Now you can expand the root node, display running processes, and expand each process further, displaying DLLs it loads. But generally you'll want the result pane to display something more useful. First of all, you need several columns with appropriate headers to display information pertaining to those processes and DLLs. To do this, you have to handle the MMCN_SHOW notification in CProcessNode and CVCDSnap1Data. Listing 5 shows the code for that.

Listing 5. Handling MMCN_SHOW.

case MMCN_SHOW:
   {
      CComQIPtr<IResultData, &IID_IResultData> spResultData(spConsole);
      // TODO : Enumerate the result pane items
      // For process nodes, add four columns
      // to display dll-specific information
      // If arg is TRUE, then we should set columns
      // for the item
      if(arg) {
         hr = spHeader->InsertColumn(
         0, L"Module's Full Path", 
         LVCFMT_LEFT, MMCLV_AUTO); 
         hr = spHeader->InsertColumn(
         1, L"Base Address", 
         LVCFMT_LEFT, MMCLV_AUTO); 
            .
            .
      } else {
         // Must delete items first
         hr = spResultData->DeleteAllRsltItems();
      }
      break;

   }

As you can see, IHeaderCtrl() (spHeader is a smart pointer to IHeaderCtrl) is used to add columns with the relevant titles to the result pane. But that isn't enough. We still have to decide what to put under each column of the result pane. ATL provides just the function to do this: GetResultPaneColInfo(). It takes an int parameter indicating which column MMC is requesting information for and returns an LPOLESTR, which points to the string to be displayed under that column. Listing 6 shows the code for the CDllNode items.

Listing 6. GetResultPaneColInfo().

LPOLESTR CDllNode::GetResultPaneColInfo(int nCol)
{
   // TODO : Return the text for other columns
   switch(nCol) {
   case 0:
      return const_cast<wchar_t*>(
         m_wstrFullPath.c_str());
   case 1:
      return const_cast<wchar_t*>(
         m_wstrBaseAddress.c_str());
         .
         .
   default:
      // should never be here!
      assert(false);
      return NULL;
   }
}

So for column number 0, the string to display is the full path; for column number 1, the base address; and so on.

Adding context menu

Handling an MMC context menu with ATL is very easy. Thanks to the ATL magic, you can implement a context menu the convenient and familiar way-by using menu resources and macros. ATL Wizard automatically provides every MMC snapin project with a menu resource with items arranged according to MMC default-see Figure 4 and Figure 5.

Listing 7 shows what a typical MMC context menu's command map looks like.

Listing 7. A typical command map for a context menu in MMC.

SNAPINMENUID( <menu resource id> )
BEGIN_SNAPINCOMMAND_MAP( <Class representing the item>, FALSE )
   SNAPINCOMMAND_ENTRY(<menu item id>, <handler>)
   SNAPINCOMMAND_RANGE_ENTRY(<id1>, <id2>, <handler>)
      .
END_SNAPINCOMMAND_MAP()

The argument passed to SNAPINMENUID determines which menu resource to use, since usually you'll want to use a different context menu for different types of items. SNAPINCOMMAND_ENTRY maps a menuitem to its handler function, while the RANGE variant maps more than one menuitem to their handler function. The handler method is expected to have the following signature:

HRESULT Func(bool& bHandled, CSnapInObjectRootBase* pObj)

Or for the RANGE variant:

HRESULT Func(UINT nId, bool& bHandled, 
              CSnapInObjectRootBase* pObj)

So you see, adding entries to an MMC context menu is very easy. For the sample snapin, I added some entries to the process node's context menu to change the process's priority class. Here's how to do that: Copy the default menu resource, and rename it. I chose IDR_PROC_MENU for the sample. Put your entries where you want them to appear in the menu. I added one submenu under Task-SetPriority-and three menuitems below it: Hi, Norm, and Lo. Add the appropriate entries to your command map. For this sample, I used the RANGE variant, because it's more convenient than mapping the menu items one by one. The command map will look like Listing 8.


Listing 8. Command map for the new context menu.

SNAPINMENUID( IDR_PROC_MENU )
BEGIN_SNAPINCOMMAND_MAP( CProcessNode, FALSE )
   SNAPINCOMMAND_RANGE_ENTRY(ID_TASK_SET_HI, ID_TASK_SET_LO, OnSetPriority)
END_SNAPINCOMMAND_MAP()

And the handler method looks like this:

STDMETHODIMP CProcessNode::OnSetPriority(
UINT nId, 
bool& bHandled, 
CSnapInObjectRootBase* pObj) {
   switch(nId) {
      // cases here…
   }   
}

Wow! I think you'll agree that this is a great way of dealing with menus. No longer do we have to deal with AddMenuItems, Command, IContextMenuCallback, and so forth. But still, with this convenience comes a little inflexibility. If you compile the snapin now, you'll get an assertion and the context menu won't be shown. We still need to do something with our wizard's magical power.

ATL magic power-up: Enhancing ATL context menu handling

One obvious drawback to ATL's way of handling menus is that it doesn't handle popup menuitems instead of ordinary ones on your menu. The problem is with ATL's implementation of CSnapInItemImpl::AddMenuItems() method (this method handles forwarded calls to IExtendContextMenu::AddMenuItems()). Open ATLSNAP.H and see for yourself: The code there assumes that you'll never add another level of submenu under the top menuitems. That's why the call to AddItem() fails and triggers an assertion.

To handle this problem, I changed the implementation of CSnapInItemImpl::AddMenuItems() a little. You'll see that, unlike the original implementation, my AddMenuItems() will check every menuitem in the menu resource down to the deepest level. So now you can make your context menu exactly the way you want it, no matter how many levels of submenu you add. Change the following piece of code (look for it in CSnapInItemImpl::AddMenuItems()):

menuItemInfo.fMask = MIIM_TYPE | MIIM_STATE | MIIM_ID;
menuItemInfo.fType = MFT_STRING;
TCHAR szMenuText[128];

for (int j = 0; 1; j++)
{
   .
   .
}

to this:

HRESULT hr = TraverseSubMenus(
      hSubMenu, 
      piCallback, 
      insertionID);
assert(SUCCEEDED(hr));

The trick lies with the recursive function TraverseSubMenus(). It simply does what the code in AddMenuItems() used to do: It iterates over each menuitem found under a submenu. The difference is that if it finds that a menuitem is actually a submenu, it won't just stop there and trigger an assertion like the original one. Instead, it will call itself, supplying that submenu as an argument. This process is repeated until it can't find another submenu to check. In that case, it will simply add the menuitem and return. Now this is what I call a power-up! Following is the code for TraverseSubMenus(). I put this function in the private section of CSnapInItemImpl class.

STDMETHOD(TraverseSubMenus)(
      const HMENU hMenu, 
      LPCONTEXTMENUCALLBACK piCallback,
      LONG lInsertionPointID) {
   
   MENUITEMINFO menuItemInfo;
   ::ZeroMemory(&menuItemInfo, sizeof(MENUITEMINFO));
   menuItemInfo.cbSize = sizeof(menuItemInfo);

   HRESULT hr = S_OK;
   TCHAR szMenuText[128];
   for (int j = 0; 1; j++) {
      menuItemInfo.fMask = MIIM_TYPE | MIIM_STATE | 
             MIIM_ID | MIIM_SUBMENU;
      menuItemInfo.fType = MFT_STRING;
      menuItemInfo.cch = 128;
      menuItemInfo.dwTypeData = szMenuText;
      TCHAR szStatusBar[256];

      if (!GetMenuItemInfo(hMenu, j, TRUE, 
                           &menuItemInfo))
         break;
      if (menuItemInfo.fType != MFT_STRING)
         continue;

      this->UpdateMenuState(menuItemInfo.wID, 
                    szMenuText, 
                    &menuItemInfo.fState);
      LoadString(_Module.GetResourceInstance(), 
          menuItemInfo.wID, szStatusBar, 256);

      OLECHAR wszStatusBar[256];
      OLECHAR wszMenuText[128];
      USES_CONVERSION;
      ocscpy(wszMenuText, T2OLE(szMenuText));
      ocscpy(wszStatusBar, T2OLE(szStatusBar));

      CONTEXTMENUITEM contextMenuItem;
      contextMenuItem.strName = wszMenuText;
      contextMenuItem.strStatusBarText = wszStatusBar;
      contextMenuItem.lCommandID = menuItemInfo.wID;
      contextMenuItem.lInsertionPointID = 
         lInsertionPointID;
      contextMenuItem.fFlags = menuItemInfo.fState;
      contextMenuItem.fSpecialFlags = 0;
      // if this is an ordinary item...
      if(menuItemInfo.hSubMenu == NULL) {
         hr = piCallback->AddItem(&contextMenuItem);
      } else {
         // if this is a submenu, call ourselves
         contextMenuItem.lCommandID = 
            (WORD)menuItemInfo.wID;
         contextMenuItem.fFlags |= MF_POPUP;
         contextMenuItem.fSpecialFlags = 
            CCM_SPECIAL_SUBMENU;
         hr = piCallback->AddItem(&contextMenuItem);
         assert(SUCCEEDED(hr));
         
         hr = TraverseSubMenus(
               menuItemInfo.hSubMenu, 
               piCallback,
               contextMenuItem.lCommandID);
      }
      assert(SUCCEEDED(hr));
   }   
   return hr;
}

Needless to say, you must back up ATLSNAP.H before you make any changes to it.

Magical defect: Fixing an ATL bug

You know, you always have to be very careful in dealing with magic, because it can be very deadly if you don't use it correctly. And it's even deadlier if you don't know-or don't care-how the magic actually works. Take, for example, the implementation of the SNAPINCOMMAND_RANGE_ENTRY macro. The original one looks like this:

#define SNAPINCOMMAND_RANGE_ENTRY(id1, id2, func) \
      if (id1 >= nID && nID <= id2) \
      { \
         hr = func(nID, bHandled, pObj); \
         if (bHandled) \
            return hr; \
      }

Can you see what's wrong with the spell-oops, code-above? Well, if you can't, compare it to this:

#define SNAPINCOMMAND_RANGE_ENTRY(id1, id2, func) \
      if (nID >= id1 && nID <= id2) \
      { \
         hr = func(nID, bHandled, pObj); \
         if (bHandled) \
            return hr; \
      }

Can you see it now? I recommend you take a close look at the ATLSNAP.H header file to get a firmer grip on how ATL does its magic. Who knows, you might find yet another bug there! s

   SNAPIN.ZIP at www.pinpub.com/vcd 

Ray F. Djajadinata is a Visual C++ developer and an MCSD who currently works with IntelliSys, a Microsoft Solution Provider in Jakarta. rayfd@hotmail.com, rayf@intellisys.co.id.