PRB: ATL Control Properties Do Not Persist when Embedded in Word

ID: Q241936


The information in this article applies to:
  • Microsoft Word 2000
  • Microsoft Word 97 for Windows
  • The Microsoft Active Template Library (ATL) 3.0


SYMPTOMS

Microsoft Word 97 and Word 2000 do not appear to save the properties of an Active Template Library (ATL) ActiveX control when the document is closed and then re-opened. Trace statements in ATL show that IPersistStorage and IPersistStreamInit are being called to both save and load the control's settings, but the control appears to be initialized to a new state every time the document is opened.


CAUSE

The problem occurs because ATL's implementation of IPersistStreamInit::Save and IPersistStreamInit::Load performs a QueryInterface call to get the IDispatch of the root control from which it will persist stock properties. Because Word wraps all embedded controls in an aggregated extender object, the QueryInterface call is returning the IDispatch of the Word extender and not the internal control. This causes the persist methods to fail when saving and loading the control in certain circumstances.


RESOLUTION

To resolve the problem, you need to overwrite the IPersistStreamInit_Load and IPersistStreamInit_Save functions for your control, and create modified versions of the global functions AtlIPersistStreamInit_Load and AtlIPersistStreamInit_Save. See the sample below for the steps to needed to resolve the issue.


MORE INFORMATION

Steps to Reproduce the Behavior

  1. Start Visual Studio 6.0 and create a new ATL Wizard project named ATLPersist. Accept the defaults for the wizard and click Finish.


  2. Insert a new ATL Object (Full Control) and name the control AtlPersistSample. On the Miscellaneous tab, check the Windowed Only box, and on the Stock Properties page, add Background Color, Caption, and Enabled. Click OK to create the control.


  3. In AtlPeristSample.h, replace the OnDraw code with the following:


  4. 
    HRESULT OnDraw(ATL_DRAWINFO& di)
    {
       USES_CONVERSION;
    
       RECT& rc = *(RECT*)di.prcBounds;
    
       COLORREF clrFore, clrBack;
       HBRUSH hOldBrush;
       HPEN hOldPen;
    
       OleTranslateColor(m_clrForeColor, NULL, &clrFore);
       OleTranslateColor(m_clrBackColor, NULL, &clrBack);
    
       hOldBrush = (HBRUSH)SelectObject(di.hdcDraw, CreateSolidBrush(clrBack));
       hOldPen = (HPEN)SelectObject(di.hdcDraw,
                          CreatePen(PS_SOLID, 1, clrFore));
    
       Rectangle(di.hdcDraw, rc.left, rc.top, rc.right, rc.bottom);
    
       SetTextColor(di.hdcDraw, clrFore);
       SetBkColor(di.hdcDraw, clrBack);
    
       SetTextAlign(di.hdcDraw, TA_CENTER|TA_BASELINE);
    
       TextOut(di.hdcDraw, (rc.left + rc.right) / 2, (rc.top + rc.bottom) / 2, 
    	  OLE2T(m_bstrCaption), m_bstrCaption.Length()); 
    
        DeleteObject(SelectObject(di.hdcDraw, hOldBrush));
        DeleteObject(SelectObject(di.hdcDraw, hOldPen));
    
        return S_OK;
    } 
  5. In the class constuctor, add the following:


  6. 
    CAtlPersistSample()
    {
       m_bWindowOnly = TRUE;
       m_clrBackColor = 0x00C0C0C0;
    } 
  7. Press the F7 key to build and register the control.


  8. Open Microsoft Word 97 or Word 2000, and, from the View|Toolbars menu, choose Control Toolbox.


  9. Using the More Controls button (at the bottom on the toolbar), find and select the ATLPersistSample class. This should add a new instance of the control to the document. Right-click on the control and choose Properties to bring up the Properties window. Change the BackColor to some color and change the Caption property to "My Caption."


  10. Save the Word document to a location on disk, and close the document. When you re-open the document, the control appears but the Caption and BackColor you selected are no longer correct.


Steps to Resolve the Issue

  1. Go back to your ATL project and add the following overrides to the AtlPersistSample class (AtlPersistSample.h):


  2. 
    HRESULT IPersistStreamInit_Load(LPSTREAM pStm, ATL_PROPMAP_ENTRY* pMap)
    {
       HRESULT hr = MyAtlIPersistStreamInit_Load(pStm, pMap, this,
                       (IUnknown*)(IDispatch*)this);
       if (SUCCEEDED(hr))
          m_bRequiresSave = FALSE;
       return hr;
    }
    
    HRESULT IPersistStreamInit_Save(LPSTREAM pStm, BOOL fClearDirty,
            ATL_PROPMAP_ENTRY* pMap)
    {
       return MyAtlIPersistStreamInit_Save(pStm, fClearDirty, pMap, this,
                       (IUnknown*)(IDispatch*)this);
    } 
  3. At the top of the header file, add the following forward declarations (just below the #include <atlctl.h>):


  4. 
    ATLAPI MyAtlIPersistStreamInit_Load(LPSTREAM pStm, 
           ATL_PROPMAP_ENTRY* pMap, void* pThis, IUnknown* pUnk);
    ATLAPI MyAtlIPersistStreamInit_Save(LPSTREAM pStm, BOOL /* fClearDirty */,
           ATL_PROPMAP_ENTRY* pMap, void* pThis, IUnknown* pUnk); 
  5. Add the following global functions to the AtlPersistSample.cpp file. These are exact copies of the ATL functions, except that you need to change the code to avoid the QueryInterface call that improperly returns the control extender instead on the base control:


  6. 
    ATLAPI MyAtlIPersistStreamInit_Load(LPSTREAM pStm, ATL_PROPMAP_ENTRY* pMap,
            void* pThis, IUnknown* pUnk)
    {
       ATLASSERT(pMap != NULL);
       HRESULT hr = S_OK;
       DWORD dwVer;
       hr = pStm->Read(&dwVer, sizeof(DWORD), NULL);
       if (FAILED(hr))
         return hr;
       if (dwVer > _ATL_VER)
         return E_FAIL;
    
       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)
          {
             void* pData = (void*) (pMap[i].dwOffsetData + (DWORD)pThis);
             hr = pStm->Read(pData, pMap[i].dwSizeData, NULL);
             if (FAILED(hr))
                return hr;
             continue;
          }
    
          CComVariant var;
    
          hr = var.ReadFromStream(pStm);
          if (FAILED(hr))
            break;
    
          if (pMap[i].piidDispatch != piidOld)
          {
             pDispatch.Release();
    
          /****
          // This is the code you replace....
             if FAILED(pUnk->QueryInterface(*pMap[i].piidDispatch,
               (void**)&pDispatch)))
             {
    	    hr = E_FAIL;
    	    break;
             }
          ***/ 
          // Instead, you just cast the passed in pUnk...
             pDispatch = (IDispatch*)pUnk;
             piidOld = pMap[i].piidDispatch;
         }
    
          if (FAILED(CComDispatchDriver::PutProperty(pDispatch,
             pMap[i].dispid, &var)))
          {
             hr = E_FAIL;
             break;
          }
       }
       return hr;
    }
    
    ATLAPI MyAtlIPersistStreamInit_Save(LPSTREAM pStm, BOOL /* fClearDirty */, 
        ATL_PROPMAP_ENTRY* pMap, void* pThis, IUnknown* pUnk)
    {
       ATLASSERT(pMap != NULL);
       DWORD dw = _ATL_VER;
       HRESULT hr = pStm->Write(&dw, sizeof(DWORD), NULL);
       if (FAILED(hr))
          return hr;
    
       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)
          {
             void* pData = (void*) (pMap[i].dwOffsetData + (DWORD)pThis);
             hr = pStm->Write(pData, pMap[i].dwSizeData, NULL);
             if (FAILED(hr))
                return hr;
             continue;
          }
    
          CComVariant var;
          if (pMap[i].piidDispatch != piidOld)
          {
             pDispatch.Release();
          /****
          // This is the code you replace....
             if (FAILED(pUnk->QueryInterface(*pMap[i].piidDispatch,
                (void**)&pDispatch)))
             {
                hr = E_FAIL;
                break;
             }
           ***/ 
          // Instead, you just cast the passed in pUnk...
             pDispatch = (IDispatch*)pUnk;
             piidOld = pMap[i].piidDispatch;
          }
    
          if (FAILED(CComDispatchDriver::GetProperty(pDispatch,
             pMap[i].dispid, &var)))
          {
             hr = E_FAIL;
             break;
          }
    
          hr = var.WriteToStream(pStm);
          if (FAILED(hr))
             break;
       }
       return hr;
    } 

© Microsoft Corporation 1999, All Rights Reserved.
Contributions by Richard R. Taylor, Microsoft Corporation


REFERENCES

For additional information on ATL controls in Office, please click the article numbers below to view the articles in the Microsoft Knowledge Base:

Q214462 PRB: Error "Bound to unknown type" Inserting ATL Control in VBA
Q197490 PRB: ATL Full Control Needs Enabled Stock Property for Access 97

Additional query words:

Keywords : kbActiveX kbATL kbCtrlCreate kbVC600 kbATL300 kbWord kbGrpDSO kbword2000
Version : WINDOWS:2000,3.0,97
Platform : WINDOWS
Issue type : kbprb


Last Reviewed: October 1, 1999
© 2000 Microsoft Corporation. All rights reserved. Terms of Use.