Invoking a .NET program on Device Insert
Given the popularity of the .NET Platform it seems odd that Microsoft does not provide easier means of doing certain incredibly useful and easy to imagine tasks from the .NET framework. It took what seems like a ridiculous amount of research to accomplish a simple task:
“Lots of programs come up as ‘launch’ options when I insert a storage card/camera/video camera/PDA/Mp3Player/younameit device into a USB port. I’d like my .NET program to be in that list and to run if the user selects it.”
File Types & WIA
The most useful thing you get in Visual Studio towards this goal is file types. You can build an MSI for your desktop application and make your “.damon” files open with this program and have right clickàOpen appear in the shell context menu for “.damon” files. I’ve written before about how to do multiple-select and some useless and goofy things with this scenario, but this doesn’t get us anywhere near where we want to be.
WIA is very useful for media-specific actions like this, and handles the “something was plugged in” scenario in specific cases. There is a WIA Automation Layer and managed wrappers for this. Obviously if you care about something besides JPGs and AVIs WIA may not work for you. Also, WIA requires that the device you’re speaking to has a WIA driver. WIA drivers are certainly very common for the appropriate device types, but when you are distributing desktop software that depends on an OEM’s drivers you’re stepping into a world of pain (Not a world of Payne, which is actually a wonderful place). Can the OEM give you merge-modules for the install? Are their drivers buggy? Be sure to treat your Support Group very well if you go down this path.
The Problem Solution: Shell Extensions and Managed Code
I’m going to focus on a simple subset of the overall problem. Despite the perceived simplicity of the final solution, I couldn’t find a single working example of this online so if you got here via Goolge you’ve hit the jackpot. Suppose you have an SD card, an SD card reader, and some family photos on said SD card. You would like a .NET program to automatically create thumbnails of these when the SD card is inserted into your card reader.
Step 1
Step 1 is to read some obscure documentation found here: http://msdn.microsoft.com/msdnmag/issues/01/11/autoplay/
The first part of this is easy to follow. The Windows Autoplay functionality mostly resides in the sub keys of HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\explorer\AutoplayHandlers\EventHandlers\
Oddly enough, the names of these sub-keys are almost meaningful and you can do some Googling Windows Live Searching to get an idea of what exactly will cause this or that handler to be invoked. In our case, the key we want for XP and Vista is ShowPicturesOnArrival. Windows scans the SD card after it’s inserted and finds JPGs or whatever and checks this sub key to determine what choices the Shell should offer the user. What can be gleaned from the MSDN article in Step 1 is that we can make up a Handler name, so I’ll call mine TestPhotoHandler. This “TestPhotoHandler” must correspond to a sub key of HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\explorer\AutoplayHandlers\Handlers (../../Handlers from ShowPicturesOnArrival), which will in turn contain some values that describe what TestPhotoHandler is to the Windows Shell.
Step 2
Step two is to populate our Handler.

· Action=what will appear to the user they are doing with this handler.
· DefaultIcon=Pretty self explanatory. You can also use the Foo.exe,-1 syntax to specify an embedded resource.
· Provider=Name of your program
· InvokeProgID, InvokeVerb=the interesting part, keep reading.
o Note that it would be FAR too easy to just let you include the path to your .exe or .dll here…
With just the above work done, you can see the following behavior in Windows:

Step 3
Telling Windows where to go from here is far more difficult than it needs to be, and I could not find a single working example of getting this stuff to work with managed code. In addition to telling Windows how to find our .NET program, Windows must be able to communicate with our program. There are three interfaces that are interesting for these purposes:
1. IHWEventHandler
2. IHWEventHandler2
3. IDropTarget
These are COM interfaces. Because I keep meaning to get back into C++ development I always install C++ accessories in Visual Studio. This means I have the various COM header and IDL files installed, and I can dig up some useful information. Recall that .NET classes can implement COM interfaces as long as you know the GUID of the COM interface. Here, then, is my IDropTarget interface definition for .NET objects.
[ComImport,
InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("00000122-0000-0000-C000-000000000046")]
public interface IDropTarget
{
[PreserveSig()]
int DragEnter(IntPtr pDataObj, // IDataObject
ulong grfKeyState, // DWORD
POINTL pt,
ref ulong pdwEffect); // DWORD*
[PreserveSig()]
int DragOver(ulong grfKeyState, // DWORD
POINTL pt,
ref ulong pdwEffect); // DWORD*
[PreserveSig()]
int DragLeave();
[PreserveSig()]
int Drop(IntPtr pDataObj, // IDataObject
ulong grfKeyState, // DWORD
POINTL pt,
ref ulong pdwEffect); // DWORD*
}
[StructLayout(LayoutKind.Sequential)]
public struct POINTL
{
public POINTL(long xx, long yy) { x = xx; y = yy; }
public long x;
public long y;
public override string ToString()
{
String s = String.Format("({0},{1})", x, y);
return s;
}
}
PointL was copied from Dino Esposito’s excellent article on Shell Extensions on The Server Side. At any rate, these three attributes on IDropTarget basically tells COM that we can speak the COM lingo and have the behaviors specified by the Interface identified by this GUID.
Step 4
Next we’ll implement the IDropTarget Interface. The implementation can supposedly reside either in an .exe or .dll, but for our purposes it only works and is only safe when the implementation is on the Entry Point of an .exe project. So,
[Guid("C1D0D2F6-5F45-4462-9835-C463AFE367E3")]
[ComVisible(true)]
public class Program : IDropTarget
{
/// <summary>
/// The main entry point for the application.
/// </summary>
static void Main()
{
There are some things to take note of here. Since we wish to enter the world of COM (and COM is love…) we put a ComVisible attribute on the class. The default Program created for you in VS2008 has Program as a static class, which cannot have attributes so we’ve removed that. We also have used the Tools in Visual Studio to create a GUID for this class. We are using an API known as “HackingtheRegistrySinceNoRealRegistrationAPIExists” and guids are the coin of the realm. I happen to hate this API but options are scarce.
Step 5
The next step is to register our code in such a way that it is findable via the appropriate magic. For .NET programs RegAsm.exe is our registrar of choice. Note that RegAsm will pout if your Assembly is not strongly named. If we now “Regasm.exe /codebase LaunchTestProg.exe” we get some interesting registry settings created for us. Under HKEY_CLASSES_ROOT\CLSID we find our Program’s GUID, along with the following:

A sub key named “ProgId” containing the fully qualified class name of the .NET program we’d like to run is nestled beneath our class id guid. This is part of the explanation for the values in Step 2. Continuing to search for our GUID in the registry we find that HKEY_CLASSES_ROOT\LaunchTestProg.Program has been created for us.
Step 6
Rather than be clever I’ll just show the final piece we need to create. The Shell depends on various lookups and naming conventions to determine what it needs to do.

Below HKEY_CLASSES_ROOT\LaunchTestProg.Program, we need to create some registry keys and values ourselves. We create sub keys shell, open, DropTarget, and put the GUID of our .NET program as a clsid attribute of the DropTarget key. The entire process can now ask the user, find, and Invoke our .NET program when a USB device is inserted and it meets the original Handler criteria.
Further Investigation
Upon testing this, I found some interesting behaviors. It appears that while the .NET project must be an .exe, what Windows is really doing is instantiating the Program object and invoking the members of IDropTarget on it. I have not tried too hard to get Main to run. In normal cases, IDropTarget.DragEnter and then IDropTarget.Drop will be called. For my particular needs, the arguments to either method are throw-away, but using these should be normal P/Invoke & Marshalling exercises. If you just want to start some other .NET code you can now do so from the your Drop() implementation.
I will also warn that you want to read this and understand what it means: http://blogs.msdn.com/junfeng/archive/2005/11/18/494572.aspx
I will also warn that the class library Assembly that contains my IDropTarget interface seems to be file-locked once I actually invoke my program. Restarting Explorer.exe fixes this, which may be a consideration if you are going to be auto-updating the program in some way. It seems odd that the .exe containing the actual program is not locked. I plan on investigating the registry areas of Windows\CurrentVersion\ShellExt\Cached to see if clearing that and firing the appropriate Registry Changed event will unlock the assembly.
Other Avenues...
You will notice DeviceGroups and DeviceHandlers keys as siblings of the “EventHandlers” subkey. I also mentioned IHWEventHandler and IHWEventHandler2. The latter is merely the Vista version of the former that adds an extra method, presumably for UAC. It seems that allowing a .NET program to reconfigure itself if a new input device is plugged in is certainly not out of the range of possibilities.
Would you like some thrown-together code that does all of this for you? I’ve been meaning to get better about posting more code, so here we are:
public class DropTargetInstaller
{
public bool Install(Assembly exe, string entryPointTypeName, string desiredHandlerName, string iconPath, string actionText, string usingText)
{
Type t = exe.GetType(entryPointTypeName);
object[] attributes = t.GetCustomAttributes(false);
GuidAttribute guid = GetGUID(attributes);
//Throws ArgumentExceptions to caller if the Assembly/class does not meet requirements
VerifySuitability(entryPointTypeName, t, attributes, guid);
string guidVal = guid.Value;
Regasm(exe);
WriteRegistryValues(desiredHandlerName, actionText, iconPath, entryPointTypeName, usingText,guidVal);
return true;
}
/// <summary>
/// Suitable classes must Implement the COM IDropTarget interface, be marked COMVisible, and contain a valid GUID attribute
/// </summary>
/// <param name="entryPointTypeName"></param>
/// <param name="t"></param>
/// <param name="attributes"></param>
/// <param name="guid"></param>
private void VerifySuitability(string entryPointTypeName, Type t, object[] attributes, GuidAttribute guid)
{
if (!VerifyDropTarget(t))
{
string msg = string.Format("Type {0} does not implement IDropTarget", entryPointTypeName);
throw new ArgumentException(msg);
}
//
if (!VerifyCOMVisible(attributes))
{
string msg = string.Format("Type {0} is missing COMVisble attribute", entryPointTypeName);
throw new ArgumentException(msg);
}
//
if (null == guid)
{
string msg = string.Format("Type {0} is missing Guid attribute", entryPointTypeName);
throw new ArgumentException(msg);
}
}
/// <summary>
///
/// </summary>
/// <param name="exe"></param>
protected void Regasm(Assembly exe)
{
string regasmExe = @"Microsoft.NET\Framework\v2.0.50727\RegAsm.exe";
string regPath = Path.Combine(@"c:\windows\", regasmExe);
//RegAsm does not support URI format (file:////c:blahblah
ProcessStartInfo info = new ProcessStartInfo(regPath, "/codebase " + exe.CodeBase.Replace("file:///",string.Empty));
info.RedirectStandardOutput = true;
info.RedirectStandardError = true;
info.UseShellExecute = false;
Process p = Process.Start(info);
using (StreamReader stdOut = p.StandardOutput)
{
using (StreamReader stdErr = p.StandardError)
{
p.WaitForExit();
string output = stdOut.ReadToEnd();
string errput = stdErr.ReadToEnd();
//Do error logging here if stdErr is not blank, etc.
int code = p.ExitCode;
}
}
}
/// <summary>
///
/// </summary>
/// <param name="desiredHandlerName"></param>
/// <param name="actionName"></param>
/// <param name="iconPath"></param>
/// <param name="entryPointName"></param>
/// <param name="usingString"></param>
/// <param name="guid"></param>
protected void WriteRegistryValues(string desiredHandlerName, string actionName, string iconPath,
string entryPointName, string usingString, string guid)
{
WriteHandlerRoot(desiredHandlerName, actionName, iconPath, entryPointName, usingString);
WritePhotosOnArrival(desiredHandlerName);
WriteDropTarget(entryPointName, guid);
}
private static void WriteDropTarget(string entryPointName, string guid)
{
RegistryKey classesRoot = Registry.ClassesRoot;
if (classesRoot.GetSubKeyNames().Contains<string>(entryPointName))
{
RegistryKey invokeProgId = classesRoot.OpenSubKey(entryPointName, true);
if (invokeProgId.GetSubKeyNames().Contains<string>("shell"))//Start over
{
invokeProgId.DeleteSubKeyTree("shell");
}
RegistryKey shell = invokeProgId.CreateSubKey("shell");
RegistryKey open = shell.CreateSubKey("open");
RegistryKey dropTarget = open.CreateSubKey("DropTarget");
dropTarget.SetValue("clsid", "{" + guid + "}");
dropTarget.Close();
open.Close();
shell.Close();
invokeProgId.Close();
}
else
{
//Regasm did not run
}
//
classesRoot.Close();
}
private static void WritePhotosOnArrival(string desiredHandlerName)
{
RegistryKey photosOnArrival =
Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\explorer\AutoPlayHandlers\EventHandlers\ShowPicturesOnArrival", true);
if (!photosOnArrival.GetValueNames().Contains<string>(desiredHandlerName))
{
photosOnArrival.SetValue(desiredHandlerName, string.Empty);
}
photosOnArrival.Close();
}
private static void WriteHandlerRoot(string desiredHandlerName, string actionName, string iconPath, string entryPointName, string usingString)
{
RegistryKey handlerRoot = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\explorer\AutoPlayHandlers\Handlers", true);
if (handlerRoot.GetSubKeyNames().Contains<string>(desiredHandlerName))
{
handlerRoot.DeleteSubKey(desiredHandlerName);
}
RegistryKey handler = handlerRoot.CreateSubKey(desiredHandlerName);
handler.SetValue("Action", actionName);
handler.SetValue("DefaultIcon", iconPath);
handler.SetValue("InvokeProgID", entryPointName);
handler.SetValue("InvokeVerb", "open");
handler.SetValue("Provider", usingString);
&nb