Click here to Skip to main content
15,883,758 members
Articles / Programming Languages / C#

Register/Unregister .NET Asseblies into GAC Using Shell Extensions

Rate me:
Please Sign up or sign in to vote.
3.57/5 (6 votes)
25 May 2007CPOL5 min read 61K   304   31   12
Registering .NET assemblies into GAC (Global Assembly Cache) using Shell extension context menus.

Screenshot - contextMenu.jpg

Introduction

In this article, I will try to demonstrate how we can use a Shell extension to create custom menus in Windows Explorer in Visual C#. We will create a menu that will be used to do the registration of a .NET assembly into the Global Assembly Cache.

Background

During the last couple of months, I was doing work on a project which is based on Windows SharePoint Server, SQL Server (Integration Services) etc., and those who work with SharePoint development (i.e., developing Web Part etc.) must already have noticed that we need to register our assemblies into the Global Assembly Cache too often. In fact, I believe during the last couple of months, if I analyzed my work, I would find the most repetitive task that I did so far is registering assemblies into the GAC. Well, today, I don't have much work pressure (I am lucky because that's a rare scenario), which made me think - is there something I can do to make this registration process simpler and faster? I found some cool topics (most of them blogs written by intelligent people) when I was searching on this issue. One of them attracted me, and it was doing nothing but putting some Registry entries to accomplish the task. I have taken the idea from this blog and written some code to make it a bit attractive. Basically, I have just implemented a Shell extension context menu in order to accomplish the task.

Creating a Shell extension context menu

The .NET Framework is still not a native part of the Windows Operating System. Specifically, the core APIs are almost made of unmanaged Win32 code. Therefore, writing a Shell extension in a managed language is not too easy. You need a solid background on COM Interoperability provided by .NET Framework in order to play with the Windows Shell. Especially, you need to know how the Runtime Callable Wrapper (RCW) and COM Callable Wrappers (CCW) work. Explaining these stuff is, of course, beyond the scope of this article; therefore, you need to go to MSDN if you already don't have any idea about these technologies. Another good start could be this article which impressed me a lot.

Generally speaking, if we want to write a Shell extension in managed code using the COM Interop layer (provided by the .NET Framework), we need to do the following things:

  • We need to import Win32 native structures, types, and interfaces into our .NET application. Basically, we need t o define a managed prototype of these unmanaged elements.
  • We need to declare the Win32 API signature into a managed form. But this is optional - it will simply make our code more structured.
  • We need to write a class that implements certain COM interfaces for the Shell extension.
  • An finally, we need to register the output assembly through the RegAsm.exe utility

Now let's figure out the details.

First of all, let's create a new project using the Class Library project template. We will add a new class and will define the Win32 structures, interfaces that we will use inside our project to create the shell context menu. We can define a new class file, and then we can start defining the prototype as follows:

C#
public enum MIIM : uint
{
    STATE =            0x00000001,
    ID =            0x00000002,
    SUBMENU    =        0x00000004,
    CHECKMARKS =    0x00000008,
    TYPE =            0x00000010,
    DATA =            0x00000020,
    STRING =        0x00000040,
    BITMAP =        0x00000080,
    FTYPE =            0x00000100
}

[StructLayout(LayoutKind.Sequential)]
public struct MENUITEMINFO
{
    public uint cbSize;
    public uint fMask;
    public uint fType;
    public uint fState;
    public int    wID;
    public int    /*HMENU*/      hSubMenu;
     public int    /*HBITMAP*/   hbmpChecked;
    public int    /*HBITMAP*/      hbmpUnchecked;
    public int    /*ULONG_PTR*/ dwItemData;
    public String dwTypeData;
    public uint cch;
    public int /*HBITMAP*/ hbmpItem;
}

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct INVOKECOMMANDINFO
{        
    public uint cbSize;             // sizeof(CMINVOKECOMMANDINFO)
    public uint fMask;              // any combination of CMIC_MASK_*
    public uint wnd;                // might be NULL (indicating no owner window)
    public int verb;
    [MarshalAs(UnmanagedType.LPStr)]
    public string parameters;        // might be NULL (indicating no parameter)
    [MarshalAs(UnmanagedType.LPStr)]
    public string directory;        // might be NULL (indicating no specific directory)
    public int Show;                // one of SW_ values for ShowWindow() API
    public uint HotKey;
    public uint hIcon;
}

[ComImport(), 
InterfaceType(ComInterfaceType.InterfaceIsIUnknown), 
GuidAttribute("000214e8-0000-0000-c000-000000000046")]
public interface IShellExtInit
{
    [PreserveSig()]
    int Initialize (IntPtr pidlFolder, 
    IntPtr lpdobj, uint /*HKEY*/ hKeyProgID);
}


[ComImport(), 
InterfaceType(ComInterfaceType.InterfaceIsIUnknown), 
GuidAttribute("000214e4-0000-0000-c000-000000000046")]
public    interface IContextMenu
{
    [PreserveSig()]
    int    QueryContextMenu(uint hmenu, uint iMenu, int idCmdFirst, 
    int idCmdLast, uint uFlags);
    [PreserveSig()]
    void    InvokeCommand (IntPtr pici);
    [PreserveSig()]
    void    GetCommandString(int idcmd, uint uflags, 
    int reserved, 
    StringBuilder commandstring, int cch);
}
    
// There are more enum and structures we need to define
// but not writing here..please take thoes from
// accompanying source archive.

As you can see, these are simply the prototypes or managed definitions of unmanaged Win32 language elements. (There are more structures and enumerations that we need to define here, but not given in the snippet above. Please collect them from the accompanying source.) We will write a new class inside which we will define the Win32 API methods that we will use later. So I named that class Win32Helpers. The code snippet of this class is given below:

C#
public class Win32Helpers
{
    [DllImport("kernel32.dll")]
    internal static extern Boolean 
    SetCurrentDirectory([MarshalAs(UnmanagedType.LPTStr)]string lpPathName);

    [DllImport("kernel32.dll")]
    internal static extern uint 
    GetFileAttributes(
    [MarshalAs(UnmanagedType.LPTStr)]string lpPathName);
    
    internal const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010;

    [DllImport("kernel32.dll")]
    internal static extern Boolean 
    CreateProcess(
        string    lpApplicationName,
        string    lpCommandLine,
        uint    lpProcessAttributes,
        uint    lpThreadAttributes,
        Boolean bInheritHandles,
        uint    dwCreationFlags,
        uint    lpEnvironment,
        string    lpCurrentDirectory,
        StartupInfo lpStartupInfo,
        ProcessInformation lpProcessInformation);

    [DllImport("shell32")]
    internal static extern uint 
    DragQueryFile(uint hDrop,uint iFile, 
    StringBuilder buffer, int cch);

    [DllImport("user32")]
    internal static extern uint CreatePopupMenu();

    [DllImport("user32")]
    internal static extern int MessageBox(int hWnd, string text, 
    string caption, int type);

    [DllImport("user32")]
    internal static extern int InsertMenuItem(uint hmenu, uint uposition, 
    uint uflags, ref MENUITEMINFO mii);
}

It's time to write a managed class that will implement the unmanaged Shell extension interfaces IShellExtInit and IContextMenu. I named this class as ContextMenuManager. Remember, as this class is implementing a COM interface, and in fact the class will work as a COM object, we need to define a GUID for the class. So generate a new GUID from Visual Studio .NET and replace the one that I have written here.

C#
[Guid("33612C08-B156-4ad2-9599-049A685B8CD0")]
public class ContextMenuManager : IShellExtInit, IContextMenu
{
    protected const string guid = "{33612C08-B156-4ad2-9599-049A685B8CD0}";

Now let's implement the interface members one by one.

C#
int IContextMenu.QueryContextMenu(uint hMenu, uint iMenu, 
                 int idCmdFirst, int idCmdLast, uint uFlags)
{       
    // Create the popup to insert
    uint handleMenuPopup = Win32Helpers.CreatePopupMenu();

    int id = 1;
    if ( (uFlags & 0xf) == 0 || 
    (uFlags & (uint)CMF.CMF_EXPLORE) != 0)
    {
        uint nselected = 
        Win32Helpers.DragQueryFile(m_hDrop, 0xffffffff, null, 0);
        if (nselected == 1)
        {
            StringBuilder sb = new StringBuilder(1024);
            Win32Helpers.DragQueryFile(m_hDrop, 0,
             sb, sb.Capacity + 1);
            fileName = sb.ToString();

            // Populate the popup menu with file-specific items
            id = PopulateMenu(handleMenuPopup, 
            idCmdFirst+ id);
        }
                
        // Add the popup to the context menu
        MENUITEMINFO menuItemInfo = 
        new MENUITEMINFO();
        menuItemInfo.cbSize = 48;
        menuItemInfo.fMask = (uint) MIIM.TYPE | 
        (uint)MIIM.STATE | (uint) MIIM.SUBMENU;
        menuItemInfo.hSubMenu = 
        (int) handleMenuPopup;
        menuItemInfo.fType = 
        (uint) MF.STRING;
        menuItemInfo.dwTypeData
         = "GAC Options"; // adding a new menu
        menuItemInfo.fState = 
        (uint) MF.ENABLED;
        Win32Helpers.InsertMenuItem
        (hMenu, (uint)iMenu, 1, ref menuItemInfo);

        // Add a separator
        MENUITEMINFO seperator = 
        new MENUITEMINFO();
        seperator.cbSize = 48;
        seperator.fMask = (uint )MIIM.TYPE;
        seperator.fType = (uint) MF.SEPARATOR;
        Win32Helpers.InsertMenuItem(
        hMenu, iMenu+1, 1, ref seperator);
    
    }
    return id;
}

void AddMenuItem(uint hMenu, string text, int id, uint position)
{
    MENUITEMINFO menuItemInfo 
    = new MENUITEMINFO();
    menuItemInfo.cbSize = 48;
    menuItemInfo.fMask = 
    (uint)MIIM.ID | (uint)MIIM.TYPE | (uint)MIIM.STATE;
    menuItemInfo.wID    = id;
    menuItemInfo.fType = 
    (uint)MF.STRING;
    menuItemInfo.dwTypeData    = text;
    menuItemInfo.fState = 
    (uint)MF.ENABLED;
    Win32Helpers.InsertMenuItem(hMenu,
     position, 1, ref menuItemInfo);
}

int PopulateMenu(uint hMenu, int id)
{
    Logger.WriteLog("populate menu");
    
    AddMenuItem(hMenu, "Re&gister", id, 0);
    AddMenuItem(hMenu, "&Unregister", ++id, 1);
    AddMenuItem(hMenu, "Show &Assembly Info", ++id, 2);
    AddMenuItem(hMenu, "Copy &Qualified Name", ++id, 3);
    return id++;
}
    
void IContextMenu.GetCommandString(int idCmd, 
     uint uFlags, int pwReserved,
     StringBuilder commandString, int cchMax)
{
    switch(uFlags)
    {
    case (uint)GCS.VERB:
        commandString = new StringBuilder("...");
        break;
    case (uint)GCS.HELPTEXT:
        commandString = new StringBuilder("..."); 
        break;
    }
}
    
void IContextMenu.InvokeCommand (IntPtr pici)
{
    try
    {
        Type typINVOKECOMMANDINFO =
         Type.GetType("AssemblyRegUtil.INVOKECOMMANDINFO");
        INVOKECOMMANDINFO ici = 
        (INVOKECOMMANDINFO)Marshal.PtrToStructure(pici, typINVOKECOMMANDINFO);

        switch (ici.verb-1)
        {
            case 0:
                Register(); // register assmebly into GAC
                break;
            case 1:
                Unregister();// Unregister
                break;
            case 2:
                ShowAssemblyInfo();// Show info
                break;
            case 3:
                CopyQualifiedName();// Copy the qualified name
                break;
        }
    }
    catch(Exception ex)
    {
        Logger.WriteLog(ex.Message);
    }
}

int    IShellExtInit.Initialize (IntPtr pidlFolder,
 IntPtr lpdobj, uint hKeyProgID)
{
    try
    {
        if (lpdobj != (IntPtr)0)
        {
            // Get info about the directory
            IDataObject dataObject = 
            (IDataObject)Marshal.GetObjectForIUnknown(lpdobj);
            FORMATETC fmt = new FORMATETC();
            fmt.cfFormat = CLIPFORMAT.CF_HDROP;
            fmt.ptd         = 0;
            fmt.dwAspect = DVASPECT.DVASPECT_CONTENT;
            fmt.lindex     = -1;
            fmt.tymed     = TYMED.TYMED_HGLOBAL;
            STGMEDIUM medium = new STGMEDIUM();
            dataObject.GetData(ref fmt, ref medium);
            m_hDrop = medium.hGlobal;
        }
    }
    catch(Exception)
    {
    }
    return 0;
}

Here we are actually creating the context menus that will be displayed when the user will click onto a file from Windows Explorer. Now, let's add two more methods that essentially will help us to write some Registry entries during the installation period.

C#
[System.Runtime.InteropServices.ComRegisterFunctionAttribute()]
static void RegisterServer(String str1)
{
    try
    {
        // For Winnt set me as an approved shellex
        RegistryKey root;
        RegistryKey rk;
        root = Registry.LocalMachine;
        rk = 
        root.OpenSubKey
        ("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved", 
        true);
        rk.SetValue(guid.ToString(), "GAC shell extension");
        rk.Close();


        root = Registry.ClassesRoot;
        rk = root.CreateSubKey("GAC\\shellex\\ContextMenuHandlers\\DLL");
        rk.SetValue("", guid.ToString());
        rk.Close();
    }
    catch(Exception e)
    {
        System.Console.WriteLine(e.ToString());
    }
}

[System.Runtime.InteropServices.ComUnregisterFunctionAttribute()]
static void UnregisterServer(String str1)
{
    try
    {
        RegistryKey root;
        RegistryKey rk;

        // Remove ShellExtenstions registration
        root = Registry.LocalMachine;
        rk = 
        root.OpenSubKey
        ("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved", 
        true);
        rk.DeleteValue(guid);
        rk.Close();

        // Delete  regkey
        root = Registry.ClassesRoot;
        root.DeleteSubKey("GAC\\shellex\\ContextMenuHandlers\\DLL");
    }
    catch(Exception e)
    {
        System.Console.WriteLine(e.ToString());
    }
}

So far, we have already written the shell extension, now it's time to implement the functionalities. For example, registering an assembly into the GAC, removing an assembly from GAC etc. Let's write a new class named GacManager to serve this purpose.

C#
public class GacManager
{
    public static void RegisterAssembly(string m_fileName)
    {
        string result = string.Empty;
        try
        {   // register the assembly
            result = RegisterAssemblyCode(m_fileName);
        }
        catch (Exception ex)
        {
            result = ex.Message;
        }
        MessageDialog msgDialog = new MessageDialog();
        if (result.ToLower().Contains("success"))
        {   // if the success contains into the message then its okay 
            msgDialog.MessageText = 
            "Successfully added to the Global Assembly Cache.";
        }
        else 
        {   // failure
            msgDialog.MessageText = 
            "Failed to register the assembly.";
        }
        msgDialog.MessageDetails = result;
        msgDialog.ShowDialog();
    }
    public static void UnregisterAssembly(string m_fileName)
    {
        string result = string.Empty;
        try
        {
            result = UnregisterCore(m_fileName);
        }
        catch (Exception ex)
        {
            result = ex.Message;
        }
        MessageDialog msgDialog = new MessageDialog();
        if (result.ToLower().Contains("uninstalled = 1"))
        {
            msgDialog.MessageText 
            = "Successfully removed from the Global Assembly Cache.";
        }
        else
        {
            msgDialog.MessageText = "Failed to unregister the assembly.";
        }
        msgDialog.MessageDetails = result;
        msgDialog.ShowDialog();
    }

    public static void CopyFullQualifiedName(string fileName)
    {
        try
        {
            Assembly assmbly = Assembly.LoadFile(fileName);
            Clipboard.SetDataObject(assmbly.FullName, true);         
        }
        catch (Exception ex)
        {
            Logger.WriteLog(ex.Message);
        }
    }

    public static void ShowAssemblyInfo(string m_fileName)
    {
        string message = string.Empty;
        string messageDetails = string.Empty;
        try
        {
            Assembly assmbly = Assembly.LoadFile(m_fileName);

            message = assmbly.FullName;
            StringBuilder sb = new StringBuilder();

            sb.AppendFormat("Codebase : {0}\n", assmbly.CodeBase);
            sb.AppendFormat("EscapedCodeBase : {0}\n", 
            assmbly.EscapedCodeBase);
            sb.AppendFormat("FullName : {0}\n", assmbly.FullName);
            sb.AppendFormat("Location : {0}\n", assmbly.Location);

            messageDetails = sb.ToString();
        }
        catch (Exception ex)
        {
            message = "Failed to read assembly info.";
            messageDetails = ex.Message;
        }

        MessageDialog msgDlg = new MessageDialog();
        msgDlg.MessageText = message;
        msgDlg.MessageDetails = messageDetails;
        msgDlg.QualifiedName = message;
        msgDlg.EnableClipboardCopy = true;
        msgDlg.ShowDialog();
    }       

    private static string RegisterAssemblyCode(string fileName)
    {
        Logger.WriteLog("Registering .. " + fileName);
        string envName = 
        "VS80COMNTOOLS"; // For VS 2003 We need to use "VS71COMNTOOLS". 
        string toolPath = Environment.GetEnvironmentVariable(envName);
        string vsCmdLinePath = 
        System.IO.Path.Combine(toolPath, "vsvars32.bat");

        Process process = new Process();

        using (System.IO.StreamReader reader = 
        new System.IO.StreamReader(vsCmdLinePath))
        {
            string value = null;
            while (null != (value = reader.ReadLine()))
            {
                if (value.IndexOf("FrameworkSDKDir") != -1)
                {
                    string sdkPath = 
                    value.Substring(value.IndexOf("=") + 1).Trim();
                    string gacutilPath = 
                    Path.Combine(sdkPath, @"bin\gacutil.exe");
                    string cmdLineArgument = " -i \"" + fileName + "\"";
                    Logger.WriteLog("Argument : "+ cmdLineArgument);
                    process.StartInfo = 
                    new ProcessStartInfo(gacutilPath, cmdLineArgument);
                    break;
                }
            }
        }
        process.StartInfo.CreateNoWindow = true;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.UseShellExecute = false;
        process.Start();

        process.WaitForExit();
        string output = process.StandardOutput.ReadToEnd();
        Logger.WriteLog("Output was : " + output);
        return output;
    }

    private static string UnregisterCore(string fileName)
    {
        Logger.WriteLog("Unregistering .. " + fileName);
        string envName = "VS80COMNTOOLS";
         // For VS 2003 We need to use "VS71COMNTOOLS". 
        string toolPath = Environment.GetEnvironmentVariable(envName);
        string vsCmdLinePath = 
        Path.Combine(toolPath, "vsvars32.bat");

        Process process = new Process();

        using (System.IO.StreamReader reader = 
        new System.IO.StreamReader(vsCmdLinePath))
        {
            string value = null;
            while (null != (value = reader.ReadLine()))
            {
                if (value.IndexOf("FrameworkSDKDir") != -1)
                {
                    string sdkPath = 
                    value.Substring(value.IndexOf("=") + 1).Trim();
                    string gacutilPath = 
                    Path.Combine(sdkPath, @"bin\gacutil.exe");
                    string asmName = Path.GetFileName( fileName);
                    if( asmName.ToLower().EndsWith(".dll"))
                        asmName = asmName.Substring(0, 
                        asmName.ToLower().LastIndexOf(".dll"));
                    string cmdLineArgument = " -u " + asmName ;
                    Logger.WriteLog("Argument : " + cmdLineArgument);
                    process.StartInfo = 
                    new ProcessStartInfo(gacutilPath, cmdLineArgument);
                    break;
                }
            }
        }
        process.StartInfo.CreateNoWindow = true;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.UseShellExecute = false;
        process.Start();
        process.WaitForExit();
        string output = process.StandardOutput.ReadToEnd();
        Logger.WriteLog("Output was : " + output);
        return output;
    }
}

As you can see, I have used the Process class to accomplish the registration task. And the current code base's target is .NET 2.0, so it will not work under .NET 1.1. You will need to modify the environment variable name from "VS80COMNTOOLS" to VS71COMNTOOLS" to make it work under VS.NET 2003.

Now it's time to deploy the shell extension. Create a Key file (using sn.exe or from Visual Studio .NET) and register the assembly. Put the assembly into GAC. We need to register this assembly into GAC because when it will be invoked from the COM Shell API, it should be located. Now open the Visual Studio console and run the following command:

C:\>regasm <your assembly name>

The last thing we need to do is modify the Registry. Open the Registry editor (from Start menu->Run, type regedit and press Enter). Expand HKEY_CLASS_ROOT and open the .dll sub key and modify the default key to GAC.

Screenshot - Registry.jpg

That's all. Hope you will find this interesting and funny.

License

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


Written By
Architect
Netherlands Netherlands
Engineer Powered by the Cloud

Comments and Discussions

 
GeneralProblem in Vista Pin
danny33311-Aug-09 17:18
danny33311-Aug-09 17:18 
Questionwhat if the type of the win32 api's argument is **? Pin
newgreenfreshhand7-Aug-07 22:29
newgreenfreshhand7-Aug-07 22:29 
AnswerRe: what if the type of the win32 api's argument is **? Pin
Moim Hossain7-Aug-07 23:04
Moim Hossain7-Aug-07 23:04 
GeneralRe: what if the type of the win32 api's argument is **? Pin
newgreenfreshhand8-Aug-07 2:25
newgreenfreshhand8-Aug-07 2:25 
GeneralDo not write in-process shell extensions in managed code Pin
Jim Barry28-May-07 15:01
Jim Barry28-May-07 15:01 
GeneralRe: Do not write in-process shell extensions in managed code Pin
Moim Hossain29-May-07 3:34
Moim Hossain29-May-07 3:34 
Generaldev box droplet batches Pin
Chris Richner25-May-07 8:41
Chris Richner25-May-07 8:41 
GeneralRe: dev box droplet batches Pin
Moim Hossain25-May-07 8:51
Moim Hossain25-May-07 8:51 
GeneralRe: dev box droplet batches Pin
Chris Richner26-May-07 5:26
Chris Richner26-May-07 5:26 
QuestionRe: dev box droplet batches Pin
Moim Hossain26-May-07 5:31
Moim Hossain26-May-07 5:31 
AnswerRe: dev box droplet batches Pin
Chris Richner27-May-07 12:06
Chris Richner27-May-07 12:06 
AnswerRe: dev box droplet batches Pin
Moim Hossain27-May-07 18:56
Moim Hossain27-May-07 18:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.