The Persistence
Implementations
Let's look at how the persistence
implementations work, using the property bag persistence
implementation as the example. All persistence implementations are
similar.
The Property
Map
The property map macros basically add a static
member function called GetPropertyMap to your class.
GetPropertyMap returns a pointer to an array of
ATL_PROPMAP_ENTRY structures. The structure looks like
this:
struct ATL_PROPMAP_ENTRY {
LPCOLESTR szDesc;
DISPID dispid;
const CLSID* pclsidPropPage;
const IID* piidDispatch;
DWORD dwOffsetData;
DWORD dwSizeData;
VARTYPE vt;
};
For example, here's a property map and the
resulting macro expansion:
BEGIN_PROP_MAP(CDemagogue)
PROP_ENTRY("Speech", DISPID_SPEECH, CLSID_NULL)
PROP_ENTRY_EX("Name", DISPID_NAME, CLSID_NULL,
IID_INamedObject)
PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
END_PROP_MAP()
This property map expands to this:
__if_not_exists(__ATL_PROP_NOTIFY_EVENT_CLASS) {
typedef ATL::_ATL_PROP_NOTIFY_EVENT_CLASS
__ATL_PROP_NOTIFY_EVENT_CLASS;
}
static ATL::ATL_PROPMAP_ENTRY* GetPropertyMap() {
static ATL::ATL_PROPMAP_ENTRY pPropMap[] = {
{OLESTR(("Speech"), DISPID_SPEECH, &CLSID_NULL,
__uudiof(IID_IDispatch), 0, 0, 0},
{OLESTR("Name"), DISPID_ NAME, &CLSID_NULL,
&IID_INamedObject, 0, 0, 0},
{OLESTR("_cx"), 0, &CLSID_NULL, NULL,
offsetof(_PropMapClass, m_sizeExtent.cx),
sizeof(((_PropMapClass*)0)-> m_sizeExtent.cx), VT_UI4},
{NULL, 0, NULL, &IID_NULL, 0, 0, 0}
};
return pPropMap;
}
The szDesc field of the structure holds
the name of the property. It's used only by the property bag
persistence implementation.
The dispid field contains the
property's dispatch identifier. All the persistence implementations
need this so they can access the property via one of the object's
IDispatch implementations by calling the Invoke
method.
The pclsidPropPage field contains a
pointer to the CLSID for the property page associated with the
object. It's not used during persistence.
The piidDispatch field contains a
pointer to the IID of the dispatch interface that supports this
property. The specified dispid is unique to this
interface.
Only PROP_DATA_ENTRY macros use the
last three fields. The dwOffsetData field contains the
offset of the specified member variable from the beginning of a
class instance. The dwSizeData field contains the size of
the variable in bytes, and the vt field contains the
variable's VARTYPE (VARIANT type enumeration
code).
The various persistence implementations
basically iterate over this map and load or save the properties
listed. For properties listed using PROP_ENTRY and
PROP_ENTRY_EX, the implementations call
IDispatch::Invoke with the specified dispid to
get or put the property.
Invoke transfers each property via a
VARIANT. The stream persistence implementation simply
wraps the variant in a CComVARIANT instance and uses its
ReadFromStream and WriteToStream methods to do
all the hard work. Therefore, stream persistence supports all
VARIANT types that the CComVARIANT persistence
implementation supports (discussed in Chapter 3, "ATL Smart Types"). The property
bag implementation has it even easier because property bags deal
directly in VARIANTs.
For properties listed using
the PROP_DATA_ENTRY macro, things aren't quite so simple.
The IPersistStreamInit implementation directly accesses
the object instance at the specified offset for the specified
length. This reads or writes the specified number of bytes directly
to or from the object.
However, the IPersistPropertyBag
implementation must read and write properties held in a
VARIANT. Therefore, this implementation copies the member
variable of the object to a VARIANT before writing the
property to the bag, and copies a VARIANT to the member
variable after reading the property from the bag. The current
implementation of IPersistPropertyBag persistence supports
only a limited set of VARIANT types; worse, it silently
fails to load and save properties with any VARTYPEs other
than these:
-
VT_UI1, VT_I1: Read and write the
variable as a BYTE.
-
VT_BOOL: Reads and writes the variable
as a VARIANT_BOOL.
-
VT_UI2, VT_I2: Read and write the
variable as a short.
-
VT_UI4, VT_I4, VT_INT, VT_UINT: Read
and write the variable as a long.
-
VT_BSTR: Reads and writes the variable
as a BSTR.
-
Any other VT_*: Silently fail.
IPersistPropertyBagImpl
The IPersistPropertyBagImpl<T>
class implements the IPersistPropertyBag interface
methods. IPersistPropertyBag, like all the persistence
interfaces, derives from IPersist, which has one method:
GetClassID.
interface IPersist : IUnknown
{ HRESULT GetClassID([out] CLSID* pclsid); }
All the persistence-implementation classes have
the same implementation of GetClassID. They call a static
member function named GetObjectCLSID method to retrieve
the CLSID. This method must be in the deriving class (your object's
class) or one of its base classes.
template <class T>
class ATL_NO_VTABLE IPersistPropertyBagImpl
: public IPersistPropertyBag {
public:
...
STDMETHOD(GetClassID)(CLSID *pClassID) {
ATLTRACE(atlTraceCOM, 2, _T("IPersistPropertyBagImpl::GetClassID\n"));
if (pClassID == NULL)
return E_POINTER;
*pClassID = T::GetObjectCLSID();
return S_OK;
}};
Normally, your class obtains its
GetObjectCLSID static member function from
CComCoClass:
template <class T, const CLSID* pclsid = &CLSID_NULL>
class CComCoClass {
public:
...
static const CLSID& WINAPI GetObjectCLSID() {return *pclsid;}
};
This implies that a class must be createable for
it to use the persistence classes. This is reasonable because it
doesn't do much good to save a class to some persistent medium and
then be unable to create a new instance when loading the object
from that medium.
The IPersistPropertyBagImpl<T>
class also implements the remaining IPersistPropertyBag
methods, including, for example, the Load method.
IPersistPropertyBagImpl<T>::Load calls
T::IPersistPropertyBag_Load to do most of the work. This
allows your class to provide this method when it needs a custom
implementation of Load. Normally, your object (class
T) doesn't provide an IPersistPropertyBag_Load
method, so this call vectors to a default implementation provided
by the base class
IPersistPropertyBagImpl<T>::IPersistPropertyBag_Load
method. The default implementation calls the global function
AtlIPersistPropertyBag_Load. This global function iterates
over the property map and, for each entry in the map, loads the
property from the property bag:
template <class T>
class ATL_NO_VTABLE IPersistPropertyBagImpl
: public IPersistPropertyBag {
public:
...
// IPersistPropertyBag
//
STDMETHOD(Load)(LPPROPERTYBAG pPropBag,
LPERRORLOG pErrorLog) {
ATLTRACE(atlTraceCOM, 2, _T("IPersistPropertyBagImpl::Load\n"));
T* pT = static_cast<T*>(this);
ATL_PROPMAP_ENTRY* pMap = T::GetPropertyMap();
ATLASSERT(pMap != NULL);
return pT->IPersistPropertyBag_Load(pPropBag,
pErrorLog, pMap);
}
HRESULT IPersistPropertyBag_Load(LPPROPERTYBAG pPropBag,
LPERRORLOG pErrorLog, ATL_PROPMAP_ENTRY* pMap) {
T* pT = static_cast<T*>(this);
HRESULT hr = AtlIPersistPropertyBag_Load(pPropBag,
pErrorLog, pMap, pT, pT->GetUnknown());
if (SUCCEEDED(hr))
pT->m_bRequiresSave = FALSE;
return hr;
}
...
};
This implementation structure provides three
places where we can override methods and provide custom persistence
support for a non-VARIANT-compatible property. We can
override the Load method itself, in effect directly
implementing the IPersistPropertyBag method.
Alternatively, we can let ATL implement Load while our
object implements IPersistPropertyBag_Load. Finally, we
can let ATL implement Load and
IPersistPropertyBag_Load while we provide a replacement
global function called AtlIPersistPropertyBag_Load and
play some linker tricks so that our object uses our global function
instead of the ATL-provided one.
The most natural method is to implement
Load. Normally, in this implementation, you call the base
class Load method to read all properties described in the
property map, and then read any custom,
non-VARIANT-compatible properties:
HRESULT CMyObject::Load(LPPROPERTYBAG pPropBag,
LPERRORLOG pErrorLog) {
HRESULT hr =
IPersistPropertyBagImpl<CMyObject>::Load(pPropBag,pErrorLog);
if (FAILED (hr)) return hr;
// Read an array of VT_I4
// This requires us to create a "name" for each array element
// Read each element as a VARIANT, then re-create the array
...
}
This approach has a few disadvantages. It's a
minor point, but the object now requires four methods for its
persistence implementation: its Load, the base class
Load, the base class IPersistPropertyBag_Load,
and the AtlIPersistPropertyBag_Load. We could copy the
base class Load implementation into the object's
Load method, but that makes the object more fragile
because future versions of ATL might change its
persistence-implementation technique.
Another slight disadvantage to this approach is
that it is clear from the ATL implementation that the ATL designers
intended for your object to override
IPersistPropertyBag_Load. Note the following code fragment
from the default implementation of Load:
STDMETHOD(Load)(LPPROPERTYBAG pPropBag, LPERRORLOG pErrorLog) {
...
T* pT = static_cast<T*>(this);
...
return pT->IPersistPropertyBag_Load(pPropBag, pErrorLog, pMap);
}
Instead of directly calling
IPersistPropertyBag_Load, which is present in the same
class as the Load method, the code calls the method using
a pointer to the deriving classyour object's class. This provides
the same functionality as making the method virtual, without the
overhead of a virtual function.
Generally, the best solution is to let the
object provide its own implementation of the
IPersistPropertyBag_Load method. In this implementation,
the object can call the global function
AtlIPersistProperyBag_Load and save any
non-VARIANT-compatible properties it possessed. Here's an
example from the BullsEye control described in Chapter 11, "ActiveX Controls."
It contains a property that is an array of long integers.
This can't be described as a VARIANT-compatible type
because it's not a SAFEARRAY, so I can't list the property
in the property map.
HRESULT CBullsEye::IPersistPropertyBag_Load(
LPPROPERTYBAG pPropBag, LPERRORLOG pErrorLog,
ATL_PROPMAP_ENTRY* pMap) {
if (NULL == pPropBag) return E_POINTER;
// Load the properties described in the PROP_MAP
HRESULT hr = AtlIPersistPropertyBag_Load(pPropBag, pErrorLog,
pMap, this, GetUnknown());
if (SUCCEEDED(hr)) m_bRequiresSave = FALSE;
if (FAILED (hr)) return hr;
// Load the indexed property - RingValues
// Get the number of rings
short sRingCount;
get_RingCount (&sRingCount);
// For each ring, read its value
for (short nIndex = 1; nIndex <= sRingCount; nIndex++) {
// Create the base property name
CComBSTR bstrName = OLESTR("RingValue");
// Create ring number as a string
CComVariant vRingNumber = nIndex;
hr = vRingNumber.ChangeType (VT_BSTR);
ATLASSERT (SUCCEEDED (hr));
// Concatenate the two strings to form property name
bstrName += vRingNumber.bstrVal;
// Read ring value from the property bag
CComVariant vValue = 0L;
hr = pPropBag->Read(bstrName, &vValue, pErrorLog);
ATLASSERT (SUCCEEDED (hr));
ATLASSERT (VT_I4 == vValue.vt);
if (FAILED (hr)) {
hr = E_UNEXPECTED;
break;
}
// Set the ring value
put_RingValue (nIndex, vValue.lVal);
}
if (SUCCEEDED(hr)) m_bRequiresSave = FALSE;
return hr;
}
The Save method works symmetrically.
The IPersistPropertyBagImpl<T> class implements the
Save method.
IPersistPropertyBagImpl<T>::Save calls
T::IPersistPropertyBag_Save to do the work. Again, your
object (class T) doesn't normally provide an
IPersistPropertyBag_Save method, so this call vectors to a
default implementation that the base class
IPersistPropertyBagImpl<T>::IPersistPropertyBag_Save
method provides. The default implementation calls the global
function AtlIPersistPropertyBag_Save. This global function
iterates over the property map and, for each entry in the map,
saves the property to the property bag.
template <class T>
class ATL_NO_VTABLE IPersistPropertyBagImpl
: public IPersistPropertyBag {
public:
...
// IPersistPropertyBag
//
STDMETHOD(Save)(LPPROPERTYBAG pPropBag, BOOL fClearDirty,
BOOL fSaveAllProperties) {
ATLTRACE(atlTraceCOM, 2, _T("IPersistPropertyBagImpl::Save\n"));
T* pT = static_cast<T*>(this);
ATL_PROPMAP_ENTRY* pMap = T::GetPropertyMap();
ATLASSERT(pMap != NULL);
return pT->IPersistPropertyBag_Save(pPropBag,
fClearDirty, fSaveAllProperties, pMap);
}
HRESULT IPersistPropertyBag_Save(LPPROPERTYBAG pPropBag,
BOOL fClearDirty, BOOL fSaveAllProperties,
ATL_PROPMAP_ENTRY* pMap) {
T* pT = static_cast<T*>(this);
HRESULT hr;
hr = AtlIPersistPropertyBag_Save(pPropBag, fClearDirty,
fSaveAllProperties, pMap, pT, pT->GetUnknown());
if (fClearDirty && SUCCEEDED(hr)) {
pT->m_bRequiresSave=FALSE;
}
return hr;
}
};
Finally, IPersistPropertyBagImpl
implements the InitNew method this way:
STDMETHOD(InitNew)() {
ATLTRACE(atlTraceCOM, 2,
_T("IPersistPropertyBagImpl::InitNew\n"));
T* pT = static_cast<T*>(this);
pT->m_bRequiresSave = TRUE;
return S_OK;
}
Therefore, you need to override InitNew
directly when you have any initialization to perform when there are
no properties to load.
IPersistStreamInitImpl
The implementation contained in
IPersistStreamInitImpl is quite similar to the one just
described. The Load and Save methods call the
IPersistStreamInit_Load and
IPersistStreamInit_Save methods, which are potentially
provided by the deriving object but typically provided by the
default implementation in IPersistStreamInitImpl. These
implementations call the global helper functions
AtlIPersistStreamInit_Load and
AtlIPersistStreamInit_Save.
template <class T>
class ATL_NO_VTABLE IPersistStreamInitImpl
: public IPersistStreamInit {
public:
...
// IPersistStream
STDMETHOD(Load)(LPSTREAM pStm) {
ATLTRACE(atlTraceCOM, 2,
_T("IPersistStreamInitImpl::Load\n"));
T* pT = static_cast<T*>(this);
return pT->IPersistStreamInit_Load(pStm,
T::GetPropertyMap());
}
HRESULT IPersistStreamInit_Load(LPSTREAM pStm,
ATL_PROPMAP_ENTRY* pMap) {
T* pT = static_cast<T*>(this);
HRESULT hr =
AtlIPersistStreamInit_Load(pStm, pMap, pT,
pT->GetUnknown());
if (SUCCEEDED(hr)) pT->m_bRequiresSave = FALSE;
return hr;
}
STDMETHOD(Save)(LPSTREAM pStm, BOOL fClearDirty) {
T* pT = static_cast<T*>(this);
ATLTRACE(atlTraceCOM, 2,
_T("IPersistStreamInitImpl::Save\n"));
return pT->IPersistStreamInit_Save(pStm, fClearDirty,
T::GetPropertyMap());
}
HRESULT IPersistStreamInit_Save(LPSTREAM pStm,
BOOL fClearDirty, ATL_PROPMAP_ENTRY* pMap) {
T* pT = static_cast<T*>(this);
return AtlIPersistStreamInit_Save(pStm, fClearDirty,
pMap, pT, pT->GetUnknown());
}
};
IPersistStreamInitImpl also implements the
InitNew method this way:
STDMETHOD(InitNew)() {
ATLTRACE(atlTraceCOM, 2,
_T("IPersistStreamInitImpl::InitNew\n"));
T* pT = static_cast<T*>(this);
pT->m_bRequiresSave = TRUE;
return S_OK;
}
Therefore, as with property bags, you need to
override InitNew directly when you have any initialization
to perform when there are no properties to load.
The implementation of the IsDirty
method assumes the presence of a member variable named
m_bRequiresSave somewhere in your class hierarchy.
STDMETHOD(IsDirty)() {
ATLTRACE(atlTraceCOM, 2,
_T("IPersistStreamInitImpl::IsDirty\n"));
T* pT = static_cast<T*>(this);
return (pT->m_bRequiresSave) ? S_OK : S_FALSE;
}
The persistence implementations originally
assumed that only ActiveX controls would use them, as if controls
were the only objects that needed a persistence implementation.
Although ATL has greatly reduced the coupling between the control
classes and the persistence implementation, the
CComControlBase class normally provides the
m_bRequiresSave variable and the SetDirty and
Getdirty helper functions usually used to access the
variable.
class ATL_NO_VTABLE CComControlBase {
public:
void SetDirty(BOOL bDirty) { m_bRequiresSave = bDirty; }
// Obtain the dirty state for the control
BOOL GetDirty() { return m_bRequiresSave; }
...
unsigned m_bRequiresSave:1;
};
To use the persistence-implementation classes in
an object that doesn't derive from CComControlBase, you
need to define the m_bRequiresSave variable in your class
hierarchy somewhere. Typically, for convenience, you also define
the SetDirty and Getdirty helper methods.
Noncontrols can use this class to provide this persistence
support:
class ATL_NO_VTABLE CSupportDirtyBit {
public:
CSupportDirtyBit() : m_bRequiresSave(FALSE) {}
void SetDirty(BOOL bDirty) {
m_bRequiresSave = bDirty ? TRUE : FALSE;
}
BOOL GetDirty() { return m_bRequiresSave ? TRUE : FALSE; }
BOOL m_bRequiresSave;
};
Finally, the IPersistStreamInitImpl
class provides the following implementation of
GetSizeMax:
STDMETHOD(GetSizeMax)(ULARGE_INTEGER* pcbSize) {
HRESULT hr = S_OK;
T* pT = static_cast<T*>(this);
if (pcbSize == NULL)
return E_POINTER;
ATL_PROPMAP_ENTRY* pMap = T::GetPropertyMap();
ATLENSURE(pMap != NULL);
// Start the size with the size of the ATL version
// we write out.
ULARGE_INTEGER nSize;
nSize.HighPart = 0;
nSize.LowPart = sizeof(DWORD);
CComPtr<IDispatch> pDispatch;
const IID* piidOld = NULL;
for (int i = 0; pMap[i].pclsidPropPage != NULL; i++) {
if (pMap[i].szDesc == NULL)
continue;
// check if raw data entry
if (pMap[i].dwSizeData != 0) {
ULONG ulSize=0;
//Calculate stream size for BSTRs special case
if (pMap[i].vt == VT_BSTR) {
void* pData = (void*)(pMap[i].dwOffsetData +
(DWORD_PTR)pT);
ATLENSURE(
pData >= (void*)(DWORD_PTR)pMap[i].dwOffsetData
&& pData >= (void*)(DWORD_PTR)pT );
BSTR bstr=*reinterpret_cast<BSTR*>(pData);
ulSize=CComBSTR::GetStreamSize(bstr);
} else {
ulSize = pMap[i].dwSizeData;
}
nSize.QuadPart += ulSize;
continue;
}
CComVariant var;
if (pMap[i].piidDispatch != piidOld) {
pDispatch.Release();
if (FAILED(pT->GetUnknown()->
QueryInterface(*pMap[i].piidDispatch,
(void**)&pDispatch))) {
ATLTRACE(atlTraceCOM, 0,
_T("Failed to get a dispatch pointer for "
"property #%i\n"), i);
hr = E_FAIL;
break;
}
piidOld = pMap[i].piidDispatch;
}
if (FAILED(pDispatch.GetProperty(pMap[i].dispid, &var))) {
ATLTRACE(atlTraceCOM, 0,
_T("Invoked failed on DISPID %x\n"),
pMap[i].dispid);
hr = E_FAIL;
break;
}
nSize.QuadPart += var.GetSize();
}
*pcbSize = nSize;
return hr;
}
Previous versions of ATL simply returned
E_NOTIMPL from the GetSizeMax function. As of
this writing, the MSDN documentation claims that ATL still does not
implement this function. In any event, the implementation that ATL
8 actually provides is fairly straightforward. GetSizeMax
loops through all the entries in the property map and accumulates
the total size each entry requires in the nSize variable.
If it finds a "raw" PROP_DATA_ENTRY, it simply increments
the total nSize by the size of the
member specified in the PROP_DATA_ENTRY macro.
Alternatively, if it finds a PROP_ENTRY or
PROP_ENTRY_EX in the map, it queries the object for the
specified IDispatch interface and wraps the resulting
interface pointer in a CComPtr. Recall from Chapter 3, "ATL Smart Types,"
that the CComPtr smart pointer template class provides a
convenient specialization for IDispatch that exposes
property "getters" and "setters." GetSizeMax uses
CComPtr<IDispatch>::GetProperty to retrieve a
VARIANT value for the property specified in the property
map entry. The function wraps the returned VARIANT in a
CComVariant and uses that class's GetSize
function to increment the nSize total for the object.
IPersistStorageImpl
The ATL implementation of
IPersistStorage is very simplistic. The Save
method creates a stream called "Contents" within the
provided storage and depends on an IPersistStreamInit
implementation to write the contents of the stream.
template <class T>
class ATL_NO_VTABLE IPersistStorageImpl
: public IPersistStorage {
public:
STDMETHOD(Save)(IStorage* pStorage, BOOL fSameAsLoad) {
ATLTRACE(atlTraceCOM, 2,
_T("IPersistStorageImpl::Save\n"));
CComPtr<IPersistStreamInit> p;
p.p = IPSI_GetIPersistStreamInit();
HRESULT hr = E_FAIL;
if (p != NULL) {
CComPtr<IStream> spStream;
static LPCOLESTR vszContents = OLESTR("Contents");
hr = pStorage->CreateStream(vszContents,
STGM_READWRITE | STGM_SHARE_EXCLUSIVE | STGM_CREATE,
0, 0, &spStream);
if (SUCCEEDED(hr)) hr = p->Save(spStream, fSameAsLoad);
}
return hr;
}
...
};
Similarly, the Load method opens the
"Contents" stream and uses the IPersistStreamInit
implementation to read the contents of the stream.
STDMETHOD(Load)(IStorage* pStorage) {
ATLTRACE(atlTraceCOM, 2, _T("IPersistStorageImpl::Load\n"));
CComPtr<IPersistStreamInit> p;
p.p = IPSI_GetIPersistStreamInit();
HRESULT hr = E_FAIL;
if (p != NULL) {
CComPtr<IStream> spStream;
hr = pStorage->OpenStream(OLESTR("Contents"), NULL,
STGM_DIRECT | STGM_SHARE_EXCLUSIVE, 0, &spStream);
if (SUCCEEDED(hr)) hr = p->Load(spStream);
}
return hr;
}
The InitNew and IsDirty
implementations retrieve the object's IPersistStreamInit
interface pointer (using a helper function to get the interface)
and delegate to the same named method in that interface:
STDMETHOD(IsDirty)(void) {
ATLTRACE(atlTraceCOM, 2,
_T("IPersistStorageImpl::IsDirty\n"));
CComPtr<IPersistStreamInit> p;
p.p = IPSI_GetIPersistStreamInit();
return (p != NULL) ? p->IsDirty() : E_FAIL;
}
STDMETHOD(InitNew)(IStorage*) {
ATLTRACE(atlTraceCOM, 2,
_T("IPersistStorageImpl::InitNew\n"));
CComPtr<IPersistStreamInit> p;
p.p = IPSI_GetIPersistStreamInit();
return (p != NULL) ? p->InitNew() : E_FAIL;
}
One of the main reasons an object supports
IPersistStorage is so the object can incrementally read
and write its state. Unfortunately, the ATL implementation doesn't
support this. The implementation does not cache the provided
IStorage interface provided during the Load and
Save calls, so it's not available for later incremental
reads and writes. Not caching the IStorage interface makes
implementing the last two methods trivial, however:
STDMETHOD(SaveCompleted)(IStorage* /* pStorage */) {
ATLTRACE(atlTraceCOM, 2,
_T("IPersistStorageImpl::SaveCompleted\n"));
return S_OK;
}
STDMETHOD(HandsOffStorage)(void) {
ATLTRACE(atlTraceCOM, 2, _T("IPersistStorageImpl::HandsOffStorage\n"));
return S_OK;
}
Generally, most objects
that need the functionality IPersistStorage provides can't
use the implementation ATL provides. They must derive directly from
IPersistStorage and implement all the methods
explicitly.
|