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.