|
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:
- 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.
- 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
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
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
>>
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
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.
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!
|