The .NET framework library has classes that allow you to add diagnostic messages to your code. These classes can be invaluable, but they can also provide more problems in your code. In this article, and the few that will follow it, I will explain how these classes work, how to use them, and the problems that these classes can cause.
Diagnostic messages are useful for reporting intermediate results so that you can determine whether your algorithm is working. They are also useful to indicate which methods are being executed, giving you an idea of code coverage-that is, how often, if at all, a method is executed-and you can determine whether your decision making code is working correctly. All of this is useful when you are diagnosing a fault, but this information is of no use to the end user who will use your retail build.
The diagnostics classes are contained in the system.dll assembly and naturally are part of the System::Diagnostics namespace. The main classes are described in the following table:
System::Diagnostic Class | Description |
Debug, Trace | Used to provide diagnostic messages and perform assertions. |
EventLog | Read and write messages to the NT event log. |
PerformanceCounter, PerformanceCounterCategory | Provide diagnostic information for the NT Performance Monitor. |
StackFrame, StackTrace | Provide information about the current call stack. |
Switch | Base class used to read the applications configuration file. |
TraceListener | Base class used for all listener classes that handle messages generated by the Trace and Debug classes. |
The EventLog class is great for reading the NT event log. It is lousy for writing events because it puts the responsibility of localization on the code that generates the event (do you really know who will read your messages?), which works against the design of the Win32 event log API that puts this responsibility on the code that reads the event log. If you want to generate event log messages call the Win32 function ::ReportEvent through Platform Invoke.
The PerformanceCounter and PerformanceCounterCategory classes allow you to generate performance data that can be viewed with the Performance Monitor. Anyone who has written perfmon code for Win32 will love these classes because they make the generation of this data so simple. I just wish the developer who created these classes could have used his talents on the event log generating code also, we then would not have the white elephant that is the EventLog class.
The StackTrace class allows you to get information about the current call stack. You get information about the current method, the position in that method, and even the memory location of the JIT-compiled code. There is a bug in this class that makes it show one more stack frame than actually exists, so the assert dialogs shown by Trace and Debug do not give the stack location.
Trace and Debug are classes used to generate trace messages and perform assertions (that will generate trace messages when the assertion fails). These trace messages are passed to a collection of TraceListener objects maintained by the process. These listener object decide what will happen to the message: The framework has classes that will report the message as an event log message, a message to the system debug stream, a line in a text file, or a message on a modal dialog.
By default, you will get an instance of the DefaultTraceListener class in the listeners collection, but you can change this either through code or through the application's configuration file. You can add or remove listener objects. The default listener will show failed assertions through a modal dialog (this can be configured) and trace messages will be directed through the Win32 ::OutputDebugString() function. As I will explain in an upcoming article, there is a danger associated with allowing failed assertions to be shown in retail builds (do you really want your customers to know that you suspected there was a bug in your code and didn't fix it?) and there is a hidden danger in using ::OutputDebugString().
The methods on the Debug and Trace classes are marked with the [Conditional] attribute, which means that if an appropriate compiler symbol is not defined, the compiler will ignore calls to these methods. Although this works fine for C#, it does not work with the C++ compiler. Again, I will leave a more detailed description for the next article. ::OutputDebugString() essentially couples your code to the code that reads the debug stream that can be harmful. As a consequence, Debug and Trace have WriteIf() and WriteLineIf(), which allow you to provide a runtime check to determine whether the message will be generated. One way that you can turn on diagnostics at run time is through a switch in the application's configuration file, and your code can get information about these switches with a class derived from Switch.
I have given a basic description of the main classes in the System::Diagnostics namespace, and in the next few articles, I will go into more details about how they work and point out the problems that occur when using these classes.
Richard Grimes speaks at conferences and writes extensively on .NET, COM, and COM+. He is the author of Developing Applications with Visual Studio .NET (Addison-Wesley, 2002). If you have comments about this topic, Richard can be reached at [email protected]. For questions regarding your newsletter subscription, please contact [email protected]. To subscribe, visit http://www.wd-mag.com/newsletters/.