DEV Community

issam boutissante
issam boutissante

Posted on

Creating Shell Extensions in .NET 8 with SharpShell

In this blog, I'll walk you through creating a Windows shell extension using the SharpShell library in .NET 8. SharpShell is a popular library for creating shell extensions, but it was traditionally used with the .NET Framework. Thanks to the Microsoft Windows Compatibility Pack, it's now possible to create shell extensions in .NET 8.

Prerequisites

  • Visual Studio 2022 or later
  • .NET 8 SDK
  • Basic knowledge of C# and Windows shell extensions

Step 1: Create a .NET 8 Class Library

First, we need to create a .NET 8 class library. Open Visual Studio and create a new project:

  1. Select Class Library.
  2. Name the project SharpShellNet8Demo.
  3. Choose .NET 8.0 as the target framework.

Step 2: Modify the .csproj File

Next, we need to download and include the necessary packages: Microsoft.Windows.Compatibility and SharpShell. Modify the project file (.csproj) to target only Windows and include these packages:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <UseWindowsForms>true</UseWindowsForms>
    <EnableComHosting>true</EnableComHosting>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Windows.Compatibility" Version="8.0.4" />
    <PackageReference Include="SharpShell" Version="2.7.2" />
  </ItemGroup>

</Project>
Enter fullscreen mode Exit fullscreen mode

Why we use these properties:

  • <UseWindowsForms>true</UseWindowsForms>: Enables Windows Forms support in the project.
  • <EnableComHosting>true</EnableComHosting>: Enables COM hosting support, necessary for registering shell extensions.

Step 3: Create a Context Menu Extension

Next, we'll create a simple context menu extension. Add a new class to your project named SimpleContextMenu.cs and implement the following code:

using SharpShell.Attributes;
using SharpShell.SharpContextMenu;
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace SharpShellNet8Demo
{
    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    [COMServerAssociation(AssociationType.AllFilesAndFolders)]
    [COMServerAssociation(AssociationType.Directory)]
    [COMServerAssociation(AssociationType.DirectoryBackground)]
    [Guid("B3B9C5B6-5C92-4D1B-85E6-2F80B72F6E28")]
    public class SimpleContextMenu : SharpContextMenu
    {
        protected override bool CanShowMenu() => true;

        protected override ContextMenuStrip CreateMenu()
        {
            var menu = new ContextMenuStrip();

            var mainItem = new ToolStripMenuItem
            {
                Text = "Simple Extension Action"
            };

            var actionItem = new ToolStripMenuItem
            {
                Text = "Perform Action"
            };
            actionItem.Click += (sender, e) => MessageBox.Show("Action performed!");

            mainItem.DropDownItems.Add(actionItem);
            menu.Items.Add(mainItem);

            return menu;
        }

        [ComRegisterFunction]
        public static void Register(Type t)
        {
            RegisterContextMenu(t, @"*\shellex\ContextMenuHandlers\");
            RegisterContextMenu(t, @"Directory\shellex\ContextMenuHandlers\");
            RegisterContextMenu(t, @"Directory\Background\shellex\ContextMenuHandlers\");
        }

        private static void RegisterContextMenu(Type t, string basePath)
        {
            string keyPath = basePath + "SimpleContextMenu";
            using (var key = Microsoft.Win32.Registry.ClassesRoot.CreateSubKey(keyPath))
            {
                if (key == null)
                {
                    Console.WriteLine($"Failed to create registry key: {keyPath}");
                }
                else
                {
                    key.SetValue(null, t.GUID.ToString("B"));
                }
            }
        }

        [ComUnregisterFunction]
        public static void Unregister(Type t)
        {
            UnregisterContextMenu(@"*\shellex\ContextMenuHandlers\");
            UnregisterContextMenu(@"Directory\shellex\ContextMenuHandlers\");
            UnregisterContextMenu(@"Directory\Background\shellex\ContextMenuHandlers\");
        }

        private static void UnregisterContextMenu(string basePath)
        {
            string keyPath = basePath + "SimpleContextMenu";
            try
            {
                Microsoft.Win32.Registry.ClassesRoot.DeleteSubKeyTree(keyPath, false);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error unregistering COM server from {keyPath}: {ex.Message}");
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation of Attributes

  • [ComVisible(true)]: Makes the class visible to COM components.
  • [ClassInterface(ClassInterfaceType.None)]: Specifies that no class interface is generated for the class.
  • [COMServerAssociation(AssociationType.AllFilesAndFolders)]: Registers the shell extension for all files and folders.
  • [COMServerAssociation(AssociationType.Directory)]: Registers the shell extension for directories.
  • [COMServerAssociation(AssociationType.DirectoryBackground)]: Registers the shell extension for the background of directories.
  • [Guid("B3B9C5B6-5C92-4D1B-85E6-2F80B72F6E28")]: Assigns a unique identifier to the class. Generate this GUID using Tools > Create GUID in Visual Studio.

Step 4: Register and Unregister the Extension

To test our shell extension, we need to register and unregister it. Create two batch files, Register.bat and Unregister.bat, to handle this.

Register.bat

@echo off
:: Check for administrative privileges
net session >nul 2>&1
if %errorlevel% == 0 (
    echo Running with administrative privileges
) else (
    echo Requesting administrative privileges...
    goto UACPrompt
)

goto Start

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

:Start
cd /d "%~dp0"

:: Find DLL files ending with comhost.dll
for /f "delims=" %%a in ('dir /b *comhost.dll') do set "DLLFile=%%a"
if defined DLLFile goto Found

echo No DLL file ending with comhost.dll found, please enter the DLL name:
SET /P DLLName=Enter the DLL name to register (include .dll extension):
set DLLFile=%DLLName%

:Found
if not exist "%DLLFile%" (
    echo The specified DLL file does not exist.
    pause
    exit /b
)

regsvr32 /s "%DLLFile%"
taskkill /f /im explorer.exe
start explorer.exe
echo %DLLFile% registered and Explorer restarted.
pause
Enter fullscreen mode Exit fullscreen mode

Unregister.bat

@echo off
:: Check for administrative privileges
net session >nul 2>&1
if %errorlevel% == 0 (
    echo Running with administrative privileges
) else (
    echo Requesting administrative privileges...
    goto UACPrompt
)

goto Start

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

:Start
cd /d "%~dp0"

:: Find DLL files ending with comhost.dll
for /f "delims=" %%a in ('dir /b *comhost.dll') do set "DLLFile=%%a"
if defined DLLFile goto Found

echo No DLL file ending with comhost.dll found, please enter the DLL name:
SET /P DLLName=Enter the DLL name to unregister (include .dll extension):
set DLLFile=%DLLName%

:Found
if not exist "%DLLFile%" (
    echo The specified DLL file does not exist.
    pause
    exit /b
)

regsvr32 /u /s "%DLLFile%"
taskkill /f /im explorer.exe
start explorer.exe
echo %DLLFile% unregistered and Explorer restarted.
pause
Enter fullscreen mode Exit fullscreen mode

Step 5: Build and Register the Extension

  1. Build your project in Visual Studio.
  2. Navigate to the output directory (usually bin\Debug\net8.0-windows).
  3. Run Register.bat as an administrator to register the shell extension.
  4. Right-click on any file or folder to see the new context menu option "Simple Extension Action".

Step 6: Unregister the Extension

When you want to unregister the shell extension, run Unregister.bat as an administrator. If you want to make any modifications to the project, you should unregister it first to avoid build errors caused by the DLL being in use. To prevent this issue, consider registering a copy of the net8.0-windows output, not the original `net8.0-w

indows` directory.

Conclusion

Creating a shell extension in .NET 8 using SharpShell and the Microsoft Windows Compatibility Pack is straightforward. This tutorial covered setting up your project, writing the shell extension code, and handling registration and unregistration. Experiment with different types of extensions to enhance your Windows Explorer experience!

Feel free to reach out if you have any questions or run into issues. Happy coding!

Top comments (7)

Collapse
 
bd9000 profile image
David Nuss

I copied verbatim what you did, and it works from VS, however, nothing shows up outside (no dropdown menu item called "Simple Extension Action" is there).
I did this after doing a release build and copying the following files over to a different folder:

06/18/2024 08:12 PM 1,049 Register.bat
05/17/2024 03:09 PM 184,320 SharpShellNet8Demo.comhost.dll
06/18/2024 08:15 PM 45,552 SharpShellNet8Demo.deps.json
06/18/2024 08:15 PM 7,168 SharpShellNet8Demo.dll
06/18/2024 08:15 PM 13,752 SharpShellNet8Demo.pdb
06/18/2024 08:15 PM 493 SharpShellNet8Demo.runtimeconfig.json
06/18/2024 08:13 PM 1,056 Unregister.bat

Do I need all 89 files in the Release output for this to work?
Is there a way to eliminate a lot of these completely unused dlls in the output (like System.Speech.dll)?

Guess I need to learn C++ for Windows (a 17MB dll is a bit huge and redundant)

Collapse
 
bd9000 profile image
David Nuss

Looks like these are all the files you actually need to run (whew!), so if you wrote an installer for say, distributing to regular people, you could extract from a zip to a folder, then run register.bat.
Nice work, by the way!

06/18/2024 08:12 PM 1,049 Register.bat
11/06/2019 10:18 PM 451,072 SharpShell.dll
05/17/2024 03:09 PM 184,320 SharpShellNet8Demo.comhost.dll
06/18/2024 08:45 PM 45,750 SharpShellNet8Demo.deps.json
06/18/2024 08:45 PM 6,656 SharpShellNet8Demo.dll
06/18/2024 08:45 PM 493 SharpShellNet8Demo.runtimeconfig.json
10/31/2023 07:59 AM 294,160 System.ComponentModel.Composition.dll
06/18/2024 08:13 PM 1,056 Unregister.bat

Collapse
 
issamboutissante profile image
issam boutissante • Edited

Yes, you will need to include all of these DLLs; otherwise, you will encounter assembly not found exceptions. Unfortunately, because this is a class library, you can't use trimming to reduce the size. Therefore, you will need to have all the files.

Alternatively, you could create a console app and use it as a trimming helper for your library by referencing it there (I haven't tried this, but it might work). This is one of the cons of using .NET 8 with SharpShell. As you mentioned, C++ is a good option, but the ease of use with .NET SharpShell is worth considering despite the large size. Ultimately, it depends on your use case.

For example, in my case, we have many shell extensions, so they share all the DLLs. We also have .NET desktop apps that use the same DLLs, and we have them all centralized in one folder. So, the DLLs are already there; I only need to add my shell DLL and the SharpShell DLL. For the others, they are already there because they are being used by other apps.

Thank you for your Feedback David, and if you find any solution for reducing the size, hope you share it with us!


I tried to use only the files you mentioned @bd9000 and it's working perfectly with just less that 1MB

that's great!

Collapse
 
mike_rdz_a920f49809a53998 profile image
Mike Rdz

How to add icons?

Collapse
 
issamboutissante profile image
issam boutissante

Actually I didn't try the icons, but I will try to add an icon using the Image property and will keep you updated!

Collapse
 
chaimae_mazzouz profile image
Chaimae Mazzouz

great explanation, thanks a lot, that was helpful

Collapse
 
issamboutissante profile image
issam boutissante

Thank you :)