techvanguards.com
Last updated on 9/27/2002

Implementing a Plug-in Framework
by Binh Ly

Download the source code for this article

A plug-in (or snap-in) framework provides a standard mechanism for developing applications that need to be easily evolved and customized. For example, you might want to develop a commercial application for different audience levels/versions (say Standard, Professional, and Enterprise). Instead of maintaining 3 separate code bases for the different versions, you can simply create a single shell/host application that incorporates the features available for each version as incremental plug-ins. Using this idea, the Standard version will have the host application + standard plug-ins, the Professional version will have the host application + standard + professional plug-ins, and the Enterprise version will have the host application + standard + professional + enterprise plug-ins.

Another reason to use plug-ins is to allow your clients to create customized sub-modules that plug in to your (host) application. This is very common for utility applications such as operating systems or operating system shells. For instance, the Win32 shell provides a slew of APIs and COM interfaces that allow you to write your own shell extensions or shell plug-ins.

Incidentally, COM is a perfect solution for implementing plug-in frameworks. This is made possible because of COM's binary interoperability aspect. In simple terms, you can use COM to create a framework consisting of host and plug-in applications, possibly written in different languages (Visual Basic, Delphi, C++, etc.), and be able to have them all work together seamlessly. In this tutorial I'll show you how to implement a simple plug-in framework based on COM.

A Universal Explorer

Basically, a plug-in framework consists of 2 parties: the host and the plug-in. The host is the application that "contains" the plug-in. The host exposes a standard interface, IHost, that the plug-in uses when talking to the host. Similarly, the plug-in also exposes a standard interface, IPlugin, that the host uses when talking to the plug-in. For our purposes, we'll create a simple application called a universal Explorer. Explorer is a host application that can be used to explore any type of information that can be presented in a hierarchical manner. This information can be your computer's file system, database master-detail relationships, organizational charts, your family tree, etc. Explorer will expose a host interface called IExplorer.

Explorer's plug-ins will be modules that know how to navigate hierarchical information. For instance, we can create a file system plug-in for exploring our computer's file system. In fact, we'll create a file system plug-in to demonstrate Explorer's capabilities. Each of Explorer's plug-ins will expose a plug-in interface called IExplorable.

Interface Design

We'll want each plug-in to be able to describe itself and to hold a pointer back to its IExplorer host. Thus:

IExplorable = interface
  procedure SetExplorer(Explorer);  //called by IExplorer host to set IExplorable's back pointer to host
  function GetDescription : string; //Explorable describes itself
end;

For any given node in the hierarchy, the Explorer host will want to be able to display its children. For this to work, there has to be way to iterate that node's child information. For instance, in a file system, if C:\ contains 5 files, there has to be a way for Explorer to ask the plug-in what the 5 files under C:\ are. IExplorable provides this facility through a second "iterator" interface, ISubItems:

ISubItems = interface
  function GetCount : integer; //returns sub-item count for a given node
  function GetItem (Index) : string; //returns each sub-item element (0-based index)
end;

Given ISubItems, Explorer can obtain how many sub-items (Count) and what the sub-items are (Item []) under any given node in the hierarchy. Therefore, we'll need to add a method to IExplorable that returns ISubItems given a node:

IExplorable = interface
  procedure SetExplorer(Explorer);  
  function GetDescription : string;    
  function GetSubItems (Path) : ISubItems;  //returns ISubItems iterator given a node
end;

The Path parameter is simply a full path (from the root) to the node where we want to obtain sub-items. For example, GetSubItems ("c:\") would return sub-files/folders at the C:\ folder level whereas GetSubItems ("c:\Level1\Level2\Level3") would return sub-files/folder at the c:\Level1\Level2\Level3 folder level.

We also want to obtain information about a given node in the hierarchy. After all, what good is an explorer if we can't get detailed information at each node. For simplicity we're only interested in information that can be presented as name-value strings. For instance, information on a node in a file system might look like this:

Property Name Value
Type File (or Folder)
Name Filename.txt
Size 1234
Date/Time 1/1/2000 12:00 AM
Attributes Archive, Hidden, System

Name-value pairs like this are really node properties, so we'll just add a GetProperties method to IExplorable:

IExplorable = interface
  procedure SetExplorer(Explorer);  
  function GetDescription : string;    
  function GetSubItems (Path) : ISubItems;  
  function GetProperties (Path) : Array;  //returns property name-value pairs for given node
end;

As you'll see later, we'll implement GetProperties by using a 2-dimensional array to return the name-value table I've shown you above.

Last but not least, we'll also get a bit fancy and provide a context menu capability for Explorer. This enables Explorer to perform any kind of custom operation on any node in the hierarchy that our plug-in supports. For our purposes we'll allow the user to be able to right-mouse-click on any node (file/folder) in the file system hierarchy and have our file system plug-in provide a context menu action that enables the user to rename a file:

Figure: Explorer context menu (consisting of Rename action)

Note that we'll allow renaming of files with *only* the Archive attribute bit set so that you don't accidentally go off and rename one of your system or hidden files. However, you can easily tweak the code to circumvent my safety protection scheme but you do so at your own risk.

Executing a custom context menu action consists of 2 stages: 1 to display the action in a popup menu and 2 to actually execute that action that the user selected from the menu. In other words, on a right-mouse-click, Explorer will first ask our plug-in for a list of action menu items that can be done on a given node. This list will be an action name-ID pair like this:

Action Name Action ID
Rename 1
Delete 2
View 3
... ...

Given this table, Explorer will show the action names in the context menu. After that, whenever the user selects an item in the menu, Explorer will then ask the plug-in to execute the action (specifically the action ID) on the selected node. In other words:

IExplorable = interface
  procedure SetExplorer(Explorer);  
  function GetDescription : string;    
  function GetSubItems (Path) : ISubItems;  
  function GetProperties (Path) : Array;  
  function GetMenuActions (Path) : Array;  //returns context menu actions for given node
  function DoMenuAction (Path; ActionId);  //executes selected action ID on given node
end;

To reiterate, Explorer will call GetMenuActions to populate the context menu with action information. Once the user selects an item from the menu, it then calls DoMenuAction to trigger the action associated with that menu item.

That's it for IExplorable. Let's now look at what we can do with the host interface, IExplorer.

For simplicity, I added a method to IExplorer that our file system plug-in can call after a successful file rename action:

IExplorer = interface(IUnknown)
  procedure RenamePath (OldPath; NewPath);  //called by plug-in when a node's name changes
end;

RenamePath is used by the plug-in to notify the Explorer host that a name change has happened to one of its nodes. This enables the host to perform any necessary action accordingly. More specifically, Explorer can implement RenamePath to refresh it's UI view of the hierarchy to reflect the name change.

That's it for the simple IExplorer.

Component Categories

One of the most common questions when designing a plug-in framework is "What is the standard mechanism of how to determine what plug-ins are available for a given host?" This question arises from the fact that everytime we run the host application, we need to determine its available (compatible) plug-ins. Do the plug-ins announce their availability by writing entries to a common registry location? An INI-file? A common database file? Somewhere?

Fortunately, COM provides a standard mechanism that enables a plug-in to announce its capabilities (i.e. what type of plug-in it is) and for a host application to announce what it offers (i.e. what can a plug-in expect from it). This mechanism is called component categories. To further illustrate, our Explorer can only host plug-ins that are explorable (i.e. supports IExplorable). We can group the set of all explorable plug-ins together and give it a name: "Explorable Plugins". In COM, when we group objects together based on some commonality, the group that we come up with is called a component category. Therefore, we say that the set of all explorable plug-ins is the "Explorable Plugins" component category.

COM component categories are stored in a distinct location in the registry (HCKR\Component Categories). Just like interfaces, coclasses, and other COMisms, each category is uniquely identified by a GUID - more specifically a category ID or CATID. By now, it should be clear that we need a CATID for "Explorable Plugins". I've defined this CATID as follows:

//category ID for explorable plug-ins
CATID_Explorable = "{5111C0AC-7397-11D3-A801-0000B4552A26}";

The next step is to get this CATID registered. Again, COM can help a bit here by providing standard interfaces (and implementation) for managing component categories. In particular, there's 2 interfaces that you'll be friends with: ICatRegister and ICatInformation. ICatRegister, as the name implies, provides mechanisms for registering (and unregistering) component categories. ICatInformation, as the name implies, provides mechanisms for obtaining registered category information. 

ICatInformation and ICatRegister are both implemented by a COM coclass defined by CLSID_StdComponentCategoryMgr. When we want ICatInformation, we simply create this coclass asking for ICatInformation, and when we want ICatRegister, we simply create this coclass asking for ICatRegister.

When registering a CATID into HKCR\Component Categories, we'd be interested in ICatRegister.RegisterCategories:

ICatRegister = interface (IUnknown)
  function RegisterCategories (
    cCategories: UINT;               //number of CATIDs to register
    rgCategoryInfo: PCATEGORYINFO    //array of category information to register
  ): HResult; stdcall;
  ... other methods omitted ...
end;

The rgCategoryInfo part simply points to an array of category information (CATEGORYINFO) records each of which looks like this:

TCATEGORYINFO = record
  catid: TGUID;                                //category ID
  lcid: UINT;                                  //locale ID, for multi-language support
  szDescription: array[0..127] of WideChar;    //category description
end;

Since we're only registering 1 category, we'll simply fill our 1 category information record as follows:

//declare variable ExplorableCategoryInfo of type TCATEGORYINFO record
var ExplorableCategoryInfo : TCATEGORYINFO;  

//initialize ExplorableCategoryInfo record
ExplorableCategoryInfo.catid = CATID_Explorable;
ExplorableCategoryInfo.lcid = LOCALE_SYSTEM_DEFAULT;  //dummy
ExplorableCategoryInfo.szDescription = "Explorable Plugins";

With this, registering our category would simply be:

var CatReg : ICatRegister;
//create standard component cat manager asking for ICatRegister
CatReg = GetStdComponentCategoryMgr (ICatRegister);
//register CATID_Explorable category
CatReg.RegisterCategories (1, ExplorableCategoryInfo);  //only 1 catinfo record to register

So far, we've just registered our new component category under HKCR\Component Categories. The next step is to mark our plug-in implementation (coclass) as supporting this category. After all, a host needs to determine which plug-ins implement a certain component category. Assuming that our file system explorer plug-in is implemented by a coclass defined by this CLSID:

CLSID_FileSystemExplorable = "{8B9A0689-7434-11D3-A802-0000B4552A26}";

The convention for announcing that this coclass implements our "Explorable Plugins" component category is to write registry subkeys under this coclass as follows:

HKCR\{8B9A0689-7434-11D3-A802-0000B4552A26}     //CLSID_FileSystemExplorable
    Implemented Categories                      //indicates we implement some categories
        {5111C0AC-7397-11D3-A801-0000B4552A26}  //indicates we implement CATID_Explorable
        ... list more implemented CATIDs here if any ...
    InprocServer32
    ...

As you might have guessed, getting these entries into the registry can be done using ICatRegister. In particular, ICatRegister.RegisterClassImplCategories registers information about a coclass's implemented categories:

ICatRegister = interface (IUnknown)
  function RegisterClassImplCategories (
    const rclsid: TGUID;  //CLSID of coclass that implements categories
    cCategories: UINT;    //count of category information to register for this coclass
    rgcatid: Pointer      //points to array of TCATEGORYINFO records containing categories to register 
  ): HResult; stdcall;
  ... other methods omitted ...
end;

With this, registering our FileSystemExplorable coclass as implementing the "Explorable Plugins" category can be done as follows:

//declare variable ExplorableCategoryInfo of type TCATEGORYINFO record
var ExplorableCategoryInfo : TCATEGORYINFO;  

//initialize ExplorableCategoryInfo record
ExplorableCategoryInfo.catid = CATID_Explorable;
ExplorableCategoryInfo.lcid = LOCALE_SYSTEM_DEFAULT;  //dummy
ExplorableCategoryInfo.szDescription = "Explorable Plugins";

var CatReg : ICatRegister;
//create standard component cat manager asking for ICatRegister
CatReg = GetStdComponentCategoryMgr (ICatRegister);
//register implemented categories for FileSystemExplorable coclass
CatReg.RegisterClassImplCategories (
  CLSID_FileSystemExplorable,  //FileSystemExplorable coclass
  1,                           //implements 1 category
  ExplorableCategoryInfo       //1 categoryinfo record
);

We can also unregister (remove from the registry) a coclass's implemented categories. This is useful when we want to unregister our plug-in from the system. To unregister, we simply use the (obvious) ICatRegister.UnRegisterClassImplCategories method:

ICatRegister = interface (IUnknown)
  function UnRegisterClassImplCategories (
    const rclsid: TGUID;  //CLSID of coclass that implements categories
    cCategories: UINT;    //count of category information to register for this coclass
    rgcatid: Pointer      //points to array of TCATEGORYINFO records containing categories to unregister 
  ): HResult; stdcall;
  ... other methods omitted ...
end;

As you can see, the unregistration syntax is exactly the same as calling RegisterClassImplCategories. The only difference here is that UnRegisterClassImplCategories will remove the registry category information from under the "Implemented Categories" subkey of your CLSID. 

Now that we can annotate each plug-in coclass in the registry to support any category, our host application can then simply scan the registry CLSID entries and check each of the "Implemented Categories" subkey to determine which coclasses are valid plug-ins (for a given category). Scanning the registry doesn't have to be manual - we again use our friend interface, ICatInformation. In particular, ICatInformation allows us to get a list of all coclasses that implement a particular CATID. This is accomplished through the ICatInformation.EnumClassessOfCategories method:

ICatInformation = interface (IUnknown)
  function EnumClassesOfCategories (
    cImplemented: UINT;   //count of implemented categories we're interested in
    rgcatidImpl: Pointer; //array of implemented categories we're interested in
    cRequired: UINT;      //count of required categories we're interested in
    rgcatidReq: Pointer;  //array of required categories we're interested in
    out ppenumClsid: IEnumGUID  //return CLSID/GUID enumerator of matching coclasses
  ): HResult; stdcall;
  ... other methods omitted ...
end;

The parameters we're most interested in are cImplemented, rgcatidImpl, and ppenumClsid. In our case, we're only interested in 1 category so cImplemented=1, and rgcatidImpl would point to our single TCATEGORYINFO record. Upon a successful scan, EnumClassesOfCategories returns an enumerator in ppenumClsid. We can then use ppenumClsid to enumerate the list of CLSIDs of each of the matching plug-in coclasses. This enumerator has the same usage rules as any other standard COM enumerator (IEnumXXX) so I won't go into the details of how to perform the enumeration - you can simply check the source code to see how it's done.

In a similar respect to "implemented categories", a plug-in may also register itself as requiring the host to implement certain interfaces (or categories). This way, a host can also tell whether or not it is possible to host a particular plug-in based on what the plug-in expects of the host. This information is stored in another subkey, "Required Categories", under each plug-in's CLSID registry key. 

Although I won't be showing how to implement "required categories" in this tutorial, it's good to know that such a facility exists and is also part of COM component categories. For a complete reference, you can check MSDN online for details.

So far, we can register component categories (CATIDs), we can register our coclasses as implementing a particular category, and we can enumerate all coclasses that implement/support a given category. With this, we're now ready to implement Explorer and our FileSystemExplorable plug-in.

Before we proceed, I just want to make a few comments about the implementation:

  1. Our Explorer host will be an MDI application wherein each MDI child form loads a separate explorable plug-in. Each explorable plug-in is presented in a simplistic tree view UI, although it's certainly possible to develop a more sophisticated UI that accurately represents your hierarchical data.
     
  2. Our file system explorable plug-in is implemented as a DLL server. Plug-ins are normally implemented as such. In addition, the file system plug-in coclass is implemented as a lightweight COM object instead of an automation object. This is simply because we have no need of automation capabilities from our plug-in coclasses.
     
  3. The IExplorable plug-in interface is implemented as IUnknown-based (instead of the more familiar IDispatch). In the type library, I've marked IExplorable with the [oleautomation] flag for it to be standard/type-library marshaled. I did this because most plug-in frameworks you'll encounter will consist of plug-in interfaces that are IUnknown-based - this way, you won't be surprised once you start implementing plug-ins for other vendor's applications/frameworks. 

    Because IExplorable is IUnknown-based, each method is implemented as returning an HRESULT (this is good programming practice). So in the actual implementation, methods that return values actually return the values through an extra [out] parameter.

That's it. Shall we?!

Defining the Plug-in Framework Interfaces

The first thing we need to do is to define our plug-in framework interfaces in a type library. From there, we can later use the type library definitions when implementing Explorer and the explorable plug-ins. To do this, we create a type library (select File | New menu and pick Type Library on the ActiveX tab in the dialog) and name it Explorer.tlb. Then we go into Explorer.tlb and define IExplorer (host interface), IExplorable (plug-in interface), and ISubItems (IExplorable helper), as discussed earlier. I won't bore you with the step-by-step details on how to do this - you can simply inspect the final Explorer.tlb file from the source download to see how I did this. After inserting the interfaces, we register the type library (click the Register Type Library icon in the Type Library Editor (TLE)) ready for COM usage.

With the registered interfaces/type library in place, let's now proceed with the real implementation.

Delphi Implementation
C++ Builder Implementation


Delphi Implementation

Implementing the File System Plug-in

In Delphi, we create an in-process (DLL) 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 "FileSystemPlugin" - Delphi will produce a file named FileSystemPlugin.dll as our plug-in server.

Then we create our plug-in coclass. To do this, we select File | New and pick COM Object in the ActiveX tab in the dialog, name the object "FileSystemExplorable", and save the new module as FileSystemExplorable.pas. 

In Delphi 4/5, make sure you uncheck the "Include Type Library" checkbox when generating the FileSystemExplorable class. We're simply creating a lightweight COM object and we don't need any information entered into a type library.

Also, Delphi 3 does not have the create "COM Object" option under the ActiveX tab. However, you can manually create the FileSystemExplorable class that looks like the one Delphi 4/5 produces. Don't worry, the VCL classes you need are there - it's just that D3 doesn't give you the wizard.

By doing this, Delphi creates a plain lightweight COM object like this:

type
  TFileSystemExplorable = class (TComObject)
  end;

const
  Class_FileSystemExplorable: TGUID = '{8B9A0689-7434-11D3-A802-0000B4552A26}';

Since we're creating a plug-in, we want FileSystemExplorable to implement the IExplorable plug-in interface. To do that, we manually add IExplorable to TFileSystemExplorable as follows:

type
  TFileSystemExplorable = class(TComObject, IExplorable)
  protected
    //IExplorable methods
    function SetExplorer(const Explorer: IExplorer): HResult; stdcall;
    function GetDescription(out Description: WideString): HResult; stdcall;
    function GetSubItems(const Path: WideString; out SubItems: ISubItems): HResult; stdcall;
    function GetMenuActions(const Path: WideString; out Actions: OleVariant): HResult; stdcall;
    function DoMenuAction(const Path: WideString; ActionId: Integer): HResult; stdcall;
    function GetProperties(const Path: WideString; out Properties: OleVariant): HResult; stdcall;
  protected
    FExplorer : IExplorer;
end;

Then we implement the methods. Let's start with the trivial SetExplorer and GetDescription:

function TFileSystemExplorable.SetExplorer (const Explorer: IExplorer): HResult;
begin
  FExplorer := Explorer;  //FExplorer is a member field of TFileSystemExplorable
  Result := S_OK;
end;

function TFileSystemExplorable.GetDescription (out Description: WideString): HResult;
begin
  Description := 'File System (Delphi version)';
  Result := S_OK;
end;

Nothing complicated here. Let's do GetSubItems instead.

Remember, GetSubItems is called by Explorer to obtain an ISubItems interface to enumerate sub-items of any node in the hierarchy. For us, that means that we have to scan the folder (specified by the node/path) using the FindFirst and FindNext functions. Let's encapsulate this process in a TSubItems class:

type
  TSubItems = class (TInterfacedObject)
  protected
    FSubItems : TStringList;
    procedure LoadSubItems (const Path : string);
    procedure LoadDrives;
    procedure LoadFiles (Path : string);
  public
    constructor Create (const Path : string);
  end;

constructor TSubItems.Create(const Path: string);
begin
  inherited Create;
  FSubItems := TStringList.Create;
  LoadSubItems (Path);
end;

//loads file system subitems given a path
procedure TSubItems.LoadSubItems(const Path: string);
begin
  //reset list
  FSubItems.Clear;
  //if path is root, load system drives else load path as folder
  if (Path = '') then
    LoadDrives
  else
    LoadFiles (Path);
end;

//load system drives into FSubItems list
procedure TSubItems.LoadDrives;
begin
  //implementation in pseudocode
  Find all drives;
  For each drive found
    Add drive name into FSubItems list;
end;

//load folder subfiles (and subfolders) into FSubItems list
procedure TSubItems.LoadFiles(Path: string);
begin
  //implementation in pseudocode
  Find all files (and folders) under Path (using FindFirst/FindNext)
  For each file (and folder) found
    Add file name into FSubItems list;
end;

Constructing a TSubItems takes care of loading the sub-files for a given path. The important method here is LoadSubItems. In LoadSubItems, if the path specified is blank, then we load the sub-items list with all the drives on your system, otherwise we simply do a FindFirst-FindNext file scan of the given path. 

Since FileSystemExplorable must hand out ISubItems to Explorer, we simply implement ISubItems into TSubItems:

type
  TSubItems = class (TInterfacedObject, ISubItems)
  protected
    //ISubItems methods
    function GetCount(out Count: Integer): HResult; stdcall;
    function GetItem(Index: Integer; out Item: WideString): HResult; stdcall;
  protected
    FSubItems : TStringList;
  ...
  end;

//return sub-item count
function TSubItems.GetCount(out Count: Integer): HResult;
begin
  Count := FSubItems.Count;
  Result := S_OK;
end;

//return sub-item text by index
function TSubItems.GetItem(Index: Integer; out Item: WideString): HResult;
begin
  Item := FSubItems [Index];
  Result := S_OK;
end;

Finally, we can go back to TFileSystemExplorable and implement GetSubItems:

function TFileSystemExplorable.GetSubItems(const Path: WideString;
  out SubItems: ISubItems): HResult;
begin
  //ask TSubItems to return a subitems list for a given path
  SubItems := TSubItems.Create (Path);
  Result := S_OK;
end;

The next step is to implement GetProperties. If you recall, GetProperties is called by Explorer to obtain name-value pairs of information for any given node. More specifically, GetProperties returns an array of name-value pairs. In COM, we can simply implement this using a variant array. Thus:

function TFileSystemExplorable.GetProperties(const Path: WideString;
  out Properties: OleVariant): HResult;
begin
  Result := S_OK;

  //Properties is a 2-dim array:
  //
  // | Property Name 1 | Property Value 1 |
  // | Property Name 2 | Property Value 2 |
  //
  Properties := VarArrayCreate ([
    0, 4, //row bounds
    0, 1 //column bounds
    ],
    varOleStr //string elements
  );

  //Type: File or Folder?
  Properties [0, 0] := 'Type';
  if IsFolder (Path) then
    Properties [0, 1] := 'Folder'
  else
    Properties [0, 1] := 'File';

  //Name
  Properties [1, 0] := 'Name';
  Properties [1, 1] := NameOfFile (Path);

  //Size
  Properties [2, 0] := 'Size';
  Properties [2, 1] := IntToStr (SizeOfFile (Path));

  //Date/Time
  Properties [3, 0] := 'Date/Time';
  Properties [3, 1] := DateTimeToStr (DateTimeOfFile (Path));

  //Attributes
  Properties [4, 0] := 'Attributes';
  Properties [4, 1] := AttributesOfFile (Path);
end;

What we're doing here is simply returning a 2-dim variant array (of 5 rows indexed from 0 to 4) consisting of the following file properties: Type, Name, Size, Date/Time, and Attributes. I'm not going to bore you with details of how to physically obtain each property value because that's not relevant - feel free to check the source code for details.

The last 2 methods we need to implement are GetMenuActions and DoMenuAction. If you recall, these methods are for the context sensitive custom operations that can be done on any node in your hierarchy. GetMenuActions returns an array of action-ID pairs to Explorer and DoMenuAction executes the user-selected action on a given node. 

Also, if you recall, we only wanted to implement a single menu action - Rename File. And we specifically want to rename files with only the Archive bit set. Thus:

const ACTION_RENAME = 1;

function TFileSystemExplorable.GetMenuActions(const Path: WideString;
  out Actions: OleVariant): HResult;
var
  ActionCount : integer;
begin
  Result := S_OK;

  //this ActionCount business demonstrates how menu item actions can be dynamic
  //depending on context
  ActionCount := 0;

  //Actions is a 2-dim array:
  //
  // | Action Name 1 | Action ID 1 |
  // | Action Name 2 | Action ID 2 |
  //
  Actions := VarArrayCreate ([
    0, 0, //row bounds
    0, 1 //column bounds
    ],
    varVariant //variant elements
  );

  //Rename
  //allow rename on files with archive attribute only
  if (FileOnlyHasArchiveAttributeSet (Path)) then
  begin
    //add Rename action
    Actions [ActionCount, 0] := 'Rename';
    Actions [ActionCount, 1] := ACTION_RENAME;  //integer constant defined as 1
    //one action in!
    inc (ActionCount);
  end;

  //you can add more custom action logic here...

  //if no Actions in context, clear it
  if (ActionCount <= 0) then Actions := Unassigned;
end;

As you can see, we're simply creating a 2-dim variant array to store the action-ID pairs. Since we're only interested in a rename action, we only have at most 1 row element in the array (hence the row bounds 0 to 0).

With this, whenever Explorer gets its action list, it will populate its context menu with it. After that, if the user selects the rename action, Explorer will then call DoMenuAction in our plug-in, passing in the ACTION_RENAME action ID constant. Thus, we can simply implement DoMenuAction to process the rename action as follows:

function TFileSystemExplorable.DoMenuAction(const Path: WideString; ActionId: Integer): HResult;
var
  NewName : string;
begin
  Result := S_OK;
  //case out ActionID
  case ActionID of
    ACTION_RENAME :
    //rename and notify host if renamed
    if (TfrmRename.Rename (Path, NewName)) then
      //notify explorer host after rename action
      FExplorer.RenamePath (Path, NewName);

    //implement other ActionIDs here, if any...
  end;
end;

In here, we check the ActionID parameter and perform the corresponding action on the given path. For ACTION_RENAME, we simply run a form (TfrmRename) that takes care of presenting the user with a simple dialog box that allows the user to input a new name for the given file. On a successful rename, we then proceed to call FExplorer.RenamePath to tell our host Explorer that a certain node has changed its name. This is done so that Explorer can then accordingly update its UI to reflect the name change.

This is an example of how a plug-in may need to communicate back to its host. Normally, this process is done so that the plug-in and the host can both synchronize themselves to reflect state change in the data manipulated by both the plug-in and the host.

So much for IExplorable. The last thing we need to do is to somehow incorporate the code that registers and unregisters component categories into our plug-in server. The easiest way to do this is that whenever our plug-in gets registered, we also register the category information along with it, and whenever it gets unregistered, we also unregister the category information appropriately. For DLL servers, the registration entry points of interested are the exported functions: DLLRegisterServer (when registering) and DLLUnregisterServer (when unregistering). 

Let's first take care of registration:

In FileSystemPlugin.dpr

library FileSystemPlugin;

//overidden to include categories registration
function DllRegisterServer: HResult; stdcall;
begin
  //call default implementation
  Result := ComServ.DllRegisterServer;
  //register as Explorable plugin
  RegisterAsExplorableClass (Class_FileSystemExplorable, True);  //True means register
end;

exports
  DllGetClassObject,
  DllCanUnloadNow,
  DllRegisterServer,
  DllUnregisterServer;

...

In here, we're simply "overriding" DLLRegisterServer to also include our category information registration routine. RegisterAsExplorableClass is defined as follows:

//registers a given class as an explorable server
procedure RegisterAsExplorableClass (const CLSID : TCLSID; Register : boolean);
var
  CatReg : ICatRegister;
begin
  CatReg := StdComponentCategoryMgr as ICatRegister;
  if (Register) then
  begin
    //first register CATID_Explorable category
    //ExplorableCategoryInfo is filled with cat info as discussed earlier
    OleCheck (CatReg.RegisterCategories (1, @ExplorableCategoryInfo));
    //then register CLSID as supporting that category
    OleCheck (CatReg.RegisterClassImplCategories (CLSID, 1, @ExplorableCategoryInfo));
  end
  else begin
    //perform unregistration here
  end;
end;

//returns standard component category manager
function StdComponentCategoryMgr : IUnknown;
begin
  Result := CreateComObject (CLSID_StdComponentCategoryMgr);
end;

What we're doing here is simply registering the "Explorable Plugins" category and then registering the FileSystemExplorable coclass as implementing that category. We've discussed these details before so there shouldn't be anything new here.

For the unregistration part, we simply hook DLLUnRegisterServer and reverse what we've done in DLLRegisterServer. Thus:

In FileSystemPlugin.dpr

library FileSystemPlugin;

//overidden to include categories unregistration
function DllUnregisterServer: HResult; stdcall;
begin
  //unregister as Explorable plugin
  RegisterAsExplorableClass (Class_FileSystemExplorable, False);  //False means unregister
  //pass on to default unregistration handler
  Result := ComServ.DllUnregisterServer;
end;

exports
  DllGetClassObject,
  DllCanUnloadNow,
  DllRegisterServer,
  DllUnregisterServer;

...

Accordingly, the unregister part in RegisterAsExplorableClass is defined as:

//registers a given class as an explorable server
procedure RegisterAsExplorableClass (const CLSID : TCLSID; Register : boolean);
var
  CatReg : ICatRegister;
begin
  CatReg := StdComponentCategoryMgr as ICatRegister;
  if (Register) then
  begin
    //perform registration here
  end
  else begin
    //note that we never unregister CATID_Explorable because other servers might
    //still use it!

    //unregister CLSID as supporting that category
    OleCheck (CatReg.UnregisterClassImplCategories (CLSID, 1, @ExplorableCategoryInfo));
    DeleteRegKey ('CLSID\' + GuidToString (CLSID) + '\' + 'Implemented Categories');
  end;
end;

//returns standard component category manager
function StdComponentCategoryMgr : IUnknown;
begin
  Result := CreateComObject (CLSID_StdComponentCategoryMgr);
end;

Note that we do an extra DeleteRegKey call to remove the "Implemented Categories" subkey to completely remove our coclass from the registry.

Another way to perform registration of the category information is to create our own customized class factory that inherits from TComObjectFactory (or descendants). TComObjectFactory has an virtual UpdateRegistry method that we can override to perform our own custom registration and unregistration.

Implementing the Explorer Host

With the plug-in in place (oops, don't forget to register your plug-in), it's now time to look at the Explorer host. As I mentioned earlier, our Explorer host is an MDI application wherein each MDI child window hosts an explorable plug-in. To do this, when Explorer loads up, it finds all registered explorable plug-ins and adds them all to a menu list. Whenever the user selects a particular plug-in from the menu, we instantiate an MDI child form (TfrmExplorerHost) and host the selected plug-in into that form.

Figure: "Explorable Plugins" menu list containing a list of registered plug-ins

To obtain a list of explorable plug-ins, we simply use COM's component category facilities as described earlier. In particular, we use ICatInformation to obtain all registered coclasses that implement the "Explorable Plugins" category. Thus:

procedure TfrmMain.LoadExplorableClasses;
var
  Count, i : integer;
  Explorable : IExplorable;
  Description : widestring;
  MenuItem : TMenuItem;
begin
  //get explorable server list
  //FExplorableClasses is an array of CLSIDs
  Count := GetExplorableClasses (FExplorableClasses);
  if (Count > 0) then
  begin
    //get description for each and populate Explore >> submenu
    for i := 1 to Count do
    begin
      //create Explorable plugin to obtain plug-in description
      Explorable := CreateComObject (FExplorableClasses [i]) as IExplorable;
      //get plug-in description
      OleCheck (Explorable.GetDescription (Description));
      //create new menu item
      MenuItem := TMenuItem.Create (Self);
      MenuItem.Caption := Description;
      //hide FExplorableClasses array index in Tag
      MenuItem.Tag := i;
      //add to Explore >> submenu
      miExplore.Add (MenuItem);
    end;
  end;
end;

What's happening here is that we're loading all explorable coclass CLSIDs into the FExplorableClasses array. Then we iterate the array and load each plug-in's description into a menu list.

Here's what GetExplorableClasses really looks like:

type
  TExplorableClasses = array [1..50] of TCLSID; //50 is arbitrary!

//returns Explorable server CLSIDs
function GetExplorableClasses (var ExplorableClasses : TExplorableClasses) : integer;
var
  CatInfo : ICatInformation;
  Enum : IEnumGuid;
  Fetched : UINT;
begin
  Result := 0;
  //get standard category information manager
  CatInfo := StdComponentCategoryMgr as ICatInformation;
  //get an enumeration of all registered Explorable classes
  //ExplorableCategoryInfo is filled with cat info as discussed earlier
  OleCheck (CatInfo.EnumClassesOfCategories (1, @ExplorableCategoryInfo, 0, NIL, Enum));
  //enumerate Explorable classes into ExplorableClasses array
  if (Enum <> NIL) then
  begin
    OleCheck (Enum.Reset);
    //fill ExplorableClasses array
    //note that if Fetched >= High (ExplorableClasses), then we may have more!
    //for our purposes, this sample code will do
    OleCheck (Enum.Next (High (ExplorableClasses), ExplorableClasses [1], Fetched));
    Result := Fetched;
  end;
end;

As we've learned earlier, ICatInformation.EnumClassesOfCategories is used to obtain all coclasses that implement a particular category. Then we use the enumerator result, Enum, to iterate matching CLSIDs and populate the ExplorableClasses array accordingly.

So far, we're able to populate a menu list of explorable plug-ins. Now we have to determine what to do when the user selects a plug-in from the list. Thus:

//called when user selects a plug-in from the explorable plug-ins menu
procedure TfrmMain.miExplorableItemClick(Sender: TObject);
var
  ExplorableClass : TCLSID;
  Explorable : IExplorable;
begin
  //get selected Explorable class
  //Tag contains index into FExplorableClasses array - this is established in
  //proc LoadExplorableClasses
  ExplorableClass := FExplorableClasses [(Sender as TMenuItem).Tag];
  //create Explorable plugin
  Explorable := CreateComObject (ExplorableClass) as IExplorable;
  //load Explorable server into new explorer host form
  TfrmExplorerHost.Load (Explorable);
end;

What we're doing here is instantiating the correct plug-in coclass based on the selected menu item. Then we load the new plug-in into the explorer host form.

The explorer host form simply consists of 2 panes: a left tree view (tvwExplorer) and a right list view (lvwProperties). The tree view displays the explorable plug-in's hierarchy while the list view displays the properties whenever a node is focused/selected in the tree view. In other words, for our FileSystemPlugin, the tree view displays your file system hierarchy and the list view displays file property information (Name, Type, Size, etc.) for the currently selected file/folder in the tree view:

Figure: Tree view and list view panes of the Explorer host form

Let's see what we can do in the tree view. First, we need to be able to expand nodes and display a node's children (sub-items) correspondingly. Recall that we had to implement the IExplorable.GetSubItems method for this. This is how Explorer would now make use of GetSubItems:

procedure TfrmExplorerHost.tvwExplorerExpanding(Sender: TObject;
  Node: TTreeNode; var AllowExpansion: Boolean);
begin
  //clear children then repopulate
  ClearNodeChildren (Node);
  ExpandNode (Node);
end;

procedure TfrmExplorerHost.ExpandNode(Node: TTreeNode);
var
  SubItems : ISubItems;
  Count, i : integer;
  ItemText : widestring;
  ChildNode : TTreeNode;
begin
  //get subitems
  if Succeeded (FExplorable.GetSubItems (NodePath (Node), SubItems)) then
  if (SubItems <> NIL) then
  begin
    //load subitems
    SubItems.GetCount (Count);
    for i := 0 to Count - 1 do
    begin
      SubItems.GetItem (i, ItemText);
      ChildNode := tvwExplorer.Items.AddChild (Node, ItemText);
    end;
  end;
end;

In ExpandNode, we first call GetSubItems to obtain an ISubItems list. We then iterate ISubItems and for each element we find, we add it as a child node underneath the current node.

When we click/select a node in the tree view, we also want to display the node's properties in the list view pane. Thus:

procedure TfrmExplorerHost.tvwExplorerChange(Sender: TObject;
  Node: TTreeNode);
begin
  LoadNodeProperties (Node);
end;

procedure TfrmExplorerHost.LoadNodeProperties(Node: TTreeNode);
var
  Properties : olevariant;
  Count, i : integer;
  ListItem : TListItem;
begin
  //clear
  lvwProperties.Items.BeginUpdate;
  lvwProperties.Items.Clear;
  lvwProperties.Items.EndUpdate;
  //get properties
  //Properties is a 2-dim array
  //
  // | Property Name 1 | Property Value 1 |
  // | Property Name 2 | Property Value 2 |
  //
  if Succeeded (FExplorable.GetProperties (NodePath (Node), Properties)) then
  //load properties
  if not VarIsEmpty (Properties) then
  begin
    //assume row-dimension is 0-based
    Count := VarArrayHighBound (Properties, 1) + 1;
    for i := 0 to Count - 1 do
    begin
      //add new property
      ListItem := lvwProperties.Items.Add;
      //get property name
      ListItem.Caption := Properties [i, 0];
      //get property value
      ListItem.SubItems.Add (Properties [i, 1]);
    end;
  end;
end;

There's nothing hard here. We simply call GetProperties to retrieve the properties array for the selected node. Then we populate the list view based on the array.

Finally, we implement menu actions. On a right-mouse-click, we first build a list of context sensitive menu actions based on the current node. This is accomplished by calling IExplorable.GetMenuActions. Thus:

procedure TfrmExplorerHost.tvwExplorerMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  Node : TTreeNode;
  pt : TPoint;
begin
  //respond to right mouse click
  if (Button = mbRight) then
  begin
    //select node where mouse was right clicked
    Node := tvwExplorer.GetNodeAt (X, Y);
    if (Node = NIL) then Exit;
    tvwExplorer.Selected := Node;
    //initialize popup menu
    if (InitializePopupMenu (Node)) then
    begin
      //show popup menu
      pt := ClientToScreen (Point (X, Y));
      PopupMenu.Popup (pt.X, pt.Y);
    end;
  end;
end;

function TfrmExplorerHost.InitializePopupMenu(Node: TTreeNode): boolean;
var
  MenuItem : TMenuItem;
  Actions : olevariant;
  Count, i : integer;
begin
  //get menu actions
  //Actions is a 2-dim array:
  //
  // | Action Description 1 | Action ID 1 |
  // | Action Description 2 | Action ID 2 |
  //
  if Succeeded (FExplorable.GetMenuActions (NodePath (Node), Actions)) then
  begin
    //add items
    if not VarIsEmpty (Actions) then
    begin
      //assume row-dimension is 0-based
      Count := VarArrayHighBound (Actions, 1) + 1;
      for i := 0 to Count - 1 do
      begin
        //add menu item
        MenuItem := TMenuItem.Create (Self);
        MenuItem.Caption := Actions [i, 0];
        //hide ActionID in Tag
        MenuItem.Tag := Actions [i, 1];
        //add to popup menu
        PopupMenu.Items.Add (MenuItem);
      end;
    end;
  end;
  //a-ok if popup menu has items
  Result := (PopupMenu.Items.Count > 0);
end;

In here, we simply call GetMenuActions to obtain the menu actions array for the selected node. Then we populate the context menu based on the array. Note how we assign the ActionID into the Tag property of each menu item. We'll use that to determine which action ID to use when a user selects an action from the menu. Thus:

procedure TfrmExplorerHost.miExplorerNodeActionClick(Sender: TObject);
var
  ActionID : integer;
begin
  //get ActionID from menu item Tag - this was set proc InitializePopupMenu
  ActionID := (Sender as TMenuItem).Tag;
  //invoke ActionID
  OleCheck (FExplorable.DoMenuAction (NodePath (tvwExplorer.Selected), ActionID));
end;

Here, we obtain the action ID back from the Tag property. Then we invoke the plug-in's DoMenuAction method passing in the current selected node and the action ID. 

The one last thing I haven't shown you is the host's (IExplorer) implementation of RenamePath. Recall that RenamePath is called by the plug-in to notify Explorer after a name change in a node. For our Explorer host form, we simply refresh the tree view node caption with the new name, as well as the corresponding properties list for that node (the properties list needs to be refreshed because it includes the name). Thus:

function TfrmExplorerHost.RenamePath(const OldPath, NewPath: WideString): HResult;
var
  Node : TTreeNode;
begin
  //find terminal node that corresponds to OldPath
  Node := FullPathToNode (RootNode, OldPath);
  if (Node <> NIL) then
  begin
    //change node text to NewPath terminal node name
    Node.Text := ExtractLastNodeName (NewPath);
    //reload node properties
    LoadNodeProperties (Node);
  end;
  Result := S_OK;
end;

I won't go into the details of the FullPathToNode and ExtractLastNodeName functions because they're not very relevant - feel free to inspect the source code for more details.

Proceed to conclusion >>


C++ Builder Implementation

Implementing the File System Plug-in

In CBuilder, we create an in-process (DLL) 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 "FileSystemPlugin" - CBuilder will produce a file named FileSystemPlugin.dll as our plug-in server.

Then we create our plug-in coclass. To do this, we select File | New and pick COM Object in the ActiveX tab in the dialog, name the object "FileSystemExplorable", and save the new module as FileSystemExplorable.cpp. 

Since we're simply creating a lightweight COM object and we don't need any information entered into a type library, I've removed the IFileSystemExplorable interface and the FileSystemExplorable coclass entries from the type library file. However, I've taken FileSystemExplorable's CLSID and embedded it directly in FileSystemExplorable.cpp/FileSystemExplorable.h.

Although I didn't need to remove the information from the type library, I did this to illustrate that the type library entries are not necessary for FileSystemExplorable to work. 

By doing this, CBuilder creates a plain lightweight COM object like this:

class ATL_NO_VTABLE TFileSystemExplorableImpl :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<TFileSystemExplorableImpl, &CLSID_FileSystemExplorable>
{
  ...
};

const GUID CLSID_FileSystemExplorable = //'{8B9A0689-7434-11D3-A802-0000B4552A26}';
  {0x8B9A0689, 0x7434, 0x11D3,{ 0xA8, 0x02, 0x00, 0x00, 0xB4, 0x55, 0x2A, 0x26} };

Note that because TFileSystemExplorableImpl is not dependent on the type library, I've also changed the UpdateRegistry method accordingly:

class ATL_NO_VTABLE TFileSystemExplorableImpl :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<TFileSystemExplorableImpl, &CLSID_FileSystemExplorable>
{
public:
  static HRESULT WINAPI UpdateRegistry(BOOL bRegister)
  {
    //changed from TTypedComServerRegistrarT because we don't need type library
    //information for our explorable plugin
    TComServerRegistrarT<TFileSystemExplorableImpl>
    regObj(GetObjectCLSID(), GetProgID(), GetDescription());
    return regObj.UpdateRegistry(bRegister);
  }
...
};

Since we're creating a plug-in, we want FileSystemExplorable to implement the IExplorable plug-in interface. To do that, we manually add IExplorable to TFileSystemExplorableImpl as follows:

class ATL_NO_VTABLE TFileSystemExplorableImpl :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<TFileSystemExplorableImpl, &CLSID_FileSystemExplorable>,
  public IExplorable
{
protected:
  BEGIN_COM_MAP(TFileSystemExplorableImpl)
    COM_INTERFACE_ENTRY (IExplorable)
  END_COM_MAP()

  //IExplorable methods
  STDMETHOD (SetExplorer) (Explorer_tlb::IExplorerPtr Explorer);
  STDMETHOD (GetDescription) (BSTR* Description);
  STDMETHOD (GetSubItems) (BSTR Path, Explorer_tlb::ISubItemsPtr* SubItems);
  STDMETHOD (GetMenuActions) (BSTR Path, TVariant* Actions);
  STDMETHOD (DoMenuAction) (BSTR Path, long ActionId);
  STDMETHOD (GetProperties) (BSTR Path, TVariant* Properties);
protected:
  IExplorerPtr mExplorer;
};

Then we implement the methods. Let's start with the trivial SetExplorer and GetDescription:

STDMETHODIMP TFileSystemExplorableImpl::SetExplorer (
  Explorer_tlb::IExplorerPtr Explorer)
{
  mExplorer = Explorer;  //mExplorer is a member field of TFileSystemExplorableImpl
  return S_OK;
}

STDMETHODIMP TFileSystemExplorableImpl::GetDescription (BSTR* Description)
{
  *Description = WideString ("File System (C++ Builder version)").Detach ();
  return S_OK;
}

Nothing complicated here. Let's do GetSubItems instead.

Remember, GetSubItems is called by Explorer to obtain an ISubItems interface to enumerate sub-items of any node in the hierarchy. For us, that means that we have to scan the folder (specified by the node/path) using the FindFirst and FindNext functions. Let's encapsulate this process in a TSubItemsImpl class:

#include <vector.h>

class ATL_NO_VTABLE TSubItemsImpl :
  public CComObjectRoot
{
protected:
  vector <String> mSubItems;
  void LoadDrives ();
  void LoadFiles (String &Path);
public:
  void Initialize (String &Path);
};

//load system drives into mSubItems list
void TSubItemsImpl::LoadDrives ()
{
  //implementation in pseudocode
  Find all drives;
  For each drive found
    Add drive name into mSubItems list;
}

//load folder subfiles (and subfolders) into mSubItems list
void TSubItemsImpl::LoadFiles (String &Path)
{
  //implementation in pseudocode
  Find all files (and folders) under Path (using FindFirst/FindNext)
  For each file (and folder) found
    Add file name into mSubItems list;
}

//loads file system subitems given a path
void TSubItemsImpl::Initialize (String &Path)
{
  //reset list
  mSubItems.clear ();
  //if path is root, load system drives else load path as folder
  if (Path == "")
    LoadDrives ();
  else
    LoadFiles (Path);
}

Constructing a TSubItemsImpl and calling Initialize takes care of loading the sub-files for a given path. The important method here is Initialize. In Initialize, if the path specified is blank, then we load the sub-items list with all the drives on your system, otherwise we simply do a FindFirst-FindNext file scan of the given path. 

Since FileSystemExplorable must hand out ISubItems to Explorer, we simply implement ISubItems into TSubItemsImpl:

class ATL_NO_VTABLE TSubItemsImpl :
  public CComObjectRoot,
  public ISubItems
{
protected:
  BEGIN_COM_MAP (TSubItemsImpl)
    COM_INTERFACE_ENTRY (ISubItems)
  END_COM_MAP ()

  //ISubItems methods
  STDMETHOD (GetCount) (long* Count);
  STDMETHOD (GetItem) (long Index, BSTR* Item);
...
};

//return sub-item count
STDMETHODIMP TSubItemsImpl::GetCount (long* Count)
{
  *Count = mSubItems.size ();
  return S_OK;
}

//return sub-item text by index
STDMETHODIMP TSubItemsImpl::GetItem (long Index, BSTR* Item)
{
  *Item = WideString (mSubItems [Index]).Detach ();
  return S_OK;
}

Finally, we can go back to TFileSystemExplorableImpl and implement GetSubItems:

STDMETHODIMP TFileSystemExplorableImpl::GetSubItems (BSTR Path,
  Explorer_tlb::ISubItemsPtr* SubItems)
{
  //initialize new subitems collection based on Path
  CComObject <TSubItemsImpl> *Items;
  CComObject <TSubItemsImpl>::CreateInstance (&Items);
  Items->AddRef ();
  Items->Initialize (String (Path));
  *SubItems = Items;
  return S_OK;
}

The next step is to implement GetProperties. If you recall, GetProperties is called by Explorer to obtain name-value pairs of information for any given node. More specifically, GetProperties returns an array of name-value pairs. In COM, we can simply implement this using a variant array. Thus:

STDMETHODIMP TFileSystemExplorableImpl::GetProperties (BSTR Path, TVariant* Properties)
{
  //Properties is a 2-dim array:
  //
  // | Property Name 1 | Property Value 1 |
  // | Property Name 2 | Property Value 2 |
  //

  Variant vProperties;
  vProperties = VarArrayCreate (OPENARRAY (int, (
    0, 4, //row bounds
    0, 1 //column bounds
    )),
    varOleStr //string elements
  );

  //Type: File or Folder?
  String FileOrFolder;
  vProperties.PutElement ("Type", 0, 0);
  if (IsFolder (FullPath))
    vProperties.PutElement ("Folder", 0, 1);
  else
    vProperties.PutElement ("File", 0, 1);

  //Name
  vProperties.PutElement ("Name", 1, 0);
  vProperties.PutElement (NameOfFile (Path), 1, 1);

  //Size
  vProperties.PutElement ("Size", 2, 0);
  vProperties.PutElement (IntToStr (SizeOfFile (Path)), 2, 1);

  //Date/Time
  vProperties.PutElement ("Date/Time", 3, 0);
  vProperties.PutElement (DateTimeToStr (DateTimeOfFile (Path)), 3, 1);

  //Attributes
  vProperties.PutElement ("Attributes", 4, 0);
  vProperties.PutElement (AttributesOfFile (Path), 4, 1);

  //transfer properties storage to output
  *Properties = vProperties;
  return S_OK;
}

What we're doing here is simply returning a 2-dim variant array (of 5 rows indexed from 0 to 4) consisting of the following file properties: Type, Name, Size, Date/Time, and Attributes. I'm not going to bore you with details of how to physically obtain each property value because that's not relevant - feel free to check the source code for details.

The last 2 methods we need to implement are GetMenuActions and DoMenuAction. If you recall, these methods are for the context sensitive custom operations that can be done on any node in your hierarchy. GetMenuActions returns an array of action-ID pairs to Explorer and DoMenuAction executes the user-selected action on a given node. 

Also, if you recall, we only wanted to implement a single menu action - Rename File. And we specifically want to rename files with only the Archive bit set. Thus:

const int ACTION_RENAME = 1;

STDMETHODIMP TFileSystemExplorableImpl::GetMenuActions (BSTR Path, TVariant* Actions)
{
  //this ActionCount business demonstrates how menu item actions can be dynamic
  //depending on context
  int ActionCount = 0;

  //Actions is a 2-dim array:
  //
  // | Action Name 1 | Action ID 1 |
  // | Action Name 2 | Action ID 2 |
  //
  Variant vActions;
  vActions = VarArrayCreate (OPENARRAY (int, (
    0, 0, //row bounds
    0, 1 //column bounds
    )),
    varVariant //variant elements
  );

  if (FileOnlyHasArchiveAttributeSet (Path))
  {
    //add Rename action
    vActions.PutElement ("Rename", ActionCount, 0);
    vActions.PutElement (ACTION_RENAME, ActionCount, 1);
    //one action in!
    ActionCount++;
  }

  //if Actions in context are available, return it to client
  VariantClear (*Actions);
  if (ActionCount > 0) *Actions = vActions;
  return S_OK;
}

As you can see, we're simply creating a 2-dim variant array to store the action-ID pairs. Since we're only interested in a rename action, we only have at most 1 row element in the array (hence the row bounds 0 to 0).

With this, whenever Explorer gets its action list, it will populate its context menu with it. After that, if the user selects the rename action, Explorer will then call DoMenuAction in our plug-in, passing in the ACTION_RENAME action ID constant. Thus, we can simply implement DoMenuAction to process the rename action as follows:

STDMETHODIMP TFileSystemExplorableImpl::DoMenuAction (BSTR Path, long ActionId)
{
  //case out ActionID
  switch (ActionId)
  {
    case ACTION_RENAME :
    {
      String NewName;
      //rename and notify host if renamed
      if (TfrmRename::Rename (String (Path), NewName))
        //notify explorer host after rename action
        mExplorer->RenamePath (Path, WideString (NewName).Detach ());
      break;
    }
  }
  return S_OK;
}

In here, we check the ActionID parameter and perform the corresponding action on the given path. For ACTION_RENAME, we simply run a form (TfrmRename) that takes care of presenting the user with a simple dialog box that allows the user to input a new name for the given file. On a successful rename, we then proceed to call mExplorer.RenamePath to tell our host Explorer that a certain node has changed its name. This is done so that Explorer can then accordingly update its UI to reflect the name change.

This is an example of how a plug-in may need to communicate back to its host. Normally, this process is done so that the plug-in and the host can both synchronize themselves to reflect state change in the data manipulated by both the plug-in and the host.

So much for IExplorable. The last thing we need to do is to somehow incorporate the code that registers and unregisters component categories into our plug-in server. The easiest way to do this is that whenever our plug-in gets registered, we also register the category information along with it, and whenever it gets unregistered, we also unregister the category information appropriately. For DLL servers, the registration entry points of interested are the exported functions: DLLRegisterServer (when registering) and DLLUnregisterServer (when unregistering). 

Let's first take care of registration:

In FileSystemPlugin.cpp

...

//overidden to include categories registration
STDAPI __export DllRegisterServer(void)
{
  HRESULT Result = _Module.RegisterServer(TRUE);
  //manually register category information
  //note: ATL 3 contains category map macros that will eliminate all this
  //hard-work. Unfortunately, BCB 4 is not on par with ATL 3 yet. :(
  RegisterAsExplorableClass (CLSID_FileSystemExplorable, true);  //true means register
  return Result;
}

...

In here, we're simply "overriding" DLLRegisterServer to also include our category information registration routine. RegisterAsExplorableClass is defined as follows:

//un/registers a class as explorable
void RegisterAsExplorableClass (REFCLSID clsid, bool Register)
{
  HRESULT hr = S_OK;
  ICatRegister *CatReg = NULL;
  hr = GetStdComponentCategoryMgr ((void**)&CatReg, IID_ICatRegister);
  if (SUCCEEDED (hr))
  {
    //initialize our category
    TCATEGORYINFO ExplorableCategoryInfo;
    InitializeExplorableCategoryInfo (ExplorableCategoryInfo);
    if (Register)
    {
      //first register CATID_Explorable category
      CatReg->RegisterCategories (1, &ExplorableCategoryInfo);
      //then register CLSID as supporting that category
      CatReg->RegisterClassImplCategories (clsid, 1, &ExplorableCategoryInfo);
    }
    else
    {
      //perform unregistration here
    }
    CatReg->Release ();
  }
}

//returns standard component category manager
HRESULT GetStdComponentCategoryMgr (void **ppv, REFIID iid = IID_IUnknown)
{
  //create standard COM component category manager
  return CoCreateInstance (CLSID_StdComponentCategoryMgr, NULL, CLSCTX_ALL,
    iid, ppv);
}

void InitializeExplorableCategoryInfo (TCATEGORYINFO &ExplorableCategoryInfo)
{
  //fill category info record 
  ExplorableCategoryInfo.catid = CATID_Explorable;
  ExplorableCategoryInfo.lcid = LOCALE_SYSTEM_DEFAULT;
  StringToWideChar (
    ExplorableCategoryDescription,
    ExplorableCategoryInfo.szDescription,
    sizeof (ExplorableCategoryInfo.szDescription) / sizeof (ExplorableCategoryInfo.szDescription [0]) 
  );
}

What we're doing here is simply registering the "Explorable Plugins" category and then registering the FileSystemExplorable coclass as implementing that category. We've discussed these details before so there shouldn't be anything new here.

For the unregistration part, we simply hook DLLUnRegisterServer and reverse what we've done in DLLRegisterServer. Thus:

In FileSystemPlugin.cpp

...

//overidden to include categories unregistration
STDAPI __export DllUnregisterServer(void)
{
  //manually unregister category information
  //note: ATL 3 contains category map macros that will eliminate all this
  //hard-work. Unfortunately, BCB 4 is not on par with ATL 3 yet. :(
  RegisterAsExplorableClass (CLSID_FileSystemExplorable, false);  //false means unregister
  return _Module.UnregisterServer();
}

...

Accordingly, the unregister part in RegisterAsExplorableClass is defined as:

//un/registers a class as explorable
void RegisterAsExplorableClass (REFCLSID clsid, bool Register)
{
  HRESULT hr = S_OK;
  ICatRegister *CatReg = NULL;
  hr = GetStdComponentCategoryMgr ((void**)&CatReg, IID_ICatRegister);
  if (SUCCEEDED (hr))
  {
    //initialize our category
    TCATEGORYINFO ExplorableCategoryInfo;
    InitializeExplorableCategoryInfo (ExplorableCategoryInfo);
    if (Register)
    {
      //perform registration here
    }
    else
    {
      //note that we never unregister CATID_Explorable because other servers might
      //still use it!

      //unregister CLSID as supporting that category
      CatReg->UnRegisterClassImplCategories (clsid, 1, &ExplorableCategoryInfo);
      DeleteRegKey ("CLSID\\" + GUIDToString (clsid) + "\\" + "Implemented Categories");
    }
    CatReg->Release ();
  }
}

Note that we do an extra DeleteRegKey call to remove the "Implemented Categories" subkey to completely remove our coclass from the registry.

In ATL 3, another way to perform registration of the category information is to use the component category map macros (BEGIN_CATEGORY_MAP, etc.). However CBuilder 4 doesn't support ATL 3 yet so we'll have to make do with the hard way.

On a different note, the COM component category constants and interfaces are supposed to be defined in comcat.h. However, by CBuilder incorporating Delphi's ActiveX.hpp module, the CBuilder compiler generates a conflict between same named interfaces in both comcat.h and ActiveX.hpp. What I ended up doing was totally getting rid of comcat.h and just using ActiveX.hpp. However, there were a few snags in doing this because ActiveX.hpp really came from Delphi so we're forced to use some Delphisms to compensate. You'll see what I mean when you take a closer look at the source code.

Implementing the Explorer Host

With the plug-in in place (oops, don't forget to register your plug-in), it's now time to look at the Explorer host. As I mentioned earlier, our Explorer host is an MDI application wherein each MDI child window hosts an explorable plug-in. To do this, when Explorer loads up, it finds all registered explorable plug-ins and adds them all to a menu list. Whenever the user selects a particular plug-in from the menu, we instantiate an MDI child form (TfrmExplorerHost) and host the selected plug-in into that form.

Figure: "Explorable Plugins" menu list containing a list of registered plug-ins

To obtain a list of explorable plug-ins, we simply use COM's component category facilities as described earlier. In particular, we use ICatInformation to obtain all registered coclasses that implement the "Explorable Plugins" category. Thus:

void TfrmMain::LoadExplorableClasses ()
{
  //get explorable server list
  int Count = GetExplorableClasses (mExplorableClasses);
  if (Count > 0)
  {
    WideString Description;
    //get description for each and populate Explore >> submenu
    for (int i = 0; i < Count; i++)
    {
      //create Explorable plugin
      IExplorablePtr Explorable;
      Explorable.CreateInstance (mExplorableClasses [i], NULL, CLSCTX_ALL);
      //get plugin description
      Explorable->GetDescription (&Description);
      //create new menu item
      TMenuItem *MenuItem = new TMenuItem (this);
      MenuItem->Caption = Description;
      //hide mExplorableClasses array index in Tag
      MenuItem->Tag = i;
      //add to Explore >> submenu
      miExplore->Add (MenuItem);
    }
  }
}

What's happening here is that we're loading all explorable coclass CLSIDs into the mExplorableClasses array. Then we iterate the array and load each plug-in's description into a menu list.

Here's what GetExplorableClasses really looks like:

//explorable classes storage array
typedef CLSID TExplorableClasses [50]; //50 is arbitrary!

//returns Explorable server CLSIDs
int GetExplorableClasses (TExplorableClasses &ExplorableClasses)
{
  int Result = 0;

  //get standard category information manager
  HRESULT hr = S_OK;
  ICatInformation *CatInfo = NULL;
  hr = GetStdComponentCategoryMgr ((void**)&CatInfo, IID_ICatInformation);
  if (SUCCEEDED (hr))
  {
    //get an enumeration of all registered Explorable classes
    //this _di_IEnumGUID thingy is part of Delphi/ActiveX.hpp workaround
    _di_IEnumGUID Enum = NULL;
    //initialize our category
    TCATEGORYINFO ExplorableCategoryInfo;
    InitializeExplorableCategoryInfo (ExplorableCategoryInfo);
    hr = CatInfo->EnumClassesOfCategories (1, &ExplorableCategoryInfo, 0, NULL, Enum);
    if (SUCCEEDED (hr))
    {
      //enumerate Explorable classes into ExplorableClasses array
      hr = Enum->Reset ();
      if (SUCCEEDED (hr))
      {
        //fill ExplorableClasses array
        //note that if Fetched >= ExplorableClassesMax, then we may have more!
        //for our purposes, this sample code will do
        int ExplorableClassesMax = sizeof (ExplorableClasses) / sizeof (ExplorableClasses [0]);
        UINT Fetched;
        hr = Enum->Next (ExplorableClassesMax, ExplorableClasses [0], Fetched);
        //save count
        if (SUCCEEDED (hr)) Result = Fetched;
      }
    }
    CatInfo->Release ();
  }
  //return registered plugin count
  return Result;
}

As we've learned earlier, ICatInformation.EnumClassesOfCategories is used to obtain all coclasses that implement a particular category. Then we use the enumerator result, Enum, to iterate matching CLSIDs and populate the ExplorableClasses array accordingly.

So far, we're able to populate a menu list of explorable plug-ins. Now we have to determine what to do when the user selects a plug-in from the list. Thus:

//called when user selects a plug-in from the explorable plug-ins menu
void __fastcall TfrmMain::miExplorableItemClick(TObject *Sender)
{
  //get selected Explorable class
  //Tag contains index into FExplorableClasses array - this is established in
  //proc LoadExplorableClasses
  CLSID ExplorableClass = mExplorableClasses [(dynamic_cast <TMenuItem*>(Sender))->Tag];
  //create Explorable plugin
  IExplorablePtr Explorable;
  Explorable.CreateInstance (ExplorableClass, NULL, CLSCTX_ALL);
  //load Explorable server into new explorer host form
  TfrmExplorerHost::Load (Explorable);
}

What we're doing here is instantiating the correct plug-in coclass based on the selected menu item. Then we load the new plug-in into the explorer host form.

The explorer host form simply consists of 2 panes: a left tree view (tvwExplorer) and a right list view (lvwProperties). The tree view displays the explorable plug-in's hierarchy while the list view displays the properties whenever a node is focused/selected in the tree view. In other words, for our FileSystemPlugin, the tree view displays your file system hierarchy and the list view displays file property information (Name, Type, Size, etc.) for the currently selected file/folder in the tree view:

Figure: Tree view and list view panes of the Explorer host form

Let's see what we can do in the tree view. First, we need to be able to expand nodes and display a node's children (sub-items) correspondingly. Recall that we had to implement the IExplorable.GetSubItems method for this. This is how Explorer would now make use of GetSubItems:

void __fastcall TfrmExplorerHost::tvwExplorerExpanding(TObject *Sender,
  TTreeNode *Node, bool &AllowExpansion)
{
  //clear children then repopulate
  ClearNodeChildren (Node);
  ExpandNode (Node);
}

void TfrmExplorerHost::ExpandNode (TTreeNode *Node)
{
  //get subitems
  ISubItemsPtr SubItems;
  HRESULT hr = mExplorable->GetSubItems (
    WideString (NodePath (Node)), (ISubItemsPtr*)&SubItems);
  if (SUCCEEDED (hr) && SubItems.IsBound ())
  {
    //load subitems
    long Count;
    SubItems->GetCount (&Count);
    for (int i = 0; i <= Count - 1; i++)
    {
      WideString ItemText;
      SubItems->GetItem (i, &ItemText);
      TTreeNode *ChildNode = tvwExplorer->Items->AddChild (Node, ItemText);
    }
  }
}

In ExpandNode, we first call GetSubItems to obtain an ISubItems list. We then iterate ISubItems and for each element we find, we add it as a child node underneath the current node.

When we click/select a node in the tree view, we also want to display the node's properties in the list view pane. Thus:

void __fastcall TfrmExplorerHost::tvwExplorerChange(TObject *Sender,
  TTreeNode *Node)
{
  LoadNodeProperties (Node);
}

void TfrmExplorerHost::LoadNodeProperties(TTreeNode *Node)
{
  lvwProperties->Items->BeginUpdate ();
  lvwProperties->Items->Clear ();
  lvwProperties->Items->EndUpdate ();
  //get properties
  //Properties is a 2-dim array
  //
  // | Property Name 1 | Property Value 1 |
  // | Property Name 2 | Property Value 2 |
  //
  TVariant vProperties;
  HRESULT hr = mExplorable->GetProperties (WideString (NodePath (Node)),
    &vProperties);
  if (SUCCEEDED (hr))
  {
    //load properties
    Variant Properties = vProperties;
    if (!VarIsEmpty (Properties))
    {
      //assume row-dimension is 0-based
      int Count = VarArrayHighBound (Properties, 1) + 1;
      for (int i = 0; i <= Count - 1; i++)
      {
        //skip uninitialized properties
        if (VarIsEmpty (Properties.GetElement (i, 0))) continue;
        //add new property
        TListItem *ListItem = lvwProperties->Items->Add ();
        //get property name
        ListItem->Caption = Properties.GetElement (i, 0);
        //get property value
        ListItem->SubItems->Add (Properties.GetElement (i, 1));
      }
    }
  }
}

There's nothing hard here. We simply call GetProperties to retrieve the properties array for the selected node. Then we populate the list view based on the array.

Finally, we implement menu actions. On a right-mouse-click, we first build a list of context sensitive menu actions based on the current node. This is accomplished by calling IExplorable.GetMenuActions. Thus:

void __fastcall TfrmExplorerHost::tvwExplorerMouseDown(TObject *Sender,
  TMouseButton Button, TShiftState Shift, int X, int Y)
{
  //respond to right mouse click
  if (Button == mbRight)
  {
    //select node where mouse was right clicked
    TTreeNode *Node = tvwExplorer->GetNodeAt (X, Y);
    if (Node == NULL) return;
    tvwExplorer->Selected = Node;
    //initialize popup menu
    if (InitializePopupMenu (Node))
    {
      //show popup menu
      TPoint pt = ClientToScreen (Point (X, Y));
      PopupMenu->Popup (pt.x, pt.y);
    }
  }
}

bool TfrmExplorerHost::InitializePopupMenu (TTreeNode *Node)
{
  //get menu actions
  //Actions is a 2-dim array:
  //
  // | Action Description 1 | Action ID 1 |
  // | Action Description 2 | Action ID 2 |
  //
  TVariant vActions;
  HRESULT hr = mExplorable->GetMenuActions (WideString (NodePath (Node)), &vActions);
  //just don't do anything if not successful
  if (SUCCEEDED (hr))
  {
    //add items
    Variant Actions = vActions;
    if (!VarIsEmpty (Actions))
    {
      //assume row-dimension is 0-based
      int Count = VarArrayHighBound (Actions, 1) + 1;
      for (int i = 0; i <= Count - 1; i++)
      {
        //skip uninitialized actions
        if (VarIsEmpty (Actions.GetElement (i, 0))) continue;
        //add menu item
        TMenuItem *MenuItem = new TMenuItem (this);
        MenuItem->Caption = Actions.GetElement (i, 0);
        //hide ActionID in Tag
        MenuItem->Tag = Actions.GetElement (i, 1);
        //add to popup menu
        PopupMenu->Items->Add (MenuItem);
      }
    }
  }
  //a-ok if popup menu has items
  return (PopupMenu->Items->Count > 0);
}

In here, we simply call GetMenuActions to obtain the menu actions array for the selected node. Then we populate the context menu based on the array. Note how we assign the ActionID into the Tag property of each menu item. We'll use that to determine which action ID to use when a user selects an action from the menu. Thus:

void __fastcall TfrmExplorerHost::miExplorerNodeActionClick(
  TObject *Sender)
{
  //get ActionID from menu item Tag - this was set proc InitializePopupMenu
  int ActionID = dynamic_cast <TMenuItem*>(Sender)->Tag;
  //invoke ActionID
  mExplorable->DoMenuAction (
    WideString (NodePath (tvwExplorer->Selected)), ActionID);
}

Here, we obtain the action ID back from the Tag property. Then we invoke the plug-in's DoMenuAction method passing in the current selected node and the action ID. 

The one last thing I haven't shown you is the host's (IExplorer) implementation of RenamePath. Recall that RenamePath is called by the plug-in to notify Explorer after a name change in a node. For our Explorer host form, we simply refresh the tree view node caption with the new name, as well as the corresponding properties list for that node (the properties list needs to be refreshed because it includes the name). Thus:

STDMETHODIMP TfrmExplorerHost::RenamePath (BSTR OldPath,
  BSTR NewPath)
{
  //find terminal node that corresponds to OldPath
  TTreeNode *Node = FullPathToNode (RootNode (), String (OldPath));
  if (Node != NULL)
  {
    //change node text to NewPath terminal node name
    Node->Text = ExtractLastNodeName (String (NewPath));
    //reload node properties
    LoadNodeProperties (Node);
  }
  return S_OK;
}

I won't go into the details of the FullPathToNode and ExtractLastNodeName functions because they're not very relevant - feel free to inspect the source code for more details.

Note that in the source code, I actually implemented IExplorer in a separate class, TExplorerHost, contained inside of TfrmExplorerHost. This is because of multiple inheritance limitations in the VCL. 

If you inspect the source, you'll notice that TExplorerHost is an ATL-based class. Creating ATL classes in a client application may not be as simple as you think. You'll need to create a dummy TComModule class and assign it to the ATL global _Module variable, and a dummy object map (BEGIN_OBJECT_MAP, END_OBJECT_MAP).  You'll also need to include ATL modules such as atlmod.h and atlimpl.h. You can verify all this in CBuilderClient.cpp. In addition, I also had to include at least the "USING_ATL" conditional define (under Project | Options | Directories/Conditionals) to enable usage of the ATL files.

Conclusion

In this tutorial, I've shown you a very simple implementation of a plug-in framework. We've learned some important concepts like component categories, how to design plug-in interfaces, and how to implement a host and a plug-in. Plug-in frameworks are very useful for developing highly modularized and customizable applications that need to evolve through time. I hope you've learned enough of the basic concepts to start thinking about how you can apply this into your own domains.

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.