Platform SDK: Active Directory, ADSI, and Directory Services

Registering the Property Page COM Object in a Display Specifier

Even after you've written and installed your property page COM object, the Windows shell and Active Directory administrative snap-ins cannot recognize it yet. For the Windows shell or Active Directory administrative snap-ins to recognize your property page, you must add a value for your property page COM object in the adminPropertyPages and/or shellPropertyPages property of the display specifier for the class whose property sheet you want to extend. Note that you can register the same property page COM object for more than one class.

If the COM object was registered in the adminPropertyPages property of the class's display specifier, 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. If you want it to appear in both the shell and administrative snap-ins, you need to register the COM object in both properties.

The adminPropertyPages and shellPropertyPages properties are multi-valued. The adminPropertyPages contains the list of property pages for Active Directory administrative snap-ins to display in the property sheet for the object class. The shellPropertyPages contains the list for the Windows shell.

Each value is a string with the following format:

ordernumber,CLSID,optionaldata

In this example, the ordernumber is an unsigned number that represents the page's position on the sheet. When a property sheet is displayed, the values are sorted using a comparison of each value's ordernumber. If more than one value has the same ordernumber, those property page COM objects are loaded in the order they are read from Active Directory ; however, you should use a non-existing ordernumber (that is, one that has not been used by other values in the property). There is no prescribed starting position, and gaps are allowed in the ordernumber sequence.

The CLSID is the CLSID of the property page COM object in the string format produced by the StringFromGUID2 function in the COM library.

The optionaldata can be retrieved by the property page COM object using the IDataObject pointer passed to its IShellExtInit::Initialize method. The optionaldata is not required. The property page COM object calls IDataObject::GetData method with the clipboard format CF_DSPROPERTYPAGEINFO and gets an global memory handle (HGLOBAL) to a DSPROPERTYPAGEINFO structure containing the offset to the Unicode string containing optionaldata. A COM object can implement more than one property page. So, one possible use of the optionaldata is to name the pages to display. This gives you the flexibility of choosing to implement multiple COM objects (one for each page) or a single COM object to handle multiple pages.

The following string is an example value for the adminPropertyPages or shellPropertyPages properties:

1,{6dfe6485-a212-11d0-bcd5-00c04fd8d5b6} 

Important  For the Windows shell, display specifier information is retrieved at user logon, and cached for the user's session. For the administrative snap-ins, the display specifier information is retrieved when the snap-in is loaded, and is cached for the lifetime of the process. For the Windows shell, this means changes to display specifiers take effect after a user logs off and back on again. For the administrative snap-ins, changes take effect when the snap-in or console file is loaded.

Adding a Value to the adminPropertyPages or shellPropertyPages properties

When you add a value for your property page COM object to adminPropertyPages or shellPropertyPages, follow these rules:

  1. Make sure it hasn't already been added.
  2. Add a new value at the end of the property page ordering list. This means setting the ordernumber portion of the value to the next value after the highest existing ordernumber.
  3. To add the value, use the IADs::PutEx method with the lnControlCode parameter set to ADS_PROPERTY_APPEND so that the value will be added to the existing values (and, therefore, not overwrite the existing values). Make sure you call IADs::SetInfo to commit the change to the directory.

The following program adds a property page to the group class in the computer's default locale. Note how the AddPropertyPageToDisplaySpecifier function checks for the property page's CLSID in the existing values, gets the highest ordernumber, and adds the value for the property page using IADs::PutEx with the ADS_PROPERTY_APPEND control code.

#include <wchar.h>
#include <objbase.h>
#include <activeds.h>
 
HRESULT AddPropertyPageToDisplaySpecifier(
    LPOLESTR szClassName, //ldapDisplayName of class
    CLSID *pPropPageCLSID //CLSID of property page COM object
    );
 
HRESULT BindToDisplaySpecifiersContainerByLocale(LCID *locale,
                                        IADsContainer **ppDispSpecCont
                                          );
 
HRESULT GetDisplaySpecifier(IADsContainer *pContainer, LPOLESTR szDispSpec, IADs **ppObject);
 
 
void wmain( int argc, wchar_t *argv[ ])
{
 
wprintf(L"This program adds a sample property page to the display specifier for group class in the local computer's default locale.\n");
 
//Initialize COM.
CoInitialize(NULL);
HRESULT hr = S_OK;
 
//Class ID for the sample property page
LPOLESTR szCLSID = L"{D9FCE809-8A10-11d2-A7E7-00C04F79DC0F}";
LPOLESTR szClass = L"group";
CLSID clsid;
//Convert to GUID.
hr = CLSIDFromString(
    szCLSID,  //Pointer to the string representation of the CLSID.
    &clsid    //Pointer to the CLSID.
    );
 
 
hr = AddPropertyPageToDisplaySpecifier(
    szClass, //ldapDisplayName of class
    &clsid //CLSID of property page COM object
    );
if (S_OK == hr)
    wprintf(L"Property page registered successfully\n");
else if (S_FALSE == hr)
    wprintf(L"Property page was not added because it was already registered.\n");
else
    wprintf(L"Property page was not added. HR: %x.\n");
 
//Uninitialize COM.
CoUninitialize();
return;
}
 
 
//Adds a property page to Active Directory admin snap-ins.
HRESULT AddPropertyPageToDisplaySpecifier(
    LPOLESTR szClassName, //ldapDisplayName of class.
    CLSID *pPropPageCLSID //CLSID of property page COM object.
    )
{
HRESULT hr = E_FAIL;
IADsContainer *pContainer = NULL;
LPOLESTR szDispSpec = new OLECHAR[MAX_PATH];
IADs *pObject = NULL;
VARIANT var;
LPOLESTR szProperty = L"adminPropertyPages";
LCID locale = NULL;
//Get the display specifier container using default system locale.
//Note that when you are adding your property page COM object,
//you should specify the locale because the registration is
//locale specific. This means if you created a property page
//for German, you want to explicitly add it to the 407 container
//so that it will be used when a computer is running with locale
//set to German AND NOT whatever locale set on the
//computer where this program is running.
hr = BindToDisplaySpecifiersContainerByLocale(&locale,
                                        &pContainer
                                        );
//TODO: Need to handle fail case where dispspec object
//is not found and give option to create one.
 
if (SUCCEEDED(hr))
{
    //Bind to display specifier object for the specified class.
    //Build the display specifier name.
    wcscpy(szDispSpec, szClassName);
    wcscat(szDispSpec, L"-Display");
    hr = GetDisplaySpecifier(pContainer, szDispSpec, &pObject);
    if (SUCCEEDED(hr))
    {
        //Convert GUID to string.
        LPOLESTR szDSGUID = new WCHAR [39];
        ::StringFromGUID2(*pPropPageCLSID, szDSGUID, 39); 
 
        //Get the adminPropertyPages property.
        hr = pObject->GetEx(szProperty,&var);
        if (SUCCEEDED(hr))
        {
          LONG lstart, lend;
          SAFEARRAY *sa = V_ARRAY( &var );
          VARIANT varItem;
          // Get the lower and upper bound.
            hr = SafeArrayGetLBound( sa, 1, &lstart );
          if (SUCCEEDED(hr))
          {
            hr = SafeArrayGetUBound( sa, 1, &lend );
          }
          if (SUCCEEDED(hr))
          {
            //Now iterate the values to check if the prop page's CLSID
            //is already registered.
            VariantInit(&varItem);
            BOOL bExists = FALSE;
            UINT uiLastItem = 0;
            UINT uiTemp = 0;
            INT iOffset = 0;
            LPOLESTR szMainStr = new OLECHAR[MAX_PATH];
            LPOLESTR szItem = new OLECHAR[MAX_PATH];
            LPOLESTR szStr = NULL;
            for ( long idx=lstart; idx <= lend; idx++ )
            {
              hr = SafeArrayGetElement( sa, &idx, &varItem );
              if (SUCCEEDED(hr))
              {
                //Check if the specified CLSID is already registered.
                wcscpy(szMainStr,varItem.bstrVal);
                if (wcsstr( szMainStr,szDSGUID))
                    bExists = TRUE;
                //Get the index which is the number before the first comma.
                szStr = wcschr(szMainStr, ',');
                iOffset = (int)(szStr - szMainStr);
                wcsncpy( szItem, szMainStr, iOffset );
                szItem[iOffset]=0L;
                uiTemp = _wtoi(szItem);
                if (uiTemp > uiLastItem)
                    uiLastItem = uiTemp;
                VariantClear(&varItem);
              }
            }
            //If the CLSID is not registered, add it.
            if (!bExists)
            {
                //Build the value to add.
                LPOLESTR szValue = new OLECHAR[MAX_PATH];
                //Next index to add at end of list.
                uiLastItem++;
                _itow( uiLastItem, szValue, 10 );   
                wcscat(szValue,L",");
                //Add the class ID for the property page.
                wcscat(szValue,szDSGUID);
//                  wprintf(L"Value to add: %s\n", szValue);
                VARIANT varAdd;
                //Only one value to add
                LPOLESTR pszAddStr[1];
                pszAddStr[0]=szValue;
                ADsBuildVarArrayStr(pszAddStr, 1, &varAdd);
 
                hr = pObject->PutEx( ADS_PROPERTY_APPEND, szProperty, varAdd );
                if (SUCCEEDED(hr))
                {
                    //Commit the change.
                    hr = pObject->SetInfo();
                }
            }
            else
                hr = S_FALSE;
          }
        }
        VariantClear(&var);
    }
    if (pObject)
    pObject->Release();
}
 
return hr;
}
 
//This function returns a pointer to the display specifier container 
//for the specified locale.
//If locale is NULL, use the default system locale and then return the locale in the locale param.
HRESULT BindToDisplaySpecifiersContainerByLocale(LCID *locale,
                                        IADsContainer **ppDispSpecCont
                                        )
{
HRESULT hr = E_FAIL;
 
if ((!ppDispSpecCont)||(!locale))
    return E_POINTER;
 
//If no locale is specified, use the default system locale.
if (!(*locale))
{
    *locale = GetSystemDefaultLCID();
    if (!(*locale))
    return E_FAIL;
}
 
//Make sure that it's a valid locale.
if (!IsValidLocale(*locale, LCID_SUPPORTED))
    return E_INVALIDARG;
 
LPOLESTR szPath = new OLECHAR[MAX_PATH*2];
IADs *pObj = NULL;
VARIANT var;
 
hr = ADsOpenObject(L"LDAP://rootDSE",
                     NULL,
                     NULL,
                     ADS_SECURE_AUTHENTICATION, //Use Secure Authentication.
                     IID_IADs,
                     (void**)&pObj);
 
if (SUCCEEDED(hr))
{
    //Get the DN to the config container.
    hr = pObj->Get(L"configurationNamingContext",&var);
    if (SUCCEEDED(hr))
    {
        //Build the string to bind to the DisplaySpecifiers container.
        swprintf(szPath,L"LDAP://cn=%x,cn=DisplaySpecifiers,%s", *locale, var.bstrVal);
        //Bind to the DisplaySpecifiers container.
        *ppDispSpecCont = NULL;
        hr = ADsOpenObject(szPath,
            NULL,
            NULL,
            ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
            IID_IADsContainer,
            (void**)ppDispSpecCont);
 
        if(FAILED(hr))
        {
            if (!(*ppDispSpecCont))
            {
                (*ppDispSpecCont)->Release();
                (*ppDispSpecCont) = NULL;
            }
        }
    }
}
//Clean up
VariantClear(&var);
if (pObj)
    pObj->Release();
 
return hr;
}
 
HRESULT GetDisplaySpecifier(IADsContainer *pContainer, LPOLESTR szDispSpec, IADs **ppObject)
{
HRESULT hr = E_FAIL;
LPOLESTR szDSPath = new OLECHAR [MAX_PATH];
IDispatch *pDisp = NULL;
 
//Build relative path to the display specifier object.
wcscpy(szDSPath, L"CN=");
wcscat(szDSPath, szDispSpec);
if (!pContainer)
{
    hr = E_POINTER;
    return hr;
}
//TODO check the other pointers.
//Initialize the output pointer.
(*ppObject) = NULL;
 
//Use child object binding with IADsContainer::GetObject.
hr = pContainer->GetObject(L"displaySpecifier",
                      szDSPath,
                      &pDisp);
if (SUCCEEDED(hr))
{
    hr = pDisp->QueryInterface(IID_IADs, (void**)ppObject);
    if (FAILED(hr))
    {
        //Clean up
        if (*ppObject)
        (*ppObject)->Release();
    }
}
 
if (pDisp)
    pDisp->Release();
 
return hr;
 
}