The Watch Window


If I had to give an Oscar for technical achievement and overall usefulness in Visual Studio .NET, the Watch window would win hands down. One idea that companies creating development tools for other environments and operating systems haven't figured out at all is that if developers can easily develop and debug their applications, they'll more likely flock to that environment or platform. The incredible power offered by the Watch window and its related cousins, the QuickWatch dialog box, Autos window, Locals window, and This/Me window, are what make the difference between floundering all day looking for an answer and quickly solving a bug.

I want to make sure to point out that you can use any of the related Watch windows to change a variable's value. Unfortunately, many developers coming over from other environments, and a few who have been doing Windows development for many years, aren't aware of the capabilities. Let's take the Autos window as an example. You just select the variable or the child variable you want to change, and click once on the value field for that variable. Simply type in the new value and you've changed the variable.

Many developers treat the Watch window as a read-only place in which they drop their variables and watch. What makes the Watch window exciting is that it has a complete expression evaluator built in. If you want to see something as an integer that's not an integer, simply cast or convert it in the same way you would if you were programming in the currently active programming language. Here's a simple example: Suppose CurrVal is declared as an integer and you want to see what it evaluates to as a Boolean. In the Name column of the Watch window, in C# and C++, enter (bool)CurrVal, or for Visual Basic .NET, enter CBool(CurrVal). The value is displayed as true or false as appropriate.

Changing an integer to a Boolean might not be that exciting, but the ability of the Watch window to evaluate expressions gives you the ultimate code-testing trick. As I've said several times in this book, code coverage is one of the goals you need to strive for when doing your unit testing. If, for example, you have a conditional expression inside a function and it's difficult to see at a glance what it evaluates to so that you can step into the true or false branch as appropriate, the Watch window becomes your savior. Since there's a full expression evaluator built in, you can simply drag the expression in question down to the Watch window and see what it evaluates to. Granted, there are some restrictions. If your expression calls all sorts of functions instead of using variables, you could be in trouble. If you look at my code, I follow the rule in which if I have three or more subexpressions in a conditional, I use only variables just so that I can see the result of the expression in the Watch window. Some of you might be truth table savants and be able to see how those expressions evaluate off the top of your heads, but I certainly am not one.

To make this clearer, let's use the next expression as an example. You can highlight everything inside the outer parentheses and drag it to the Watch window. However, since it's on three lines, the Watch window interprets it as three separate lines in its display. I still do that so that I can copy and paste the two lines into the first one and thus build my final expression without too much typing. Once the expression is entered on one line in the Watch window, the value column is either true or false, depending on the values in the variables.

if ( ( eRunning == m_eState    ) ||      ( eException == m_eState  ) &&      ( TRUE == m_bSeenLoaderBP )   )

The next step is to put each of the variables that make up a part of the expression in their own entries in the Watch window. The really cool part is that you can start changing the values of the individual variables and see that the full expression on the first line automatically changes based on the changing subexpressions. I absolutely love this feature because it not only helps with code coverage, but it also helps you see the data coverage you need to generate.

Calling Methods in the Watch Window

Something I've found relatively amusing about some developers who have moved to Windows development from those UNIX operating systems is that they insist UNIX is better. When I ask why, they indignantly respond in their suspender-wearing glory, "In GDB you can call a function in the debuggee from the debugger!" I was amazed to learn that operating system evaluations revolved around an arcane debugger feature. Of course, those suspenders snap pretty quickly when I tell them that we've been able to call functions from Microsoft debuggers for years. You might wonder what's so desirable about that. If you think like a debugging guru, however, you'll realize that being able to execute a function within the debugger allows you to fully customize your debugging environment. For example, instead of spending 10 minutes looking at 10 different data structures to ensure data coherency, you can write a function that verifies the data and then call it when you need it most—when your application is stopped in the debugger.

Let me give you two examples of places I've written methods that I called only from the Watch window. The first example is when I had a data structure that was expandable in the Watch window, but to see all of it I would have been expanding the little pluses all the way past the Canadian border and up into the North Pole area. By having the debugger-only method, I could more easily see the complete data structure. The second example was when I inherited some code that (don't laugh) had nodes that were shared between a linked list and a binary tree. The code was fragile, and I had to be doubly sure I didn't screw up anything. By having the debugger-only method, I was in essence able to have an assertion function I could use at will.

What's interesting in managed applications is that any time you view an object property in the Watch window, the getter accessor is called. You can easily verify this by putting the following property in a class, starting to debug, switching to the This/Me window, and expanding the this/Me value for the object. You'll see that the name returned is the name of the AppDomain the property is part of.

Public ReadOnly Property WhereAmICalled() As String     Get         Return AppDomain.CurrentDomain.FriendlyName     End Get End Property 

Of course, if you have an object property that copies a 3-gigabyte database, the automatic property evaluation could be a problem. Fortunately, Visual Studio .NET allows you to turn off the property evaluation in the Options dialog box, Debugging folder, General property page, Allow Property Evaluation In Variables Windows check box. What's even better is that you can turn this property evaluation on and off on the fly and the debugger immediately responds to the change. You can instantly tell when property evaluation is disabled because the Watch window family says this in the Value field: "Function evaluation is disabled in debugger windows. Check your settings in Tools.Options.Debugging.General."

Native code is a bit different in that you have to tell the Watch window to call the method. Please note that I'm using the generic term "method" here. In actuality, for native code, you can reliably call only C functions or static C++ methods. As regular native C++ methods need the this pointer, which you might or might not have depending on the context. Managed methods are a little more this pointer friendly. If you're stopped in the class that contains the method to call, the Watch window automatically assumes the active this pointer is the one to use.

As you've seen, calling managed code properties is trivial. Calling methods in managed or native code is just like calling them from your code. If the method doesn't take any parameters, simply type the method name and add the open and close parameters. For example, if your debugging method is MyDataCheck ( ), you'd call it in the Watch window with MyDataCheck ( ). If your debugging method takes parameters, just pass them as if you're calling the function normally. If your debugging method returns a value, the Value column in the Watch window displays the return value.

A common problem when calling native functions from the Watch window is ensuring that they are valid. To check a function, type the function name into the Watch window and don't add any quotes or parameters. The Watch window will report the type and address of the function in the Value column if it can be found. Additionally, if you'd like to specify advanced breakpoint syntax (which I'll discuss in detail in Chapter 7) to the function, to narrow down scope, you can do that as well.

A few rules apply to both managed and native code when calling methods from the Watch window. The first rule is that the method should execute in less than 20 seconds because the debugger UI stops responding until the method finishes. After 20 seconds, managed code shows "Evaluation of expression or statement stopped," "error: function '<method name>' evaluation timed out," or "Error: cannot obtain value," and native code shows "CXX001: Error: error attempting to execute user function." The good news is that your threads will continue to execute. That's wonderful news because for calling native methods in Visual Studio 6 that timed out, the debugger just killed the currently executing thread. The other good news compared to previous editions of Visual Studio is that if you want to, you can leave called methods in the Watch window in multithreaded programs. Previous versions killed any thread that happened to become active in the place you originally executed your debug method in another thread. The final rule is common sense: only read memory to do data validations. If you think debugging a program that changes behavior because of a side effect in an assertion is tough, wait until you mess with something on the fly in the Watch window. Additionally, if you do some sort of output, make sure to stick with just the trace method of choice for the environment.

What I've covered here about the Watch window is what .NET and native debugging have in common. You can also expand your own types automatically in the Watch window, but there's quite a difference between how it's done in managed vs. native code. Additionally, native debugging offers all sorts of other options for data formatting and control. To learn more, make sure to read in Chapter 6 and Chapter 7 the individual discussions of the Watch window.

The Set Next Statement Command

One of the coolest hidden features in the Visual Studio .NET debugger is the Set Next Statement command. It is accessible in both source windows and the Disassembly window on the shortcut menu, but only when you're debugging. What the Set Next Statement command lets you do is change the instruction pointer to a different place in the program. Changing what the program executes is a fantastic debugging technique when you're trying to track down a bug or when you're unit testing and want to test your error handlers.

A perfect example of when to use Set Next Statement is manually filling a data structure. You single-step over the method that does the data insertion, change any values passed to the function, and use Set Next Statement to force the execution of that call again. Thus, you fill the data structure by changing the execution code.

I guess I should mention that changing the instruction pointer can easily crash your program if you're not extremely careful. If you're running in a debug build, you can use Set Next Statement without much trouble in the source windows. For native optimized builds in particular, your safest bet is to use Set Next Statement only in the Disassembly window. The compiler will move code around so that source lines might not execute linearly. In addition, you need to be aware if your code is creating temporary variables on the stack when you use Set Next Statement. In Chapter 7, I'll cover this last situation in more detail.

If I'm looking for a bug and my hypothesis is that said bug might be in a certain code path, I set a breakpoint in the debugger before the offending function or functions. I check the data and the parameters going into the functions, and I single-step over the functions. If the problem isn't duplicated, I use the Set Next Statement command to set the execution point back to the breakpoint and change the data going into the functions. This tactic allows me to test several hypotheses in one debugging session, thus saving time in the end. As you can imagine, you can't use this technique in all cases because once you execute some code in your program, executing it again can destroy the state. Set Next Statement works best on code that doesn't change the state too much.

As I mentioned earlier, the Set Next Statement command comes in handy during unit testing. For example, Set Next Statement is useful when you want to test error handlers. Say that you have an if statement and you want to test what happens when the condition fails. All you need to do is let the condition execute and use Set Next Statement to move the execution point down to the failure case. In addition to Set Next Statement, the Run To Cursor menu option, also available on the right-click shortcut menu in a source code window when debugging, allows you to set a one-shot breakpoint. I also use Run To Cursor quite a bit in testing.

Filling data structures, especially lists and arrays, is another excellent use of Set Next Statement when you're testing or debugging. If you have some code that fills a data structure and adds the data structure to a linked list, you can use Set Next Statement to add some additional items to the linked list so that you can see how your code handles those cases. This use of Set Next Statement is especially handy when you need to set up hard-to-duplicate data conditions when you're debugging.

start sidebar
Common Debugging Question: Can Visual Studio .NET also debug regular ASP Web applications?

It sure can, but not out of the box because you have to add some registry keys and set up DCOM permissions on the Web server. It's hard to find the steps because they're buried way down deep in the documentation. Search for the topic "ASP Remote Debugging Setup" to see the necessary steps.

end sidebar




Debugging Applications for Microsoft. NET and Microsoft Windows
Debugging Applications for MicrosoftВ® .NET and Microsoft WindowsВ® (Pro-Developer)
ISBN: 0735615365
EAN: 2147483647
Year: 2003
Pages: 177
Authors: John Robbins

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net