Implementing an STL-Based Collection

ATL Collections and Enumerators

ATL provides the ICollectionOnSTLImpl interface to enable you to quickly implement collection interfaces on your objects. To understand how this class works, you will work through a simple example (below) that uses this class to implement a read-only collection aimed at Visual Basic clients.

To complete this procedure, you will:

Generating a New Simple Object

First, create a new project and use the ATL COM AppWizard to generate a Simple Object called Words. Make sure that a dual interface called IWords is generated. Objects of the generated class will be used to represent a collection of words (that is, strings).

Editing the IDL File

Now, open the IDL file and add the three properties necessary to turn IWords into a read-only collection interface, as shown below:

    [
        object,
        uuid(0D44F689-B373-11D2-9A7F-50F653C10000),
        dual,                                             // (1)
        pointer_default(unique),
        nonextensible                                     // (2)
    ]
    interface IWords : IDispatch
    {
        [id(DISPID_NEWENUM), propget]                     // (3)
        HRESULT _NewEnum([out, retval] IUnknown** ppUnk);

        [id(DISPID_VALUE), propget]                       // (4)
        HRESULT Item(
                        [in] long Index, 
                        [out, retval] BSTR* pVal);        // (5)

        [id(0x00000001), propget]                         // (6)
        HRESULT Count([out, retval] long* pVal);
    };

This is the standard form for a read-only collection interface designed with Visual Basic clients in mind. The numbered comments in this interface definition correspond to the comments below:

  1. Collection interfaces are usually dual because Visual Basic accesses the _NewEnum property via IDispatch::Invoke (so pure vtable interfaces don't allow you to support Visual Basic's For Each...Next syntax). However, Visual Basic clients can access the remaining methods via the vtable, so dual interfaces are preferable to dispinterfaces.

  2. If a dual interface or dispinterface will not be extended at run-time (that is, you won't provide extra methods or properties via IDispatch::Invoke), you should apply the nonextensible attribute to your definition. This attribute enables Visual Basic to perform full code verification at compile time. In this case, the interface should not be extended.

  3. The correct DISPID is important if you want Visual Basic to be able to use this property. (Note that there is only one underscore in DISPID_NEWENUM).

  4. You can supply any value as the DISPID of the Item property. However, Item typically uses DISPID_VALUE to make it the default property of the collection. This allows Visual Basic clients to refer to the property without naming it explicitly.

  5. The data type used for the return value of the Item property is the type of the item stored in the collection as far as COM clients are concerned. The interface returns strings, so you should use the standard COM string type, BSTR. You can store the data in a different format internally as you'll see shortly.

  6. The value used for the DISPID of the Count property is completely arbitrary. There's no standard DISPID for this property.

Creating Typedefs for Storage and Exposure

Once the collection interface is defined, you need to decide how the data will be stored, and how the data will be exposed via the enumerator.

The answers to these questions can be provided in the form of a number of typedefs, which you can add near the top of the header file for your newly created class:

    // Store the data in a vector of std::strings
    typedef std::vector< std::string >         ContainerType;

    // The collection interface exposes the data as BSTRs
    typedef BSTR                               CollectionExposedType;
    typedef IWords                             CollectionInterface;

    // Use IEnumVARIANT as the enumerator for VB compatibility
    typedef VARIANT                            EnumeratorExposedType;
    typedef IEnumVARIANT                       EnumeratorInterface;

In this case, you will store the data as a std::vector of std::strings. std::vector is an STL container class that behaves like a managed array. std::string is the Standard C++ Library's string class. These classes make it easy to work with a collection of strings.

Since Visual Basic support is vital to the success of this interface, the enumerator returned by the _NewEnum property must support the IEnumVARIANT interface. This is the only enumerator interface understood by Visual Basic.

Creating Typedefs for Copy Policy Classes

The typedefs you have created so far provide all the information you need to create further typedefs for the copy classes that will be used by the enumerator and collection:

    // Typedef the copy classes using existing typedefs
    typedef VCUE::GenericCopy<EnumeratorExposedType, ContainerType::value_type>    EnumeratorCopyType;
    typedef VCUE::GenericCopy<CollectionExposedType, ContainerType::value_type>    CollectionCopyType;

In this example, you can use the custom GenericCopy class defined in VCUE_Copy.h and VCUE_CopyString.h from the ATLCollections project. You can use this class in other code, but you may need to define further specializations of GenericCopy to support data types used in your own collections. For more information, see ATL Copy Policy Classes.

Creating Typedefs for Enumeration and Collection

Now all the template parameters necessary to specialize the CComEnumOnSTL and ICollectionOnSTLImpl classes for this situation have been provided in the form of typedefs. To simplify the use of the specializations, create two more typedefs as shown below:

    typedef CComEnumOnSTL< EnumeratorInterface, &__uuidof(EnumeratorInterface), EnumeratorExposedType, EnumeratorCopyType, ContainerType >    EnumeratorType;

    typedef ICollectionOnSTLImpl< CollectionInterface, ContainerType, CollectionExposedType, CollectionCopyType, EnumeratorType >    CollectionType;

Now CollectionType is a synonym for a specialization of ICollectionOnSTLImpl that implements the IWords interface defined earlier and provides an enumerator that supports IEnumVARIANT.

Editing the Wizard-Generated Code

Now you must derive CWords from the interface implementation represented by the CollectionType typedef rather than IWords, as shown below:

class ATL_NO_VTABLE CWords : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CWords, &CLSID_Words>,
    public IDispatchImpl<CollectionType, &IID_IWords, &LIBID_ATLCOLLECTIONSLib>
{
public:
    DECLARE_REGISTRY_RESOURCEID(IDR_STRINGCOLLECTION)
    DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CWords)
    COM_INTERFACE_ENTRY(IWords)
    COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

// IWords
public:
};

Adding Code to Populate the Collection

The only thing that remains is to populate the vector with data. In this simple example, you can add a few words to the collection in the constructor for the class:

    CWords()
    {
        m_coll.push_back("this");
        m_coll.push_back("is");
        m_coll.push_back("a");
        m_coll.push_back("test");
    }

Now, you can test the code with the client of your choice.