Platform SDK: Active Directory, ADSI, and Directory Services

Implementing the Property Page COM Object

To extend property sheets for directory objects, a property page COM object must implement two shell interfaces: IShellExtInit and IShellPropSheetExt.

A property page COM object is instantiated when the user views the properties for an object of a class for which the property page has been registered. If the COM object was registered in the class's adminPropertyPages property, the property page appears when the user views properties for objects of that class in Active Directory administrative snap-ins. If it was registered in shellPropertyPages, the property page appears in the Windows shell.

The IShellExtInit interface has only one method and it must be implemented:

IShellExtInit::Initialize
After the property page COM object is instantiated, the shell calls the IShellExtInit::Initialize method. This is the property page COM object's only opportunity to get the IDataObject pointer that enables the object to get information about the selected objects. You should store the IDataObject pointer in a member variable so you can use the IDataObject::GetData method to retrieve information with the following clipboard formats:
CFSTR_DS_DISPLAY_SPEC_OPTIONS
Provides information about whether the property page was invoked by the Windows shell or by an Active Directory administrative snap-in. The data is returned as an HGLOBAL that points to a DSDISPLAYSPECOPTIONS structure. The structure contains additional information that tells you about the user context and server where the shell or snap-in retrieved the data about the selected objects. This additional information is useful if you need to bind to one or more objects in the selection.
CFSTR_DSOBJECTNAMES
Provides the list of objects that were selected when the property page was invoked by the Windows shell or by an Active Directory administrative snap-in. The data is returned as an HGLOBAL that points to a DSOBJECTNAMES structure. The structure contains the count of items in the selection and a pointer to an array of DSOBJECT structures that represent each selected item. The DSOBJECT structure contains the ADsPath, class name, and flags (indicating whether the page should be read-only and whether the object is a container). If it is necessary to read or modify properties on the object, the ADsPath can be used to bind to the object and perform the necessary operations.

You should also store the clipboard formats as member variables. The IShellExtInit::Initialize method is a good place to register these formats.

CFSTR_DSPROPERTYPAGEINFO
Applies only to property pages. This clipboard format provides the optionaldata string at the end of property page COM object's value in the adminPropertyPages and shellPropertyPages property. If optionaldata was not specified, IDataObject::GetData returns E_NOTIMPL.

The following code fragment contains an implementation of the IShellExtInit::Initialize method. This method releases the IDataObject pointer if IShellExtInit::Initialize was called before, stores the passed IDataObject pointer in a member variable, and registers the two clipboard formats in a member variable:

STDMETHODIMP CMyDsPropertyPage::Initialize(LPCITEMIDLIST pIDFolder,
                                   LPDATAOBJECT pDataObj,
                                   HKEY hRegKey)
{
    // Initialize can be called more than once.
    if (m_pDataObj)
        m_pDataObj->Release();
 
    // Keep a pointer to the IDataObject.
    if (pDataObj)
    {
        m_pDataObj = pDataObj;
        pDataObj->AddRef();
    }
 
    //Register the clipboard formats to use for getting info about
    //the selected DS objects.
    m_cfDsDispSpecOptions = RegisterClipboardFormat(CFSTR_DS_DISPLAY_SPEC_OPTIONS);
    m_cfDsObjectNames = RegisterClipboardFormat(CFSTR_DSOBJECTNAMES);
 
    return NOERROR;
} 

Note that the IShellExtInit::Initialize method is implemented the same way for context menu extensions.

After IShellExtInit::Initialize returns, the IShellPropSheetExt::AddPages method is called.

You can implement the property page(s) in the class implementing the IShellPropSheetExt and IShellExtInit interfaces. Or you can implement each property page in a class that is completely separate from the class implementing the two shell extension interfaces.

The class that implements the two shell extension interfaces only needs to be instantiated for the creation of the pages, whereas the class for each page lasts as long as the property sheet. By implementing a class for each page, the class can be tailored for manipulating the attributes displayed on its page. If the property pages are implemented as separate classes, the class implementing the property page should be initialized with the appropriate member data such as the IDataObject pointer and any other data about the selected objects required by the property page class.

The IShellPropSheetExt interface has two methods that must be implemented:

IShellPropSheetExt::AddPages
This method is called just before the property sheet is displayed. This is the property page COM object's opportunity to create one or more property pages using the CreatePropertySheetPage function. In the PROPSHEETPAGE structure passed to CreatePropertySheetPage, store a this pointer to the class that owns the property page in the lParam member, so that the dialog box procedure for the property page can access the class members. When the dialog box procedure for the property page is called, the pointer to the PROPSHEETPAGE structure is passed as the lParam. This means the dialog box procedure has access to the class members.

Important  The IShellPropSheetExt::AddPages method call runs on the caller's main thread. The dialog box procedure runs on a different thread. Therefore, do not reference the IDataObject pointer in the dialog box procedure unless it is first marshalled to this thread.

If the dialog box procedure requires data from the IDataObject, you can avoiding having to marshal the IDataObject pointer by extracting the data that the dialog box procedure needs and storing that data in member variables in the class. This can be done in the IShellExtInit::Initialize or the IShellPropSheetExt::AddPages methods. Then the dialog box procedure can access the class members via the lParam. Typically, the dialog box procedure will need only the ADsPath of the selected objects so that it can then use the ADsPath information to bind and perform operations on those objects.

IShellPropSheetExt::ReplacePage
This method is not called. It is only called for extension pages for the Control Panel. The method should just return E_FAIL.

The following code fragment contains an implementation of the IShellPropSheetExt methods and the dialog box procedure for the single property page added by the IShellPropSheetExt::AddPages method. The property page binds to the selected object and displays some properties for it. For the full implementation of this sample, see the following DSSamplePage sample.

STDMETHODIMP DSSamplePage::AddPages(LPFNADDPROPSHEETPAGE lpfnAddPage, LPARAM lParam)
{
    HRESULT hr = S_OK;
    STGMEDIUM ObjMedium = {TYMED_NULL};
    FORMATETC fmte = {g_cfDsObjectNames, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
    LPDSOBJECTNAMES pDsObjectNames;
    PWSTR pwzObjName;
    PWSTR pwzClass;
 
    // Get the path to the DS object from the data object.
    // Note: This call runs on the caller's main thread. The pages' window
    // procs run on a different thread, so don't reference the data object
    // from a winproc unless it is first marshaled on this thread.
    // For this sample, we extract the data we want and store
    // it in member variables so that the dialog proc
    // can access the data without having to marshall
    // the IDataObject pointer.
    hr = m_pDataObj->GetData(&fmte, &ObjMedium);
    if (SUCCEEDED(hr))
    {
        pDsObjectNames = (LPDSOBJECTNAMES)ObjMedium.hGlobal; 
 
        if (pDsObjectNames->cItems < 1)
        {
            hr = E_FAIL;
        }
        pwzObjName = (PWSTR)ByteOffset(pDsObjectNames,
                                       pDsObjectNames->aObjects[0].offsetName);
        pwzClass = (PWSTR)ByteOffset(pDsObjectNames,
                                       pDsObjectNames->aObjects[0].offsetClass);
        // Save the ADsPath of object
        m_ObjPath = new WCHAR [wcslen(pwzObjName )+1];
        wcscpy(m_ObjPath,pwzObjName);
    }
 
    // Now release the ObjMedium:
    // If punkForRelease is NULL, the receiver of 
    // the medium is responsible for releasing it; otherwise, 
    // punkForRelease points to the IUnknown on the appropriate 
    // object so its Release method can be called. 
 
    ReleaseStgMedium(&ObjMedium);
 
    PROPSHEETPAGE psp;
    HPROPSHEETPAGE hpage;
    HRESULT hres = 0;
 
    LPCSHELLEXT lpcsext = this;
    //
    // Create a property sheet page object from a dialog box.
    //
    // We store a pointer to our class in the psp.lParam, so we
    // can access our class members from within the DSSamplePageDlgProc.
    //
    // If the page needs more instance data, you can append
    // arbitrary size of data at the end of this structure,
    // and pass it to the CreatePropSheetPage. In such a case,
    // the size of entire data structure (including page specific
    // data) must be stored in the dwSize field.   Note that in
    // general you should NOT need to do this, as you can simply
    // store a pointer to data in the lParam member.
    //
 
    psp.dwSize      = sizeof(psp);    // no extra data.
    psp.dwFlags     =  PSP_USETITLE | PSP_USECALLBACK;
    psp.hInstance   = g_hInstance;
    psp.pszTemplate = MAKEINTRESOURCE(IDD_DSSamplePage);
    psp.hIcon       = 0;
    psp.pszTitle    = L"Object Stats";
    psp.pfnDlgProc  = DSSamplePageDlgProc;
    psp.pcRefParent = NULL;//&g_cRefThisDll;
    psp.pfnCallback = DSSamplePageCallback;
    psp.lParam      = (LPARAM)lpcsext;
    
    AddRef();
    hpage = CreatePropertySheetPage(&psp);
    
    if (hpage) 
    {
        if (!lpfnAddPage(hpage, lParam)) 
        {
            DestroyPropertySheetPage(hpage);
            Release();
        }
    }
 
    return NOERROR;
 
}
 
 
STDMETHODIMP DSSamplePage::ReplacePage(UINT uPageID, LPFNADDPROPSHEETPAGE lpfnReplaceWith, LPARAM lParam)
{
    return E_FAIL;
}
 
 
BOOL CALLBACK DSSamplePageDlgProc(HWND hDlg, 
                             UINT uMessage, 
                             WPARAM wParam, 
                             LPARAM lParam)
{
    LPPROPSHEETPAGE psp=(LPPROPSHEETPAGE)GetWindowLong(hDlg, DWL_USER);
    UINT iIndex=0;
    LPCSHELLEXT lpcs;
 
    switch (uMessage)
    {
        //
        // When the shell or snap-in creates a dialog box for a property sheet page,
        // it passes the pointer to the PROPSHEETPAGE data structure as
        // lParam. The dialog procedures of extensions typically store it
        // in the DWL_USER of the dialog box window.
        //
        case WM_INITDIALOG:
            {
                SetWindowLong(hDlg, DWL_USER, lParam);
                BSTR bsResult;
 
                psp = (LPPROPSHEETPAGE)lParam;
 
                lpcs = (LPCSHELLEXT)psp->lParam;
 
                HRESULT hr; 
                IADs* pIADs = NULL;
 
                hr = ADsGetObject(  lpcs->m_ObjPath, IID_IADs,(void **)&pIADs);
 
                if (SUCCEEDED(hr))
                {
                    // Retrieves the GUID for this object- The guid uniquely identifies 
                    // this directory object. The Guid is globally unique
                    // Also the guid is rename/move safe. The ADsPath below returns the 
                    // CURRENT location of the object- The guid remains constant regardless of 
                    // name or location of the directory object
                    pIADs->get_GUID(&bsResult); 
                    SetWindowText(GetDlgItem(hDlg,IDC_GUID),bsResult);
                    SysFreeString(bsResult);
 
                    // Retrieves the RDN
                    pIADs->get_Name(&bsResult); 
                    SetWindowText(GetDlgItem(hDlg,IDC_NAME),bsResult);
                    SysFreeString(bsResult);
 
                    // Retrieves the value in the class attribute, that is, group
                    pIADs->get_Class(&bsResult); 
                    SetWindowText(GetDlgItem(hDlg,IDC_CLASS),bsResult);
                    SysFreeString(bsResult);
 
                    // Retrieves the full literal LDAP path for this object.
                    // This may be used to re-bind to this object- though for persistent
                    // storage (and to be 'move\rename' safe) it is suggested that the 
                    // guid be used instead of the ADsPath
                    pIADs->get_ADsPath(&bsResult); 
                    SetWindowText(GetDlgItem(hDlg,IDC_ADSPATH),bsResult);
                    SysFreeString(bsResult);
 
                    // Retrieves the LDAP path for the parent\container for this object
                    pIADs->get_Parent(&bsResult); 
                    SetWindowText(GetDlgItem(hDlg,IDC_PARENT),bsResult);
                    SysFreeString(bsResult);
 
                    // Retrieves the LDAP path for the Schema definition of the object returned from 
                    /// the IADs::get_Schema() member
                    pIADs->get_Schema(&bsResult); 
                    SetWindowText(GetDlgItem(hDlg,IDC_SCHEMA),bsResult);
                    SysFreeString(bsResult);
                    
                    pIADs->Release();
                    pIADs = NULL;
                }
            }
            break;
 
        case WM_DESTROY:
            RemoveProp(hDlg, L"ID");
            break;
 
        case WM_NOTIFY:
            switch (((NMHDR FAR *)lParam)->code)
            {
                case PSN_SETACTIVE:
                    break;
 
                case PSN_APPLY:
                    //TODO: Apply changes user made here.
                     break;
 
                default:
                    break;
            }
            break;
 
        default:
            return FALSE;
    }
 
    return TRUE;
}