Novell is now a part of Micro Focus

Rapid NetWare Application Development with Borland Delphi and Apiary Developer's Suite for NetWare

Articles and Tips: article

BRIAN MCCORMICK
Systems Engineer
Apiary, Inc.

01 Jul 1995


This DevNote examines some of the features of Apiary Developer's Suite for NetWare. This DevNote also shows how to develop a small browser application for NetWare Directory Services (NDS). While we're writing this example, we'll explore both the Apiary class library and how to use NWCALLS functions to make up for some of its limitations (the class library is not exhaustive).

Introduction

By now you have probably seen a demonstration or a review of Borland's new Delphi visual development tool. No doubt you are thinking, "Sure, that looks great, but my applications have to take advantage of NetWare features. I have to use C or C++." In fact, with Apiary Developer's Suite for NetWare you can access all the same NetWare features that you can with C or C++, and have the advantage of Rapid Application Development that Borland's Delphi provides. You also get the choice of using the same NWCALLS functions that you have been using all this time, or you can take advantage of Apiary's class library and save yourself hours of development effort for most NetWare development tasks.

Over the next few pages, we'll examine some of the features of Apiary Developer's Suite for NetWare and along the way we'll develop a small browser application for NetWare Directory Services (NDS). Using this application, it will be possible to navigate through the NDS tree and examine individual objects in the directory and their attributes. Since the application uses NDS features, it requires NetWare 4.x. Apiary Developer's Suite does support NetWare 2.x and 3.x, however, and you could write a bindery, print queue management, or drive mapping example just as easily.

While we're writing this example, we'll explore both the Apiary class library and how to use NWCALLS functions to make up for some of its limitations (the class library is not exhaustive). We'll make a mistake or two to see how errors are handled, and we'll make some comparisons to see just how much programming effort is being saved. If you want to save even more programming effort, of course, you can log onto CompuServe and GO APIARY to download the source and a compiled executable for this application.

Ordering Information

Apiary Developer's Suite for NetWare, Delphi Edition can be ordered directly from Apiary:

Apiary, Inc. 10201 W. Markham, Suite 101 Little Rock, AR 72205 (501) 221-3699 Fax: (501) 221-7412

Apiary Developer's Suite for NetWare costs $249. There are no runtime royalties on compiled applications developed using this software.

Getting Started

Before we start in earnest on this application, it's worth examining what files come with the Developer's Suite that we will be needing. For the most part, that means we'll need the on-line help for the client SDK for NetWare, the class library reference, and the compiled "units" (Delphi jargon for modules) that are relevant to our project. There are 38 example programs included with the product, including a more sophisticated version of this browser, however, we'll be ignoring these. The printed manual that comes with the product will also be useful. It is invaluable as a quick how-to reference. It isn't intended to be a complete function or class library reference, however, and that is one reason why we will use the on-line help extensively.

Complete source for the class library and the client SDK interface modules is provided, however, we'll be using the ones which came with the product pre-compiled. These are located in a directory which the Developer's Suite installation routine added to Delphi's search path, so we won't have to go looking for them. The client SDK modules all begin with "NW" and have the same names as their NetWare Client SDK for C counterparts. The class library modules all begin with "ACN" (for Apiary Classes for NetWare) and are named according to the NetWare service area the classes defined within them cover. The NetWare Directory Services module, around which we will develop most of this application, is called ACNDS.

It's worth mentioning at this point that the Apiary Class library is not a library made of Delphi components. A decision was made early on in the development of this product to create a more traditional code-based class library rather than a set of components. While the drag and drop programming method allowed by components is very convenient, the Apiary class library does gain some flexibility by not being component based. It is not, for example, tied to a particular means of presenting data to the user like Apiary's VBX for NetWare (also shipped as part of this product) is. As we'll see, the class library doesn't lose much in terms of ease of use as a result of this decision.

The Main Form

The main form of our application must provide all the controls we will use to navigate the NDS tree. This essentially means that it needs to have a means of recording and changing the current position in the tree (a context), a means of selecting a new position within the tree, and a means of determining what objects are contained within the current context. Figure 1 shows the NDS Browser's main form. The form is built in the Delphi environment by simply clicking and dragging. This form uses list boxes to display the list of objects and the list of container objects within the context displayed in an edit box. To change the context and update both lists, modify the contents of the edit box and click on the rescan button.

Figure 1: This is the Main Form.

To implement the underlying code for this project, we'll need to use a variety of NetWare Directory Services calls. If you have ever done this sort of thing before, you know that we've bitten off a pretty good piece of work. Using the easiest possible way with the client SDK, we can obtain the list of objects using NWDSList. First we'll need to create a context handle and allocate a buffer to receive the results, then we'll initialize an iteration handle and start calling NWDSList.

For each buffer full of data we'll need to call NWDSGetObjectCount to find out how many objects are in the buffer and call NWDSGetObjectName for each of those to get the names. A similar procedure applies to filling the containers list, except that this procedure begins with a call to NWDSListContainers instead of NWDSList. After all this is done, we'll have to remember to free our buffer, and perhaps our context handle.

An alternative to all this, which (as we'll see later) is also more flexible, is to use the TDSContainer class provided as part of Apiary's class library. It is easy to create an instance of this class and make it refer to a particular location within the directory.

The resulting object contains a property which is a virtual array of TDSObject objects, each of which can provide information about the object in the directory it represents. A similar property contains a virtual array of TDSContainer objects, each representing a container object in the location being examined. Container objects actually end up appearing twice, once in the list of containers, and once in the list of objects.

By now this may sound more complicated than using the SDK approach. The truth is in the coding, however, and here's what it all translates to. The code below shows the implementation for the rescan button on the main form. You enter this code after double-clicking on the rescan button from within the development environment.

CurCtxt.Free;

CurCtxt := TDSContainer.Create(CurContext.Text);

with ContainerList, CurCtxt do begin

   Clear;

   Max := ContainerCount-1;

   for i := 0 to Max do

      Items.AddObject(Containers[i].Name, Containers[i]);

   end;

with ObjectList, CurCtxt do begin

   Clear;

   Max := ObjectCount-1;

   for i := 0 to Max do

      Items.AddObject(Objects[i].Name, Objects[i]);

   end;

Before we could enterthis code, we had to add the unit ACNDS to the uses list in the interface section of the main form's associated code file. We also declared a public reference to a TDSContainer object called CurCtxt as a member of the class the environment created to represent the main form. Finally, we declared some local variables and started coding.

In essence, all the rescan button code does is to free any old TDSContainer object, create a new one based on the context specified in the edit control "CurContext", and scan the Containers and Objects lists furnished by the resulting object. The TDSContainer class provides the properties ContainerCount and ObjectCount, and allow us to determine how many containers and objects there are to be scanned. Under Delphi, list boxes have a single property called Items that contains methods for adding items to the list. The most commonly used method is the Add method which adds a string to the list box. In this example, however, we're using AddObject. This allows both a string and an object reference to be added to the list. This will simplify things later.

Although it may seem as though we've barely begun, this form in the application is now almost finished. All that's left is some code to put into the edit box the name of any container clicked on in the containers list, and to pop up another form allowing specific objects to be examined when an item is double-clicked on in the objects list. Since these are both fairly basic Delphi programming issues and complete source is listed at the end of this article, we'll skip that for now and examine object attributes.

The Object Inspector Form

At this point, our application allows us to browse the directory tree, but not to see or examine object attributes. Rather than cluttering up the existing form with a large number of controls, let's create a new form. We'll use his form to browse a particular directory object's attributes, and to examine the values of those attributes. The form should also display the name of the object selected so that the user doesn't have to refer back to the main form to know what he is looking at. Figure 2 shows what this form looks like.

Figure 2: The Object Inspector Form.

All we have to add now is some code to display the distinguished name and names of all the attributes of the object selected on the main form. The application should do this whenever this form is displayed. We also need to add code that displays an attribute's values when the user selects an item from the list of attribute names.

Using the client SDK, we would need to use NWDSRead to read the object's attributes. We could read the attribute values at the same time if we wanted, but we would need to store all that data somewhere and display only what was requested. The Client API for C documentation lists a sixteen step process for performing this call. The Developer's Suite provides all the same functions, so this same process could be followed in our application, however, we'll use the class library since it simplifies things greatly. The code for displaying the selected object's distinguished name and attributes is shown below.

with Form1.ObjectList do

   DSObj := TDSObject(Items.Objects[ItemIndex]);

with DSObj do begin

   ObjName.Caption := DN;

   AttrList.Clear;

   Max := AttributeCount-1;

   for i := 0 to Max do

      AttrList.Items.AddObject(Attributes[i].Name, Attributes[i]);

   end;

AttrValues.Clear;

You may recall that I mentioned earlier that adding both strings and object references to the list of objects on the main form would simplify things later. Well, this is where things get simplified. The second line of the previous code fragment shows how to obtain a reference to the object the user selected. Rather than creating a new one, we just picked the existing one out of the list. Because the TDSContainer instance that created the object owns it, we never need to free it. We assign the variable DSObj to a local reference to the object and use it within this routine.

The remaining code in the previous code fragment should look familiar to you. The code for loading a list box with the names of an object's attributes is exactly the same as the code for adding object names or container names to a list box. Only the names of the objects involved have changed. This is a consequence of the fact that the class library tries to present as much data as possible in the form of virtual arrays of objects. You'll see this construct not only in the Directory Services classes, but also in those that deal with drive mapping, queue job management, bindery services, and just about all the other classes the Developer's Suite provides.

With this method of presenting data in mind, you may already have guessed what the code for displaying the an attribute's values looks like. Selecting an item in the list of attribute names should execute the code shown below.

with AttrList do

    DSAttr := TDSAttribute(Items.Objects[ItemIndex]);

AttrValues.Clear;

Max := DSAttr.ValueCount-1;

for i := 0 to Max do

    AttrValues.Items.Add(DSAttr.Values[i].AsString);

This example uses the AsString property of a attribute value object, which converts whatever value is there into a Delphi string and returns it. You can write attribute values similarly using the class library. Similar data type conversions are provided for related properties such as AsInteger, AsDateTime, AsBoolean, and AsStringList. It is possible for a requested conversion not to be supported by the class library, in which case Delphi generates an exception. It is possible to read and write the raw data through a provided pointer called Value if that should suit your needs more closely.

Error Management

Most of the functions in the NetWare client SDK provide a return code that can be interpreted in order to diagnose error conditions. While we could write a program that did no error checking at all, it would only work under perfect conditions and would generally fail in a disastrous way. All the same function calls and error codes you might expect to be available are available in Apiary's product. The class library portion of the product handles errors differently, however. It uses Delphi's exception handling mechanism.

When a NetWare client SDK function being called by the class library returns an error code, an ENetWare exception is raised. This exception contains the error code returned by NetWare. By catching these exceptions, an application can implement its own error handling logic.

The Apiary class library also generates another kind of exception. There are occasions when a NetWare client SDK function has not returned an error, but the class library can not perform a request. When this happens, the class library raises an ENetWareClasses exception. It doesn't contain an error code, just a short description of the error.

There are several places in the program we've written where we might take advantage of exception handling to produce a more useable program. The final listing contains all the changes that were made in this regard. The code below shows the modifications we made to handle the two most likely error conditions that might occur.

with AttrList do

   DSAttr := TDSAttribute(Items.Objects[ItemIndex]);

with AttrValues do begin

   Clear;

   try

      Max := DSAttr.ValueCount;

      for i := 0 to Max-1 do

         Items.Add(DSAttr.Values[i].AsString);

   except

      on ENetWare do begin

         Items.Add('The selected attribute value');

         Items.Add('could not be read.')

         end;

      on ENetWareClasses do begin

         Items.Add('The selected attribute value');

         Items.Add('could not be displayed.');

         end;

    end;

    end;

The ENetWare error intercepted is most likely to be caused by the client not having sufficient rights to read the attribute's value, so we display an error message indicating this. The default message telling what NetWare client error actually occurred would not normally be meaningful to the application's user. The ENetWareClasses error most likely to be generated is a translation error. The handler for this exception simply indicates that the data could not be displayed as an alternative to the default handling, which indicates that data of a particular syntax could not be translated to a string.

Advanced Features

Our application is done at this point, but let's go back and change it a bit just to explore what sort of capabilities the Apiary class library has. Let's try modifying the application so that it only selects objects that are of class User. We'll do this using the class library first, then go back and do it a slightly different way that takes advantage of the class library but also uses the NetWare client SDK. This mixing of approaches to solving the problem provides a capability not directly supported by the class library, while still reducing the overall complexity of our application.

To select only objects of class User from the directory, we'll add the single line of code below to the handler for the Rescan button on our main form. This code goes right before the line that assigns ObjectCount-1 to Max.

CurCtxt.FilterExpression := 'Object Class = ''User''';

FilterExpression is a property of the TDSContainer class that allows you to provide a filter expression that will be applied to the data. A routine included as part of the Apiary class library parses the expression you pass to it. If you assign a zero length string, it tells the class library not to use a filter expression at all. Limiting our application to only displaying user objects is perhaps not the most useful application of this feature, but it would be quite simple to provide an edit control on the main form for entering an expression.

As powerful as this feature is, the parser provided as part of the class library does have some limitations. There are some operations it simply won't do. Also, all data provided it must be translatable from a string. The example given below shows how to mix class library programming with SDK programming in order to overcome both these limitations. It is a great deal more complex than the single line of code above, but no more so than a typical SDK implementation with the same capability. The code below also shows the use of the FTOK_BASECLS token, which is not currently supported by the class library's built in parser (it may be in the near future, however since it is quite useful).

Ret := NWDSAllocFilter(Filt);

Ctxt := TDSContext.Create;

Ctxt.Name := CurCtxt.Name;

try

   Ret := NWDSAddFilterToken(Filt, FTOK_BASECLS, nil, 0);

   if Ret << 0 then raise ENetWare.Create(Ret);<
   Ret := NWDSAddFilterToken(Filt, FTOK_ANAME, PChar('User'),

             SYN_CI_STRING);

   if Ret << 0 then raise ENetWare.Create(Ret);<
   Ret := NWDSAddFilterToken(Filt, FTOK_END, nil, 0);

   if Ret << 0 then raise ENetWare.Create(Ret);<
   Buf  := TDSBuffer.Create(Ctxt);

   try

      Ret := NWDSPutFilter(Ctxt.Handle, Buf.Handle, Filt, nil);

      if Ret << 0 then raise ENetWare.Create(Ret);<
      CurCtxt.FilterBuffer := Buf;

   except

      Buf.Free;

      NWDSFreeFilter(Filt, nil);

      Ctxt.Free;

      raise;

   end;

except

   NWDSFreeFilter(Filt, nil);

   Ctxt.Free;

   raise;

end;

The property that allows this interface is the FilterBuffer property of TDSContainer. FilterBuffer is an alternative to FilterExpression which allows us to provide a filter expression of our own already packed into a buffer. The class library disposes of this buffer just as though it were its own, so there is no need for us to free it. Both the buffer and the context being used in this example are also encapsulated by classes, and the actual NetWare handles for these resources are accessed through their Handle properties.

Summary

Apiary Developer's Suite for NetWare, Delphi Edition is a full featured set of tools for developing NetWare client applications that run under Windows. It contains a full translation of Novell's NetWare Client SDK into Delphi, but it also goes a step further. With the included class library,you can implement many of your application's features with much less effort. The majority of developers be able to accomplish most things they will want to do with this package using the class library. On those occasions when they need features not supported by the class library, however, the class library works well in combination with conventional SDK development.

Source

Most of the examples given so far are small pieces of code taken out of context for illustrative purposes. The following is the complete source for the project discussed in this article. We've lengthend the code somewhat by adding exception handling. You could leave out almost all of the exception handling if it was acceptable for your application to display NetWare error codes instead of readable error messages.

DNDemo1 (Main Form)

unit DNDemo1;

interface

uses

 SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,

 Forms, Dialogs, StdCtrls, ACNDS;

type

 TForm1 = class(TForm)

   ContainerList: TListBox;

   CurContext: TEdit;

   Label1: TLabel;

   ObjectList: TListBox;

   RescanBtn: TButton;

   procedure RescanBtnClick(Sender: TObject);

   procedure ContainerListClick(Sender: TObject);

   procedure FormShow(Sender: TObject);

   procedure ObjectListDblClick(Sender: TObject);

 private

   { Private declarations }

 public

   { Public declarations }

   CurCtxt: TDSContainer;

 end;

var

 Form1: TForm1;

implementation

{$R *.DFM}

uses ACNDSCon, DNDemo2, NWDSFilt, NWDSType, NWDSDefs, ACNExcpt;

procedure TForm1.RescanBtnClick(Sender: TObject);

var i   : Integer;

   Max : Integer;

   Filt: p_Filter_Cursor_T;

   Buf : TDSBuffer;

   Ret : NWDSCCODE;

   Ctxt: TDSContext;

begin

  Ctxt := nil;

  { Free any old TDSContainer instance and create a new one.}

  CurCtxt.Free;

  try

     CurCtxt := TDSContainer.Create(CurContext.Text);

  except

     { If there is an exception, force CurCtxt to nil to }

     { eliminate the possibility of a GPF on calling Free }

     CurCtxt := nil;

  end;

  { Update the list of containers }

  with ContainerList, CurCtxt do begin

     Clear;

     try

        Max := ContainerCount-1;

     except

        { This is the first place that an invalid container }

        { name being specified in the CurContext edit can   }

        { cause an exception.

                             }

        MessageDlg('The specified container could not be scanned.',

                   mtError, [mbOk], 0);

        Exit;

     end;

     for i := 0 to Max do

         Items.AddObject(Containers[i].Name, Containers[i]);

     end;

  { Update the list of objects }

  with ObjectList, CurCtxt do begin

     Max := 0;

     Clear;

     { Uncomment the following line to limit     }

     { browsed objects to objects of class User. }

     { CurCtxt.FilterExpression := 'Object Class = ''User'''; }

     { Uncomment the following block in order to  }

     { use an alternative method to limit browsed }

     { objects to objects of class User.          }

     (*** BLOCK BEGIN ***.)

     Ret := NWDSAllocFilter(Filt);

     Ctxt := TDSContext.Create;

     Ctxt.Name := CurCtxt.Name;

     try

        Ret := NWDSAddFilterToken(Filt, FTOK_BASECLS, nil, 0);

        if Ret << 0 then raise ENetWare.Create(Ret);<
        Ret := NWDSAddFilterToken(Filt, FTOK_ANAME, PChar('User'),

                  SYN_CI_STRING);

        if Ret << 0 then raise ENetWare.Create(Ret);<
        Ret := NWDSAddFilterToken(Filt, FTOK_END, nil, 0);

        if Ret << 0 then raise ENetWare.Create(Ret);<
        Buf  := TDSBuffer.Create(Ctxt);

        try

           Ret := NWDSPutFilter(Ctxt.Handle, Buf.Handle, Filt, nil);

           if Ret << 0 then raise ENetWare.Create(Ret);<
           CurCtxt.FilterBuffer := Buf;

        except

           Buf.Free;

           NWDSFreeFilter(Filt, nil);

           Ctxt.Free;

           raise;

        end;

     except

        NWDSFreeFilter(Filt, nil);

        Ctxt.Free;

        raise;

     end;

     (***  BLOCK END  ***)

     Max := ObjectCount-1;

     for i := 0 to Max do

         Items.AddObject(Objects[i].Name, Objects[i]);

     end;

  Ctxt.Free;

end;

procedure TForm1.ContainerListClick(Sender: TObject);

begin

  { Place the distinguished name (DN) of the object selected }

  { from the list into the CurContext edit control.          }

  with ContainerList do

     CurContext.Text := TDSContainer(Items.Objects[ItemIndex]).DN;

end;

procedure TForm1.FormShow(Sender: TObject);

var Context: TDSContext;

begin

  { Find the default context and place it in the CurContext  }

  { edit control.  Then update the display.  This causes the }

  { initial display to show the default context and its      }

  { contents.           

                                   }

  Context := TDSContext.Create;

  CurContext.Text := Context.Name;

  Context.Free;

  RescanBtnClick(Sender);

end;

procedure TForm1.ObjectListDblClick(Sender: TObject);

begin

  Form2.ShowModal;

end;

end.

DNDemo2 (Object Inspector)

unit DNDemo2;

interface

uses

 SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,

 Forms, Dialogs, StdCtrls, Buttons;

type

 TForm2 = class(TForm)

   Label1: TLabel;

   ObjName: TLabel;

   AttrList: TListBox;

   AttrValues: TListBox;

   BitBtn1: TBitBtn;

   procedure FormShow(Sender: TObject);

   procedure AttrListClick(Sender: TObject);

 private

   { Private declarations }

 public

   { Public declarations }

 end;

var

 Form2: TForm2;

implementation

{$R *.DFM}

uses DNDemo1, ACNDS, ACNExcpt;

procedure TForm2.FormShow(Sender: TObject);

var DSObj: TDSObject;

   Max  : Integer;

   i    : Integer;

begin

  with Form1.ObjectList do

     DSObj := TDSObject(Items.Objects[ItemIndex]);

  with DSObj do begin

     ObjName.Caption := DN;

     AttrList.Clear;

     Max := AttributeCount;

     for i := 0 to Max-1

do

        AttrList.Items.AddObject(Attributes[i].Name, Attributes[i]);

     end;

  AttrValues.Clear;

end;

procedure TForm2.AttrListClick(Sender: TObject);

var DSAttr: TDSAttribute;

   Max   : Integer;

   i     : Integer;

begin

  with AttrList do

     DSAttr := TDSAttribute(Items.Objects[ItemIndex]);

  with AttrValues do begin

     Clear;

     try

        Max := DSAttr.ValueCount;

        for i := 0 to Max-1 do

           Items.Add(DSAttr.Values[i].AsString);

     except

       on ENetWare do begin

          Items.Add('The selected attribute value');

          Items.Add('could not be read.')

          end;

       on ENetWareClasses do begin

          Items.Add('The selected attribute value');

          Items.Add('could not be displayed.');

          end;

     end;

     end;

end;

end.

* Originally published in Novell AppNotes


Disclaimer

The origin of this information may be internal or external to Novell. While Novell makes all reasonable efforts to verify this information, Novell does not make explicit or implied claims to its validity.

© Copyright Micro Focus or one of its affiliates