Summary
The .Net Development Platform provides very rich facilities for interoperability and integration with unmanaged code written using COM. The Windows Shell, which originated with the Windows 95 user interface has always been heavily based on COM and exposes several extensibility points through a number of COM interfaces. As each successive iteration of Windows has appeared, more and more extension facilities have been provided, particularly with Windows 2000. One of those facilities which appeared in Windows 2000 was the Column Handler. This article will demonstrate techniques for COM Interop by creating a Column Handler in C#.
Introduction to Column Handlers
Besides the usual Name, Size, Type and Date columns in the Details view of Windows Explorer, there are a further 28 columns capable of being added to the view when running Windows XP. There are columns for photographs taken with a digital camera and columns for your music tracks. Adding another column to this list is a matter of implementing the IColumnProvider
COM interface and registering your handler in the HKEY_CLASSES_ROOT\Folder\ShellEx\ColumnHandlers
key. A Column Handler is therefore one of the simpler shell extensions to implement, as there are usually several other interfaces required when implementing other shell extensions.
Finding the unmanaged definitions
The COM interfaces and structures used within Windows Explorer are defined solely by C++ header files. There are no type libraries nor IDL to work with. Therefore, we must find all the information from the header files and move these definitions into the managed world, precisely as they are defined in the header files. If we deviate the layout of a field within a struct by just 1 byte, then the code may refuse to work without any indication of the fault. Therefore, it is worth spending time checking and double checking the definitions.
The COM interface for an IColumnProvider
is defined in ShlObj.h
and looks like:
DECLARE_INTERFACE_(IColumnProvider, IUnknown)
{
STDMETHOD (QueryInterface)(THIS_ REFIID riid, void **ppv) PURE;
STDMETHOD_(ULONG, AddRef)(THIS) PURE;
STDMETHOD_(ULONG, Release)(THIS) PURE;
STDMETHOD (Initialize)(THIS_ LPCSHCOLUMNINIT psci) PURE;
STDMETHOD (GetColumnInfo)(THIS_ DWORD dwIndex, SHCOLUMNINFO *psci) PURE;
STDMETHOD (GetItemData)(THIS_ LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd,
VARIANT *pvarData) PURE;
};
The three IColumnProvider methods include three structures which are also defined in ShlObj.h and one structure defined in ShObjIdl.idl. These are:
typedef struct {
ULONG dwFlags;
ULONG dwReserved;
WCHAR wszFolder[MAX_PATH];
} SHCOLUMNINIT, *LPSHCOLUMNINIT;
typedef const SHCOLUMNINIT* LPCSHCOLUMNINIT;
typedef struct {
SHCOLUMNID scid;
VARTYPE vt;
DWORD fmt;
UINT cChars;
DWORD csFlags;
WCHAR wszTitle[MAX_COLUMN_NAME_LEN];
WCHAR wszDescription[MAX_COLUMN_DESC_LEN];
} SHCOLUMNINFO, *LPSHCOLUMNINFO;
typedef const SHCOLUMNINFO* LPCSHCOLUMNINFO;
typedef struct {
ULONG dwFlags;
DWORD dwFileAttributes;
ULONG dwReserved;
WCHAR* pwszExt;
WCHAR wszFile[MAX_PATH];
} SHCOLUMNDATA, *LPSHCOLUMNDATA;
typedef const SHCOLUMNDATA* LPCSHCOLUMNDATA;
In addition, there is a further structure defined in ShObjIdl.idl
typedef struct {
GUID fmtid;
DWORD pid;
} SHCOLUMNID, *LPSHCOLUMNID;
typedef const SHCOLUMNID* LPCSHCOLUMNID;
Manually defining the managed Metadata
Now we have all the unmanaged definitions, we need to duplicate these in the managed world. This is done by creating managed metadata which exactly matches the unmanaged definitions. Note that I said exactly - if part of the structure is not quite correct, or the wrong datatype is defined on a method, then more than likely the shell extension will refuse to work. I will talk more about this later.
Managed Metadata is not written using a separate language. In COM programming, IDL was the metadata language to complement C++. However in .Net programming, the metadata is a .Net language - and for this shell extension, we will use C#.
The first step is to define the IColumnProvider
metadata. If we look at the interface defined in shlobj.h
, we see that it begins with the three standard IUnknown methods. These can be ignored because .Net COM Interop will automatically create these for us. Therefore, our interface, written in C# is:
[ComVisible(false), ComImport, Guid("E8025004-1C42-11d2-BE2C-00A0C9A83DA1"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IColumnProvider {
[PreserveSig()] int Initialize(LPCSHCOLUMNINIT psci);
[PreserveSig()] int GetColumnInfo(int dwIndex, out SHCOLUMNINFO psci);
[PreserveSig()]
int GetItemData( LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd,
out object pvarData);
}
Notice the PreserveSig
attribute. This stops COM Interop from treating the return value as an out param and uses the return value as the COM HRESULT. These methods still reference the four structures, so let's rewrite those in C#:
[ComVisible(false),
StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public class LPCSHCOLUMNINIT {
public uint dwFlags;
public uint dwReserved;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)]
public string wszFolder;
}
[ComVisible(false), StructLayout(LayoutKind.Sequential)]
public struct SHCOLUMNID {
public Guid fmtid;
public uint pid;
}
[ComVisible(false), StructLayout(LayoutKind.Sequential)]
public class LPCSHCOLUMNID {
public Guid fmtid;
public uint pid;
}
[ComVisible(false), StructLayout(LayoutKind.Sequential,
CharSet=CharSet.Unicode, Pack=1)]
public struct SHCOLUMNINFO {
public SHCOLUMNID scid;
public ushort vt;
public LVCFMT fmt;
public uint cChars;
public SHCOLSTATE csFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=80)]
public string wszTitle;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]
public string wszDescription;
}
[ComVisible(false), StructLayout(LayoutKind.Sequential,
CharSet=CharSet.Unicode)]
public class LPCSHCOLUMNDATA{
public uint dwFlags;
public uint dwFileAttributes;
public uint dwReserved;
[MarshalAs(UnmanagedType.LPWStr)]
public string pwszExt;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)]
public string wszFile;
}
The observant ones reading this will have gathered there are in fact five structures defined above. The even more observant ones will have spotted the second and third structures are identical apart from one is defined as a struct, the other as a class. This is because SHCOLUMNID
is used twice - once as a pointer to a SHCOLUMNID
, and the other time as an inline SHCOLUMNID
. ie One is heap allocated, the other is stack allocated - the method GetData()
passes a pointer to a SHCOLUMNID
as it's first argument, while the structure SHCOLUMNINFO
has an inline SHCOLUMNID
as it's first field.
I mentioned previously that you have to match the managed metadata exactly to the unmanaged definitions and I certainly didn't get these structures correct the first time! It took many iterations to get them correct. By far the trickiest structure was SHCOLUMNINFO
which is defined with single byte packing - a rule which defines alignment of fields which are not an integral number of machine words in size. I had originally missed the definition for this in the header file. I'll reproduce it here:
#include <pshpack1.h>
That's it! pshpack1.h is a very short header which defines:
#pragma pack(1)
At this stage, I was pessimistic that .Net metadata would be able to support such an overly-memory efficient formatting option. RAM is so cheap these days that saving the odd byte for each column in a Windows Explorer window is more hassle than it's worth. However, .Net does indeed support packing rules - a testament to the completeness of the COM Interop team at Microsoft.
Implementing IColumnProvider
Now we've defined the metadata, it's time to implement the sole interface and create ourselves a Column Handler. For this example, I've chosen to create a new column which will contain the MD5 checksum value for the file. This is a good way of checking uniqueness of the file in a folder, even if each file has a different name.
To make it easier to define column handlers in future, I started by creating an abstract implementation of the IColumnHandler
interface. This provides a base class which defines basic services for registering the Column Handler in the Windows Registry. We can simply derive from the base class and override the three methods.
Initialize
simply returns success, so we'll move onto GetColumnInfo
and describe what is going on. Windows Explorer will ask all registered column handlers for information about their column. Each column handler can implement more than one column, so Explorer will call GetColumnInfo
with an index parameter. If S_FALSE
is returned from GetColumnInfo
for a specific index, then Explorer will stop calling the method and will know how many columns it supports. We then create a new SHCOLUMNINFO
structure and fill it in with details about our column. This structure will be returned via the out parameter on GetColumnInfo
.
public override int GetColumnInfo(int dwIndex, out SHCOLUMNINFO psci) {
psci=new SHCOLUMNINFO();
if(dwIndex!=0)
return S_FALSE;
try {
psci.scid.fmtid=GetType().GUID;
psci.scid.pid=0;
psci.vt=(ushort)VarEnum.VT_BSTR;
psci.fmt=LVCFMT.LEFT;
psci.cChars=40;
psci.csFlags=SHCOLSTATE.TYPE_STR;
psci.wszTitle = "MD5 Hash";
psci.wszDescription = "Provides an MD5 Hash of every file";
} catch(Exception e) {
MessageBox.Show(e.Message);
return S_FALSE;
}
return S_OK;
}
The MD5 is generated when the GetItemData()
method is called. This method really just contains System.IO code to load the contents of the file and uses the MD5CryptoServiceProvider
to generate an MD5 checksum.
public override int GetItemData( LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd,
out object pvarData) {
pvarData=string.Empty;
if(((FileAttributes)pscd.dwFileAttributes|FileAttributes.Directory)==
FileAttributes.Directory)
return S_FALSE;
if(pscid.fmtid!=GetType().GUID || pscid.pid!=0)
return S_FALSE;
try {
MD5 md5 = new MD5CryptoServiceProvider();
byte[] result;
using(Stream stream=File.OpenRead(pscd.wszFile)) {
result = md5.ComputeHash(stream);
}
StringBuilder output=new StringBuilder(2+(result.Length*2));
foreach(byte b in result) {
output.Append(b.ToString("x2"));
}
pvarData="0x" + output.ToString();
} catch(UnauthorizedAccessException) {
return S_FALSE;
}catch(Exception e) {
MessageBox.Show(e.Message);
return S_FALSE;
}
return S_OK;
}
To test the column handler, set the Debug Mode to "Program" and the Start Application to the full path to Windows Explorer (eg C:\Windows\Explorer.exe). Both these settings can be found in the Project Properties, underneath the debugging section. You will also need to register this assembly with COM - This can be achieved by setting Register for COM Interop to true in the Build section of the Properties dialog. In addition, the DLL should be registered in the GAC, because Windows Explorer doesn't know how to probe inside your project folder for the Column Handler DLL. Simply issue the command gacutil -i MD5ColumnHandler.dll
, or drag and drop the DLL into the GAC (C:\Windows\Assembly) folder using Windows Explorer.
Finally, if you want to restart an instance of Windows Explorer, you should use the official way to shut down Explorer, rather than killing it from Task Manager! To shut down officially, hit Start->Shutdown (or Turn Off Computer on Windows XP), then while holding the control shift and alt keys, click cancel on the shutdown dialog. To start a new instance of Windows Explorer, bring up Task Manager by holding the control and shift keys, then hit escape. In Task Manager, choose File->New Task and enter "Explorer.exe". This will have restarted Windows Explorer properly.
Conclusion
Writing any Interop code in .NET trivial as long as you have the managed metadata definitions. The creation of these definitions invariably proves to be the most problematic and time-consuming part. As you can see, we've successfully managed to create a Column Handler for Windows Explorer with very little code, although Windows Explorer does tend to use more RAM because it now hosts the Common Language Runtime. As long as you have a machine with plenty of RAM, this is never a problem.
Thanks to Robert Plant for reviewing and suggestions.