Introduction
As our ASP.NET application grows, we can no longer use the normal Start debugging button
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.
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.
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
{
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;
public Connect()
{
}
public void OnConnection(object application, ext_ConnectMode
connectMode, object addInInst, ref Array custom)
{
try
{
_applicationObject = (DTE2)application;
_addInInstance = (AddIn)addInInst;
commandEvents = _applicationObject.Events.get_CommandEvents(
"{00000000-0000-0000-0000-000000000000}", 0);
commandEvents.BeforeExecute +=
new _dispCommandEvents_BeforeExecuteEventHandler(
commandEvents_BeforeExecute);
switch (connectMode)
{
case ext_ConnectMode.ext_cm_Startup:
break;
case ext_ConnectMode.ext_cm_AfterStartup:
Array dummyArr = new string[1];
OnStartupComplete(ref dummyArr);
break;
case ext_ConnectMode.ext_cm_UISetup:
object[] contextGUIDS = new object[] { };
Commands2 commands = (Commands2)_applicationObject.Commands;
string toolsMenuName;
try
{
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
{
toolsMenuName = "Tools";
}
Microsoft.VisualStudio.CommandBars.CommandBar
menuBarCommandBar =
((Microsoft.VisualStudio.CommandBars.CommandBars)
_applicationObject.CommandBars)["MenuBar"];
CommandBarControl toolsControl = menuBarCommandBar.Controls[toolsMenuName];
CommandBarPopup toolsPopup = (CommandBarPopup)toolsControl;
try
{
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);
if ((command != null) && (toolsPopup != null))
{
command.AddControl(toolsPopup.CommandBar, 1);
}
}
catch (System.ArgumentException)
{
}
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 (sCommandName.Equals("Debug.Start") ||
sCommandName.Equals("Debug.AttachtoProcess"))
{
commandBarButton.FaceId = DETACH_ICON_FACEID;
commandBarButton.TooltipText = DETACH_TOOLTIP;
}
else if (sCommandName.Equals("Debug.StopDebugging") ||
sCommandName.Equals("Debug.DetachAll") ||
sCommandName.Equals("Debug.TerminateAll"))
{
commandBarButton.FaceId = ATTACH_ICON_FACEID;
commandBarButton.TooltipText = ATTACH_TOOLTIP;
}
}
}
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());
}
}
public void OnAddInsUpdate(ref Array custom)
{
}
public void OnStartupComplete(ref Array custom)
{
Command loCommand = null;
CommandBar loCommandBar = default(CommandBar);
commandBarButton = default(CommandBarButton);
CommandBars loCommandBars = default(CommandBars);
try
{
try
{
loCommand = _applicationObject.Commands.Item(
_addInInstance.ProgID + "." + MY_COMMAND_NAME, -1);
}
catch
{
}
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));
}
loCommandBars = (CommandBars)_applicationObject.CommandBars;
loCommandBar = loCommandBars["Debug"];
standardCommandBarControl =
(CommandBarControl)loCommand.AddControl(
loCommandBar, loCommandBar.Controls.Count + 1);
standardCommandBarControl.Caption = MY_COMMAND_NAME;
commandBarButton = (CommandBarButton)standardCommandBarControl;
commandBarButton.Style = MsoButtonStyle.msoButtonIcon;
commandBarButton.FaceId = ATTACH_ICON_FACEID;
commandBarButton.TooltipText = ATTACH_TOOLTIP;
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
{
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);
}
}
public void OnBeginShutdown(ref Array custom)
{
}
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;
}
}
}
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.