What’s in This Chapter
Wrox.com Downloads for This Chapter
Please note that all the code examples for this chapter are available as a part of this chapter’s code download on the book’s website at www.wrox.com/go/csharp5programmersref on the Download Code tab.
Visual Studio’s code editor includes tools that help you avoid bugs. Some of the things the editor does to help you include
void
return type that don’t return a value on all code pathsDespite those tools, however, bugs are as certain in programming as death and taxes. Although it is theoretically possible for a program to be bug-free, chances are good that any nontrivial program contains bugs. Actually, in some testing strategies it’s an axiom that the program contains bugs. The goal is not to fix every bug but to fix so many of the bugs that those that remain occur extremely rarely.
Although this book is about the C# language and not Visual Studio, any reasonably complete C# book should provide some coverage of debugging tools and techniques. This chapter briefly describes some of the tools Visual Studio provides to help you track down and eliminate bugs. It also explains tracing methods that you can use to figure out what a program is doing and hopefully what it is doing wrong.
The Debug menu contains commands that are generally useful when you debug a program. They include commands to set and clear breakpoints, one of Visual Studio’s most useful features.
A breakpoint is a spot in the code that is marked to suspend execution. When the program reaches a line marked with a breakpoint, execution pauses so that you can examine the program’s status. While execution is paused, you can hover over variables to see their values, enter expressions in the Immediate window to evaluate them, and change values by setting variables equal to new values in the Immediate window. You can even edit the code to some extent and then continue running.
To set a breakpoint, place the cursor in the code editor on the line where you want to pause execution. Then press F9 or click in the margin to the left of the line of code to create the breakpoint. To remove a breakpoint, place the cursor on the line, and press F9 or click in the margin again.
The commands that are visible in the Debug window change depending on several conditions, such as the type of file you have open, whether the program is running, the line of code that contains the cursor, and whether that line contains a breakpoint. The following list briefly summarizes the most important items available in the Debug menu while execution is stopped at a breakpoint.
The Debug menu’s Windows submenu contains commands that display debugging-related windows. The following list briefly describes the most useful of these windows. The sections that follow this one provide more detail about the Breakpoints, Command, and Immediate windows.
Console
statements.Often it is useful to make the visible debug windows occupy separate tabs in the same area at the bottom of the IDE. That enables you to switch between them quickly and easily without them taking up too much space.
The Breakpoints window lists all the breakpoints. If you double-click a breakpoint in the list, you can easily jump to the code that holds it.
The icons to the left of the breakpoints shown in Figure 10-2 give information about their properties. The following list describes the icons.
Right-click a breakpoint and select Condition to display the dialog box shown in Figure 10-3. By default, a breakpoint pauses execution whenever it is reached. You can use this dialog box to add an additional condition that determines whether the breakpoint pauses the program when it is reached. In Figure 10-3, the breakpoint pauses execution if the expression (number < 10) && (i > 5)
is true
when the code reaches the breakpoint.
Right-click a breakpoint and select Hit Count to display the Breakpoint Hit Count dialog box, as shown in Figure 10-4. Each time the code reaches a breakpoint, it increments the breakpoint’s hit count. You can use this dialog box to make the breakpoint interrupt execution when it has been reached a specific number of times, a multiple of some number of times, or at least a certain number of times.
Right-click a breakpoint and select Filter to display the Breakpoint Filter dialog box, shown in Figure 10-5. You can enter a filter expression to make the breakpoint pause execution only for certain machines, processes, or threads.
Right-click a breakpoint and select When Hit to display the When Breakpoint Is Hit dialog box, as shown in Figure 10-6. Here you can specify the actions the debugger takes when the breakpoint is activated. Select the Print a Message check box to make the program display a message in the Immediate window. Select the Continue Execution check box to make the program continue running without stopping after it displays its message.
The Immediate window enables you to evaluate expressions and execute commands while the program is stopped in the debugger. One of the more useful commands displays a variable’s or expression’s value. Simply type the variable or expression and press Enter to see its value.
You can also set a variable’s value in the Immediate window. Simply type the variable’s name, the equal sign, and its new value. For example, the statement Width = 300
sets Width
equal to 300. If the code is running in a Windows Form, and no other variable named Width
is defined, this would set the form’s width to 300.
In addition to viewing and modifying variable values, the Immediate window enables you to execute methods so that you can easily test them. For example, suppose you have written a CheckPrinter
method. Then you could type the statement CheckPrinter()
in the Immediate window to execute that method.
Executing methods in the Immediate window enables you to test them quickly and easily without writing user interface code to handle all possible situations. You can call a method passing it different parameters to see what happens. If you set breakpoints within the method, the debugger pauses execution there.
The Debug
and Trace
classes provide several methods such as Write
and WriteLine
for displaying output. They let your program display information about what it is doing.
When you initially configure Visual Studio for C#, that information is written into the Immediate window. You can change that behavior in two ways. First, you can select Tools ⇒ Options, go to Debugging, and select General. If you uncheck the “Redirect All Output Window text to the Immediate Window” option, the Debug
and Trace
writing methods send output to the Output window instead of the Immediate window. (Even though that option says it redirects all output, it actually redirects only Debug
and Trace
output, not output generated by Console
writing methods such as Console.WriteLine
.)
The second method for changing where Debug
and Trace
output goes is to implement trace listeners. This is a useful and complicated enough technique that it is described in the following section.
The breakpoints described in the previous sections are extremely useful for figuring out what a program is doing. They let you step through the code as it executes to see what steps the program executes and to see what values are contained in variables.
However, sometimes a breakpoint can interfere with the program’s operation, so it prevents you from observing its normal behavior. For example, if the program uses event handlers to track the mouse’s position, a breakpoint stops the event handlers so that the program can’t track the mouse anymore.
Sometimes it’s hard to predict when an error will occur. For example, if a program encounters a bug only after running for several hours, it may be impractical to step through the code until a mistake occurs.
In those cases, it may be better to record events as they occur in a log or text file and then look at the record later. By default the Trace
and Debug
classes send their output to the Immediate or Console window. However, those classes share a Listeners
collection that keeps track of listener objects that should receive that output.
Initially, that collection holds the DefaultTraceListener
object, but you can remove that object or add new listeners if you like. Other listeners can send output to other locations such as log files, system event logs, or text files.
The .NET Framework provides the following three kinds of trace listener classes.
ConsoleTraceListener
—Sends output to the Console windowEventLogTraceListener
—Sends output to an event logTextWriterTraceListener
—Sends output to a stream such as a FileStream
The TraceIntoTextFile example program, which is available for download on the book’s website, uses the following code to demonstrate a trace listener.
private void Form1_Load(object sender, EventArgs e)
{
// Create the trace output file.
Stream stream = File.Create("DebugLog.txt");
// Create a TextWriterTraceListener for the trace output file.
Debug.Listeners.Add(new TextWriterTraceListener(stream));
// Write a startup note into the trace file.
Debug.WriteLine(DateTime.Now.ToString() + ": Debugging session started");
}
// Create and process a Student.
private void processButton_Click(object sender, EventArgs e)
{
Student student = new Student()
{
FirstName = firstNameTextBox.Text,
LastName = lastNameTextBox.Text,
Street = streetTextBox.Text,
City = cityTextBox.Text,
State = stateTextBox.Text,
Zip = zipTextBox.Text
};
ProcessStudent(student);
}
// Process a student.
private void ProcessStudent(Student student)
{
Debug.WriteLine("ProcessStudent");
Debug.Indent();
Debug.WriteLine("Name: " + student.FirstName + " " + student.LastName);
Debug.WriteLine("Address: " + student.Street);
Debug.WriteLine(" " + student.City + " " + student.State + " " +
student.Zip);
Debug.Unindent();
///... Process the student here...
}
// Close the Debug trace file.
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
Debug.WriteLine(DateTime.Now.ToString() + ": Debugging session ended");
Debug.Close();
}
When the form loads, the program creates a file named DebugLog.txt. It uses that file’s stream to create a new TextWriterTraceListener
and adds it to the Debug
object’s Listeners
collection.
The program then uses Debug.WriteLine
to write a start message. Because the Debug
object’s Listeners
collection contains the default listener in addition to the new TextWriterTraceListener
, the message is displayed in the Immediate window and written into the file.
If you enter information about a student in the form’s TextBox
es and then click the Process button, the processButton_Click
event handler executes. It creates a new Student
object and passes it to the ProcessStudent
method.
The ProcessStudent
method uses Debug
statements to display the Student
’s information in the Immediate window and to write it into the listener file. The method uses Debug.Indent
and Debug.Unindent
to indent the Student
’s information.
When you later close the form, the Form1_FormClosing
event handler uses Debug.WriteLine
to display an ending message. It then calls Debug.Close
to flush any output that hasn’t been written into the listener file and to close the file. If you skip this step, some of the output is likely to be lost.
The following text shows sample output written into the listener file.
4/1/2014 3:06:25 PM: Debugging session started
ProcessStudent
Name: Rod Stephens
Address: 1337 Leet St
Bugsville AZ 87654
4/1/2014 3:06:27 PM: Debugging session ended
You can make a couple of useful changes to the previous program. First, you can open the listener file for appending instead of writing over previous output. Second, you can allow sharing when you open the file, so other programs such as Microsoft Word and Notepad can view the file while the program is still running. (If you allow sharing, you need to flush output every time you write or set AutoFlush
to true
. Otherwise the other program won’t see the latest items written into the file.)
The following code shows how you can open the listener file and allow sharing.
// Open the trace output file. Allow sharing and use AutoFlush.
Stream stream = File.Open("DebugLog.txt",
FileMode.Append, FileAccess.Write, FileShare.Read);
Debug.AutoFlush = true;
C# and Visual Studio cannot debug your programs for you, but they do provide the tools you need to do it yourself. Breakpoints are particularly useful. They enable you to pause execution at selected lines of code, so you can examine the program’s variables and step through execution line by line to see what the program is doing, and hopefully what it is doing wrong. You can place conditions on breakpoints, so they stop the program only under certain conditions, such as when a breakpoint has been reached a certain number of times or when a variable contains a particular value.
The Debug
and Trace
classes provide methods that the program can use to display diagnostic information in the Immediate window. By adding trace listeners to those classes, you can save information in other places such as event logs or text files without interrupting the program. Later you can analyze that information to see what the program did.
The chapters in Part II, “C# Language Elements,” of this book focus on small-scale programming issues. They explain how to create variables, perform calculations, and overload operators; how to control program flow with statements such as if
, switch
, for
, and while
; how to select and arrange data with LINQ; and how to use debugging and tracing tools to understand what a program is doing and find bugs.
The chapters in Part III, “Object-Oriented Programming,” move to a higher level and describe object-oriented concepts. They explain key object-oriented concepts, how to build structures and classes, how to use collection classes, and how to use generics to make one class work with many data types.
private long Factorial(long number)
{
if (number <= 1) return 1;
return number * Factorial(number - 1);
}
Use the Debug
class’s methods to trace this method’s execution. Each call to the Factorial
method should indicate when it is called and with what parameter. Before returning, each call should display its result. Use indentation to make it easier to match calls and results. For example, the following text shows the output while calculating Factorial(4)
.
Factorial(4)
Factorial(3)
Factorial(2)
Factorial(1)
Result: 1
Result: 2
Result: 6
Result: 24
Factorial
method displays its parameter and result as in the following text.
Factorial(1) = 1
Factorial(2) = 2
Factorial(3) = 6
Factorial(4) = 24
Debug
statements to display each call and its result in the order in which they are called? For example, can you make the program display the following trace when calling Factorial(4)
?
Factorial(4) = 24
Factorial(3) = 6
Factorial(2) = 2
Factorial(1) = 1
Factorial
method to produce this kind of display. (Hint: You may want to make larger changes to the method, for example, changing its parameter list or return value.)Debug
and Trace
classes let a program provide different levels of output depending on whether the DEBUG
and TRACE
preprocessor symbols are defined. Create a PrintMessage
method that takes two parameters: a debugging level and a message. If the debugging level is less than or equal to a corresponding preprocessor symbol, display the message in the Console window. (For example, if the symbol DEBUG2
is defined and the method’s debugging level is less than or equal to 2, display the message.)
Compare this method to the Debug
and Trace
classes. How could you improve this method?
Debug
and Trace
messages into the file Messages.txt instead of displaying them in the Console or Immediate window. Open the file for append, allow read-only sharing, automatically flush the log files, and include the time with each message. Write a few messages using Debug
, Trace
, and Console
. Before the program closes the file, open the file with Microsoft Word and Notepad to verify that you can do it.Debug
and Trace
classes is that they share the same Listeners
collection, so you can’t send Debug
output to one location and Trace
output to another. Write a method called LogMessage
that takes two parameters: a filename and a message. The method should append the current time plus the message to the indicated file. (Chapter 19, “File System Objects,” has a lot to say about reading and writing files. For now just use System.IO.File.AppendAllText
to add text to the file.) Then rewrite the program you wrote for Exercise 6 to use that method to write messages into the files DebugLog.txt and TraceLog.txt.18.118.208.97