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



The information in this article applies to:

  • Microsoft Word 2002
  • Microsoft Word 2000
  • Microsoft Word 97 for Windows
  • The Microsoft Active Template Library (ATL) 3.0

This article was previously published under Q241936

SYMPTOMS

Microsoft Word does not appear to save the properties of an Active Template Library (ATL) ActiveX control when the document is saved 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:
    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;
    }
    					
  4. In the class constuctor, add the following:
    CAtlPersistSample()
    {
       m_bWindowOnly = TRUE;
       m_clrBackColor = 0x00C0C0C0;
    }
    					
  5. Press the F7 key to build and register the control.
  6. Start Microsoft Word, point to Toolbars on the View menu, and then click Control Toolbox to display Control Toolbox toolbar.
  7. 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."
  8. 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. Sometimes Word may start in design mode (for example if a user clicks No to enabling macros) and in this case, the control may appear to be correct, but in fact is not loaded. What you are seeing is the metafile. When the document is placed in run mode again, it will appear uninitialized.

Steps to Resolve the Issue

  1. Go back to your ATL project and add the following overrides to the AtlPersistSample class (AtlPersistSample.h):
    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);
    }
    					
  2. At the top of the header file, add the following forward declarations (just below the #include <atlctl.h>):
    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);
    					
  3. 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:
    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;
    }
    					

REFERENCES

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

214462 PRB: Error "Bound to unknown type" Inserting ATL Control in VBA

197490 PRB: ATL Full Control Needs Enabled Stock Property for Access 97


Modification Type:MajorLast Reviewed:12/15/2003
Keywords:kbCtrlCreate kbprb KB241936