New information has been added to this article since publication.
Refer to the Editor's Update below.
Windows Shell
Create Namespace Extensions for Windows Explorer with the .NET Framework
Dave Rensin
Code download available at:WindowsShell.exe(224 KB)
This article assumes you're familiar with C#
SUMMARY
Extending the Windows shell with namespace extensions allows you to create some custom functionality for Windows Explorer. One common use is to enable Explorer to present a list of items that do not exist in one real folder, but actually reside in a number of places. The view on the folder makes it look like these items are in one place, so managing them becomes easier. This article illustrates the process of creating custom shell namespace extensions using C# and the .NET Framework. The author dispels some myths about the difficulty of writing such extensions, and shows that it is easier than it was before .NET. Along the way he outlines undocumented interfaces and describes advanced techniques for consuming them in .NET.
Contents
Introducing Shell Namespace Extensions
The Basics
First Secret—Use the Source, Luke...
Second Secret—Beware the Inheritance Tax
Third Secret—Triple-check Your Parameters
More Basics
ShellFolder
Fourth Secret—Lies, Damn Lies, and PIDLs
The ShellFolder Class
Properties
Methods
The ShellView Class
Variables
Methods
Common Dialogs with Extended Messaging
Common Dialogs with No Extended Messaging
Microsoft Office Dialogs
A Parting Thought
Your Very Own Shell Extension
Conclusion
[Editor's Update - 6/23/2006: Because shell extensions are loaded into arbitrary processes and because managed code built against one version of the runtime may not run in a process running an earlier version of the runtime, Microsoft recommends against writing managed shell extensions and does not consider them a supported scenario.]
Suppose you're a programmer working on a new software product that's going to change the world, a worthy goal for all new software products. For the sake of efficiency and performance, you decide to store the objects your product manipulates in a database on a far-away server. You're using C#, IIS, and Web Services and things are going great. Then one day the head of product marketing comes by your office to have a chat.
It seems, he says, that potential customers love the product concept, but hate the idea of using a special application to file and retrieve the objects. Instead, they want to be able to browse the objects from any Windows®-based application using the standard File Open and File Save dialogs.
Introducing Shell Namespace Extensions
For the uninitiated, shell namespace extensions are special COM objects that allow you to add onto, and generally manipulate, the Windows shell. Their uses are many and varied, but if you believe what you read on various message boards and FAQs, there's no such thing as an easy or quick-and-dirty namespace extension.
In this article, I'll show you how this just isn't so. Namespace extensions are eminently buildable, as long as you know the secrets. I will acquaint you with those secrets so that you can join the action-packed world of custom extension writing. If, at the end of this article, you still don't feel comfortable building your own extensions, take heart. I will point you to a base class I have written that will make your life much easier. All you will need to do is inherit from it and add less than 10 lines of code, and you'll have a fully working namespace extension.
The Basics
Not well documented, and often misunderstood, the core interfaces for a shell namespace extension (shown in Figure 1) aren't as hard to use as you may think. In order to create your own extension from scratch you will need to build two classes. The first class needs to implement the interfaces IPersistFolder and IShellFolder. The second class will have to implement IShellView. Along the way, you will also need to stub out interfaces for ICommDlgBrowser, IOleWindow, and IShellBrowser.
Figure 1** Windows Shell **
A word to the wise: if you've never written a class that implemented an interface not native to the Microsoft® .NET Framework, then stop reading this and review interop now. The online help is a good reference, but the information is a bit scattered. I usually recommend the book C# and the .NET Platform by Andrew Troelsen (APress, 2003). In any case, you should pay particular attention to the .NET ComImport attribute.
For those of you with at least a passing familiarity with the subject, I'll move on. When I began to compile the list of interfaces, structures, and enumerations that I would have to define in .NET, I started to feel a bit queasy. Like any good programmer, I don't want to do any more work than I have to do to get the job done.
The big problem with stubbing out these interfaces is that they are all inherited from IUnknown instead of IDispatch. The main implication is that the functions have to be defined in a particular order known as vtable order. That means you need to make sure that you define the methods of the interface in exactly the same order they are defined in their respective .h files. Since the online help doesn't always tell you what the vtable order is for a given interface, you will be stuck flipping back and forth between several different C++ .h files—but perhaps there's another way.
First Secret—Use the Source, Luke...
Any old C++ programmer will tell you that the first thing a compiler does when building a project is preprocess all of the source files. This means that it takes your various #include statements and resolves them all into one large file. It also substitutes all special literals, like macros and string constants, into their actual values as set forth in various #defines. The bottom line is that the preprocessor will put all the little bits of information you will need into one handy spot. Getting it to work was remarkably easy.
There are three main .h files concerned with namespace extensions. (They, of course, make references to many other .h files, but that's the preprocessor's problem.) They are shlobj.h, shobjidl.h, and shlguid.h. To get the complete set of secrets contained within them, and all the other files they reference, I created a simple file named test.cpp that had the following three lines of code:
#include <shlobj.h> #include <shobjidl.h> #include <shlguid.h>
Next, I opened a command prompt and typed the following line:
cl /E test.cpp > test.txt
CL.exe is the .NET compiler. The /E option tells it to just preprocess the file without trying to compile it. The output is then redirected to the file test.txt.
Note that CL.exe is not normally in the path, so you will need to open the command prompt shortcut installed in the Microsoft Visual Studio® .NET Start Menu program group in order to run it. After this operation, the resulting test.txt file contains a greatly resolved set of interface definitions. For example, the definition for the interface IOleWindow now looks like this:
struct __declspec(uuid("00000114-0000-0000-C000-000000000046")) IOleWindow : public IUnknown { public: virtual HRESULT __stdcall GetWindow( HWND *phwnd) = 0; virtual HRESULT __stdcall ContextSensitiveHelp( BOOL fEnterMode) = 0; };
This is just a short step from its required C# form of:
[ComImport()] [Guid("00000114-0000-0000-C000-000000000046")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IOleWindow { void GetWindow( out IntPtr phwnd); void ContextSensitiveHelp( bool fEnterMode); }
This trick saves a lot of unnecessary typing and guarantees that your definitions are in the correct vtable order, but you're not out of the woods yet.
Second Secret—Beware the Inheritance Tax
So now you have all your interfaces in one convenient place and you're ready to define them in C#. Along the way, you notice that some of the interfaces are inherited from each other.
For example, IPersistFolder is based on IPersist which, in turn, is inherited from IUnknown. Given this, you might think that you could save some typing by doing the following in your C# code:
public interface IPersist { // stuff specific only to IPersist } public interface IPersistFolder : IPersist { // stuff specific only to IPersistFolder }
This makes sense, right? After all, IPersistFolder should inherit all of IPersist's methods and the resulting vtable should come out right. Unfortunately, I discovered that the wrong functions were getting called. At first I thought I had mixed up the order, but a quick look at the original C++ definitions showed that this was not the case. I then defined IPersistFolder thusly:
public interface IPersistFolder { // stuff specific to IPersist only // stuff specific to IPersistFolder only }
Lo and behold, it worked! Notice that I've abandoned any idea that IPersistFolder is inherited from anything at all and just included the stubs from IPersist right in its definition. In all candor, I can't tell you why this is but it definitely works just fine and shouldn't give you any problems.
So now you have all your interfaces in place and you know how to handle their inheritances in your definitions. It's time to start coding, right? Not quite yet.
Third Secret—Triple-check Your Parameters
Technically this isn't so much a secret as it is an admonishment, but so many people are stung by it that it might as well have been classified information. Let's say that the C++ definition for some method called Foo looks like this:
void Foo (long i, int j)
Let's also suppose you stub it out in C# with the same header. Chances are the code in that function will never get called. The reason is that unmanaged code treats a long as 32 bits but managed code (generated by C#) treats it as 64 bits. This means the call stack for your function will be off by 4 bytes and the Windows shell will most probably get an exception when trying to call it. Instead, your method should be defined as follows:
void Foo(Int32 i, Int32 j)
I know this seems like a small detail, but if you're not careful this will come back to bite you. For example, you may someday want to implement context menus, property pages, or some other thing for which I have not already provided an interface. Should that situation ever come to pass, you will certainly be better off having read the last few pages.
Now that all the housekeeping is out of the way, it's time to dig seriously into the heart of the extension.
More Basics
The included source code is actually split between two projects. The first is the base class assembly I made reference to in the beginning. Its name is MSDNMagNSEBase (short for MSDN® Magazine Namespace Extension Base Class). The files in this project are mmc.cs, Shell.cs, ShellFolder.cs, and ShellView.cs.
The second project is a sample extension I created that shows how to use the base class. It's called MSDNMagazineSampleNamespaceExtension and its files are Class1.cs and Form1.cs. For the time being, I'm going to concentrate on the first project as that represents the heart of the code.
The average extension has two parts—a folder and a view. Typically, these parts are represented by two different classes, although this is not a strict requirement. The folder class must implement the interfaces IPersistFolder and IShellFolder and the view class must implement IShellView. In my case, these classes are located in the files ShellFolder.cs and ShellView.cs, respectively.
The folder class is responsible for managing the data associated with your extension, while the view class is responsible for managing the user interface of the extension. For those readers who are or were MFC programmers, this is conceptually similar to the document/view architecture.
ShellFolder
The most common use for namespace extensions is to present a virtual directory of items to the Windows shell. These items may be actual files on disk or not; it really doesn't matter. For example, when you open My Network Places you are looking at a shell extension that presents a virtual folder of visible remote computers. Of course, these items don't actually represent files, but the Windows shell neither knows nor cares about that. All it cares about is that it has been presented with a list of unique items. The details of how those items are managed are completely up to the extension.
The items managed by the folder are represented by a PIDL—a pointer to a list of unique identifiers. Each virtual item managed by the folder has its own PIDL.
The first thing you should know about PIDLs is that they are defined only as a high-level concept. This is intentional. What you store in the PIDL is entirely up to you and Windows sets almost no restrictions. The downside to the opaque nature of these odd little creatures is that documentation surrounding them is rather vague. In fact, it has been my experience that PIDL management is the most confusing and daunting part of writing a shell namespace extension. A quick scan of message boards dealing with shell extensions makes this fact manifestly evident.
Like everyone else who has ever written a namespace extension, I spent a lot of time writing my own PIDL manager class to handle these nettlesome beasts. It wasn't until I finally completed my first extension that I noticed something truly surprising about them.
Fourth Secret—Lies, Damn Lies, and PIDLs
PIDL management is a largely useless activity that can be avoided when writing your own shell extension. Forget about writing a PIDL manager, implementing IEnumIDList, or any of the many other things usually required for managing objects. It's all completely unnecessary.
After my third or fourth extension, I settled upon a technique that makes life a lot easier. To understand how it works, though, you first need to understand a little bit about how the shell, the folder, and the view all communicate with each other.
The Windows shell (explorer.exe) is natively equipped to manage and display physical items like files and directories. In order to display virtual items (such as remote computers or printers), it relies on shell namespace extensions.
Consider the case of the My Network Places. When you open a Windows Explorer window and double-click My Network Places (or any shell namespace extension), a certain sequence of events occurs. First, the extension's object that implements IShellFolder is loaded into memory. Then Windows asks the object for its unique ID—its PIDL. Armed with the PIDL, Windows asks the extension for its display name. Next, Windows asks the extension for a list of any items that it is currently managing—things like remote computers or virtual files. The list is returned using the interface IEnumIDList. Windows iterates through each item in the list and asks the extension for the display name of the item. Windows next asks the extension for a reference to the object that implements IShellView—in this case, my view class. Finally, Windows asks the view object to load its UI by calling CreateViewWindow.
This is the shorthand version of what happens. A few other calls are also made to the extension to ask it about other kinds of things it can do, but that's not particularly important for the moment. From my own experience, and from the experiences of people who have talked about writing shell extensions, the hardest part of the process is managing PIDLs. There seems to be universal agreement that the thing to do is to write a PIDL management class that implements the IEnumIDList interface. Inside that class, you would allocate and free memory and generally manage the identifiers for your items.
This is hard in C++, but in C# the structures and types required for this aren't native and don't have good analogs. This means that a PIDL manager class in C# will be doing a lot of reading, writing, allocating, and freeing with IntPtrs. It's complicated, difficult to debug, and very prone to memory leaks. As it turns out, though, it's also not necessary. If you return a NULL when Windows asks for your IEnumIDList, it will just assume that you have no items in your view and drop the matter. If you never tell Windows that you have any items to manage, then there is no need for complicated PIDL management. This will save you time, energy, and code.
Of course, just because Windows thinks there are no items being managed by your extension doesn't mean that there really aren't any items. The UI displayed in your view is controlled by a window you create. This means that the UI for your extension can show any number of items while Windows thinks you have none. The two are completely separate.
Besides ease of implementation, there are performance reasons why you may want to tell Windows there are no items under management. In certain circumstances, Windows will ask for more than just the display name of your items. It may also ask for things like size, creation time, and other attributes. If, like me, you are writing an extension to manage items on another server, that could mean a lot of unnecessary round-trips.
I prefer to use a technique that's much simpler than traditional PIDL management. My extensions only worry about two PIDL values—0 and 255. A PIDL of 0 represents the extension itself or the directory on disk where the extension is rooted. A PIDL of 255 represents the currently selected item on my view.
For example, in my extension I might show a list of 100 or more virtual items. As the users click each item, I send Windows a special notification that the selected item has changed. Windows then asks me for the PIDL of the newly selected item; I tell it 255. Next, Windows asks me for the display name of the item with PIDL 255. Since I obviously know the display name of the item just selected, I return it. The salient code can be found in the GetItemObject method of the ShellView class and will be covered in much greater depth a little later in this article.
The ShellFolder Class
I store a number of variables internally in my ShellFolder class to make life a bit easier. These are shown here:
m_view This is a reference to the view object.
m_PIDLData This variable is a byte array that holds the PIDL passed initially into the folder object from Windows.
m_mdo When Windows asks for a specific PIDL it will sometimes ask for it in the guise of an IDataObject. The MyDataObject class is a simple implementation of that interface. The m_mdo variable is just a reference to the current data object under management.
m_workingDir My extension is managing a list of virtual items on a remote server. Since my users want to access those items through applications like Notepad or Microsoft Word, I need a place on disk to reconstitute them before the application tries to open them. This is that place. In general, it's a good idea to have a working directory associated with your extension if you plan to access your extension through the File Open or File Save dialogs of a Windows-based application.
m_currentItem This holds the file name of the reconstituted item. It follows the same rationale as m_workingDir.
m_form This is the System.Windows.Forms.Form that will hold your view UI. This is a form that you design.
m_parentWindow This represents the window housing my extension. It's particularly important if the extension is being loaded inside of a common dialog.
m_parentWindowText This variable holds a string representing the title of the parent window.
Properties
In addition to the variables I have just outlined, I have added a number of properties to the Shellfolder class. The following sections describe them briefly.
ParentWindow and ParentWindowText These are exposed as properties so that people inheriting from this base class can only retrieve them, not set them.
WorkingDirectory This is exposed as a property because some checking needs to happen when the value is set. Specifically, I want to make sure that the value always has its trailing backslash (\).
SelectedItem This is exposed as a property because when it is changed I need to notify the Windows shell. This is only relevant if the extension is sitting inside of a File Open or File Save dialog. In the set code, I query to see if I can find a pointer to a ICommDlgBrowser interface. If I can, then I'm in such a dialog and I notify it that the selected item has changed by calling OnStateChange.
Methods
In this section, I'll flesh out some of the details of the various methods in the ShellFolder class. While not exactly a line-by-line description, it should give you a good idea of what's going on.
byte[] PidlToBytes At a couple of places in the code it was easier for me to debug if I converted the contents of a PIDL into a byte array like this:
length0data0length1data1 ... lengthNdataN
This function computes the total length of the PIDL by iterating through it, then reads the whole thing into a byte array. Item lengths are 16-bit unsigned integers (ushort in C#).
void GetClassID At various points in the life of your extension, the Windows shell will want to know the GUID of the folder object. This method gets it.
long Initialize Namespace extensions can be anchored in any number of places in the Windows shell. They can be on the desktop, inside My Computer, My Network Places, Control Panel, a specific directory on disk, or many other places. This is called rooting. When Windows first initializes an extension, it calls the Initialize method and passes in a PIDL. This PIDL represents the unique path to the place in the Windows shell where your extension is rooted. You won't often need to make reference to this PIDL, but it's handy to keep around just in case. In my case, I just store it in the m_PIDLData variable and construct a new IDataObject with it.
void ParseDisplayName On occasion, Windows will ask your extension for the PIDL and attributes of a particular item that it thinks you are managing. It does this by calling ParseDisplayname. Since all my objects have the same PIDL value (255), I just return it. In addition, I tell Windows that are no attributes to report and that I have consumed 100 percent of the string that was passed in. (Some extensions only consume part of the string when they have subfolders, but that's not an issue here.)
void EnumObjects When Windows wants the complete list of items your extension is managing, it calls EnumObjects and expects to get an IEnumIDList interface back. Since I'm taking shortcuts with my PIDL management and want Windows to think I have no items, I just set the value to NULL.
void BindToObject Sometimes Windows wants a particular interface that it thinks your folder object might know about. When that happens it calls BindToObject, passing in a PIDL to the item it cares about and a CLSID for the interface it wants for that specific item. In my case Windows doesn't think I have any items, so when it calls this method it's really asking if the folder object implements the given interface. All I do is query the IUnknown interface of the folder to see if it implements the requested interface. If so, that's great. Otherwise, Windows gets a NULL in return.
At this point you might be wondering why the shell doesn't just call the QueryInterface method of my interface. After all, that's how one normally acquires a specific interface for an object. Windows doesn't necessarily think that the folder object is the item that supports the interface it is interested in. Instead, the folder might be holding a reference to an item that implements the desired interface. In that case, a call to QueryInterface won't help.
void CompareIDs When Windows thinks an extension is managing some objects, it may try to sort them. Since it has no idea what your PIDLs mean, it has to ask you about the relative value of two PIDLs. It does so by calling this method. As I have no items, this method will never be called so its implementation is blank.
long CreateViewObject Once the basic housekeeping is out of the way, Windows is going to ask your extension for a reference to its view object—the class that implements IShellView. That happens here. All I do is query the view class for its IShellView interface and return it. In addition, Windows gives me a handle to the window which is presently hosting the extension. I store that handle for future use and get the title bar text of the window as well. This will be important later when you need to know if you are in a File Open or File Save dialog.
long GetAttributesOf This method is called when Windows wants to know something about an item it thinks you are managing. In my case, I make sure the SFGAO_FILESYSTEM (0x40000000) bit is set. This makes Windows think that the item is part of the file system. This little white lie makes things work properly when the extension is loaded in a common dialog box. There is, however, an important side effect to this. Windows will absolutely think that the item is part of the file system. That means the extension will have to be notified just before Windows tries to actually open the item on disk so it can materialize it in the right spot. I'll dig much deeper into this problem later on when I discuss the implementation of the view class.
void GetUIObjectOf GetUIObjectOf is called when Windows wants a specific interface for an item it thinks you are managing. These can include context menus, drop targets, and other things. Since I don't bother implementing these interfaces, I just return NULL.
void GetDisplayNameOf In the simplified PIDL management system I use, there are only ever two possible PIDL values—0 and 255. GetDisplayNameOf is called when Windows wants the display name of a given item. If the value I get is 0, then I return the display name of the extension. In my case, that's the working directory. If the value of the PIDL is 255, then I return the display name of the currently selected item. In my case, that's a virtual file name. The reason I always return directory and file names is because it makes operating the extension in an open/save dialog much easier. For example, if in response to a PIDL of 0, I might return "c:\temp\" and in response to a PIDL of 255 I might return "test.txt," then Windows will think that the currently selected item is c:\temp\test.txt. When the user clicks the OK button in the open/save dialog, I will get the event and know which file the user thinks they're opening. Then, I'll reconstitute that file in the place the user is looking before the application actually tries to open it. I'll show you how that works shortly. The thing to remember here is that this function is your only opportunity to influence where Windows thinks your file actually is.
void SetNameOf Windows calls this method if the user is changing the name of an item managed by the extension. This is only important if you have written an extension that allows Windows to manage the UI instead of you. Since that's not the case here, I just set the return value to NULL and move on.
Many first-time extension writers think that they can take a shortcut by telling Windows to manage the UI of the extension. Their logic is that by doing so they can avoid having to create a class that implements IShellView. They are correct, but they are badly misled. While they will save some effort in UI development, they will absolutely have to implement a thorough PIDL management system. In my experience, that's a lot harder than creating a custom user interface.
void DoDefaultAction I wrote this method so I could easily trick the Windows shell into thinking that the OK button of an open/save dialog was pressed. This is important in order to allow users to double-click an item in your extension and have it automatically open. I will cover the criteria for deciding when to use this method a little later. For now, it's important to note that this function is only germane if the extension is hosted inside an open/save dialog, and all the checks for that condition are hidden from you in the method body.
That said, let's look at how this method operates. It's vital to check that I can successfully query for an ICommDlgBrowser interface. If I can, then I know the extension is in an open/save dialog and I call OnDefaultCommand to signal to the dialog that the OK button has been pushed. If not, then I know that my extension is being loaded someplace other than in a common dialog control and I needn't worry about telling Windows that the OK button was pushed.
void RegisterFunction/void UnregisterFunction These functions are conveniences that take care of the setup and cleanup of all the necessary registry keys associated with this extension. For example, in order for an extension to load, it has to be registered in a list of approved extensions under the following key:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved
For maximum compatibility with different versions of Windows, two special keys, WantsFORPARSING and Attributes, need to be added to the extension's normal COM registration keys. Since the RegisterFunction and UnregisterFunction methods are marked with the [ComRegisterFunction] and [ComUnregisterFunction] attributes respectively, they will be called when the assembly is registered and unregistered with COM. Alternately, you could build a setup project in Visual Studio and have it make the needed registry changes.
void Setup This is another special method I created that makes things a little easier for you. In the constructor of your custom extension that inherits from this base class, you call the Setup method to make sure that the base class can properly manage your working directory, event notifications, and the form to display for your UI. Once again, I'll discuss this a bit later.
The ShellView Class
Now let's look seriously at the view part of the folder/view pair. The view object is where all the UI for your extension lives. The principal responsibility of the view object is to manage a window you design that holds the visual contents of the extension.
Although there are a couple of pitfalls to avoid, the view object isn't nearly as difficult to implement as the folder object. In this project, the view object is in the class named ShellView and is contained in the file ShellView.cs.
Variables
My base view class needs to keep track of some important items, such as the following:
m_mc This is a reference to the ShellFolder class that created this view. I use this in a few places when I need to ask the folder for some information. In general, it's good practice to have your ShellFolder and ShellView classes keep references to each other.
m_form This is the System.Windows.Forms.Form-derived window that you supply. You can put anything you want on the window—the extension doesn't care.
m_shell This holds a reference to an IShellBrowser. The IShellBrowser interface is important for querying information about the window that is hosting the extension, as well as for signaling state changes in the extension.
m_hhook In order to trap when the users clicks the OK button in an open/save dialog, I have to install a Windows hook. A Windows hook tells the operating system to notify you when certain events occur. (The specific events depend on the kind of hook you set.) This particular hook notifies me whenever a window in the current thread has finished processing a message in its WndProc function. When the hook is installed, it returns a handle. The reason I keep this handle around is so I can uninstall the hook before the extension closes. It's very important to uninstall your hooks when you no longer need them because each hook slows down the operating system somewhat.
m_folderSettings When the extension loads, it will be informed of the current view settings in the Windows shell. These include the kind of view the folder had last, such as list, detail, and icon. It's important to restore these settings before the extension exits, so I keep them here.
h When a Windows hook is installed it requires a pointer to a callback function. That function will receive all the hooked notifications. The common language runtime (CLR) has no specific idea of a function pointer, but it will marshal a delegate as a function pointer when the delegate is passed as a parameter to an external function. This variable is a reference to that delegate.
m_fired Certain events that I look for in my hook may fire more than once on certain occasions. Since I only want to notify my view once that a certain event has occurred, I keep a Boolean around that lets me know if the event has already fired.
Methods
Most of the method implementations in this class are quite sparse; only two of them have much meat. Nonetheless, in this section I'm going to walk you through all of them and point out why some are stubs while others are well implemented.
ShellView This is the constructor for the class. There's not a lot going on in here. All I do is store a reference to the folder that created this view and create the delegate that will be used as the callback for the window hook. In fact, it's important to keep to a minimum the number of things you do in this constructor. This is because this call must complete before the UI can draw. If you load it up with lots of queries on remote machines or other time-consuming tasks, there will be a noticeable delay in the drawing of your window.
void GetWindow On occasion, Windows will ask the view for the HWND of its window, which this method returns.
void ContextSensitiveHelp If this extension supported context-sensitive help, then I would do something useful here. It doesn't, so the implementation is empty. If you did want to implement help, this is how it would work: when the user presses either F1 or Shift-F1 while your extension is active, this method will be called with a value of true. If you want to display help, this is where you do it. If you want the parent window hosting your extension not to bother showing its own help (since you'll be handling it), set the parameter of this method to false. If you don't, your user will see two help screens, which would be no help at all.
long TranslateAcceleratorA My extension doesn't worry about special accelerator keys, so I return S_FALSE (1) to indicate to Windows that it should handle the key. But just because I don't handle special keys doesn't mean you won't. For example, you might want to handle the Backspace key as a convenient way to go to a previous screen. If so, you should tell Windows that you have already handled this key and not to bother with it. In that case, you should return S_TRUE (0). Otherwise, both you and your parent window will try to handle the key.
void EnableModeless This method is used to enable or disable modeless dialogs. However, it's not currently used in Windows so the implementation is empty.
void UIActivate UIActivate is called whenever the UI state of the extension changes. When the extension is being closed (UI state 1), I have to close the underlying form.
void Refresh This is called whenever Windows thinks the extension needs to refresh the contents of the view. I don't care about this event in my extension, but you might. Specifically, this method is called when the user presses F5 while your extension has focus. If you are displaying a list of virtual items from a server, this is a good place to update your UI. If you're interested in making a small improvement to this base class, let the base class handle this event; you can still use this base class and get notified of refresh events.
void DestroyViewWindow This method is called when the extension is about to be unloaded. It's important here to close the underlying form and to uninstall the Windows hook. It's particularly important to perform the latter. If you don't, you will slow down your computer at the very least. In the worst-case scenario, you will leak RAM or crash the shell.
void GetCurrentInfo Windows calls the GetCurrentInfo method when it wants to know the view state of the extension. This is the same view state that was stored earlier in m_folderSettings. In this case, I just return that value.
void AddPropertySheetPages Although I have chosen not to add property pages to the Options property in the View menu item, if you are interested in doing this, refer to the IShellView::AddPropertySheetPages method in the Visual Studio on-line help.
void SaveViewState Before Windows unloads the extension, it provides it with an opportunity to save its current view state. Since this extension draws its own UI and doesn't really care about the view state, I do nothing. If, however, you wanted to alter your UI based on the current view state, you would need to save the last known state here.
void SelectItem Windows calls this method when it thinks a new item in the view has been selected. The manner in which this extension is constructed makes this method meaningless since you already know which item is currently selected.
If I were managing a list of items using a PIDL management system, then this method would be pretty important. Of course, that's more work than I really need to do.
long GetItemObject Windows calls this method when it wants extension-specific data about the items in the view. It tells the extension what specifically it wants through a UInt16 named uItem. In my case, I only care about two cases for uItem. First, there is SVGIO_SELECTION. Here, Windows is asking for the IDataObject for the currently selected item. In this case, I return an IDataObject interface that contains the PIDL value of 255. Second, there is SVGIO_ALLVIEW. With this case, Windows wants the same thing as SVGIO_SELECTION but for all the items. In this case, I return an IDataObject with a PIDL of 0. It's also possible that Windows will ask for some other kind of interface (other than IDataObject) in this call. For maximum flexibility, I query the view object for the requested interface. If it exists, return it. If the view doesn't have the interface, I query the folder for it. If I find it, I return it. If neither the view nor the folder have the interface, I return an error.
As you have most likely noticed, all the methods of the view class thus far have been remarkably short on code. I've intentionally saved the best for last. By far, the most interesting code is found in the next two methods.
void CreateViewWindow This method is what Windows calls when it wants you to render your extension's UI for the first time. In the case of this extension, that means something quite specific. All the UI for this extension is on a separate form that you maintain. This method will modify that form's properties so that it becomes a child of the window that contains the extension, not the desktop. It will also make sure that certain style bits are set on the form and that the form is properly positioned in the window.
Once the current folder settings for the Windows shell are passed into this method, you should save them for later use. Windows will pass in a reference to an IShellBrowser interface. This interface is important because it's the only way to get an HWND for the parent window. Also, the IShellBrowser is needed in order to signal the shell that the selected item has changed. (You can see this in use by examining the set handler for the SelectedItem property of the ShellFolder class.)
Now, call the GetWindow method of IShellBrowser to get the handle to the window hosting the extension and change the parent of the custom .NET Framework form to the newly acquired window. This is done with the following code:
ShellFolder.SetParent( m_form.Handle, hwnd ); ShellFolder.SetWindowLong( m_form.Handle, -16, 0x40000000 ); ShellFolder.SetWindowPos( m_form.Handle, 0, 0, 0, w, h, 0x0017 );
The first line of this code changes the parent. The next line changes the window style of the form to WS_CHILD. This is critical as any window that is the child of another window must have this style. The third line calls SetWindowPos to save the style change for the form. The SDK documentation is pretty clear on this point. You must call SetWindowPos directly after a call to SetWindowLong in order for the style changes to stick.
Resize the new form to fit within the parent window. One of the other parameters passed into this method is the RECT for the newly created area for your form. This is represented in the code by the variable prcView.
Finally, install the Windows hook that will post-process the events for all the Windows in the current thread. You must also make sure that the extension is properly notified when the user pushes the OK button in an open/save dialog.
long OnDialogMsg The extension I'm creating here manages virtual items that reside on a remote server. Windows, however, thinks that the items reside somewhere in the local file system. This means that I have to insert code between the time when the user selects the item he wants to open and when the application calling into the extension actually tries to read it from disk. The right time to do this is when the user selects the OK button in an open/save dialog. The interfaces Windows provides for shell namespace interaction do not provide an easy mechanism to capture this event. In fact, there are three distinct cases that need to be handled: common dialogs with extended messaging, common dialogs with no extended messaging, and Office applications.
Common Dialogs with Extended Messaging
When a Windows-based application uses the common open/save dialog boxes, it can either use a standard dialog or an extended dialog. The extended dialog will actually send a message when either the OK or Cancel buttons are selected. Unfortunately, most applications don't use the extended dialog, but for those that do, the code is fairly straightforward.
First, you have to look for a WM_NOTIFY event (0x004E). Then, you have to look for a special notify code of 0xFFFFFDA2. The dialog sends this code when the OK button is selected.
If you have triggered both of those conditions and a delegate has been registered to receive a notification of the event, then you fire the custom OnAction event.
Common Dialogs with No Extended Messaging
For those applications that use the nonextended common dialogs for open and save, the situation gets a bit trickier. In this case, I can only infer that the OK button has been pushed. This is because whenever a button on a dialog is selected, a BN_SETSTATE event is generated. It doesn't matter why the button was pushed. It could be because the user tabbed to it and pushed SPACEBAR, because the user used the mouse to click it, or because the user used a keyboard accelerator. In all cases, this event is triggered. Furthermore, the event is triggered with a nonzero WPARAM parameter.
Of course, there could be many buttons on the dialog, so you want to make sure that you're catching the correct one.
Each control on a dialog has a unique identifier known as a control ID. In the case of common dialogs, the control ID of the OK button is always 1. So, after you catch the BN_SETSTATE event with a nonzero WPARAM, you find out which control ID generated the event by calling GetDlgCtrlID. If the ID is equal to 1 and there are delegates registered to handle the event, then you should fire it off.
The preceding two scenarios will suffice for most Windows-based applications. However, one very important class of Windows-based applications does not use the common dialog boxes for opening and saving items. Unfortunately, this also happens to be the most popular set of applications currently in use—the Microsoft Office suite.
Microsoft Office Dialogs
The various Microsoft Office applications use a different set of dialogs for their open/save operations. Thankfully, these dialogs are common across all the applications in the suite. Nonetheless, in order to correctly support these applications you have to develop a different set of code.
The internal operation of the Office open/save dialogs isn't documented so, as in the case of nonextended common dialogs, you have to infer how they work. In my case, I spent a lot of time in the Spy++ tool that comes with Visual Studio.
Spy++ is a standalone application you can use to examine all the windows that are currently open, as well as all the messages they generate. I did a lot of spying on the message flow to and from the open/save dialogs in the various Office applications.
Apparently, when an Office application starts, it registers a special message named WM_OBJECTSEL with Windows. Then during the course of various open/save operations, it sends these messages. The trick, it seems, is to figure out which of those messages means that the Open.Save button was pushed.
Upon closer examination of the messages I noticed that when either the Open or Save buttons were pushed, the various Office apps would send a WM_OBJECTSEL with an LPARAM of 37 (0x25). Knowing this, the process became easy. I needed to find out what the message ID corresponding to WM_OBJECTSEL is by calling RegisterWindowMessage. If that message has been received, check the LPARAM. If the LPARAM is 0x25 and there are delegates to be triggered, fire the OnAction event.
A Parting Thought
If you look closely at the OnDialogMsg code you will notice that I maintain a Boolean value named m_fired. Although I've never seen it happen, it is possible that one of these events may fire more than once during an open or save process. I use this Boolean value to make doubly sure that I only trigger the OnAction event once.
As a convenience I thought it would be nice to include some information about the kind of event that was fired when I trigger OnAction. This method checks the text of the window hosting the extension to see if either the words "open" or "save" appear. As a result, if you register a delegate to handle this event, you will receive one of three values:
- ShellActionType.Open—the result of an open request
- ShellActionType.Save—the result of a save event
- ShellActionType.Unknown—from an unknown dialog type
At this point you know everything you need to know to create your very own shell namespace extension from scratch. Beware: it's still a lot of work.
Your Very Own Shell Extension
I hate to rewrite what I can repurpose. For that reason I've wrapped up all of my accumulated understanding of extensions into a simple base class named ShellFolder. Creating a custom extension from this class couldn't be easier. You can start by compiling the files comprising the base class into an assembly and creating a new project of type Class Library. Add a reference to the base class assembly and create a new Form in your project, setting its window border style to none. Then create a method in your form with the signature of public void OnAction(MSDNMagazine.Shell.ShellActionType t). Make your new main class inherit from MSDNMagazine.Shell.ShellFolder. Figure 2 shows a complete sample implementation for your new class.
Figure 2 Custom Class Extension
[ProgId("MSDN Magazine Sample Shell Namespace Extension")] [Guid("6B49E580-186E-4f8c-AB6A-E55D6F0F171D")] public class Class1 : MSDNMagazine.Shell.ShellFolder { public Class1() { // // TODO: Add constructor logic here // Form1 m = new Form1(); m.m_folder = this; this.Setup( "c:\\temp\\test", new MSDNMagazine.Shell.OnDefaultAction(m.OnAction), m ); }
There are two things to notice here. First, I've given my new object a very descriptive progId. This is a shortcut. The base class will take this progId and make it the text label you see when you browse to your extension. For example, if I root my extension on the desktop its label will be "MSDN Magazine Sample Shell Namespace Extension." Of course, you can change this to whatever you like. Second, the constructor has to do three important things. It must create a new instance of my custom form (Form1), hand the form a reference to the ShellFolder-derived object, and call Setup in the base class. This function takes three parameters. First, it takes a working folder (c:\temp\). Second, it takes a delegate to be called when the OK/Open/Save button is pressed. My custom form (Form1) has a method named OnAction that I would like called in this case, so I pass it as the second parameter. Finally, it takes the form that holds the display contents of my extension. In this case, the form that contains my custom UI is in variable m.
That's the whole implementation for the main class—all that's left is for you to design the layout of your custom form. Just to round things off, though, Figure 3 shows a sample of a basic implementation of your OnAction event handler.
Figure 3 OnAction Event Handler
public void OnAction(MSDNMagazine.Shell.ShellActionType t) { if ( t == MSDNMagazine.Shell.ShellActionType.Open ) { // do special Open stuff } if ( t == MSDNMagazine.Shell.ShellActionType.Save ) { // do special Save stuff } if ( t == MSDNMagazine.Shell.ShellActionType.Unknown ) { // probably do nothing... } }
There's one other thing I should mention. Let's say you want to support double-clicking as a method to open an item in your extension. To do this, you need to override the relevant double-click event in your UI and use code similar to the following:
// get whatever remote data you need Windows to open and // store it in a definite place on disk — like c:\temp\test\foo.txt this.m_folder.SelectedItem = "c:\\temp\\test\\foo.txt"; this.m_folder.DoDefaultAction()
This tells Windows that the currently selected item in your view is c:\temp\test\foo.txt and simulates to Windows that the user has clicked the OK/Open/Save button. All the code for this extension is available for download at the link at the top of this article.
Conclusion
After I had finished and tested my extension base class, one of my developers asked what the hardest part was. For me, it was setting aside some of the things I thought I knew about writing shell namespace extensions. You may never have to go beyond the base class, but if you do, at least you'll know how. Mark Twain was right. It's not what we don't know that hurts us—it's that we know so much that isn't true.
For related articles see:
Creating a Shell Namespace Extension
For background information see:
C# and the .NET Platform by Andrew Troelson (APress, 2003)
Dave Rensin recently left Omnisky Corporation to become Chief Technology Officer of QKnow Corporation, located in Washington, D.C. He is the author of four books and numerous articles on distributed computing systems. Contact him at Dave@rensin.com.