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_PRIMARY
0
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/
), and click on the Class… button, we see something like this:
Administrator
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.