Share via


Example: Implementing a Property Page

 

The new home for Visual Studio documentation is Visual Studio 2017 Documentation on docs.microsoft.com.

The latest version of this topic can be found at Implementing a Property Page (ATL).

This example shows how to build a property page that displays (and allows you to change) properties of the Document Classes interface. This interface is exposed by documents in Visual Studio's Common Environment Object Model Examples (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).

The example is based on the ATLPages sample.

To complete this example, you will:

  • Add the ATL property page class using the Add Class dialog box and the ATL Property Page Wizard.

  • Edit the dialog resource by adding new controls for the interesting properties of the Document interface.

  • Add message handlers to keep the property page site informed of changes made by the user.

  • Add some #import statements and a typedef in the Housekeeping section.

  • Override IPropertyPageImpl::SetObjects to validate the objects being passed to the property page.

  • Override IPropertyPageImpl::Activate to initialize the property page's interface.

  • Override IPropertyPageImpl::Apply to update the object with the latest property values.

  • Display the property page by creating a simple helper object.

  • Create a macro that will test the property page.

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:

Editing a dialog resource

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