Click here to Skip to main content
6,594,432 members and growing! (15,231 online)
Email Password   helpLost your password?
General Reading » Hardware & System » General     Intermediate License: The Code Project Open License (CPOL)

Register/Unregister .NET Asseblies into GAC using Shell Extentions

By Moim Hossain

Registering .NET assemblies into GAC (a.k.a Global Assembly Cache) using Shell Extension context menus
C# 2.0.NET 2.0, Win2K, WinXP, Win2003VS2005, Dev
Posted:25 May 2007
Views:29,196
Bookmarked:27 times
Unedited contribution
Announcements
Loading...
 
Search    
Advanced Search
Add to IE Search
printPrint   add Share
      Discuss Discuss   Broken Article?Report  
5 votes for this article.
Popularity: 2.33 Rating: 3.33 out of 5
1 vote, 20.0%
1

2
1 vote, 20.0%
3

4
3 votes, 60.0%
5

Screenshot - contextMenu.jpg

Introduction

In this article, I will try to demonstrate how we can use the 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 an .NET assembly into the Global Assembly Cache.

Background

During the last couple of months, I am doing work on a project which is based on Windows Share Point server, SQL server (integration services) etc. and those who did work with Share Point development (i.e. developing web part etc) must already noticed that we need to register our assemblies into the Global Assembly Cache too often. In fact, I believe during the last couple of month, if I analyze my works, I would find, the most repetitive task that I did so far, is registering assemblies into the GAC.
Well today, I don't have that much work pressure (I am lucky Coz, it's a rare scenario I believe), which made me think - if there is something I can do to make this registration process simpler and faster? I found some cool topics (most of them are BLOGs written by some intelligent peoples) when I was searching on this issue. One of them attracted me and it was doing nothing but putting some registry entries to accomplish this task. I have taken the idea from that BLOG and written some codes 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

.NET Framework is still not a native part of Windown 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 stuffs provided by .NET framework in order to play with Windows Shell. Especially, you need to know how the Runtime Callable Wrapper (a.k.a RCW) and COM Callable Wrappers (a.k.a CCW) works. Explaining these stuffs is of course beyond the scope of this article therefore you need to go MSDN if you already don't have any idea about these technologies. Another good start could be this article which impressed me much.


Generally speaking, if we want to write a shell extension in managed code using the COM Interop layer (provided by .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 interface for 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 now add a new class and will define the win32 structures, interfaces that 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.
    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 definition of unmanaged win32 language elements. (There are more structures and enumerations that we need to define here but not given into 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 a Win32Helpers. The code snippet of this class is given below.

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);

}

Its time to write a managed class that will implement the unmanaged Shell Extension interface 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 GUID for the class. So generate a new GUID from the Visual Studio .NET and replace the one that I have written here.

[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.

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 user will click onto a file from the Windows Explorer. Now let's add two more methods that essentially will help us to write some registry entries during the installation period.

[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 its time to implement the functionalities to it, For example, registering the assembly into the GAC, removing from GAC etc. Let's write a new class named GacManager to serve this purpose.

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 the Visual studio .net) and register the assembly. Put the assembly into the GAC. We need to register this assembly into the 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 modifying the registry. Open the registry editor (from start menu->run. Type regedit press enter). Expand the 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 it interesting and funny.

License

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

About the Author

Moim Hossain


Member
Jack of all trades....Hmm...(nope not saying the rest)
Occupation: Software Developer (Senior)
Company: BlueCielo ECM Solutions BV
Location: Netherlands Netherlands

Other popular Hardware & System articles:

Article Top
You must Sign In to use this message board.
FAQ FAQ 
 
Noise Tolerance  Layout  Per page   
 Msgs 1 to 12 of 12 (Total in Forum: 12) (Refresh)FirstPrevNext
GeneralProblem in Vista PinmemberMember 166717618:18 11 Aug '09  
Questionwhat if the type of the win32 api's argument is **? Pinmembernewgreenfreshhand23:29 7 Aug '07  
AnswerRe: what if the type of the win32 api's argument is **? PinmemberMoim Hossain0:04 8 Aug '07  
GeneralRe: what if the type of the win32 api's argument is **? Pinmembernewgreenfreshhand3:25 8 Aug '07  
GeneralDo not write in-process shell extensions in managed code PinmemberJim Barry16:01 28 May '07  
GeneralRe: Do not write in-process shell extensions in managed code PinmemberMoim Hossain4:34 29 May '07  
Generaldev box droplet batches PinmemberChris Richner9:41 25 May '07  
GeneralRe: dev box droplet batches PinmemberMoim Hossain9:51 25 May '07  
GeneralRe: dev box droplet batches PinmemberChris Richner6:26 26 May '07  
QuestionRe: dev box droplet batches PinmemberMoim Hossain6:31 26 May '07  
AnswerRe: dev box droplet batches PinmemberChris Richner13:06 27 May '07  
AnswerRe: dev box droplet batches PinmemberMoim Hossain19:56 27 May '07  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

PermaLink | Privacy | Terms of Use
Last Updated: 25 May 2007
Editor:
Copyright 2007 by Moim Hossain
Everything else Copyright © CodeProject, 1999-2009
Web22 | Advertise on the Code Project