- Understanding Shell Extension Handlers
- Creating Our Shell Extension
- Registering the Extension
- Where Are We?
Implementing shell extensions require us to delve into the world of COM.
The entire shell extension system is based on registration of a shell extension into the shell and implementing specific COM interfaces for different types of extensions.
For the shell to access those interfaces, we will need to expose a class as a COM object.
We can create a class that is exposed as a COM interface using COM Interop:
using System.Runtime.InteropServices; [Guid("2AA8DDCB-0540-4cd3-BD31-D91DADD81ED3"), ComVisible(true)] public class CleanVS : IContextMenu, IShellExtInit { //... }
There are several things here to pay attention to. First, we are adding two attributes to our new class; Guid and ComVisible. These attributes are used to expose a managed type as a COM object through Interop. The value in the Guid attribute is a randomly generated Guid value as can be created with GuidGen.exe. ComVisible simply annotates this as a COM visible type. By default, all types in an assembly will be exposed via COM. In this case, we only want this one type to be exposed so we add the [assembly:ComVisible(false)] to the assembly level attributes to hide all types unless expressly exposed via the ComVisible(true) attribute.
Secondly, our class implements two interesting interfaces: IContextMenu and IShellExtInit. These two interfaces are well-known interfaces. The easiest way to implement these interfaces is to simply include the interface definitions like so:
[ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), GuidAttribute("000214e8-0000-0000-c000-000000000046")] public interface IShellExtInit { [PreserveSig()] int Initialize (IntPtr pidlFolder, IntPtr lpdobj, uint /*HKEY*/ hKeyProgID); } [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), GuidAttribute("000214e4-0000-0000-c000-000000000046")] public interface IContextMenu { // IContextMenu methods [PreserveSig()] int QueryContextMenu(uint hmenu, uint iMenu, int idCmdFirst, int idCmdLast, uint uFlags); [PreserveSig()] void InvokeCommand (IntPtr pici); [PreserveSig()] void GetCommandString(int idcmd, uint uflags, int reserved, StringBuilder commandstring, int cch); }
In both of these interfaces, we show that they are well-known with the ComImport attribute. Because both of these interfaces are derived from IUnknown (in the COM nomenclature), we can annotate the interfaces with the InterfaceType attribute. Next, we need to add the GuidAttribute attribute. The Guids are the actual CLSIDs for the interfaces. Finally, the PreserveSig attributes on the methods ensures that the signature is maintained when exposed to COM Interop. The interface definition requires this because the interfaces are actually defined elsewhere. The important thing to know is that you need to include these in order to write our extension, these interfaces are critical.
Now that we have our class and the required interfaces to implement, we can start to put the guts of our shell extension together. The first method to focus on is the IShellExtInit.Initialize method:
IDataObject m_dataObject = null; uint m_hDrop = 0; int IShellExtInit.Initialize(IntPtr pidlFolder, IntPtr lpdobj, uint hKeyProgID) { try { m_dataObject = null; if (lpdobj != (IntPtr)0) { // Store the COM Interface for the selected item m_dataObject = (IDataObject)Marshal.GetObjectForIUnknown(lpdobj); FORMATETC fmt = new FORMATETC(); fmt.cfFormat = CLIPFORMAT.CF_HDROP; fmt.ptd = 0; fmt.dwAspect = DVASPECT.DVASPECT_CONTENT; fmt.lindex = -1; fmt.tymed = TYMED.TYMED_HGLOBAL; STGMEDIUM medium = new STGMEDIUM(); m_dataObject.GetData(ref fmt, ref medium); m_hDrop = medium.hGlobal; } } catch(Exception) { } return 0; }
The purpose of the Initialize method is to get information about the selected items in the shell and save that information for later calls. The m_dataObject and m_hDrop fields are used to store this information in the COM object between calls.
NOTE
For more information about what's actually going on here, see the "Creating Shell Extension Handlers" article on the Microsoft MSDN site.
Understanding this code is not necessary to create a shell extension handler, but in the big picture it might be helpful. Note that we are ignoring real exceptions in the gathering of this code because alerting the user of a problem here would be of little value.
Next, we need to implement the IContextMenu methods. This interface is specific to the type of shell extension we are creating. If you were to create another type of shell extension, you may need to implement an entirely different interface.
The first method to implement is the QueryContextMenu:
int IContextMenu.QueryContextMenu(uint hmenu, uint iMenu, int idCmdFirst, int idCmdLast, uint uFlags) { // The first id to use (should be 1) int id = 1; // If its a directory we can show the menu item if (IsDirectorySelected(uFlags)) { // Create a new Menu Item to add to the popup menu MENUITEMINFO mii = new MENUITEMINFO(); mii.cbSize = 48; mii.fMask = (uint)MIIM.ID | (uint)MIIM.TYPE | (uint)MIIM.STATE; mii.wID = idCmdFirst + id; mii.fType = (uint)MF.STRING; mii.dwTypeData = "Clean VS.NET Temp Files"; mii.fState = (uint)MF.ENABLED; // Add it to the item DllImports.InsertMenuItem(hmenu, (uint)2, 1, ref mii); // Since we only added a single item, // just increment the id by 1 id++; } // Return the new id number if we actually added an item return id; }
This method is called by the shell before it shows the popup menu, but after it has created it. The purpose of the call is to allow you to add a new item into the menu. In this case, we only want to add the menu item if the selected item is a directory. So we have a small subroutine (IsDirectorySelected) that determines this. If it is a directory, we create a new menu item. We are adding the menu item with the Win32 menu item structure. Again, this article is too short to explain the ins and outs of Win32 Menus, but it is explained very clearly on MSDN. The id variable is used to keep a count of how many menu items we added to the menu. Curiously, this needs to start with 1 and be incremented for every menu item you add. The id is returned whether you add the menu or not.
The next method to implement is the GetCommandString method:
void IContextMenu.GetCommandString(int idcmd, uint uflags, int reserved, StringBuilder commandstring, int cch) { switch(uflags) { case (uint)GCS.VERB: { commandstring = new StringBuilder("CleanVS".Substring(1, cch-1)); break; } case (uint)GCS.HELPTEXT: { commandstring = new StringBuilder("Removes all temporary files " + "from a directory (and sub directories) from Visual " + "Studio Builds".Substring(1, cch-1)); break; } case (uint)GCS.VALIDATE: { break; } } }
This method is used to get the strings to populate the shell as needed to alert the user. For example, the GCS.VERB flag means to return a very simple and small name for the operation. You will notice that we truncate all strings to the length of the cch parameter. The GetCommandString method dictates the maximum length of returned strings and this allows us to make sure we never violate this contract.
Last to implement is the InvokeCommand method:
void IContextMenu.InvokeCommand(IntPtr pici) { try { StringBuilder sb = new StringBuilder(1024); // Get the Directory Information DllImports.DragQueryFile(m_hDrop, 0, sb, sb.Capacity + 1); string directory = sb.ToString(); // Show Status Form statusDlg.Show(); CleanDirectory(directory); } catch(Exception e) { System.Windows.Forms.MessageBox.Show("Error : " + e.ToString(), "Error in CleanVS"); } finally { // Hide the status window statusDlg.Hide(); } }
This method is used to actually perform the operation in response to a user selecting our menu item. We use a Win32 API call (DragQueryFile) to get the information about the selected items (as gathered by the Initialize method). This allows us to get the full name of the directory. We then show a status dialog and call the CleanDirectory method to actually do the cleaning.
NOTE
The work that the shell extension performs is not the focus of this article. If you are interested, you can download the source code here for more information.