Using ADSI Classes

I think we should get back to our code, and dig a little deeper into the IADsClass interface. We'll attach some code to our Class… button so that it will give the class details of the currently selected object. For this to work, we need to have an object selected, so we should have the button disabled initially and only enable it when the user selects an object:

void CDirManagerDlg::OnSelchangeObjects() 
{
   // Object selected - enable "Class" button
   GetDlgItem(IDC_CLASS)->EnableWindow(TRUE);
}

We'll also disable the button at the end of OnEnumerate():

         pObjects->AddString(csItem);
   }

   // Disable "Class" button, as no item selected
   GetDlgItem(IDC_CLASS)->EnableWindow(FALSE);
}

This is what actually happens when we click on the Class… button:

void CDirManagerDlg::OnClass() 
{
   // Get currently selected item

   CListBox* pObjects = static_cast<CListBox*>(GetDlgItem(IDC_OBJECTS));
   int index = pObjects->GetCurSel();

   CString csObject;
   pObjects->GetText(index, csObject);

   // Extract pathname

   int start = csObject.Find('(');
   int end = csObject.Find(')');
   CString csPath = csObject.Mid(start + 1, end - start - 1);

   // Get object details

   IADsPtr pObject;
   HRESULT hr = ADsGetObject(_bstr_t(csPath), IID_IADs,
                                       reinterpret_cast<void**>(&pObject));

   if (FAILED(hr))
   {
      DisplayComError(_T("Failed to get object"), hr);
      return;
   }

   // Get class path

   BSTR bstrClassPath;
   hr = pObject->get_Schema(&bstrClassPath);

   if (FAILED(hr))
      AfxMessageBox(_T("No class path defined"));

   else
   {
   // Pass class path in to class dialog

      CString csClassPath = bstrClassPath;
      SysFreeString(bstrClassPath);

      CClassDlg dlg(csPath, csClassPath);
      dlg.DoModal();
   }
}

The critical line is that call to get_Schema(), which gets hold of the pathname to our class object. In certain cases, there won't be a class object; for example, the Schema itself doesn't have a class, and neither do any of the Properties defined in it.

We pass the class pathname, along with the pathname of our selected object, into a second dialog, CClassDlg. The dialog looks like this:

We have two list boxes:

IDC_MANDATORY for mandatory properties

IDC_OPTIONAL for optional properties

There are three static text fields at the top of the dialog:

IDC_CLASS

IDC_PRIMARY0

IDC_OID

Make these three as wide as will fit on the dialog (we've got a UUID to fit in one of them!). Inside the class dialog, we extract the class information, like this:

BOOL CClassDlg::OnInitDialog() 
{
   CDialog::OnInitDialog();

   // Get hold of object from path passed in

   IADsPtr pObject;

   if (m_csObjectPath != _T(""))
   {
      HRESULT hr = ADsGetObject(_bstr_t(m_csObjectPath), IID_IADs, 
         reinterpret_cast<void**>(&pObject));

      if (FAILED(hr))
      {
         DisplayComError(_T("Failed to get object"), hr);
         EndDialog(IDOK);
         return TRUE;
      }
   }

This dialog can also be used to get the class details if we double-click on a class item in the main list, so we might not have an object path set up.

   // Get hold of class from path passed in
   IADsClassPtr pClass;
   HRESULT hr = ADsGetObject(_bstr_t(m_csClassPath), IID_IADsClass, 
      reinterpret_cast<void**>(&pClass));

   if (FAILED(hr))
   {
      DisplayComError(_T("Failed to get class object"), hr);
      EndDialog(IDOK);
      return TRUE;
   }

Once we have got a pointer to the class interface, we can extract a few of its properties:

   // Get hold of class name and determine whether or not it's a container

   BSTR bstrName;
   pClass->get_Name(&bstrName);

   CString csName = bstrName;
   SysFreeString(bstrName);

   VARIANT_BOOL bContainer;
   pClass->get_Container(&bContainer);

   // Format header and output to form

   CString csHeader;

   if (bContainer == VARIANT_TRUE)
     csHeader.Format("Class <%s> (container)", 
                        static_cast<LPCSTR>(csName));
   else
     csHeader.Format("Class <%s> (not a container)",
                     static_cast<LPCSTR>(csName));

   SetDlgItemText(IDC_CLASS, csHeader);

   // Find out primary interface name and output to form

   BSTR bstrPrimary;
   hr = pClass->get_PrimaryInterface(&bstrPrimary);

   if (hr == S_OK)
   {
      CString csPrimary = bstrPrimary;
      SysFreeString(bstrPrimary);

      csHeader.Format("Primary i/f: %s", static_cast<LPCSTR>(csPrimary));

      SetDlgItemText(IDC_PRIMARY, csHeader);
   }

   // Find out OID and output to form

   BSTR bstrOID;
   hr = pClass->get_OID(&bstrOID);

   if (hr == S_OK)
   {
      CString csOID = bstrOID;
      SysFreeString(bstrOID);

      csHeader.Format("OID: %s", static_cast<LPCSTR>(csOID));

      SetDlgItemText(IDC_OID, csHeader);
   }

And now we can look at the object's mandatory and optional property settings. They get returned to us as an array of VARIANTs

   // Get list of mandatory properties

   _variant_t vProps;
   hr = pClass->get_MandatoryProperties(&vProps);

   if (FAILED(hr))
   {
      DisplayComError(_T("Failed to get mandatory properties"), hr);
      EndDialog(IDOK);
      return TRUE;
   }

   // Output to screen

   OutputArray(vProps, pObject, IDC_MANDATORY);
   
   // Get list of mandatory properties

   hr = pClass->get_OptionalProperties(&vProps);

   if (FAILED(hr))
   {
      DisplayComError(_T("Failed to get optional properties"), hr);
      EndDialog(IDOK);
      return TRUE;
   }

   // Output to screen

   OutputArray(vProps, pObject, IDC_OPTIONAL);

   return TRUE;  // return TRUE unless you set the focus to a control
                 // EXCEPTION: OCX Property Pages should return FALSE
}

Here we go through the slightly tedious process of extracting our property values from the incoming VARIANT array:

void CClassDlg::OutputArray(const VARIANT& vData, IADs* pObject, int nID)
{
   // Extract safe array from incoming variant

   COleSafeArray array(vData);

   long lBound;
   array.GetLBound(1, &lBound);

   long uBound;
   array.GetUBound(1, &uBound);

   int nItem = uBound - lBound;
   VARIANT HUGEP* pVData;
   array.AccessData(reinterpret_cast<void**>(&pVData));

   // Cycle through array, outputting property names and values to screen
   for (int item = 0; item < nItem; item++)
   {
      CString csProp;
      csProp = pVData[item].bstrVal;

Now we've got the property name, we can see if it's got a value for the current object:

      CString csItem;

      if (pObject)
      {
         _variant_t vValue;
         HRESULT hr = pObject->Get(pVData[item].bstrVal, &vValue);

         if (hr == S_OK)
         {
            CString csValue;

            switch (vValue.vt)
            {
               case VT_BSTR:
                  csValue = vValue.bstrVal;
                  break;

               case VT_I4:
                  csValue.Format("%ld", vValue.lVal);
                  break;

               case VT_DATE:
                  csValue = COleDateTime(vValue).Format();
                  break;

               default:
                  csValue.Format("type %d data", vValue.vt); 
                  break;
            }

            csItem.Format("%s = %s", static_cast<LPCSTR>(csProp),
                        static_cast<LPCSTR>(csValue));
         }

         else
            csItem.Format("%s (unset)", static_cast<LPCSTR>(csProp));
      }

      else
         csItem = csProp;

      static_cast<CListBox*>(GetDlgItem(nID))->AddString(csItem);
   }

   array.UnaccessData();
}

Finally, we need two alternative constructors, one where we're just getting information on a class, and one where we have an object path defined:

CClassDlg::CClassDlg(const CString& csPath, CWnd* pParent /*=NULL*/)
   : CDialog(CClassDlg::IDD, pParent)
{
   //{{AFX_DATA_INIT(CClassDlg)
      // NOTE: the ClassWizard will add member initialization here
   //}}AFX_DATA_INIT

   m_csObjectPath = _T("");
   m_csClassPath = csPath;
}

CClassDlg::CClassDlg(const CString& csObjectPath,
                     const CString& csClassPath, CWnd* pParent /*=NULL*/)
   : CDialog(CClassDlg::IDD, pParent)
{

   m_csObjectPath = csObjectPath;
   m_csClassPath = csClassPath;
}

We'll also need to add a couple of private members to the CClassDlg class:

   CString m_csObjectPath;
   CString m_csClassPath;

If we now build and run the application, and get to the list that we had before, let's see what happens if we select a typical object (say the one that we saw before, with pathname WinNT://OVERTHEHILLS/
Administrator
), and click on the Class… button, we see something like this:

Here we see one or two recognizable features in the optional properties list, such as the description that we see in the standard user administration tool, and the details of last log-in time and so on. Notice that UUID for the primary interface; if you look at the type library for IADs, you'll find that it is indeed the IID for the IADsUser interface.

OK, so we can extract the class details for an Active Directory object. But where are these class definitions kept, and, furthermore, can we add some of our own?

Let's put the second question to one side for a moment and concentrate on the first. In the WinNT: namespace, the class definitions are kept in the Schema container. Run up the DirManager application again, and take a look at the list that we found our Administrator object in. If you look here, you should find another object called Schema. (If you try to get the class for this, by the way, you'll find that — as I mentioned earlier — the class pathname isn't defined.) Double-click on this item to open up the container, and you should see a whole list of objects, like this:

All of these objects are classes, properties or syntaxes. At this point, we can finish off the code executed on the double-click of an item in the box. Let's do something different when we choose a leaf rather than a container; if the object is a class, we'll output the class definition using the same dialog as for displaying the class details:

   // Get class type for item

   BSTR bstrClass;
   pObject->get_Class(&bstrClass);
   CString csClass = bstrClass;
   SysFreeString(bstrClass);

   // If it's a class, set up class path and invoke class details dialog

   if (csClass == _T("Class"))
   {
      CClassDlg dlg(csPath);
      dlg.DoModal();
   }

If it's a property, we can show the syntax instead:

   else
   {
      CString csDetails;
      
      // Otherwise, if it's a property, get syntax and output to user

      if (csClass == _T("Property"))
      {
         IADsPropertyPtr pProperty;
         hr = pObject->QueryInterface(IID_IADsProperty,
                                     reinterpret_cast<void**>(&pProperty));
         if (FAILED(hr))
         {
            DisplayComError(_T("Failed to QI for property interface"), hr);
            return;
         }

         BSTR bstrSyntax;
         pProperty->get_Syntax(&bstrSyntax);
         CString csSyntax = bstrSyntax;
         SysFreeString(bstrSyntax);

         csDetails.Format("Property with syntax %s",
                        static_cast<LPCSTR>(csSyntax));
      }

And if it's a syntax, on the other hand, let's show the OLE data type:

      // If it's a syntax, get data type and output to user

      else if (csClass == _T("Syntax"))
      {
         IADsSyntaxPtr pSyntax;
         hr = pObject->QueryInterface(IID_IADsSyntax,
                                       reinterpret_cast<void**>(&pSyntax));

         if (FAILED(hr))
         {
            DisplayComError(_T("Failed to QI for syntax interface"), hr);
            return;
         }

         long lType;
         pSyntax->get_OleAutoDataType(&lType);

         csDetails.Format("Syntax of type %ld", lType);
      }

Finally, if it's a plain old object, let's just show the class type:

      // Otherwise, it's just an object - output its class

      else
         csDetails.Format("Object with class %s",
                        static_cast<LPCSTR>(csClass));

      AfxMessageBox(csDetails);
   }

}

Let's try this code out with some of the objects in the Schema folder. We can see, to take just a few examples, that Computer is a class object, OperatingSystem is a property of syntax String and String is a syntax of type 8 (which turns out to be VT_BSTR — so no surprises there). Notice, incidentally, that there isn't a class object in the list called Class — I was quite disappointed to find out that it's hard-wired, but I suppose you'd get into a hideous self-referential loop if it wasn't.

Before we leave the WinNT: namespace and dive into LDAP, we've one more important topic to cover. It's time we did something with that New… button.

© 1998 by Wrox Press. All rights reserved.