Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Assert Enhancements

0.00/5 (No votes)
3 Jul 2003 2  
This article describes an ready-to-use enhanced Assert dialog and its implementation. The dialog offers features such as "Always Ignore" and displays the actual Assert expression that failed.

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.

Sample image

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".

Sample image

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.

Sample image

There are a number of ways that this assert dialog box can be improved, but, for now, it serves my needs.

  1. Adding a button to "Save asserts to a file."
  2. Ignore all asserts in the method, class, file, or stack trace containing the assert.
  3. 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:

  1. call the Setup method in Main.
    SuperAsserter.Setup()
  2. 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here