Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Tools

Creating Trace Listeners in .NET


November, 2004: Creating Trace Listeners in .NET

Michael is a software developer for CGH Technologies in Washington, D.C. He also works as a contractor for the FAA and teaches college programming classes. He can be contacted at [email protected].


The Microsoft .NET Framework comes with powerful, extensible classes for debugging and tracing applications. For instance, one of the most useful debugging techniques is to log the progress of an application as it executes. This lets you identify where in your code the application is when problems occur. This technique is generally faster than running programs through the debugger. Before .NET, most C++ programmers used printf() to log messages to a console window while the application ran. When a debugger was available, TRACE() and other macros were used to send messages directly to the debugger's output window. In .NET, you can use these same techniques through trace listeners exposed by the Framework. In this article, I'll show how to create new trace listeners in .NET.

Trace Listeners

A trace listener is a class that listens for trace and debug messages from .NET applications. Most trace listeners display messages to users through a text box or by writing messages to files. Trace listeners can modify messages before displaying them, or even filter out messages if it wishes. Basically, the trace listener can do whatever it wants with the messages. It is common practice to simply log the message with possibly some formatting thrown in.

Trace listeners derive from the System.Diagnostics.TraceListener base class. This abstract base class provides the interface and base functionality that all trace listeners expose. Table 1 identifies the most important members of this class. The .NET Framework ships with three trace listeners:

  • EventLogTraceListener for logging messages to event logs.
  • DefaultTraceListener for logging messages to standard debug output such as debuggers.
  • TextWriterTraceListener for logging messages to text files.

These predefined trace listeners cover the common cases but you can also define your own.

Derived classes must implement, at a minimum, the Write() and WriteLine() methods. It is generally a good idea to also implement Close() and Flush() since these methods require interaction with the underlying output object used by the trace listener. In derived classes, only the class itself knows how its underlying output object should react to these requests.

The only real requirement for a trace listener is to provide a way for users to see generated messages. Consequently, it is easy to attach trace listeners to almost any control. It is even possible to stream the messages across networks to remote machines through HTTP, SOAP, or named pipes. Once you have written a trace listener, it is straightforward to use existing code as a basis for writing more.

Debug versus Trace

.NET includes both the Debug and Trace classes. These classes expose static methods to assist you in debugging and tracing your application. Among other things, they expose the same interface that a trace listener implements. To write a message to the trace listeners, you call the Write() or WriteLine() methods on either of these classes. These methods walk the list of registered trace listeners and call the appropriate method on each listener.

The only real difference between the Debug and Trace classes is when they are defined. The Debug class is defined only in debug builds. The Trace class, by default, is defined in both debug and release builds. Debug is designed for use in debugging your applications. The Assert() method is used to validate conditions, and the Write() and WriteLine() methods log important information about the execution. When your code is compiled for release mode, the Debug class—and all calls to it—are removed. This reduces the size of your code while increasing speed. Unfortunately, all trace messages are lost as well. This is where Trace comes in.

Trace is always defined by default. Use Trace to log messages that you may want to see in both debug and release mode. Standard practice in .NET is to define a flag in the .config file that specifies whether or not to enable logging of your application. When set to True (whether in debug or release), trace messages are generated. This makes trace messages great for determining the state of your application, even when running on a client's machine. If you compile your code from the command line, include TRACE in your preprocessor definitions. This macro is what includes the Trace class in your application.

I recommend using the Assert() method only with the Debug class. I also recommend that you limit how much you log through the Trace class since it impacts your application's performance. Use Debug for more verbose logging. In general, assertions and verbose log messages are only needed while debugging and will not provide much benefit when running on a client's machine, considering the performance hit your application takes.

Threading Issues

Debug and Trace are both threadsafe, meaning that multiple threads can call them and no synchronization is needed by the caller. Therefore, trace listeners are, by definition, threadsafe. Synchronization of internal data is not needed, but care should be taken to ensure that no thread-specific data is used.

However, there is still one multithread issue that must be taken into account. Listeners that use UI controls must ensure that any changes made to the UI control occurs on the thread owning the UI. This is a standard rule in Windows programming. Windows requires that any changes made, such as the text of a text box or the node of a tree, occur on the thread that created the UI control (the owning thread). If this rule is not followed, weird errors can occur. In one case I experienced, clicking a button on a form crashed Framework with a null reference.

Fortunately, .NET controls have been implemented to support this feature through the InvokeRequired property defined for all Control-derived classes. This property is set to True whenever a control is referenced on a thread other than the thread that created it. If this property is True, then you must use the BeginInvoke() method on the parent form to execute the code. This method ensures that the code is run on the thread responsible for the UI. Listing One demonstrates how to use this property to append text to a TextBox control.

The signature of the method that BeginInvoke() eventually calls is identical to the signature of the method that is currently executing. By matching the signatures, you can implement one method that handles both cases. The method uses an if statement to check if you are running on the UI thread using the control's InvokeRequired property. If BeginInvoke() is not required, then you simply work with the control as you normally would because you are running on the UI thread.

The interesting case comes in when you are not on the UI thread. You have to run this method (call it method A) on the UI thread. To do so, you ask the form that owns the control to run method A asynchronously using its BeginInvoke() method. This method is one of the few methods that can be called on any thread. The BeginInvoke() method runs the method A on the UI thread of the form. BeginInvoke() needs the name of the method (A) to execute and the parameters to pass to it. First, create a delegate that contains the signature of method A. Next, create an instance of the delegate and specify method A as its parameter. Then create an object array containing the same parameters that were received originally in method A. Finally pass the delegate and the object array to BeginInvoke() and it runs method A on the UI thread at some future time. Because, in this case, no clean up of any parameters is needed, the method can simply return. If some clean up was needed, then the method would have to block until the invocation was complete. When method A is eventually run on the UI thread, the InvokeRequired property is False and the method fails through to the else clause where the control's properties are set normally.

Sample Application

Before running the test application (available electronically; see "Resource Center," page 5), try changing the indentation of the messages using the up-down control to see how the listeners react. Also, try specifying the category for some messages to see how the listeners react. This gives you an idea of how the code works once we start looking at it. Since there is only one thread, you don't have to worry about whether you have to use Invoke() on the controls or not. The code is written to support this, so feel free to modify the application to create a separate thread that generates trace messages to prove that the code works correctly.

You'll need a C# compiler to compile the code. The code was developed using Visual Studio .NET 2003 with the .NET Framework 1.1, but any compiler and any version of the Framework works.

TextBoxTraceListener

The first trace listener simply sends the trace message to a text box or rich edit control. The TextBoxTraceListener (available electronically) attaches to any TextBox or RichTextBox control and sends all messages to the control. The control must be a multilined control for this to work. I also recommend that the control be read only.

To create a new class that derives from TraceListener, implement the Write() and WriteLine() methods to append the message to the end of the control's text. If this is the WriteLine() method, append a "\r\n" to move to the next line. You also need to append any indentation that may exist by prepending a string containing a number of spaces equal to the IndentLevel multiplied by the IndentSize.

TextBoxTraceListener appends indentation in front of every message it prints. If you call Write(), then the message is not appended with a new line character and the next message appears on the same line. If a category is specified for a message, then the category is inserted into the message after the indentation but before the message.

In Listing Two, all the variants of Write() and WriteLine() make the appropriate changes to the message and then pass it to a private AppendControlText() helper routine. This method does the work of appending the message to the end of the existing text. This method uses Listing One to ensure that the text is modified on the correct thread, using BeginInvoke() if needed.

The only real work left to do is to associate the control with the listener. To do this, create a new property on the listener called Owner. Set Owner to be of type TextBoxBase. Define a private field to back the property. When you later create the listener, set the Owner to the appropriate control. That's it. You now have a trace listener that can be attached to any TextBox or RichTextBox control and process messages from Debug and Trace.

The Close() method resets Owner to null. All the Write() and WriteLine() methods check for null and do nothing if the Owner is null. The Flush() method forces a refresh of the control. Because all messages are appended immediately, Flush() has no real meaning here.

Why TextBoxBase instead of TextBox? TextBoxBase is the base class for TextBox and RichTextBox. TextBox is far easier to work with than RichTextBox, but it is limited in how much it can handle. In my case, the limit was too low to be useful. RichTextBox, on the other hand, has no limit and makes a better output control for trace messages. It also supports nice formatting. By using TextBoxBase as the Owner type, you can decide which one works better for your needs. The listener works with both. See TextBoxTraceListener.cs (available electronically) for the full implementation.

TreeViewTraceListener

The second sample trace listener is similar to the first. This trace listener attaches to any TreeView control and creates nodes in the tree to represent the messages.

As with the TextBoxTraceListener, you create a new class derived from TraceListener. Implement the Write() and WriteLine() methods. Finally, create the Owner property and the underlying private field, just like in the TextBoxTraceListener except make it of type TreeView.

Each message is a node in the tree. Messages are appended to the tree. The indentation of a message determines the level of the node. An indentation of zero represents a root node. An indentation of one represents a child of the last inserted root node. An indentation of two represents a child of the last inserted child of the last root node, and so on. This requires quite a bit of work and is the most difficult part of the entire listener. All the variants of Write() and WriteLine() call the private helper routine AddNode() to actually insert the message into the tree. This helper routine calls MoveToDepth() to move to the correct location in the tree. See TreeViewTraceListener.cs (available electronically) for the details. AddNode performs the same function as AppendControlText() in TextBoxTraceListener. It hides the details of invoking the control on the right thread.

Just like TextBoxTraceListener, when you create the listener set its Owner property to the appropriate control and, once the trace listener is installed, Debug and Trace messages appear in the tree. One other feature you may want to configure is the category image. The trace listener exposes the SetCategory() method to let you associate an image with a category. The tree must already be configured to use an image list before this works. If you associate an image with a category and call a Write() or WriteLine() method and specify a category, then the trace listener sets the appropriate image for the node when it is inserted. All you need to do is call SetCategory and specify the category name and image index from the image list. The trace listener handles the rest.

The Close() method resets the Owner property to null. All the methods check the Owner and do nothing if its Owner is null. Flush() forces a refresh of the tree. As with TextBoxTraceListener, all the messages are written when they are received so Flush() has no meaning. See TreeViewTraceListener.cs for the full implementation.

Installing and Removing Listeners

You must install listeners in one of two ways before the Framework can use them. The first way is to add a line to your application's config file. Example 1 shows how to specify a new listener. This automatically creates a listener of type listenertype and installs it in the list of listeners. You can optionally specify initialization information as well. The listenername value is a name that can be used to later find the listener in the Listeners collection of Trace or Debug. Unfortunately, this method won't work for the trace listeners I describe here because these trace listeners need actual objects and none are available when the configuration file is read.

The second way to install a trace listener—the one I use in the sample application—is to create the object manually, set any properties, and add it to the list using the Trace.Listeners property. The best place to do this is in your form's OnLoad() event handler. The objects are created by then and can be safely referenced. Listing Three is used in the sample application to install both the trace listeners presented here.

Removing a trace listener is easy. Call the Remove() method on the Trace.Listeners property. Remove() takes the name of the listener as a parameter so you must set the trace listener's name using the Name property before you insert it into the list originally. Optionally, you can enumerate the collection and find the listener manually.

Enhancements

The biggest enhancement you can make is to modify the code to work with other controls. In fact, you could modify the code to accept any control by changing the Owner property to type Control, which exposes the Text property. You could modify the listener to change the Text property whenever a new message is received. This is probably not very useful, but a status bar that displays the logged messages has potential. Controls that display a lot of data (such as list boxes and views) are generally good candidates. You can also attach a listener to any output device such as a file, stream, or database. The provided classes can be easily modified to handle any specific output object you want.

Currently TextBoxTraceListener attaches the category to the beginning of the trace message. If the Owner control is a RichTextBox object, then you could instead change the color of the text. For example, you could use black for normal messages, yellow for warnings, and red for errors. I recommend that you expose a set of properties to permit users of your class to associate colors with categories similar to how the TreeViewTraceListener class exposes categories.

TextBoxTraceListener currently builds the indentation string manually. The TraceListener base class provides a protected NeedsIndent property that specifies whether indentation is actually needed. If indentation is needed, your Write() and WriteLine() methods should then call the protected WriteIndent() method to actually write the indentation. This change would make TextBoxTraceListener more compliant with the base class. It also solves the problem that currently exists when calling Write() multiple times. Each time Write() is called, the indentation string is appended again. The indentation string should only be called the first time Write() is called after a call to WriteLine(). This would simplify the string processing that the class currently does.

TreeViewTraceListener currently walks the tree manually whenever new messages are added. This is a costly operation. The class could instead store the last node. When the next message comes in, the stored node can determine where the next message is inserted. Of course, the indentation value has to be factored in.

TreeViewTraceListener currently treats calls to Write() as though they were WriteLine() by creating new nodes for every message. This method can be modified to have the message appended to the end of the last message. Write() is useful when building messages that contain a lot of custom fields, such as parameter values. Therefore, it makes sense for them to all appear in the same node of the tree.

Conclusion

The .NET Framework provides powerful ways to assist you in debugging and analyzing applications. This functionality can be extended through trace listeners to permit you to log messages to devices other than the debugger's output window. It also makes it easy for you to expose some sort of "debug window" in your application that can be enabled programmatically. Such a feature is useful when you must diagnose problems with applications on machines that don't have access to debuggers. Furthermore, with the Trace class, this functionality can be included in both debug and release builds.

DDJ



Listing One

//Define a delegate for the invoke routine - match the delegate signature to
//the actual method we want to call so we can use a simple "if" statement to
//differentiate the logic. 
delegate void AppendControlTextDelegate ( TextBoxBase box, string message );

//This method is called by both the UI and non-UI threads.
private void AppendControlText ( TextBoxBase box, string message )
{
   //If we are not running on the UI thread
   if (box.InvokeRequired)
   {
      //Create the delegate to do the work; pass it this method as a parameter
      AppendControlTextDelegate del = new AppendControlTextDelegate(
                                          AppendControlText);
      //Invoke the delegate with the parameters we received in this call
      box.FindForm().BeginInvoke(del, new object[] {box, message});
   } else
      //We are on the UI thread so we can use the control directly
      box.AppendText(message);
   }
}
Back to article


Listing Two
//Write the given object.
public override void Write ( object obj )
{           
   Write(obj.ToString());
}
//Write the specified message.
public override void Write ( string message )
{
   if (m_Owner == null)
      return;
   //Send it to the control
   AppendControlText(m_Owner, AppendIndent(message));
}
//Write the given object using the specified category.
public override void Write ( object obj, string category )
{
   Write(obj.ToString(), category);
}
//Write the given message with the specified category.
public override void Write ( string message, string category )
{
   if (m_Owner == null)
      return;
   //Format the text
   StringBuilder bldr = new StringBuilder();
   bldr.AppendFormat("{0}{1}: {2}", GetIndentString(), category, message);
   //Send it to the control
   AppendControlText(m_Owner, bldr.ToString());
}
//All these versions are the same as Write() but also append a newline.
public override void WriteLine ( object obj )
{
   WriteLine(obj.ToString());
}
public override void WriteLine ( string message )
{
   if (m_Owner == null)
      return;
   //Append the newline
   StringBuilder bldr = new StringBuilder();
   bldr.AppendFormat("{0}{1}\r\n", GetIndentString(), message);
   //Send it to the control         
   AppendControlText(m_Owner, bldr.ToString());
}
public override void WriteLine ( object obj, string category )
{
   WriteLine(obj.ToString(), category);
}
public override void WriteLine ( string message, string category )
{
   if (m_Owner == null)
      return;
   //Format the text
   StringBuilder bldr = new StringBuilder();
   bldr.AppendFormat("{0}{1}: {2}\r\n", GetIndentString(), category, message);
   //Send it to the control
   AppendControlText(m_Owner, bldr.ToString());
}
Back to article


Listing Three
//By this time the controls are all created and initialized but
//the form hasn't been displayed yet.
protected override void OnLoad(EventArgs e)
{
   base.OnLoad (e);
   //Add the textbox listener
   Trace.Listeners.Add(new TextBoxTraceListener(m_rtxtLog)); 
   //For the treeview listener associate the categories with icons
   TreeViewTraceListener lstnr = new TreeViewTraceListener(m_treeLog);
   lstnr.SetCategory("", 0);
   lstnr.SetCategory("Warning", 1);
   lstnr.SetCategory("Error", 2);
   //Now add the listener
   Trace.Listeners.Add(lstnr);         
}
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.