| < Free Open Study > |
|
When you define interfaces that contain methods returning interfaces, you essentially allow the object user to fetch a single object at a time. However, what if you wished to develop a COM interface that allowed a client to request a batch of items during a single call? For example, assume we have a coclass that contains 100 instances of another object, say a CoCarLot object containing CoCars.
Internally, CoCarLot may maintain an array of ICreateCar interfaces to represent each CoCar object. Once a client is able to create an instance of CoCarLot, it may ask for a reference to some specific interface that provides access to the subobjects. Let's call this hypothetical interface IEnumerateCars. Now, in your opinion, what sort of methods would this interface contain? At minimum, we may wish to allow clients to request a batch of ICreateCar interfaces using one method. Another method may allow the client to skip over some elements in the collection, while another may allow the client to reset the cursor to the beginning of the collection.
Now assume we have another development effort under way. This time we have a CoEmployees coclass that maintains 600 CoEmployee objects. Again, we may define a custom interface called IEnumerateEmployees, which contains methods looking (and behaving) much like the previous IEnumerateCars interface. When you boil things down to their essence, the only thing truly different between IEnumerateCars and IEnumerateEmployees is what they provide access to (cars or employees). The general behavior of each interface will most likely be the same.
Rather than forcing each and every COM developer to create unique names for enumeration methods, we are given a standard to follow such that all COM enumeration interfaces have the same basic set of methods. This is a good thing of course, as a developer is able to feel more at home when working with enumeration interfaces developed by individual programmers.
This standard is provided by a fictitious COM interface: IEnumXXXX. The odd thing about this interface is that it really does not exist (even odder is that you will find an entry for IEnumXXXX in online help). This interface is simply a suggested form to follow when creating a COM enumeration object. Replace "XXXX" with the name of the subobjects you are providing access to (e.g., IEnumCars, IEnumEmployees, IEnumFrogs, or whatnot).
COM does provide a number of canned IEnumXXXX interfaces which provide a way to iterate over many standard COM data types. For example, IEnumConnectionPoints and IEnumConnections are used to allow a client to grab "outbound" interfaces and information about connected clients from a coclass (more on this later in Chapter 12). Here is a partial listing of some predesigned COM enumeration interfaces:
Standard Enumeration Interface | Meaning in Life |
---|---|
IEnumUnknown | Allows a client to iterate over a set of IUnknown interfaces. As you may expect, from a given IUnknown pointer the client can navigate to more interesting interfaces on the underlying object. |
IEnumVARIANT | Allows a client to iterate over a collection of VARIANT data types. |
IEnumString | Provides a way for a client to iterate over a collection of COM strings. |
IEnumConnectionPoints | Allows a client to iterate over any "connection points" held in the container object. A connection point is a standard way for a coclass to send events to any interested clients. |
IEnumConnections | Enumerates over the set of "interested clients" (i.e., connections) for a given connection point object. |
If this set of standard enumeration interfaces does not fit the bill for your current needs, you may create your own custom enumeration interface. However, regardless of which type of enumerator you are implementing (or using) each will have a similar set of methods. The names of each method will be the same (if you uphold the standard); however, the parameters to each method will more than likely be different, depending on what your container object is exposing (cars, employees, frogs, and so on). To qualify as a COM enumerator interface, you should support the following methods:
Enumeration Interface Method | Meaning in Life |
---|---|
Next() | Allows a client to request a batch of elements from the container (e.g., "Give me the next 10 frogs"). |
Skip() | Allows a client to skip over some number of items in the container (e.g., "Skip over 4 frogs"). |
Reset() | Moves the container's cursor back to the beginning of the collection (e.g., "Move to the beginning of the frog list"). |
Clone() | Allows a client to make a copy of the current enumeration (e.g., "Copy the current frog enumerator"). |
Given these four methods, you should now understand the form provided by the mythical IEnumXXXX interface:
// The IEnumXXXX interface does not exist in the real world, but provides // a suggested form for all COM enumeration interfaces to follow. interface IEnumXXXX : IUnknown { HRESULT Next( [in] ULONG cElements, // Number requested. [out] XXX* rgIXXX, // Array of XXX. [out] ULONG* pFetched); // How many were returned? HRESULT Skip( [in] ULONG cElements) // Number to skip over. HRESULT Reset(); // Move to beginning of list. HRESULT Clone( [out] IEnumXXXX**); // Give out a copy. };
Notice that a traditional COM enumeration does not provide a way to insert or remove items from the container's underlying set of objects. Given this, a COM enumerator object will typically maintain some set of items with some upper limit (a standard C-style array usually fits the bill quite nicely).
If these facts about the COM enumerator strike you as too limiting, hang in there. In just a bit we will discuss COM collections, which are a variation on the simple enumeration pattern and do provide a way to insert and remove items in an [oleautomation] compatible manner. To get the ball rolling, we will now develop a simple COM enumeration from the ground up in C++, and then see what ATL has to offer us by way of framework support.
On the CD Your companion CD-ROM contains the AgeEnum server (and C++ client), found under Labs\Chapter 11\AgeEnum. Again, feel free to load it up and follow along.
The COM enumeration pattern consists of three players. First we have the container class that is home to the entire set of subobjects under its management, for example, CoFrogHolder. Next, we have the subobjects themselves (CoFrog). The container object will maintain a set of these subitems and define a cursor variable representing the current item "pointed to" in the set (m_currFrog). Finally, we have the enumerator interface (IEnumFrogs), which allows the client to pull over some number of subobjects. It is the enumerator interface that allows the client to affect the position of the cursor.
Imagine a very simple container named CoAgeHolder, which maintains a set of unsigned longs representing some arbitrary ages of some item. A client may manipulate these ULONG types using the custom IEnumAge interface. If the client obtains the IEnumAge interface from the container, it now has a way to pull over batches of ULONGs using the Next(), Skip(), Reset(), and Clone() methods. Here is the IDL:
// The CoAgeHolder container will implement this enumeration interface. [ object, uuid(B5F10D50-53F1-11d3-AB20-00A0C9312D57) ] interface IEnumAge : IUnknown { // Give me the next n number of ULONGs. HRESULT Next([in] ULONG celt, [out, size_is(celt), length_is(*pCeltFetched)] ULONG* rgVar, [out] ULONG * pCeltFetched); // Skip over n items. HRESULT Skip([in] ULONG celt); // Reset the internal cursor to the beginning. HRESULT Reset(); // Give out a snapshot of the current enumerator. HRESULT Clone([out] IEnumAge ** ppEnum); };
CoAgeHolder will derive from IEnumAge, and therefore must provide implementation for seven methods. We will ignore the code behind IUnknown, as I am quite sure you can envision these details by now. The coclass will specify a few points of private data, most notably a ULONG to represent the cursor and an array of ULONGs to hold the ages:
// This class is a container of ULONGs. Using the IEnumAge interface, // a client can move the internal cursor around, and request a batch of ages. const ULONG MAX = 10000; class CoAgeHolder : public IEnumAge { public: ... // IEnumAge STDMETHODIMP Next(ULONG celt, ULONG* rgVar, ULONG * pCeltFetched); STDMETHODIMP Skip(ULONG celt); STDMETHODIMP Reset(); STDMETHODIMP Clone(IEnumAge ** ppEnum); // Helper for Clone(). SetIndex(ULONG pos); private: ULONG m_currentAge; // Internal cursor. ULONG m_theAges[MAX]; // Array of ages (ULONGs). ULONG m_refCount; };
The constructor of CoAgeHolder will be responsible for filling the ULONG array with some test data.
// When the container is created, fill the array with some test data. CoAgeHolder::CoAgeHolder() { g_objCount++; m_currentAge = 0; m_refCount = 0; // Fill the array with some arbitrary data. for(int i = 0; i < MAX; i++) m_theAges[i] = i * 10; }
Now that the container has some valid data, we can examine the implementation details behind the IEnumAge interface, where the action really happens.
Recall that once a client obtains a pointer to IEnumAge, it is able to call the set of methods that operates on the underlying array of ULONGs. Of these four methods, the easiest of all to contend with is Reset(), which will do just as you expect: relocate the cursor to the beginning of the array:
// Reset the internal cursor of the array. STDMETHODIMP CoAgeHolder::Reset() { m_currentAge = 0; return S_OK; }
Of greater interest is the implementation of Next(). Clients will call this method when they wish to request a batch of items in a single round trip. Now, as we implement this method, we need to be aware that a client may ask for any number of items. Our CoAgeHolder contains exactly 10,000 ULONGs; if the client asks for 16,930 items, we need to be smart about this and check for overflow.
Furthermore, what if the internal cursor is currently at position 9,998 in the array and the client asks for 45 more items? Again, we need to check for possible overflow. Given these two issues, here is a possible implementation of the Next() method:
// Allow clients to grab a batch of ages... STDMETHODIMP CoAgeHolder::Next(ULONG celt, ULONG* rgelt, ULONG* pceltFetched) { ULONG cFetched = 0; // While the number grabbed is less than the number requested // AND the cursor's position is still less (or equal to) than MAX. while(cFetched < celt && m_currentAge <= MAX) { rgelt[cFetched] = m_theAges[m_currentAge]; m_currentAge++; cFetched++; } *pceltFetched = cFetched; // How many could we hand out? return cFetched == celt ? S_OK : S_FALSE; }
Here, the client tells us how many items it is interested in obtaining using the first ULONG parameter (celt), and provides a place to put the items (the second parameter) which happens to be an array of ULONGs. Finally, before exiting, we set the final parameter to the exact number we were able to give back (e.g., "You asked for 73 ages, I could only give you 51") and return an appropriate HRESULT (S_FALSE indicates overflow).
With Reset() and Next() accounted for, we will move onto the details of the Skip() method. Assume you are a client that has obtained an IEnumAge interface and reset the cursor to the beginning of the list. You then may wish to get the next five items, starting with the third item. Skip() may be called to move the internal cursor over some items, without returning any information to the client.
As with the Next() method, we will still need to check for possible overflow. By convention, we will return S_OK or S_FALSE based on whether or not we are able to skip all requested items:
// Move the cursor ahead some number, and check for wrap around. STDMETHODIMP CoAgeHolder::Skip(ULONG celt) { ULONG cSkipped = 0; // How many we have skipped. while(cSkipped < celt && m_currentAge <= MAX) { m_currentAge++; cSkipped++; } if(cSkipped == celt) return S_OK; else return S_FALSE; }
Last but not least, we have the Clone() method. When a client wishes to clone an existing enumerator, what it is asking for is a brand new copy of the enumerator interface (in this case IEnumAge) such that the underlying container (in this case CoAgeHolder) looks exactly like the current container. If you like, think of the Clone() method as the copy constructor for a COM enumerator.
Now, exactly what needs to take place in your Clone() method will be very specific to your own enumerator object. For CoAgeHolder, we only need to be sure that we "remember" the position of the cursor. More sophisticated enumerations may need to copy a good deal of internal state data.
If you recall, we held this value in a private data member we named m_currentAge.
To help with the implementation of Clone(), assume that CoAgeHolder defines a helper function which will set this data member to some value. Here, then, is all we need:
// Internal helper function for the Clone() method. CoAgeHolder::SetIndex(ULONG pos) { m_currentAge = pos; // Set my index to your index. } // The client wants back a copy of the current container object. // STDMETHODIMP CoAgeHolder::Clone(IEnumAge** ppEnum) { IEnumAge* pEnum; // Make new AgeHolder & set state. CoAgeHolder *pNew = new CoAgeHolder; pNew->SetIndex(m_currentAge); // Hand out IEnumAge (QI calls AddRef(), client will Release()). pNew->QueryInterface(IID_IEnumAge, (void**)&pEnum); *ppEnum = pEnum; return S_OK; }
With this, we have a coclass implementing IEnumAge and each of the four methods expected in any given COM enumeration interface. Now that we have a COM coclass supporting a rather large number of age data, we can see how a C++ COM client can use IEnumAge to pull over a controlled batch of items.
COM enumeration is a fancy way of saying, "Give me a bunch of items right now," with the added ability to skip over items and reset the internal cursor as well as make a copy of the current state of affairs. Consider the client output of Figure 11-2:
Figure 11-2: A C++ enumeration client.
First, the client asks for 400 ages directly, printing out each item. This would entail the use of the Next() method. In the following code, notice how the client is able to determine how many items were indeed fetched from the container using the final parameter of the Next() method (uElementsRead):
// Clients call the Next() method to grab n number of items. void main() { ... ULONG uElementsRead; ULONG pAges[1000]; IEnumAge* pEAge; // Create the age holder and get the enumerator. CoCreateInstance(CLSID_CoRawAgeHolder, NULL, CLSCTX_SERVER, IID_IEnumAge, (void**)&pEAge); // Go get 400 ages at once. pEAge->Next(400, pAges, &uElementsRead); // Show value for each of them. for(ULONG i = 0; i < uElementsRead; i++) { cout << "Age " << i << " is " << pAges[i] << endl; } ... }
Given Figure 11-2, you can see that the Skip(), Clone(), and Reset() members were also called by the C++ client. Again, if you wish to explore this raw C++ enumerator in more detail, check out your companion CD-ROM.
Now that we have some theory behind us, let's see how ATL can help us build COM enumerators and create a more interesting enumerator that maintains a number of COM objects rather than simple ULONGs.
Now that you have seen what a COM enumerator looks like in the raw, it is time to get some help using the ATL framework. As you have already seen, there is nothing terribly daunting about creating an enumerator yourself. In fact, you may very well choose to use the techniques you have just seen in an ATL project. Just write some IDL for your IEnumXXXX interface, and flesh out the implementation of Next(), Skip(), Clone(), and Reset(). If you would rather have some assistance with these details, ATL does provide a small set of classes on your behalf. In Figure 11-3, you can see the names of (and relationships between) the core ATL enumerator templates:
Figure 11-3: The core ATL enumerator templates.
Note | ATL does provide an alternative set of templates which may be used to implement a COM enumeration: IEnumOnSTLImpl<> and CComEnum- OnSTL<>. As the name implies, these templates will only work with containers that represent the internal subobjects with an STL style container. Check out <atlcom.h> for formal definitions. |
The most derived member in the ATL enumerator class hierarchy is CComEnum<>. This class will by used by your container object to provide an implementation of the Skip(), Next(), Clone(), and Reset() methods of your custom IEnumXXXX interface. The parameters to CComEnum<> can be a bit frightening to see all at once. So, before we create a full enumerator using ATL, we will ease into things by examining the hierarchy from the top down.
Recall that the general flavor of any COM enumerator interface has four methods; however, the type of thing enumerated will dictate the parameters to each method. ATL has abstracted these variations away using a C++ template. CComIEnum<> is that template, and is defined in <atlcom.h> as the following (note each method is pure virtual):
// CComIEnum provides the four methods for your COM enumerator. template<class T> class ATL_NO_VTABLE CComIEnum : public IUnknown { public: STDMETHOD(Next)(ULONG celt, T* rgelt, ULONG* pceltFetched) = 0; STDMETHOD(Skip)(ULONG celt) = 0; STDMETHOD(Reset)(void) = 0; STDMETHOD(Clone)(CComIEnum<T>** ppEnum) = 0; };
As you can see, the type "T" will serve as a placeholder for the type of thing you are enumerating. For example, if we were iterating over an array of frog objects, we can derive a class from CComIEnum<IFrog>. While you could inherit the pure virtual methods of your COM enumerator using CComIEnum<>, you really have not gained much. You would still need to write the IDL code for each member of the IEnumFrogs interface, as well as all the implementation code. CComIEnum<> is not meant to be a direct base class for your ATL coclass, but to serve as the base class for the next member in the chain, CComEnumImpl<>.
As the name suggests, this class provides the implementations for the methods of the CComIEnum<> abstract class. It is in here that the details of Skip(), Reset(), Clone(), and Next() are found. CComEnumImpl<> is also found in <atlcom.h> and is listed below (in a slightly condensed form):
// The members of CComIEnum<> are implemented here. template <class Base, const IID* piid, class T, class Copy> class ATL_NO_VTABLE CComEnumImpl : public Base { public: ... STDMETHOD(Next)(ULONG celt, T* rgelt, ULONG* pceltFetched); STDMETHOD(Skip)(ULONG celt); STDMETHOD(Reset)(void){m_iter = m_begin;return S_OK;} STDMETHOD(Clone)(Base** ppEnum); HRESULT Init(T* begin, T* end, IUnknown* pUnk, CComEnumFlags flags = AtlFlagNoCopy); ... };
When ATL constructs an enumerator object, CComIEnum<> is passed in as the "Base" parameter to CComEnumImpl<>. The four inherited methods are implemented as you would expect: Next() will grab "celt" number of items and place them in the array. Clone() will return a new enumerator and so forth.
Notice, however, that CComEnumImpl<> also defines an Init() method. This member is used as a secondary creation method to provide the ATL enumerator object with a copy of (or instructions to share) the enumerated items. More on this in just a bit.
At the end of the inheritance chain is CComEnum<>. This is the template you will be directly creating if you wish to build your COM enumerator using the ATL framework. As most of its functionality comes from the CComIEnum<> and CComEnumImpl<> templates, this class is quite simple.
CComEnum<> is in charge of supplying a COM_MAP to return your IEnumXXXX interface to interested clients, and that's it. Beyond deriving from the previously mentioned ATL templates, CComEnum<> also gains functionality from our good friend CComObjectRootEx<>. Here is the definition as found in <atlcom.h>:
// CComEnum<> is the end of the ATL inheritance chain. This is the class // you will directly create when using ATL to build your COM enumerations. template <class Base, const IID* piid, class T, class Copy, class ThreadModel = CComObjectThreadModel> class ATL_NO_VTABLE CComEnum : public CComEnumImpl<Base, piid, T, Copy>, public CComObjectRootEx< ThreadModel > { public: typedef CComEnum<Base, piid, T, Copy > _CComEnum; typedef CComEnumImpl<Base, piid, T, Copy > _CComEnumBase; BEGIN_COM_MAP(_CComEnum) COM_INTERFACE_ENTRY_IID(*piid, _CComEnumBase) END_COM_MAP() };
CComEnum<> takes a number of parameters. Here is a breakdown of each required parameter:
CComEnum<> Parameter | Meaning in Life |
---|---|
class Base | The name of the IEnumXXXX interface this ATL enumerator will be implementing (ex: IEnumPerson). This parameter will be passed over to CComIEnum<>. |
const IID* piid | The GUID constant that identifies this custom IEnumXXXX interface (ex: IID_IEnumPerson). |
class T | The type of items the enumerator will iterate over (ex: IPerson*). |
class Copy | This one is new. This parameter defines the "copy semantics" used by the ATL enumerator object. You will specify a given _CopyXXX<> class here, which informs ATL how to copy your items (e.g., the Clone() method). |
The fourth parameter of CComEnum<> is one of a handful of ATL-supplied "copy" classes used by ATL enumerations and collections. As you already know, various COM data types require different memory management details. COM text strings need to be created and freed. COM interfaces need to be AddRef()-ed and Release()-ed. VARIANTs need to be copied using VariantCopy(). Given that these data items typically end up in a COM enumeration, ATL provides a set of classes that copy a given item with the correct semantics. For example, ATL's implementation of Item() will make use of the _CopyXXX<> class when filling the array of items for the interested client. Here is a relevant subset of the ATL copy classes:
ATL Copy Template | Meaning in Life |
---|---|
_Copy<class T> | Used to copy simple data types. You may create an ATL enumeration of int, float, or long data types (or structures of these types). This class uses a simple memcpy() for its copy semantics. |
_CopyInterface<class T> | Used to maintain the correct reference count necessary for copying a COM interface. AddRef() and Release() are called automatically. |
_Copy<VARIANT> | If your enumeration is maintaining a list of VARIANT structures, this class calls the correct variant system functions on your behalf. |
_Copy<LPOLESTR> | Maintains the memory management details for COM strings. |
Note | When you are building an ATL enumeration that maintains a set of COM objects you will always want to use the _CopyInterface<> template. |
To illustrate how to use ATL to build COM enumerators, let's create a CoPeopleHolder container class, which maintains an array of CoPerson subobjects, each supporting the IPerson interface. CoPeopleHolder provides controlled access to the CoPerson objects through the custom IEnumPerson interface:
// CoPeopleHolder implements this enumeration interface. [ object, uuid(B5F10D50-53F1-11d3-AB20-00A0C9312D57) ] interface IEnumPerson : IUnknown { HRESULT Next([in] ULONG celt, [out, size_is(celt), length_is(*pCeltFetched)] IPerson** rgVar, [out] ULONG * pCeltFetched); HRESULT Skip([in] ULONG celt); HRESULT Reset(); HRESULT Clone([out] IEnumPerson ** ppEnum); };
Now, we need to insert a new ATL Simple Object to serve as the container class (CoPeopleHolder). Assume the default interface of CoPeopleHolder (IPeopleHolder) defines a single method named GetPersonEnum(). This method is responsible for returning the IEnumPerson interface to the interested client. Here is the IDL:
// Return a pointer to IEnumPerson. [ object, uuid(FE41A722-53E5-11D3-AB20-00A0C9312D57) ] interface IPeopleHolder : IUnknown { [helpstring("method GetPersonEnum")] HRESULT GetPersonEnum([out] IEnumPerson** ppEnumPerson); };
The next ATL Simple Object is named CoPerson. This coclass represents the internal subobjects that CoPeopleHolder is responsible for containing. The [default] interface of CoPerson defines two COM properties: Name and ID. CoPeopleHolder will still need to maintain an array of IPerson interfaces, which will be created during the FinalConstruct() call and freed from within FinalRelease():
// Create all the people when CoPeopleHolder comes to life. HRESULT CoPeopleHolder::FinalConstruct() { CComBSTR name[MAX] = {L"Fred", L"Alice", L"Joe", L"Mitch", L"Beth", L"Mikey", L"Bart", L"Mary", L"Wally", L"Pete"}; for(int i = 0; i < MAX; i++) { CComObject<CPerson>* pNewPerson = NULL; CComObject<CPerson>::CreateInstance(&pNewPerson); pNewPerson->put_ID(i); pNewPerson->put_Name(name[i].Copy()); // Get IPerson from the new object and add to the array. IPerson* pPerson = NULL; pNewPerson->QueryInterface(IID_IPerson, (void**)&pPerson); m_theFolks[i] = pPerson; } return S_OK; } // Destroy all the subobjects. void CoPeopleHolder::FinalRelease() { for(int i = 0; i < MAX; i++) { m_theFolks[i]->Release(); } }
Now that we have an array of IPerson interfaces, we can make use of CComEnum<> to build the COM enumerator object. Before we see the final code block, we need to think through some steps. We will be creating a CComEnum<> object that takes four parameters:
The name of the IEnumXXXX interface it will be implementing.
The GUID of the custom enumerator interface.
The name of the interface the enumerator is handling.
The _Copy<> class, which will be used to take care of copy semantics.
Initially, we can envision the code as so:
// Sending in the necessary parameters to CComEnum<>. CComEnum<IEnumPerson, &IID_IEnumPerson, IPerson*, _CopyInterface<IPerson> >
Note | Be very careful here. You must have a space between the closing > > in the CComEnum<> definition. If you do not, the compiler will think you have typed a >> bit shift operator! |
Now, as we are in ATL land we need to create our COM objects using CComObject<>, which is also a template! Thus, we need to drop in the previous line of code as the sole parameter to CComObject<>. In all its glory, here is the final line to create the ATL enumerator (held in a typedef to minimize further typing):
// We have three templates to worry about when creating an ATL enumerator. typedef CComObject< CComEnum<IEnumPerson, &IID_IEnumPerson, IPerson*, _CopyInterface<IPerson> > > EnumPerson;
Once we have created the coclass, we need to call the Init() method of the ATL enumerator, informing it of the bounds of the IPerson* array and a flag which specifies how ATL should manage the array. Finally, we will give out the IEnumPerson* to the interested client. With this, we can now see all the code behind the GetPersonEnum() method:
// This method is in charge of returning the IEnumPerson interface to the client. STDMETHODIMP CPeopleHolder::GetPersonEnum(IEnumPerson **ppEnumPerson) { // Make the ATL enumeration object. typedef CComObject< CComEnum<IEnumPerson, &IID_IEnumPerson, IPerson*, _CopyInterface<IPerson> > > EnumPerson; EnumPerson* pNewEnum = NULL; EnumPerson::CreateInstance(&pNewEnum); // Now fill the enumerator with all the IPerson objects. pNewEnum->Init(&m_theFolks[0], &m_theFolks[MAX], NULL, AtlFlagCopy); // Now return the enumerator. return pNewEnum->QueryInterface(IID_IEnumPerson, (void**)ppEnumPerson); }
The final parameter of the Init() method is used to specify how you wish your ATL enumerator object to handle the contents of the array. The set of permissible values is obtained from the following (C++!) enumeration, defined in < atlcom.h>:
// You may initialize your ATL enumeration with any of the following flags. enum CComEnumFlags { AtlFlagNoCopy = 0, AtlFlagTakeOwnership = 2, AtlFlagCopy = 3 };
Here is a breakdown of what each flag signifies:
Init() Flag | Meaning in Life |
---|---|
AtlFlagNoCopy | Signifies that the container object already has an existing array of items it is maintaining, and will share this set with the ATL enumerator object. |
AtlFlagCopy | Signifies that although the container has an existing array of items, the ATL enumerator should maintain a separate copy of the data. |
AtlFlagTakeOwnership | Informs the ATL enumerator that the container does not have a private copy of the data, and will fill the enumerator with data it will not be maintaining itself. In other words, the container does not have private state data representing the subobjects. |
In our example, as CoPeopleHolder does have a private copy of IPerson interfaces, we can specify either AtlFlagNoCopy or AtlFlagCopy.
To see our tester in action, here is a C++ client making use of the Visual C++ native COM support:
// Using the ATL enumerator (assume we have #import-ed the *.tlb file). int main(int argc, char* argv[]) { CoInitialize(NULL); HRESULT hr; ULONG uElementsRead; IPersonPtr pPerson[20]; IPeopleHolderPtr pPeople(CLSID_PeopleHolder); IEnumPersonPtr pEnum; pPeople->GetPersonEnum(&pEnum); hr = pEnum->Next(90, (IPerson**)&pPerson, &uElementsRead); for(int i = 0; i < uElementsRead; i++) { _bstr_t name; name = pPerson[i]->GetName(); cout << "Name is: " << name << endl; cout << "ID is: " << pPerson[i]->GetID() << endl; pPerson[i] = NULL; } pPeople = NULL; pEnum = NULL; CoUninitialize(); return 0; }
The next lab will allow you to build this enumerator for yourself. After that point, we will get to know COM collections, the language-neutral variation on the COM enumerator pattern. COM enumerations are very C++ centric.
In this lab you will build a COM enumerator with the assistance of ATL. You will define a custom variation on the IEnumXXXX interface, and use the ATL framework to help flesh out the details of Next(), Skip(), Reset(), and Clone(). Once you have developed the enumerator, you will build a C++ client to test the server.
On the CD The solution for this lab can be found on your CD-ROM under:
Labs\Chapter 11\ATLEnumServer
Labs\Chapter 11\ATLEnumServer\CPP Client
Begin by creating a new ATL DLL project named ATLEnumServer. Use the ATL Object Wizard to insert a new Simple Object named PeopleHolder. Be sure to select the Custom vTable option for your initial [default] interface (IPeopleHolder). This coclass will be used to hold onto a number of CPeople objects, and provide access to these items through a custom enumerator. Add a single method to IPeopleHolder named GetPersonEnum(). This method will take a single parameter of type IEnumPerson** (which we will define in the next step). Here is the IDL for IPeopleHolder:
// Clients can access our CPeople objects from this custom enumerator interface. interface IPeopleHolder : IUnknown { [helpstring("method GetPersonEnum")] HRESULT GetPersonEnum([out] IEnumPerson** ppEnumPerson); };
Now, we need to insert another ATL Simple Object to represent the items held by the enumerator (people objects). Insert a new ATL Simple Object named Person. This coclass will have two properties in the [default] interface: Name and ID. Here is the IDL for IPerson:
// Each CPerson implements this [default] interface. interface IPerson : IUnknown { [propget] HRESULT Name([out, retval] BSTR *pVal); [propput] HRESULT Name([in] BSTR newVal); [propget] HRESULT ID([out, retval] short *pVal); [propput] HRESULT ID([in] short newVal); };
Implement these properties in the CPerson coclass, using a CComBSTR to represent the Name property and a short to represent the ID:
// Implementing the person sub object. STDMETHODIMP CPerson::get_Name(BSTR *pVal) { *pVal = m_bstrName.Copy(); return S_OK; } STDMETHODIMP CPerson::put_Name(BSTR newVal) { m_bstrName = newVal; return S_OK; } STDMETHODIMP CPerson::get_ID(short *pVal) { *pVal = m_ID; return S_OK; } STDMETHODIMP CPerson::put_ID(short newVal) { m_ID = newVal; return S_OK; }
Now we need to create a number of these CPerson objects when CPeopleHolder comes to life. Add a private array of type IPerson* in the private sector of CPeopleHolder, and populate this array inside FinalConstruct(). Assume the MAX constant has been set to 10, and fill each person with some initial data.
// Fill your IPerson* array with data when the CPeopleHolder is created. // The class defines: IPerson* m_theFolks[MAX]; HRESULT FinalConstruct() { // Some names for data. CComBSTR name[MAX] = {L"Fred", L"Alice", L"Joe", L"Mitch", L"Beth", L"Mikey", L"Bart", L"Mary", L"Wally", L"Pete" }; for(int i = 0; i < MAX; i++) { // Make a new person. CComObject<CPerson>* pNewPerson = NULL; CComObject<CPerson>::CreateInstance(&pNewPerson); pNewPerson->put_ID(i); pNewPerson->put_Name(name[i].Copy()); // Get IPerson from the new object and place in array. IPerson* pPerson = NULL; pNewPerson->QueryInterface(IID_IPerson, (void**)&pPerson); m_theFolks[i] = pPerson; } return S_OK; }
Because the CPeopleHolder class has created a number of CPerson objects, we need to ensure that the objects are released during the FinalRelease():
// Free up each CPerson in the IPerson* array when CPeopleHolder is destroyed. void FinalRelease() { for(int i = 0; i < MAX; i++) { m_theFolks[i]->Release(); } }
Now that we have a container for the CPerson objects, our next step is to implement the GetPersonEnum() method, which is currently empty. For that matter, we need to define IEnumPerson!
The people holder object allows COM clients to access the subobject it is maintaining using a custom enumerator interface. Write some IDL code to define the following IEnumPerson interface (sorry, there is no wizard for this step):
// The custom enumerator interface that allows controlled access to all sub objects. [ object, uuid(B5F10D50-53F1-11d3-AB20-00A0C9312D57) ] interface IEnumPerson : IUnknown { HRESULT Next([in] ULONG celt, [out, size_is(celt), length_is(*pCeltFetched)] IPerson** rgVar, [out] ULONG * pCeltFetched); HRESULT Skip([in] ULONG celt); HRESULT Reset(); HRESULT Clone([out] IEnumPerson ** ppEnum); };
Recall that the Next() method will allow a client to request some number of IPerson interfaces. Skip() allows the client to, well, skip over items in the IPerson* array. Reset() moves the internal cursor to the beginning of the array, while Clone() allows the client to maintain a snapshot of the current enumerator and obtain a new IEnumPerson enumeration. All of this logic will be supplied by the core set of ATL enumeration templates.
ATL does a great job of implementing the four methods for a custom enumerator interface, as long as you can follow the syntactic maze. In your implementation of GetPersonEnum(), use CComObject<> to create a new CComEnum<> object. When you initialize the CComEnum<> object, send in the upper and lower bounds of your IPerson* array. Finally, before you exit, set the client-supplied IEnumPerson** parameter to the IEnumPerson interface of your custom ATL enumerator object:
// This method is in charge of returning the IEnumPerson interface // to the client. STDMETHODIMP CPeopleHolder::GetPersonEnum(IEnumPerson **ppEnumPerson) { // Create a new CComEnum<> of IPerson* interfaces. typedef CComObject< CComEnum<IEnumPerson, &IID_IEnumPerson, IPerson*, _CopyInterface<IPerson> > > EnumPerson; EnumPerson* pNewEnum = NULL; EnumPerson::CreateInstance(&pNewEnum); // Now fill the enumerator with copies of the IPerson objects. pNewEnum->Init(&m_theFolks[0], &m_theFolks[MAX], NULL, AtlFlagCopy); // Now return the enumerator (client will call Release()). return pNewEnum->QueryInterface(IID_IEnumPerson, (void**)ppEnumPerson); }
Go ahead and compile the project to ensure there are no syntactical errors. In the final step, we will build a C++ client to work with this enumerator object (remember, COM enumerators are currently not directly usable from VB or late bound clients).
Create a new C++ Console Application. To help minimize the COM goo, we will make use of the #import statement, rather than raw COM library calls. We have already seen similar code in this chapter, but for completion, here is the full implementation of main():
// The C++ client. #import "C:\ATL\Labs\Chapter 11\ATLEnumServer\ATLEnumServer.tlb" no_namespace named_guids int main(int argc, char* argv[]) { CoInitialize(NULL); IPeopleHolderPtr pPeople(CLSID_PeopleHolder); IEnumPersonPtr pEnum; pPeople->GetPersonEnum(&pEnum); HRESULT hr; ULONG uElementsRead; IPersonPtr pPerson[20]; hr = pEnum->Next(90, (IPerson**)&pPerson, &uElementsRead); // I asked for 90 people, but the object returns how many were fetched. for(int i = 0; i < uElementsRead; i++) { _bstr_t name; name = pPerson[i]->GetName(); cout << "Name is: " << name << endl; cout << "ID is: " << pPerson[i]->GetID() << endl; pPerson[i] = NULL; } pPeople = NULL; pEnum = NULL; CoUninitialize(); return 0; }
When you run your program, you will see the following output:
Figure 11-4: Using a collection of COM objects.
Not too bad. ATL has done most of the grunge work for you. When you are creating a COM enumeration with ATL, simply design the IEnumXXXX interface and the subobjects it is providing access to, and make use of the various framework templates.
As mentioned in this chapter, COM enumerators do not provide a standard way to insert additional items into the container. Furthermore, many COM language mappings do not have a way to work with custom enumeratations. The next logical step is to examine COM collections, which are more or less language-neutral enumerators.
| < Free Open Study > |
|