Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Implement an Autoplay Handler

5.00/5 (22 votes)
18 Sep 2006CPOL8 min read 2   1.7K  
Implementing an Autoplay handler in C#.

Sample Image - AutoplayDemo.gif

Contents

Introduction

AutoplayDemo is a small application that appears in the Removable Disk Inserted dialog box and can be notified by the shell of all the files on the inserted disk. The sample application has two primary components: a Windows Form that receives the file names that are on the inserted disk, and a shell extension that receives the insertion notification.

Registration of the application requires entering quite a few registry keys that quickly became tedious, so the sample application has an implementation of an ATL (Active Template Library) registry script.

So this is sort of two articles for the price of one.

Autoplay Handler

What is Autoplay?

Autoplay Version 2 is a feature in Windows XP that will scan the first four levels of a removable media, when it arrives, looking for media content types (music, graphics, or video). Registration of applications is done on a content type basis. When a removable media arrives, Windows XP determines what actions to perform by evaluating the content and comparing it to registered handlers for that content. A detail article on "Autoplay in Windows XP" can be found on MDSN.

An application will register a handler for Autoplay events associated with a media type. In the case of this demo, it will register for events generated when the media contains graphic files. To register a handler, you have to add registry entries in three places:

  1. The handler definition must be defined as a unique key under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\... Explorer\AutoplayHandlers\Handlers\HandlerName. The "..." is not part of the key.
    HKEY_LOCAL_MACHINE
      SOFTWARE
        Microsoft
          Windows
            CurrentVersion
              Explorer
                AutoplayHandlers
                  Handlers
                    DemoAutoPlayOnArrival
                      Action [REG_SZ]= Load Files
                      DefaultIcon [REG_EXPAND_SZ]= 
                                   C:\WINDOWS\assembly\GAC_MSIL\AutoPlayListener\
                                   1.0.0.0__e2b9b927fbb09dc3\AutoPlayListener.dll, 
                                   32512
                      InvokeProgID [REG_SZ]= 
                             Almdal.AutoPlayListener.AutoPlayOnArrivalHandler
                      InvokeVerb [REG_SZ]= import
                      Provider [REG_SZ]= Autoplay Demo File Loader
    • Action: Text string that represents what action the application will take with regard to the content type that triggered an Autoplay response.
    • DefaultIcon: Icon that represents the application in the Autoplay UI.
    • InvokeProgID: This is the ProgID of an application or COM component providing a Shell sub key that will be invoked.
    • InvokeVerb: This is the verb under the ProgID specified in the InvokeProgID value.
    • ProviderText: The string that represents the application.
  2. Under ProgID key, the class ID (CLSID) of the COM object that implements the IDropTarget interface must be defined.
    HKEY_CLASSES_ROOT
      Almdal.AutoPlayListener.AutoPlayOnArrivalHandler
        shell
          import
            DropTarget
               ClSID= {{ec2a75bc-680c-4af0-b306-eedf980c0ae3}}
  3. Finally the name of the autoplay handler has to be added to the appropriate event under the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\... Explorer\AutoplayHandlers\EventHandlers. Again, the "..." is not part of the key.

    We want to be notified when graphics arrive, so we add a string value with our handler name (defined in step 1) to the key ShowPicturesOnArrival. Windows has predefined several event handlers, and to get a complete list, you can look at the above key in the registry. The names are fairly self explanatory.

    HKEY_LOCAL_MACHINE
      SOFTWARE
        Microsoft
          Windows
            CurrentVersion
              Explorer
                AutoplayHandlers
                  EventHandlers
                    ShowPicturesOnArrival
                      DemoAutoPlayOnArrival [REG_SZ]

Implementation

I chose to implement the file handling in a separate process as opposed to in the shell extension. The shell extension functions as the "server". It gets invoked by the shell when the user selects it from the Media arrival dialog box. It then unpacks the file names that are passed in, and sends the file name to the "client" by an anonymous pipe.

Files on the removable media are passed to the shell extension by way of the Drop method in the IDropTarget interface. The other IDropTarget interface methods: DragEnter, DragOver, and DragLeave are not used, and just have skeleton implementations.

C#
public int DragEnter(IntPtr pDataObj, ulong grfKeyState, 
                     POINTL pt, ref ulong pdwEffect) {
    pdwEffect = (ulong)DROPEFFECT.COPY;
    return 0;
}

public int DragOver(ulong grfKeyState, 
           POINTL pt, ref ulong pdwEffect) {
    pdwEffect = (ulong)DROPEFFECT.COPY;
    return 0;
}

public int DragLeave() {return 0;}

When files are passed to the Drop method, they are not in the standard clipboard format, but are passed as a Shell ID List. The Autoplay handler needs to register this clipboard format when it starts.

C#
protected static uint AUTOPLAY_SHELLIDLISTS = 
    WinApi.RegisterClipboardFormat(WinApi.CFSTR_AUTOPLAY_SHELLIDLISTS);

The Drop method first finds the "client" application. If it is not currently running, it starts it.

C#
Process[] all = Process.GetProcesses();
Process myProcess = null;
foreach (Process p in all) {
  try {
    if ("Idle".Equals(p.ProcessName) || "System".Equals(p.ProcessName))
        continue;
    if (p.MainModule.ModuleName.StartsWith("WindowsApplication2")) {
      myProcess = p;
      break;
    }
  } catch (Win32Exception ex) {
    EventLog.WriteEntry("Almdal.AutoPlayOnArrivalHandler", 
                        p.ProcessName + ": " + ex.Message, 
                        EventLogEntryType.Warning);
    throw ex;
  }
}
return (myProcess != null) ? myProcess : StartOurExe();

Starting the executable consists of looking up the path from the registry and then calling the Process.Start method, passing the path to the executable as a string parameter.

C#
Process myProcess = null;
try {
  RegistryKey hklm = Registry.LocalMachine;
  RegistryKey rk = hklm.CreateSubKey("Software\\Almdal\\AutoPlayDemo");
  string executable = 
     (string)rk.GetValue("{0F08197F-AF66-4198-9673-C5B5A33AACED}");
  if (rk.GetValue("{0F08197F-AF66-4198-9673-C5B5A33AACED}") == null) {
    throw new Exception("Client Not defined, " + 
                        "Run the Demo program first to" + 
                        " initialize the location of the executable");
  }
  myProcess = Process.Start(executable);
  while (myProcess.MainWindowHandle == IntPtr.Zero) {
    myProcess.Refresh();
  }
} catch (Exception ex) {
  EventLog.WriteEntry("Almdal.AutoPlayOnArrivalHandler", 
                      ex.Message, EventLogEntryType.Error);
  throw ex;
}
return myProcess;

It then sends the read handle of the anonymous pipe to the "client". I'll come back to the pipe handling later.

C#
WinApi.SendMessage(myProcess.MainWindowHandle,
                   START_LOAD,
                   _pipe.ReadHandle(myProcess.Handle),
                   0);

Then, the Drop method gets the IDataObject interface for the information passed from the shell, and ensures that the format is indeed CFSTR_AUTOPLAY_SHELLIDLISTS. If it is, we can process the shell ID list, otherwise we throw a NotImplementedException.

C#
// Get info about the files that are coming in the droptarget interface
IDataObject pdataobject = (IDataObject)Marshal.GetObjectForIUnknown(pDataObj);
FORMATETC fmt = new FORMATETC();

fmt.cfFormat = AUTOPLAY_SHELLIDLISTS;

fmt.ptd = IntPtr.Zero;
fmt.dwAspect = DVASPECT.DVASPECT_CONTENT;
fmt.lindex = -1;
fmt.tymed = TYMED.TYMED_HGLOBAL;

int hr = pdataobject.QueryGetData(ref fmt);

if (hr == 0) {
  hr = HandleAutoplayShellIDList(pdataobject);
}
else {
  throw new NotImplementedException();
}

The HandleAutoplayShellIDList method takes the dropped data object, and extracts the CIDA structure, which is used with the CFSTR_SHELLIDLIST clipboard format to transfer the pointer to an item identifier list (PIDL) of one or more Shell namespace objects (PIDLs). A description of the shell namespace, including what item identifiers are, can be found here.

C#
FORMATETC fmt = new FORMATETC();
fmt.cfFormat = AUTOPLAY_SHELLIDLISTS;
fmt.ptd = IntPtr.Zero;
fmt.dwAspect = DVASPECT.DVASPECT_CONTENT;
fmt.lindex = -1;
fmt.tymed = TYMED.TYMED_HGLOBAL;
STGMEDIUM medium = new STGMEDIUM();

hr = pdataobject.GetData(ref fmt, ref medium);
if (hr == 0) {
  CIDA cida = (CIDA)WinApi.GlobalLock(medium.hGlobal);

  if (!IntPtr.Zero.Equals(cida)) {
  //  actually got the lock on the global memory
    hr = ProcessCIDA(cida);

    WinApi.GlobalUnlock(cida.Detach());
  }
  WinApi.ReleaseStgMedium(ref medium);
}

The ProcessCIDA method takes the dropped data object, and retrieves the pointer to item identifier lists (PIDLs).

C#
int count = cida.Count;
for (int iItem = 1; iItem < count; ++iItem) {
//    item zero is the folder (so there count + 1 items
  IntPtr folder = (IntPtr)cida.Folder;
  IntPtr item = (IntPtr)cida[iItem];
  PIdl full = (PIdl)WinApi.ILCombine(folder, item);

  if (!full.isNull()) {
    IntPtr ptr;
    IntPtr pidlItem;
    hr = WinApi.SHBindToParent((IntPtr)full, WinApi.IID_IShellFolder, 
                                out ptr, out pidlItem);

    if (hr == 0) {
      IShellFolder psf = (IShellFolder)Marshal.GetObjectForIUnknown(ptr);

      STRRET strDisplayName;

      // Or this in if you only want the filename: SHGDN_INFOLDER
      hr = 
        (int)((IShellFolder)psf).GetDisplayNameOf(pidlItem, 
                                                  SHGNO.SHGDN_FORPARSING, 
                                                  out strDisplayName);

      if (hr == 0) {
        StringBuilder szDisplayName = new StringBuilder(WinApi.MAX_PATH);

        hr = WinApi.StrRetToBSTR(ref strDisplayName, pidlItem, 
                                 out szDisplayName);

        if (hr == 0) {
          _writer.WriteLine(szDisplayName.ToString());
          _writer.Flush();
        }
      }
      Marshal.FinalReleaseComObject(psf);
    }

    WinApi.ILFree(full.Detach());
  }
}

Pipe handling

The handling of the pipe is handled in the SharedPipes library. There are a lot of good pipe handling articles on CodeProject, but none seemed to be light-weight and anonymous. I just wanted a simple one. So, I created a library to facilitate sharing between the two end points (client and server).

The constructor is overloaded. The constructor with no parameters creates the write end of the pipe.

C#
if (CreatePipe(out _hReadPipe, out _hWritePipe, IntPtr.Zero, 0) == false) {
    throw new Win32Exception(Marshal.GetLastWin32Error());
}

_direction = Direction.Write;

The constructor that takes a pipe handle as a parameter will create the read end of the pipe.

C#
_hReadPipe = new SafeFileHandle(hReadPipe, true);
_direction = Direction.Read;
_hWritePipe = new SafeFileHandle(IntPtr.Zero, true);

There are Reader and Writer properties that expose a StreamReader and StreamWriter to the pipe, respectively.

C#
public StreamWriter Writer {
  get {
    return new StreamWriter(new FileStream(_hWritePipe, 
                            FileAccess.Write));
  }
}

public StreamReader Reader {
  get {
    return new StreamReader(new FileStream(_hReadPipe, 
                            FileAccess.Read));
  }
}

The only real trick to passing a handle is that the handle needs to be mapped into the receiving process' address space. This is done using the Windows API DuplicateHandle method.

C#
IntPtr targetHandle;
DuplicateHandle(Process.GetCurrentProcess().Handle, 
                _hReadPipe, 
                targetProcess, 
                out targetHandle, 
                0, 
                false, 
                3);
return targetHandle;

Installing and using the demo

Download the demo zip file, and extract it to a directory. I have created a batch file to register the assemblies and the client. You can run this from a command prompt. A parameter of /r will do the registration. A parameter of /u will uninstall the registrations. Now, when you plug in a removable media containing images, you should see the autoplay demo as a choice on the Autoplay system dialog. The AutoplayDemo arrival handler only registers itself for the ShowPicturesOnArrival Autoplay event.

C# Registry Scripts

When I was developing the Autoplay Handler, I had to code the registration of the handler. But I found this to be cumbersome and potentially error prone. I missed the ATL (Active Template Library) way of having a script with substitutable parameters. So, I decided to emulate the ATL approach in C#. For comparison, the original code based approach is listed below. The scripted approach is listed later in the article.

Specify code approach

C#
RegistryKey hkcr = Registry.ClassesRoot;
RegistryKey rk = hkcr.CreateSubKey("Almdal.AutoPlayListener" 
                                   ".AutoPlayOnArrivalHandler.1");
rk.SetValue("", "Demo autoplay handler");
RegistryKey rk2 = rk.CreateSubKey("CLSID");
rk2.SetValue("", guid);
rk2.Close();
rk.Close();

rk = hkcr.CreateSubKey("Almdal.AutoPlayListener." 
                       "AutoPlayOnArrivalHandler");
rk.SetValue("", "Demo autoplay handler");
rk2 = rk.CreateSubKey("CLSID");
rk2.SetValue("", guid);
rk2.Close();

rk2 = rk.CreateSubKey("CurVer");
rk2.SetValue("", "Almdal.AutoPlayListener." 
             "AutoPlayOnArrivalHandler.1");
rk2.Close();

rk2 = rk.CreateSubKey("shell\\import\\DropTarget");
rk2.SetValue("CLSID", guid);
rk2.Close();
rk.Close();

hkcr.Close();

RegistryKey hklm = Registry.LocalMachine;
rk = hklm.OpenSubKey("Software\\Microsoft\\Windows\\" 
                     "CurrentVersion\\Explorer\\AutoplayHandlers");
rk2 = rk.OpenSubKey("EventHandlers\\ShowPicturesOnArrival", true);
rk2.SetValue("DemoAutoPlayOnArrival", "");
rk2.Close();

rk2 = rk.OpenSubKey("Handlers", true);
RegistryKey rk3 = rk2.CreateSubKey("DemoAutoPlayOnArrival");
rk3.SetValue("Action", "Load Files");

string dllLoc = typeof(AutoPlayOnArrivalHandler).Assembly.Location;
StringBuilder icon = new StringBuilder(dllLoc).Append(", 32512");
rk3.SetValue("DefaultIcon", icon.ToString());
rk3.SetValue("InvokeProgID", 
             "Almdal.AutoPlayListener.AutoPlayOnArrivalHandler");
rk3.SetValue("InvokeVerb", "import");
rk3.SetValue("Provider", "Autoplay Demo Loader");
rk3.Close();
rk2.Close();
rk.Close();
hklm.Close();

What's a registry script

If you have ever done any programming with ATL, you will be familiar with the registry script that the ATL Control Wizard automatically generates for you. It is the file with the .rgs extension. This script contains a nested list of registry keys and values to be added, updated, or removed from the registry.

For all the details, the MSDN documentation is here.

Variable substitution

There is the ability to specify parameters to the registry script at registration time. These parameters are identified by a leading and trailing percent sign (%).

A simple registry script

An example of a simple registry script to add the location of the executable to the registry is shown below:

HKLM {
  NoRemove Software
  {
    NoRemove %Company%
    {
       ForceRemove %AppID%
       {
         val %Location% = s "%LOCATION%"
       }
     }
  }
}

This tells the script processor to:

  1. Open the HKEY_LOCAL_MACHINE registry key.
  2. Open the "Software" registry key. If the script is being run to deregister, then don't delete this key.
  3. Open the registry key that is defined by the Company parameter. On deregister, don't delete.
  4. On registration, first delete the key specified by the AppID parameter, and then add it again.
  5. Add a registry value. The name of the registry value is specified by the Location parameter, and the actual value is specified by the LOCATION parameter.

The following key values can be used as registry root keys:

HKEY_CLASSES_ROOTHKCR
HKEY_CURRENT_USERHKCU
HKEY_LOCAL_MACHINEHKLM
HKEY_USERSHKU
HKEY_PERFORMANCE_DATAHKPD
HKEY_DYN_DATAHKDD
HKEY_CURRENT_CONFIGHKCC

A couple of other points about registry scripts:

  • There is an implied nesting of registry keys and values with a set of braces {}.
  • There can contain more than one registry root key in a script (it just has to be at the highest level of nesting).
  • The registry script is case sensitive, so NoRemove is not the same as NOREMOVE.
  • The default value for a registry key is assigned by specifying the the value on the key specification.

CRegistryScript library

The CRegistryScript is a library that is installed into the Global Assembly Cache so that it can be used by C# shell extensions. It will load a specified string containing the resource script to process. This can be in an external file or, as in this sample, the resource file.

Variables are defined for the script by calling the AddVariable method. This method takes two parameters: the variable name, and the value to be substituted.

To use the script for registration, call the Register method. To remove the registry definitions, call the Unregister method.

At this point, the only type of values that can can be added to the registry are string or DWORD values. Anything else will throw a NonImplementedExecption.

The AutoPlay Registry script

Here is the registry script used to define the registry entries for the autoplay handler described above:

HKCR
{
  ForceRemove %PROGID%.%VERSION% = s '%DESCRIPTION%'
  {
    CLSID = s '%CLSID%'
  }
  ForceRemove %PROGID% = s '%DESCRIPTION%'
  {
    CLSID = s '%CLSID%'
    CurVer = s '%PROGID%.%VERSION%'
    shell
    {
      %InvokeVerb%
      {
        DropTarget {
          val CLSID = s '%CLSID%'
        }
      }
    }
  }
}

HKLM {
  NoRemove Software
  {
    NoRemove Microsoft
    {
      NoRemove Windows
      {
        NoRemove CurrentVersion
        {
          NoRemove Explorer
          {
            NoRemove AutoplayHandlers
            {
              NoRemove EventHandlers
              {
                NoRemove ShowPicturesOnArrival
                {
                  val %HANDLERNAME% = s ''
                }
              }
              NoRemove Handlers
              {
                ForceRemove %HANDLERNAME%
                {
                  val Action = s '%Action%'
                  val DefaultIcon = s '%MODULE%, %ICON%'
                  val InvokeProgID = s '%PROGID%'
                  val InvokeVerb = s '%InvokeVerb%'
                  val Provider = s '%Provider%'
                }
              }
            }
          }
        }
      }
    }
  }
}

The code to register the script

C#
string regScript = Almdal.AutoPlayListener.Properties.Resources.RegistryScript;
CRegistryScript script = new CRegistryScript();

//  the HKCR\CLSID\guid key is inserted into the registry by
//  the .Net framework. See the Dino Esposito's article
//  http://www.theserverside.net/tt/articles/showarticle.tss?id=ShellExtensions
//  for more details on creating a shell extension in C#.
script.AddVariable("PROGID", typeof(AutoPlayOnArrivalHandler).FullName);
script.AddVariable("VERSION", FileVersion);
script.AddVariable("DESCRIPTION", "Demo autoplay handler");
script.AddVariable("CLSID", typeof(AutoPlayOnArrivalHandler).GUID.ToString("B"));
script.AddVariable("HANDLERNAME", "DemoAutoPlayOnArrival");
script.AddVariable("Action", "Load Files");
script.AddVariable("Provider", "Autoplay Demo File Loader");
script.AddVariable("InvokeVerb", "import");
script.AddVariable("ICON", "32512");
script.AddVariable("MODULE", typeof(AutoPlayOnArrivalHandler).Assembly.Location);

if (register) {
  script.Register(regScript);
}
else {
  script.Unregister(regScript);
}

Credit where credit's due

In researching this article, I made use of many web resources, but three stand out beyond the rest. So thank you to all the anonymous ones, and a special thanks to the following:

History

  • Initial version 1.0 - Sept. 18, 2006.
  • Updated version 1.1 - Sept. 20, 2006 (Hopefully, I corrected more typo's than I created.)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)