Using the Trace and Debug Classes

Using the Trace and Debug Classes

It’s often useful to have your application generate information in the form of debugging or trace messages while it executes. Prior to the release of the .NET Framework and Visual C# .NET, creating an infrastructure for displaying this information was a difficult and time-consuming task.

With Visual C# .NET, you can take advantage of the .NET Framework’s excellent support for handling tracing and debugging messages. The .NET Framework includes classes to control trace and debug output messages and to write output messages to files, streams, and the event log. This section discusses these classes in detail.

Generating Program Trace Information

Trace and debug messages are handled by classes found in the System.Diagnostics namespace; these classes provide easy access to basic tracing functionality. For example, the following code sends a debug message to the debug console:

Debug.WriteLine("Hello Debugger");

When the Debug class is used to generate an output message, output is sent to an output device only for Visual C# .NET debug builds. The Trace class is similar to the Debug class and is used to generate output messages for all builds, as follows:

Trace.WriteLine("Hello Debugger");

When you create a new project configuration setting, define the DEBUG symbol to enable output with the Debug class and the TRACE symbol to enable output with the Trace class.

Displaying Messages with the Trace and Debug Classes

The Trace and Debug classes are composed of static methods, so you never create an instance of these classes. Both classes expose the same methods and behave in the same manner, except that they’re enabled by different conditional compilation symbols. The commonly used methods and properties from the Trace and Debug classes are listed here:

  • Assert  Generates an assertion violation message if a supplied expression is false

  • AutoFlush  Gets or sets a Boolean value that indicates whether each listener should be flushed automatically after every write operation

  • Close  Flushes the message buffer and closes all listeners

  • Fail  Generates an assertion violation message

  • Flush  Flushes the message buffer

  • Listeners  Returns the TraceListenerCollection object associated with trace and debug message output

  • Write  Generates an output message without appending a newline character

  • WriteIf  Generates an output message without appending a newline character if a supplied expression is true

  • WriteLine  Generates an output message and appends a newline character

  • WriteLineIf  Generates an output message and appends a newline character if a supplied expression is true

Several of the methods in this list display output messages; however, each of these methods is used in different scenarios. The following sections discuss these methods in more detail.

Asserting That Expressions Are True

The Assert method is used to display an error message when a condition that’s expected to evaluate as true evaluates as false. When displayed to the user, the message is clearly defined as an assertion failure. When encountered in an interactive process, such as a Microsoft Windows Forms or console application, instances of the DefaultTraceListener class display a dialog box similar to the one shown in Figure 9-3. You’ll find a detailed discussion of the DefaultTraceListener class later in this chapter, in the section “Consuming Trace Messages with Trace Listeners.”

Figure 9-3.
Dialog box generated by trace and debug output with the Assert method.

As you can see, this dialog box includes call stack information when available. Where debug symbols are available, the stack trace includes file name and line number information.

When a DefaultTraceListener object detects that the Assert method has been called from a server process, the listener doesn’t display a dialog box because there’s no interactive user to observe and dismiss the dialog box. Instead, it writes the output message to the Visual Studio Output window and any other debuggers currently accepting output from the Microsoft Win32 OutputDebugString function.

The Assert method has three versions. The most basic version simply accepts an expression that triggers an assertion failure message if the expression evaluates as false, as shown here:

Debug.Assert(ceilingHeight > 0);

The second version of Assert accepts a second parameter that serves as a short error message describing the assertion violation:

Debug.Assert(ceilingHeight > 0, "The sky is falling"); 

The third version of Assert accepts a third parameter that includes detailed information about the assertion violation:

Debug.Assert(ceilingHeight > 0,
             "The sky is falling",
             ChickenLittle.ToString());
Displaying Failure Messages

The Fail method is similar to the Assert method, except that messages are always sent to the Listeners collection. Trace and debug messages that are generated using the Fail method are displayed as assertion violations, in the same way as failed assertions generated with the Assert method.

The Fail method has two versions, as shown in the following code:

Debug.Fail("Allocation failure");
Debug.Fail("Allocation failure",
           "Could not allocate a new port for the session");

The first version of Fail accepts a description of the failure. The second version of the Fail method accepts a second parameter that provides a more detailed description of the failure.

Writing General-Purpose Output Messages

The Trace and Debug classes offer four additional methods that are used to generate output messages to the Listeners collection: Write, WriteIf, WriteLine, and WriteLineIf. Unlike the Assert and Fail methods described earlier, the methods described in this section are used to generate informational messages that don’t necessarily indicate that an error exists. The DefaultTraceListener class never uses dialog boxes to display messages that have been created using these methods.

The first two of these methods, Write and WriteIf, create messages without appending a line terminator. The WriteIf method includes a parameter that controls output. If the expression passed as a parameter evaluates as true, the trace or debug message is displayed. If the expression evaluates as false, the message isn’t generated.

The Write and WriteIf methods each have four overloaded versions. The first version generates an output message using the string passed as a parameter, as shown here:

// Display trace message.
Trace.Write("Close button clicked");
// Conditionally display trace message.
Trace.WriteIf(count > 3, "Clicked " + count.ToString() + "times."); 

The second version accepts any object as a parameter. The ToString method for the object is used to generate the output message sent to the Listeners collection:

private void HandleOverdraft(object sender, System.EventArgs e)
{
    Trace.Write(sender);
    Trace.WriteIf(sender.GetType() == typeof(BankAccount), sender);
    
    

}

In this version, the Trace.Write method unconditionally displays the string returned from the sender.ToString method, whereas the Trace.WriteIf method displays a trace message only if the sender object is an instance of BankAccount.

The third and fourth overloaded versions of Write and WriteIf are similar to the first two versions, except that they each accept an additional string parameter used to specify a category name for the message, as shown here:

private void HandleOverdraft(object sender, System.EventArgs e)
{
    Trace.Write(sender, "Overdraft");
    Trace.WriteIf(sender.GetType() == typeof(BankAccount),
                  sender,
                  "Overdraft");
    
    

}

The Trace and Debug classes also include WriteLine and WriteLineIf methods, which are comparable to Write and WriteIf, except that a line terminator (typically \r\n for text messages) is added by the listener objects that handle each message.

Controlling Output with Switches

In addition to controlling trace and debug output through symbols injected into your code during compilation, fine-grained control over output can be achieved by using classes derived from the Switch class. The interesting and useful feature shared by Switch objects is that each instance can be controlled by an application’s configuration file, and therefore by an administrator, without recompiling your code. Switch objects are used to provide Boolean values that are used in conjunction with the WriteIf and WriteLineIf methods.

Switch objects are always given a display name and description. The name of the Switch object is used to look up the value for the object in the configuration file. By default, all Switch objects are disabled, so if the runtime can’t locate configuration information about a particular Switch object, the Switch object is turned off. The following sections describe how the classes derived from Switch are used to control trace and debug output.

Using the BooleanSwitch Class

The BooleanSwitch class is used to create simple Switch objects that can be either enabled or disabled. The following code creates a BooleanSwitch object with a display name of mySwitch:

BooleanSwitch theSwitch = new BooleanSwitch("mySwitch",
                                            "Application tracing");
theSwitch.Enabled = true;

This code also enables the mySwitch object programmatically. The switch can be used to control tracing or debugging output using code such as this:

Trace.WriteLineIf(theSwitch.Enabled, "An overdraft occurred");

The BooleanSwitch.Enabled property returns either true or false, which you can use to control message output with WriteIf or WriteLineIf methods.

As an alternative to enabling the switch statement in your code, you can control the switch object in an application’s configuration file. An application’s configuration file is always placed in the application’s directory and has the same name as the application, with .config added to the configuration file name. For example, an application named SwitchTest.exe would have a configuration file named SwitchText.exe.config. The following configuration file enables the mySwitch object discussed earlier:

<configuration>
    <system.diagnostics>
        <switches>
            <add name="mySwitch" value="1" />
        </switches>
    </system.diagnostics>
</configuration>

Switches are controlled by adding XML element nodes inside the switches element. Multiple switch objects can be configured through a configuration file by adding additional elements to the switches node. An add element, as shown in this example, configures a switch object with a specific value. BooleanSwitch objects are disabled by default and are enabled if they’re assigned a nonzero value in a configuration file.

Providing Multiple Trace Levels with the TraceSwitch Class

The TraceSwitch class is used to provide multiple tracing levels instead of the simple on/off control offered by the BooleanSwitch class. The TraceSwitch class uses the TraceLevel enumeration to control the output of trace and debug messages, as described in Table 9-1.

An instance of the TraceSwitch class is constructed just like the BooleanSwitch objects described in the previous section. Tracing is enabled for a Trace­Switch object through the Level property, however, as shown here:

TraceSwitch theSwitch = new TraceSwitch("mySwitch",
                                        "Application tracing");
theSwitch.Level = TraceLevel.Verbose; 

This code enables all types of tracing through the switch. To prevent the output of any messages, set the Level property to TracingLevel.Off, as shown here:

theSwitch.Level = TraceLevel.Off; 

The TraceSwitch class exposes four trace-level properties that return Boolean values that depend on the currently defined trace level:

  • TraceError

  • TraceWarning

  • TraceInfo

  • TraceVerbose

If the trace level is enabled for a particular tracing category, the property returns true. For example, if the trace level is set to TracingLevel.Warning, the Trace­Error and TraceWarning properties will return true, whereas the TraceInfo and TraceVerbose properties will return false.

To send controlled output messages to any of the available listeners, use the WriteIf or WriteLineIf method and pass one of the trace-level properties as the controlling parameter, as shown here:

Trace.WriteLineIf(mySwitch.TraceError, "An overdraft occurred");

In this example, the mySwitch.TraceError property will return true if error-level tracing is enabled, allowing the trace message to be output. Otherwise, the Trace­Error property will return false, and the trace message won’t be sent to any of the listeners.

When using a configuration file to control a TraceSwitch object, you must provide a scalar value that corresponds to a TraceLevel using the values from Table 9-2.

The following application configuration file sets the tracing level for mySwitch to TraceLevel.Verbose:

<configuration>
    <system.diagnostics>
        <switches>
            <add name="mySwitch" value="4" />
        </switches>
    </system.diagnostics>
</configuration>

The ability to control trace output in a configuration file makes it practical to add sophisticated tracing to your application. During normal operation, tracing can be turned off, and your application can execute with very little performance cost. When tracing is required, it can be enabled from the configuration file.

Consuming Trace Messages with Trace Listeners

Visual C# .NET and the .NET Framework provide excellent support for consuming the trace output messages that are generated by the Trace and Debug classes. Your output messages are sent to TraceListener objects, which are responsible for handling output messages, as shown in Figure 9-4.

Figure 9-4.
Output sent from the Trace and Debug classes to TraceListeners.

By default, trace and debug messages are sent to an instance of the DefaultTraceListener class. This class routes output messages to the Visual Studio Output window, as well as passing the output to any applications that receive output from the Win32 OutputDebugString function, such as third-party debuggers or monitoring tools.

All trace listeners are instances of classes derived from the abstract TraceListener class. The .NET Framework includes three nonabstract listener classes:

  • DefaultTraceListener  As mentioned, sends messages to Visual Studio and applications that receive output through the OutputDebugString function

  • EventLogTraceListener  Sends messages to the Windows event log

  • TextWriterTraceListener  Sends messages to a TextWriter or Stream object

In addition to these three classes, you can derive new classes from the Trace­Listener class to provide custom trace listener functionality.

Listener objects are stored in a collection that’s shared by the Trace and Debug classes. Any listeners associated with either class are available to both classes. The following code creates a new listener attached to the console and adds the listener to the Listeners collection:

TextWriterTraceListener listener = null;
listener = new TextWriterTraceListener(System.Console.Out);
Trace.Listeners.Add(listener);

To remove all listeners from the Listeners collection, call the Clear method exposed by the collection, as shown here:

Trace.Listeners.Clear();
Sending Trace Information to TextWriters and Streams

The TextWriterTraceListener class is used to write trace information to a TextWriter or Stream object, such as a log file, network stream, or console. Text­WriterTraceListener is the most flexible of the listener classes, enabling you to send output to a wide variety of destinations. Because of this flexibility, the Text­WriterTraceListener class has seven constructors.

The default constructor for the TextWriterTraceListener class creates a listener object that uses a TextWriter instance for output. The TextWriterTrace­Listener class exposes a Writer property that’s used to access the associated TextWriter object, as shown here:

StringWriter sw = new StringWriter(new StringBuilder());
TextWriterTraceListener listener = null;
listener = new TextWriterTraceListener();
listener.Writer = sw;

In this example, an instance of StringWriter, a subclass of TextWriter, is used for output. Instead of working through the two-step process required in the example, you can pass the TextWriter object as a parameter to a second constructor:

TextWriterTraceListener listener = null; 
listener = new TextWriterTraceListener(writer);

A third constructor for TextWriterTraceListener creates a listener that sends output to any Stream instance. This enables you to send trace and debug output to a wide variety of destinations, including network streams, file streams, and the console. An earlier example (see the previous section, “Consuming Trace Messages with Trace Listeners”) demonstrated associating a TextWriterTraceListener object with the console. The code below binds a TextWriterTraceListener object to a file stream.

FileStream stream = new FileStream("TraceOutput.txt", 
                                   FileMode.Create);
TextWriterTraceListener listener = null;
listener = new TextWriterTraceListener(stream);
Trace.Listeners.Add(listener);

Although the preceding code can be generalized for any type of stream, the TextWriterTraceListener class offers a fourth constructor, which simplifies the code required to send output to a file. Instead of creating an instance of FileStream, you can simply pass the name of the file to the TextWriterTraceListener constructor, as shown here:

TextWriterTraceListener listener = null;
listener = new TextWriterTraceListener("TraceOutput.txt");

For each of these three constructors (for TextWriter, Stream, and string), an additional overloaded constructor allows you to name the listener by passing its name as a second parameter, as shown here:

listener = new TextWriterTraceListener("TraceOutput.txt", "filetrace");

Providing a name for the listener makes it easy to remove the listener from the Listeners collection later. To turn off output for the listener, call its Flush and Close methods, like this:

listener.Flush();
listener.Close();
Using the EventLogTraceListener Class

The EventLogTraceListener class is used to route trace and debug messages to the Windows event log. The event log can be located on a different machine, which makes the EventLogTraceListener class useful even on machines running an operating system that doesn’t support the event log, such as Microsoft Windows Me.

There are several ways to create and use the instances of the EventLogTraceListener class. The default constructor creates an instance of the class that isn’t associated with a specific event log. Before output can be sent to the event log, you must explicitly affiliate an EventLog object with the listener, as shown in the following code:

EventLogTraceListener logListener = null;

    

EventLog myEventLog = new EventLog(evLogName);
logListener = new EventLogTraceListener();
logListener.EventLog = myEventLog;

A second constructor for EventLogTraceListener allows you to pass an EventLog object during construction, reducing the number of steps required to create and initialize the listener, as follows:

logListener = new EventLogTraceListener(myEventLog); 

Alternatively, you can take advantage of the third constructor for EventLogTraceListener, which accepts the name of an event source as a parameter:

logListener = new EventLogTraceListener("MyEventLog"); 

When sending trace and debug output to the event log, keep in mind that the event log isn’t designed to be a boundless repository for output. Instead, it’s designed to serve as a container for essential log messages. If you use an EventLogTraceListener object, consider restricting the number of messages sent to the listener.

Creating a Custom Trace Listener

As an example of how to create a custom trace listener, the companion CD includes the XMLListener application. The XmlStreamTraceListener class contained in this application formats trace and debug messages in XML and then sends the output to any Stream object. Although classes from the .NET Framework’s XML namespace are used to provide the XML output, the interesting parts of the class can be appreciated without any knowledge of XML. (The XML classes used in this example are covered in detail in Chapter 19.)

The following code implements the XmlStreamTraceListener class:

public class XmlStreamTraceListener: TraceListener
{
    public XmlStreamTraceListener(){}

    public XmlStreamTraceListener(Stream stream, string name)
        : base(name)
    {
        Open(stream);
    }

    public override void Close()
    {
        if(_writer != null)
        {
            _writer.Close();
            _writer = null;
        }
        else
            throw new InvalidOperationException();
    }

    public void Open(Stream stream)
    {
        _writer = new XmlTextWriter(stream, Encoding.UTF8);
        _writer.Formatting = Formatting.Indented;
        WriteDocumentHeader();
    }

    public void Dispose()
    {
        Dispose(true);
    }

    protected override void Dispose(bool disposing)
    {
        if(disposing)
        {
            if(_writer != null)
            {
                _writer.Close();
            }
        }
    }

    public override void Fail(string message)
    {
        WriteFailNode(message, null);
    }

    public override void Fail(string message, string detailMessage)
    {
        WriteFailNode(message, detailMessage);
    }

    public override void Flush()
    {
        _writer.Flush();
    }

    public override void Write(object o)
    {
        WriteTraceNode(o.ToString(), null);
    }

    public override void Write(string message)
    {
        WriteTraceNode(message, null);
    }

    public override void Write(object o, string category)
    {
        WriteTraceNode(o.ToString(), category);
    }

    public override void Write(string message, string category)
    {
        WriteTraceNode(message, category);
    }

    public override void WriteLine(object o)
    {
        WriteTraceNode(o.ToString(), null);
    }

    public override void WriteLine(string message)
    {
        WriteTraceNode(message, null);
    }

    public override void WriteLine(object o, string category)
    {
        WriteTraceNode(o.ToString(), category);
    }

    public override void WriteLine(string message, string category)
    {
        WriteTraceNode(message, category);
    }

    protected void WriteFailNode(string message, string detail)
    {
        _writer.WriteStartElement("AssertionViolation", traceNamespace);
        if(_stackTrace == true)
        {
            WriteStackTrace();
        }
        if(message != null && message.Length != 0)
        {
            _writer.WriteElementString("Message", 
                traceNamespace,
                message);
        }
        if(detail != null && detail.Length != 0)
        {
            _writer.WriteElementString("DetailedMessage", 
                traceNamespace,
                detail);
        }
        _writer.WriteEndElement(); // AssertionViolation
    }

    protected void WriteTraceNode(string message, string category)
    {
        _writer.WriteStartElement("Trace", traceNamespace);
        if(_stackTrace == true)
        {
            WriteStackTrace();
        }
        _writer.WriteStartElement("Message");
        if(category != null)
        {
            string prefix = _writer.LookupPrefix(traceNamespace);
            _writer.WriteStartAttribute(prefix,
                                        "Category",
                                        traceNamespace);
            _writer.WriteString(category);
            _writer.WriteEndAttribute();     
        }
        _writer.WriteString(message);
        _writer.WriteEndElement(); // Message
        _writer.WriteEndElement(); // Trace
    }

    protected void WriteStackTrace()
    {
        _writer.WriteStartElement("Stack", traceNamespace);
        _writer.WriteStartElement("StackFrames", traceNamespace);

        StackTrace trace = new StackTrace(true);
        int frameCount = trace.FrameCount;
        for(int n = 0; n < frameCount; ++n)
        {
            StackFrame frame = trace.GetFrame(n);
            MethodBase methodBase = frame.GetMethod();

            // Don't display ourselves or internal tracing
            // methods in the call stack.
            if(methodBase.ReflectedType == this.GetType() ¦¦
                methodBase.ReflectedType == typeof(Trace)  ¦¦
                methodBase.ReflectedType.Name == "TraceInternal")
                continue;
            
            // If part of the call stack lacks debug symbols, we
            // won't have valid data for these values.
            string fileName = frame.GetFileName();
            if(fileName == null)
                fileName = "Not available";
            string lineNo = frame.GetFileLineNumber().ToString();
            string methodName = methodBase.Name;
            if(methodName == null)
                methodName = "Not available";

            _writer.WriteStartElement("StackFrame", traceNamespace);
            _writer.WriteElementString("FileName",
                traceNamespace,
                fileName);
            _writer.WriteElementString("LineNumber", 
                traceNamespace,
                lineNo);
            _writer.WriteElementString("MethodName",
                traceNamespace,
                methodName);
            _writer.WriteEndElement(); // StackFrame
        }
        _writer.WriteEndElement(); // StackFrames
        _writer.WriteEndElement(); // Stack
    }

    protected void WriteDocumentHeader()
    {
        _writer.WriteStartDocument(true);
        _writer.WriteStartElement(tracePrefix,
                                  "TraceInformation",
                                  traceNamespace);
        _writer.WriteStartElement("Traces",
                                  traceNamespace);
    }

    protected void CloseDocument()
    {
        _writer.WriteEndDocument();
        _writer.Close();
    }

    bool StackTrace
    {
        set { _stackTrace = value; }
        get { return _stackTrace; }
    }

    protected bool _stackTrace = true;
    protected XmlTextWriter _writer = null;

    private const string traceNamespace = "urn:csharpcoreref";
    private const string tracePrefix    = "cscr";
}

The XmlStreamTraceListener class exposes a Boolean property named StackTrace that specifies whether a stack trace is to be included with messages. The default value for StackTrace is true, and it can be changed at any time during execution.

In addition to a protected member variable that tracks the current state of the StackTrace property, the listener has an instance of XmlTextWriter named _writer that’s used to generate the XML document. The Open method is used to create and initialize _writer and is called during construction if the user passes a Stream object as a constructor parameter. Alternatively, the default constructor can be used to create a listener instance, followed by a call to the Open method.

Much of the code in XmlStreamTraceListener overrides methods declared in the TraceListener base class. Methods such as Write, WriteLine, Fail, Close, and Flush are called by the Trace and Debug classes to generate output messages or otherwise manage message output. The Write and WriteLine methods are implemented in the same way—they simply call WriteTraceNode. This listener doesn’t inject any sort of line terminator for messages, because all messages are serialized as an XML document. The Close and Flush methods call the appropriate methods on the XmlTextWriter object associated with the listener.

The implementations of WriteTraceNode and WriteFailNode have a small bit of code that’s used to provide proper XML formatting for each new trace node in the XML document. If the StackTrace property is true, these methods call WriteStackTrace to inject stack trace information into the document. Otherwise, the message is formatted and serialized to the XML document.

The XmlStreamTraceListener class is used much like any other listener. The following code creates an instance of the listener, adds it to the Listeners collection, generates trace messages, and flushes and closes the listener:

using MSPress.CSharpCoreRef.XmlListener;

    

class TestXmlListenerApp
{
    static void Main(string[] args)
    {
        FileStream stream = new FileStream("TraceInfo.xml",
                                           FileMode.Create);
        XmlStreamTraceListener xstl = new 
            XmlStreamTraceListener(stream,
                                   "XML application tracing");
        Trace.Listeners.Add(xstl); 

        GenerateAssertion();
        
        Trace.Flush();
        Trace.Close();
    }

    static void GenerateAssertion()
    {
        Trace.Assert(false);
    }
}

When you run this code, an XML document named TraceInfo.xml that contains an assertion violation will be generated. Included as part of the assertion violation is a stack trace that consists of two frames. XmlStreamTraceListener is just one example of how the TraceListener class can be extended to create new classes that handle trace and debug messages.



Part III: Programming Windows Forms