Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An almost complete Namespace Extension Sample

0.00/5 (No votes)
12 Aug 2004 1  
An implementation of a shell namespace extension that uses the system provided ShellView (SHCreateShellFolderView)

Sample Image - NamespaceExtImpl_FileDialog.jpg

Contents

Introduction

This article shows you an almost complete implementation of a Shell Namespace Extension. It is the result of nights passed to achieve two goals:

  • Use the system provided ShellView object.
  • Be able to use this extension in the FileDialog (CommonDialog).
The first goal is an answer to the article by Henk Devos 'Namespace extensions - the undocumented Windows Shell'. Several users requested a demo project for his article, here it is!

The second goal was the spec I had to implement. This sample will in fact act as a Favorites 'shortcut' system for the DirectoryOpus file manager (well, far more than a file manager, see DOpus).

Before going on with this article, you should read Michael Dunn's 'The Complete Idiot's Guide to Writing Namespace Extensions - Part I ', which is the base I used to realize this one.

Some more infos are available in Microsoft Knowledge Base Article - 216954, named "HOWTO: Support Common Dialog Browsing in a Shell Namespace Extension".

Requirements

System

Because this extension makes use of the SHCreateShellFolderView function, you must have IE 4.0 minimum installed on your system. Note that for WinNT4 (and also Win95 if applicable) Active Desktop (ships with IE 4) has to be installed even if not enabled.

I tested it on several Windows platforms, here is the list with some comments:

OS version Shell version Comment
Win98 4.7x - The removal message (@%MODULE%,-300) is not supported
- Didn't test Office FileDialog
WinNT4 SP6 4.7x - The removal message (@%MODULE%,-300) is not supported
- Didn't test Office FileDialog
Win2K 5.0 Okay, OfficeXP FileDialog works ok
WinXP Pro 6.0 Okay, OfficeXP FileDialog (through MSDev 7) works ok

Since this extension works well on the previously listed platforms, it should also for these (but I didn't test them):

  • WinME
  • WinXP Home
  • Server edition of NT4, W2K, W2003
I didn't care about Windows 95, at the time I write this article (2004 A.D.) I consider this OS as obsolete. Note though that it should work because the dependencies are highly related to Internet Explorer and not really to the OS.

Development Environement

The environement I used to create this sample is MSDev 6.0 with ATL 3.0. I also successfully compiled it (though not the MinSize builds) under MSDev 7.0 with ATL 7.0.

You must have the Microsoft Platform SDK, I used the one from February 2003. This is important because I use recently documented API. So if you have an old SDK (for example shipped with MSDev) it will not compile.

What will this extension do?

This extension will act as a Favorites shortcut system. Unlike the system Favorites folder, I do not want to use a .lnk file for each shortcut (which is a text file representing the shortcut). The shortcuts I want are stored in the Registry. As told in the intro of this article, the shortcuts are the one stored by DirectoryOpus.

What composes a shortcut?

From now on, I will use the term Favorite instead of shortcut, so of what a Favorite is made?
  • A name (example: "_/# Fancy Temp Dir #\_")
  • A path (example: "E:\Temp")
  • A rank
The rank determines the position in the Favorites List. The lower value, the upper in the list.

You certainly noticed that the name can contain any chars, including the one that are not allowed in paths.

Wanted behaviour

The root view of my namespace has to show the Favorites, their names, and if in 'Details' mode, the path and the rank.

The behaviour when the user selects one of the Favorite is to go to the real path of the item. This is the only behaviour needed, what the user makes after this does not concern us, but the target path namespace.

This sounds like it should be simple and stupid to implement. Aaaah, but like always, there are ten ways to do the same thing with computers, and different programs (read different MS teams) will use all of them and maybe one or two more.

How is it implemented?

Okay, let's go to the real work. What should we implement to achieve the wanted behaviour? Valuable informations lie in the Platform SDK, check it for details.

PIDL layout

First of all, we must choose a PIDL layout. What are our items? They are Favorites. What is a Favorite composed of? A name, a path and a rank. I choosed to embed all these infos in the PIDL. For this I have a class CPidlMgr that will do general PIDL handling, and a CDataFavo class that handles embedded info.

Example to get the Path from a PIDL:

    LPOLESTR pOleStr = CDataFavo::GetPath( pidl );

See that string type: LPOLESTR, all string info are stored as UNICODE strings in my PIDL, even for ANSI builds. This is a design choice, read the SDK to know why not use TCHAR strings.

The CDataFavo class inherits from CPidlData which is used by CPidlMgr to create new PIDLs. To create a new PIDL, first create a CDataFavo object and then populate it with the SetXXX() methods. Then use CPidlMgr::Create() to get the PIDL. Example:

    LPITEMIDLIST pidl;
    
    CDataFavo Favo;
    Favo.SetName(_T("_/# Fancy Temp Dir #\_"));
    Favo.SetPath(_T("E:\\Temp"));
    Favo.SetRank(3);
    
    pidl = m_PidlMgr.Create(Favo);

Use cases for Explorer

I'll describe the control flow of the extension with use cases. I'll not go into deep explanations for all these use cases, please read the code, trace it, modify it. If you stil miss something, ask it on the message board.

You will note that there are differences between the Explorer behaviour and the FileDialog behaviour.

I assume you have an explorer with the TreeView (at the left) enabled. I'll call the right view (where you see the items) the ShellView.

The first thing Explorer or any other controler will do is call our IPersistFolder::Initialize() method. As described in Michael Dunn's article, we simply save here the passed pidl which is the position of our extension in the system namespace.

Then the user will do one of the followings:

Clicking on the Namespace icon in the TreeView

So what happens when you click on the namespace icon in the TreeView? Another method to have exactly the same behaviour is when you double-click the namespace icon in the ShellView (when the Desktop content is shown).

Explorer will want to show the namespace items, for this it calls IShellFoler::CreateViewObject() requesting IShellView. Here I create the view as requested. Because it is the system view (see Shell View) several methods of IShellFolder will be called to populate the view.

Clicking the plus sign of the Namespace icon in the TreeView

Explorer will call this sequence:

  • EnumObjects() once
  • CompareIDs() several times
  • GetAttributesOf() for each item
  • GetDisplayNameOf() for each item
  • GetUIObjectOf() for each item to get IExtractIcon

The result is the tree that gets expanded with our items (if they are SFGAO_FOLDER), showing their names. Note that at this stage the ShellView still displays another directory.

Clicking a favorite item in the TreeView

Now if the user clicks on one of our item in the tree, BindToObject() is called passing the PIDL of the item. What we want is explorer to display the target path content in the ShellView.

We have the target path (in our pidl) and we must BindToObject() to this target path. So we create an absolue pidl (from the Desktop) and pass it to BindToObject(). Here it what it looks:

    HRESULT hr;
    CComPtr<IShellFolder> DesktopPtr;

    hr = SHGetDesktopFolder(&DesktopPtr);
    if (FAILED(hr))
        return hr;

    LPITEMIDLIST pidlLocal;
    hr = DesktopPtr->ParseDisplayName(NULL, pbcReserved, 
       CDataFavo::GetPath(pidl), NULL, &pidlLocal, NULL);
    if (FAILED(hr))
        return hr;

    hr = DesktopPtr->BindToObject(pidlLocal, pbcReserved, riid, ppvOut);

    ILFree(pidlLocal);
    return hr;

Double-clicking a favorite item in the ShellView

Although the behaviour should be the same as clicking a favorite item in the TreeView, explorer does it in another fashion.

In fact, double-clicking an item in the ShellView corresponds to invoking the default entry of the item's context menu. When exploring folders, the default entry is "Explore", this is why the behaviour is the same. Note that you can implement a context menu with a different default entry and thus changing the 'double-click' behaviour.

This means that explorer will call our IShellFolder::GetUIObjectOf() method, requesting IContextMenu. In our case we do not need to implement IContextMenu, what we'll do is simply delegate this call to the target path.

To see how this can be done, read the next paragraph.

Right-clicking a favorite item in the ShellView

Here a context menu has to be displayed. This menu is related to the selected item. If several items are selected, the menu has to display entries that applies to all of the selected items.

For my extension, I choosed to display the context menu of the target path, so I don't have to implement one myself. I also only handle single selected items, this is because each item can point to a different storage. So, I can't easily delegate (that means without tons of code) the context menu to different storages at the same time.

Explorer will call our IShellFolder::GetUIObjectOf() method. To delegate it to the target path we have to get a IShellFolder to the parent of the target path in order to call its GetUIObjectOf() with the single-item pidl.

To achieve this, we must first convert the target path to an absolute pidl, this is done like that:

    hr = SHGetDesktopFolder(&DesktopPtr);
    if (FAILED(hr))
        return hr;

    LPITEMIDLIST pidlLocal;
    hr = DesktopPtr->ParseDisplayName(NULL, NULL, 
       CDataFavo::GetPath(*pPidl), NULL, &pidlLocal, NULL);
    if (FAILED(hr))
        return hr;

Now pidlLocal contains the absolute pild to the target path. We must now get the parent IShellFolder, this could be done with SHBindToParent(), but this function is only available from Shell version 5.0, so here is an equivalent code:

    LPITEMIDLIST pidlRelative;

    // pidlTmp will point to the single-item pidl of the target path

    LPITEMIDLIST pidlTmp = ILFindLastID(pidlLocal);

    // Now strips the last part of the pidl, to have the pidl of the parent

    pidlRelative = ILClone(pidlTmp);
    ILRemoveLastID(pidlLocal);

    // We can now get the parent IShellFolder

    hr = DesktopPtr->BindToObject(pidlLocal, NULL, 
          IID_IShellFolder, (void**)&TargetParentShellFolderPtr);
    ILFree(pidlLocal);
    if (FAILED(hr))
    {
        ILFree(pidlRelative);
        return hr;
    }

TargetParentShellFolderPtr has now the parent IShellFolder so that we can call its GetUIObjectOf method with the single-item pidl which is in pidlTmp.

    hr = TargetParentShellFolderPtr->GetUIObjectOf(hwndOwner, 1, 
        (LPCITEMIDLIST*)&pidlRelative,
        riid, puReserved, ppvReturn);
The redirection is made, if any of the previous function fails the context menu is simply not shown. This can happen when the target path doesn't exist or is inaccessible (network).

Use cases for the FileDialog

The FileDialog which is the one from Common Dialogs, behaves a little different than explorer.

First the namespace icon has to be displayed in the upper ComboBox, for that we must register our extension with the following flags: SFGAO_FILESYSANCESTOR, SFGAO_FILESYSTEM, SFGAO_FOLDER, SFGAO_BROWSABLE. This correspond to the Attributes value in the registry under the ShellFolder key, see the project .rgs file.

I don't remember exactly at which condition (OS, shell version, use case) but not implementing IShellFolder2 can lead to problems, so implement it. The only added method is GetCurFolder() which simply return a copy of the pidl passed to IShellFolder::Initialize().

Choosing the Namespace icon in the upper ComboBox

The FileDialog will simply call IShellFolder::CreateViewObject(), because we use the system ShellView several methods of IShellFolder will be called in response to creating the view object. They are (in no particular order):

  • EnumObjects()
  • CompareIDs()
  • GetAttributesOf()
  • GetDisplayNameOf()
  • GetUIObjectOf()

Double-clicking a favorite item in the View

For a reason I don't really understand, the FileDialog will not simply call IShellFolder::BindToObject() with the item pidl.

It first callsIShellFolder::GetUIObjectOf() requesting a IDataObject. Looking at this interface in the SDK informs us that it is used to exchange any form of data between modules. It is used for clipboard, drag and drop and the like.

So how are we concerned about this? The FileDialog will use a IDataObject as a container of the item pidl.

The called sequence for this use case is (removing calls to GetAttributesOf() and GetDisplayNameOf()):

  • GetUIObjectOf()
  • BindToObject()
The only method of IDataObject that is called by the FileDialog is GetData(). The purpose is to get the item pidl. All other methods can simply return E_NOTIMPL.

Let's take a look at it:

STDMETHODIMP CDataObject::GetData(LPFORMATETC pFE, LPSTGMEDIUM pStgMedium)
{
    // Is the caller requesting a pidl?

    if (pFE->cfFormat == m_cfShellIDList)
    {
        // Return the item pidl in the form of a CIDA structure

        pStgMedium->hGlobal = CreateShellIDList(m_pidlParent, 
           (LPCITEMIDLIST*)&m_pidl, 1);

        if (pStgMedium->hGlobal)
        {
            pStgMedium->tymed = TYMED_HGLOBAL;

            // Even if our tymed is HGLOBAL, WinXP calls ReleaseStgMedium()

            // which tries to call pUnkForRelease->Release() : BANG! 

            // (if not NULL)

            pStgMedium->pUnkForRelease = NULL;
            return S_OK;
        }
    }

    return E_INVALIDARG;
}

When GetUIObjectOf() is called, we create a IDataObject object and set its m_pidlParent and m_pidl with the item pidl,GetData() simply return them in a CIDA structure which is done by CreateShellIDList().

Beware of the line pStgMedium->pUnkForRelease = NULL;, as described in the comment WinXP could crash if you omit it.

Right-clicking a favorite item in the ShellView

Like in Explorer, this will show a context menu. The behaviour is the same as for explorer (see Right-clicking a favorite item in the ShellView) but before calling GetUIObjectOf(), it also gets the item pidl through IDataObject, like described above.

Problems with Office FileDialog

Yes, the Office FileDialog is not the one from CommonDialog, it is a modified one.
Note that MSDev 7.x (.net) also has this modified FileDialog.

It has two drawbacks:

FileSystem existance check

The first drawback is that it will check every folder that you are browsing, and complain if it is not a valid File System folder (a valid path).

It gets the folder path by calling our IShellFolder::GetDisplayNameOf() method with the SHGDN_FORPARSING flag. For virtual folders (like ours) the SDK states that in response to this call we should return the namespace extension GUID preceded by double semi-colons, like this: "::{GUID}". Of course this string is not a valid path, so the extension can't be used from the Office FileDialog, sigh!

To resolve this, we must return a valid path instead of "::{GUID}". Because my extension doesn't have an install folder, I decided to return a path that should be present in all systems: the temporary directory.

The first part of the GetDisplayNameOf() method looks like this:

STDMETHODIMP CShellFolder::GetDisplayNameOf(
  LPCITEMIDLIST pidl, DWORD uFlags, LPSTRRET lpName)
{
    if ((pidl == NULL) || (lpName == NULL))
        return E_POINTER;

    // Return name of Root

    if (pidl->mkid.cb == 0)
    {
        switch (uFlags)
        {
        case SHGDN_NORMAL | SHGDN_FORPARSING :    
                  // <- if wantsFORPARSING is present in the regitry

            TCHAR TempPath[MAX_PATH];
            if (GetTempPath(MAX_PATH, TempPath) == 0)
                return E_FAIL;

            return SetReturnString(TempPath, *lpName) ? S_OK : E_FAIL;
        }
        // We dont' handle other combinations of flags

        return E_FAIL;
    }

    // Getting item names follows here

    ...
    ...
}

There is one more thing to do. Like I said before, the FileDialog will call GetDisplayNameOf() to retrieve the folder name you are browsing. This is true only if you inform it that you want to be called for parsing these names. By default it will not call you. Enabling this behaviour needs a registry value to be set. Create an empty string value named 'wantsFORPARSING' under the key HKCR\CLSID\{extension guid here}\ShellFolder.

Sub-items browsing

Another modified behaviour is when the Office FileDialog browses sub-items. It still calls the root IShellFolder BindToObject() method, but with multi-level pidl. Note that this is totaly legal (see SDK), but it adds complexity for our code.

Until now our BindToObject() expected a single-level pidl which contained the favorite target path. We now must handle pidl that will still start with our item but with sub-items related to the target path sub-folders.

I modified the code of BindtoObject()

    // Handle multi-level pidl differently

    if (!m_PidlMgr.IsSingle(pidl))
    {
        HRESULT hr;
        hr = SHGetDesktopFolder(&DesktopPtr);
        if (FAILED(hr))
            return hr;

        LPITEMIDLIST pidlLocal;
        hr = DesktopPtr->ParseDisplayName(NULL, pbcReserved, 
           CDataFavo::GetPath(pidl), NULL, &pidlLocal, NULL);
        if (FAILED(hr))
            return hr;

        // Bind to the root folder of the favorite folder

        CComPtr<IShellFolder> RootFolderPtr;
        hr = DesktopPtr->BindToObject(pidlLocal, NULL, 
          IID_IShellFolder, (void**)&RootFolderPtr);
        ILFree(pidlLocal);
        if (FAILED(hr))
            return hr;

        // And now bind to the sub-item of it

        return RootFolderPtr->BindToObject(m_PidlMgr.GetNextItem(pidl),
            pbcReserved, riid, ppvOut);
    }

    // Here comes the previous code

    ...

First I check if it's a single level pidl or not. Then I get the target path pidl, like in the previous code. Then I get the IShellFolder of the target path with the first BindToObject(), this permits us to call its BindToObject() with the reminder of the pidl which is the sub (or sub-sub, or ...) folder of it.

Shell View

The system ShellView provided by the newly documented API, but existing from shell 4.7x, SHCreateShellFolderView() does much of the work. To populate and handle the items, it will call our IShellFolder methods and also IShellFolder2 methods. Basically the only thing we have to do is implement these methods which, for the majority of them, is already done.

Be aware that not implementing IShellFolder2 will lead to a 'script error' when Web View is enabled (almost on Win2K). So implement IShellFolder2 even if you return E_NOTIMPL from every methods.

WARNING: I didn't use SHCreateShellFolderViewEx() like described in Henk Devos article. I used to, but I changed because this function didn't work (didn't create the view) in the FileDialog.

When using SHCreateShellFolderView(), you have to provide a IShellFolderViewCB which contains the MessageSFVCB method that the ShellView will call to let you handle some messages. Because I use ATL, I made a base class to encapsulate the View creation but better, to handle the view messages through a standard message map. So to handle messages, just sub-class it and add the message handlers you are interested in.

Here is an example, handling one message:

#include <ShellFolderView.h>    // This one contains the base class


class CShellView : public CShellFolderViewImpl
{
public:
    // The message map

    BEGIN_MSG_MAP(CShellView)
        MESSAGE_HANDLER(SFVM_COLUMNCLICK, OnColumnClick)
    END_MSG_MAP()

    // When a user clicks on a column header in details mode

    LRESULT OnColumnClick(UINT uMsg, WPARAM wParam, 
             LPARAM lParam, BOOL &bHandled)
    {
        // Shell version 4.7x doesn't understand S_FALSE

        // as described in the SDK.

        SendFolderViewMessage(SFVM_REARRANGE, wParam);
        return S_OK;
    }

The bHandled parameters works like in standard message map, if you didn't handle the message, put FALSE in it (when entering your function it is TRUE). If you handled the message, the return value (LRESULT) will be returned from MessageSFVCB. This permit you to return any value as described in the SDK.

Sometimes your handler has to send messages to the view, this is done with the method SendFolderViewMessage() which will call internally SHShellFolderView_Message().

To provide a standard view, you do not have to handle all the messages described in the SDK. In my extension I handle only two messages and it works great.

Explorer will request a ShellView by calling your IShellFolder::CreateViewObject() method, here is the code to create a view of our class:

STDMETHODIMP CShellFolder::CreateViewObject(HWND hwndOwner,
    REFIID riid, void** ppvOut)
{
    // Make sure the caller requested an IShellView

    if (riid == IID_IShellView)
    {
        // Create the view object

        CComObject<CShellView>* pViewObject;
        hr = CComObject<CShellView>::CreateInstance(&pViewObject);
        if (FAILED(hr))
            return hr;

        // AddRef the object while we are using it

        pViewObject->AddRef();

        // Create the view

        hr = pViewObject->Create((IShellView**)ppvOut, hwndOwner, 
           (IShellFolder*)this);

        // We are finished with our own use of the view object 

        // (AddRef()'ed by Create() if successfull)

        pViewObject->Release();

        return hr;
    }

    // We do not handle other objects

    return E_NOINTERFACE;
}

That's it!

Check the code of my extension, it contains some error checking and also a mechanism to trace all the ShellView messages. This can be usefull to investigate a little more.

Points of Interest

ATL builds

When creating an ATL project, the _ATL_MIN_CRT symbol is defined. This is to avoid linking with the standard CRT (the goal was to minimize the executable file size). Because I use some CRT features, I did remove it, but then I checked again to see what features from the CRT I used.

I didn't use much of them, mostly string and memory (str* and mem*) functions. Also static objects. This brings me to using AtlAux which does minimal CRT-like things and redirects string functions to Windows API. You can find it here on CodeProject. For the memory functions, I simply got them from the CRT sources and put them in stdafx.cpp

So here are the different configurations:

  • MinSize makes use of _ATL_MIN_CRT with auto-redirects to non CRT APIs (via AtlAux)
  • MinDependencies makes use of the CRT (statically linked)
This shows you a sample which is non-CRT dependant. While testing this under VC7 and thus ATL 7.0, I found that most of the MinCRT things gone away, so take this as an example of what could be done. Note also that the MinSize builds will dynamically link to ATL.DLL which is not found by default in the OS.

How to add the extension in the Places Bar of the File Dialog?

Take a look at the picture at the top of this article. See that icon in the left pane of the FileDialog? This is a shortcut icon that will act like selecting our namespace icon in the upper ComboBox. But how to do that?

Everything lie in the registry, take a look at HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\comdlg32\PlacesBar. There, create a string value named 'PlaceN' where N is from 0 to 4. Set its value to "::{E477F21A-D9F6-4B44-AD43-A95D622D2910}" which is our extension CLSID prepended by two semicolons. This will do the trick. Further details can be found here.

Conclusion

This is the first version of this namespace extension, it implements basic features but it shows how to achieve them, this was the goal of this article. At the time of writing I'm already developing the second version which will cover folders and sub-folders. I'll update this article once I've finished with it.

I wrote this article after finishing and not during the development, so I may have loosed focus on some issues. Let me know if you miss some information or if parts of this article are too obscure.

History

  • 12 august 2004
    • First release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here