Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Explorer Shell Extensions in .NET (Revised)

4.92/5 (12 votes)
16 Sep 2015CPOL22 min read 37.5K   1.9K  
Framework for NET based Shell Context Menus using VB, C#

Introduction

This article will provide an updated or enhanced version of the code from: How to Write Windows Shell Extension with .NET Languages by MS-Pl (Microsoft) along with step-by-step instructions on how to customize and implement it for your app. All the hard work was done there - updated to make it easier to implement.

All the copyrights, warnings and additional files remain in the code and project and it remains the work of MS-Pl.

Image 1

Background

The article cited covers the requirements for implementing a Shell Extension for an Explorer Context Menu with a sort of demo project in VB and C# are included. The steps to rework the demo into a practical extension and send more than one file is somewhat unclear.

In the course of reworking that codebase for something, a few issues emerged and several refinements were made. The code was also re-factored to allow it to act as a sort of template or plug in which can be easily customized for each implementation simply by changing the value of a few variables.

The model is one of a helper DLL providing the Context Menu which starts an associated EXE passing the selected files on the command line.

The ability for Dynamic and Self Registration has also been added. This allows your main app to register the DLL or register different extensions as needed. This functionality is provided by a new ShellReg helper class which would be used by your main app.

Do You Really Need A Shell Extension?

Since Explorer Context Menus can easily be implemented without a Shell Extension, the answer may actually be "no".

At the end of this article, there is a brief overview of some of the issues regarding NET based Shell Extensions. You should inform yourself of the issues before implementing a NET Shell Extension (rather than a standard Context Menu). Given the need for a Shell Extension, the modified code template provided here should make it fairly simple.

In the course of things, the means to make Registry based (standard) context menus easier to use emerged resulting in a companion article Explorer Context Menu Manager, a helper class to add, change and delete Registry based Explorer Context Menus from your app.

Change Summary

  1. Most notable is the ability to register more than one extension, start a "main" app rather than just showing a Dialog, and passing more than one file to the target app.
  2. Several fixes for Option Strict (mostly just .ToString and Convert.Toxxxx).
  3. The original code did not anticipate custom extensions (".foo" vs ".txt"). As a result, a NullReference Exception could result in the RegisterShellExtContextMenuHandler and UnregisterShellExtContextMenuHandler methods. I think this was unique to the VB version.
  4. Normally, Explorer will start the helper DLL in a System directory; your DLL in turn will start the main app in that same directory. This was modified to start both with the folder they reside in as the current directory so that any other required DLLs can be found.
  5. A simple logger has been added for debugging.
  6. Added a demo/test MainApp. The app just displays the arguments (files) passed, but allows you to easily verify that the base framework works. In addition to acting as a test target, it includes the demo code for self-registration.
  7. Fixed argument processing to handle embedded spaces correctly.
  8. Added code to (hopefully) render backgrounds pseudo-transparent.
  9. Combined the VB and C# projects into 1 solution. The VB and C# DLL output was made interchangeable; mostly jiggering the VB Namespace method.
  10. Most of all, refactored the code to use a set of control variables so that, in most cases, the only code modifications needed will be to change some of these values. These are located as a block at the top of the MyAppShellMenu class in FileContextMenuExt.

New functionality added includes Dynamic self-registration: Register, UnRegister or modify the extensions associated with your Shell Extension DLL from the related main app.

This article will focus on the steps required to implement this base framework rather than rehash previous ground. For a more complete basic understanding of Shell Extensions, read the original article.

VERY IMPORTANT NOTE

The changes to the original codebase are primarily to make it easier to quickly produce a working Shell Extension helper. But 'easier' does not mean 'trivial'. Unlike a typical VB/NET project, it is tedious to compile and test a Shell Extension: once you right click on an extension your handler supports, your DLL will be in use by Explorer which prevents you from simply recompiling and replacing the old version. Unregistering it does not release it.

Take the time to read and digest the article and steps. Apply your code changes carefully and thoughtfully before compiling and testing. Additional notes on debugging and usage are provided after the step-by-step instructions.

Using the Code

Project Setup

The framework is intended primarily as a separate helper DLL for a main app. For example, a dedicated MyAppShellExt.DLL which works with Explorer to start MyMainApp.EXE, passing the selected files via the command line. This is a fairly common model -- you often see xxxShell.DLL files stored with EXE files to provide this type of service (though the reason for this is sometimes that the DLL is in a different language).

Explorer will start your ShellExt DLL with a System folder as the working directory. In order to be able to find the related app to start, the ShellExt DLL must reside in the same folder as the app.

1. Change the Assembly name

  • Go to Project Properties -> Application and change the Assembly name (This is not absolutely required, but having the DLL name related to the main app makes sense.)
  • Optionally, you may want to also change the Assembly information so it references your work.
  • DO NOT change the NET Framework version. It must be NET 4.0 or higher (see the original article for details).

2. Configure the project for AnyCPU

  • This will allow the DLL to register on 32 or 64bit OS.
  • If the MainApp must run as 32bit (perhaps it uses 32bit DLLs), compile the target app to x86. As far as I know, no one has yet worked out a sure way for a 64bit process to force an AnyCPU app to start as a 32bit process (or vice versa).

Open ContextMenuExt.vb|cs for the rest of the changes. No changes are required to any other file.

VB.NET
<Guid("679944C8-FEC8-446A-8089-64E0DE515898"),
ClassInterface(ClassInterfaceType.None),
ComVisible(True)>
Public Class MyShellMenu
    '...

3. Create your own class GUID (see Tools Menu, Create GUID) and replace the one listed.

The new GUID assures that your handler is uniquely identified.

4. Change the class name

The demo/framework uses MyShellMenu. You likely want the names to relate to the associated app:

Main App: FizzBar.EXE
Assembly: FizzBarExt.DLL or FizzBarShell.DLL
Class: FizzBarContextMenu or FizzBarMenu

I don't think these need to be unique since the GUID is used as the identifier. But a meaningful name makes sense to associate the assembly-class with the main app they are associated with. This is particularly true if/when using the debug log.

The VB IDE manages Namespaces differently than C# projects. In order for the VB and C# DLLs to be interchangeable, the VB DLL project implements Namespaces more in the manner of C#. That is, they are manually declared in both classes in the DLL project.

There generally should be no reason to change the NameSpace in either C# or VB.

Control Variables

To control or change various options, there is a block of control variables at the top of the MyShellMenu class. The section looks like this:

VB.NET
<Guid("679944C8-FEC8-446A-8089-64E0DE515898"),
ClassInterface(ClassInterfaceType.None), ComVisible(True)>
Public Class MyShellMenu
    Implements IShellExtInit, IContextMenu

    ' Everything you may want to change should be between the star bars
    '*****  *****  *****

    ' {PL} list of file exts to associate with this handler
    Private Shared exts As String() = {".txt", ".foo", ".log", ".lst"}

    ' The base name of the app to run - do not hard code a path!
    ' The code will figure out where it is.
    Private AppName As String = "MyMainApp.exe"

    ' menu text and such
    Private menuText As String = "Open in MyMainApp"
    Private verb As String = "loadfiles"
    Private verbCanonicalName As String = "OpenInMyMainApp"
    Private verbHelpText As String = "Open Files in MyMainApp"

    ' If you want to send only the file clicked on, change to TRUE
    Private FeedFirstFileOnly As Boolean = False

    ' simple step logger for debugging...you'll find it in MyDocuments
    Private LogFile As String = "ShellExtDebug.log"
    ' Be sure to set to false for release version
    Private LogActive As Boolean = False
    ' Start a new Log each time or append
    Private LogAppend As Boolean = False

    Private AddSeperator As Boolean = False

    ' Resource name of the image you wish to use
    Private bmpName As String = "starBlue"

    ' oldBackColor == the back color of the Resource image,
    '   typically Transparent or Fuchsia
    ' newBackColor == Color to change all oldBackColor pixels to
    '   Nothing (default) == use default ContextMenuStrip.BackColor
    Private oldBackColor As Color = Color.Transparent
    Private newBackColor As Color? = Nothing

    ' end of stuff to change region for simply sending files
    '*****  *****  *****

    ' {PL} The names of the selected file
    Private selectedFiles As List(Of String)

    Private targetApp As String = ""

    Private menuBmp As IntPtr = IntPtr.Zero
    Private IDM_DISPLAY As UInteger = 0

    Private Shared AsmHandlerName As String = ""


    Public Sub New()     ' in C#:  public MyShellMenu()
       ...

Note that if you followed the project-level instructions above, the GUID and the Class name will be different.

Descriptions
VB.NET
Private Shared exts As String() = {...}

This is the list of extensions your ContextMenu is to respond to. If there is just one, list only one in the array literal. This is used when the DLL is registered, so leave it an array, leave it Shared and use lower case.

VB.NET
Private menuText As String = "Open in MyMainApp"
Private verb As String = "loadfiles"
Private verbCanonicalName As String = "OpenInMyMainApp"
Private verbHelpText As String = "Open Files in MyMainApp"

These are the text and such used for the menu display and identifying your menu handler.

VB.NET
Private FeedFirstFileOnly As Boolean = False

There could be niche cases where someone wants to only send a single file to the MainApp even when multiples are selected. In such a case, set FeedFirstFileOnly to True. The file which the user clicks on should be the only one sent. (If you are sending one file, you should really implement a simple Context Menu.)

VB.NET
Private LogFile As String = "MyShellExtDebug.log"
Private LogActive As Boolean = False
Private LogAppend As Boolean = False

Since a Shell Extension can be tedious to debug, a simple Log mechanism is provided to report progress in the main methods involved. LogFile is the name of the file to use and will be created in My Documents. Once you have your menu handler working as you want, you can set LogActive to False. LogAppend controls whether the log is appended to each session or only contains the reports from the last run.

Don't assume all failures to launch are the result of a problem in the Shell Extension! On more than one occasion, after several cycles of unregistering the Shell Ext and restarting Explorer, the problem turned out to be caused by "a minor refinement" in the main app. The MyMainApp project exists to help verify the core extension DLL works.

VB.NET
Private AddSeperator As Boolean = False

Simple True/False whether you want to add a separator after your entry.

VB.NET
Private bmpName As String = "starBlue"

The 16x16 image name, if any, in Resources you wish to use on your menu. Set to empty string to omit an image.

VB.NET
Private oldBackColor As Color = Color.Transparent
Private newBackColor As Color? = Nothing

No matter the format or method, images cannot seem to be drawn with a transparent background. This is not immediately apparent in the MS code because they used an image with almost no background. An informal survey of Shell Extensions reveals that many have the same problem, including Adobe.

The solution, such as it is, would seem to be to manually map any Transparent pixels to the background color of the menu. But there is virtually no information to be found regarding the Explorer or system menus and how to get their BackColor. There is also the chance that the value may be ignored when themes are used.

A solution which seems to work is to use the default back color of a ContextMenu. The premise being that NET/Windows somehow, somewhere uses the same default color for all such menus, themes or no themes. (This may not be universally true, I have a limited number of OS versions and theme mixes to be certain).

newBackColor is the BackColor to use in place of the oldBackColor. A default of Nothing results in the default BackColor of a ContextMenu to be used.

oldBackColor is the BackColor in the image. Typically this is Color.Fuchsia as a placeholder on a bitmap, though Transparent will also work on simple, clean PNG files.

Should you acquire more information regarding this, the GetPseudoTransparentBitmap() function may be all that needs to be changed.

Note to VB developers: As described later, there may be some performance advantages to using the C# version. The variable block above is nearly identical, but modifying the C# version also requires changing the constructor and destructor to use the new class name:

C#
public MyShellMenu()        //  constructor
{
...

~MyShellMenu()                // destructor
{

If you changed the class name to FizzBarMenu, these must both be changed as well:

C#
public FizzBarMenu()        // case sensitive!
...
~FizzBarMenu()

That's it! Tweaking those few variables should be all that is needed to convert the generic template into a working Shell Extension. The next step is to compile and test your extension helper and main app combo. Read the following topics for more information.

Image 2

Create A Test Folder

Never run or register your Shell Extension from a VS debug folder. This can give you a false view of how well it works and will prevent you from recompiling the Shell Handler. Once invoked, the Shell Extension remains in use by Explorer until you reboot/restart Explorer. UnRegister won't cause Explorer to release it.

Use a Test folder similar to the runtime environment such as C:\Program Files\Test Project.

Note that you cannot simply test a new compile from a new folder (e.g. C:\Program ...\prj name 2) unless you also create an entirely new handler (GUID, Assembly.Class Name, etc).

Registering

You will need to register your Shell Ext to test it. The MS-PL project includes some VS2010 installer projects, but these are not available in VS2012 and beyond.

To register your DLL: find the RegAsm for your NET version (typically, Microsoft.NET\Framework64\v4.0.xxxx). If you start your command window from Start Menu | Visual Studio Tools | Developer Command Prompt, it should start with the correct RegAsm in the path. Be sure to Run as Admin and open an x64 window if your OS is 64bit.

In the command window with Administrator rights, navigate to where your Shell DLL and related app are located:

VB.NET
regasm MyAppShellExt.dll /codebase

This associates your ShellExt Class/DLL with the file extensions specified in the code. You will need

to do the same via an installer or script when deploying your app. To UnRegister:

VB.NET
regasm MyAppShellExt.dll /u

If you get an error that the DLL is not a valid NET assembly, in this case it may mean there is a bitness mismatch with the assembly. If RegAsm reports success, but your entry does not show on the Context Menu, you may have used an x86 window/RegAsm on a 64bit OS.

Once you test your Shell Extension, Explorer will have it open and you will not be able to copy a new build over it. You cannot just close File Explorer, unfortunately.

Extension Exclusions

When the user right-clicks on a file with an extension your Shell Extension supports, your menu will display. However, there can be many files selected, some with extensions which are not registered by your app. These "other" files are also passed to your app (when FeedFirstFileOnly is False).

This is not a bug. All the Context Menu Handler code cares about is the file under the mouse when clicked.

Unsupported files could be filtered out by extension before they are passed to your Main App. To me, it is better to keep the DLL simple and handle any filtering in the actual app which can be updated at will and much more easily than the helper DLL. It is easier for your app to provide robust handling such as a "Try Anyway?" dialog to allow for cases when the user knows the app will work even though you may not have anticipated it.

Debug a Shell Extension

This usually works...

  • Change to Debug config; x64 on a 64bit OS
  • Set the desired breakpoints in your code
  • On the Tools or Debug menu (it is on both in mine), select 'Attach to process',
  • Select 'Explorer.exe' from the list of processes

You will have to run Visual Studio As Administrator. I can't work out why it doesn't work sometimes, but it may have been that VS was not running as Admin or the configuration was wrong (x86).

Explorer will now act as your Test Project to invoke your Shell Extension. This is more useful with DLLs which do more than pass a command line. Feeding files is so simple that a log file or even MessageBox will usually suffice. The revised code provides for simple logging.

How to Stop / Restart Explorer for Testing

One way, is to Log Off then right back on. This will reset Explorer but will also require you close all apps and processes.

- OR -

  • Using your Admin Command Prompt Window:
VB.NET
taskkill /F /IM explorer.exe
  • Unregister your old version
  • Start a new Explorer from a non ADMIN command window (see note):
VB.NET
start C:\Windows\explorer.exe
  • Copy the new version to your test folder and re-register.

IMPORTANT NOTE

If you restart Explorer from a command window which has Admin rights, the apps you start from Start Menu will inherit those Admin rights which may not be what you want. Environment folder paths may resolve to something other than your normal Windows Logon!

Killing Explorer will also stop all other Extension type apps and remove many SystemTray icons.

Dynamic and Self-Register

The demo includes a nascent class to allow the target app to Register/UnRegister the Shell Extension DLL. This can allow your main app to toggle its Explorer ContextMenu as a user option.

Another use for this is to allow the user to select the desired extensions for your ContextMenu. For instance, from a list of {".txt", ".foo", ".log", ".lst", ".bar"}, the user might select all but ".foo" or only ".log" to associate with your Context Menu.

ShellReg Class

ShellReg is a helper class for your MainApp to invoke the registration methods in your shell extension DLL. In all cases, the app using these methods (typically, your MainApp.exe) must reside in the same directory as the target DLL. Note that ShellReg invokes new methods in your Shell Extension DLL, if you tinker with the names in one or the other, things may break.

ShellReg Methods

Each method requires the name of the DLL and Context Menu class which are passed in the constructor:

VB.NET
Private asmName As String = "MyShellExt.dll"
Private className As String = "ShellExtContextMenuHandler.MyShellMenu"
...
Dim myShReg As New ShellReg(asmName, className)

Note that the Namespace is integral to the class name.

Sub RegisterShellExt()

Standard registration of your Context Menu helper DLL. Example:

VB.NET
myShReg.RegisterShellExt()

This will attempt to run RegEdit as an elevated process to register your Shell Extension DLL. If the user denies permission, it won't run.

Sub UnRegisterShellExt()

Unregisters your shell menu class (e.g. MyShellMenu). Example:

VB.NET
myShReg.UnRegisterShellExt()

This too will attempt to run RegEdit as an elevated process to unregister your Shell Extension DLL. If the user denies permission, it won't run.

Function GetShellExtList() As String()

Gets the static list of extensions defined in your DLL class. This prevents you from having to code a second set in your MainApp possibly resulting in mismatched lists.

VB.NET
Dim allExts As String() = myShReg.GetShellExtList()

Sub RegisterExtensions(newExts As String())

This will (re)register your context menu class with those extensions passed which are also in the master shared/static list. That is, if the list of extensions in MyAppShellExt is {".txt", ".foo", ".log", ".lst"}, then any subset of that list can be passed.

You cannot dynamically expand the list, for example adding ".baz", because there is no (easy) way to later remove that extension association. In order to register a new (sub)set of extensions, the old list is first unregistered (the method does this for you). It is easier, simpler and faster to work from a defined master list rather than check every extension in the registry just in case a new one was added. The Shared extension array in your DLL is the master list.

VB.NET
Dim newExts As String() = {".txt", ".log", ".lst"}
...
myShReg.RegisterNewExtensions(newExts)

This relies on a method I added to the core Shell Registration code. It is not something standard with all Shell Extensions.

This will also require Admin rights since it will ultimately be making changes to HKCR in the registry. However, the app can do this by invoking a new method in the Shell Extension DLL, so the UAC presented to the user will be for your App.

How this is works is probably best understood by examining the MyMainApp code. The button click events in the TestForm start a new elevated instance of the MyMainApp with /reg on the command line.

Now, switch over to Sub Main in Program.vb. When a switch is detected in args(), rather than starting the WinForms app, it invokes the appropriate method in the Shell Extension DLL, then terminates. (This assumes the end user grants permission for MyMainApp to run elevated). In some cases, it might be simpler to create a small console app to perform the registry tasks, but this method avoids another helper assembly.

These work pretty well with a few limitations:

  1. As before, the app which will use these must be a 64bit process if the OS is 64bit, otherwise none of the registry actions will work (or rather, they will be incorrect and not be seen by Explorer).
  2. As noted, any application wanting to use ShellReg methods must have elevated access since it makes changes to the Registry. See the demo for how to appear to request elevated rights for the current app.
  3. UnRegister seems to work every time to disable the related ContextMenu helper.

The Demo

Image 3

The demo is more of a test app. It is not meant to be run from the VS IDE Debugger. As described above, you do not want to register files in your VS project folders as Explorer Extensions. Instead, compile the app and copy it and the related DLL to a test folder.

The DLL needs to be registered with Explorer, and the DLL expects to find the related EXE to start in the same folder as it. Normally, the Extension starts in the Windows\System folder, which in turn will start the target app in the same folder. One of the changes noted earlier is that the Extension DLL changes the current directory so that when the target app starts any other dependencies can be found. For this to work, they need to be in the same folder.

The files of course can be anywhere. Some simple, empty text files with various extensions can be found in the Test Files folder in the zip.

The Test App is simple and basic. It is not Single Instance and won't forward the commandline to an existing instance. However, that is covered in a companion article and elsewhere such as this StackOverflow question.

Summary

The focus of the original article dealt with implementing the interfaces and code required for a ContextMenu provider Shell Extension. As a result, the demo code illustrates only that in a rather straight forward fashion without regard to code reuse.

The intention of this revision was to convert that code into a reusable template of sorts to make implementing a custom menu easy and fast. Aside from a few steps to create unique names and identifiers, this can be done with a few changes to a small block of variables.

Do take care with the changes you need to make. As noted earlier, easy does not mean trivial.

Project Files

CSShellExtension

C# project files to create the demo Shell Extension handler

MyMainApp

VB Project to act as a stand-in for a main app and display the files passed on the commandline. Will work with either (or both) shell extension DLLs.

VBShellExtension

VB project files to create the demo Shell Extension handler

Both the VB and C# projects include installer files which were part of the original article/project. Also included is the original ReadMe.

Resources and References

Addendum: NET Based Shell Extensions

There are a number of articles warning about using managed code (.NET) for Shell Extensions. A series of articles on Microsoft's All In One Code site demonstrating NET Shell Extensions would seem to be contradictory, but that entire series of NET based Shell Extensions have apparently been withdrawn. The one dealing with a Context Menu extension was reposted here as the original article on which this one is based.

The Articles

Reading the articles is advised, but the gist seems to be:

  1. NET extensions must load the NET runtimes which slows Explorer down
  2. Assorted issues with the CLR and how it works being at odds with COM, interfaces and the unmanaged host
  3. Depending on the specific extension type (there are several), the likelihood of one or another may increase
  4. There can be resource disposal issues with some extension types

Read the articles and decide for yourself how to proceed. Here are some obvious suggestions:

Keep It Simple

There are many types of Shell Extensions, and most are much more ambitious than a Context Menu handler which launches an app. A Thumbnail Handler, a Property Sheet Handler or an Extension which might include a number of forms and classes to directly provide the desired service, would seem more likely to encounter problems.

There just is not that much going on in a Context Menu launcher: it provides a structure describing the menu item and then responds when that menu item is clicked. Still, once instantiated it becomes part of the system shell, and every app inherits them when using things like a OpenFileDialog.

Net Shell Extensions are slower - a performance hit is noticeable with just 1 or 2 in use. Shell Extensions are a pain to develop, test and debug. That alone is enough to avoid them.

Use a Simple Context Menu

As noted, there are very few differences between a basic Explorer Context Menu and the one provided by a Shell Extension. Natively, the only advantage to a Shell Extension is that multiple files can be sent to one target app instance. VB especially makes it very easy to "forward" arguments to the first instance of an app.

The companion article Explorer Context Menu Manager provides a helper class to help make a Registry based static Context Menu flexible so you can avoid a Shell Extension in many situations.

Use the C# Version

In order to minimize what Explorer needs to load, consider using the C# version. This should prevent having to load additional VisualBasic related assemblies. Simple observations of Explorer's memory usage seems to indicate about 22k less for the C# version.

The C# version of the above code is laid out the same and requires only one additional step to customize for your use. The variable block is identical and you can use the VB version as a Rosetta Stone. It simply is not a monumental undertaking.

All things considered, it is probably best to use a NET Shell Extension only as a last resort. In most cases, a simple Context Menu is not only simpler, they have no issue with transparent backgrounds and will work just fine.

History

  • 2015.09.09 - Initial post

License

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