techvanguards.com
Last updated on 9/27/2002

Implementing Object Hierarchies
by Binh Ly

Download the source code for this article

An object hierarchy is a set of objects that are grouped together in a parent-child relationship. An excellent example of an object hierarchy is your computer's file system. Your computer's file system consists of several disk drives. Each drive has a root folder (or root directory). Each root folder has several files. A file can also be a folder, which makes it a subfolder. Each subfolder has more files and sub-subfolders. The list can go on and on but what's important here is that an object hierarchy can be formed from a group of objects that exhibit parent-child relationships.

Using our example above, here's how a file system's object hierarchy might look like:

Figure: File system object hierarchy

In the diagram, FileSystem is the root object of the hierarchy since it is the top-most parent object. FileSystem contains a Drives sub-object which is really a collection of Drive objects. An object which is a collection of other objects of the same kind is also called a collection object, i.e. Drives is a collection object. Also notice the relationship between FileSystem and Drives: since FileSystem "contains" Drives, FileSystem acts as a parent object of Drives, and Drives acts as a child of FileSystem, hence the term hierarchy. Continuing further, Drive is a parent of Folder, i.e. a Drive contains a root Folder, and Folder is a parent of the Files collection. The Files collection is rather interesting because it contains both File and Folder objects. In this respect, we treat Folder as a File (a folder really is a special type of file) and therefore, for our purposes, Files is a collection of File objects.

So much for that introduction, let's now move on to the fun stuff: How do we implement such an object hierarchy in our COM server? 

Interface Design

Let's start with our FileSystem object. Since FileSystem contains Drives,  FileSystem's interface should look something like this:

IFileSystem = interface
  property Drives : IDrives;
end;

Drives is a collection of Drive objects. Obviously, we will want the client to be able to navigate Drives to 1) determine how many drives are there and 2) access each Drive object. It shouldn't be that hard to see that Drives must look something like this:

IDrives = interface
  property Count : integer;
  property Item [Index : integer] : IDrive;
end;

Count is used to return the number of Drive objects in Drives. Item [] is used to access each Drive in the list by its position index. We'll be consistently using 0-based indexes all throughout this example, i.e. Drives [0] is the first Drive, Drives [1] is the second Drive, ..., and Drives [Count - 1] is the last Drive. Simple enough?

Each Drive has a root Folder among others. It also has a drive type (floppy, hard drive, network drive, etc.), a name (volume name) and a drive letter. In other words:

IDrive = interface
  property DriveType : integer;
  property Name : string;
  property Letter : string;
  property Root : IFolder;
end;

Remember what we said about Folders: a Folder is really a type of File. A Folder, like a File, has a name, a set of attributes (hidden, readonly, etc.), a file size, and a time stamp. Of course, a Folder or File has more properties than that but we'll just keep them to the bare minimum for our purposes. So let's first define File:

IFile = interface
  property Name : string;
  property Attributes : integer;
  property Size : integer;
  property Time : float;  // time is a floating point for implementation convenience
  property IsFolder : boolean;  //used to determine if this file is really a folder
end;

Since a Folder is really a type of File, we'll make Folder "inherit" from File. In addition, a Folder is different from a File in that a Folder may also contain Files in it:

IFolder = interface (IFile)  // IFolder "inherits" from IFile
  property Files : IFiles;
end;

That leaves us with the Files collection. Like Drives, we also want clients to be able to navigate Files. Therefore, Files must look something like this:

IFiles = interface
  property Count : integer;
  property Item [Index : integer] : IFile;
end;

In lay terms, the file count is accessible using Count, and each File (IFile) is accessible using Item []. As a reminder, Item [] is 0-based, i.e. Item [0] is the first File and Item [Count - 1] is the last File. Also remember, Files is a collection of *both* File's and Folder's. If you go back to IFile, you'll notice that it has an IsFolder property that is used to determine if a File is indeed a Folder.

Whew! Did you get all that? If not, I strongly suggest you read them over again before you go any further. If you did understand all that, let's move on...

Actually, before we proceed, let's first lay down a few constraints or ground rules for our implementation:

  1. For simplicity purposes, we'll create an in-process (DLL) COM server for our file system objects. We also want this server to be usable from a wide range of clients; therefore we'll make sure that each object in the hierarchy supports IDispatch (automation) for our late-bound friends.
     
  2. The client must *only* be allowed to create the FileSystem object. In other words, the client cannot explicitly create Drives, or Drive, or Files, etc. If the client wants to access the other objects, it must navigate the hierarchy starting from the FileSystem object downwards. Also, once the client releases its reference to FileSystem, we'll assume that the client is done with the server.

Now that we've taken care of that, shall we? 

Delphi Implementation
C++ Builder Implementation  


Delphi Implementation

First things first. In Delphi, we create an in-process COM server project using the wizard accessible from the File | New menu and by picking ActiveX Library on the ActiveX tab in the dialog. Let's name this project "FileSystemServer" - Delphi will produce a file named FileSystemServer.dll as our server.

Now that we have our server, the next step is to create the FileSystem object. Since we want FileSystem (and all other objects) to support automation and we also want it to be externally creatable by clients, we want to implement it as an automation object with a class factory. Again, Delphi's wizard can help a bit here. Select File | New and pick Automation Object on the ActiveX tab in the dialog, name the object "FileSystem", and save the new unit as FileSystem.pas. Delphi will produce a TFileSystem class like this:

type
  TFileSystem = class(TAutoObject, IFileSystem)
    ...
  end;

implementation
...
initialization
  TAutoObjectFactory.Create (ComServer, TFileSystem, Class_FileSystem,
    ciMultiInstance, tmApartment);
end.

TFileSystem is the class that implements our FileSystem object. The TAutoObjectFactory.Create line at the bottom of the unit exposes TFileSystem's class factory thus making TFileSystem an externally creatable class, i.e. clients can create the FileSystem object. By the way, COM calls an "externally creatable object" a coclass - in case you've heard of the term but didn't really know what it means.

The more interesting part is that TFileSystem derives from TAutoObject and implements the IFileSystem interface which we've previously designed. TAutoObject gives us automation (IDispatch) support and works in tandem with TAutoObjectFactory. On the same note, you'll notice that the wizard automatically creates IFileSystem in the type library whose parent interface is, not surprisingly, IDispatch. So far, so good!

Our design says IFileSystem supports a Drives property of type IDrives. 

IFileSystem = interface
  property Drives : IDrives;
end;

To do that, we first need to define the IDrives interface. In the type library editor (TLE), let's create a new interface and name it IDrives. Then we go back to IFileSystem and add the Drives property (readonly) of type IDrives.

Figure: Defining IFileSystem

In order to implement IFileSystem.Drives, we first need to implement our Drives object. Again, recall that we want Drives to support automation (IDispatch) but this time, we do not want Drives to be externally creatable by clients, i.e. the client cannot create Drives - the client can only access Drives through IFileSystem.Drives. In other words, we want to implement an object that supports IDispatch but does not expose a class factory. We're in luck because Delphi has just the class we need - TAutoIntfObject. Note that unlike TFileSystem, we do not use TAutoObject because TAutoObject works with a class factory and we specifically do not want Drives to be externally creatable - ergo, no class factory. 

Strictly speaking, TAutoObjectFactory is capable of not exposing a class externally. You can do this by passing the ciInternal instancing flag into its constructor's fourth parameter. However, we simply don't need the overhead of TAutoObject when the lightweight TAutoIntfObject will do. The only case that I can think of where you need to use TAutoObject over TAutoIntfObject is if you want your object to lock the entire server; i.e. the server cannot unload while an external client still has a reference to the object. In our case, we'll assume that the lifetime of any of the sub-objects is a "proper subset" of the lifetime of FileSystem. In other words, once the client releases FileSystem, it is assumed that the client is done with our server, and it no longer has references to any of FileSystem's sub-objects. If you find yourself designing a hierarchy where this is not the case, then use TAutoObject (and ciInternal) for your sub-objects.

TAutoIntfObject is a pretty interesting class in that it provides IDispatch support in tandem with a type library - so we don't really need to mess with implementing IDispatch at all! Strictly speaking though, it is COM that actually provides the facility of a convenient IDispatch implementation based on type information in a type library - TAutoIntfObject simply wraps that facility for us. As long as we correctly "bind" TAutoIntfObject to our desired interface in the type library, we can let it go and do some kick-ass IDispatch handling. To see what I mean, let's look at how we would implement Drives based on TAutoIntfObject:

type
  //drives collection
  TDrives = class (TAutoIntfObject, IDrives)
  public
    constructor Create;
  end;

constructor TDrives.Create;
begin
  //initialize and bind to IDrives interface defn in tlb
  inherited Create (ComServer.TypeLib, IDrives);
end;

The constructor binding rule for TAutoIntfObject is simple: you pass the type library that defines the interface you want to bind as the first parameter, and you pass the IID of the interface you want to bind to as the second parameter. In TDrives.Create, we use ComServer.TypeLib, which references our server's type library, and we use IDrives, which means we want the automation to be implemented thru the binding to the IDrives interface. 

Since Drives, or TDrives is a collection of Drive objects, we need a list to store individual Drive's. More specifically, we need a list to store individual IDrive interfaces. Delphi's TInterfaceList provides such facility. Therefore:

type
  //drives collection
  TDrives = class (TAutoIntfObject, IDrives)
  protected
    FDrives : TInterfaceList;
  public
    constructor Create;
    destructor Destroy; override;
  end;

constructor TDrives.Create;
begin
  //initialize and bind to IDrives interface defn in tlb
  inherited Create (ComServer.TypeLib, IDrives);
  //initialize internal drives collection
  FDrives := TInterfaceList.Create;
end;

destructor TDrives.Destroy;
begin
  //release internal drives collection
  FDrives.Free;
  inherited;
end;

Now that we have FDrives set up, we can then populate it with the actual Drive objects:

procedure TDrives.InitializeDrives;
var
  LogicalDrives : dword;
  DriveNo : integer;
  DriveExists : boolean;
  DriveLetter : char;
  Drive : IDrive;
begin
  //loads system drives into drives collection
  LogicalDrives := GetLogicalDrives;
  for DriveNo := 0 to 25 do
  begin
    //inspect bitmask to see which drives exist
    DriveExists := (((1 shl DriveNo) and LogicalDrives) <> 0);
    if (DriveExists) then
    begin
      //if a drive exists, add to collection
      DriveLetter := char (DriveNo + Ord ('A'));
      Drive := TDrive.Create (DriveLetter);
      FDrives.Add (Drive);
    end;
  end;
end;

What we're doing here is we're using a Win32 API function (GetLogicalDrives) to determine which drives exist and for each drive, we create a TDrive (Drive) instance and we add it to the FDrives list. We'll talk about TDrive later but for now, assume that TDrive is our Drive object. With InitializeDrives in place, we can simply call it from TDrive's constructor:

constructor TDrives.Create;
begin
  //initialize and bind to IDrives interface defn in tlb
  inherited Create (ComServer.TypeLib, IDrives);
  //initialize internal drives collection
  FDrives := TInterfaceList.Create;
  InitializeDrives;
end;

One last thing before we leave TDrives: recall that we want TDrives to implement this interface:

IDrives = interface
  property Count : integer;
  property Item [Index : integer] : IDrive;
end;

To do that, we use the TLE to add the properties to IDrives - but before we do that, we need to add the IDrive interface first so that we can refer to it from the Item [] property. So in the TLE, we create the IDrive interface, and then add the Count and Item [] properties to IDrives. 

Figure: Defining IDrives

All that's left is to implement Count and Item [] as follows:

function TDrives.Get_Count: Integer;
begin
  //return drive count
  Result := FDrives.Count;
end;

function TDrives.Get_Item(Index: Integer): IDrive;
begin
  //extract item from drives collection and return as IDrive
  Result := FDrives.Items [Index] as IDrive;
end;

One thing worth noting: we wanted our Item [] property to be 0-based. Since FDrives (TInterfaceList) is also 0-based, our indexing implementation here just coincides beautifully!

With TDrives in place, we can now go back and implement IFileSystem.Drives: 

type
  TFileSystem = class(TAutoObject, IFileSystem)
  protected
    function Get_Drives: IDrives; safecall;
  protected
    FDrives : IDrives;
  end;

function TFileSystem.Get_Drives: IDrives;
begin
  //initialize drives if not initialized yet
  if (FDrives = NIL) then FDrives := TDrives.Create;
  Result := FDrives;
end;

The next step is to implement the Drive object TDrive. Remember, we want TDrive to expose this interface:

IDrive = interface
  property DriveType : integer;
  property Name : string;
  property Letter : string;
  property Root : IFolder;
end;

To do that, we again use the TLE. Since, the the Root property refers to IFolder, we first create the IFolder interface. Then we add the properties to IDrive. Note that Name and Letter are of type WideString, which is the automation compatible string data type.

Figure: Defining IDrive

Let's now implement TDrive. Along the same lines as TDrives, we also want TDrive to be a TAutoIntfObject. Thus:

type
  //a single drive
  TDrive = class (TAutoIntfObject, IDrive)
  public
    constructor Create (Letter : char);
  end;

constructor TDrive.Create(Letter: char);
begin
  //initialize and bind to IDrive interface defn in tlb
  inherited Create (ComServer.TypeLib, IDrive);
end;

We then implement the IDrive methods:

type
  //a single drive
  TDrive = class (TAutoIntfObject, IDrive)
  protected
    { IDrive }
    function Get_DriveType: Integer; safecall;
    function Get_Name: WideString; safecall;
    function Get_Letter: WideString; safecall;
    function Get_Root: IFolder; safecall;
  protected
    FDriveType : integer;
    FName : string;
    FLetter : char;
    FRoot : IFolder;
  end;

function TDrive.Get_DriveType: Integer;
begin
  Result := FDriveType;
end;

function TDrive.Get_Letter: WideString;
begin
  Result := FLetter;
end;

function TDrive.Get_Name: WideString;
begin
  Result := FName;
end;

function TDrive.Get_Root: IFolder;
begin
  //if root is not initialized, initialize it
  if (FRoot = NIL) then FRoot := TFolder.Create (FLetter + ':\');
  Result := FRoot;
end;

There's nothing really complicated here except possibly for TDrive.Get_Root. In there, we simply create a TFolder instance that points to the root folder (FLetter + ':\') such as c:\. We'll look closer at TFolder later on.

Finally, we initialize our drive's properties as follows:

//determine drive volume name 
function GetVolumeName (Letter : char) : string;
begin
  //implementation omitted for brevity
end;

procedure TDrive.InitializeDrive(Letter: char);
begin
  //initializes drive params
  FLetter := Letter;
  //get drive type
  FDriveType := GetDriveType (pchar (Letter + ':\'));
  //get drive name
  case FDriveType of
    dtFloppy:
      FName := 'Floppy';
    dtFixed, dtCDROM, dtRAMDisk:
      FName := GetVolumeName (Letter);
    dtRemote:
      FName := 'Network Drive';
  end;
end;

constructor TDrive.Create(Letter: char);
begin
  //initialize and bind to IDrive interface defn in tlb
  inherited Create (ComServer.TypeLib, IDrive);
  //initialize drive properties
  InitializeDrive (Letter);
end;

That should do it for TDrive. It's now time to do TFolder.

Recall that a Folder shares the same characteristics of a File. More specifically, a Folder is a File plus more, i.e. a Folder can contain files underneath it while a File cannot. Implementation-wise, what we're saying here is that Folder inherits from File. Therefore, we want to look at File first.

Again, we use the TLE to create IFile. Then we add properties to IFile according to our design:

IFile = interface
  property Name : string;
  property Attributes : integer;
  property Size : integer;
  property Time : float;  // time is a floating point for implementation convenience
  property IsFolder : boolean;  //used to determine if this file is really a folder
end;

Figure: Defining IFile

Note that in the type library, we use WideString for String, Double for Float, and WordBool for boolean. That's just the automation way of doing things.

After that, we then implement IFile using a TFile class:

type
  TFile = class (TAutoIntfObject, IFile)
  protected
    { IFile }
    function Get_Name: WideString; safecall;
    function Get_Attributes: Integer; safecall;
    function Get_IsFolder: WordBool; safecall;
    function Get_Size: Integer; safecall;
    function Get_Time: Double; safecall;
  protected
    FName : string;
    FAttributes : integer;
    FSize : integer;
    FTime : double;
    procedure SetFields (const Name : string;
      Attributes, Size : integer; Time : double);
  public
    constructor Create (const Name : string;
      Attributes, Size : integer; Time : double); overload;
  end;

constructor TFile.Create(const Name: string;
  Attributes, Size: integer; Time: double);
begin
  //initialize and bind to IFile interface defn in tlb
  inherited Create (ComServer.TypeLib, IFile);
  //initialize fields
  SetFields (Name, Attributes, Size, Time);
end;

procedure TFile.SetFields(const Name: string;
  Attributes, Size: integer; Time: double);
begin
  FName := Name;
  FAttributes := Attributes;
  FSize := Size;
  FTime := Time;
end;

I'm not going to show you how the IFile properties are implemented because they're no-brainers. You can simply look at the final source code if you wish. The only thing that is of interest is the overload keyword in the constructor. We'll see how that is significant when we next implement TFolder.

TFolder is really a TFile with an additional Files property:

IFolder = interface (IFile)  // IFolder "inherits" from IFile
  property Files : IFiles;
end;

So now, we go back to our TLE and change IFolder's parent interface to IFile. Then we want to add the Files property to IFolder but before we do that, we first need to create the IFiles interface. Using the the TLE, we create IFiles and then we add property Files of type IFiles to IFolder.

Figure: Defining IFolder

Then we implement TFolder:

type
  //a single folder
  TFolder = class (TFile, IFolder)
  protected
    { IFolder }
    function Get_Files: IFiles; safecall;
  protected
    FFiles : IFiles;
  public
    constructor Create (const Name : string;
      Attributes, Size : integer; Time : double); overload;
end;

Notice that TFolder inherits from TFile. More importantly, we see the overload keyword again. Recall that TAutoIntfObject's constructor binds to a specific interface in a type library. We want TFile to bind to IFile and TFolder to bind to IFolder. But since TFolder inherits from TFile, we cannot simply say:

constructor TFolder.Create(const Name: string;
  Attributes, Size: integer; Time: double);
begin
  inherited Create (Name, Attributes, Size, Time);  //calls TFile.Create 
end;

If we do that, TFolder.Create will call TFile.Create which will, in turn, call TAutoIntfObject.Create (ComServer.TypeLib, IFile). But we want TFolder to bind to IFolder, not IFile. What we really want is:

constructor TFolder.Create(const Name: string;
  Attributes, Size: integer; Time: double);
begin
  //initialize and bind to IFolder interface defn in tlb
  inherited Create (ComServer.TypeLib, IFolder);
  //initialize fields
  SetFields (Name, Attributes, Size, Time);
end;

Without the overload keyword, Delphi won't let you compile TFolder's call to inherited Create (ComServer.TypeLib, IFolder) because that doesn't match TFile's constructor. With the overload keyword, we're telling Delphi that we're introducing overloaded methods (methods of the same name but with different parameter signatures) in such a way that if we make a call to an overloaded method, Delphi will match our call to the specific method that matches our parameter list. In other words, inherited Create (ComServer.TypeLib, IFolder) matches TAutoIntfObject.Create and therefore, calls TAutoIntfObject.Create instead of TFile.Create. Pretty cool huh?!

The only class we have left is TFiles. Let's look at its design:

IFiles = interface
  property Count : integer;
  property Item [Index : integer] : IFile;
end;

Like TDrives, this should be a no-brainer. 

type
  //files collection
  TFiles = class (TAutoIntfObject, IFiles)
  protected
    { IFiles }
    function Get_Count: Integer; safecall;
    function Get_Item(Index: Integer): IFile; safecall;
  protected
    FFiles : TInterfaceList;
    procedure InitializeFiles (const Path : string);
  public
    constructor Create (const Path : string);
    destructor Destroy; override;
  end;

constructor TFiles.Create(const Path: string);
begin
  //initialize and bind to IFiles interface defn in tlb
  inherited Create (ComServer.TypeLib, IFiles);
  //initialize fields
  FFiles := TInterfaceList.Create;
  InitializeFiles (Path);
end;

destructor TFiles.Destroy;
begin
  FFiles.Free;
  inherited;
end;

procedure TFiles.InitializeFiles(const Path: string);
var
  NewFile : IFile;
  MoreFiles : boolean;
begin
  //shown in pseudocode for brevity
  MoreFiles := FindFirst (Path + '\*.*');
  while (MoreFiles) do
  begin
    //determine whether file or folder
    if (FileIsFolder) then
      //create as new folder
      NewFile := TFolder.Create (FileName, FileAttr, FileSize, FileTime)
    else
      //create as new file
      NewFile := TFile.Create (FileName, FileAttr, FileSize, FileTime);

    //add new file to list
    FFiles.Add (NewFile);

    //proceed next
    MoreFiles := FindNext (Path + '\*.*');
  end;
end;

function TFiles.Get_Count: Integer;
begin
  Result := FFiles.Count;
end;

function TFiles.Get_Item(Index: Integer): IFile;
begin
  Result := FFiles.Items [Index] as IFile;
end;

The only thing that looks complicated is InitializeFiles, but that's just logic for scanning for files in a path on your drive. Note though, that in the scanning process, if a folder is found, we create a TFolder, and if a file is found, we create a TFile. Either way, we still add the new file (or folder) to the FFiles list. Also note that the Name we're storing in TFile (TFolder) is not the full name/path, only the short name - this is important because we don't want to waste a lot of resources by storing full paths to each file and folder in our list. 

With TFiles in place, now would be a good time to finally implement IFolder.Files.

function TFolder.Get_Files: IFiles;
begin
  //initialize files if not initialized yet
  if (FFiles = NIL) then FFiles := TFiles.Create (Self, FullPath);
  Result := FFiles;
end;

Since a TFolder (or TFile) only stores the folder's short name (not the full path), what should FullPath return? Good question! What we really need here is the ability for TFolder to have a link to its parent folder, and its parent have a link to its parent's parent, and so on, all the way up to the drive's root folder. This way, we can always determine the full path to a folder by recursively walking the chain of parent folders all the way to the root folder. 

If we add a ParentFolder property to TFolder (or TFile) which points to a Folder's (or File's) parent TFolder, we can easily determine a folder's (or file's) full path using this algorithm:

function TFile.FullPath : string;
var
  Parent : IFolder;
  ParentName : string;
begin
  //walk parent path to build full path
  Result := FName;
  Parent := ParentFolder;
  while (Parent <> NIL) do
  begin
    //get immediate parent folder name
    ParentName := Parent.Name;
    //if no trailing backslash in it, append it
    if (ParentName [Length (ParentName)] <> '\') then ParentName := ParentName + '\';
    //prepend parent folder name to path
    //this is our full path so far
    Result := ParentName + Result;
    //move up to next level
    Parent := Parent.ParentFolder;
  end;
end;

I have highlighted the most important parts of the process. In simple terms, what we're doing here is we're walking up the chain of ParentFolder's and each time we find one, we prepend its name to our path, eventually resulting in a path that reflects an aggregation of all the folder names (separated by backslashes) from the root folder down to to our file (or folder).

Since the FullPath algorithm is useful in this case, let's add the ParentFolder property to IFile.

IFile = interface
  property Name : string;
  property Attributes : integer;
  property Size : integer;
  property Time : float;
  property IsFolder : boolean;
  property ParentFolder : IFolder;
end;

You already know the drill: we have to go to the TLE again and add property ParentFolder to IFile. Then we implement it:

type
  TFile = class (TAutoIntfObject, IFile)
  protected
    ...
    function Get_ParentFolder: IFolder; safecall;
  protected
    FParentFolder : IFolder;
    ...
  end;

function TFile.Get_ParentFolder: IFolder;
begin
  Result := FParentFolder;
end;

But wait a minute. Wouldn't this cause a circular reference problem? A Folder object refers to a Files object, Files refer to individual File objects, and each File refers back to Folder - a circular reference. To understand what the circular reference problem is, consider this simple scenario:

A server contains 2 objects, A and B. A is a root object and B is a sub-object of A, i.e. A holds a reference to B. In COM, an object gets destroyed only if there are no more client references to it. Now, if B has a reference back to A resulting in a circular reference, then as long as B's reference to A is active, A will never get destroyed. Now consider an external client who creates A, then asks A to create B, and then releases A. Will A get destroyed? No, because B still refers to it. So we need to destroy B so that it can release its reference to A. But we can't destroy B because A still has a reference to it. So we can't destroy A to destroy B nor can we destroy B to destroy A, hence the term "circular reference". Under normal circumstances, all objects involved in a circular reference chain will never get destroyed.

One way to avoid the circular reference problem is to insert a weak reference into the chain. A weak reference is a reference that doesn't AddRef the object that's being referred to. In other words, if B has a weak reference to A, then A isn't really aware that B refers to it so therefore, once the last client releases A (clients of A exclude B), A will self destruct without regard for B's weak reference to it.

In Delphi, a weak reference can be implemented by using an untyped pointer instead of an interface type:

type
  TFile = class (TAutoIntfObject, IFile)
  protected
    ...
    function Get_ParentFolder: IFolder; safecall;
  protected
    FParentFolder : pointer;
    ...
  end;

function TFile.Get_ParentFolder: IFolder;
begin
  Result := IFolder (FParentFolder);
end;

Note that we have to do a hard-cast here to return the IFolder-ness of our untyped FParentFolder pointer. 

One final detail eludes us: how do we pass the ParentFolder pointer into TFile? Simplest way is through its constructor:

constructor TFile.Create(ParentFolder : IFolder; const Name: string;
  Attributes, Size: integer; Time: double);
begin
  //initialize and bind to IFile interface defn in tlb
  inherited Create (ComServer.TypeLib, IFile);
  //initialize fields
  SetFields (ParentFolder, Name, Attributes, Size, Time);
end;

procedure TFile.SetFields(ParentFolder: IFolder; const Name: string;
  Attributes, Size: integer; Time: double);
begin
  FName := Name;
  FAttributes := Attributes;
  FSize := Size;
  FTime := Time;
  //pointer ref results in a weak ref to parent, i.e. no AddRef
  FParentFolder := pointer (ParentFolder);
end;

But who passes ParentFolder into TFile's constructor? The TFiles container, of course:

procedure TFiles.InitializeFiles(ParentFolder : IFolder; const Path: string);
var
  NewFile : IFile;
  MoreFiles : boolean;
begin
  //shown in pseudocode for brevity
  MoreFiles := FindFirst (Path + '\*.*');
  while (MoreFiles) do
  begin
    //determine whether file or folder
    if (FileIsFolder) then
      //create as new folder
      NewFile := TFolder.Create (ParentFolder, FileName, FileAttr, FileSize, FileTime)
    else
      //create as new file
      NewFile := TFile.Create (ParentFolder, FileName, FileAttr, FileSize, FileTime);

    //add new file to list
    FFiles.Add (NewFile);

    //proceed next
    MoreFiles := FindNext (Path + '\*.*');
  end;
end;

constructor TFiles.Create(ParentFolder: IFolder; const Path: string);
begin
  //initialize and bind to IFiles interface defn in tlb
  inherited Create (ComServer.TypeLib, IFiles);
  //initialize fields
  FFiles := TInterfaceList.Create;
  InitializeFiles (ParentFolder, Path);
end;

And who passes ParentFolder into TFiles' constructor? The TFolder parent, of course:

function TFolder.Get_Files: IFiles;
begin
  //initialize files if not initialized yet
  if (FFiles = NIL) then FFiles := TFiles.Create (Self, FullPath);
  Result := FFiles;
end;

Got it?!

Navigating FileSystem

With our FileSystem server in place, let's now take a look at how a client would navigate the FileSystem hierarchy. Before we proceed, make sure you build the server project and then register it (using the Run | Register ActiveX Server menu item).

Let's start with a simple one: display drive information from the Drives and Drive objects.

uses
  FileSystemServer_TLB;

procedure TForm1.DisplayDrives(Sender: TObject);
var
  FileSystem : IFileSystem;
  Drives : IDrives;
  Drive : IDrive;
  i : integer;
begin
  //create FileSystem
  FileSystem := CoFileSystem.Create;
  //access Drives sub-object
  Drives := FileSystem.Drives;
  //display drive count
  Memo1.Lines.Add ('There are ' + IntToStr (Drives.Count) + ' drives on your computer.');

  //display drive information
  Memo1.Lines.Add ('They are:');
  for i := 0 to Drives.Count - 1 do
  begin
    Drive := Drives.Item [i];
    //display single drive information
    Memo1.Lines.Add (' ' + Drive.Name + ' (' + Drive.Letter + ':)');
  end;
end;

We first create the FileSystem object. Then we access the Drives sub-object and display the drive count. Then we iterate the Drives object and for each Drive, we display the volume name and drive letter in a nice manner. Here's a sample output from my machine:

Figure: Sample output from Drives and Drive objects

Going deeper in the hierarchy:

procedure TForm1.DisplayDrives(Sender: TObject);
var
  FileSystem : IFileSystem;
  Drives : IDrives;
  Drive : IDrive;
  i : integer;
  Root, SubFolder : IFolder;
  Files : IFiles;
  AFile : IFile;
  FileOrFolder : string;
begin
  //create FileSystem
  FileSystem := CoFileSystem.Create;
  //access Drives sub-object
  Drives := FileSystem.Drives;

  //display folder information on C:
  Root := Drives.Item [1].Root; // assumes 0=A, 1=C, 2=D, etc.
  Memo1.Lines.Add ('The root folder on drive ' + Drives.Item [1].Letter + ':' +
    ' is named ' + Root.Name);
  //display file information on root
  Files := Root.Files;
  Memo1.Lines.Add ('It contains ' + IntToStr (Files.Count) + ' files and folders');
  Memo1.Lines.Add ('They are:');
  for i := 0 to Files.Count - 1 do
  begin
    AFile := Files.Item [i];
    //display single file/folder information
    if (AFile.IsFolder) then FileOrFolder := '[Folder]' else FileOrFolder := '[File]';
    Memo1.Lines.Add (' ' + AFile.Name + ' ' + FileOrFolder);
    //if folder, display how many files and folders are beneath it
    if (AFile.IsFolder) then
    begin
      SubFolder := AFile as IFolder;
      Memo1.Lines.Add (' ' + 'contains ' + IntToStr (SubFolder.Files.Count) +
        ' files and folders');
    end;
  end;
end;

Here, we focus on drive C: (Drives.Item [1] to be exact). We get the root folder on C: and display it. Then we show how many files and folders are in it. Then we go through each file and folder and list them one-by-one. If the item is a file, we display its name, and if it's a folder, we display its name and also a count of files and folders that are beneath it. That's all there is to it and here's a sample output from my machine:

Figure: Sample output from Drive, Folder, Files, and File objects

Since you might think that our client application is rather lame and useless, I've created a "neat" FileSystem Explorer client ala Windows Explorer. Here's a screen shot from FileSystem Explorer:

Figure: FileSystem Explorer application

FileSystem Explorer is available from the FileSystemDelphi.zip download.

Adding the Finishing Touches

If you use Visual Basic (VB), you might be familiar with a couple of VBisms such as the For Each feature and the array default property feature. For Each is a standard mechanism to cycle through a collection object like the following sample code shows:

Private Sub DisplayDrives_Click()
  'declare variables
  Dim FileSystem As FileSystemServer.FileSystem
  Dim Drives As FileSystemServer.IDrives
  Dim Drive As FileSystemServer.IDrive

  'create FileSystem
  Set FileSystem = New FileSystemServer.FileSystem
  'access Drives sub-object
  Set Drives = FileSystem.Drives
  'display drive count
  List1.AddItem "There are " & Drives.Count & " drives on your computer."

  'display drive information
  List1.AddItem "They are:"
  For Each Drive In Drives
    List1.AddItem " " & Drive.Name & " (" & Drive.Letter & ":)"
  Next
End Sub

To support this feature, an object collection needs to implement the COM IEnumVariant interface. That's what For Each expects. IEnumVariant looks like this:

//details omitted for brevity
IEnumVariant = interface (IUnknown)
  function Next: HResult; stdcall;
  function Skip: HResult; stdcall;
  function Reset: HResult; stdcall;
  function Clone: HResult; stdcall;
end;

So what really happens is that VB calls methods of IEnumVariant (such as Next) to implement its For Each feature. It's really not that hard to implement IEnumVariant but if you, like me, don't want to waste time on reinventing the wheel, I have already provided a class that hides IEnumVariant's gory details. In order to plug my TEnumVariantCollection class into your object, you need to implement an interface with 3 simple methods:

IVariantCollection = interface
  //used by enumerator to lock list
  function GetController : IUnknown; stdcall;
  //used by enumerator to determine how many items
  function GetCount : integer; stdcall;
  //used by enumerator to retrieve items
  function GetItems (Index : olevariant) : olevariant; stdcall;
end;

To see this in action, let's try it on our Drives object, shall we?

type
  //drives collection
  TDrives = class (TAutoIntfObject, IDrives, IVariantCollection)
  protected
    { IVariantCollection }
    function GetController : IUnknown; stdcall;
    function GetCount : integer; stdcall;
    function GetItems (Index : olevariant) : olevariant; stdcall;
  ...
  end;

function TDrives.GetController: IUnknown;
begin
  //always return Self here
  Result := Self;
end;

function TDrives.GetCount: integer;
begin
  //always return collection count here
  Result := FDrives.Count;
end;

function TDrives.GetItems(Index: olevariant): olevariant;
begin
  //always return collection item here
  Result := FDrives.Items [Index] as IDispatch;
end;

Is that simple enough for you? But wait, how does IEnumVariant get into the picture? Not to worry, it's quite simple: clients of IEnumVariant expect your collection object to expose a property named _NewEnum whose type is IUnknown and whose dispid is DISPID_NEWENUM or -4. Therefore, we go back to the TLE, add the readonly property _NewEnum to IDrives, set its return type to IUnknown, and set its ID to -4. 

Figure: Defining IEnumVariant for IDrives

Then we implement it:

function TDrives.Get__NewEnum: IUnknown;
begin
  //use my TEnumVariant helper class :)
  Result := TEnumVariantCollection.Create (Self);
end;

That should do it for our IEnumVariant! Also note that I've implemented this for TFiles.

In addition to the For Each feature, VB also has the ability to use default array properties (or more specifically, default properties). What this means is that instead of this:

set Drive = Drives.Item (0)

you can simply do this:

set Drive = Drives (0)

Not surprisingly, it's very easy to support this kind of feature. Automation rules dictate that if you have an interface property whose dispid is DISPID_VALUE or 0, clients will treat that as the default property. In other words, if we go back to the TLE and change the ID values of IDrives.Item [] and IFiles.Item [] both to 0, that should make the Item [] property default for the Drives and the Files objects.

Go to conclusion >>


C++ Builder Implementation

First things first. In CBuilder, we create an in-process COM server project using the wizard accessible from the File | New menu and by picking ActiveX Library on the ActiveX tab in the dialog. Let's name this project "FileSystemServer" - CBuilder will produce a file named FileSystemServer.dll as our server.

Now that we have our server, the next step is to create the FileSystem object. Since we want FileSystem (and all other objects) to support automation and we also want it to be externally creatable by clients, we want to implement it as an automation object with a class factory. Again, CBuilder's wizard can help a bit here. Select File | New and pick Automation Object on the ActiveX tab in the dialog, name the object "FileSystem", and save the new module as FileSystem.cpp. CBuilder will produce a TFileSystemImpl class like this:

class ATL_NO_VTABLE TFileSystemImpl :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<TFileSystemImpl, &CLSID_FileSystem>,
  public IDispatchImpl<IFileSystem, &IID_IFileSystem, &LIBID_FileSystemServer>
{
protected:
  BEGIN_COM_MAP(TFileSystemImpl)
    COM_INTERFACE_ENTRY(IFileSystem)
    COM_INTERFACE_ENTRY(IDispatch)
  END_COM_MAP()
  ...
};

TFileSystemImpl is the class that implements our FileSystem object. The CComCoClass inherited class exposes TFileSystemImpl's class factory thus making TFileSystemImpl an externally creatable class, i.e. clients can create the FileSystem object. By the way, COM calls an "externally creatable object" a coclass - in case you've heard of the term but didn't really know what it means.

The classes CComObjectRoot(Ex), CComCoClass, IDispatchImpl, etc. are all part of a library of classes specifically used for C++ COM development. This class library is called Microsoft's Active Template Library (ATL). If you're not familiar with ATL, I suggest you read Active Template Library - A Developer's Guide by Tom Armstrong, or if you like it more grungy, try ATL Internals by Brent Rector and Chris Sells.

The more interesting part is that TFileSystemImpl implements the IFileSystem interface which we've previously designed. The IDispatchImpl inherited class gives us automation (IDispatch) support using IFileSystem. On the same note, you'll notice that the wizard automatically creates IFileSystem in the type library whose parent interface is, not surprisingly, IDispatch. So far, so good!

Our design says IFileSystem supports a Drives property of type IDrives. 

IFileSystem = interface
  property Drives : IDrives;
end;

To do that, we first need to define the IDrives interface. In the type library editor (TLE), let's create a new interface and name it IDrives. Then we go back to IFileSystem and add the Drives property (readonly) of type IDrives.

Figure: Defining IFileSystem

In order to implement IFileSystem.Drives, we first need to implement our Drives object. Again, recall that we want Drives to support automation (IDispatch) but this time, we do not want Drives to be externally creatable by clients, i.e. the client cannot create Drives - the client can only access Drives through IFileSystem.Drives. In other words, we want to implement an object that supports IDispatch but does not expose a class factory. We're in luck because ATL can also help us here. We simply need a class similar to TFileSystemImpl except for the CComCoClass part. 

To see what I mean, let's look at how we would implement Drives without CComCoClass:

class ATL_NO_VTABLE TDrivesImpl :
  public CComObjectRoot,
  public IDispatchImpl <IDrives, &IID_IDrives, &LIBID_FileSystemServer>
{
protected:
  BEGIN_COM_MAP(TDrivesImpl)
    COM_INTERFACE_ENTRY(IDrives)
    COM_INTERFACE_ENTRY(IDispatch)
  END_COM_MAP()
  ...
};

TDrivesImpl is the class that implements our Drives object. Again, using IDispatchImpl here gives us IDispatch (automation) support using the IDrives interface. More importantly, we've taken out CComCoClass to get rid of the class factory support!

Since Drives, or TDrivesImpl is a collection of Drive objects, we need a list to store individual Drive's. More specifically, we need a list to store individual IDrive interfaces. STL's vector class is perfect for this. Therefore:

#include <vector.h>

class ATL_NO_VTABLE TDrivesImpl :
  public CComObjectRoot,
  public IDispatchImpl <IDrives, &IID_IDrives, &LIBID_FileSystemServer>
{
protected:
  BEGIN_COM_MAP(TDrivesImpl)
    COM_INTERFACE_ENTRY(IDrives)
    COM_INTERFACE_ENTRY(IDispatch)
  END_COM_MAP()
  ...
protected:
  vector <IDrivePtr> mDrives;
};

STL stands for Standard Template Library. The STL is another framework of classes that provide facilities for the common C++ programming needs and algorithms. The STL vector class, for instance, is a templatized version of a growable array. If you're not familiar with STL, there's a lot of references out there - try searching amazon.com for books on STL.

Notice that we've created a vector of IDrivePtr elements. The CBuilder TLE creates IDrivePtr once you add IDrive to the type library and refresh its implementation. IDrivePtr is nothing but a smart interface pointer for IDrive pointers, i.e. it handles AddRef's and Release's for you automatically. By creating a vector of IDrivePtr's, we eliminate the hassle of manually calling AddRef's and Release's for each Drive object in our list.

Now that we have mDrives set up, we can then populate it with the actual Drive objects:

void TDrivesImpl::InitializeDrives ()
{
  //build drive list
  DWORD LogicalDrives = GetLogicalDrives ();
  for (int DriveNo = 0; DriveNo < 26; DriveNo++)
  {
    bool DriveExists = ((1 << DriveNo) & LogicalDrives);
    if (DriveExists)
    {
      //if drive found, initialize it
      char DriveLetter = (char) (DriveNo + 'A');
      CComObject <TDriveImpl> *Drive;
      CComObject <TDriveImpl>::CreateInstance (&Drive);
      Drive->AddRef ();
      Drive->InitializeDrive (DriveLetter);
      IDrivePtr NewDrive = Drive;
      //add to list
      mDrives.push_back (NewDrive);
    }
  }
}

What we're doing here is we're using a Win32 API function (GetLogicalDrives) to determine which drives exist and for each drive, we create a TDriveImpl (Drive) instance and we add it to the mDrives list. We'll talk about TDriveImpl later but for now, assume that TDriveImpl is our Drive object. 

One last thing before we leave TDrivesImpl: recall that we want TDrivesImpl to implement this interface:

IDrives = interface
  property Count : integer;
  property Item [Index : integer] : IDrive;
end;

To do that, we use the TLE to add the properties to IDrives - but before we do that, we need to add the IDrive interface first so that we can refer to it from the Item [] property. So in the TLE, we create the IDrive interface, and then add the Count and Item [] properties to IDrives. 

Figure: Defining IDrives

All that's left is to implement Count and Item [] as follows:

STDMETHODIMP TDrivesImpl::get_Count (long* Value)
{
  *Value = mDrives.size ();
  return S_OK;
}

STDMETHODIMP TDrivesImpl::get_Item (long Index, Filesystemserver_tlb::IDrivePtr* Value)
{
  *Value = mDrives [Index];
  return S_OK;
}

One thing worth noting: we wanted our Item [] property to be 0-based. Since mDrives (vector) is also 0-based, our indexing implementation here just coincides beautifully!

With TDrivesImpl in place, we can now go back and implement IFileSystem.Drives: 

STDMETHODIMP TFileSystemImpl::get_Drives(IDrivesPtr* Value)
{
  //initialize drives if not yet initialized
  if (!mDrives)
  {
    //create TDrivesImpl
    CComObject <TDrivesImpl> *Drives;
    CComObject <TDrivesImpl>::CreateInstance (&Drives);
    Drives->AddRef ();
    //populate drives list
    Drives->InitializeDrives ();
    mDrives = Drives;
  }
  *Value = mDrives;
  return S_OK;
}

The next step is to implement the Drive object TDriveImpl. Remember, we want TDriveImpl to expose this interface:

IDrive = interface
  property DriveType : integer;
  property Name : string;
  property Letter : string;
  property Root : IFolder;
end;

To do that, we again use the TLE. Since, the the Root property refers to IFolder, we first create the IFolder interface. Then we add the properties to IDrive. Note that Name and Letter are of type BSTR, which is the automation compatible string data type.

Figure: Defining IDrive

Let's now implement TDriveImpl. Along the same lines as TDrivesImpl, we also want TDriveImpl to support IDispatch, but not externally-createable, i.e. no CComCoClass. Thus:

class ATL_NO_VTABLE TDriveImpl :
  public CComObjectRoot,
  public IDispatchImpl <IDrive, &IID_IDrive, &LIBID_FileSystemServer>
{
protected:
  BEGIN_COM_MAP(TDriveImpl)
    COM_INTERFACE_ENTRY(IDrive)
    COM_INTERFACE_ENTRY(IDispatch)
  END_COM_MAP()
  ...
};

We then implement the IDrive methods:

class ATL_NO_VTABLE TDriveImpl :
  public CComObjectRoot,
  public IDispatchImpl <IDrive, &IID_IDrive, &LIBID_FileSystemServer>
{
public:
  TDriveImpl () : mDriveType (dtUnknown) {}
protected:
  ...
  //IDrive
  STDMETHOD (get_DriveType) (long* Value);
  STDMETHOD (get_Name) (BSTR* Value);
  STDMETHOD (get_Letter) (BSTR* Value);
  STDMETHOD (get_Root) (Filesystemserver_tlb::IFolderPtr* Value);
protected:
  String mName;
  String mLetter;
  long mDriveType;
  IFolderPtr mRoot;
};

//in Drive.cpp

STDMETHODIMP TDriveImpl::get_DriveType (long* Value)
{
  *Value = mDriveType;
  return S_OK;
}

STDMETHODIMP TDriveImpl::get_Name (BSTR* Value)
{
  *Value = WideString (mName).Detach ();
  return S_OK;
}

STDMETHODIMP TDriveImpl::get_Letter (BSTR* Value)
{
  *Value = WideString (mLetter).Detach ();
  return S_OK;
}

//return root folder for this drive
STDMETHODIMP TDriveImpl::get_Root (Filesystemserver_tlb::IFolderPtr* Value)
{
  //initialize root if not initialized yet
  if (!mRoot)
  {
    CComObject <TFolderImpl> *Folder;
    CComObject <TFolderImpl>::CreateInstance (&Folder);
    Folder->AddRef ();
    //setup root folder
    Folder->InitializeAsRoot (mLetter + ":\\");
    mRoot = Folder;
  }
  *Value = mRoot;
  return S_OK;
}

There's nothing really complicated here except possibly for TDriveImpl::get_Root. In there, we simply create a TFolderImpl instance that points to the root folder (mLetter + ":\\") such as c:\. We'll look closer at TFolderImpl later on.

Finally, we initialize our drive's properties as follows:

//determine drive volume name 
String GetVolumeName (char Letter)
{
  //implementation omitted for brevity
}

void TDriveImpl::InitializeDrive (char DriveLetter)
{
  //initialize drive params
  mLetter = DriveLetter;
  String Root = String (Letter) + ":\\";
  //get drive type
  mDriveType = GetDriveType (Root.c_str ());
  //get drive name
  switch (mDriveType)
  {
    case dtFloppy:
      mName = "Floppy";
      break;
    case dtFixed:
    case dtCDROM:
    case dtRAMDisk:
      mName = GetVolumeName (DriveLetter);
      break;
    case dtRemote:
      mName = "Network Drive";
  }
}

That should do it for TDriveImpl. It's now time to do TFolderImpl.

Recall that a Folder shares the same characteristics of a File. More specifically, a Folder is a File plus more, i.e. a Folder can contain files underneath it while a File cannot. Implementation-wise, what we're saying here is that Folder inherits from File. Therefore, we want to look at File first.

Again, we use the TLE to create IFile. Then we add properties to IFile according to our design:

IFile = interface
  property Name : string;
  property Attributes : integer;
  property Size : integer;
  property Time : float;  // time is a floating point for implementation convenience
  property IsFolder : boolean;  //used to determine if this file is really a folder
end;

Figure: Defining IFile

Note that in the type library, we use BSTR for String, double for Float, and VARIANT_BOOL for boolean. That's just the automation way of doing things.

After that, we then implement IFile using a TFileImpl class:

//base for TFileImpl and TFolderImpl
template <typename DispIntfT, const IID *piid, const GUID *plibid>
class ATL_NO_VTABLE TBaseFileImpl :
  public IDispatchImpl <DispIntfT, piid, plibid>
{
public:
  TBaseFileImpl () : mAttributes (0), mSize (0), mTime (0) {}
  virtual ~TBaseFileImpl () {}

  void Initialize (String Name, int Attributes,
    int Size, double Time)
  {
    //initialize fields
    mName = Name;
    mAttributes = Attributes;
    mSize = Size;
    mTime = Time;
  }
public:
  //IFile
  STDMETHOD (get_Name) (BSTR* Value);
  STDMETHOD (get_Attributes) (long* Value);
  STDMETHOD (get_IsFolder) (TOLEBOOL* Value);
  STDMETHOD (get_ParentFolder) (Filesystemserver_tlb::IFolderPtr* Value);
  STDMETHOD (get_Size) (long* Value);
  STDMETHOD (get_Time) (double* Value);
protected:
  String mName;
  long mAttributes;
  long mSize;
  double mTime;
};

//TFileImpl
class ATL_NO_VTABLE TFileImpl :
  public CComObjectRoot,
  public TBaseFileImpl <IFile, &IID_IFile, &LIBID_FileSystemServer>
{
protected:
  BEGIN_COM_MAP(TFileImpl)
    COM_INTERFACE_ENTRY(IFile)
    COM_INTERFACE_ENTRY(IDispatch)
  END_COM_MAP()
};

If you look closely, I've actually created 2 classes: TBaseFileImpl and TFileImpl. This is because Folder and File share common characteristics (Name, Attribute, Size, etc.). By separating those shared characteristics into the TBaseFileImpl base class, we can later create a TFolderImpl class that inherits (and thus, reuses) TBaseFileImpl.

I'm not going to show you how the IFile properties are implemented because they're no-brainers. You can simply look at the final source code if you wish. 

Next stop is TFolderImpl. TFolderImpl is really a TFileImpl with an additional Files property:

IFolder = interface (IFile)  // IFolder "inherits" from IFile
  property Files : IFiles;
end;

So now, we go back to our TLE and change IFolder's parent interface to IFile. Then we want to add the Files property to IFolder but before we do that, we first need to create the IFiles interface. Using the the TLE, we create IFiles and then we add property Files of type IFiles to IFolder.

Figure: Defining IFolder

Then we implement TFolderImpl:

class ATL_NO_VTABLE TFolderImpl :
  public CComObjectRoot,
  public TBaseFileImpl <IFolder, &IID_IFolder, &LIBID_FileSystemServer>
{
protected:
  BEGIN_COM_MAP (TFolderImpl)
    COM_INTERFACE_ENTRY (IFile) // a folder is also an IFile
    COM_INTERFACE_ENTRY (IFolder)
    COM_INTERFACE_ENTRY (IDispatch)
  END_COM_MAP ()
protected:
  //IFolder
  STDMETHOD (get_Files) (Filesystemserver_tlb::IFilesPtr* Value);
protected:
  IFilesPtr mFiles;
};

As expected, TFolderImpl inherits from TBaseFileImpl. In other words, we don't need to re-implement the IFile methods in TFolderImpl because we can simply reuse our friend, TBaseFileImpl. Pretty cool huh?!

The only class we have left is TFilesImpl. Let's look at its design:

IFiles = interface
  property Count : integer;
  property Item [Index : integer] : IFile;
end;

Like TDrivesImpl, this should be a no-brainer. 

class ATL_NO_VTABLE TFilesImpl :
  public CComObjectRoot,
  public IDispatchImpl <IFiles, &IID_IFiles, &LIBID_FileSystemServer>
{
public:
  void InitializeFiles (IFolder *ParentFolder, String Path);
protected:
  BEGIN_COM_MAP(TFilesImpl)
    COM_INTERFACE_ENTRY(IFiles)
    COM_INTERFACE_ENTRY(IDispatch)
  END_COM_MAP()

  //IFiles
  STDMETHOD (get_Count) (long* Value);
  STDMETHOD (get_Item) (long Index, Filesystemserver_tlb::IFilePtr* Value);
protected:
  vector <IFilePtr> mFiles;
};

//in Files.cpp

STDMETHODIMP TFilesImpl::get_Count (long* Value)
{
  *Value = mFiles.size ();
  return S_OK;
}

STDMETHODIMP TFilesImpl::get_Item (long Index, Filesystemserver_tlb::IFilePtr* Value)
{
  *Value = mFiles [Index];
  return S_OK;
}

//load file list from physical path
void TFilesImpl::InitializeFiles (String Path)
{
  //shown in pseudocode for brevity
  bool MoreFiles = (FindFirst (Path + "\\*.*"));
  while (MoreFiles)
  {
    IFilePtr NewFile;

    //determine whether file or folder
    if (FileIsFolder)
    {
      CComObject <TFolderImpl> *Folder;
      CComObject <TFolderImpl>::CreateInstance (&Folder);
      Folder->AddRef ();
      //initialize folder params
      Folder->Initialize (FileName, FileAttr, FileSize, FileTime);
      //since this is an IFolder, we want to QI for IFile
      //because our filelist stores IFile's
      CComQIPtr <IFile, &IID_IFile> File (Folder);
      NewFile = File;
    }
    else
    {
      //create as file
      CComObject <TFileImpl> *File;
      CComObject <TFileImpl>::CreateInstance (&File);
      File->AddRef ();
      //initialize file params
      File->Initialize (FileName, FileAttr, FileSize, FileTime);
      NewFile = File;
    }

    //add new file to list
    mFiles.push_back (NewFile);

    //proceed next
    MoreFiles = (FindNext (Path + "\\*.*"));
  }
}

The only thing that looks complicated is InitializeFiles, but that's just logic for scanning for files in a path on your drive. Note though, that in the scanning process, if a folder is found, we create a TFolderImpl, and if a file is found, we create a TFileImpl. Either way, we still add the new file (or folder) to the mFiles list. Also note that the Name we're storing in TFileImpl (TFolderImpl) is not the full name/path, only the short name - this is important because we don't want to waste a lot of resources by storing full paths to each file and folder in our list. 

With TFilesImpl in place, now would be a good time to finally implement IFolder.Files.

STDMETHODIMP TFolderImpl::get_Files (Filesystemserver_tlb::IFilesPtr* Value)
{
  //initialize files if not initialized yet
  if (!mFiles)
  {
    CComObject <TFilesImpl> *Files;
    CComObject <TFilesImpl>::CreateInstance (&Files);
    Files->AddRef ();
    //populate files list
    Files->InitializeFiles (this, GetPath ());
    mFiles = Files;
  }
  *Value = mFiles;
  return S_OK;
}

Since a TFolderImpl (or TFileImpl) only stores the folder's short name (not the full path), what should GetPath return? Good question! What we really need here is the ability for TFolderImpl to have a link to its parent folder, and its parent have a link to its parent's parent, and so on, all the way up to the drive's root folder. This way, we can always determine the full path to a folder by recursively walking the chain of parent folders all the way to the root folder. 

If we add a ParentFolder property to TFolderImpl (or TFileImpl) which points to a Folder's (or File's) parent TFolderImpl, we can easily determine a folder's (or file's) full path using this algorithm:

//get path implementation:
//walk the ParentFolder chain all the way up to the root
String TFileBaseImpl::GetPath ()
{
  String Path = mName;
  IFolderPtr Parent (ParentFolder, true);
  while (Parent)
  {
    //get name of immediate parent folder
    String ParentName = Parent->get_Name ();
    //if no trailing backslash in name, append it
    if (ParentName.SubString (ParentName.Length (), 1) != "\\") ParentName = ParentName + "\\";
    //prepend parent folder name to result path
    //this is our full path so far
    Path = ParentName + Path;
    //move up to next level
    Parent = Parent->get_ParentFolder ();
  }

  return Path;
}

I have highlighted the most important parts of the process. In simple terms, what we're doing here is we're walking up the chain of ParentFolder's and each time we find one, we prepend its name to our path, eventually resulting in a path that reflects an aggregation of all the folder names (separated by backslashes) from the root folder down to to our file (or folder).

Since the GetPath algorithm is useful in this case, let's add the ParentFolder property to IFile.

IFile = interface
  property Name : string;
  property Attributes : integer;
  property Size : integer;
  property Time : float;
  property IsFolder : boolean;
  property ParentFolder : IFolder;
end;

You already know the drill: we have to go to the TLE again and add property ParentFolder to IFile. Then we implement it:

//base for TFileImpl and TFolderImpl
template <typename DispIntfT, const IID *piid, const GUID *plibid>
class ATL_NO_VTABLE TBaseFileImpl :
  public IDispatchImpl <DispIntfT, piid, plibid>
{
public:
  //IFile
  ...
  STDMETHOD (get_ParentFolder) (Filesystemserver_tlb::IFolderPtr* Value)
  {
    if (mParentFolder)
    {
      *Value = mParentFolder;
      (*Value)->AddRef ();
    }
    else
      *Value = (IFolder*) NULL;

    return S_OK;
  }

protected:
  IFolder *mParentFolder;
  ...
};

Note that we implement it in TBaseFileImpl. This way, it automatically propagates to both TFileImpl and TFolderImpl!

One final detail eludes us: how do we pass the ParentFolder pointer into TBaseFileImpl? Simplest way is through its Initialize method:

//base for TFileImpl and TFolderImpl
template <typename DispIntfT, const IID *piid, const GUID *plibid>
class ATL_NO_VTABLE TBaseFileImpl :
  public IDispatchImpl <DispIntfT, piid, plibid>
{
public:
  void Initialize (IFolder *ParentFolder, String Name, int Attributes,
    int Size, double Time)
  {
    //initialize fields
    mParentFolder = ParentFolder;
    mName = Name;
    mAttributes = Attributes;
    mSize = Size;
    mTime = Time;
  }
}

But who passes ParentFolder into TBaseFile::Initialize? The TFilesImpl container, of course:

//load file list from physical path
void TFilesImpl::InitializeFiles (IFolder *ParentFolder, String Path)
{
  //shown in pseudocode for brevity
  bool MoreFiles = (FindFirst (Path + "\\*.*"));
  while (MoreFiles)
  {
    IFilePtr NewFile;

    //determine whether file or folder
    if (FileIsFolder)
    {
      CComObject <TFolderImpl> *Folder;
      CComObject <TFolderImpl>::CreateInstance (&Folder);
      Folder->AddRef ();
      //initialize folder params
      Folder->Initialize (ParentFolder, FileName, FileAttr, FileSize, FileTime);
      //since this is an IFolder, we want to QI for IFile
      //because our filelist stores IFile's
      CComQIPtr <IFile, &IID_IFile> File (Folder);
      NewFile = File;
    }
    else
    {
      //create as file
      CComObject <TFileImpl> *File;
      CComObject <TFileImpl>::CreateInstance (&File);
      File->AddRef ();
      //initialize file params
      File->Initialize (ParentFolder, FileName, FileAttr, FileSize, FileTime);
      NewFile = File;
    }

    //add new file to list
    mFiles.push_back (NewFile);

    //proceed next
    MoreFiles = (FindNext (Path + "\\*.*"));
  }
}

And who passes ParentFolder into TFilesImpl::InitializeFiles? The TFolderImpl parent, of course:

STDMETHODIMP TFolderImpl::get_Files (Filesystemserver_tlb::IFilesPtr* Value)
{
  //initialize files if not initialized yet
  if (!mFiles)
  {
    CComObject <TFilesImpl> *Files;
    CComObject <TFilesImpl>::CreateInstance (&Files);
    Files->AddRef ();
    //populate files list
    Files->InitializeFiles (this, GetPath ());
    mFiles = Files;
  }
  *Value = mFiles;
  return S_OK;
}

Got it?!

Navigating FileSystem

With our FileSystem server in place, let's now take a look at how a client would navigate the FileSystem hierarchy. Before we proceed, make sure you build the server project and then register it (using the Run | Register ActiveX Server menu item).

Let's start with a simple one: display drive information from the Drives and Drive objects.

#include "FileSystemServer_TLB.h"

void __fastcall TForm1::DisplayDrivesClick(TObject *Sender)
{
  //create FileSystem
  TCOMIFileSystem FileSystem = CoFileSystem::Create ();
  //access Drives sub-object
  IDrivesPtr Drives = FileSystem->Drives;
  //display drive count
  Memo1->Lines->Add (String () + "There are " + Drives->Count + " drives on your computer");

  //display drive information
  Memo1->Lines->Add ("They are:");
  for (int i = 0; i < Drives->Count; i++)
  {
    IDrivePtr Drive = Drives->get_Item (i);
    //display single drive information
    Memo1->Lines->Add (String () + " " + Drive->Name + " (" + Drive->Letter + ":)");
  }
}

We first create the FileSystem object. Then we access the Drives sub-object and display the drive count. Then we iterate the Drives object and for each Drive, we display the volume name and drive letter in a nice manner. Here's a sample output from my machine:

Figure: Sample output from Drives and Drive objects

Going deeper in the hierarchy:

#include "FileSystemServer_TLB.h"

void __fastcall TForm1::DisplayDrivesClick(TObject *Sender)
{
  //create FileSystem
  TCOMIFileSystem FileSystem = CoFileSystem::Create ();
  //access Drives sub-object
  IDrivesPtr Drives = FileSystem->Drives;

  //display folder information on C:
  Memo1->Lines->Add ("");
  IFolderPtr Root = Drives->get_Item (1)->Root; // assumes 0=A, 1=C, 2=D, etc.
  Memo1->Lines->Add (String () + "The root folder on drive " +
  Drives->get_Item (1)->Letter + ":" + " is named " + Root->Name);
  //display file information on root
 
IFilesPtr Files = Root->Files;
  Memo1->Lines->Add (String () + "It contains " + Files->Count + " files and folders");
  Memo1->Lines->Add ("They are:");
  for (int i = 0; i < Files->Count; i++)
  {
    IFilePtr File = Files->get_Item (i);
    //display single file/folder information
    String FileOrFolder;
    if (File->IsFolder) FileOrFolder = "[Folder]"; else FileOrFolder = "[File]";
    Memo1->Lines->Add (String () + " " + File->Name + " " + FileOrFolder);
    //if folder, display how many files and folders are beneath it
    if (File->IsFolder)
    {
      IFolderPtr SubFolder = File;
      Memo1->Lines->Add (String () + " " + "contains " + SubFolder->Files->Count +
        " files and folders");
    }
  }
}

Here, we focus on drive C: (Drives.Item [1] to be exact). We get the root folder on C: and display it. Then we show how many files and folders are in it. Then we go through each file and folder and list them one-by-one. If the item is a file, we display its name, and if it's a folder, we display its name and also a count of files and folders that are beneath it. That's all there is to it and here's a sample output from my machine:

Figure: Sample output from Drive, Folder, Files, and File objects

Since you might think that our client application is rather lame and useless, I've created a "neat" FileSystem Explorer client ala Windows Explorer. Here's a screen shot from FileSystem Explorer:

Figure: FileSystem Explorer application

FileSystem Explorer is available from the FileSystemDelphi.zip download.

Adding the Finishing Touches

If you use Visual Basic (VB), you might be familiar with a couple of VBisms such as the For Each feature and the array default property feature. For Each is a standard mechanism to cycle through a collection object like the following sample code shows:

Private Sub DisplayDrives_Click()
  'declare variables
  Dim FileSystem As FileSystemServer.FileSystem
  Dim Drives As FileSystemServer.IDrives
  Dim Drive As FileSystemServer.IDrive

  'create FileSystem
  Set FileSystem = New FileSystemServer.FileSystem
  'access Drives sub-object
  Set Drives = FileSystem.Drives
  'display drive count
  List1.AddItem "There are " & Drives.Count & " drives on your computer."

  'display drive information
  List1.AddItem "They are:"
  For Each Drive In Drives
    List1.AddItem " " & Drive.Name & " (" & Drive.Letter & ":)"
  Next
End Sub

To support this feature, an object collection needs to implement the COM IEnumVARIANT interface. That's what For Each expects. IEnumVARIANT looks like this:

//details omitted for brevity
IEnumVARIANT = interface (IUnknown)
  function Next: HResult; stdcall;
  function Skip: HResult; stdcall;
  function Reset: HResult; stdcall;
  function Clone: HResult; stdcall;
end;

So what really happens is that VB calls methods of IEnumVARIANT (such as Next) to implement its For Each feature. It's really not that hard to implement IEnumVARIANT but if you, like me, don't want to waste time on reinventing the wheel, I have created a class that provides IEnumVARIANT access to an STL vector of smart pointers (CBuilder's TComInterface class). In order to plug my CVectorEnumVariant class into your object, you need to initialize it with a couple of simple template parameters:

template <typename ItemT, typename CopyT>
class CVectorEnumVariant;

template <typename InterfaceT>
class CopyInterfaceToVariant;

ItemT specifies the the TComInterface specialized class type of each element in your vector. CopyT specifies a class type that handles copying a vector element into a VARIANT storage. I've also provided a templatized CopyT called CopyInterfaceToVariant that handles copying a TComInterface into a VARIANT. To initialize it, specify the TComInterface specialized class type of each element in your vector into InterfaceT.

To see this in action, let's try it on our Drives object, shall we?

//define IEnumVARIANT on vector <IDrivePtr>
typedef CVectorEnumVariant <IDrivePtr, CopyInterfaceToVariant <IDrivePtr> > TEnum;
//create IEnumVARIANT enumerator 
CComObject <TEnum> *Enum;
CComObject <TEnum>::CreateInstance (&Enum);

Is that simple enough for you? But wait, how does IEnumVARIANT get into the picture? Not to worry, it's quite simple: clients of IEnumVARIANT expect your collection object to expose a property named _NewEnum whose type is IUnknown and whose dispid is DISPID_NEWENUM or -4. Therefore, we go back to the TLE, add the readonly property _NewEnum to IDrives, set its return type to IUnknown, and set its ID to -4. 

Figure: Defining IEnumVARIANT for IDrives

Then we implement it:

//IEnumVARIANT implementation for VB's ForEach
STDMETHODIMP TDrivesImpl::get__NewEnum (LPUNKNOWN* Value)
{
  //define IEnumVARIANT on vector <IDrivePtr>
  typedef CVectorEnumVariant <IDrivePtr, CopyInterfaceToVariant <IDrivePtr> > TEnum;
  //create enumerator
  CComObject <TEnum> *Enum;
  CComObject <TEnum>::CreateInstance (&Enum);

  //initialize our enumerator with our mDrives vector
  //Initialize () returns the IEnumVARIANT that we want _NewEnum to return to the client
  *Value = Enum->Initialize (mDrives, GetUnknown ());
  return S_OK;
}

That should do it for our IEnumVARIANT! Also note that I've implemented this for TFilesImpl.

In addition to the For Each feature, VB also has the ability to use default array properties (or more specifically, default properties). What this means is that instead of this:

set Drive = Drives.Item (0)

you can simply do this:

set Drive = Drives (0)

Not surprisingly, it's very easy to support this kind of feature. Automation rules dictate that if you have an interface property whose dispid is DISPID_VALUE or 0, clients will treat that as the default property. In other words, if we go back to the TLE and change the ID values of IDrives.Item [] and IFiles.Item [] both to 0, that should make the Item [] property default for the Drives and the Files objects.

Conclusion

This tutorial has shown you how to design and implement an object hierarchy in your COM server. I hope that you've learned enough to go out there and experiment with the endless possibilities that can be done with this knowledge. Have fun!

A Word of Caution

All source code presented here is not necessarily production quality code. In particular, tasks like error handling, multithreading, optimizations, etc. were not taken into consideration because they are not relevant to the tutorial topic. Therefore, I do not make any guarantees that the code will work for you, and you cannot hold me liable for any damages resulting from the use (or misuse) of this material. In other words, don't blame me if you get ****ed!

Copyright (c) 1999-2011 Binh Ly. All Rights Reserved.