An almost complete Namespace Extension Sample






4.87/5 (19 votes)
Aug 13, 2004
16 min read

209237

3764
An implementation of a shell namespace extension that uses the system provided ShellView (SHCreateShellFolderView)
Contents
- Introduction
- Requirements
- What will this extension do?
- How is it implemented?
- Points of Interest
- Conclusion
- History
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 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 theSHCreateShellFolderView
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
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
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()
onceCompareIDs()
several timesGetAttributesOf()
for each itemGetDisplayNameOf()
for each itemGetUIObjectOf()
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()
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)
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.