|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
SummaryThe .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 HandlersBesides 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 Finding the unmanaged definitionsThe 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 DECLARE_INTERFACE_(IColumnProvider, IUnknown)
{
// IUnknown methods
STDMETHOD (QueryInterface)(THIS_ REFIID riid, void **ppv) PURE;
STDMETHOD_(ULONG, AddRef)(THIS) PURE;
STDMETHOD_(ULONG, Release)(THIS) PURE;
// IColumnProvider methods
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; // initialization flags ULONG dwReserved; // reserved for future use. WCHAR wszFolder[MAX_PATH]; // fully qualified folder path (or empty typedef struct { SHCOLUMNID scid; // OUT the unique identifierIn 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 MetadataNow 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 [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);
/// Note: these objects must be threadsafe! GetItemData _will_ be called
/// simultaneously from multiple threads.
[PreserveSig()]
int GetItemData( LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd,
out object /*VARIANT */ pvarData);
}
Notice the [ComVisible(false),
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 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 #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 IColumnProviderNow 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
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;
// Cast to a ushort, because a VARTYPE is ushort and a VARENUM is int
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 public override int GetItemData( LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd,
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 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. ConclusionWriting 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. | ||||||||||||||||||||