Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Add-in to attach ASP.NET debugger

0.00/5 (No votes)
30 Oct 2008 1  
This Visual Studio add-in puts a nice-cool shortcut button on your debug toolbar to attach your code to the ASP.NET debugger (aspnet_wp.exe).

Introduction

As our ASP.NET application grows, we can no longer use the normal Start debugging button StartDebug.GIF to debug our application, because the application might be doing a lot of other processing before jumping to our code where we are interested in debugging. This is when the option of Debug -> Attach to process ... in Visual Studio comes handy. This is in used by many of us when we navigate to the concerned page where there is a problem, and then we attach the debugger to the aspnet_wp.exe process. But this step requires four mouse clicks and when you are doing this 100 times a day, it is 400 clicks, and so on. I was annoyed by this and that is why I created this add-in which places a nice button on the debug toolbar to attach to the ASP.NET process with a single click.

Background

This was my first add-in development, so I had to dig around on the web a lot and actually I got a lot of help from www.mztools.com.

I got a lot of help from these two articles on their site and of course MSDN helped:

Using the Code

To use the add-in, unzip AttachToASPNETAddinDLL.zip from the link above. Copy the files AttachToASPNETAddin.AddIn and AttachToASPNETAddin.dll to the Addins folder in your <My Documents folder>\Visual Studio 2005\. If that folder is not there, create that folder. This is the folder from where Visual Studio picks up the add-ins. So there is no need for a setup project here. The add-ins in Visual Studio 2005 now have xCopy deployment.

The .Addin file is just an XML file which helps Visual Studio in locating the add-in executable and various parameters like Company, Author name etc. The most important one is the Assembly node which keeps the path of the add-in executable. This path can be local, relative, or even a URL.

Now, open just one VS 2005 instance and select Tools -> Add-in Manager, as shown in this image.

Check the check-box on the left to the add-in name and check the Startup checkbox. This operation will bring up the add-in button in this instance and also in all the later VS 2005 instances.

The debugger button looks like this when the add-in is loaded.

AttachImage.PNG

The debugger button looks like this when the button is pressed and the code is attached to the aspnet_wp process. Pressing the button (now with a cross sign) detaches the code from the debugger.

DetachImage.PNG

These buttons can be easily changed by changing the constants ATTACH_ICON_FACEID and DETACH_ICON_FACEID with the faceID you want to show. There are plenty of web pages flowing on the internet if you 'Google' for face IDs.

I got a face ID browsing utility as an Excel macro from http://peltiertech.com/Excel/Zips/ShowFace.zip.

Here is the source code of the backbone of the add-in, the Connect class:

using System.Diagnostics; 
using System;
using Extensibility;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.CommandBars;
using System.Resources;
using System.Reflection;
using System.Globalization;
using System.Windows.Forms;

namespace AttachToASPNETAddin
{
    /// <summary>The object for implementing an Add-in.</summary>
    /// <seealso class="IDTExtensibility2" >
    public class Connect : IDTExtensibility2, IDTCommandTarget
    {
        private const string ATTACH_TOOLTIP = "Attach ASP.NET debugger";
        private const int ATTACH_ICON_FACEID = 2151;
        private const string DETACH_TOOLTIP = "Detach ASP.NET debugger";
        private const int DETACH_ICON_FACEID = 1088;
        private const string MY_COMMAND_NAME = "AttachToASPNETAddin";
        private DTE2 _applicationObject;
        private AddIn _addInInstance; 
        private CommandBarControl standardCommandBarControl;
        private CommandEvents commandEvents;
        private CommandBarButton commandBarButton;
        
        /// <summary>Implements the constructor
        /// for the Add-in object. Place your initialization 
        /// code within this method.</summary>
        public Connect()
        {
        }

        /// <summary>Implements the OnConnection method of the IDTExtensibility2 interface. 
        /// Receives notification that the Add-in is being loaded.</summary>
        /// <param term="application">Root object of the host application.</param>
        /// <param term="connectMode">Describes how the Add-in is being loaded.</param>
        /// <param term="addInInst">Object representing this Add-in.</param>
        /// <seealso class="IDTExtensibility2">
        public void OnConnection(object application, ext_ConnectMode 
               connectMode, object addInInst, ref Array custom)
        {
            try
            {
                _applicationObject = (DTE2)application;
                _addInInstance = (AddIn)addInInst;
                
                // get all the command events
                commandEvents = _applicationObject.Events.get_CommandEvents(
                   "{00000000-0000-0000-0000-000000000000}", 0);                
                // attach the event handler
                commandEvents.BeforeExecute += 
                  new _dispCommandEvents_BeforeExecuteEventHandler(
                  commandEvents_BeforeExecute);

                switch (connectMode)
                {
                    case ext_ConnectMode.ext_cm_Startup:
                        break;
                    // The add-in was marked to load on startup 
                    // Do nothing at this point because
                    // the IDE may not be fully initialized 
                    // VS will call OnStartupComplete when ready 

                    case ext_ConnectMode.ext_cm_AfterStartup:
                        Array dummyArr = new string[1];
                        // The add-in was loaded after startup 
                        // Initialize it in the same way that when
                        // is loaded on startup calling manually this method: 
                        OnStartupComplete(ref dummyArr);
                        break;

                    case ext_ConnectMode.ext_cm_UISetup:

                        object[] contextGUIDS = new object[] { };
                        Commands2 commands = (Commands2)_applicationObject.Commands;
                        string toolsMenuName;

                        try
                        {
                            //If you would like to move the command
                            // to a different menu, change the word "Tools" to the 
                            //  English version of the menu. This code
                            //  will take the culture, append on the name of the menu
                            //  then add the command to that menu.
                            //  You can find a list of all the top-level menus in the file
                            //  CommandBar.resx.
                            ResourceManager resourceManager = new ResourceManager(
                              "AttachToASPNETAddin.CommandBar", 
                              Assembly.GetExecutingAssembly());
                            CultureInfo cultureInfo = 
                              new System.Globalization.CultureInfo(
                              _applicationObject.LocaleID);
                            string resourceName = String.Concat(
                              cultureInfo.TwoLetterISOLanguageName, "Tools");
                            toolsMenuName = resourceManager.GetString(resourceName);
                        }
                        catch
                        {
                            //We tried to find a localized version
                            //of the word Tools, but one was not found.
                            //Default to the en-US word,
                            //which may work for the current culture.
                            toolsMenuName = "Tools";
                        }

                        //Place the command on the tools menu.
                        //Find the MenuBar command bar, which is the
                        //top-level command bar holding all the main menu items:
                        Microsoft.VisualStudio.CommandBars.CommandBar 
                          menuBarCommandBar = 
                          ((Microsoft.VisualStudio.CommandBars.CommandBars)
                          _applicationObject.CommandBars)["MenuBar"];

                        //Find the Tools command bar on the MenuBar command bar:
                        CommandBarControl toolsControl = menuBarCommandBar.Controls[toolsMenuName];
                        CommandBarPopup toolsPopup = (CommandBarPopup)toolsControl;

                        //This try/catch block can be duplicated if you
                        //wish to add multiple commands to be handled by your Add-in,
                        //  just make sure you also update the
                        //  QueryStatus/Exec method to include the new command names.
                        try
                        {
                            //Add a command to the Commands collection:
                            Command command = commands.AddNamedCommand2(_addInInstance, 
                              "AttachToASPNETAddin", 
                              "AttachToASPNETAddin", 
                              "Executes the command for AttachToASPNETAddin", 
                              true, 59, ref contextGUIDS, 
                              (int)vsCommandStatus.vsCommandStatusSupported + 
                              (int)vsCommandStatus.vsCommandStatusEnabled, 
                              (int)vsCommandStyle.vsCommandStylePictAndText, 
                              vsCommandControlType.vsCommandControlTypeButton);

                            //Add a control for the command to the tools menu:
                            if ((command != null) && (toolsPopup != null))
                            {
                                command.AddControl(toolsPopup.CommandBar, 1);
                            }
                        }
                        catch (System.ArgumentException)
                        {
                            // If we are here, then the exception is probably
                            // because a command with that name
                            // already exists. If so there is no need
                            // to recreate the command and we can 
                            // safely ignore the exception.
                        }
                        break;
                }
            }
            catch (Exception e)
            {
                MessageBox.Show(e.ToString());
            }
        }

        private void commandEvents_BeforeExecute(string Guid, int ID, 
                object CustomIn, object CustomOut, ref bool CancelDefault)
        {
            EnvDTE.Command objCommand = default(EnvDTE.Command);
            string sCommandName = null;

            objCommand = _applicationObject.Commands.Item(Guid, ID);

            if ((objCommand != null))
            {
                sCommandName = objCommand.Name;
                if (string.IsNullOrEmpty(sCommandName))
                {
                    sCommandName = "";
                }

                // if the command name matches these commands, modify the button
                if (sCommandName.Equals("Debug.Start") /* Command ID is 295 */ ||
                    sCommandName.Equals("Debug.AttachtoProcess")) /* Command ID is 213 */
                {
                    commandBarButton.FaceId = DETACH_ICON_FACEID;
                    commandBarButton.TooltipText = DETACH_TOOLTIP;
                }
                else if (sCommandName.Equals("Debug.StopDebugging") /* Command ID is 179 */ || 
                    sCommandName.Equals("Debug.DetachAll") ||
                    sCommandName.Equals("Debug.TerminateAll"))
                {
                    commandBarButton.FaceId = ATTACH_ICON_FACEID;
                    commandBarButton.TooltipText = ATTACH_TOOLTIP;
                }
            } 
        }

        /// <summary>Implements the OnDisconnection method of the IDTExtensibility2 interface. 
        /// Receives notification that the Add-in is being unloaded.</summary>
        /// <param term="disconnectMode" />Describes how the Add-in is being unloaded.</param>
        /// <param term="custom">Array of parameters that are host application specific.</param>
        /// <seealso class="IDTExtensibility2" />
        public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
        {
            try
            {
                if ((standardCommandBarControl != null))
                {
                    standardCommandBarControl.Delete(true);
                }
                commandEvents = null;
            }

            catch (System.Exception e)
            {
                System.Windows.Forms.MessageBox.Show(e.ToString());
            }
        }

        /// <summary>Implements the OnAddInsUpdate
        /// method of the IDTExtensibility2 interface. 
        /// Receives notification when the collection
        /// of Add-ins has changed.</summary>
        /// <param term="custom">Array of parameters
        /// that are host application specific.</param>
        /// <seealso class="IDTExtensibility2" />
        public void OnAddInsUpdate(ref Array custom)
        {
        }

        /// <summary >Implements the OnStartupComplete method of the IDTExtensibility2 interface. 
        /// Receives notification that the host application has completed loading.</summary >
        /// <param term="custom" >Array of parameters that are host application specific.</param >
        /// <seealso class="IDTExtensibility2" >
        public void OnStartupComplete(ref Array custom)
        {
            Command loCommand = null;
            CommandBar loCommandBar = default(CommandBar);
            commandBarButton = default(CommandBarButton);

            CommandBars loCommandBars = default(CommandBars);
            try
            {
                // Try to retrieve the command, just in case it was already created 
                try
                {
                    loCommand = _applicationObject.Commands.Item(
                      _addInInstance.ProgID + "." + MY_COMMAND_NAME, -1);
                }
                catch
                {
                }
                // Add the command if it does not exist 
                if (loCommand == null)
                {
                    object[] dummyObject = new object[1];
                    loCommand = _applicationObject.Commands.AddNamedCommand(
                      _addInInstance, MY_COMMAND_NAME, 
                      MY_COMMAND_NAME, "Executes the command for MyAddin", 
                      true, 59, ref dummyObject, 
                      (int)(vsCommandStatus.vsCommandStatusSupported | 
                      vsCommandStatus.vsCommandStatusEnabled));
                }
                // get the collection of commandbars 
                loCommandBars = (CommandBars)_applicationObject.CommandBars;
                // Now get the debug toolbar instance 
                loCommandBar = loCommandBars["Debug"];
                // Now add a button to the built-in "debug" toolbar 
                standardCommandBarControl = 
                  (CommandBarControl)loCommand.AddControl(
                  loCommandBar, loCommandBar.Controls.Count + 1);
                standardCommandBarControl.Caption = MY_COMMAND_NAME;
                // Change the button style 
                commandBarButton = (CommandBarButton)standardCommandBarControl;
                commandBarButton.Style = MsoButtonStyle.msoButtonIcon;
                commandBarButton.FaceId = ATTACH_ICON_FACEID;
                commandBarButton.TooltipText = ATTACH_TOOLTIP;
                // attach the button click event
                commandBarButton.Click += 
                  new _CommandBarButtonEvents_ClickEventHandler(
                  commandBarButton_Click);
            }
            catch (System.Exception e)
            {
                System.Windows.Forms.MessageBox.Show(e.ToString());
            }
        }

        private void commandBarButton_Click(CommandBarButton pCommBarBtn, 
                     ref bool CancelDefault)
        {
            try
            {
                // Get an instance of the currently running Visual Studio IDE.
                EnvDTE80.DTE2 dte2;
                dte2 = (EnvDTE80.DTE2)
                  System.Runtime.InteropServices.Marshal.GetActiveObject(
                  "VisualStudio.DTE.8.0"); 
                EnvDTE80.Debugger2 dbg2 = (EnvDTE80.Debugger2)(dte2.Debugger);
                EnvDTE80.Transport trans = dbg2.Transports.Item("Default");
                EnvDTE80.Engine[] dbgeng = new EnvDTE80.Engine[2];
                dbgeng[0] = trans.Engines.Item("Managed");
                EnvDTE80.Process2 proc2 = (EnvDTE80.Process2)dbg2.GetProcesses
                    (trans, System.Environment.MachineName).Item("aspnet_wp.exe");
                if ((proc2.IsBeingDebugged))
                {
                    proc2.Detach(false);
                    pCommBarBtn.FaceId = ATTACH_ICON_FACEID;
                    pCommBarBtn.TooltipText = ATTACH_TOOLTIP;
                }
                else
                {
                    proc2.Attach2(dbgeng);
                    pCommBarBtn.FaceId = DETACH_ICON_FACEID;
                    pCommBarBtn.TooltipText = DETACH_TOOLTIP;
                }
            }
            catch (System.Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        /// <summary >Implements the OnBeginShutdown method of the IDTExtensibility2 interface. 
        /// Receives notification that the host application is being unloaded.</summary >
        /// <param term="custom" >Array of parameters that are host application specific.</param >
        /// <seealso class="IDTExtensibility2" >
        public void OnBeginShutdown(ref Array custom)
        {
        }
        
        /// <summary >Implements the QueryStatus method of the IDTCommandTarget interface. 
        /// This is called when the command's availability is updated</summary >
        /// <param term="commandName" >The name of the command to determine state for.</param >
        /// <param term="neededText" >Text that is needed for the command.</param >
        /// <param term="status" >The state of the command in the user interface.</param >
        /// <param term="commandText" >Text requested by the neededText parameter.</param >
        /// <seealso class="Exec" >
        public void QueryStatus(string commandName, vsCommandStatusTextWanted neededText, 
               ref vsCommandStatus status, ref object commandText)
        {
          if(neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
            {
                if(commandName == "AttachToASPNETAddin.Connect.AttachToASPNETAddin")
                {
                    status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported|
                              vsCommandStatus.vsCommandStatusEnabled;
                    return;
                }
            }
        }

        /// <summary >Implements the Exec method of the IDTCommandTarget interface. 
        /// This is called when the command is invoked.</summary >
        /// <param term="commandName" >The name of the command to execute.</param >
        /// <param term="executeOption" >Describes how the command should be run.</param >
        /// <param term="varIn" >Parameters passed from the caller to the command handler.</param >
        /// <param term="varOut" >Parameters passed from the command handler to the caller.</param >
        /// <param term="handled" >Informs the caller if the command was handled or not.</param >
        /// <seealso class="Exec" >
        public void Exec(string commandName, vsCommandExecOption executeOption, 
               ref object varIn, ref object varOut, ref bool handled)
        {
            handled = false;
            if(executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)
            {
                if(commandName == "AttachToASPNETAddin.Connect.AttachToASPNETAddin")
                {
                    handled = true;
                    return;
                }
            }
        }
    }
}

Points of Interest

Building an add-in seems to be complicated in the beginning as the VS 2005 Add-in wizard spits out a lot of built-in code which looks like pre-war ones. The object names are also scary which has a prefix too (introduced in Visual Studio 2005) like IDTExtensibility2, DTE2, etc.

The fun part was catching the Visual Studio events (e.g., when VS's own Start debugging button is invoked or stopped using the Stop button). After learning how to catch these events, I was able to modify the functionality of the add-in button and its image according to the debugger state. You can see that in the commandEvents_BeforeExecute event.

This article works fine for Visual Studio 2005, but will only need DTE class level modification to work for Visual Studio 2008.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here