Now that you've gained a good understanding of the Visual Studio .NET debugger's capabilities, in this chapter I'll look in more detail at how to debug Windows Forms applications, including class libraries and Windows control libraries (known in the COM world as dynamic link libraries [DLLs] and Windows controls , respectively). During this chapter you're going to use the Visual Studio debugger and its facilities in earnest. If you haven't already read my discussions on this debugger and its facilities in Chapters 3 and 4, now would be an excellent time to go back and do this.
This chapter first looks at debugging a relatively simple real-life Windows Forms application ”in fact, it uses the sorting application originally mentioned at the end of Chapter 4 in the context of code optimization. Later I show you how to handle some tricky debugging situations associated with Windows Forms applications. Finally, you'll learn how to debug Windows Forms controls, Visual Studio add-ins, and Windows control designers.
The first application to be debugged is a Windows Forms program that compares the performance of various sorting algorithms. The application is a Visual Studio solution containing two VB .NET projects; you can find this solution in the DebugDemo folder. The first project in the solution, DebugDemoForm , is a form that acts as the user interface for the application, and the other project, DebugDemoLogic , is a class library containing each of the sorting algorithms being tested . Though it would be simpler to add the sorting algorithm class to the main application, separation of the user interface and the business logic into two components is good development practice and also makes it easier to show you some subtleties in the debugging process.
Figure 7-1 shows the user interface for the sort testing application. The text boxes on the left side of the window allow you to specify the length and values of the array to be sorted, and the radio buttons on the right side allow you to choose the sort algorithm to be tested. The label placed in the lower right corner of the window shows the time that the chosen algorithm took to sort the chosen array.
Having set all of the appropriate debugging options as discussed in Chapter 4, you now need to build the application before you can debug it. From the Build ’ Configuration Manager menu item, check that the active solution configuration is set to Debug and that each project's Configuration setting also shows Debug. This tells the compiler that you want to create a debug build of the application.
For the purposes of this chapter, you should also ensure that the Build check box is selected for each of the projects. If you're working with an application where you know that some of the solution's projects won't change while you're testing or debugging, then you can deselect the Build option for projects that you don't want to build every time. This can save you a considerable amount of build time when you have a solution with multiple projects.
Now you can build the application by choosing the Build ’ Build Solution menu item. For this application, the build process creates an. exe file for the user interface and a .dll file for the class library. In addition, it creates a .pdb file containing debug symbols for each of these two components. The Output window should show that both of the components have been built successfully. If there had been any compilation errors or warnings, these would appear in the Task List window.
After completing the debugging preparations , you can finally begin the debugging investigation. Start the sorting program by pressing F5, which is the standard shortcut key for starting a program with debugging enabled. If the Debug and Debug Location toolbars aren't visible, you should right-click in the toolbar area and select these two toolbars from the drop-down menu.
To break into a program automatically, you can add a breakpoint in the program's Source window and then perform a program action that causes this breakpoint to be hit. This program containing the breakpoint will then become the active debug process. For the purposes of this debugging session, you should add a breakpoint on line 178 in the DemoForm.vb source code by clicking this line in the far-left shaded area of the Source window. The result should be an active breakpoint glyph: a black circle representing an active breakpoint.
Now you can right-click the black circle representing the breakpoint and select Breakpoint Properties from the resulting menu. The resulting dialog window, as shown in Figure 7-2, shows that a file breakpoint has been set at the first character of line 178 in the DemoForm.vb source file. The reason that this dialog window allows a character setting other than 1 is that you might have multiple statements on a single line and want to set the breakpoint on one of the later statements. You can also see that this is a simple breakpoint. To set an advanced breakpoint, you would use either of the two buttons shown in the dialog window to add a breakpoint condition or a hit count, as I discussed in Chapter 3.
If you now click the program's Start sort button, the application drops into the debugger at the breakpoint that you added on line 178. Looking at the Debug Location toolbar, you can see that it's no longer grayed-out ”the Program drop-down shows the name of the executable and its process ID, the Thread drop-down shows the ID of the program's main thread, and the Stack Frame drop-down shows the current call stack. The executable's process ID is the same as the PID shown in the Processes tab of Task Manager.
If you ever have any problems with breakpoints not being hit, the first step you should take is to check the Modules window. You can find this on the Debug ’ Windows menu. This window shows you every module loaded by your program. The two modules of interest now are DebugDemoForm.exe and DebugDemoLogic.dll. The Information column in the Modules window shows you whether or not debug symbols were loaded for each module. These debug symbols are contained within the.pdb file generated by the build process for each module, as explained in Chapter 3. Unless these symbol files are loaded, the debugger is unable to work back from the processor-native code being executed to the associated source code, which means that your breakpoints won't be hit and you won't be able to debug the application.
Another common reason for breakpoints not being hit is when you start an application from within Visual Studio by using Ctrl-F5 rather than F5. Ctrl-F5 is the menu shortcut that tells Visual Studio to start your application without attaching a debugger, so breakpoints will simply be ignored.
A third reason for missing breakpoints is when multiple copies of your solution's projects exist on disk, and Visual Studio is loading a version without a compatible debug symbol file. You can spot this situation by checking the load path of each of your solution's modules in the Modules window. If this happens, you need to stop the program and then tell Visual Studio to load and execute the correct version of the errant module. The best way of doing this is to rename or delete any stray versions that are causing Visual Studio to become confused .
A fourth reason for missing breakpoints is when you launch your application in a Release configuration rather than a Debug configuration. You can tell which configuration is being used by checking the Configuration drop-down on the Standard toolbar. The default release configuration doesn't allow debugging.
A final reason for breakpoints not being hit is when the debug symbol file doesn't match the code actually executing. In this case, the debugger either will be unable to load the debug symbols or will load them but be unable to construct a path back from the native code to your source code. This is one reason why it's very important to keep a matching.pdb file for every production assembly that you build.
Once the sorting application has stopped at the breakpoint on line 178, you can use the mouse to "hover" over variables and functions to see both values and definitions. For instance, if you hover with the mouse over the function in line 183 called DoSort , you can see that this function is called with an enumeration called m_SortType and returns a variable of type Double . Then if you look in the same way at the actual argument to the function, the ToolTip will show that m_SortType = BubbleSort . It's useful to note that these ToolTips also work at design time, when you're writing your code ”you're able to see the definition of every constant, variable, and function.
Like most of the debugging windows, the Me window can be found hanging off the Debug menu. This window provides you with a very quick way of seeing the values belonging to whichever object is currently in scope ”in this case, the form called DemoForm . Figure 7-3 shows this window, with one of the form's text box controls expanded for easier viewing. As you can see, the property evaluations are disabled within the Me window because of my earlier recommendation to turn off property evaluation in the debugging windows.
The Breakpoints window allows you to examine and manipulate all of the breakpoints in your solution. One neat facility available from this window is the ability to disable one or more breakpoints. This means that you can ignore breakpoints that don't apply to your current debugging scenario, but you can still have them available when your scenario changes. Combine that with the fact that all breakpoints are stored in a solution's .suo file so that you can unload and reload solutions without losing the breakpoints and you have a major advance over debugging within the VB.Classic environment.
Now bring up the first Watch window and double-click the SortObject variable on line 178. You can drag this variable onto the Watch window and see its associated values. Keep this window open because you'll be using it again very shortly.
Stepping into the DoSort procedure using the Debug ’ Step Into menu item provides an opportunity to investigate another debugging window. If you stop on line 51 of the DemoSort.vb file and display the Call Stack window, you can see the current call stack. This lists all of the procedure calls that are currently active, starting at the top with the current procedure. The lines shown in gray are the ones for which debug symbols are not available. Now drag the ArrayItem variable from line 51 onto the same Watch window that's displaying the SortObject variable. Notice that only one of these variables is in scope, so only one value is being shown.
The Call Stack window allows you to switch the current program context, otherwise known as the stack frame . If you right-click the second line in the Call Stack window, the one that represents the procedure that invoked the current procedure, you can choose the Switch To Frame menu item. Now you can see that the SortObject variable shown in the Watch window is back in scope, and the ArrayItem variable is not. The ability to perform this type of "backward" debugging can make a major contribution to the swift location of bugs .
Another good use of the Call Stack window is to display the call stack after a bug has been found. This makes it easier to discover methods that are being invoked at the wrong time or from the wrong place.
Now you can use the Debug ’ Step Out menu item to return to line 178 of the DemoSort.vb file. Disable the breakpoint on line 178 by removing the check mark on the left side of the Breakpoints window and then press F5 to continue executing the program. It's time to go bug hunting.
For demonstration purposes, I've added a subtle bug to the procedure that performs the selection sort. After each array sorting, a procedure called SortCheck is called that verifies that the array has really been sorted correctly. If you test the "Selection sort" option while setting the "Max item value" to 100, you can see that the SortCheck routine uses an assertion to report that the array hasn't been sorted correctly. The task now is to find where the bug is happening and ultimately how to fix it. To do this, you need to look at the selection sort procedure to find out how it's supposed to work and then run one or more tests to establish where and how the bug is occurring.
A quick look at the SortSelection procedure shows you that the code runs through the unsorted array multiple times, and on each occasion it finds the lowest (by value) unsorted item and swaps that with the lowest (by index) unsorted item. Because the selection sort works item by item upward through the array, it should never move a sorted item to a position where it has a larger value than the unsorted item directly above it. This can therefore be the condition for the advanced breakpoint, which will be placed at the point in the sorting process where each unsorted item is about to be swapped into its correct sorted position.
Run the sorting application and set the number of array items to 10 and the range of array item values to 100. These low numbers should give a manageable array size and make it easier for you to inspect the array values.
Now right-click the gray band to the left of line 167 and add a file breakpoint. In the breakpoint condition, add BestValue>=ListBeingSorted(LoopOuter+1) as the breakpoint condition, and then set the breakpoint condition to be "is true". This test is designed to trigger the breakpoint only when the selection sort sets the BestValue variable to a wrong value (i.e., a value that is larger than the value of the unsorted item directly above it).
Next , launch Watch window 1, clear any variables currently shown in this window, and then drag the ListBeingSorted , BestValue , and LoopOuter variables to this window. You can use this window to monitor the state of the array as it is sorted. Then launch Watch window 2 and drag the ListUnsorted variable to this window. This will show the original unsorted array for comparison with the array that is being sorted.
Now choose the "Selection sort" option and start the sort. When the conditional breakpoint is hit, your display should look something like Figure 7-4. Your numbers should be identical to those shown because the list to be sorted is always produced from the same seed. If you look closely at the array being sorted in Watch window 1, you can see that something is very wrong. The BestValue variable contains a value (12) that doesn't actually exist in the unsorted array. Here's the first clue to the bug ”it looks as though BestValue is being corrupted somewhere. Notice how you were able to find preliminary information about this problem with a single breakpoint and without having to step through the code. Conditional breakpoints allow you to go straight to the heart of a problem because you can calculate expressions and conditions that express a problem precisely, and your breakpoints can be set up to trigger only when those calculated conditions are met.
Now that you've found a possible cause of the bug, the next step is to locate where the BestValue variable is being corrupted. First, you'll need to find all statements where BestValue is changed. Here you can employ a little trick. If you change the declaration of BestValue so that it's named differently (say, by adding a single letter to the end of the variable name), BestValue will become undefined and every place that it's mentioned in the Source window will be underlined with a colored squiggle . In this way, you can locate every instance where BestValue is modified. If you use this trick in the SortSelection procedure, you can see that this variable is modified on lines 154 and 160. So now you can add a file breakpoint to both of these lines, with a conditional expression of BestValue = 12 (this being the rogue value that you found when the breakpoint on line 167 was hit). Finally, perform another selection sort using the same parameters as before. You should find that the first breakpoint to be triggered is the one on line 160 ”this looks like it could be the statement to blame.
A close look at the statement on line 160 shows you that while BestValue has a value of 12, no such value exists in the unsorted array. The closest value is 11, which appears twice in the unsorted array at positions 2 and 9. Looking at the LoopInner variable, you can see that it shows a value of 9. The item at position 9 in the unsorted array has a value of 11, not 12. So it looks as though this might be an off-by-one error: The statement on line 160 is adding 1 to the value of each item that it's sorting, which definitely doesn't seem to be correct. If you remove the + 1 from the statement on line 160 and rerun a selection sort using the same parameters, you can see that the problem appears to be fixed, in that the SortCheck procedure doesn't detect any further sorting problems.
Notice how with just two conditional breakpoints and no single-stepping through code, you were able to diagnose and fix a quite subtle bug. Of course, I've simplified the debugging process in this example because in the real world you need to review and test each bug fix to ensure that it hasn't caused any new bugs, and you also ought to look through the rest of your application for similar types of bugs.
There are some common debugging issues with Windows Forms that bite everybody at least once, so this section describes these situations and looks at ways to handle them.
One of the very first drawing issues you may notice is that your application's window will not repaint itself while you're in the debugger. For instance, you might be adding items to a list box in a loop and have a breakpoint on the ListBox.Items.Add statement in the middle of the loop in order to see where a rogue item is being added to the list box. You'll find that as soon as you hit the first breakpoint iteration and start stepping, the form won't redraw itself while you're stepping. For a VB.Classic developer used to seeing a form redraw itself after every step, this behavior is a little annoying. The best way of handling this situation is to use a conditional breakpoint so that your code only stops at the exact point where you want to debug. This avoids having to use the debugger to step through the code, and therefore the window will be drawn properly right up until the point where the breakpoint (and the bug) occurs.
A related issue concerns the debugging of window repainting code (usually in the OnPaint event) where activation of the debugger window interferes with the window painting done by your application. For example, you might put a breakpoint on one of the first statements in a form's OnPaint event. As soon as that breakpoint is hit, the debugger window activates and comes to the front of the screen, thereby overlapping or covering the window that you're debugging. After you've looked around at a few variable values, you resume execution of the program, expecting to see the next breakpoint being hit. Because resuming code execution brings your application window to the front once more, Windows quite sensibly decides to send a message to your window telling it to repaint itself. This causes the original breakpoint at the beginning of the OnPaint event to be triggered again, and you're back where you started.
There are some effective solutions to this nasty problem. If you have two monitors on your PC, your application window can sit in one monitor and the debugger can sit in the other; in this way, you can avoid window overlap problems. Alternatively, you can use remote debugging, as described in Chapter 15. With remote debugging, your application resides on one machine and displays its window on that machine's screen, while you sit at another machine that executes the debugger. The final solution that I'm going to suggest involves a neat trick using Windows Terminal Server (WTS). Try creating a WTS session from your machine back onto your machine and starting your application in the WTS window. Then you can start the debugger as normal and attach to the WTS session. Now you won't see any interference between the application and debugger windows, even though they're running on the same monitor.
When you're debugging control and window focus problems, you can use the same solutions described previously for solving issues with window painting code.
Another tricky problem can occur when you try debugging the code in a MouseMove or MouseEntry event handler. As soon as the mouse moves, your breakpoint will be hit. This isn't very useful, because normally you want to investigate only a specific sort of mouse movement (for instance, at a particular location in a window). This is a perfect situation for using a conditional breakpoint. You really want the breakpoint to trigger only when the mouse event happens at the same time as you're pressing a certain key, so that you can control the breakpoint's behavior. You might try adding the following expression to a breakpoint, which should only trigger the breakpoint when you're holding down the Shift key:
(Control.ModifierKeys And Keys.Shift) = Keys.Shift
Unfortunately, this doesn't work because the debugger is unable to evaluate the Keys.Shift expression. You'll just see a message stating that the breakpoint can't be set because the "condition is invalid." Instead, you have to use the literal key representation in the breakpoint, so the following conditional expression works just fine when you hold down the Shift key:
(Control.ModifierKeys And 65536) = 65536
Debugging MouseDown and MouseUp events can also cause problems because the MouseUp event is never triggered if you release the mouse button while paused on a breakpoint in a MouseDown event handler. Everett McKay, in his excellent book Debugging Windows Programs (Addison-Wesley, 2000), comes up with an interesting solution to this problem. He recommends keeping the mouse button pressed down while operating the debugger with the keyboard alone. Then you can release the mouse button when the application window has regained the focus.
When you're debugging KeyDown and KeyUp event handlers, the same considerations apply as discussed previously with MouseDown and MouseUp event handlers.