Example: Implementing a Property Page
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).
Observação
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 file name of the document and its read-only status.
Observação
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.
Observação
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;
}
Observação
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;
}
Observação
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.
Observação
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.