This documentation is archived and is not being maintained.

Extending the ATL Framework Part 3: A Reusable Design

Visual Studio .NET 2003
 

Martin Lapierre
DevInstinct.com

February 2002

Summary: In this last part of the ATL Extension Series, a list component with proxy characteristics is created, along with a Marshalling by Value (MBV) map component. This article assumes you are familiar with COM, ATL, STL collections, persistence, and marshalling. (10 printed pages)

Download Sample

Contents

Introduction
Project Setup
Creating a List Component
Building an Enhanced Map Sample
Testing the Collections
ATL Extension Code Updates
Conclusion

Introduction

In this series of articles we explore the design of the ATL Framework and use the framework as a model to create extended support for persistence and marshalling. In this last part of the ATL Extension Series we create a smart, persistent, list component with proxy characteristics, and a persistent Marshalling by Value (MBV) map component in the spirit of ATL.

Project Setup

The ATL Extension is built for reusability and is designed in a manner so coding of common-code for custom marshalled COM-collections is no longer necessary. The following collections meet this objective while providing persistence and scripting engines support.

The ATLExtDemo download is a simple COM DLL created with the ATL COM AppWizard. The ATLExtDemo download includes the ATLExtDemo.vbs script file used to test the collections. The download features COM calls that trace in debug mode through the definition of the ATL_TRACE_CATEGORY to atlTraceCOM.

To set up the project for debugging follow these steps:

  1. Edit the settings.
  2. Specify the location of the wscript.exe script engine (usually located in \winnt\system32\) as the executable for the debug session.
  3. Type the full path name of the ATLExtDemo.vbs file as the program argument(s).

For more information on ATLExtDemo.vbs, see Testing the Collections.

ATLExtDemo.idl contains COM interfaces used by the collections. ATLExtDemo.idl defines IATLExtCollection, which is used as the base interface for our collections. For IATLExtCollection to support scripting, IATLExtCollection is constructed as a dual interface that manages the VARIANT type with basic properties. The following example excludes interface and properties attributes.

interface IATLExtCollection : IDispatch
{
  HRESULT _NewEnum([out, retval] IUnknown **ppIEnum);
  HRESULT Item([in] VARIANT *pvaIndex, [out, retval] VARIANT *pvaItem);
  HRESULT Count([out, retval] long *plCount);
};

Creating a List Component

The ATL Framework handles the basic COM collection properties. You will need to supply additional functionality to avoid a read-only collection. Our list component is dynamic and offers element addition and removal, as shown by its interface:

interface IATLExtList : IATLExtCollection
{
  HRESULT Add([in] VARIANT *pvaItem, [in, defaultvalue(ATLEXT_COLL_FIRST)]
long lIndex);
  HRESULT Remove([in, defaultvalue(ATLEXT_COLL_LAST)] long lIndex);
  HRESULT RemoveAll();
};

The necessary ATL support comes from the CComEnumOnSTL and ICollectionOnSTLImpl class templates. CComEnumOnSTL provides the enumerator for our collection, and ICollectionOnSTLImpl provides the basic collection properties implementation.

We will use CComVariant as our STL list element type, and will use _Copy<VARIANT> as our class policy for copying elements from the STL list to the enumerator, and to the output parameter of the Item property.

Note   ATL supports only long-based indexes for the Item property. Consequently, the download includes a custom implementation.

The following example shows the associated typedefs.

typedef std::list<CComVariant> STLListCComVariant;

typedef CComEnumOnSTL< IEnumVARIANT, 
                       &IID_IEnumVARIANT, 
                       VARIANT, 
                       _Copy<VARIANT>, 
                       STLListCComVariant > CComEnumVariantOnSTLList;

typedef ICollectionOnSTLImpl< IATLExtList, 
                              STLListCComVariant, 
                              VARIANT, 
                              _Copy<VARIANT>, 
                              CComEnumVariantOnSTLList > IATLExtListOnSTLListImpl;

Adding persistence and marshalling support for our collection is accomplished with the IPersistStreamInitOnSTLImpl and IMarshalOnSTLImpl class templates designed in the second part of the ATL Extension Series.

As shown in the next example, both IPersistStreamInitOnSTLImpl and IMarshalOnSTLImpl use identical template parameters:

  • The component class (our example uses CList).
  • The STL collection type.
  • The element(s) type to persist and marshal.
  • An optional class policy that defaults to _Persist<ItemType> (our example uses _Persist<VARIANT>.
class CList;

typedef ATLExt::IPersistStreamInitOnSTLImpl< CList, 
                                             STLListCComVariant,
                                             VARIANT > IPersistStreamInitOnSTLListImpl;

typedef ATLExt::IMarshalOnSTLImpl< CList,
                                   STLListCComVariant,
                                   VARIANT > IMarshalOnSTLListImpl;

typedef IPersistStorageImpl< CList > IPersistStorageOnSTLListImpl;

Review the definition of IPersistStorageOnSTLListImpl again. Since IPersistStreamInitOnSTLImpl works well with IPersistStorageImpl (IPersistStorageImpl uses IPersistStreamInit) and IPersistStreamInitImpl, we will provide free, basic persistence for structured storage.

To create our component class called CList, CList should inherit the different types we have defined:

class ATL_NO_VTABLE CList : 
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CList, &CLSID_List>,
  public IDispatchImpl<IATLExtListOnSTLListImpl, &IID_IATLExtList,
&LIBID_ATLEXTDEMOLib>,
  public IPersistStreamInitOnSTLListImpl,
  public IPersistStorageOnSTLListImpl,
  public IMarshalOnSTLListImpl

The COM Map includes simple entries for the collection interfaces, the persistence standard interfaces, and the IMarshal interface for marshalling support. The base interface(s) is now exposed to promote our component's wide reusability.

BEGIN_COM_MAP(CList)
  COM_INTERFACE_ENTRY(IATLExtCollection)
  COM_INTERFACE_ENTRY(IATLExtList)
  COM_INTERFACE_ENTRY(IDispatch)
  COM_INTERFACE_ENTRY(IPersistStreamInit)
  COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
  COM_INTERFACE_ENTRY(IPersistStorage)
  COM_INTERFACE_ENTRY(IMarshal)
END_COM_MAP()

The ATL persistence mechanism requires you to add the m_bRequiresSave member.

Building an Enhanced Map Sample

The balance of this article discusses how the global functions and class policies, developed in the second part of the ATL Extension Series, are used to enhance ATL seamlessly.

The following map offers the functionality of any generic, scriptable dictionary. However, to make persistence somewhat complicated, we will add these properties:

  • An ID (a long value).
  • A name (a BSTR).

How to use ATL persistence in conjunction with STL collection persistence and the BSTR persistence class policy is revealed as you progress through the balance of this article.

The next example lists the COM interfaces for the map. Note that our additional properties are kept in a separate interface that is different than the generic map interface. See the full source code for their implementation.

interface IATLExtMap : IATLExtCollection
{
  HRESULT Add([in] VARIANT *pvaKey, [in] VARIANT *pvaItem);
  HRESULT Remove([in] VARIANT *pvaKey);
  HRESULT RemoveAll();
  HRESULT Exists([in] VARIANT *pvaKey, [out, retval] VARIANT_BOOL
*pvabExists);
  HRESULT Items([out, retval] IATLExtList **ppICollItems);
  HRESULT Keys([out, retval] IATLExtList **ppICollKeys);
};
interface IATLExtMapEn : IATLExtMap
{
  HRESULT ID([out, retval] long *pVal);
  HRESULT ID([in] long newVal);
  HRESULT Name([out, retval] BSTR *pVal);
  HRESULT Name([in] BSTR newVal);
};

ATL does not provide a proper copy class policy for an STL pair element. Consequently, a custom class policy is required before proceeding with the usual typedefs:

class _CopyMapToVariant
{
public:
  static HRESULT copy(VARIANT* p2, std::pair<CComBSTR const,
CComVariant> *p1)
    {return VariantCopy(p2, &p1->second);}
  static void init(VARIANT* p) {VariantInit(p);}
  static void destroy(VARIANT* p) {VariantClear(p);}
};

typedef std::map<CComBSTR, CComVariant> STLMapCComBSTR2CComVariant;

typedef CComEnumOnSTL< IEnumVARIANT, 
                       &IID_IEnumVARIANT, 
                       VARIANT, 
                       _CopyMapToVariant, 
                       STLMapCComBSTR2CComVariant > 
CComEnumVariantOnSTLMap;

typedef ICollectionOnSTLImpl< IATLExtMapEn, 
                              STLMapCComBSTR2CComVariant, 
                              VARIANT, 
                              _CopyMapToVariant, 
                              CComEnumVariantOnSTLMap > IATLExtMapOnSTLMapImpl;

CMap is used for the following map. Adding persistence support is done differently than the list component. First, we need to use a property map to store our ID property, and use IPersistStreamInitImpl to persist it.

Note   For Microsoft® Visual Studio® version 6.0, we use a version called IPersistStreamInitImpl2. IPersistStreamInitImpl2 implements the GetSizeMax method and is provided by Chris Sells and Brent Rector (see references for ATL Internals).

Marshalling by Value (MBV) is supported by delegating IMarshal calls to IPersistStreamInit methods with the IMarshalOnStreamImpl class template. For more information, see the first part of the ATL Extension Series.

class CMap;

#if _MSC_VER < 1300
typedef ATLInternals::IPersistStreamInitImpl2< CMap > 
IPersistStreamInitImplForMap;
#else
typedef IPersistStreamInitImpl< CMap > IPersistStreamInitImplForMap;
#endif

typedef ATLExt::IMarshalOnStreamImpl< CMap > IMarshalOnStreamImplForMap;

typedef IPersistStorageImpl< CMap > IPersistStorageImplForMap;

Typedefs are optional. However, typedefs produce cleaner code. The class declaration becomes:

class ATL_NO_VTABLE CMap : 
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CMap, &CLSID_Map>,
  public IDispatchImpl<IATLExtMapOnSTLMapImpl, &IID_IATLExtMapEn,
&LIBID_ATLEXTDEMOLib>,
  public IPersistStreamInitImplForMap,
  public IPersistStorageImplForMap,
  public IMarshalOnStreamImplForMap

Once the ID property is situated in a property map, the ID property is smoothly persisted and marshaled by our map component:

BEGIN_PROP_MAP(CMap)
  PROP_DATA_ENTRY("ID", m_lID, VT_UI4)
END_PROP_MAP()

Next, persistence for the STL collection (the m_coll member defined by the ATL Framework), and for the Name property, are added. Add persistence by overriding the delegate methods of IPersistStreamInitImpl in the component class. The global functions used to persist the STL collection in IPersisStreamInitOnSTLImpl and the class policies determine the success of this exercise.

Next, define a custom persistence class policy for an STL pair element. The policy must write on and read off:

  • The stream.
  • The key name and its associated item.

The policy must also compute the required size of the pair. Another STL map detail to consider is that a key is stored as a constant, and to properly manage the pair, two overloaded destroy methods are necessary.

class _PersistMap
{
public:
  static HRESULT Load(std::pair<CComBSTR, CComVariant> *pT, LPSTREAM pStm)
  {
    TestHR(ATLExt::_Persist<BSTR>::Load(&pT->first, pStm))
    return ATLExt::_Persist<VARIANT>::Load(&pT->second, pStm);
  }
  static HRESULT Save(std::pair<CComBSTR const, CComVariant> *pT, 
                      LPSTREAM pStm, ATLExt::_MarshalInfo 
*pMshlInfo = NULL)
  {
    CComBSTR *pccbstrNoConst = const_cast<CComBSTR*>(&pT->first);
    TestHR(ATLExt::_Persist<BSTR>::Save(&*pccbstrNoConst, pStm))
    return ATLExt::_Persist<VARIANT>::Save(&pT->second, pStm);
  }
  static HRESULT GetSizeMax(std::pair<CComBSTR const, CComVariant> *pT, 
                            ULARGE_INTEGER *pcbSize, ATLExt::_MarshalInfo
*pMshlInfo = NULL)   
  {
    ULARGE_INTEGER cbBSTRSize = {0};
    CComBSTR *pccbstrNoConst = const_cast<CComBSTR*>(&pT->first);
    TestHR(ATLExt::_Persist<BSTR>::GetSizeMax(&*pccbstrNoConst,
&cbBSTRSize))
    TestHR(ATLExt::_Persist<VARIANT>::GetSizeMax(&pT->second, pcbSize,
pMshlInfo))
    pcbSize->QuadPart += cbBSTRSize.QuadPart;
    return S_OK;
  }
  static void init(std::pair<CComBSTR, CComVariant>*) {}
  static void destroy(std::pair<CComBSTR const, CComVariant> *pT) 
    {pT->second.Clear();}
  static void destroy(std::pair<CComBSTR, CComVariant> *pT) 
    {pT->first.Empty(); pT->second.Clear();}
};

The Name property of the CMap class can also be persisted by Persist<BSTR>. In the end, approximately three lines of code will associate everything we have done with the framework methods. The next example is the override of IPersistStreamInit_Save for our map collection.

HRESULT CMap::IPersistStreamInit_Save(LPSTREAM pStm, BOOL fClearDirty, 
ATL_PROPMAP_ENTRY* pMap)
{ 
  // Save the property map.
  TestHR(IPersistStreamInitImplForMap::IPersistStreamInit_Save(pStm, 
fClearDirty, pMap))
   
  // Save the STL collection.
  TestHR((AtlPersistOnSTL_Save<STLMapCComBSTR2CComVariant, pair<CComBSTR,
CComVariant>, _PersistMap>(pStm, fClearDirty, &m_coll)))

  // Save the Name property.
  TestHR(_Persist<BSTR>::Save(&m_ccbstrName.m_str, pStm))

  if (fClearDirty) 
// IPersistStreamInitImpl does not clear the dirty flag.
    m_bRequiresSave = FALSE;
  return S_OK;
}

The previous example shows that the strength of the framework comes from its widely reusable design. We could have easily persisted any contained interface or VARIANT member with the _PersistInterface or _Persist<VARIANT> class policies. The same strategy can be applied to IPersistStreamInitOnSTLImpl, IMarshalOnSTLImpl, and other framework templates.

Testing the Collections

The ATLExtDemo.vbs script creates a list and inserts a map in it. Then ATLExtDemo.vbs marshals the list by value (and by the same occasion the contained map). For this to work locally, it uses the ATLExtDemo.MBVUtil component, which exposes a single method called ForceMBV. ForceMBV invokes CoMarshalInterface with the destination context set to MSHCTX_DIFFERENTMACHINE, which causes our marshalling implementation to be called.

While running the script, pay attention to the COM interfaces as they are called in the debug window. Modifying the list of interfaces exposed by the components, adding to the collections non-persistent components, and tracing _PersistInterface methods and the ATL Extension global functions illustrates how the framework handles the varied possibilities.

ATL Extension Code Updates

For the latest code update, see ATLExtension Update.

Conclusion

Building on the ATL Framework provides flexible options including:

  • A reusable Marshalling by Value (MBV) solution.
  • Generic persistence and marshalling mechanisms for STL collections, VARIANTs, BSTRs, and interfaces pointers.

About the Author

Martin Lapierre is a freelance writer and consultant hosted in Montreal, Quebec, Canada. He holds a BS in Computer Science from the University of Montreal and has been designing desktop, n-tiers, and Web applications on the Windows platform since 1995.

References

Show: