Understanding the Object Map

 < Free Open Study > 



The ATL object map contains critical information for each coclass contained in the server. By making use of this map, the global CComModule instance (_Module) is able to iterate over each object hosted by the server and request some action from the coclass. Possible actions include duties such as registration, unregistration, and the creation of the coclass's class factory. The object map is also used to perform some additional duties we have not yet encountered, such as providing access to the object's "category map," performing object initialization and termination as well as retrieving a self-describing registration string. We will be examining each of these duties throughout the remainder of this chapter.

Assume we have an empty server generated with the ATL COM AppWizard named ATLShapesServer.dll. The object map is completely devoid of entries at this point, and looks like the following:

// An empty object map, as generated by the ATL COM AppWizard. BEGIN_OBJECT_MAP(ObjectMap) END_OBJECT_MAP()

Note 

ATL object maps are constructed in the same manner and provide the same basic services for both DLL and EXE servers.

Building an Object Map

The BEGIN_OBJECT_MAP macro (defined in <atlcom.h>) expands to define an array of _ATL_OBJMAP_ENTRY structures. The sole parameter that is sent into BEGIN_OBJECT_MAP becomes the name of the resulting _ATL_OBJMAP_ENTRY array:

// Declare an array of _ATL_OBJMAP_ENTRY structures named x. The name of // the ATL COM AppWizard-generated array is ObjectMap. // #define BEGIN_OBJECT_MAP(x) static _ATL_OBJMAP_ENTRY x[] = { 

The END_OBJECT_MAP macro simply terminates this array by providing all NULL entries for an _ATL_OBJMAP_ENTRY structure. As the CComModule instance iterates over the map, it knows the end of the map has been reached when it encounters all NULL entries:

// Terminate the _ATL_OBJMAP_ENTRY array. #define END_OBJECT_MAP() {NULL, NULL, NULL, NULL,\                        NULL, NULL, NULL, NULL} };

At this point, our server's empty object map expands to nothing more than a one-item array of _ATL_OBJMAP_ENTRY structures named ObjectMap:

// What the BEGIN_OBJECT_MAP and END_OBJECT_MAP macros resolve to. static _ATL_OBJMAP_ENTRY ObjectMap[] = {      {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL} };

The _ATL_OBJMAP_ENTRY Structure

Now for the big question on your mind: What exactly is the _ATL_OBJMAP_ENTRY structure? This ATL structure is used to fully describe the creation and registration characteristics for every coclass contained in the server. Keep in mind that if you do not include a coclass in the object map, clients will not be able to create an instance using the COM library (e.g., CoCreateInstance()) and the ATL framework will not be able to register it into the system registry. From a high level, the _ATL_OBJMAP_ENTRY structure specifies the following information:

  • The CLSID of the coclass.

  • Pointer to the function that registers the type information of this coclass.

  • Pointer to the function used to create the coclass's class factory.

  • Pointer to the function used to create an instance of the coclass.

  • Pointer to the class factory for the coclass.

  • The DWORD ID returned from CoRegisterClassObject().

  • Pointer to the function returning an object's string description.

  • Pointer to the function returning an object's category map.

  • Pointer to the function responsible for initializing the object.

I would guess the functionality of the first six items look familiar to you by now. You will get to know the remaining three by the end of this chapter.

Many of the fields of the _ATL_OBJMAP_ENTRY structure are pointers to a specific function. These functions are either supplied directly by your coclass or from one of its base classes. ATL understands which function to reference when filling a given field based on the function's signature (i.e., the return type, name, and argument list).

Many of these signatures have a corresponding ATL typedef used to identify them. For example, the function in your coclass (or base class) marked with the _ATL_CATMAPFUNC typedef will be used to fill the field pointing to your coclass's GetCategoryMap() function. Here are some of the ATL function signature typedefs, as found in <atlbase.h>:

// Specifies a "creator" function. typedef HRESULT (WINAPI _ATL_CREATORFUNC)(void* pv, REFIID riid, LPVOID* ppv); // Specifies a function returning your object's string description. typedef LPCTSTR (WINAPI _ATL_DESCRIPTIONFUNC)(); // Signature for the function returning your coclass's category map. // As you will see, _ATL_CATMAP_ENTRY is an array used to define // the "category map" for your coclass. typedef const struct _ATL_CATMAP_ENTRY* (_ATL_CATMAPFUNC)();

Armed with this knowledge, here is the full definition of the _ATL_OBJMAP_ENTRY structure, also defined in <atlbase.h>:

// Each entry in the OBJECT_MAP is a _ATL_OBJMAP_ENTRY structure. // Take a note of the various function signature typedefs. struct _ATL_OBJMAP_ENTRY {      const CLSID* pclsid;      HRESULT (WINAPI *pfnUpdateRegistry)(BOOL bRegister);      _ATL_CREATORFUNC* pfnGetClassObject;      _ATL_CREATORFUNC* pfnCreateInstance;      IUnknown* pCF;      DWORD dwRegister;      _ATL_DESCRIPTIONFUNC* pfnGetObjectDescription;      _ATL_CATMAPFUNC* pfnGetCategoryMap;      void (WINAPI *pfnObjectMain)(bool bStarting); };

The following table should help you begin to make some initial sense of these fields. By the way, "pfn" is Hungarian notation for "pointer to a function":

_ATL_OBJMAP_ENTRY Field

Meaning in Life

pclsid

This holds the CLSID of the coclass.

pfnUpdateRegistry

Function used to update the registry with coclass-specific information.

pfnGetClassObject

Function used to create the class factory for the associated coclass.

pfnCreateInstance

Function used to create the coclass identified by the pclsid member.

pCF

Pointer to the class factory for a given coclass.

dwRegister

Numerical identifier returned by CoRegisterClass- Object(). Used to remove a class factory from class table upon EXE shutdown.

pfnGetObjectDescription

Function that provides the "object description" string for the coclass.

pfnGetCategoryMap

Function returning the "category map" for the coclass.

pfnObjectMain

Function used to initialize and terminate an instance of the coclass during the loading and unloading of the server.

The OBJECT_ENTRY Macro

Now that we have a better understanding of the _ATL_OBJMAP_ENTRY structure, we need to know how to fill it with coclass-specific information. As you may have suspected, this task is delegated to additional ATL macros. The most common entry found in the object map is the OBJECT_ENTRY macro, which is used to specify an externally creatable coclass. There is an additional object map macro, OBJECT_ENTRY_NON_CREATE- ABLE, used to define a non-creatable coclass that we examine later in this chapter during our discussion of server object models.

OBJECT_ENTRY takes two parameters: the CLSID of the coclass and the ATL-based class that implements it. Assume ATLShapesServer.dll contains the CoHexagon simple object. The object map would now look like the following:

// Our server contains a single creatable coclass named CoHexagon. BEGIN_OBJECT_MAP(ObjectMap)      OBJECT_ENTRY(CLSID_CoHexagon, CoHexagon) END_OBJECT_MAP()

The OBJECT_ENTRY macro is defined in <atlcom.h> and is used to populate the fields of the _ATL_OBJMAP_ENTRY structure, given a C++ implementation class and the corresponding CLSID. Thus, each OBJECT_ENTRY entry increases the size of the _ATL_OBJMAP_ENTRY array by one. Here is the definition of the OBJECT_ENTRY macro, annotated with comments illustrating which function in the class (CoHexagon) sets the correct field of the _ATL_OBJMAP_ENTRY structure:

// The fields of an _ATL_OBJMAP_ENTRY structure are set by the // OBJECT_ENTRY macro. #define OBJECT_ENTRY(clsid, class) { \ &clsid, \                                             // [pclsid] class::UpdateRegistry, \                              // [pfnUpdateRegistry] class::_ClassFactoryCreatorClass::CreateInstance, \   // [pfnGetClassObject] class::_CreatorClass::CreateInstance, \               // [pfnCreateInstance] NULL, \                                               // Holds IUnknown* field. 0, \                                                  // Holds dwRegister field. class::GetObjectDescription, \                        // [pfnGetObjectDescription] class::GetCategoryMap, \                              // [pfnGetCategoryMap] class::ObjectMain },                                  // [pfnObjectMain]

As you can see, the scope resolution operator is used to scope the class parameter to the correct function. However, if you examine the definition of CoHexagon you will fail to find any functions named GetObjectDescription(), UpdateRegistry(), ObjectMain(), and so forth:

// The functions required by the OBJECT_ENTRY macro appear to be missing... class ATL_NO_VTABLE CCoHexagon :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass<CCoHexagon, &CLSID_CoHexagon>,      public IDraw { public:      CCoHexagon()      {      } DECLARE_REGISTRY_RESOURCEID(IDR_COHEXAGON) DECLARE_PROTECT_FINAL_CONSTRUCT() BEGIN_COM_MAP(CCoHexagon)      COM_INTERFACE_ENTRY(ICoHexagon) END_COM_MAP() }; 

Rest assured that these functions do exist; however, they are typically produced using a number of ATL macros. Some of these macros are specified directly in your coclass, while many others are inherited from one of your base classes. One of the nice things about the ATL framework is its ease of extendability. If you do not like the default support specified by one of your base classes, you are free to override that behavior, typically by inserting an ATL macro or two. Our next task is to investigate the following:

  • Which ATL macros expand to define a given function referenced by an object map?

  • Where are these macros initially declared?

  • Can we override the default macros and when might we want to do so?

As we examine the functions of the ATL object map, be very aware that the ATL framework always provides you with default implementations for each. Much of the time you will want to use this default functionality, allowing you to quickly turn your attention to defining and implementing COM interfaces.

However, ATL does provide a way to override this default behavior to tailor-make your latest COM opus. That said, let's see exactly how a class such as CoHexagon provides the UpdateRegistry() method.

Specifying the UpdateRegistry() Method

The pfnUpdateRegistry field of the _ATL_OBJMAP_ENTRY structure points to your coclass's UpdateRegistry() method:

// Your class's UpdateRegistry() function is placed into the pfnUpdateRegistry // field. #define OBJECT_ENTRY(clsid, class) { ... class::UpdateRegistry, \                          // [pfnUpdateRegistry] ...

The ATL framework will access this function whenever your server is told to register or unregister itself. Although you cannot find a method named UpdateRegistry() within CoHexagon, you will find the following default macro:

// The default implementation of UpdateRegistry() is specified using the // DECLARE_REGISTRY_RESOURCEID macro. class ATL_NO_VTABLE CCoHexagon :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass<CCoHexagon, &CLSID_CoHexagon>,      public IDraw { ...      DECLARE_REGISTRY_RESOURCEID(IDR_COHEXAGON) ... }; 

This macro is one of four possible ATL registration macros that expand to an implementation of UpdateRegistry(), all of which are defined in <atlcom.h>. Here is a rundown of each registration macro:

ATL Registration Macro

Meaning in Life

DECLARE_REGISTRY_RESOURCEID

The default macro. Used to register your coclasses based on the resource ID of the object's binary RGS file.

DECLARE_REGISTRY_RESOURCE

Also registers your coclass based on your RGS file. Takes a string name, rather than the resource ID.

DECLARE_NO_REGISTRY

Used to prevent any registration of the coclass. Often used for "non-creatable" coclasses.

DECLARE_REGISTRY

Bypasses the RGS file, and allows you to send in a minimal set of registration parameters.

Examining the ATL Registration Macros

DECLARE_REGISTRY_RESOURCEID takes a single parameter: the resource ID of the binary RGS file. Recall that all of your server's binary RGS resources are listed under the REGISTRY folder of the ResourceView tab, as shown in Figure 9-1:


Figure 9-1: The registration macros typically make use of resource IDs.

A closely related macro named DECLARE_REGISTRY_ RESOURCE takes the string name of the resource rather than the numerical resource ID. Both macros ultimately call CComModule::UpdateRegistryFromRe- source() which runs the registry script, registering or unregistering the coclass accordingly, based on the BOOL parameter. Here are the expansions of these first two ATL registration macros:

// Take the resource ID and pass into CComModule for processing. #define DECLARE_REGISTRY_RESOURCEID(x)\      static HRESULT WINAPI UpdateRegistry(BOOL bRegister)\      {\           return _Module.UpdateRegistryFromResource(x, bRegister);\      } // Take the string name of the resource and pass into CComModule for processing. #define DECLARE_REGISTRY_RESOURCE(x)\      static HRESULT WINAPI UpdateRegistry(BOOL bRegister)\      {\           return _Module.UpdateRegistryFromResource(_T(#x), bRegister);\      }

The third ATL registry macro, DECLARE_NO_REGISTRY, is implemented as a simple no-op. This macro is commonly used when you have a "non-creatable" coclass and do not wish to specify any registry information (therefore preventing clients from directly instantiating any objects). Here is the definition of DECLARE_NO_REGISTRY:

// Defines a no-op version of UpdateRegistry. #define DECLARE_NO_REGISTRY()\      static HRESULT WINAPI UpdateRegistry(BOOL /*bRegister*/)\      {return S_OK;} 

Finally, we have the DECLARE_REGISTRY macro, which allows you to avoid registry scripts altogether, and specify the minimal set of registry information for a coclass (the CLSID, ProgIDs, object description string, and the threading model identifier).

This approach is not really recommended, as ATL's registry scripting language provides a much more fluid manner to register your server. Furthermore, most COM servers need more detailed registration information than the DECLARE_REGISTRY macro provides. Nevertheless, here is the definition of DECLARE_REGISTRY:

// If you want to bypass RGS files, you can use the DECLARE_REGISTRY // macro. #define DECLARE_REGISTRY(class, pid, vpid, nid, flags)\      static HRESULT WINAPI UpdateRegistry(BOOL bRegister)\      {\           return _Module.UpdateRegistryClass(GetObjectCLSID(), pid, vpid, nid,\                flags, bRegister);\      }

By default, the ATL Object Wizard inserts the DECLARE_REGISTRY_RESOURCEID macro into your coclass header file. If you wish to use any other variations, you only need to substitute the desired macro in your coclass's header file. For example, if we wish to prevent the ATL framework from adding registry information for our CoHexagon class, we could write the following:

// To use a stock UpdateRegistry() method, simply define a given registry macro // in your coclass's header file. class ATL_NO_VTABLE CCoHexagon :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass<CCoHexagon, &CLSID_CoHexagon>,      public IDraw { ...      // Nope... DECLARE_REGISTRY_RESOURCEID(IDR_COHEXAGON)      DECLARE_NO_REGISTRY() ... };

Customizing UpdateRegistry()

If you have some special registration needs, you are of course able to provide your own custom version of UpdateRegistry(). The only requirement is that your UpdateRegistry() implementation has the same function signature as found in the stock macros. Typically, you will want to be sure and call CComModule::UpdateRegistryFromResource() before you exit your custom implementation of UpdateRegistry(), allowing ATL to perform the default registration duties based on your RGS file. Here is a simple example:

// To provide your own implementation of UpdateRegistry(), remove all ATL // registry macros and write it yourself. class ATL_NO_VTABLE CCoHexagon :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass<CCoHexagon, &CLSID_CoHexagon>,      public IDraw { ...      static HRESULT WINAPI UpdateRegistry(BOOL b)      {           if (b)                MessageBox(NULL, "Registering...", "Hey!", MB_OK);           else                MessageBox(NULL, "Unregistering...", "Hey!", MB_OK);           // Perform default registration based on RGS file.           return _Module.UpdateRegistryFromResource(IDR_COHEXAGON, b);      } ... };

When is UpdateRegistry() Called?

To finish up our discussion of this first field of the _ATL_OBJMAP_ENTRY structure, we need to understand when UpdateRegistry() is called by the ATL framework. Recall that the ATL COM AppWizard has implemented DllRegisterServer() and DllUnregister- Server() with the help of CComModule:

// DLL exports which provide self-registration support. //STDAPI DllRegisterServer(void) {     return _Module.RegisterServer(TRUE);     } STDAPI DllUnregisterServer(void) {     return _Module.UnregisterServer(TRUE);     }

EXE servers will call RegisterServer() and UnregisterServer() based on the value of the LPTSTR command line parameter. The FindOneOf() helper function will scan a string for a set of tokens:

// EXE servers and the UpdateRegistry() invocation. extern "C" int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR lpCmdLine, int /*nShowCmd*/) {      TCHAR szTokens[] = _T("-/");      LPCTSTR lpszToken = FindOneOf(lpCmdLine, szTokens); ...      if (lstrcmpi(lpszToken, _T("UnregServer"))==0)      {           nRet = _Module.UnregisterServer(TRUE);      ...      }      if (lstrcmpi(lpszToken, _T("RegServer"))==0)      {           nRet = _Module.RegisterServer(TRUE);      ...      } ... }

CComModule::RegisterServer() and CComModule::UnregisterServer() make calls to ATL helper functions (AtlModuleRegisterServer() and AtlModuleUnregisterServer() respectively) which iterate over the object map, calling UpdateRegistry() for each coclass in the object map:

// Your server's DllRegisterServer() method calls CComModule::RegisterServer() // which in turn calls AtlModuleRegisterServer() which triggers each coclass's // pfnUpdateRegistry method. ATLINLINE ATLAPI AtlModuleRegisterServer(_ATL_MODULE* pM,                BOOL bRegTypeLib, const CLSID* pCLSID) { ...      // Loop over the object map and call each class's UpdateRegistry() method.      hRes = pEntry->pfnUpdateRegistry(TRUE); ... } 

Quite a winding path, yes? The good news is, all you need to worry about is providing an implementation of UpdateRegistry() in your coclass, and ATL will take care of the rest. As well, the UpdateRegistry() implementation defined by the DECLARE_REGISTRY_RE- SOURCEID macro is typically exactly what you require.

Later in our lab work, we will create a customized UpdateRegistry() method. Until then, let's move on and figure out how the pfnGetClassObject field is set, allowing ATL to create the class factory for your object. But first, a word about ATL creator classes.

Understanding ATL COM Creators

The ATL framework creates your class factories and coclasses in slightly different ways based off a number of COM influences. For example, in ATL, your coclasses are created based on your object's level of thread awareness and aggregation support. To help streamline the differences between creating aggregated versus non-aggregated objects, traditional class factories versus custom class factories (and so on), ATL provides a level of abstraction called a creator class. An ATL creator will (for lack of a better word) create a given ATL object based off all the possible combinations of COM influences. In this chapter we will see the use of the CComCreator<> and CComCreator2<> creator templates.

Each ATL creator class has a single static method named CreateInstance(), which is used to assemble the object. The implementation of this method is determined by the configuration of the class under construction. A handy typedef will be used to represent the layout of the ATL creator, which helps clean up the object definition and reduces source code clutter. For example, the fields in the _ATL_OBJMAP_ENTRY structure that point to the function used to create a class factory and the related coclass are both called CreateInstance(); however, each is colored by the correct creator typedef (_ClassFactoryCreatorClass or _CreatorClass):

// _ClassFactoryCreatorClass and _CreatorClass are typedefs to the correct ATL // creator class. class::_ClassFactoryCreatorClass::CreateInstance, \    // [pfnGetClassObject] class::_CreatorClass::CreateInstance, \                // [pfnCreateInstance]

Specifying the _ClassFactoryCreatorClass::CreateInstance() Method

Now that we have a feeling for these ATL creator classes, let's see one in action. The pfnGetClassObject field of the _ATL_OBJMAP_ENTRY structure points to the function used to create the class factory for a given coclass. The OBJECT_ENTRY macro sets this field using the correct creator typedef provided by your class, hence class::_ClassFactoryCreatorClass::CreateInstance:

// The _ClassFactoryCreatorClass::CreateInstance() function is // held by the pfnGetClassObject field. #define OBJECT_ENTRY(clsid, class) { ... class::_ClassFactoryCreatorClass::CreateInstance, \      // [pfnGetClassObject] ...

If you have a DLL server, the ATL framework will access pfnGetClassObject during calls to DllGetClassObject() via CComModule::GetClassObject(). GetClassObject() in turn calls an ATL helper function named AtlModuleGetClassObject():

// DllGetClassObject is implemented by a call to CComModule::GetClassObject() // which calls AtlModuleGetClassObject(). This ATL helper function accesses the // pfnGetClassObject field of the _ATL_OBJMAP_ENTRY structure. ATLINLINE ATLAPI AtlModuleGetClassObject(_ATL_MODULE* pM, REFCLSID rclsid,                                      REFIID riid, LPVOID* ppv) { ...      if (pEntry->pCF == NULL)           hRes = pEntry->pfnGetClassObject(pEntry->pfnCreateInstance,                IID_IUnknown, (LPVOID*)&pEntry->pCF); ... }

EXE servers access pfnGetClassObject via CComModule::RegisterClassObject(), which in turn accesses pfnGetClassObject to post class factories to the class table:

// ATL EXE servers access pfnGetClassObject via RegisterClassObject(). extern "C" int WINAPI _tWinMain(HINSTANCE hInstance,    HINSTANCE /*hPrevInstance*/, LPTSTR lpCmdLine, int /*nShowCmd*/) { ...      hRes = _Module.RegisterClassObjects(CLSCTX_LOCAL_SERVER,           REGCLS_MULTIPLEUSE | REGCLS_SUSPENDED); ... }

Now that we have seen when this method is called, the next question is where this method is hiding. Your class's _ClassFactoryCreatorClass::CreateInstance() method is provided by a set of ATL class factory macros. Here is a rundown of the most common:

ATL Class Factory Creation Macro

Meaning in Life

DECLARE_CLASSFACTORY

The default macro. Provides a generic class factory implementing IClassFactory.

DECLARE_CLASSFACTORY_EX

Allows you to specify a custom class factory implementation.

DECLARE_CLASSFACTORY2

Allows you to create a licensed class factory supporting IClassFactory2.

DECLARE_CLASSFACTORY_SINGLETON

Allows you to construct a class factory which hands out multiple references to a single coclass.

DECLARE_CLASSFACTORY_AUTO_THREAD

Allows an EXE server to span objects which reside in multiple apartments.

ATL's Default Class Factory Support

To begin, recall that any creatable ATL class must derive from CComCoClass<>:

// CComCoClass defines a default implementation of // _ClassFactoryCreatorClass::CreateInstance(). class ATL_NO_VTABLE CCoHexagon :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass<CCoHexagon, &CLSID_CoHexagon>,      public IDraw { ... };

Among other things, this ATL template provides a default class factory. If we examine the definition of CComCoClass, we will find the following macro:

// The DECLARE_CLASSFACTORY macro expands to provide an implementation // for _ClassFactoryCreatorClass::CreateInstance template <class T, const CLSID* pclsid = &CLSID_NULL> class CComCoClass { public:      DECLARE_CLASSFACTORY() ... };

Exactly what this macro expands to will depend upon the type of COM server you are working with. Recall that EXE servers typically do not want the class factory's reference count to influence the termination of the server, as the server is responsible for shutting itself down. DLL servers, on the other hand, must allow their reference count to influence the unloading of the server, as the DLL should not be unloaded until all clients are finished with all interface pointers.

In short, a class factory will be implemented in slightly different ways based off the type of component housing. ATL takes care of the differences between EXE and DLL class factories using the CComObjectCached<> and CComObjectNoLock<> templates, using the correct COM creator.

DECLARE_CLASSFACTORY and DECLARE_CLASSFACTORY_EX

The DECLARE_CLASSFACTORY macro will first make a preprocessor check for the _USRDLL flag. If _USRDLL has been defined by your project, you are building an in-proc server and therefore want ATL to create a class factory implementation that takes reference counting into account, using CComObjectCached<>.

If _USRDLL is not defined you are creating an EXE, and ATL will create a class factory which will not affect the server's object count, using CComObjectNoLock<>. As you can see, DECLARE_CLASSFACTORY expands to call another ATL class factory macro named DECLARE_CLASSFACTORY_EX:

// The class that implements the class factory depends on the _USRDLL flag. #define DECLARE_CLASSFACTORY()      DECLARE_CLASSFACTORY_EX(CComClassFactory) // The class factories are living in a DLL, so use CComObjectCached<>. #if defined(_WINDLL) | defined(_USRDLL) #define DECLARE_CLASSFACTORY_EX(cf) \ typedef CComCreator< CComObjectCached< cf > > _ClassFactoryCreatorClass; #else // The class factories are living in an EXE, so use CComObjectNoLock<>. #define DECLARE_CLASSFACTORY_EX(cf) \ typedef CComCreator< CComObjectNoLock< cf > > _ClassFactoryCreatorClass; #endif 

Notice that _ClassFactoryCreatorClass is the typedef of the creator class creating your class factory, based on its reference counting needs. ATL's CComClassFactory class is passed into the DECLARE_CLASSFACTORY_EX macro, which provides a default class implementing IClassFactory (more on this in a minute). The CComCreator<> template provides the actual static CreateInstance() method, which ends up being the function pointed to by the pfnGetClassObject field.

So to recap, DECLARE_CLASSFACTORY will expand in one of two ways based on the type of component home (DLL or EXE). CComCreator<> provides the CreateInstance() method. CComObjectCached<> (DLL) and CComObjectNoLock<> (EXE) are used to specify the reference counting behavior for your class factory:

// pfnGetClassObject is filled by one of these two implementations of // _ClassFactoryCreatorClass::CreateInstance(). // DLL version. CComCreator< CComObjectCached< CComClassFactory > >::CreateInstance()  // EXE version. CComCreator< CComObjectNoLock< CComClassFactory > >::CreateInstance()

CComClassFactory: The Default Implementation of IClassFactory

CComClassFactory provides a default implementation of the IClassFactory interface. If you examine this class (<atlcom.h>) you will see it derives from IClassFactory, implements IClassFactory::CreateInstance() and IClassFactory::LockServer(), and provides a COM_MAP with an entry for IClassFactory. We will see what the SetVoid() and the _ATL_CREATORFUNC* member variable (m_pfnCreateInstance) are used for in just a moment. Here is the formal definition of CComClassFactory:

// CComClassFactory provides a standard implementation of IClassFactory. class CComClassFactory :      public IClassFactory,      public CComObjectRootEx<CComGlobalsThreadModel> { public:      BEGIN_COM_MAP(CComClassFactory)           COM_INTERFACE_ENTRY(IClassFactory)      END_COM_MAP()      // IClassFactory methods      STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid, void** ppvObj);      STDMETHOD(LockServer)(BOOL fLock);      // SetVoid() will assign CComClassFactory::m_pfnCreateInstance.      void SetVoid(void* pv);      // Used to store the pointer to the _ATL_OBJMAP_ENTRY      // pfnCreateInstance field. Thus! Your class factory has a pointer to      // the function used to create the correct coclass!      _ATL_CREATORFUNC* m_pfnCreateInstance; };

Implementing CComClassFactory::LockServer()

CComClassFactory::LockServer() is implemented with help from your global CComModule instance. As you would expect, the server's active object count needs to be adjusted based off the value of the incoming BOOL parameter:

// CComClassFactory::LockServer() adjusts the server's object count. STDMETHOD(LockServer)(BOOL fLock) {      if (fLock)           _Module.Lock();      else           _Module.Unlock();      return S_OK; }

Implementing CComClassFactory::CreateInstance()

CComClassFactory::CreateInstance() is implemented to operate on the internal m_pfnCreateInstance member variable. This public member variable points to the function used to create the corresponding coclass:

// CComClassFactory::CreateInstance() calls the function which creates the coclass, held // in the m_pfnCreateInstance data member. STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid,                               void** ppvObj) {      ATLASSERT(m_pfnCreateInstance != NULL);      HRESULT hRes = E_POINTER;      if (ppvObj != NULL)      {           *ppvObj = NULL;           // Can't ask for anything other than IUnknown when aggregating           if ((pUnkOuter != NULL) && !InlineIsEqualUnknown(riid))           {                hRes = CLASS_E_NOAGGREGATION;           }           else                hRes = m_pfnCreateInstance(pUnkOuter, riid, ppvObj);      }           return hRes; }

When the CComCreator<> class creates an instance of CComClassFactory, it will set this member variable to the m_pfnCreateInstance field of your _ATL_OBJMAP_ENTRY structure using the SetVoid() helper function. Recall that pfnCreateInstance is the field that creates the coclass itself (CoHexagon). Therefore, this data member points to the creator function of your coclass! Here is the relevant code behind CComCreator<>::Create- Instance():

// CComCreator:: CreateInstance() calls SetVoid(), which sets // CComClassFactory::m_pfnCreateInstance. template <class T1> class CComCreator { public:      static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, LPVOID* ppv)      {           ATLASSERT(*ppv == NULL);           HRESULT hRes = E_OUTOFMEMORY;           T1* p = NULL;           ATLTRY(p = new T1(pv))               if (p != NULL)           {                p->SetVoid(pv);                p->InternalFinalConstructAddRef();                hRes = p->FinalConstruct();                p->InternalFinalConstructRelease();                if (hRes == S_OK)                     hRes = p->QueryInterface(riid, ppv);                if (hRes != S_OK)                     delete p;           }           return hRes;      } };

Again, CComClassFactory::SetVoid() sets the m_pfnCreateInstance data member to the function used to create the associated coclass (the default implementation found in CComObjectRootBase does nothing):

// CComClassFactory overrides the default SetVoid() method defined in // CComObjectRootBase. void SetVoid(void* pv) {      m_pfnCreateInstance = (_ATL_CREATORFUNC*)pv; }

Finally, we can see what CComClassFactory::CreateInstance() is all about. It calls a function to create your coclass, which happens to be the same function pointed to in the m_pfnCreateInstance field of the _ATL_OBJMAP_ENTRY structure.

Now the good news is your ATL coclasses will have a class factory supplied for free by the DECLARE_CLASSFACTORY macro defined in CComCoClass. It is edifying to have a better understanding of what ATL is doing under the hood; however, you can live a happy and productive life without it.

Before we move on, let's examine how we can override this default ATL class factory and why we might want to do so.

Licensed Class Factories and IClassFactory2

DECLARE_CLASSFACTORY provides a standard class factory for your coclass via CComClassFactory. If you wish to supply an alternative class factory for your coclass, you may override the default behavior specified in CComCoClass<> by supplying an alternative class factory macro in your coclass definition.

For example, sometimes you want to make money on your hard-won coclasses. Imagine you just created a fantastic new ActiveX control and want to sell it for $20 a pop. You would like to ensure that a given developer couldn't create the object until he or she can show proof of purchase. For this very reason COM allows you to implement a class factory using the IClassFactory2 interface. This interface derives from IClassFactory, and adds additional methods which allow you to locate and test for a valid license file (*.lic). As this support is most common among ActiveX controls, the IDL definition of IClassFactory2 is found in <ocidl.idl>:

// Implemented by a class factory which performs license checks // before creating the coclass. interface IClassFactory2 : IClassFactory {      HRESULT GetLicInfo([out] LICINFO * pLicInfo);      HRESULT RequestLicKey([in] DWORD dwReserved, [out] BSTR * pBstrKey );      HRESULT CreateInstanceLic([in] IUnknown * pUnkOuter,                             [in] IUnknown * pUnkReserved,                             [in] REFIID riid,                             [in] BSTR bstrKey,                             [out, iid_is(riid)] PVOID * ppvObj ); };

ATL provides the DECLARE_CLASSFACTORY2 macro to create a class factory deriving from (and implementing) IClassFactory2. This macro still leverages the DECLARE_CLASSFACTORY_EX macro to expose the correct CComCreator; however, this time, your class factory is represented by CComClassFactory2<>:

// Used to define a class factory with support for licensing. #define DECLARE_CLASSFACTORY2(lic) \      DECLARE_CLASSFACTORY_EX(CComClassFactory2<lic>)

The lic parameter is a C++ class (provided by you) that implements the licensing validation code. We will see an example of creating a licensed class factory in our discussion of ActiveX controls (Chapter 14), but for the sake of illustration, assume we have such a class named CMyLic. We could then create a licensed class object as so:

// CoHexagon: You've got to pay to play. class ATL_NO_VTABLE CCoHexagon :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass<CCoHexagon, &CLSID_CoHexagon>,      public IDraw,      public IShapeEdit,      public IErase,      public IShapeID { public:      ...      DECLARE_CLASSFACTORY2(CMyLic)      ... };

Singleton Class Factories

Sometimes you may wish to design a class factory that creates a single instance of a coclass, regardless of how many clients may request a new instance. To do so would require tweaking the code behind IClassFactory::CreateInstance() such that all interface pointers are returned from a single object as opposed to a new object every time. ATL provides the DECLARE_CLASSFACTORY_SINGLETON macro for this very purpose.

When this macro expands, your class factory will be implemented using CComClassFactorySingleton<>:

// Create a singleton. #define DECLARE_CLASSFACTORY_SINGLETON(obj)\      DECLARE_CLASSFACTORY_EX(CComClassFactorySingleton<obj>)

CComClassFactorySingleton<> contains a private data member of type CComObjectGlobal<>. This data member represents the single instance of the associated coclass (for example, CoHexagon). Here is the formal definition. Notice that all references to the object are handed out using the single instance of the CComObjectGlobal<> member variable:

// Singletons are represented by CComClassFactorySingleton<>. template <class T> class CComClassFactorySingleton : public CComClassFactory { public:      void FinalRelease()      {           CoDisconnectObject(m_Obj.GetUnknown(), 0);      }      // IClassFactory      STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid, void** ppvObj)      {           HRESULT hRes = E_POINTER;           if (ppvObj != NULL)           {                *ppvObj = NULL;                // aggregation is not supported in Singletons.                ATLASSERT(pUnkOuter == NULL);                if (pUnkOuter != NULL)                     hRes = CLASS_E_NOAGGREGATION;                else                {                     if (m_Obj.m_hResFinalConstruct != S_OK)                          hRes = m_Obj.m_hResFinalConstruct;                     else                          hRes = m_Obj.QueryInterface(riid, ppvObj);                }           }           return hRes;      }      CComObjectGlobal<T> m_Obj;     // A single instance of the coclass. };

While a singleton might sound perfect in many situations, beware. Singleton class factories living in a DLL are only unique on a per-process basis. Thus, if ten clients are using a CoHexagon created by a singleton, this does not imply that all ten processes are using the same instance. EXE singletons are only unique for a given machine.

A better approach is to have a normal class factory that creates any number of new objects, and allow these objects to share instance data by use of some global utility object. This would require you to implement IClassFactory in such a way that when the class factory creates a new instance of the coclass, some secondary initialization is performed to "prep" the object to look like all the other existing coclasses. How, then, can we specify our own implementation of IClassFactory? Glad you asked.

A Class Factory of Your Very Own

If you want to create a customized class factory that will function within the ATL framework, you may use the DECLARE_CLASSFACTORY_EX macro and specify the name of your custom class implementing IClassFactory. This class will need to derive from CComClassFactory. As an example, if we wanted to create an implementation of IClassFactory that keeps a running count of how many CoHexagon objects have been created, we may create a custom class factory, declared as so:

// Every time we create a CoHexagon, we will increase a global counter. class CMyCustomCF : public CComClassFactory {      ...      STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid,                               void** ppvObj)      {           ...           ++g_HexCount;           ...      } };

We would then specify this custom class factory in cohexagon.h using the DECLARE_CLASSFACTORY_EX macro:

// Providing a custom class factory for CoHexagon. class ATL_NO_VTABLE CCoHexagon :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass<CCoHexagon, &CLSID_CoHexagon>,      public IDraw,      public IShapeEdit,      public IErase,      public IShapeID { public:      ...      DECLARE_CLASSFACTORY_EX(CMyCustomCF)      ... };

To sum up ATL's class factory support, ATL coclasses automatically inherit a standard class factory from CComCoClass<>, via the DECLARE_CLASSFACTORY macro. This macro expands to parameterize the implementation of CComCreator::CreateInstance() based off the class implementing IClassFactory(2) (CComClassFactory, CComClass- Factory2, CComClassFactorySingleton), as well as the correct module locking support (CComObjectCached or CComObjectNoLock).

If you wish to replace this default support, you may specify any of the alternative macros (DECLARE_CLASSFACTORY_EX, DECLARE_CLASSFACTORY_SINGLETON, or DECLARE_CLASSFACTORY2) in your header file. As with most things, just because you can does not mean you have to. The default class factory will serve your needs (almost) every time.

Specifying the _CreatorClass::CreateInstance() Method

The _ATL_OBJMAP_ENTRY structure contains another parameterized CreateInstance() method, again using a COM creator. This method, marked by the pfnCreateInstance field, is used to specify which function to call to create the actual coclass (recall that your class factory holds onto this function pointer):

// Your coclass's CreateInstance() function is placed into the pfnCreateInstance // field. #define OBJECT_ENTRY(clsid, class) { ...     class::_CreatorClass::CreateInstance, \                // [pfnCreateInstance] ...

You receive a default implementation of _CreatorClass::CreateInstance() from CComCoClass<> using the DECLARE_AGGREGATABLE macro:

// DECLARE_AGGREGATABLE is defined in CComCoClass to implement // the function used to create your coclass. template <class T, const CLSID* pclsid = &CLSID_NULL> class CComCoClass { public: ...      DECLARE_AGGREGATABLE(T) ... };

Beyond this default macro, ATL provides a number of alternatives. Here are some of the most common:

ATL Coclass Creation Macro

Meaning in Life

DECLARE_AGGREGATABLE

Specifies that your coclass may work as an aggregated object, but is not required to.

DECLARE_NOT_AGGREGATABLE

Specifies that your coclass cannot work as an aggregated object.

DECLARE_ONLY_AGGREGATABLE

Specifies that your object cannot work as a stand-alone object, and must be part of an aggregate.

The DECLARE_AGGREGATABLE Macro and CComCreator2<>

DECLARE_AGGREGATABLE is defined in <atlcom.h>. As you recall, an ATL coclass needs to specify how it wishes to be created with regard to aggregation. The DECLARE_AGGREGATABLE macro states that your coclass can work as an aggregated object or a stand-alone, non-aggregated object. This macro makes use of the CComCreator2<> template, where the "2" signals the two possible creation behaviors (aggregated or not).

If your coclass is working as an inner object, your class will be passed into CComAggObject<>, which is then passed into CComCreator<>. This will implement the necessary hooks to allow your coclass to work as an aggregate:

// Your object will work as an inner object. CComCreator< CComAggObject< x > >

If your object will not work as an aggregate, you will be sent into the old familiar CComObject<> template:

// Your object will not be an inner object. CComCreator< CComObject< x > >

To allow one or the other of these creations to take place, each of the CComCreator<> classes becomes a parameter to CComCreator2<>. The end result is a typedef to _CreatorClass::CreateInstance(). Here then is the DECLARE_AGGREGATABLE macro:

// The default aggregation macro allows your coclass to work as an aggregate // (CComCreator< CComAggObject< > > ) or not // (CComCreator< CComObject< > > ) #define DECLARE_AGGREGATABLE(x) public:\      typedef CComCreator2<     CComCreator< CComObject< x > >, \                          CComCreator< CComAggObject< x > > > \      _CreatorClass; 

More often than not, this is exactly the behavior you want, and you can rest assured the ATL framework has provided you with the support your COM objects need.

Specifying Alternative Aggregation Support

Like the class factory macros, if you wish to change the level of aggregation support in your coclass, you have options. If you know which level of aggregation support you wish to specify at the time you are using the ATL Object Wizard, you may say so using the Attributes tab. Based on your selection, ATL will either leverage the default CComCoClass aggregation macro, or insert one of the following ATL macros in your coclass header file. If you change your mind after you have used the ATL Object Wizard, you may simply add the correct macro into your coclass after the fact.

The default option used to specify your level of aggregation is Yes, and therefore you automatically inherit the functionality of the DECLARE_AGGREGATABLE macro.

Choosing No will add DECLARE_NOT_AGGREGATABLE to your coclass header file, and Only will insert DECLARE_ONLY_AGGREGATABLE.

Specifying Non-Aggregatable Classes

If you select DECLARE_NOT_AGGREGATABLE, your coclass will not work as an aggregate and can only function as a stand-alone object. Here, your coclass will be created using either CComObject<> or CComFailCreator<> based on whether your class is being asked to function as an aggregate. CComFailCreator<> wraps up the logic for signaling the failed creation of a coclass:

// If you want to prevent your coclass from functioning as an aggregate, use the // DECLARE_NOT_AGGREGATABLE macro. #define DECLARE_NOT_AGGREGATABLE(x) public:\      typedef CComCreator2< CComCreator< CComObject< x > >, \           CComFailCreator<CLASS_E_NOAGGREGATION> > \                _CreatorClass;

Specifying Only-Aggregatable Classes

The DECLARE_ONLY_AGGREGATABLE macro specifies that your coclass cannot function as a stand-alone object and must be aggregated. If you select this option, CCom- Creator2 will use CComAggObject<> for your aggregated implementation, and return E_FAIL (via CComFailCreator<>) if it is ever asked to work as a stand-alone object.

// If you wish to have a coclass which can only work as an aggregate, // use the DECLARE_ONLY_AGGREGATABLE macro. #define DECLARE_ONLY_AGGREGATABLE(x) public:\      typedef CComCreator2< CComFailCreator<E_FAIL>, \           CComCreator< CComAggObject< x > > > \                _CreatorClass;

If you wish to change this default behavior, simply add an alternative aggregation macro to your coclass. For example, if we wish to ensure that CoHexagon can never be aggregated by another object, update the class definition as so:

// Change the default _CreatorClass::CreateInstance() method to ensure no one // can ever aggregate CoHexagon class ATL_NO_VTABLE CCoHexagon : ... { ...      DECLARE_NOT_AGGREGATABLE(CCoHexagon) ... };

To sum up the pfnCreateInstance field of the _ATL_OBJMAP_ENTRY structure, pfnCreateInstance points to the CreateInstance() method configured by the correct COM creator via CComCreator2<>.

The creator understands exactly how to create your object with regard to the issue of aggregation. By default, ATL will provide you with the behavior defined by the DECLARE_AGGREGATABLE macro. If you want a new set of creators, just plug in the desired aggregation macro in your derived class.

Specifying the GetObjectDescription() Method

Those last two fields of the _ATL_OBJMAP_ENTRY structure may have caused your head to explode. This is a normal response. However, with that bit of nastiness behind us, we can look at some more intuitive function entry points. GetObjectDescription() is a method used to return a string which identifies your coclass, and is represented by the pfnGetObjectDescription field of the _ATL_OBJMAP_ENTRY structure.

// pfnGetObjectDescription points to your class's GetObjectDescription() method. #define OBJECT_ENTRY(clsid, class) { ... class::GetObjectDescription, \                     // [pfnGetObjectDescription] ...

CComCoClass<> provides a default implementation for this method, which returns a NULL string:

// The default implementation of GetObjectDescription() in CComCoClass<> // does nothing. static LPCTSTR WINAPI GetObjectDescription() {      return NULL; }

GetObjectDescription() is a method that you will not need to worry about 99.9% of the time, allowing you to leverage the default implementation. Recall that when ATL is asked to register your objects, every coclass listed in the OBJECT_MAP will have its UpdateRegistry() method called. The point here is every coclass in the map will be registered.

If you wish to selectively register (or unregister) a subset of objects in your server, you need to create an object supporting the IComponentRegistrar interface (which is one possible choice from the ATL Object Wizard). This standard COM interface gives you the ability to selectively choose which coclasses should be registered.

If you obtain a pointer to an object implementing the IComponentRegistrar interface, you may call GetComponents(), which is used to call the GetObjectDescription() method defined in your coclass. If you want to go in this direction, you will need to provide a non-NULL return value for GetObjectDescription() using the DECLARE_OB- JECT_DESCRIPTION macro:

// CoHexagon is now providing its own description string. class ATL_NO_VTABLE CCoHexagon : ... { ...      DECLARE_OBJECT_DESCRIPTION("The very independent CoHexagon") ... };

As I am sure you guessed, the string parameter to this macro becomes the physical return value of GetObjectDescription():

// Allows your coclass to provide an object description string. #define DECLARE_OBJECT_DESCRIPTION(x)\ static LPCTSTR WINAPI GetObjectDescription()\ {\      return _T(x);\ }

So when is GetObjectDescription() called? When iterating over your _ATL_OBJMAP_ ENTRY array, if a non-NULL return value is obtained from GetObjectDescription(), your coclass will not be registered by ATL, as it assumes a Component Registrar Object will be doing so on a more selective basis:

// Your class's GetObjectDescription() method is checked during the registration // and unregistration of your server. ATLINLINE ATLAPI AtlModuleRegisterServer(_ATL_MODULE* pM, BOOL bRegTypeLib,                                     const CLSID* pCLSID) { ...      _ATL_OBJMAP_ENTRY* pEntry = pM->m_pObjMap;      for (;pEntry->pclsid != NULL; pEntry = _NextObjectMapEntry(pM, pEntry))      {           if (pCLSID == NULL)           {                if (pEntry->pfnGetObjectDescription != NULL &&                     pEntry->pfnGetObjectDescription() != NULL)                          continue;           }      ...      } ... }

Therefore what you are stating when you add the DECLARE_OBJECT_DESCRIPTION macro in your coclass is "Don't bother to register me. I'm taken care of." As mentioned, this is not a method you will need to worry about overwriting, but now you know how you can.

Understanding COM Categories

Before we can understand how ATL provides a pointer to your class's GetCategoryMap() function, we need to understand COM categories. As you are well aware, a COM object may support any number of interfaces, with each interface expressing a singular functionality. IDraw expresses the ability to be rendered, IShapeEdit expresses the ability to edit an existing shape, and so on.

Now, what if you created a set of coclasses {CoHexagon, CoPoint, CoCone} all supporting the same set of interfaces. If each of these coclasses could belong to a named group, a client could then work with any member of the set with the foreknowledge of how they behave. This would be very helpful from a client's point of view if any member of this set would do for the task at hand (e.g., "I just want to work with anything that can draw something!") As well, if a client is able to understand what using a member of this set requires, the client has an easy way to determine if it should bother to create the object at all.

A coclass may specify a category to which it belongs. For example, we (as the object developer) may decide that any object supporting the interface set {IDraw, IShapeEdit, IErase, IShapeID} belongs to the category "Drawable." Once the category is defined (and as long as we have a client that understands the implications of using objects belonging to the Drawable category) we provide a way for the client to make an assumption about the objects in the Drawable category: Drawable objects support four well-known drawing related interfaces and can thus be treated the same way.

A coclass may specify the categories it belongs to during the registration process by adding the correct settings under its HKCR\CLSID entry. Category information is logged into the system registry under HKCR\Component Categories, which contains all registered categories on a given machine:

click to expand
Figure 9-2: The registry holds all component categories in a unique subkey.

A COM category is identified by a GUID, termed a CATID (category ID). As you may expect, there are a number of predefined categories (and corresponding CATIDs) that a coclass may gain membership to. Many of these predefined CATIDs are defined in <comcat.h>:

// Some predefined defined Category IDs. EXTERN_C const CATID           CATID_Insertable; EXTERN_C const CATID           CATID_Control; EXTERN_C const CATID           CATID_Programmable; EXTERN_C const CATID           CATID_IsShortcut; EXTERN_C const CATID           CATID_NeverShowExt; EXTERN_C const CATID           CATID_DocObject; EXTERN_C const CATID           CATID_Printable; EXTERN_C const CATID           CATID_RequiresDataPathHost; EXTERN_C const CATID           CATID_PersistsToMoniker; EXTERN_C const CATID           CATID_PersistsToStorage; EXTERN_C const CATID           CATID_PersistsToStreamInit; EXTERN_C const CATID           CATID_PersistsToStream; EXTERN_C const CATID           CATID_PersistsToMemory; EXTERN_C const CATID           CATID_PersistsToFile; EXTERN_C const CATID           CATID_PersistsToPropertyBag; EXTERN_C const CATID           CATID_InternetAware;

Viewing Registered Component Categories

The OLE/COM Object Viewer lists all Component Categories on your machine under the Grouped by Component Category node. When you expand a subnode, you will see the list of all coclasses supporting a given category:

click to expand
Figure 9-3: Registered COM categories.

To learn exactly which CATIDs constitute a given COM category, select a subnode and examine the right-hand pane of the OLE/COM Object Viewer. As an example, here is the "safely scriptable" category:

click to expand
Figure 9-4: The safe for scripting COM category.

Categories of COM Categories

At the risk of sounding redundant, COM defines two categories of COM categories. If an object is a member of a category that assumes a client must be able to work with each interface expressed by the category, we say the coclass supports a "required category." For example, ActiveX controls typically support the "Control" category, which informs a client it should be able to work with all interfaces necessary for an ActiveX control, and can therefore be rendered, persisted, and so forth. On the other hand, if a coclass supports a category which does not require the client to work with the collection of interfaces expressing the category, we say the coclass defines an "implemented category." Implemented categories are more of a "heads-up" for the client.

An object choosing to belong to a given category needs to specify the full set of implemented or required categories, under HKCR\CLSID. For example, here is the Implemented Categories subkey for the Microsoft StatusBar Control:

click to expand
Figure 9-5: A given coclass lists the categories to which it belongs.

Assigning an ATL Coclass to a COM Category

So how can we use ATL to specify COM categories for a coclass? ATL defines a category map used to hold the set of all COM categories supported by a coclass. This map expands to provide the GetCategoryMap() method. Recall the pfnGetCategoryMap field of _ATL_OBJMAP_ENTRY is set to this method:

// pfnGetCategoryMap points to your class's GetCategoryMap() method. #define OBJECT_ENTRY(clsid, class) { ... class::GetCategoryMap, \                         // [pfnGetCategoryMap] ...

CComCoClass<> defines a default implementation of GetCategoryMap(), which simply returns NULL, signifying that your coclass has no COM categories to be registered:

// CComCoClass<> defines a default GetCategoryMap() method for your coclasses. // static const struct _ATL_CATMAP_ENTRY* GetCategoryMap() {      return NULL; } 

When you wish your coclass to supply a set of COM categories, you will need to override this default behavior and specify your own category map (and therefore your own GetCategoryMap() implementation). There is no ATL wizard for this process, but doing so is quite simple.

Defining a Custom Category Map

A custom category map is established using the BEGIN_CATEGORY_MAP and END_CATEGORY_MAP macros. These macros expand to define an array of _ATL_CATMAP_ENTRY structures as well as the GetCategoryMap() method which returns this array:

// Declare an array of _ATL_CATMAP_ENTRY structures named 'pMap' // and also define the GetCategoryMap() function to return an array of these // structures. #define BEGIN_CATEGORY_MAP(x)\   static const struct _ATL_CATMAP_ENTRY* GetCategoryMap() {\   static const struct _ATL_CATMAP_ENTRY pMap[] = { // Terminate the array. #define END_CATEGORY_MAP()\   { _ATL_CATMAP_ENTRY_END, NULL } };\   return( pMap ); }

Next, you must generate a GUID for your new CATID. It will behoove you to add a new header file to your project (wrapped in #ifndef/#endif logic) to hold all your CATIDs. In this way, you may include this file into each class making reference to the underlying GUIDs. Using guidgen.exe, choose the static const GUID option and substitute the <<name>> marker with your new CATID. By convention, all category IDs begin with the CATID_ prefix:

// A custom GUID for CATID_DRAWABLE // {2134FA20-1B04-11d3-B8F4-0020781238D4} static const GUID CATID_DRAWABLE = { 0x2134fa20, 0x1b04, 0x11d3, { 0xb8, 0xf4, 0x0, 0x20, 0x78, 0x12, 0x38, 0xd4 } };

Next, in your coclass's header file, define a category map with the corresponding category map macros. Populate this map using the IMPLEMENTED_CATEGORY and REQUIRED_CATEGORY macros. These macros make use of an ATL-provided flag marking the type of category:

// The macros used to fill your category map. #define IMPLEMENTED_CATEGORY( catid ) \ { _ATL_CATMAP_ENTRY_IMPLEMENTED, &catid }, #define REQUIRED_CATEGORY( catid ) \ { _ATL_CATMAP_ENTRY_REQUIRED, &catid },

Here is our good friend CoHexagon, now a proud member of the Drawable category:

// If you wish to override the default implementation of GetCategoryMap() as // defined by CoComCoClass<>, you must add a category map by hand. #include "catids"     // Defines the CATID_DRAWABLE constant. class ATL_NO_VTABLE CCoHexagon : ... { ...      // A Category Map      BEGIN_CATEGORY_MAP(CCoHexagon)           IMPLEMENTED_CATEGORY(CATID_DRAWABLE)      END_CATEGORY_MAP() ... }; 

Finally, you will need to update your RGS file to update the Component Category key under HKCR with your new CATID. Although ATL will assign the correct category entries for the coclass by using the category map, ATL will not add the necessary HKCR\Component Categories listing (thus you need to update your RGS file):

HKCR { ...      NoRemove 'Component Categories'      {           {2134FA20-1B04-11d3-B8F4-0020781238D4}           {                val 409 = s 'Drawable Objects'           }      } ... }

Once you recompile and reregister your server, ATL will update your CoHexagon registry information to support the new CATID. As you can see, since we used the IMPLEMENTED_CATEGORY macro entry in our category map, the ATL registrar added the Implemented Category subkey listing our custom CATID:

click to expand
Figure 9-6: Result of the custom category map.

The OLE/COM Object Viewer acknowledges the Drawable category as well. Assume ATLShapesServer.dll contains three coclasses, each belonging to the Drawable category:

click to expand
Figure 9-7: The drawable objects.

To sum up, the _ATL_OBJMAP_ENTRY's pfnGetCategoryMap field is set to your coclass's GetCategoryMap() method. CoComCoClass<> provides a default implementation that effectively returns an empty _ATL_CATMAP_ENTRY array, informing _Module that you have no categories to register.

If you wish to support COM categories, you must create a category map yourself. In this case, _Module will call your custom GetCategoryMap() method and iterate over your _ATL_CATMAP_ENTRY array during the registration process.

Note 

In the upcoming lab, we will also see how a COM client can obtain members of a given category at runtime using the Category Manager.

Specifying the ObjectMain() Method

The final field of the _ATL_OBJMAP_ENTRY structure, pfnObjectMain, is filled with an implementation of your class's ObjectMain() function:

// pfnObjectMain points to your class's ObjectMain() method. #define OBJECT_ENTRY(clsid, class) { ... class::ObjectMain },                         // [pfnObjectMain]

CComObjectRootBase provides an implementation, which you inherit by default:

// ObjectMain() is called during Module::Init() and Module::Term() static void WINAPI ObjectMain(bool bStarting ) {}

This method will be called by the ATL framework under two conditions: When your server is loaded by the COM runtime, ATL will iterate over the object map, calling each coclass's ObjectMain() with the bStarting parameter set to true. In here, you may safely initialize your object's resources and so forth. When your server is unloaded, ObjectMain(false) will be called to allow you to deallocate any acquired resources.

If you wish to override the default behavior, simply provide an implementation in your coclass. There is not a helper macro to insert this method. It is a simple override:

// When an ATL server is loaded or unloaded, ObjectMain() is called for each coclass // listed in the object map. class ATL_NO_VTABLE CCoHexagon : ... { ...      static void WINAPI ObjectMain(bool starting)      {           if(starting)                MessageBox(NULL, "The server is starting!",                          "Hex ObjectMain() Says", MB_OK);           else                MessageBox(NULL, "The server is stopping!",                          "Hex ObjectMain() Says", MB_OK);      } ... }; 

Building Server Object Models

We have now examined each field of the _ATL_OBJMAP_ENTRY structure. As you have seen, your object map is populated with the OBJECT_ENTRY macro; however, there is one additional option. Many COM servers (DLLs or EXEs) provide a relatively "flat" object model. In other words, the server provides a class factory for any number of top-level coclasses. Each of these coclasses is considered creatable in that they may be instantiated directly by a call to CoCreateInstance() and/or CoGetClassObject(). Assume ATLShapesSer- ver.dll contains the following creatable coclasses (lollipops removed for clarity):

click to expand
Figure 9-8: A very flat object model.

Each of these coclasses would have an entry in the server's object map:

// A server with four creatable objects. BEGIN_OBJECT_MAP(ObjectMap)      OBJECT_ENTRY(CLSID_CoHexagon, CCoHexagon)      OBJECT_ENTRY(CLSID_CoPentagon, CCoPentagon)      OBJECT_ENTRY(CLSID_CoPoint, CCoPoint)      OBJECT_ENTRY(CLSID_CoGoogolplex, CCoGoogolplex) END_OBJECT_MAP()

Other times, COM servers (DLLs or EXEs) may provide a more hierarchical object model within the binary home. To provide a level of depth, we may designate some objects in the hierarchy to be "topmost objects" creatable by COM library calls. Other objects in the server are not directly creatable by COM library calls, but must be created indirectly through another topmost object.

For example, assume that each topmost object in the server has an associated offscreen buffer object. If you are unfamiliar with graphics programming, an offscreen buffer is a region in memory to which you may render images, and then transfer the image (or a section of it) onto the screen. The idea here is that a client should not be able to have access to a buffer object until it has obtained a valid shape:

click to expand
Figure 9-9: A slightly deeper object model.

For objects that are "visible" by the client but not directly creatable, ATL provides the OBJECT_ENTRY_NON_CREATEABLE macro.

OBJECT_ENTRY_NON_CREATEABLE: Defining "Non-Creatable" Coclasses

This macro is not available using the ATL Object Wizard, and must be entered by hand if you wish to make use of it. This macro declares a "visible but not directly creatable" coclass, which can be very useful if you are creating an object hierarchy. Let's revisit the question "Why would we wish to define a coclass that is not directly creatable by a client?"

Assume CoHexagon contains a method in the [default] interface, returning an interface to a non-creatable coclass named HexBuffer. Again, the idea here is that a client should not be allowed to directly create a HexBuffer object, but must obtain an interface pointer from some drawable object. In this way, we can create object models that define a "front line" of creatable objects and a "back line" set of objects that are returned indirectly to the client. If we wish to model this sort of object design, we can modify our object map to look like the following:

// A server with one creatable object and one non-creatable object. BEGIN_OBJECT_MAP(ObjectMap)      OBJECT_ENTRY(CLSID_CoHexagon, CoHexagon)      OBJECT_ENTRY_NON_CREATEABLE(HexBuffer) END_OBJECT_MAP()

The OBJECT_ENTRY_NON_CREATEABLE macro ensures that the object is non-creatable by providing a NULL entry for the pfnGetClassObject and pfnCreateInstance fields of the _ATL_OBJMAP_ENTRY structure:

// This object map macro only specifies the registry, category map, and initialization // function. #define OBJECT_ENTRY_NON_CREATEABLE(class) \ {&CLSID_NULL, \ class::UpdateRegistry, \ NULL, NULL, NULL, 0, NULL, \ class::GetCategoryMap, \ class::ObjectMain },

Beyond listing HexBuffer with the OBJECT_ENTRY_NON_CREATEABLE macro, to complete the implementation of a non-creatable coclass, you should edit your IDL file to specify the [noncreatable] attribute on the coclass definition. This informs various high- level COM language mappings (such as Visual Basic) to issue a runtime error if the user attempts to create the object directly. Here is the IDL:

// Non-creatable coclasses should be identified as such with the [noncreatable] attribute. // Note the spelling discrepancy! IDL says 'creatable', while ATL says 'createable'. [      uuid(4A01DD03-066C-11D3-B8E5-0020761438D4),      helpstring("HexBuffer Class"),      noncreatable ] coclass HexBuffer {      [default] interface IHexBuffer; }; 

As a final tweak, we should also add the DECLARE_NO_REGISTRY macro to the HexBuffer coclass to ensure the ProgIDs and CLSIDs are not entered into the registry. In the following lab, you will see exactly how to return a reference to a [noncreatable] object to an interested client.

Object Map Summary

So then! The BEGIN_OBJECT_MAP and END_OBJECT_MAP macros expand to define an array of _ATL_OBJMAP_ENTRY structures. We have examined (in detail) what a given entry in the object map resolves to. This structure contains a ton of relevant information regarding the creation and registration of the coclass, most in the form of pointers to functions.

We have seen how a coclass (in this case CoHexagon) or one of its base classes will provide a specific macro which expands to an implementation of these functions. The OBJECT_ENTRY and OBJECT_ENTRY_NON_CREATEABLE macros are used to pair up a specific function from an ATL coclass to the correct field of the _ATL_OBJMAP_ENTRY structure.

To wrap up this chapter, the following lab will give you a chance to extend the default ATL COM AppWizard-generated component house with a number of bells and whistles.

Lab 9-1: Heavy-Duty ATL Component Housing

The purpose of this lab is to give you a chance to try some of the new tricks introduced throughout this chapter. You will be building a small object hierarchy consisting of creatable and non-creatable objects. The topmost objects will all belong to a custom-implemented COM category.

You will also get to specify some custom registry information for each creatable object. As a final bonus, you will examine some C++ clients that make use of your custom COM category.

 On the CD   The solution for this lab can be found on your CD-ROM under:
Labs\Chapter 09\ATLShapes
Labs\Chapter 09\ATLShapes\VB Client
Labs\Chapter 09\ATLShapes\CPP Client
Labs\Chapter 09\ATLShapes\MFC Client

Step One: Create the Initial Server

Create a new DLL server named ATLShapes.dll using the ATL COM AppWizard (all of the default settings will be fine). You will be developing two directly creatable coclasses: CoHexagon and CoLine, both supporting the IDraw and IShapeID interfaces. To begin, insert a Simple Object named CoHexagon using the ATL Object Wizard. Change the name of the [default] interface to IDraw, and be sure you select a custom interface (the remaining attributes may be left to their defaults). Do not add methods to this interface just yet. Go ahead and compile to get up and running.

Now insert CoLine. Note that the [default] interface found in the Names tab will be automatically named ICoLine. Change this to IDraw as well, and be sure that you select a custom interface from the Attributes tab. When you examine ClassView, you will see a problem: Two definitions of IDraw will be found in your IDL file (see Figure 9-10):


Figure 9-10: Conflicting GUIDs.

To remedy this problem, delete the IDL definition for one of the two IDraw interfaces and all associated attributes (it does not matter which one, as they are both empty at this point). Add the [oleautomation] attribute to the remaining IDraw interface. Save the IDL file and examine ClassView again; things should have cleaned up just fine.

Now, right-click on the IDL IDraw lollipop from ClassView and select the Add Method Wizard. Add a single method named Draw(), returning an HRESULT and taking no parameters. When you select OK, go look at ClassView again; notice how both CoLine and CoHexagon have been given stub code for Draw()! Cool, huh? You can use this same trick whenever your server defines a single interface supported by multiple objects. In the implementation of each coclass, simply display a message box identifying which shape is being drawn:

// Draw the hexagon. STDMETHODIMP CCoHexagon::Draw() {      MessageBox(NULL, "I am drawing a hexagon", "IDraw::Draw",                MB_OK | MB_SETFOREGROUND);      return S_OK; }

Now we will define the IShapeID interface. Open your IDL file and define an empty interface named IShapeID:

// This interface allows the user to give a unique ID to a given shape. [ object, uuid(88A294F0-2127-11d3-B8F7-0020781238D4), oleautomation] interface IShapeID : IUnknown { };

Next, add support for this interface to both coclasses in the library statement of your IDL file. Save and compile your IDL, right-click on your CoHexagon class from ClassView, and use the Implement Interface Wizard to add support for IShapeID. Do the same for CoLine.

Add a single BSTR property to IShapeID named ShapeName. When you click OK, notice again that both CoHexagon and CoLine have been given get_ShapeName and put_ShapeName method stub code.

Each class should maintain a private CComBSTR that will hold the name for a given shape. Set the CComBSTR member variable to an empty string in the constructor of each coclass. For each [propput], assign the incoming BSTR parameter to your private CComBSTR object. For each [propget], set the BSTR* parameter to a copy of your CComBSTR's underlying buffer. For example:

// Return the shape's name. STDMETHODIMP CCoLine::get_ShapeName(BSTR *pVal) {      *pVal = m_name.Copy();      return S_OK; } STDMETHODIMP CCoLine::put_ShapeName(BSTR newVal) {      m_name = newVal;      return S_OK; } 

Step Two: Define a Custom COM Category

Given that CoHexagon and CoLine both support the same interfaces (and thus provide the same functionality), it would be nice to introduce a COM category for objects that support IDraw and IShapeID.

Insert a new text file into your project, saved as catids.h. Use guidgen.exe to create a new GUID for a COM category named CATID_NamedShape. Be sure to wrap your GUID in #ifndef, #define, #endif syntax, as both CoHexagon and CoLine will need to include this file (if you forget, you will get multiple redefinition errors):

#ifndef _CAT #define _CAT // {E5A9E920-2129-11d3-B8F7-0020781238D4} static const GUID CATID_NamedShape = { 0xe5a9e920, 0x2129, 0x11d3, { 0xb8, 0xf7, 0x0, 0x20, 0x78, 0x12, 0x38, 0xd4 } }; #endif // _CAT

Include this file in cohexagon.h and coline.h. Now we need to override the default GetCategoryMap() method we have inherited from CComCoClass<>. Add a category map to CoHexagon which specifies a single Implemented Category using your new CATID:

// The Category Map (don't forget to #include catids.h) BEGIN_CATEGORY_MAP(CCoHexagon)      IMPLEMENTED_CATEGORY(CATID_NamedShape) END_CATEGORY_MAP()

Do the same for CoLine:

// The Category Map BEGIN_CATEGORY_MAP(CoLine)      IMPLEMENTED_CATEGORY(CATID_NamedShape) END_CATEGORY_MAP()

As you recall, ATL will use the category map to ensure your coclass is registered with the correct category subkeys. Therefore, if you examine the CLSID entries for each object (after a recompile) you will find an Implemented Category subkey has been inserted (thanks to the category map and ATL registration).

However, we need to edit our RGS file to specify the Component Category listing to insert under HKCR\Component Category. Open one of your RGS files (it does not matter which one) and add the following scripting code. Be sure the GUID you enter is the same as the CATID generated by guidgen.exe. Let's call our category "Named Drawing Objects." Be extra careful when adding RGS scripts. For example, be sure there are no spaces between your curly brackets and the GUID, and there are spaces between the assignment operators:

HKCR { ...      NoRemove 'Component Categories'      {           {E5A9E920-2129-11d3-B8F7-0020781238D4}           {                val 409 = s 'Named Drawing Objects'           }      }      ... }

When you have recompiled, open the OLE/COM Object Viewer and expand the Grouped by Component Category node. You should see CoHexagon and CoLine strutting their stuff under the Named Drawing Objects subnode:

click to expand
Figure 9-11: The Named Drawing Objects.

Later in this lab we will build some C++ clients that are able to discover the members of the Named Drawing Objects category at run time.

Step Three: Custom Registration

This chapter has shown you how the _ATL_OBJMAP_ENTRY structure maintains the pfnUpdateRegistry field to point to a coclass's UpdateRegistry() method. CoLine and CoHexagon automatically inherit an implementation of this method via the DECLARE_REGISTRY_RESOURCEID macro listed in each class's header file.

We are going to specialize the registration of our shape objects, so comment out the DECLARE_REGISTRY_RESOURCEID macro in each class and provide a new listing for UpdateRegistry() for both (call UpdateRegistryFromResource() to force ATL to perform the default registration duties):

// Put in each class's header file. static HRESULT WINAPI UpdateRegistry(BOOL b) {      // Be sure your resource ID is correct for the given coclass!      return _Module.UpdateRegistryFromResource(IDR_COLINE, b); }

The task here is to add custom installation information which identifies the time the coclass was installed on a given machine. We will do this by defining a custom registry script placeholder (much like the framework provided "%MODULE%"). Add the following functionality to the UpdateRegistry() functions:

// We will add the time that this class was installed // as part of our custom registration.     static HRESULT WINAPI UpdateRegistry(BOOL b) {      USES_CONVERSION;      char time[50];      SYSTEMTIME sysTime;      GetLocalTime(&sysTime);      // Format the time into the char array.      sprintf(time, "Hour %.2d : Min %.2d : Second %.2d : MS %.2d",           sysTime.wHour, sysTime.wMinute,           sysTime.wSecond, sysTime.wMilliseconds);      // Now we need to create a special tag to use in the RGS script.      _ATL_REGMAP_ENTRY regMap[] =      {           { OLESTR("INSTALLTIME"), A2W(time)},           {0, 0}      };      // Do default processing of RGS file.      return _Module.UpdateRegistryFromResource(IDR_COLINE, b, regMap); }

ATL makes it possible to extend the syntax of RGS files to serve your own needs. Notice the declaration of an _ATL_REGMAP_ENTRY array. The _ATL_REGMAP_ENTRY array is terminated by NULL entries, hence the {0, 0} entry.

This array is an optional parameter to CComModule::UpdateRegistryFromResource(), and is used to send in additional "%...%" tags above and beyond the framework defaults (such as %MODULE%).

Here, we have created a two-item array for a tag named INSTALLTIME, which maps to the current system time. We obtain this time using the Win32 GetLocalTime() function. This function takes a SYSTEMTIME structure, and will populate the fields of this structure with time and date information.

Finally, we need to edit the RGS file for each coclass in our server making use of this placeholder (i.e., every coclass that has supplied a custom UpdateRegistry() method). We get to choose where this entry will be made, and what the name of the subkey should be. Let's say that the ProgIDs for each coclass will include an extra subkey called InstallTime. Here is coline.rgs to get you started. Do the same for CoHexagon.

ATLShapes.CoLine.1 = s 'CoLine Class' {      CLSID = s '{00D7BA29-20FF-11D3-B8F7-0020781238D4}'      InstallTime = s '%INSTALLTIME%' } 
ATLShapes.CoLine = s 'CoLine Class' {      CLSID = s '{00D7BA29-20FF-11D3-B8F7-0020781238D4}'      CurVer = s 'ATLShapes.CoLine.1'      InstallTime = s '%INSTALLTIME%' } 

Go ahead and recompile your project. This will automatically register your server. Use regedit.exe to hunt down the ProgIDs for each server. You should see something similar to Figure 9-12:

click to expand
Figure 9-12: Results of our custom UpdateRegistry() logic.

You are free to add as many registry symbols as you feel necessary to register your server. Just extend the _ATL_REGMAP_ENTRY array with n number of entries, and be sure you have a terminating {0, 0} entry.

If you are up for it, try to add another custom entry that identifies the name of the developer who created this object (your solution shows the necessary logic, so take a peek if you get stuck).

Step Four: Define a Non-Creatable Class

Recall that object maps may specify creatable and non-creatable objects. At first, the idea of a non-creatable object seems quite odd. However, the principle of topmost creating subclass objects is extremely common. Just look at the object model for the Data Access Objects (DAO) or Microsoft Excel (among others). Let's provide the ability for CoLine and CoHexagon to create a non-creatable sub object for a client and return the [default] interface of the new object.

Insert a new Simple Object named COSBuffer (Off Screen Buffer). Go ahead and use all defaults from the Name tab; however, be sure to set the Interface type to Custom. As this class is not creatable, we do not need any registry information placed in the system registry. Add the DECLARE_NO_REGISTRY macro in your new class's header file, and comment out the existing registration macro.

Now add a single method to the [default] IOSBuffer interface called RenderTo- Memory() and implement accordingly:

STDMETHODIMP COSBuffer::RenderToMemory() {      // Simulate bit blitting to memory.      MessageBox(NULL, "I am rendering to memory", "Off Screen Buffer Says...",                MB_OK | MB_SETFOREGROUND);      return S_OK; }

The ATL Object Wizard will always update your object map with OBJECT_ENTRY macros, which are used to identify creatable classes. Go into the object map and remove the wizard-generated entry for COSBuffer, and replace it with the correct OBJECT_ENTRY_ NON_CREATEABLE macro. Your object map should now look like the following:

// Specifying a non-creatable object. BEGIN_OBJECT_MAP(ObjectMap)      OBJECT_ENTRY(CLSID_CoHexagon, CCoHexagon)      OBJECT_ENTRY(CLSID_CoLine, CCoLine)      OBJECT_ENTRY_NON_CREATEABLE(COSBuffer) END_OBJECT_MAP()

Recall that every non-creatable class should be marked as such in the IDL code. Open up the project's IDL file, and add the [noncreatable] attribute to your coclass statement (also recall the inconsistent spelling—this is not a typo in the book).

// Recall this will force many COM language mappings to throw an error // if the user attempts to create the object. [ uuid(00D7BA3B-20FF-11D3-B8F7-0020781238D4),   noncreatable, helpstring("OSBuffer Class") ] coclass OSBuffer {      [default] interface IOSBuffer; };

Step Five: Provide Access to the Non-Creatable Class

Non-creatable objects are eventually creatable, just not directly by the client. Other objects in the server are responsible for creating them indirectly for the client, through an interface method. Add another method to IDraw named GetOSBuffer() using the Add Method Wizard. Depending on the ordering of your IDL interface definitions, you may have to make a forward declaration to IOSBuffer. The IDL code will look like the following:

// Forward declaration (if IDraw is defined before IOSBuffer in the IDL file). interface IOSBuffer; [...] interface IDraw : IUnknown {      HRESULT Draw();      // Returns a reference to the non-creatable sub object.      HRESULT GetOSBuffer([out, retval] IOSBuffer** pBuffer); };

Notice that the new IDraw method returns an IOSBuffer pointer to the client. Next, we need to implement the GetOSBuffer() method for both CoLine and CoHexagon. We will be passing COSBuffer as a template parameter to CComObject<>. After that, we will call the static CreateInstance() method, and if successful, query for IID_IOSBuffer. Implement GetOSBuffer() as the following for each creatable class:

// Create the inner object for the client. STDMETHODIMP CCoLine::GetOSBuffer(IOSBuffer **pBuffer) {      // Create a brand new COSBuffer for the client.      HRESULT hr;      CComObject<COSBuffer>* pBuf = NULL;      hr = CComObject<COSBuffer>::CreateInstance(&pBuf);      // Now ask for the [default] IOSBuffer interface.      if(SUCCEEDED(hr))      {           // Technically we are using a local copy so we should           // AddRef() and Release().           pBuf->AddRef();           hr = pBuf->QueryInterface(IID_IOSBuffer, (void**)pBuffer);           pBuf->Release();      }      return hr; } 

Now, if a client has obtained a valid IDraw interface from either CoLine or CoHexagon, they may call GetOSBuffer() and manipulate the IOSBuffer interface indirectly returned by the topmost CoLine and CoHexagon coclasses.

Step Six: A Visual Basic Test Client

Open up Visual Basic and create a new Standard EXE workspace. Set a reference to your ATLShapes type library from the Project | References menu. Now, create a simple GUI to create CoLines and CoHexagons. Also provide a way to set a name for each shape. Here is an initial GUI (we are not trying to win any user interface awards here):

click to expand
Figure 9-13: Yet another VB test client.

Add some private variables to your [General][Declarations] section to hold CoLine, CoHexagon, and IShapeID variables. When the user clicks on the Make X buttons, set the correct variable to a new instance and assign the name of the shape. For example:

' [General] [Declarations] ' Private mHex As CoHexagon Private mLine As CoLine Private osb As OSBuffer           ' Next step...see below. Private itfID As IShapeID Private Sub btnGetHex_Click()     ' Create and name a hexagon.      Set mHex = New CoHexagon      mHex.Draw      Set itfID = mLine      itfID.ShapeName = txtHex.Text      MsgBox itfID.ShapeName End Sub

Now extend your GUI to include a way to access the OSBuffer object for a given shape (assume we have added two buttons to the main Form object). The code behind each Get X's Buffer button will call the GetOSBuffer() method of the [default] shape interface (IDraw). Because we marked the IOSBuffer parameter as [out, retval], we can "receive" this interface pointer as a physical return value. Here is the VB code behind btnGetHexOSB:

' Call IDraw::GetOSBuffer and cache the IOSBuffer interface in the ' osb variable. ' Private Sub btnGetHexOSB_Click()      Set osb = mHex.GetOSBuffer      osb.RenderToMemory      Set osb = Nothing      ''''''''''''''' Dim WontWork as New OSBuffer End Sub

To test the non-creatable nature of the OSBuffer object, uncomment the line of code above. That wraps up the VB test client.

Step Seven: Dynamically Obtain COM Categories

The final part of this lab will allow you to create some C++ clients that dynamically discover the ProgIDs of the objects belonging to your custom COM category. Begin by creating a new Win32 Console Application. For this step of the lab, you will need to copy over the correct MIDL-generated files (*_i.c and *.h) as well as the server's catids.h file.

When a COM client wishes to discover objects belonging to a COM category, the first step is to create an instance of a standard system-level COM object named the "component categories manager" that has the preassigned CLSID of CLSID_StdComponent- CategoriesMgr. This object supports a standard COM interface named ICatInformation. With this interface, we can make requests of the manager, such as "look up all objects belonging to the Named Drawing Object category."

Begin by including <comcat.h>, and create an instance of the manager object.

The ICatInformation interface supplies the EnumClassesOfCategories() method, which allows you to obtain a standard COM enumerator interface, IEnumCLSID. From this interface, you may call Next() to fetch a batch of CLSIDs representing the objects belonging to some COM category. We will examine COM enumerators in gory detail in Chapter 11, so let's hold the details until then. Here is a main() function which requests the next 20 CLSIDs. Of course we only have two items belonging to this custom category, so we can test how many we successfully obtained using the final DWORD parameter of the Next() method:

// This C++ client is able to discover who is a Named Drawing Object. int main(int argc, char* argv[]) {      CoInitialize(NULL);      ICatInformation *pCatInfo = NULL;      HRESULT hr;      // Create the categories manager.      hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, 0,           CLSCTX_SERVER, IID_ICatInformation, (void**)&pCatInfo);      if(SUCCEEDED(hr))      {           IEnumCLSID *pCLSIDS = NULL;           CATID catids[1];           catids[0] = CATID_NamedShape;           // I am looking for CATID_NamedShape...           hr = pCatInfo->EnumClassesOfCategories(1, catids, -1, 0, &pCLSIDS);           CLSID clsid[20];           LPOLESTR progID;           cout << "Here are the proud members of " << endl                << "the Named Drawing Objects category" << endl << endl;           do           {     // Ask for the next 20 CLSIDs.                DWORD numb = 0;                hr = pCLSIDS->Next(20, clsid, &numb);                if(SUCCEEDED(hr))                {                     // How many did I really get?                     for(DWORD i = 0; i < numb; i++)                     {                          ProgIDFromCLSID(clsid[i], &progID);                          char buff[30];                          WideCharToMultiByte(CP_ACP, NULL, progID,                          -1, buff, 30, NULL, NULL);                          cout << buff << endl;                     }                }           }while(hr == S_OK);           pCLSIDS->Release();      }      pCatInfo->Release();      CoUninitialize();      return 0; }

When you execute the code, you will see CoLine and CoHexagon have been identified as members of the Named Drawing Objects category:

click to expand
Figure 9-14: Interacting with the COM category manager.

Now, imagine if you were to create an MFC application that used similar code to place the ProgIDs of the Named Drawing Objects into a list box. You could allow the user to select a given shape, which you create dynamically based on the ProgID. This sort of functionality is the same as we see in VB when we pull up the Components dialog box to insert a new ActiveX control. This tool lists all objects belonging to the Control category, and creates the user-specified item.

Assume you have created a dialog-based MFC application using the MFC AppWizard. We could create a very simple GUI which contains a single list box to hold the ProgIDs of each member of the Named Drawing Objects category. We could allow the end user to select an item from this list to create an instance, as suggested by Figure 9-15:

click to expand
Figure 9-15: The Named Drawing Objects launcher utility.

To load up all members of the CATID_NamedShape category, we could add the following code to OnInitDialog(). This is essentially the same code as we have seen in the Win32 Console client; however, we are now placing the ProgIDs into the list box, rather than the IO stream. Here are the relevant changes:

// After creating the category manager, place each ProgID into a list box. do {      DWORD numb = 0;      hr = pCLSIDS->Next(20, clsid, &numb);      if(SUCCEEDED(hr))      {           for(DWORD i = 0; i < numb; i++)           {                ProgIDFromCLSID(clsid[i], &progID);                char buff[30];                WideCharToMultiByte(CP_ACP, NULL, progID, -1, buff,                                  30, NULL, NULL);                // Add to the list box named IDC_SHAPELIST.                CListBox *pList = (CListBox*)GetDlgItem(IDC_SHAPELIST);                pList->AddString(buff);           }      } }while(hr == S_OK);

The code behind the BN_CLICKED event handler is straight COM. Obtain the ProgID from the list box (based on the current selection) and call CoCreateInstance():

// Activate the correct object, based on the selection in the list box. void CMFCClientDlg::OnMakeShape() {      // Figure out which item they clicked on.      char progID[80];      CListBox *pList = (CListBox*)GetDlgItem(IDC_SHAPELIST);      pList->GetText(pList->GetCurSel(), progID);      // Now make that object.      CLSID clsid;      wchar_t wide[80];      mbstowcs(wide, progID, 80);      CLSIDFromProgID(wide, &clsid);      IDraw* pDraw = NULL;      if(SUCCEEDED(CoCreateInstance(clsid, NULL, CLSCTX_ALL, IID_IDraw,                 (void**)&pDraw)))      {           pDraw->Draw();           pDraw->Release();      }      else           AfxMessageBox("Can't draw...Bad ProgID!"); } 

If you would like to see the complete MFC test client, check out your CD-ROM. Better yet, create one of your own!



 < Free Open Study > 



Developer's Workshop to COM and ATL 3.0
Developers Workshop to COM and ATL 3.0
ISBN: 1556227043
EAN: 2147483647
Year: 2000
Pages: 171

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net