Example: Implementing a Property Page

The ATL Property Page wizard is not available in Visual Studio 2019 and later.

This example shows how to build a property page that displays (and allows you to change) properties of the Document Classes interface.

The example is based on the ATLPages sample.

To complete this example, you will:

Adding the ATL Property Page Class

First, create a new ATL project for a DLL server called ATLPages7. Now use the ATL Property Page Wizard to generate a property page. Give the property page a Short Name of DocProperties then switch to the Strings page to set property-page-specific items as shown in the table below.

Item Value
Title TextDocument
Doc String VCUE TextDocument Properties
Helpfile <blank>

The values that you set on this page of the wizard will be returned to the property page container when it calls IPropertyPage::GetPageInfo. What happens to the strings after that is dependent on the container, but typically they will be used to identify your page to the user. The Title will usually appear in a tab above your page and the Doc String may be displayed in a status bar or ToolTip (although the standard property frame doesn't use this string at all).

Note

The strings that you set here are stored as string resources in your project by the wizard. You can easily edit these strings using the resource editor if you need to change this information after the code for your page has been generated.

Click OK to have the wizard generate your property page.

Editing the Dialog Resource

Now that your property page has been generated, you'll need to add a few controls to the dialog resource representing your page. Add an edit box, a static text control, and a check box and set their IDs as shown below:

Screenshot of a dialog resource in the visual editor.

These controls will be used to display the file name of the document and its read-only status.

Note

The dialog resource does not include a frame or command buttons, nor does it have the tabbed look that you might have expected. These features are provided by a property page frame such as the one created by calling OleCreatePropertyFrame.

Adding Message Handlers

With the controls in place, you can add message handlers to update the dirty status of the page when the value of either of the controls changes:

BEGIN_MSG_MAP(CDocProperties)
   COMMAND_HANDLER(IDC_NAME, EN_CHANGE, OnUIChange)
   COMMAND_HANDLER(IDC_READONLY, BN_CLICKED, OnUIChange)
   CHAIN_MSG_MAP(IPropertyPageImpl<CDocProperties>)
END_MSG_MAP()

   // Respond to changes in the UI to update the dirty status of the page
   LRESULT OnUIChange(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
   {
      wNotifyCode; wID; hWndCtl; bHandled;
      SetDirty(true);
      return 0;
   }

This code responds to changes made to the edit control or check box by calling IPropertyPageImpl::SetDirty, which informs the page site that the page has changed. Typically the page site will respond by enabling or disabling an Apply button on the property page frame.

Note

In your own property pages, you might need to keep track of precisely which properties have been altered by the user so that you can avoid updating properties that haven't been changed. This example implements that code by keeping track of the original property values and comparing them with the current values from the UI when it's time to apply the changes.

Housekeeping

Now add a couple of #import statements to DocProperties.h so that the compiler knows about the Document interface:

// MSO.dll
#import <libid:2DF8D04C-5BFA-101B-BDE5-00AA0044DE52> version("2.2") \
   rename("RGB", "Rgb")   \
   rename("DocumentProperties", "documentproperties")   \
   rename("ReplaceText", "replaceText")   \
   rename("FindText", "findText")   \
   rename("GetObject", "getObject")   \
   raw_interfaces_only

// dte.olb
#import <libid:80CC9F66-E7D8-4DDD-85B6-D9E6CD0E93E2> \
   inject_statement("using namespace Office;")   \
   rename("ReplaceText", "replaceText")   \
   rename("FindText", "findText")   \
   rename("GetObject", "getObject")   \
   rename("SearchPath", "searchPath")   \
   raw_interfaces_only

You'll also need to refer to the IPropertyPageImpl base class; add the following typedef to the CDocProperties class:

typedef IPropertyPageImpl<CDocProperties> PPGBaseClass;

Overriding IPropertyPageImpl::SetObjects

The first IPropertyPageImpl method that you need to override is SetObjects. Here you'll add code to check that only a single object has been passed and that it supports the Document interface that you're expecting:

STDMETHOD(SetObjects)(ULONG nObjects, IUnknown** ppUnk)
{
   HRESULT hr = E_INVALIDARG;
   if (nObjects == 1)
   {
      CComQIPtr<EnvDTE::Document> pDoc(ppUnk[0]);
      if (pDoc)
         hr = PPGBaseClass::SetObjects(nObjects, ppUnk);
   }
   return hr;
}

Note

It makes sense to support only a single object for this page because you will allow the user to set the file name of the object — only one file can exist at any one location.

Overriding IPropertyPageImpl::Activate

The next step is to initialize the property page with the property values of the underlying object when the page is first created.

In this case you should add the following members to the class since you'll also use the initial property values for comparison when users of the page apply their changes:

CComBSTR m_bstrFullName;  // The original name
VARIANT_BOOL m_bReadOnly; // The original read-only state

The base class implementation of the Activate method is responsible for creating the dialog box and its controls, so you can override this method and add your own initialization after calling the base class:

STDMETHOD(Activate)(HWND hWndParent, LPCRECT prc, BOOL bModal)
{
   // If we don't have any objects, this method should not be called
   // Note that OleCreatePropertyFrame will call Activate even if
   // a call to SetObjects fails, so this check is required
   if (!m_ppUnk)
      return E_UNEXPECTED;

   // Use Activate to update the property page's UI with information
   // obtained from the objects in the m_ppUnk array

   // We update the page to display the Name and ReadOnly properties
   // of the document

   // Call the base class
   HRESULT hr = PPGBaseClass::Activate(hWndParent, prc, bModal);
   if (FAILED(hr))
      return hr;

   // Get the EnvDTE::Document pointer
   CComQIPtr<EnvDTE::Document> pDoc(m_ppUnk[0]);
   if (!pDoc)
      return E_UNEXPECTED;
   
   // Get the FullName property
   hr = pDoc->get_FullName(&m_bstrFullName);
   if (FAILED(hr))
      return hr;

   // Set the text box so that the user can see the document name
   USES_CONVERSION;
   SetDlgItemText(IDC_NAME, CW2CT(m_bstrFullName));

   // Get the ReadOnly property
   m_bReadOnly = VARIANT_FALSE;
   hr = pDoc->get_ReadOnly(&m_bReadOnly);
   if (FAILED(hr))
      return hr;

   // Set the check box so that the user can see the document's read-only status
   CheckDlgButton(IDC_READONLY, m_bReadOnly ? BST_CHECKED : BST_UNCHECKED);

   return hr;
}

This code uses the COM methods of the Document interface to get the properties that you're interested in. It then uses the Win32 API wrappers provided by CDialogImpl and its base classes to display the property values to the user.

Overriding IPropertyPageImpl::Apply

When users want to apply their changes to the objects, the property page site will call the Apply method. This is the place to do the reverse of the code in Activate — whereas Activate took values from the object and pushed them into the controls on the property page, Apply takes values from the controls on the property page and pushes them into the object.

STDMETHOD(Apply)(void)
{
   // If we don't have any objects, this method should not be called
   if (!m_ppUnk)
      return E_UNEXPECTED;

   // Use Apply to validate the user's settings and update the objects'
   // properties

   // Check whether we need to update the object
   // Quite important since standard property frame calls Apply
   // when it doesn't need to
   if (!m_bDirty)
      return S_OK;
   
   HRESULT hr = E_UNEXPECTED;

   // Get a pointer to the document
   CComQIPtr<EnvDTE::Document> pDoc(m_ppUnk[0]);
   if (!pDoc)
      return hr;
   
   // Get the read-only setting
   VARIANT_BOOL bReadOnly = IsDlgButtonChecked(IDC_READONLY) ? VARIANT_TRUE : VARIANT_FALSE;

   // Get the file name
   CComBSTR bstrName;
   if (!GetDlgItemText(IDC_NAME, bstrName.m_str))
      return E_FAIL;

   // Set the read-only property
   if (bReadOnly != m_bReadOnly)
   {
      hr = pDoc->put_ReadOnly(bReadOnly);
      if (FAILED(hr))
         return hr;
   }

   // Save the document
   if (bstrName != m_bstrFullName)
   {
      EnvDTE::vsSaveStatus status;
      hr = pDoc->Save(bstrName, &status);
      if (FAILED(hr))
         return hr;
   }

   // Clear the dirty status of the property page
   SetDirty(false);

   return S_OK;
}

Note

The check against m_bDirty at the beginning of this implementation is an initial check to avoid unnecessary updates of the objects if Apply is called more than once. There are also checks against each of the property values to ensure that only changes result in a method call to the Document.

Note

Document exposes FullName as a read-only property. To update the file name of the document based on changes made to the property page, you have to use the Save method to save the file with a different name. Thus, the code in a property page doesn't have to limit itself to getting or setting properties.

Displaying the Property Page

To display this page, you need to create a simple helper object. The helper object will provide a method that simplifies the OleCreatePropertyFrame API for displaying a single page connected to a single object. This helper will be designed so that it can be used from Visual Basic.

Use the Add Class dialog box and the ATL Simple Object Wizard to generate a new class and use Helper as its short name. Once created, add a method as shown in the table below.

Item Value
Method Name ShowPage
Parameters [in] BSTR bstrCaption, [in] BSTR bstrID, [in] IUnknown* pUnk

The bstrCaption parameter is the caption to be displayed as the title of the dialog box. The bstrID parameter is a string representing either a CLSID or a ProgID of the property page to display. The pUnk parameter will be the IUnknown pointer of the object whose properties will be configured by the property page.

Implement the method as shown below:

STDMETHODIMP CHelper::ShowPage(BSTR bstrCaption, BSTR bstrID, IUnknown* pUnk)
{
   if (!pUnk)
      return E_INVALIDARG;

   // First, assume bstrID is a string representing the CLSID 
   CLSID theCLSID = {0};
   HRESULT hr = CLSIDFromString(bstrID, &theCLSID);
   if (FAILED(hr))
   {
      // Now assume bstrID is a ProgID
      hr = CLSIDFromProgID(bstrID, &theCLSID);
      if (FAILED(hr))
         return hr;
   }

   // Use the system-supplied property frame
   return OleCreatePropertyFrame(
      GetActiveWindow(),   // Parent window of the property frame
      0,           // Horizontal position of the property frame
      0,           // Vertical position of the property frame
      bstrCaption, // Property frame caption
      1,           // Number of objects
      &pUnk,       // Array of IUnknown pointers for objects
      1,           // Number of property pages
      &theCLSID,   // Array of CLSIDs for property pages
      NULL,        // Locale identifier
      0,           // Reserved - 0
      NULL         // Reserved - 0
      );
}

Creating a Macro

Once you've built the project, you can test the property page and the helper object using a simple macro that you can create and run in the Visual Studio development environment. This macro will create a helper object, then call its ShowPage method using the ProgID of the DocProperties property page and the IUnknown pointer of the document currently active in the Visual Studio editor. The code you need for this macro is shown below:

Imports EnvDTE
Imports System.Diagnostics

Public Module AtlPages

Public Sub Test()
    Dim Helper
    Helper = CreateObject("ATLPages7.Helper.1")

    On Error Resume Next
    Helper.ShowPage( ActiveDocument.Name, "ATLPages7Lib.DocumentProperties.1", DTE.ActiveDocument )
End Sub

End Module

When you run this macro, the property page will be displayed showing the file name and read-only status of the currently active text document. The read-only state of the document only reflects the ability to write to the document in the development environment; it doesn't affect the read-only attribute of the file on disk.

See also

Property Pages
ATLPages Sample