![]() |
General Reading »
Hardware & System »
General
Intermediate
License: The Code Project Open License (CPOL)
Register/Unregister .NET Asseblies into GAC using Shell ExtentionsBy Moim HossainRegistering .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
|
||||||||
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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.
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.
.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:
Now let's figure out the details.
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
That's all. Hope you will find it interesting and funny.
| You must Sign In to use this message board. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
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 |