Customizing Exception Handling in the VS Debugger

2 Comments April 2, 2009


In the last two weeks I've gotten many questions about how to automate and program the exception handling the Visual Studio debugger so I thought I'd show how to some cool macros I've created to make it easy. If you're interested in more about making the most of the debugger you can always come to our Devscovery conference in New York City starting on April 14th. (Bethany, Wintellect's marketing maestro, is going to love me for working in the advertisement there!).

All the questions I got about the debugger exception handling fell into two buckets. The first was that teams have custom exceptions that they want to add to the Exceptions dialog and don't want to have to add those manually every time the move to a new machine. Since the Exception settings are stored in the .SUO file next to the solution, you also lose those exceptions if you delete the .SUO file. The second question was about programmatically setting just a handful of exceptions to stop when thrown. What was interesting about the second question is that the people asking had the neat idea of having their tests running under the Visual Studio debugger and configured to ignore all exceptions but a handful. That way they'd have those tough error conditions already in the debugger in order to make their debugging easier. I thought that was a very interesting idea.

As with most things in life, there's some good news and bad news about programmatically manipulating exceptions through the Visual Studio automation model. The good news is that the automation model has everything you need. The bad news is that certain operations, like setting all exceptions to stop when thrown, if done with a macro as so abysmally slow that it's essentially unusable. I suspect the performance would be better if I wrote an add-in. I'm hoping the VS 2010 will improve the performance of macros so more people will consider writing quick extensions to the IDE.

Let me start by showing you a simple macro from Jim Griesmer that sets a CLR exception to stop when thrown:

Sub BreakWhenThrown(Optional ByVal strException As String = "")
    
Dim dbg As Debugger3 = DTE.Debugger
    
Dim eg As ExceptionSettings = _
        dbg.ExceptionGroups.Item(
"Common Language Runtime Exceptions")
    eg.SetBreakWhenThrown(
True, eg.Item(strException))
End Sub

To execute the above macro, you'll open the Command window and pass the full name of the exception on the command line like the following:

>Macros.MyMacros.CLRExceptions.BreakWhenThrown System.ArgumentException

The macro itself if relatively straightforward. Once you have the Debugger3 interface, you can get the exceptions under a category by name. As you probably guessed the names of the exception groups maps to exactly what you see in the Exceptions dialog in Visual Studio.

Note that my Exception dialog probably looks different than yours because I turned off Just My Code in the Visual Studio Options dialog (Options, Debugging, General). I highly recommend you do as well because having Just My Code turned on turns off very valuable features such as debugging optimized builds and the awesome .NET Reference Source Code debugging.

The real work in the macro is all in the ExceptionsGroup interface as it has the methods on it to set, create, and remove individual exceptions. To get an individual exception, you access the ExceptionsGroup Items collection by exception name to get the ExceptionSetting.

At the bottom of this blog entry, I included the macro source code for a set of macros that wrap up adding, removing, and configuring CLR exceptions easy with full error handling and reporting. For those of you doing native development, you'll get the idea what you need to do. The one difference with Win32 Exceptions verses the other categories of exceptions is that you'll need to specify the exception codes to those exceptions.

In the code I wanted to include macros that would let me set or clear stopping when exceptions were thrown. The problem was that the macro literally took more than 15 minutes to enumerate and set each exception setting. It's faster to manually bring up the Exceptions dialog and click the check box next to the category. I'll keep looking to see if I can find a faster way to enable and disable stopping when thrown.

Those of you using my Debugger Settings add in I'm working on adding support for exception settings to it. Keep reading this space for updates. I'm worried about the performance of saving and restoring the exception settings given the horrible performance I'm seeing from the macro so it may turn out I won't add it.

The team that was running their tests under the debugger and wanted to automate setting various exceptions also was looking for a way to programmatically execute a macro in Visual Studio. They wanted to be able to configure their special exceptions as well as automatically attach or start their application. Fortunately, that's easy to do with the /command command line option to DEVENV.EXE. That will start the IDE and execute a Visual Studio command or macro.

As always, let me know if you have any questions or have an idea you'd like to see explored.

'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' CLRExceptions - John Robbins (c) 2009 - john@wintellect.com
' Macros that make it easier to manipulate CLR exceptions in the debugger.
'
' Do whatever you want with this code.
'
' Version 1.0 - April 1, 2009
'   - Initial version.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Imports System
Imports EnvDTE
Imports EnvDTE80
Imports EnvDTE90
Imports System.Diagnostics
Imports System.Text
Imports System.Collections.Generic
Imports System.Runtime.InteropServices

Public Module CLRExceptions

    
' Adds a new exception name to the list of CLR exceptions.
    Sub CLRExceptionCreate(Optional ByVal strException As String = "")
        CreateDeleteCLRException(strException,
True)
    
End Sub

    ' Removes an exception from the list of CLR exceptions.
    Sub CLRExceptionDelete(Optional ByVal strException As String = "")
        CreateDeleteCLRException(strException,
False)
    
End Sub

    ' Sets the specifically named exception to stop when thrown.
    Sub CLRExceptionBreakWhenThrown(Optional ByVal strException As String = "")
        SetClrException(strException,
True)
    
End Sub

    ' Sets the specifically named exception to continue when thrown.
    Sub CLRExceptionContinueWhenThrown(Optional ByVal strException _
                                      
As String = "")
        SetClrException(strException,
False)
    
End Sub

    ' Helper method to create or delete a CLR exception.
    Private Sub CreateDeleteCLRException(ByVal strException As String, _
                                        
ByVal create As Boolean)
        
Dim dbg As Debugger3 = DTE.Debugger
        
Dim cmdWindow As CmdWindow = New CmdWindow()
        
If (True = String.IsNullOrEmpty(strException)) Then
            cmdWindow.WriteLine("You must pass the exception as the parameter")
            
Return
        End If

        ' If ExceptionGroups is null, there's no project loaded.
        If (dbg.ExceptionGroups IsNot Nothing) Then

            Dim eg As ExceptionSettings = _
                dbg.ExceptionGroups.Item(
"Common Language Runtime Exceptions")
            
Try
                If (True = create) Then
                    eg.NewException(strException, 100)
                    cmdWindow.WriteLine(
"New CLR exception created: " + _
                                        strException)
                
Else
                    eg.Remove(strException)
                    cmdWindow.WriteLine(
"CLR exception deleted: " + _
                                        strException)
                
End If
            Catch ex As COMException
                cmdWindow.WriteLine(ex.Message)
            
End Try
        Else
            cmdWindow.WriteLine("You must open a solution to set exceptions")
        
End If
    End Sub

    ' Helper method to have a CLR exception stop or continue when thrown.
    Private Sub SetClrException(ByVal strException As String, _
                                
ByVal enabled As Boolean)
        
Dim dbg As Debugger3 = DTE.Debugger
        
Dim cmdWindow As CmdWindow = New CmdWindow()

        
If (True = String.IsNullOrEmpty(strException)) Then
            cmdWindow.WriteLine("Missing exception parameter")
            
Return
        End If

        Dim eg As ExceptionSettings = _
                dbg.ExceptionGroups.Item(
"Common Language Runtime Exceptions")
        
' If ExceptionGroups is null, there's no project loaded.
        If (dbg.ExceptionGroups IsNot Nothing) Then
            Try
                eg.SetBreakWhenThrown(enabled, eg.Item(strException))
                
Dim msg As String = "continue"
                If (True = enabled) Then
                    msg = "break"
                End If
                cmdWindow.WriteLine("Exception '" + strException + _
                                    
"' will " + msg + " when thrown")
            
Catch ex As COMException
                cmdWindow.WriteLine(
"Problem setting exception: " + ex.Message)
            
End Try
        Else
            cmdWindow.WriteLine("You must open a solution to set exceptions")
        
End If
    End Sub
End
Module

' Drop this into a module called Utilites
Imports System
Imports EnvDTE
Imports EnvDTE80
Imports EnvDTE90
Imports System.Diagnostics
Imports System.Globalization
Imports System.Text.RegularExpressions
Imports System.Collections.Generic

Public Module Utilities

     
' Stuff a new GUID into the current cursor location.
      Sub InsertGuid()
           
Dim objTextSelection As TextSelection
            objTextSelection =
CType(DTE.ActiveDocument.Selection(),  _
                                                 EnvDTE.TextSelection)
           
Dim cult As CultureInfo = _
                                     System.Threading.Thread.CurrentThread.CurrentUICulture
            objTextSelection.Text = Guid.NewGuid.ToString.ToUpper(cult)
     
End Sub

      ' My wrapper around the CommandWindow to make it easier to use.
      Public Class CmdWindow
           
' The internal Command window.
            Private m_CmdWnd As CommandWindow

           
' The constructor that simply gets the command window.
            Public Sub New()
                  m_CmdWnd =
CType(DTE.Windows.Item(EnvDTE.Constants.vsWindowKindCommandWindow).Object, CommandWindow)
           
End Sub

            ' Public method to write a line with a CRLF appended.
            Public Sub WriteLine(Optional ByVal Str As String = "")
                  m_CmdWnd.OutputString(Str + vbCrLf)
           
End Sub

            ' Public method to write a line.
            Public Sub Write(ByVal Str As String)
                  m_CmdWnd.OutputString(Str)
           
End Sub

            ' Clears the command window.
            Public Sub Clear()
                  m_CmdWnd.Clear()
           
End Sub

            ' Sends the input to the command window like you typed it.
            Public Sub SendInput(ByVal Command As String, Optional ByVal Commit As Boolean = True)
                  m_CmdWnd.SendInput(Command, Commit)
           
End Sub

      End Class
      Public Class OutputPane
           
' The output pane this class wraps.
            Private m_OutPane As OutputWindowPane
           
' The class constructor.
            Public Sub New(ByVal Name As String, _
                                
Optional ByVal ShowIt As Boolean = True)
                 
' First, get the main output window itself.
                  Dim Win As Window = _
                                      DTE.Windows.Item(EnvDTE.Constants.vsWindowKindOutput)
                 
' Show the window if I'm supposed to.
                  If (True = ShowIt) Then
                        Win.Visible = True
                  End If
                  ' Convert the window to a real output window.  
                  ' The VB way of casting!
                  Dim OutWin As OutputWindow = CType(Win.Object, OutputWindow)
                 
' Use exception handling to access this pane if it already exists.
                  Try
                        m_OutPane = OutWin.OutputWindowPanes.Item(Name)
                 
Catch e As System.Exception
                       
' This output tab doesn't exist, so create it.
                        m_OutPane = OutWin.OutputWindowPanes.Add(Name)
                 
End Try
                  ' If it's supposed to be visible, make it so.
                  If (True = ShowIt) Then
                        m_OutPane.Activate()
                 
End If
            End Sub
            ' Allows access to the value itself.
            Public ReadOnly Property OutPane() As OutputWindowPane
                 
Get
                        Return m_OutPane
                 
End Get
            End Property
            ' Wrapper.  Get the underlying object for all the rest
            ' of the OutputWindowPane methods.
            Public Sub Clear()
                  m_OutPane.Clear()
           
End Sub
            ' The functions I wanted to add.
            Public Sub WriteLine(ByVal Text As String)
                  m_OutPane.OutputString(Text + vbCrLf)
           
End Sub
            Public Sub Write(ByVal Text As String)
                  m_OutPane.OutputString(Text)
           
End Sub
      End Class

      ' SplitParameters - Splits up a parameter string passed to a macro
      ' into an array of parameters.
      Function SplitParameters(ByVal parameterString As String) As String()

           
' No sense doing anything if no parameters.
            If (0 = parameterString.Length) Then
                  Exit Function
            End If

            ' Cool regex from Michal Ash:
            ' http://regexlib.com/REDetails.aspx?regexp_id=621
            Dim initialArray As String()
            initialArray = Regex.Split(parameterString, _
                                   
",(?!(?<=(?:^|,)\s*\""(?:[^\""]|\""\""|\\\"")*,)" & _
                                   
"(?:[^\""]|\""\""|\\\"")*\""\s*(?:,|$))", _
                                    RegexOptions.Singleline)
           
' Go through and scrape off any extra whitespace on parameters.
            Dim returnArray As List(Of String) = New List(Of String)
           
Dim i As Int32
           
For i = 0 To initialArray.Length - 1
                  returnArray.Add(initialArray(i).Trim())
           
Next
            Return (returnArray.ToArray())
     
End Function

End
Module

 


2 Comments

  • Gravatar Image
    Mike Cline August 27, 2010 2:00 PM

    I know that this is a super-old post, but I thought I would post a comment/question anyhow.I have been trying turn on/off exception-catching programmatically and so thanks for posting information on it. Like you, I noticed that th performance is pretty poor -- it takes a moment to turn on/off one kind of exception, and so if you try to do something like I did:Dim exceptionGroup As ExceptionSettings = debugger.ExceptionGroups.Item("Common Language Runtime Exceptions")For Each item As ExceptionSetting In exceptionGroup exceptionGroup.SetBreakWhenThrown(True, item)NextThen it takes an eternity.Really I don't care to turn on/off specific ones, I'm happy to just turn on/off the entire CLR Exception group, which is what I normally do when I am doing it manually through the dialog.I have not come across any way to programmatically change the settings of an entire ExceptionSettings group at once, besides iterating them the slow way that I posted above. Are you aware of any shortcut for setting the state of an ExceptionSettings group?

  • Gravatar Image
    fun pics April 26, 2013 1:22 AM

    I can't get over how sensational your writing type is, you have to publish lots much more.

Have a Comment?

Archives

Blogs