Example: Implementing a Property Page

ATL COM Property Pages

In this example, you'll see how to build a property page that displays (and allows you to change) properties of the ITextDocument interface. This interface is exposed by the TextDocument object in the Developer Studio object model (although the property page that you'll create won't care where the objects it manipulates come from as long as they support the correct interface).

To complete this example, you will:

Using the ATL Object Wizard

First, create a new ATL project for a DLL server called "ATLPages." Now use the ATL Object Wizard to generate a property page. Give the property page a Short Name of "TextDocumentProperties" 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:

These controls will be used to display the filename 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(CTextDocumentProperties)
    COMMAND_HANDLER(IDC_NAME, EN_CHANGE, OnUIChange)
    COMMAND_HANDLER(IDC_READONLY, BN_CLICKED, OnUIChange)
    CHAIN_MSG_MAP(IPropertyPageImpl<CTextDocumentProperties>)
END_MSG_MAP()

    LRESULT OnUIChange(WORD wNotifyCode, WORD wID, HWND hWndCtl,
                       BOOL& 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. If you do need to do that, it's a simple matter to write the code yourself.

Housekeeping

Now add a couple of #includes to TextDocumentProperties.h so that the compiler knows about the ITextDocument interface:

#include <ObjModel\TextGuid.h>
#include <ObjModel\TextAuto.h>

You'll also need to refer to the IPropertyPageImpl base class, so to make things easier, add the following typedef to the CTextDocumentProperties class:

typedef IPropertyPageImpl<CTextDocumentProperties> 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 ITextDocument interface that you're expecting:

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

Note   It only makes sense to support a single object for this page because you will allow the user to set the filename 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. 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;

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

    // Get the ITextDocument pointer
    CComQIPtr<ITextDocument> pDoc(m_ppUnk[0]);
    if (!pDoc)
        return E_UNEXPECTED;

    // Get the FullName property
    CComBSTR bstr;
    hr = pDoc->get_FullName(&bstr);
    if (FAILED(hr))
        return hr;

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

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

    // Set the check box to show the document's read-only status
    CheckDlgButton(IDC_READONLY,
                   bReadOnly ? BST_CHECKED : BST_UNCHECKED);

    return hr;
}

This code uses the COM methods of the ITextDocument 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.

Override IPropertyPageImpl::Apply

When the user wants 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;

    // Check whether we need to update the object
    // (Property frame may call Apply when it doesn't need to)
    if (!m_bDirty)
        return S_OK;

    HRESULT hr = E_UNEXPECTED;

    // Get a pointer to the document
    CComQIPtr<ITextDocument> pDoc(m_ppUnk[0]);
    if (!pDoc)
        return hr;

    // Set the read-only property
    hr = pDoc->put_ReadOnly(IsDlgButtonChecked(IDC_READONLY) ? VARIANT_TRUE : VARIANT_FALSE);
    if (FAILED(hr))
        return hr;

    // Get the filename and save the document
    CComBSTR bstrName;
    GetDlgItemText(IDC_NAME, bstrName.m_str);

    DsSaveStatus status;
    hr = pDoc->Save(CComVariant(bstrName), CComVariant(), &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 top of this implementation avoids unnecessary updates of the objects if Apply is called more than once.

Note   ITextDocument 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. You can see that the code in a property page doesn't have to limit itself to getting or setting properties.

Testing the Property Page

To display this page, you need to create a simple helper object that you can use to display a property page. 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 scripting languages.

Create a new Simple Object using the ATL Object Wizard 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. 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
                );
}

To allow this helper to be used from script, you need to mark it as safe. You can do this by supporting the IObjectSafety interface. Add IObjectSafetyImpl as a base class as shown

    public IObjectSafetyImpl<CHelper, INTERFACESAFE_FOR_UNTRUSTED_CALLER>

Also add an entry to the COM map for this interface:

    COM_INTERFACE_ENTRY(IObjectSafety)

Creating a Macro

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

Sub ATLPagesTest()
    Dim Helper
    Set Helper = CreateObject("ATLPages.Helper.1")

On Error Resume Next       ' Handle case where there's no active document
    If LCase(ActiveDocument.Type) = "text" Then    ' Only text documents
        Helper.ShowPage ActiveDocument.Name, _
                        "ATLPages.TextDocumentProperties.1", _
                        ActiveDocument
    End If
End Sub

When you execute this macro, the property page will be displayed showing the filename and read-only status of the currently active text document. (Note that the read-only state of the document only reflects the ability to write to the document in the Visual C++ environment; it doesn't affect the read-only attribute of the file on disk. Documents under source control are unaffected by this property page).

If you execute the macro without a document being active, the On Error Resume Next statement will ensure that nothing happens.

For information on creating this macro, see Writing VBScript Macros by Hand.