ATL Persistence Implementations

[Previous] [Next]

ATL provides the IPersistStreamInitImpl, IPersistStorageImpl, and IPersistPropertyBagImpl template classes to enable your object to load and save its state through any client that is capable of providing a stream, a storage, or a property bag. The ATL Object Wizard automatically includes the first two as base classes when inserting a full control. No support is added when inserting a simple object using the Object Wizard; all three of these classes rely heavily on the property map in your object, and the Object Wizard doesn't create property maps for simple objects. You can still use the persistence classes in simple objects—you just have to add the property map manually and do a little extra work by hand. We mentioned the property map in previous chapters, but now we'll add the details necessary for understanding how it fits into the persistence implementation.

Property Maps Revisited

Property maps are easy to identify in your object header file: just look for the BEGIN_PROP_MAP and END_PROP_MAP macros. These macros declare a static GetPropMap function for the class and a static array of ATL _PROPMAP_ENTRY structures within the class, as shown here:

 #define BEGIN_PROP_MAP(theClass) \     typedef _ATL_PROP_NOTIFY_EVENT_CLASS \         _ _ATL_PROP_NOTIFY_EVENT_CLASS; \     typedef theClass _PropMapClass; \     static ATL_PROPMAP_ENTRY* GetPropertyMap()\     {\         static ATL_PROPMAP_ENTRY pPropMap[] = \         { #define END_PROP_MAP() \             {NULL, 0, NULL, &IID_NULL, 0, 0, 0} \         }; \         return pPropMap; \     } 

Between the BEGIN and END macros, several macros insert ATL_PROPMAP_ENTRY data. The data structure has the following members:

 struct ATL_PROPMAP_ENTRY {     LPCOLESTR szDesc;     DISPID dispid;     const CLSID* pclsidPropPage;     const IID* piidDispatch;     DWORD dwOffsetData;     DWORD dwSizeData;     VARTYPE vt; }; 

The macros used to populate this structure vary depending on whether the property is part of your exposed dispatch interface or is class member data that you want to save as an internal state. The macros related to persistence and the reasons for using them are listed in Table 11-1.

Table 11-1. Persistence Macros.

Macro Description
PROP_ENTRY(szDesc, dispid, clsid) Inserts a property into the map, assuming a single IDispatch interface
PROP_ENTRY_EX(szDesc, dispid, clsid, iidDispatch) Used for properties in objects with multiple dual interfaces
PROP_DATA_ENTRY(szDesc, member, vt) For internal state not exposed through a dispatch interface; vt indicates the VARTYPE of the data

The PROP_ENTRY macro populates the first three members of the ATL_PROPMAP_ENTRY structure with the parameters passed in and fixes the piidDispatch entry at &IID_IDispatch. PROP_ENTRY_EX allows you to specify piidDispatch. Both macros leave the dwOffsetData, dwSizeData, and vt members unused (0). Also, the clsid parameter can be used to specify the property page associated with the property or &CLSID_NULL if not applicable. This information isn't required for persistence but provides a convenient place to get the property page IDs for a container when it requests them. The PROP_DATA_ENTRY macro initializes only the description, offset, and size of the data member being persisted.

ATL persistence classes use the GetPropertyMap static function defined by the BEGIN_PROPERTY_MAP macro to get the first entry in the map and enumerate through the ATL_PROPMAP_ENTRY structures.

IPersistStreamInitImpl

IPersistStreamInitImpl implements and derives directly from IPersistStreamInit. It is templatized only on your object C++ class name, which allows ATL access to your overrides and any other inherited members through the static_cast<T*>(this) cast operation. This interesting technique is used throughout ATL, and the persistence classes are no exception.

Saving an Object

To successfully persist your object, the container will first create a stream object—that is, some implementation of IStream. Your object doesn't know or care what medium lies beneath the interface, which is one of the reasons we use interface-based programming in the first place. Once the stream is created, the first bit of information your object is asked for is its CLSID. After all, persisted properties must belong to a specific type of object, so the type information must be saved along with the object state. To get the CLSID, containers holding an interface pointer to the object can call QueryInterface for IPersist and call GetClassID. Because IPersistStreamInit derives from IPersist, IPersistStreamInitImpl must also implement the one and only IPersist method: GetClassID. The implementation of GetClassID looks like this:

 STDMETHOD(GetClassID)(CLSID *pClassID) {     *pClassID = T::GetObjectCLSID();     return S_OK; } 

The template parameter T is used to access the static GetObjectCLSID method in your object. GetObjectCLSID is inherited from CComCoClass, which you'll see in your object inheritance like this:

 public CComCoClass<CAtlFullControl, &CLSID_AtlFullControl> 

Because CComCoClass takes the CLSID as a template parameter, it is qualified to supply it to the ATL persistence implementation. All of the ATL persistence classes rely on this method to obtain the CLSID for the persisted object while saving. Once retrieved, the container can utilize the COM API WriteClassStm(pStream, clsid) to write the CLSID as the first piece of persisted data describing the object.

So far we haven't used the property map as promised, but we're about to. The container now has the choice of which persistence interface it prefers to use. This is a simple matter of using QueryInterface for the IPersistxxx interfaces in the order of preference. Assuming IPersistStreamInit is the first choice found that the object supports, the Save method is called with the IStream pointer as a parameter. IPersistStreamInitImpl supplies a default implementation as follows:

 STDMETHOD(Save)(LPSTREAM pStm, BOOL fClearDirty) {     T* pT = static_cast<T*>(this);     return pT->IPersistStreamInit_Save(pStm, fClearDirty,         T::GetPropertyMap()); } 

The fClearDirty flag is used to set the state of the "dirty" flag in the object after saving but is ignored in the Save implementation. ATL keeps tabs on the dirty state in the m_bRequiresSave member of CComControlBase, which the control indirectly inherits from CComControl. If your object doesn't inherit from CComControl (for example, it's a simple object), you'll need to manually add this data member in your object header. The dirty state can be checked with the IPersistStreamInitImpl::IsDirty method. Moving on with the Save operation, IPersistStreamInit_Save is called with the Save parameters plus the property map pointer. This helper function delegates yet another time to the AtlIPersistStreamInit_Save global function, adding a pointer to the object and its IUnknown pointer.

 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()); } 

At this point, you might be wondering about all of this indirection. Consider that you could create your own version of the Save code by implementing IPersistStreamInit_Save yourself. Because this method is called from IPersistStreamInit::Save using the cast object pointer, your derived method would be called instead of the default. It's another point of extensibility. Also, delegating the real work to AtlIPersistStreamInit_Save can make smaller code when using ATL as a DLL, because the implementation for this global function is found in ATL.DLL for ReleaseMinSize builds.

AtlIPersistStreamInit_Save writes out the ATL version number and then uses the property map to save each property to the container-supplied stream in turn. The version number is used when the object is loaded from a stream to ensure compatibility between Load and Save implementations. To get the property value, AtlIPersistStreamInit_Save calls QueryInterface through the IUnknown pointer for the dispatch interface specified in the property map entry, shown here:

 if(pMap[i].piidDispatch != piidOld) {     pDispatch.Release();     if(FAILED(pUnk->QueryInterface(*pMap[i].piidDispatch,         (void**)&pDispatch)))     {         hr = E_FAIL;         break;     }     piidOld = pMap[i].piidDispatch; } 

The pDispatch pointer is cached until a different dispatch ID is found in the map. AtlIPersistStreamInit_Save checks the map for every entry, because PROP_ENTRY_EX can be used to specify the dispatch interface each property is exposed through. Once a dispatch pointer is obtained, AtlIPersistStreamInit_Save calls into the object to get the value using CComDispatchDriver. Remember that the dispatch ID for the property was passed in as an argument to the PROP_ENTRY macro. ATL then writes the returned property value to the stream. CComVariant is handy for the last step because it has stream read/write capability, as you can see here:

 if(FAILED(CComDispatchDriver::GetProperty(pDispatch,     pMap[i].dispid, &var))) {     hr = E_FAIL;     break; } hr = var.WriteToStream(pStm); 

For persisted values placed in the map using PROP_DATA_ENTRY, no dispatch interface is involved. A pointer to the data member is calculated based on the dwOffsetData member of ATL_PROPMAP_ENTRY, and the dwSizeData member tells ATL how many bytes to write out. The dwSizeData member will be 0 for map entries entered using PROP_ENTRY or PROP_ENTRY_EX and nonzero for PROP_DATA_ENTRY entries. The relevant code is shown here:

 if(pMap[i].dwSizeData != 0) {     void* pData = (void*) (pMap[i].dwOffsetData + (DWORD)pThis);     hr = pStm->Write(pData, pMap[i].dwSizeData, NULL);     if(FAILED(hr))         return hr;     continue; } 

After looping through the entire property map, AtlIPersistStreamInit_Save exits and the save operation is complete.

Creating and Loading an Object

With the saved object safely tucked away in persistent storage, the object itself can be released and later re-created. To accomplish re-creation, the container can perform these steps:

  1. Create an IStream on the storage medium.
  2. Read the CLSID of the object from the stream using ReadClassStm.
  3. Call either CoCreateInstance or CoCreateInstanceEx to create the object.
  4. Call QueryInterface through IPersistStreamInit for the object.
  5. Call IPersistStreamInit::Load and pass in the IStream pointer.

The ATL implementation of the Load function reverses what Save did. It loads the ATL version number and then loops through the property map extracting the variant values from the stream using CComVariant::ReadFromStream. The values are then used to call into the appropriate dispatch interface on the object and set the property. For raw data values, the data is read directly from the stream into the data member using dwOffsetData in the map entry. After loading, the dirty bit is set to FALSE, because the object has a known state.

If an object is being newly created, as when a button is dropped onto a Visual Basic form, IPersistStreamInit::InitNew should be called by the container instead of Load. InitNew does nothing in IPersistStreamInitImpl and should be overridden in the object class to put properties into their initial state.

IPersistStorageImpl

A container might prefer to keep its persisted state in a compound document, or structured storage. Each object is given its own storage to manage as needed to get the persistence job done. To see the results of this, let's create a structured storage using the ActiveX control test container.

  1. Launch the test container from the Tools menu in Microsoft Visual Studio.
  2. From the Edit menu, choose Insert New Control.
  3. Pick the control of your choice from the dialog box and click OK. The control should now be visible.
  4. Now click on the Control menu and choose Save To Storage.

NOTE
There are also menu selections for saving to a stream, known as property bags, which we talked about earlier. We'll look at property bags in more detail right after storages.

  1. Enter a file name for the structured storage and click the Save button. The DocFile Viewer applet will open. Folder icons indicate storages, and page icons represent streams.
  2. From the Tree menu, choose Expand All. Your file should have one storage and one or more streams beneath it. If you want to look at the actual bytes stored, double-click on a stream. The byte format is, of course, under the control of the object being persisted. To the container, it's just a stream of bytes.

To support this kind of persistence, an object must implement IPersistStorage. ATL provides IPersistStorageImpl, which is suitable in most cases. If you need to create more than one stream in the provided storage, you'll need to override the default behavior.

Saving to a Storage

The container kicks off the save process by creating an IStorage with one of the structured storage APIs (such as StgCreateDocfile) and then calls QueryInterface through IPersistStorage for the persisted object. As with IPersistStream, the CLSID of the object is obtained using IPersistStorage::GetClassID. The CLSID can be written directly to the storage using WriteClassStg. The container then calls IPersistStorage::Save, passing the IStorage interface as a parameter. On the object side, IPersistStorageImpl::Save then creates a stream named "Contents." This name is hard-coded in IPersistStorageImpl; if you want to use a different name, you'll have to override the Save method. The resulting IStream pointer is then passed to the IPersistStreamImpl::Save method for standard stream-based persistence, which we covered in the previous section. When the save operation is complete and the IPersistStorageImpl::Save method returns, the container calls IPersistStorage::SaveCompleted to indicate the process is complete. IPersistStorageImpl reuses the IPersistStreamInitImpl code for its read/write functionality.

Loading from a Storage

To load a persisted object from a storage, the container opens the storage and obtains the IStorage interface. It then uses ReadClassStg to read the CLSID of the object that the storage belongs to. With the CLSID in hand, the object can be created using either CoCreateInstance or CoCreateInstanceEx and queried for IPersistStorage. IPersistStorage::Load is then called, which ATL implements to open the "Contents" stream and hand it off to IPersistStreamInitImpl::Load.

If you want to step through the saving and loading action, you'll find once again that the test container is a handy tool.

  1. Create an ATL DLL project that contains a full control.
  2. Modify the project settings for the debug build (Project_Settings menu, Debug tab) so that the debug executable is the test container. This is a predefined choice—just click the browse button on the edit field and choose ActiveX Control Test Container from the pop-up menu.
  3. Build the project in debug mode.
  4. Open ATLCOM.H and put breakpoints in the IPersistStorageImpl methods.
  5. Run the project. The test container application will start.
  6. In the test container, insert the control from the Edit-Insert New Control menu.
  7. Save the control to storage from the Control menu. You should hit breakpoints IPersistStorageImpl::GetClassID, Save, and SaveCompleted.
  8. Delete the control from the test container (from the Edit menu).
  9. Insert the control from storage using the Edit-Insert Control from the Storage menu. You should hit the IPersistStorageImpl::Load breakpoint.

IPersistPropertyBagImpl

Property bags enable the container to choose the persistence format. Unlike IStream and IStorage, IPropertyBag has no predefined implementation. The container can choose any format that it can parse to retrieve the CLSID of the object and its name/value pairs representing the properties. Visual Basic form files (.frm) and controls hosted in Web pages are two examples of the use of property bag persistence to save properties as text. A Visual Basic form with a single button on it has this format when persisted:

 Begin VB.Form Form1      Caption         =   "Form1"     ClientHeight    =   3195     ClientLeft      =   60     ClientTop       =   345     ClientWidth     =   4680     LinkTopic       =   "Form1"     ScaleHeight     =   3195     ScaleWidth      =   4680     StartUpPosition =   3  'Windows Default     Begin VB.CommandButton Command1          Caption         =   "Command1"         Height          =   615         Left            =   360         TabIndex        =   0         Top             =   600         Width           =   2055     End End 

Each object is enclosed in a Begin/End block with the named values inside. Each named value in the file shown is the result of an IPropertyBag::Write call. Write takes the name of the property and a variant value as arguments from the object being saved. Nested objects, such as the button in the file above, are passed to the container as the VT_UNKNOWN type.

Property bag persistence for ATL objects is supplied by the IPersistPropertyBagImpl class, from which your object derives. Controls and objects created with the ATL Object Wizard don't derive from this class by default, so you'll have to add the inheritance by hand.

Adding Property Bag Support

Like the other persistence implementations, IPersistPropertyBagImpl relies on the property map. If you're starting with a control generated by the Object Wizard, the map will already be there. Simple objects don't have property maps by default, so you'll need to create the map before we continue. Use the BEGIN_PROP_MAP and END_PROP_MAP macros to declare the map, and add a BOOL member variable m_bRequiresSave. Controls automatically inherit this member from CComControlBase.

With the property map in place, you can add property bag support by inheriting from IPersistPropertyBagImpl, as shown here:

 public IPersistPropertyBagImpl<theClass>, 

The template parameter theClass should be replaced with your object C++ class name. Because containers expect QueryInterface to successfully return an IPersistPropertyBag pointer, you need this entry in the interface map:

 COM_INTERFACE_ENTRY(IPersistPropertyBag) 

Your object is now ready for property bag persistence! Let's look at how IPersistPropertyBagImpl works.

Saving to a Property Bag

As with the other persistence interfaces, IPersistPropertyBagImpl implements the IPersist method GetClassID. After retrieving the type information, the container calls the Save method, passing in an IPropertyBag pointer. Save takes an additional BOOL parameter, fSaveAllProperties. If FALSE, only those properties that have changed since the last save are persisted. ATL ignores this flag, and all properties are persisted with each call to Save. Save eventually delegates to AtlIPersistPropertyBag_Save, which enumerates the property map to get each property value as a variant. In addition to a value, a text description is needed before you can call IPropertyBag::Write. The map stores this value, which you passed in as the first parameter to the PROP_ENTRY macro. Retrieving it is a simple matter of accessing the szDesc member of the ATL_PROPMAP_ENTRY structure:

 HRESULT hr = pPropBag->Write(pMap[i].szDesc, &var); 

For raw data entries (PROP_DATA_ENTRY), IPersistPropertyBagImpl supports a subset of the possible variant types. These include VT_UI1, VT_I1, VT_BOOL, VT_UI2, VT_UI4, VT_INT, and VT_UINT.

We looked at one example of a property bag: the Visual Basic form file. You can also use the test container to save objects to a property bag that is displayed in a list view control. This option is available with the other Save selections under the Control menu. Figure 11-2 shows a Microsoft Calendar Control persisted to a property bag in the test container.

Figure 11-2. Microsoft Calendar Control persisted to a property bag.

Nonvolatile property bag persistence isn't available in the test container as of this writing, and it doesn't support nested objects. Even so, it's a quick and dirty debugging tool to tuck away in your arsenal.

Loading from a Property Bag

To restore an object from a property bag, the container can open the file or other property bag storage medium and retrieve the CLSID for the object. This is a container-specific operation that varies between applications. Once the CLSID has been retrieved and the object is created using either CoCreateInstance or CoCreateInstanceEx, IPersistPropertyBag::Load is called to set the properties. As with the other persistence load and save implementations, Load delegates to AtlIPersistPropertyBag_Load. This global function walks the property map to retrieve each value in turn with the container-provided IPropertyBag pointer. IPropertyBag::Read takes the property description as an [in] parameter and a variant as an [out] parameter to receive the value. Before calling Read, AtlIPersistPropertyBag_Load creates an empty variant and invokes the get method on the property. The returned value is unimportant, but the type, shown here, is important:

 CComVariant var; if(FAILED(CComDispatchDriver::GetProperty(pDispatch,     pMap[i].dispid, &var))) {     return E_FAIL; } 

With the variant correctly typed, the value is read from the container's property bag:

 HRESULT hr = pPropBag->Read(pMap[i].szDesc, &var, pErrorLog); 

This process continues for all properties exposed through a dispatch interface in the property map. For raw data values exposed through PROP_DATA_ENTRY, the process is somewhat different. The following code shows how ATL reads a raw data value from the container:

 CComVariant var; if(pMap[i].dwSizeData != 0) {     void* pData = (void*) (pMap[i].dwOffsetData + (DWORD)pThis);     HRESULT hr = pPropBag->Read(pMap[i].szDesc, &var, pErrorLog); } 

Notice how the empty CComVariant var is used to retrieve the value in pPropBag->Read. Because the type of the variant hasn't been specified (VT_EMPTY by default), the container must decide what type the data is. This can result in errors. For example, the extents of an ATL control are declared in the PROP_DATA_ENTRY macro as type VT_UI4, but they have values small enough to fit in a VT_I2. If the container uses the most efficient type to return the value, the VT_I2 will be copied to the property map declared type VT_UI4, leaving garbage in the upper half of the property value. If the variant var is initialized to the type stored in the map, the container can return the correct size value. The corrected code is shown here:

 CComVariant var; var.vt = pmap[i].vt; if(pMap[i].dwSizeData != 0) {     void* pData = (void*) (pMap[i].dwOffsetData + (DWORD)pThis);     HRESULT hr = pPropBag->Read(pMap[i].szDesc, &var, pErrorLog); } 

This discrepancy should be fixed in the next version of ATL. As we mentioned in the previous section, only a subset of the variant types is supported for raw data entries in IPersistPropertyBagImpl.



Inside Atl
Inside ATL (Programming Languages/C)
ISBN: 1572318589
EAN: 2147483647
Year: 1998
Pages: 127

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