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

.NET Shell Extensions - Adding submenus to Shell Context Menus and Dynamically Loading Context Menu

4.33/5 (3 votes)
7 Oct 2015CPOL4 min read 22.8K   839  
Windows Shell Extension - Adding submenus to Shell Context Menus and dynamically loading them using sharpshell library.

Abstract

In this article, I have made an attempt to explain how to dynamically load Windows shell context menus and how to attach submenus to them via Sharpshell library using .NET code.

I will explain though an example, which loads different menu and submenus based on the selected item - a file or a directory as shown below.

Snapshot of windows shell extension, whenthe selected item is a directory

Snapshot of windows context menu, when the selected item is a directory

Snapshot of windows shell extension, whenthe selected item is a file

Snapshot of windows context menu, when the selected item is a file

Introduction

Recently, I discovered about Sharpshell library to build shell extensions and it's simply awesome, many thanks to author Dave Kare. I struggled a little to dynamically load the sub menu, thought a write-up on it would help someone, which is the reason behind this post.

What Do We Address Via This Post?

The problem with sharpshell library[Issue1, Issue2] till date is that the CreateMenu method which the library uses to build context menu is called only once, so loading the menus dynamically is not straightforward.

We address this by declaring the menu item as a class field instead of defining it in CreateMenu method and then we return this menu item from CreateMenu. We make use of another UpdateMenu method to update the context menus dynamically by in turn calling CreateMenu. Each time a file is seleted CanShowMenu method will be invoked to check if the underlying context menu should be displayed for the selected field. We call the UpdateMenu method inside CanShowMenu method whenever the condition to the underlying context menu is true. 

As we go through the example, you'll realize how simple it is.

We will see the post in 2 steps.

  • 1st step- How to load the menus/submenus dynamically
  • 2nd step- To add the submenus via sharpshell

Background

If you're not aware of Sharpshell Library, I strongly recommend you have a look at this awesome library, here I'm assuming you're familiar with creating context menu, registering and deregistering shell extensions (COM DLLs) via Sharpshell, if not you can have a quick read from the link below:

Code Overview

Before we jump into the steps, I want you to have a quick look at the code, so that you'll have a global idea on the code and you'll know which part I'm explaining. Have a quick glance at the below code:

C#
// <summary>
// The SubMenuExtension is an example shell context menu extension,
// implemented with SharpShell. It loads the menu dynamically
// files.
// </summary>
[ComVisible(true)]
[COMServerAssociation(AssociationType.AllFiles)]
[COMServerAssociation(AssociationType.Directory)]
public class DynamicSubMenuExtension : SharpContextMenu
{
    //  let's create the menu strip.
    private ContextMenuStrip menu = new ContextMenuStrip();

    // <summary>
    // Determines whether the menu item can be shown for the selected item.
    // </summary>
    // <returns>
    //   <c>true</c> if item can be shown for the selected item for this instance.; 
    // otherwise, <c>false</c>.
    // </returns>
    protected override bool CanShowMenu()
    {            
        //  We can show the item only for a single selection.
        if (SelectedItemPaths.Count() == 1)
        {   this.UpdateMenu();
            return true; 
        } 
        else 
        {   return false; 
        }
    }

    // <summary>
    // Creates the context menu. This can be a single menu item or a tree of them.
    // Here we create the menu based on the type of item
    // </summary>
    // <returns>
    // The context menu for the shell context menu.
    // </returns>
    protected override ContextMenuStrip CreateMenu()
    {
        menu.Items.Clear();
        FileAttributes attr = File.GetAttributes(SelectedItemPaths.First());

        // check if the selected item is a directory
        if (attr.HasFlag(FileAttributes.Directory)) 
        {
            this.MenuDirectory();
        }
        else
        {
            this.MenuFiles();
        }

        // return the menu item
        return menu;
    }

    // <summary>
    // Updates the context menu. 
    // </summary>
    private void UpdateMenu()
    {
        // release all resources associated to existing menu
        menu.Dispose();
        menu = CreateMenu();
    }

    // <summary>
    // Creates the context menu when the selected item is a folder.
    // </summary>
    protected void MenuDirectory()
    {
        ToolStripMenuItem MainMenu;
        MainMenu = new ToolStripMenuItem
        {
            Text = "MenuDirectory",
            Image = Properties.Resources.Folder_icon
        };

                ToolStripMenuItem SubMenu1;
                SubMenu1 = new ToolStripMenuItem
                {
                    Text = "DirSubMenu1",
                    Image = Properties.Resources.Folder_icon
                };

                var SubMenu2 = new ToolStripMenuItem
                {
                    Text = "DirSubMenu2",
                    Image = Properties.Resources.Folder_icon
                };
                SubMenu2.DropDownItems.Clear();
                SubMenu2.Click += (sender, args) => ShowItemName();

                        var SubSubMenu1 = new ToolStripMenuItem
                        {
                            Text = "DirSubSubMenu1",
                            Image = Properties.Resources.Folder_icon
                        };
                        SubSubMenu1.Click += (sender, args) => ShowItemName();
        
        // Let's attach the submenus to the main menu
        SubMenu1.DropDownItems.Add(SubSubMenu1);
        MainMenu.DropDownItems.Add(SubMenu1);
        MainMenu.DropDownItems.Add(SubMenu2);

        menu.Items.Clear();
        menu.Items.Add(MainMenu);
    }

    // <summary>
    // Creates the context menu when the selected item is of file type.
    // </summary>
    protected void MenuFiles()
    {
        ToolStripMenuItem MainMenu;
        MainMenu = new ToolStripMenuItem
        {
            Text = "MenuFiles",
            Image = Properties.Resources.file_icon
        };

                ToolStripMenuItem SubMenu3;
                SubMenu3 = new ToolStripMenuItem
                {
                    Text = "FileSubMenu1",
                    Image = Properties.Resources.file_icon
                };

                var SubMenu4 = new ToolStripMenuItem
                {
                    Text = "FileSubMenu2",
                    Image = Properties.Resources.file_icon
                };
                SubMenu4.DropDownItems.Clear();
                SubMenu4.Click += (sender, args) => ShowItemName();

                        var SubSubMenu3 = new ToolStripMenuItem
                        {
                            Text = "FileSubSubMenu1",
                            Image = Properties.Resources.file_icon
                        };
                        SubSubMenu3.Click += (sender, args) => ShowItemName();

        // Let's attach the submenus to the main menu
        SubMenu3.DropDownItems.Add(SubSubMenu3);
        MainMenu.DropDownItems.Add(SubMenu3);
        MainMenu.DropDownItems.Add(SubMenu4);

        menu.Items.Clear();
        menu.Items.Add(MainMenu);
    }

    // <summary>
    // Shows name of selected files.
    // </summary>
    private void ShowItemName()
    {
        //  Builder for the output.
        var builder = new StringBuilder();
        FileAttributes attr = File.GetAttributes(SelectedItemPaths.First());

        //  check if selected item is a directory.
        if (attr.HasFlag(FileAttributes.Directory))
        {
            //  Show folder name.
            builder.AppendLine(string.Format("Selected folder name is {0}", 
            Path.GetFileName(SelectedItemPaths.First())));
        }
        else
        {         
                //  Show the file name.
                builder.AppendLine(string.Format("Selected file is {0}", 
                Path.GetFileName(SelectedItemPaths.First())));            
        }

        //  Show the ouput.
        MessageBox.Show(builder.ToString());
    }
}

Steps

As I mentioned earlier, I have tried to explain the code in two parts as follows.

Step 1: How to Load Menus/Submenus Dynamically

Declare Menu Item

Firstly let's declare the menu item as a class field.

C#
//  lets create the menu strip.
private ContextMenuStrip menu = new ContextMenuStrip();

CanShowMenu Method

The underlying context menu item will be displayed only for a single selection.

C#
protected override bool CanShowMenu()
{   
    if (SelectedItemPaths.Count() == 1)
    {   this.UpdateMenu();
        return true; 
    } 
    else 
    {   return false; 
    }
}

CreateMenu Method

Here, we attach a different menu and submenu items based on the selection, if it's a directory, then MenuDirectory method will be called and MenuFiles method will be called if it's a file. These methods will generate the context menu items for the corresponding selection and will be attached to the main menu item, which will be displayed as the context menu.

C#
protected override ContextMenuStrip CreateMenu()
{
            menu.Items.Clear();
            FileAttributes attr = File.GetAttributes(SelectedItemPaths.First());

            // check if the selected item is a directory
            if (attr.HasFlag(FileAttributes.Directory)) 
            {
                this.MenuDirectory();
            }
            else
            {
                this.MenuFiles();
            }
    return menu;
}  

Though CreateMenu method is called only once, the UpdateMenu method is called each time when we are supposed to show the context menu, which will make the dynamic rendering of context menu possible.

Step 2: Adding submenus via sharpshell

Adding submenus is simple, we're doing it in both MenuDirectory and MenuFiles methods, we will see how we add sub menus for directory menu and the other one is almost similar.

As shown in the first figure in the introduction section, we add DirSubMenu1 and DirSubMenu2 submenus under MenuDirectory menu and SubSubMenu1 submenu under DirSubMenu1.

We create the MainMenu and attach it to menu item, to add submenu each time we create a ToolStripMenuItem item, define its properties and then we attach it as a dropdown to the parent menu item.

Here's how we implement it.

C#
protected void MenuDirectory()
{
    ToolStripMenuItem MainMenu;
    MainMenu = new ToolStripMenuItem
    {
        Text = "MenuDirectory",
        Image = Properties.Resources.Folder_icon
    };

            ToolStripMenuItem SubMenu1;
            SubMenu1 = new ToolStripMenuItem
            {
                Text = "DirSubMenu1",
                Image = Properties.Resources.Folder_icon
            };

            var SubMenu2 = new ToolStripMenuItem
            {
                Text = "DirSubMenu2",
                Image = Properties.Resources.Folder_icon
            };
            SubMenu2.DropDownItems.Clear();
            SubMenu2.Click += (sender, args) => ShowItemName();

                    var SubSubMenu1 = new ToolStripMenuItem
                    {
                        Text = "DirSubSubMenu1",
                        Image = Properties.Resources.Folder_icon
                    };
                    SubSubMenu1.Click += (sender, args) => ShowItemName();
    
    // Lets attach the submenus to the main menu
    SubMenu1.DropDownItems.Add(SubSubMenu1);
    MainMenu.DropDownItems.Add(SubMenu1);
    MainMenu.DropDownItems.Add(SubMenu2);

    menu.Items.Clear();
    menu.Items.Add(MainMenu);
}

ShowItemName Method

Here, we simply display the selected item.

C#
private void ShowItemName()
{
    var builder = new StringBuilder();
    FileAttributes attr = File.GetAttributes(SelectedItemPaths.First());

    if (attr.HasFlag(FileAttributes.Directory))
    {
        builder.AppendLine(string.Format("Selected folder name is {0}", 
        	Path.GetFileName(SelectedItemPaths.First())));
    }
    else
    {
        builder.AppendLine(string.Format("Selected file is {0}", 
        	Path.GetFileName(SelectedItemPaths.First())));
    }
    MessageBox.Show(builder.ToString());
}

Registration

There are many ways to register COM DLLs which we get on building our project as you can see in Dave's article, I usually prefer Microsoft's Regasm tool, but during the development stage, I prefer using Server Manager Tool, we can test the DLL without registering into system which is cool and there is an option to attach debugger.

If you're planning to deploy sharpshell server in your project, you should try Server Registration Manager, the registration process is quite simple, with Advanced installer I had some problem with it though.

Registration / Unregistration using Regasm via Batch Scripts

If you are using regasm for registering the shell extension, creating a batch script would make the registration / unregistration process easier.

Here's a small batch script for registering DLL, you can find one for deregistration in the attachments of this post. Just make sure to place the batch scripts in the same folder as the DLL file.

PowerShell
@ECHO OFF
ECHO ###    Register DLL     ###
REM  --> code from https://sites.google.com/site/eneerge/scripts/batchgotadmin

:: BatchGotAdmin
:-------------------------------------
REM  --> Check for permissions
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"

REM --> If error flag set, we do not have admin.
if '%errorlevel%' NEQ '0' (
    echo Requesting administrative privileges...
    goto UACPrompt
) else ( goto gotAdmin )

:UACPrompt
    echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
    echo UAC.ShellExecute "%~s0", "", "", "runas", 1 >> "%temp%\getadmin.vbs"

    "%temp%\getadmin.vbs"
    exit /B

:gotAdmin
    if exist "%temp%\getadmin.vbs" ( del "%temp%\getadmin.vbs" )
    pushd "%CD%"
    CD /D "%~dp0"
:--------------------------------------

REM  --> Check OS and register accordingly 
:CheckOS
reg Query "HKLM\Hardware\Description\System\CentralProcessor\0" | 
find /i "x86" > NUL && set SysOS=32BIT || set SysOS=64BIT
    
if %SysOS%==32BIT ( 
"%windir%\Microsoft.NET\Framework\v4.0.30319\regasm.exe" /codebase %cd%\DynamicSubMenus.dll 
) else (
"%windir%\Microsoft.NET\Framework64\v4.0.30319\regasm.exe" /codebase %cd%\DynamicSubMenus.dll )

:END

PAUSE
@ECHO ON

License

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