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 context menu, when the selected item is a directory
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:
[ComVisible(true)]
[COMServerAssociation(AssociationType.AllFiles)]
[COMServerAssociation(AssociationType.Directory)]
public class DynamicSubMenuExtension : SharpContextMenu
{
private ContextMenuStrip menu = new ContextMenuStrip();
protected override bool CanShowMenu()
{
if (SelectedItemPaths.Count() == 1)
{ this.UpdateMenu();
return true;
}
else
{ return false;
}
}
protected override ContextMenuStrip CreateMenu()
{
menu.Items.Clear();
FileAttributes attr = File.GetAttributes(SelectedItemPaths.First());
if (attr.HasFlag(FileAttributes.Directory))
{
this.MenuDirectory();
}
else
{
this.MenuFiles();
}
return menu;
}
private void UpdateMenu()
{
menu.Dispose();
menu = CreateMenu();
}
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();
SubMenu1.DropDownItems.Add(SubSubMenu1);
MainMenu.DropDownItems.Add(SubMenu1);
MainMenu.DropDownItems.Add(SubMenu2);
menu.Items.Clear();
menu.Items.Add(MainMenu);
}
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();
SubMenu3.DropDownItems.Add(SubSubMenu3);
MainMenu.DropDownItems.Add(SubMenu3);
MainMenu.DropDownItems.Add(SubMenu4);
menu.Items.Clear();
menu.Items.Add(MainMenu);
}
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());
}
}
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.
private ContextMenuStrip menu = new ContextMenuStrip();
CanShowMenu Method
The underlying context menu item will be displayed only for a single selection.
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.
protected override ContextMenuStrip CreateMenu()
{
menu.Items.Clear();
FileAttributes attr = File.GetAttributes(SelectedItemPaths.First());
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.
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();
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.
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.
@ECHO OFF
ECHO
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