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 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:
- 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.
- 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}}
- 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]
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.
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.
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.
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.
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.
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
.
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.
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)) {
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).
int count = cida.Count;
for (int iItem = 1; iItem < count; ++iItem) {
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;
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());
}
}
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.
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.
_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.
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.
IntPtr targetHandle;
DuplicateHandle(Process.GetCurrentProcess().Handle,
_hReadPipe,
targetProcess,
out targetHandle,
0,
false,
3);
return targetHandle;
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.
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.
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();
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.
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 (%).
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:
- Open the HKEY_LOCAL_MACHINE registry key.
- Open the "Software" registry key. If the script is being run to deregister, then don't delete this key.
- Open the registry key that is defined by the
Company
parameter. On deregister, don't delete.
- On registration, first delete the key specified by the
AppID
parameter, and then add it again.
- 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_ROOT | HKCR |
HKEY_CURRENT_USER | HKCU |
HKEY_LOCAL_MACHINE | HKLM |
HKEY_USERS | HKU |
HKEY_PERFORMANCE_DATA | HKPD |
HKEY_DYN_DATA | HKDD |
HKEY_CURRENT_CONFIG | HKCC |
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.
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
.
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
string regScript = Almdal.AutoPlayListener.Properties.Resources.RegistryScript;
CRegistryScript script = new CRegistryScript();
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);
}
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:
- Initial version 1.0 - Sept. 18, 2006.
- Updated version 1.1 - Sept. 20, 2006 (Hopefully, I corrected more typo's than I created.)