Introduction
The framework includes an entire namespace and a set of classes for
debugging. In many ways it provides a lot of support for debugging, but it is
still lacking in the basics.
This article focuses on the Assert dialog, probably the most important
feature provided by the debug classes. Unfortunately, the assert dialog is very
weak. In this article, I will discuss how to improve the Assert dialog by taking
advantage of some of the customization features offered by the framework.
In addition, I have included an enhanced ready-to-use Assert dialog box that
you can use immediately in your programs to improve your debugging
experience.
The Standard Assert Dialog
I will begin by discussing the default dialog box and then follow that
discussion with my enhanced replacement.
The default assert dialog box looks like the following.
It displays any text from an Debug.Assert(expr, message)
or
Debug.Fail(message)
console. Unfortunately, if you do not specify a
message in the Debug.Assert(expr)
statement, the debugger has no
message and merely reports just the filename and line number, so it is not
always clear what was triggered.
The default assert also provides the oddly named "abort," "retry" and
"ignore" buttons. Retry invokes the debugger, abort exits the application and
ignore continues execution from the point of the assert.
Even though, the framework provides a number of debugging features, it falls
short of the debugging experience that I have had with C++, pre .NET. For one
thing, I could be in a loop in which the assert continually fires ad infinitum,
and have no way to bypass this specific assert.
Secondly, I have to prepare a message for each assert that I write, or I will
not have a descriptive message to view when the assert displays, but instead of
line number, which is meaningless to me until I go into the debugger. Because
about a large percentage of the lines of code I write consists of asserts, this
quickly becomes very cumbersome; I also wonder if the strings themselves will be
carried over into my release application, thereby bloating the size of my
application or making it easier for someone to deobfuscate my code.
Another pet peeve is the varying size of the dialog box, if the file path is
too long, I have a wide dialog; if I am deep into a recursive function, I get a
tall dialog. In either case, it obscures what is underneath and causes repaint
events to be emitted.
SuperAsserter
Now let's look at my enhanced assert dialog box. This is my
SuperAsserter
dialog with a custom assert message "Testing".
Notice there are four buttons below. "Retry" is appropriately renamed
"Debug," and we have a new button called "Ignore Always," which forces the
assert dialog box to remain hidden upon each invocation of any troublesome
assert.
The window always remains the same size, regardless of the size of the stack
trace, but it can be resized to suit the developers needs. The stack trace is
kept in a scrolling textbox, whose contents can be copied, if need be. The label
above the textbox indicates the most important information about the assert in
large bolded text, the problem method, file, line, number and any custom
message.
When a Debug.Assert
does not contain a custom message, a line is
read in directly from the source file and line number, if found; and the text of
the line is cached in memory for subsequent invocations of the same assert. The
line of text is used in placed of the empty custom message. The following dialog
box is then produced.
There are a number of ways that this assert dialog box can be improved, but,
for now, it serves my needs.
- Adding a button to "Save asserts to a file."
- Ignore all asserts in the method, class, file, or stack trace containing the
assert.
- Reporting other information such as the Win32 last error message and son on.
I had also planned to produce another SuperCatcher
dialog box
that can be displayed when an exception is fired. In this case, the dialog box
would show all inner exceptions and display the line of code that trigger each
exception as well as the message, type and properties of each exception.
SuperAsserter
can be used in both console and windows
applications. With console applications, it is necessary to include references
to System.Windows.Forms
and System.Drawing
dlls
because the dialog box is a WinForm
.
To setup SuperAsserter
do either of the follow:
- call the
Setup
method in Main
. SuperAsserter.Setup()
- add the
SuperAsserter
singleton class instance to either
Debug.Listeners or Trace.Listeners Debug.Listeners.Remove("Default");
Debug.Listeners.Add(Instance);
Default is the name of the default listener. If you don't remove it, you will
get two dialogs per assert--the new and the old. You can also remove all
listeners by calling Debug.Listeners.Clear
().
Debug and Tracing Basics
The primary location of debugging support in .NET is through
System.Diagnostics
namespace.
The first two classes you should know are System.Debug
and
System.Trace
, which are virtually identical, except for the fact
that method calls of System.Debug
are omitted in the release
builds, whereas they are both present for System.Trace
.
More precisely, the System.Debug
class has an attribute
[ConditionalAttribute("DEBUG")]
attached. This is another feature
supported by the compiler to aid debugging. Any method with has a
Conditional
attribute applied will not be called when the
preprocessor symbol has not been defined. Defines can set through the C#
compiler switch for defines (example, in /d:DEBUG or /d:TRACE
), or
project options. You can also declare defines in the top of a file--but it has
to be written before any code.
Of course, the preprocessors #if DEBUG
and #endif
can also be used to omit debug. The conditional attribute was absolutely
necessary for C# and VB.NET because those languages have limited preprocessor
support, unlike C++. There would be no other way for Assert()
calls
to be eliminated in the release step, without bracketing the line of code with
#if's
. Note that the CLR does not require a language to honor the
conditional attribute.
System.Debug
and System.Trace
have several
functions.
Fail(message) |
By default, produces a dialog box indicating the file and line and stack
trace with the abort, retry and fail options. |
Assert(expression, message) |
Calls Fail if expression is false. |
Assert(expression) |
Calls Fail if expression is false, with message being the empty
string. |
WriteLine(string) |
By default, emits the string to the Output window in the debugger.
Internally, this calls the Win32 API function
OutputDebugString . |
WriteLineIf(string, expression) |
Calls writeline if the condition is true. Similar to, if
(expression) WriteLine(string) |
The descriptions above indicate the behavior of the
DefaultTraceListener
that is attached to the Listeners
collection of both the Debug
and Trace
class. There
are other types of listeners that could be attached such as a
TraceListener
that produces event log output and another that
writes out to a stream.
There are two other provided TraceListeners, TextWriterTraceListener
and EventLogTraceListener
. To get a listener that writes out
to stdout or stderr, use new TextWriterTraceList(Console.Out or
Console.Error).
The default TraceListener
, DefaultTraceListener
,
can be replaced by another listener by the following approaches.
To remove the default TraceListener
, call
Debug.Listeners.Remove("Default").
To remove all existing listener, call
Debug.Listeners.Clear();
To add a new listener, call
Debug.Listeners.Add( new TextWriterTraceListener(Console.Error) );
The DefaultTraceListener
invokes a message box when
Debug.Assert or Debug.Fail
is called, and the
WriteLine
commands outputs to the Debugger Output window. This
includes the Trace
class, calling the Trace.Assert()
command may produce a message box in the shipped product.
Custom TraceListeners
To create SuperAsserter
, I constructed a new class derived from
DefaultTraceListener
.
I then overrode the void Fail(string message, string
detailMessage)
method. This method is automatically called by
Assert
, if the expression passed to it is false.
Fail
calls an Windows Form class called AssertBox
,
that implements the actual dialog.
Control is returned to Fail
when the dialog is closed. If the
user chose "Debug", Fail
calls Debugger.Break
. It was
necessary to add the attribute DebuggerHiddenAttribute
applied to
the Fail
method, because not doing so will cause the debugger to
break inside the Fail method instead of the code that we are interested in--the
method that invoked Assert
in the first place.
Conclusion
This represents just one of my articles in the debugging series. There will
be others. I'd appreciate your vote; it is a powerful motivating force for
me.
Version History
Version |
Description |
June 28, 2003 |
Original article. |