Debugging code is probably one of the most essential tasks that a developer performs. Being able to run your application and pause the execution of code midway is a lifesaver. But there is a lot more to debugging than just setting breakpoints and viewing results.
Using breakpoints, conditional breakpoints, breakpoint actions and labels, and exporting breakpoints
Using data tips
The DebuggerDisplay attribute
Diagnostic Tools and Immediate Window
Attaching to a running process
Remote debugging
Visual Studio gives developers all the required tools in order to effectively debug the code you are experiencing problems with. Without being able to debug your code, it will be virtually impossible to resolve any issues you might be experiencing.
Not being able to effectively debug your application (not knowing how to effectively use the tools you have) is just as bad as not having the tools to debug with in the first place.
Working with Breakpoints
If you are familiar with debugging in Visual Studio, this chapter might seem like old hat for you. Stick around, there might be sections discussed here that you didn’t know about.
If you are new to Visual Studio, the concept of debugging in Visual Studio is when you run your application with the debugger attached. Debugging allows you to step through the code and view the values stored in variables. More importantly, you can see how those values change.
Setting a Breakpoint
The Start button now changes to display Continue. Remember, at this point, your code execution is paused in Visual Studio at the breakpoint you set earlier.
Step Into (F11)
Step Over (F10)
Step Out (Shift+F11)
When you step into a method, you jump to the point in the editor where that method’s code is. If you do not want to step into the method, you can click the Step Over button or press F10 to carry on with the next line of code. If you are inside a method and want to step out and continue debugging the calling code, click the Step Out button or press Shift+F11.
Step into Specific
Imagine that we need a method that generates a waybill number based on specific business rules. Then, when the application starts, the text box field is auto populated with the generated waybill number.
Waybill Generation Code
Form Load
If you had placed a breakpoint on the line of code that contains the GenerateWaybill() method and step into the methods by pressing F11, you would first step into method WBPartA(), then into method WBPartB(), and lastly into the GenerateWaybill() method.
Simply select the method you want to step into and off you go.
Run to Click
This will advance the debugger to the line of code where you clicked, allowing you to continue stepping through the code from the new location. Quite handy if you do not want to be pressing F10 a gazillion times.
Run to Cursor
Doing this will start the debugger and set a temporary breakpoint on the line you right-clicked. This is useful for quickly setting a breakpoint and starting the debugger at the same time. When you reach the breakpoint, you can continue debugging as normal.
Be aware though that you will be hitting any other breakpoints set before the temporary breakpoint first. So, you will need to keep on pressing F5 until you reach the line of code you set the temporary breakpoint on.
Force Run to Cursor
Here, we have a breakpoint at the top of the method that is currently hit, and the debugger is paused. I want to run the cursor to the return statement, but there are two breakpoints between where my debugger is and where I want to be. To skip the breakpoints in the middle, hover the mouse over the line that you want to move to next, and hold down the Shift key. The Run to execution button with the single arrow (as seen in Figure 3-6) now changes to the Force run execution to here button with a double arrow as seen in Figure 3-8. This will now advance the debugger to the line of code you forced the debugger to, skipping all the breakpoints in between. This is especially convenient since I do not have to remove my breakpoints, nor do I have to keep on pressing F5 for each breakpoint hit.
Conditional Breakpoints and Actions
Sometimes, you need to use a condition to catch a bug. Let’s say that you are in a for loop, and the bug seems to be data related. The erroneous data only seems to enter the loop after several hundred iterations. If you set a regular breakpoint, you will be pressing F10 until your keyboard stops working.
We will discuss Actions shortly.
You will have noticed the Actions checkbox from the Breakpoint Settings. You will also see the Actions menu on the context menu in Figure 3-9. Here, you can add an expression to log to the Output Window using specific keywords that are accessed using the $ symbol.
$ADDRESS – Current instruction
$CALLER – Previous function name
$CALLSTACK – Call stack
$FILEPOS – The current file and line position
$FUNCTION – Current function name
$PID – Process ID
$PNAME – Process name
$TICK – Milliseconds elapsed since the system was started, up to 49.7 days
$TID – Thread ID
$TNAME – Thread name
Action Expression
If you want to pause the code execution, then you need to uncheck the Continue execution checkbox.
Temporary Breakpoints
You can also hold down Shift+Alt+F9, T to do the same thing.
Dependent Breakpoints
If you look at Figure 3-16 again, you will notice an option to insert a dependent breakpoint. A dependent breakpoint is a fantastic addition to Visual Studio because it is a breakpoint that will only pause the debugger when another breakpoint is hit on which it has been marked as a dependent.
Dragging Breakpoints
You can also drag breakpoints to a different line of code. To do this, click and hold on the breakpoint and start dragging your mouse. You can now move it to another line.
Manage Breakpoints with Labels
This is where the Breakpoints window comes in handy. Think of it as mission control for managing complex debugging sessions. This is especially helpful in large solutions where you might have many breakpoints set at various code files throughout your solution.
The Breakpoints window allows developers to manage the breakpoints that they have set by allowing them to search, sort, filter, enable, disable, and delete breakpoints. The Breakpoints window also allows developers to specify conditional breakpoints and actions.
Compare the line numbers of the breakpoints listed in Figure 3-19 with the breakpoints displayed in Figure 3-18. You will see that this accurately reflects the breakpoints displayed in the Breakpoints window.
The only problem with this window is that it doesn’t help you much in the way of managing your breakpoints. At the moment, the only information displayed in the Breakpoints window is the class name and the line number.
You can type in a new label or choose from any of the existing labels available. If you swing back to the Breakpoints window, you will see that these labels are displayed (Figure 3-22), making the identification and management of your breakpoints much easier.
You are in a better position now with the breakpoint labels set to manage your breakpoints more effectively.
Exporting Breakpoints
If you would like to save the current state and location of the breakpoints you have set, Visual Studio allows you to export and import breakpoints. This will create an XML file with the exported breakpoints that you can then share with a colleague.
I foresee the use of Visual Studio Live Share replacing the need to share breakpoints with a colleague just for the sake of aiding in debugging an application. There are, however, other situations I can see exporting breakpoints as being beneficial.
I’m not too convinced that the icons used on the import and export buttons are indicative of importing and exporting something, but that is just my personal opinion.
Using DataTips
DataTips in Visual Studio allows developers to view information about variables during a debug session. You can only view DataTips in break mode, and DataTips only work with variables that are currently in scope.
DataTips also allow you to edit the value of the variable, as long as the value isn’t read-only. To do this, simply select the value in the DataTip and enter a new value. Then press the Enter key to save the new value.
Visualizing Complex Data Types
The Subject Class
Convert List to DataTable
Create the List<Subject> and the DataTable
The magnifying glass icon tells us that one or more visualizers are available for the variable, in this example, the DataTable Visualizer.
Bonus Tip
Add a DataTip Expression
This is great if you forgot to add a variable watch or just want to see some additional info regarding the variable in the DataTip.
Using the Watch Window
The Watch window allows us to keep track of the value of one or more variables and also allows us to see how these variable values change as one steps through the code.
Here, you can open the visualizer by clicking the magnifying glass icon or expanding the table variable to view the other properties of the object. I use the Watch window often as it is a convenient way to keep track of several variables at once.
The DebuggerDisplay Attribute
In the previous section, we discussed how to add a variable to the Watch window in Visual Studio. We saw that we can view the value of a variable or variables easily from this single window.
This will quickly become rather tedious, especially when you are dealing with a rather large list, and you are looking for a specific value.
This is where the DebuggerDisplay attribute comes into play. We are going to modify the Subject class.
Ensure that you add the statement using System.Diagnostics to your code file.
Modified Subject Class
The use of “nq” in the DebuggerDisplay attribute will remove the quotes when the final value is displayed. The “nq” means “no quotes.”
Evaluate Functions Without Side Effects
While debugging an application, we probably do not want the state of the application to change because of an expression we are evaluating. It is, unfortunately, a fact that evaluating some expressions might cause side effects.
The Student Class
In this class, we have a HasSubjects() method that simply returns a Boolean indicating if the Student class contains a list of subjects. We also have a property called StudentSubjects that returns the list of subjects. If the list of subjects is null, it creates a new instance of List<Subject>.
It is here that the side effect is caused. If the HasSubjects() method returns false, calling the StudentSubjects property will change the value of HasSubjects().
When we call the StudentSubjects property, we see this side effect come into play in Figure 3-34. As soon as this property is called, the value of the HasSubjects() method changes.
This means that the state of our Student class has changed because of an expression that we ran in the Watch window.
This time, the value of the HasSubjects() method remains the same, which means that the state of your class remains unchanged. As you have probably guessed by now, the nse added after the expression stands for “No Side Effects.”
Format Specifiers
Format specifiers allow you to control the format in which a value is displayed in the Watch window. Format specifiers can also be used in the Immediate and Command window. Using a format specifier is as easy as entering the variable expression and typing a comma followed by the format specifier you want to use. The following are the C# format specifiers for the Visual Studio debugger.
ac
Force evaluation of an expression decimal integer
d
Decimal integer
dynamic
Displays the specified object using a Dynamic View
h
Hexadecimal integer
nq
String with no quotes
nse
Evaluates expressions without side effects where “nse” means “No Side Effects”
hidden
Displays all public and nonpublic members
raw
Displays item as it appears in the raw node. Valid on proxy objects only
results
Used with a variable that implements IEnumerable or IEnumerable<T>. Displays only members that contain the query result
You will recall that we used the “nq” format specifier with the DebuggerDisplay attribute discussed in a previous section.
Diagnostic Tools
Visual Studio gives developers access to performance measurement and profiling tools. The performance of your application should, therefore, be high on your priority list. An application that suffers from significant performance issues is as good as broken (especially from an end user’s perspective).
As you debug your application, you can see the CPU usage, memory usage, and other performance-related information.
CPU Usage
Because this excludes the calling and called functions, you get a better understanding of the function you are evaluating and can determine if it is the performance bottleneck or not.
Memory Usage
Visual Studio Diagnostic Tools allows developers to see what the change in memory usage is. This is done by taking snapshots. When you start debugging, place a breakpoint on a method you suspect is causing a memory issue. Then you step over the method and place another breakpoint. An increase is indicated with a red up arrow as seen in Figure 3-44.
The Events View
IntelliTrace events are available in this tab if you have Visual Studio Enterprise.
For a comparison of the Visual Studio 2022 Editions, head on over to https://visualstudio.microsoft.com/vs/compare/ and see what each edition has to offer.
PerfTips allows developers to quickly identify potential issues in your code.
The Right Tool for the Right Project Type
Performance Tools for Project Types
Performance Tool | Windows Desktop | UWP | ASP.NET/ASP.NET Core |
---|---|---|---|
CPU Usage | Yes | Yes | Yes |
Memory Usage | Yes | Yes | Yes |
GPU Usage | Yes | Yes | No |
Application Timeline | Yes (XAML) | Yes | No |
PerfTips | Yes | Yes | Yes |
Performance Explorer | No | No | No |
IntelliTrace | .NET with VS Enterprise only | .NET with VS Enterprise only | .NET with VS Enterprise only |
Events viewer | Yes | Yes | Yes |
.NET Async | Yes (.NET only) | Yes | Yes |
.NET Counters | Yes (.NET Core only) | No | Yes (ASP.NET Core only) |
Database | Yes (.NET Core only) | No | Yes (ASP.NET Core only) |
.NET Object Allocation | Yes (.NET only) | Yes | Yes |
Immediate Window
The Immediate Window in Visual Studio allows you to debug and evaluate expressions, execute statements, and print the values of variables. If you don’t see the Immediate Window, go to the Debug menu, and select Windows, and click Immediate or hold down Ctrl+D, Ctrl+I.
Opening up the Immediate Window and typing in sub.SubjectDescription will display its value as seen in Figure 3-49.
DisplayMessage Function
Any breakpoints contained in the function will break the execution at the breakpoint. Use the debugger to examine the program state.
Attaching to a Running Process
The available processes list allows you to select the process you want to attach to. You can quickly find the process you want by typing the name in the filter process text box.
You can, for example, attach to the w3wp.exe process to debug a web application running on IIS. To debug a C#, VB.NET, or C++ application on the local machine, you can use the Attach to Process by selecting the <appname>.exe from the available processes list (where <appname> is the name of your application).
Attach to a Remote Process
To debug a process running on a remote computer, select Debug and click Attach to Process menu, or hold down Ctrl+Alt+P to open the Attach to Process window. This time, select the remote computer name in the Connection target by selecting it from the drop-down list or typing the name in the connection target text box and pressing Enter.
If you are unable to connect to the remote computer using the computer name, use the IP and port address.
Remote Debugger Port Assignments
Visual Studio 2022: 4026
Visual Studio 2019: 4024
Visual Studio 2017: 4022
Visual Studio 2015: 4020
Visual Studio 2013: 4018
Visual Studio 2012: 4016
The port assigned to the Remote Debugger is incremented by two for each release of Visual Studio.
Reattaching to a Process
Starting with Visual Studio 2017, you can quickly reattach to a process you previously attached to. To do this, you can click the Debug menu and select Reattach to Process or hold down Shift+Alt+P. The debugger will try to attach to the last process you attached by matching the previous process ID to the list of running processes. If that fails, it tries to attach to a process by matching the name. If neither is successful, the Attach to Process window is displayed and lets you select the correct process. The option to reattach to a process you previously attached will only be available if you had previously attached to it.
Remote Debugging
Sometimes, you need to debug an application that has already been deployed to a different computer. Visual Studio allows you to do this via remote debugging. To start, you need to download and install remote tools for Visual Studio 2022 on the remote computer.
Remote tools for Visual Studio 2022 enables app deployment, remote debugging, testing, profiling, and unit testing on computers that don’t have Visual Studio 2022 installed.
System Requirements
Windows 11
Windows 10 (not phone)
Windows 8 or 8.1 (not phone)
Windows 7 SP 1
Windows Server 2016
Windows Server 2012 or Windows Server 2012 R2
Windows Server 2008 SP 2, Windows Server 2008 R2 Service Pack 1
1.6 GHz or faster processor
1 GB of RAM (1.5 GB if running on a VM)
1 GB of available hard disk space
5400 RPM hard drive
DirectX 9-capable video card running at 1024 x 768 or higher display resolution
The remote computer and your local machine (the machine containing Visual Studio) must both be connected over a network, workgroup, or homegroup. The two machines can also be connected directly via an Ethernet cable.
Take note that trying to debug two computers connected through a proxy is not supported.
It is also not recommended to debug via a dial-up connection (do those still exist?) or over the Internet across geographical locations. The high latency or low bandwidth will make debugging unacceptably slow.
Download and Install Remote Tools
Connect to the remote machine and download and install the correct version of the remote tools required for your version of Visual Studio. The link to download the remote tools compatible with all versions of Visual Studio 2022 is https://visualstudio.microsoft.com/downloads#remote-tools-for-visual-studio-2022.
If you are using Visual Studio 2017, for example, download the latest update of remote tools for Visual Studio 2017.
Also, be sure to download the remote tools with the same architecture as the remote computer. This means that even if your app is a 32-bit application, and your remote computer is running a 64-bit operating system, download the 64-bit version of the remote tools.
Running Remote Tools
After the installation has been completed on the remote machine, run the Remote Debugger application as Administrator if you can. To do this, right-click the Remote Debugger app, and click Run as Administrator.
If you encounter this window, it possibly means that there is a configuration issue that you need to resolve. The Remote Debugging Configuration dialog box will prompt you to correct configuration errors it picks up. Do this by clicking the Configure remote debugging button.
You are now ready to start remote debugging your application.
Start Remote Debugging
The great thing about the Remote Debugger on the remote computer is that it tells you the server name to connect to. In Figure 3-56, you can see that the server is named 20F4B56E-AB26-4:4026 where 4026 is the port assignment for Visual Studio 2022. Make a note of this server name and port number.
- 1.
Click the Debug tab, check the Use remote machine checkbox, and enter the remote machine name and port noted earlier. In our example, this is 20F4B56E-AB26-4:4026.
- 2.
Make sure that you leave the Working directory text box empty and do not check Enable native code debugging.
- 3.
When all this is done, save the properties and build your project.
- 4.
You now need to create a folder on the remote computer that is the same path as the Debug folder on your local machine (the Visual Studio machine). For example, the path to the project Debug folder on my local machine is <source path> ShipmentLocatorAppVisualStudioRemoteDebuginDebug. Create this same path on the remote machine.
- 5.
Copy the executable that was just created by the build you performed in step 3 to the newly created Debug folder on the remote computer.
Be aware that any changes to your code or rebuilds to your project will require you to repeat step 5.
- 6.
Ensure that the Remote Debugger is running on the remote computer. The description (Figure 3-56) should state that it is waiting for new connections.
- 7.
On your local machine, start debugging your application, and if prompted, enter the credentials for the remote machine to log on. Once logged on, you will see that the Remote Debugger on the remote computer displays that the remote debug session is now active (Figure 3-58). A point to note here is that if you trust the network that you are debugging across, and you are having problems logging on, you can specify that no authentication is done. In the project properties, change the Authentication mode from Windows Authentication to No Authentication (Figure 3-57). Then, on the remote machine, click the Remote Debugger and click the Tools menu, and select Options. Here, you can specify that no authentication is done and that any user can debug.
- 8.
After a few seconds, you will see your application’s main window displayed on the remote machine (Figure 3-59). Yep, breakfast is the most important meal of the day.
- 9.
On the remote machine, take whatever action is needed to hit the breakpoint you set earlier. I simply set a breakpoint behind the Start button click event handler. When you hit the breakpoint, it will be hit on your local machine (Visual Studio machine).
If you need any project resources to debug your application, you will have to include these in your project. The easiest way is to create a project folder in Visual Studio and then add the files to that folder. For each resource you add to the folder, ensure that you set the Copy to Output Directory property to Copy always.
Summary
In this chapter, we saw that Visual Studio provides a rich set of debugging tools for developers. Breakpoints provide a lot of flexibility when you need to pause your application and inspect the state of variables and other objects. Remote debugging allows developers to inspect the state of the application when running on a machine that is not under their direct control. The Diagnostic Tools also allow developers to inspect CPU and memory usage for their applications and help identify bottlenecks. In the next chapter, we will take a closer look at unit testing and how to create and run unit tests, using IntelliTest, and how to measure code coverage in Visual Studio.