[GregorView]

A .NET-implemented Shell Namespace Extension

Saturday, July 26, 2003

GregorView is a hybrid COM/.NET DLL that functions a Shell Namespace Extension intermediary. It must be registered, and implements the the basic Shell interfaces, such as IShellFolder and IShellFolderView. However, it does not provide any data (that is, folder and file objects) or display details (such as the view in the Explorer's right pane or icons etc.). That is the task of a .NET managed class library.

Note: GregorView is still in the experimental stages, so don't use it in a production system.

Highlights

Isolating the COM plumbing

A typical Namespace Extension interfaces with the Shell and provides the data and display options as well. GregorView, by contrast, is restricted to wrapping up the COM/Shell wiring, while the more interesting parts (i.e., the data) come from a different DLL (the identity and location of which is configurable in the registry).

One COM DLL, many extensions

A second major characteristic is GregorView.dll's genericity. When a Namespace Extension is registered, the COM class ID of a class that implements IShellFolder must be specified. That class must be registered, and COM will load the DLL, and ask for what it calls its "class factory". The class factory will then instantiate an object of that class (which is the extension's root folder). Now, an ordinary Extension DLL deals with shell folder objects of one given Class (that is, what COM calls a "CoClass", identified by a CLSID) only.

GregorView instead uses the same implementation of the infamous IPersistFolder and IShellFolder interfaces for any class ID, and will happily instantiate the C++ class (which is called GregorView::Internal::CGregorViewShellFolder), provided that the given COM class ID is registered, and there is sufficient information about the managed implementation DLL under its registry key (fully validating the class ID is ensured by means of .NET reflection against the managed DLL). In other words, GregorView's IShellFolder implementation (one unmanaged C++ class) appears to COM as several COM classes. This is possible because COM neither wants nor needs to know much about classes (except that a class of a given ID implements a given interface). Every instance of the folder class will save a copy of the official COM class ID. The DLL itself has no hard-coded class ID information whatsoever.

Installation

The COM GregorView.dll, and the sample .NET DLL, LisaView.dll, can be copied to any suitable location.

Registration

The downloads has .reg files, which need to be double-clicked. But with frequent registry file format changes, this might not work on all operating systems and versions. The following describes the registry keys and values for manual registration:

Registering the COM class ID

Each managed implementation (.NET DLL) must be assigned a class ID. This class ID's key groups both information about the COM DLL (GregorView.dll), as well as the .NET class library. The class ID corresponds to the .NET DLL; the COM DLL is the same for each pair of class ID and .NET DLL. The example, LisaView, registers like this:

HKEY_CLASSES_ROOT
 CLSID
  {0E54A2CA-11B9-4b1a-BFC6-B2F3A213F6D1}
                        (Default)        "Lisa View"
   DefaultIcon
                        (Default)        "<path>\SomeIco.ico"
   InprocServer32
                        (Default)        "<path>\GregorView.dll"
                        ThreadingModel   "Apartment"
   ManagedImplementation
                        (Default)        "<path>\LisaView.dll"
   ProgID
                        (Default)        "LisaView.LisaView"
   ShellFolder
                        Attributes       a0000004 (2684354564)

Other managed implementations would register the same InprocServer32, but specify a different managed DLL (and of course, a different icon etc.).

The "Attributes" value under the "ShellFolder" subkey controls the behaviour and appearance of the namespace extension's root folder (something that is normally done by calling IShellFolder::GetAttributesOf, but is impossible for an extension's root because of the implementation boundary). The values here indicate that it is a folder, and that delete operations result in merely hiding the folder. For details, see the SFGAO_* constants.

The "ProgID" subkey pairs up with the "LisaView.LisaView\Clsid" key (under classes_root). It's optional, but useful for testing outside the Shell's Desktop process:

HKEY_CLASSES_ROOT
 LisaView.LisaView
  Clsid
                        (Default)        "{0E54A2CA-11B9-4b1a-BFC6-B2F3A213F6D1}"

Choosing a junction point

This tells the Shell where to find the Namespace Extension, and how to instantiate an object whose class implements IShellFolder. In this case, the Desktop folder is used. Parallel to the class ID you'll find other virtual folders living on the Desktop:

HKEY_LOCAL_MACHINE
 Software
  Microsoft
   Windows
    CurrentVersion
     Explorer
      Desktop
       NameSpace
        {0E54A2CA-11B9-4b1a-BFC6-B2F3A213F6D1}
                       (Default)         "Lisa View"

The code

Key places

How the Shell gets the root folder

Every COM DLL must have an entry point for getting the class factory. It is passed the class ID for the objects to instantiate, as well as the interface ID for the IClassFactory interface:

// GregorView.cpp
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID * ppv)
{
    // valicate requested interface
    if(riid != IID_IClassFactory
    || ppv == NULL){
        return E_INVALIDARG;
    }

    // reset out parameter
    *ppv = NULL;

    // make sure extension point exists or can be created (this will verify the class ID)
    CGregorViewExtensionPoint * pExPoint = GetExtensionPoints().GetOrCreate(rclsid);
    if(pExPoint == NULL){
        // the class is not registered, or we couln't instantiate the extension point
        return E_INVALIDARG; 
    }

    // create class factory
    CGregorViewClassFactory * pFactory = new CGregorViewClassFactory(rclsid);
    if(pFactory == NULL){
        return E_OUTOFMEMORY;
    }

    // set out parameter
    pFactory->AddRef();
    *ppv = (void*)pFactory;

    // return success
    return S_OK;

} // DllGetClassObject

The bridge to the managed world

GregorView.dll grabs the file path to the .NET dll from the registry, and then looks for a subclass of GregorView::Managed::CConnector. The connector object is able to instantiate shell folders:

// CGregorViewExtensionPoint.cpp
CConnector * CGregorViewExtensionPoint::GetConnector(void)
{
    if(m_pConnector == NULL){
        try{

            // convert assembly path to System::String
            String * ps = CStringToSysString(m_szManagedDllPath);

            // load assembly
            Assembly * pAssembly = Assembly::LoadFrom(ps);

            // walk types, find connector
            Type * pTypes[] = pAssembly->GetTypes();
            Type * pConnectorBase = __typeof(CConnector);
            for(int i = 0; i < pTypes->Length; i++){
    	    	      Type * pType = pTypes[0];
    	    	      if(pType->IsSubclassOf(pConnectorBase)){
    	    	          // instantiate
    	    	          Object * pObj = Activator::CreateInstance(pType);
    	    	          if(pObj != NULL){
    	    	    	         m_pConnector = __try_cast<CConnector*>(pObj);
    	    	    	         break;
    	    	          }
    	    	      }
    	       }
    	   }catch(Exception * pExc){
    	       (void)pExc;
        }
    }

    // done
    return m_pConnector;

} // GetConnector

Managing item IDs

The infamous "pidls" are wrapped up in the CGregorViewItemId class. It contains key information, which consists of an embedded string for the item name. Instantiating single-level ID lists follows regular C++ syntax, using an overloaded operator new (this is intended to follow the Shell's rule of using the IShellMalloc interface for all allocations and deallocations).

Shots

OK, I couldn't resist.

Since GregorView.dll is both a COM and a .NET DLL, it's viewable in Dependency Walker, in .NET object browsers, such as the IDE that comes with the Scripting component (of Gregor.NET), as well as in COM object browsers, such as OLE View.

Here's a picture showing the managed implementation's coding in WebEdit.NET. Registration is low-level labour, as expected.

Debugging in the Shell is fun - I use to simply attach a running Explorer process. Here's the code for some test data, and the corresponding folder structure. The view object simplistically looks like this.