Implementing a Custom TraceListener

A mainstay of software engineering is a concept called tracing. The first time I encountered the trace capability was in the C programming language, implemented as a macro. The basic idea is that you insert statements into your code that provide information about your application while it is running. This is referred to as tracing. Historically tracing was something that occurred while debugging and testing. As you might imagine writing trace information to a console was especially useful before the integrated debugger. (The first integrated debugger I used was Borland's Turbo Pascal.) By using a trace mechanism you could run your program normally and view the trace information concurrently. Programmers devised some very clever ways to send trace output to separate views, second monitors, or text files. For Visual Basic 6 programmers think of tracing as the Debug.Print capability.

Often times the oldest and simplest techniques ripen because they are useful. Tracing has tremendous utility and as a result is an integral part of Visual Basic .NET. Debug.Print has been replaced with Debug.Write (or WriteLine) and Trace.Write (and WriteLine) have been added to the System.Diagnostics namespace. Debug and Trace work the same way. The biggest difference is that each is controlled by a different pragma. When the DEBUG constant is defined then Debug.Write and Debug.WriteLine are enabled. When the TRACE pragma is defined then Trace.Write and Trace.WriteLine statements are enabled. Both DEBUG and TRACE are defined when a solution is configured for Debug builds, but only Trace is defined when your solution is configured for Release builds. In practice you can use Debug to provide you with information when you are unit testing and Trace to provide information after you deploy your application.

There is substantial support for debugging and tracing built into .NET. By default Trace.Write and Trace.WriteLine statements are sent to the Output window in the IDE. The Output window is the default trace listener. In fact, there is a class named DefaultTraceListener that inherits from the TraceListener class.

Tip: The TraceListener is based on the Observable pattern.

Microsoft also realizes that you may likely want to send trace information somewhere besides the Output window. To this end other TraceListeners exist, including the TextWriterTraceListener for tracing to text files and the EventLogTraceListener for tracing to the EventLog. Since these TraceListeners are built on top of a coherent framework—the .NET framework—and you applications are built on top of the same framework, you have the option of implementing custom TraceListener classes.

I provided some basic background information, but if you need more fundamental help on tracing then pick up a copy of my Sams Visual Basic .NET Unleashed from Sams Publishing. In this article we will take a step forward, and I will show you how to implement a custom TraceListener. (In the process of demonstrating a custom TraceListener there is some example code that demonstrates how to make the listener begin listening and a few examples of tracing.) For a second example of a listener download the shared source code library named Rotor and explore the source for the DefaultTraceListener in sscli\fx\src\compmod\system\diagnostics\defaulttracelistener.cs. Rotor is written in C#, which is very similar to the C programming language.

Implementing a Custom TraceListener

To create a custom TraceListener we need to rely on class inheritance supported in VB .NET. (VB 6 only supported interface inheritance.) TraceListener is defined in the System.Diagnostics namespace. Thus we will need to import the System.Diagnostics namespace, inherit from the TraceListener class, and implement new behaviors for virtual methods defined in TraceListener. We can look at the Rotor source code for the TraceListener class or the Visual Studio .NET help to gain insight on necessary steps for implementing a custom TraceListener. (Excerpts from the TraceListener are provided in listing 1 followed by synopsis of the code.)

Listing 1: Excerpts from TraceListener, provided as part of the shared source code library.

1:  namespace System.Diagnostics {
2:      using System;
3:      using System.Text;
4:  
5:      public abstract class TraceListener : MarshalByRefObject,
                             IDisposable {
6:  
7:          protected TraceListener () {
8:          }
9:  
10:         protected TraceListener(string name) {
11:             this.listenerName = name;
12:         }
13: 
14:         public virtual string Name {
15:             get { 
            return (listenerName == null) ? "" : listenerName; }
16: 
17:             set { listenerName = value; }
18:         }
19: 
20:         protected virtual void Dispose(bool disposing) {
21:             return;
22:         }
23: 
24: 
25:         public virtual void Close() {
26:             return;
27:         }
28: 
29:         public virtual void Flush() {
30:             return;
31:         }
32: 
33: 
34:         public virtual void Fail(string message) {
35:             Fail(message, null);
36:         }
37: 
38:         public virtual void Fail(string message, string 
                                     detailMessage) {
39:         StringBuilder failMessage = new StringBuilder();
40:         failMessage.Append(SR.GetString(SR.TraceListenerFail));
41:         failMessage.Append(" ");
42:         failMessage.Append(message);
43:         if (detailMessage != null) {
44:                 failMessage.Append(" ");
45:                 failMessage.Append(detailMessage); 
46:             }
47: 
48:             WriteLine(failMessage.ToString());
49:         }
50: 
51:         public abstract void Write(string message);
52: 
53:         public virtual void Write(object o) {
54:             if (o == null) return;
55:             Write(o.ToString());
56:         }
57: 
58:         public virtual void Write(string message, 
                                      string category) {
59:             if (category == null)
60:                 Write(message);
61:             else
62:                 Write(category + ": " + ((message == null) ?
                                    string.Empty : message));
63:         }
64: 
65:         public virtual void Write(object o, string category) {
66:             if (category == null)
67:                 Write(o);
68:             else
69:                 Write(o == null ? "" : o.ToString(), category);
70:         }
71: 
72:         protected virtual void WriteIndent() {
73:             NeedIndent = false;
74:             for (int i = 0; i < indentLevel; i++) {
75:                 if (indentSize == 4)
76:                     Write("    ");
77:                 else {
78:                     for (int j = 0; j < indentSize; j++) {
79:                         Write(" ");
80:                     }
81:                 }
82:            }
83:         }
84: 
85:         public abstract void WriteLine(string message);
86: 
87:         public virtual void WriteLine(object o) {
88:             WriteLine(o == null ? "" : o.ToString());
89:         }
90: 
91:         public virtual void WriteLine(string message, 
                                          string category) {
92:             if (category == null)
93:                 WriteLine(message);
94:             else
95:                 WriteLine(category + ": " + ((message == null) ?
                                   string.Empty : message));
96:         }
97: 
98:         public virtual void WriteLine(object o, 
                                          string category) {
99:             WriteLine(o == null ? "" : o.ToString(), category);
100:         }
101:     }
102: }

Note: Listing 1 is an incomplete listing. The code in listing 1 is owned by Microsoft. For a complete listing download Rotor and explore the file \sscli\fx\src\compmod\system\diagnostics\tracelistener.cs.

The first important piece of information is that the class is marked as abstract (see line 5). Abstract is a new concept for many Visual Basic 6 programmers. The modifier abstract equates to MustInherit in Visual Basic .NET, and it means that instances of TraceListener cannot be created. Abstract classes—referred to as MustInherit classes—are incomplete classes. The second piece of information is that both constructors are protected (refer to lines 7 and 10). Protected or private constructors are used when objects may only be created by a factory method—a shared method—or the class will always be inherited, as is the case with TraceListener.

Next, you will note that any member that uses the virtual modifier—Overridable in Visual Basic .NET—can be overridden in any subclass, but you are not required to do so. Examples of virtual members include the Name property and the Close method. Finally members marked as abstract—MustOverride in Visual Basic .NET—are incomplete methods. There are two such methods—Write and WriteLine on lines 51 and 85, respectively—in listing 1. You must provide implementations for abstract methods.

Thus, from our overview we know that to create a custom TraceListener we must inherit from TraceListener and provide an implementation for Write and WriteLine. Returning to lines 51 and 85 we can see that Write and WriteLine both return a type void and accept one string argument. When you see void in C# this equates to a subroutine in Visual Basic .NET. A basic do-nothing TraceListener framing the essential ingredients is provided in listing 2.

Listing 2: A bare bones custom TraceListener.

Public Class MyListener
  Inherits System.Diagnostics.TraceListener

  Public Overloads Overrides Sub Write(ByVal Message As String)

  End Sub

  Public Overloads Overrides Sub WriteLine(ByVal Message As String)

  End Sub

End Class

Note: The basic code in listing 2 would make a good project template. Refer to the September codeguru article Technique Sharing with project Templates for more information on project templates.

Listing 2 meets all of the requirements of the minimum requirements of a custom TraceListener. The stubs for Write and WriteLine demonstrate how to implement abstract methods, and the statement beginning with the keyword Inherits demonstrates the syntax for inheritance in VB.NET. (It is interesting to note that the framework itself is implemented in C#, clearly demonstrating multi-language capabilities in .NET.)

Implementing Required Methods

One extension to tracing that I played with was to use it for Web pages. Because a Web page is rendered and disconnected from the server after it is rendered, I played with the idea of storing the trace messages in a Queue. A System.Collections.Queue is a convenient way to store trace information in the order that it arrives. If we include a convenient way for a client to access this trace information then we can queue it up and display it when it is convenient to do so. In an ASP.NET Web application it is only convenient to display the content at the time the page is rendered. Caching the data in the queue works reasonably well toward this end.

Note: HttpContext, Page, and UserControl objects have a Trace property that sends trace information to a Web page. In a Web application you may need to distinguish between the control's Trace and the System.Diagnostics.Trace class by using the qualified name of the Trace class. Information sent via the context, page or user control is written directly to the page if tracing is enabled. Unfortunately using the built-in ASP.NET Trace property dumps the text directly to the page, which will screw up your presentation. This approach is useful for debugging but not in deployment. The technique described in this section of the article can be practically used in a deployed application.

I have included the implementation of the TraceListener with the queue but not the Web application. If you are interested in using this approach for tracing in a Web application I have experimented with it by using the Session cache to store the listener. In this manner I am able to get at Trace messages that are only relevant on a per-user basis. This idea is still a conceptual approach for Web applications but looks promising. Here is the code (listing 3) for the custom TraceListener.

Listing 3: A custom TraceListener that stores trace messages in a queue for later access.

1:  Imports System.Collections
2:  Imports System.Text
3:  
4:  
5:  Public Class MyListener
6:    Inherits System.Diagnostics.TraceListener
7:  
8:    Private q As Queue
9:  
10:   Public Sub New()
11:     q = New Queue()
12:   End Sub
13: 
14:   Public Overloads Overrides Sub Write( _
15:     ByVal Message As String)
16: 
17:     q.Enqueue(Message)
18: 
19:   End Sub
20: 
21:   Public Overloads Overrides Sub WriteLine( _
22:     ByVal Message As String)
23: 
24:     q.Enqueue(Message + Environment.NewLine)
25:   End Sub
26: 
27:   Public ReadOnly Property Text() As String
28:   Get
29:     Return GetText()
30:   End Get
31:   End Property
32: 
33:   Private Function GetText() As String
34:     Dim S As StringBuilder = New StringBuilder()
35:     Dim E As IEnumerator = q.GetEnumerator
36:     While (E.MoveNext)
37:       S.Append(CType(E.Current, String))
38:     End While
39: 
40:     q.Clear()
41:     Return S.ToString()
42: 
43:   End Function
44: 
45: End Class

Listing 3 was generated from the stub in listing 2. The Write and WriteLine methods stuff the trace information into the Queue created in the listener's constructor. The data from the Queue can be retrieved at any time accessing the public, read-only, Text property. (Each time the Text property is read the queue is flushed in the example.

The Text property is supported by a private GetText method that uses a StringBuilder and IEnumerator to concatenate all of the Trace messages into a single string. Lines 33 through 43 demonstrate how to use a StringBuilder and IEnumerator.

Testing Your TraceListener

To test our custom TraceListener we can use a simple Windows application project. We can send data to it implicitly with the Trace class. However, we will need to keep track of the Listener to read from the Text property at a later time.

To simulate the asynchronous behavior of a Web application I created a Windows application that employs a Timer to read from the listener. Every time the Timer expires our listener is queried for Trace messages. Listing 4 shows the code—minus the auto-generated code—that tests the custom TraceListener.

Listing 4: Testing the custom TraceListener.

1:  Public Class Form1
2:      Inherits System.Windows.Forms.Form
3:  
4:  [ Windows Form Designer generated code ]
5:  
6:    Private Listener As MyListener = New MyListener()
7:    Private Sub Button1_Click(ByVal sender As System.Object, _
8:      ByVal e As System.EventArgs) Handles Button1.Click
9:  
10:     Trace.WriteLine(DateTime.Now.ToString())
11: 
12:   End Sub
13: 
14:   Private Sub Form1_Load(ByVal sender As System.Object, _
15:     ByVal e As System.EventArgs) Handles MyBase.Load
16:     Trace.Listeners.Add(Listener)
17:   End Sub
18: 
19:   Private Sub Timer1_Tick(ByVal sender As System.Object, _
20:     ByVal e As System.EventArgs) Handles Timer1.Tick
21:     TextBox1.Text = Listener.Text
22:   End Sub
23: End Class

The field Listener creates an instance of MyListener on line 6. The Form1_Load event places the TraceListener into the Trace.Listeners collection. This step is essential for the listener to begin receiving messages. Placing the listener in the Trace.Listeners collection is an excellent comparative analogy to how the Windows operating system sends messages to controls using the operating system's assigned handle.

In our test application, when a button is clicked we invoke Trace.WriteLine to send some Trace data. The data could be anything. All registered listeners will receive the traced data. Hence you will see the trace data—the date and time in our sample—displayed in the default listener—the Output window—each time the button is clicked and in the TextBox in the sample application, each time the Timer's Tick event fires.

Summary

Custom TraceListener objects are relatively easy to define. Inherit from the System.Diagnostics.TraceListener class and provide an implementation for Write and WriteLine. (In addition, you have the option of overriding the Flush, Fail, and Close events.) The key is to use the Trace behaviors, and if you find you need some custom tracing behavior then create the custom TraceListener.

About the Author

Paul Kimmel is a freelance writer for Developer.com and CodeGuru.com. Look for his recent book "Advanced C# Programming" from McGraw-Hill/Osborne on Amazon.com. Paul will be a keynote speaker in "Great Debate: .NET or .NOT?" at the Fall 2002 Comdex in Las Vegas, Nevada. Paul Kimmel is available to help design and build your .NET solutions and can be contacted at pkimmel@softconcepts.com.

# # #



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • 10 Rules that Make or Break Enterprise App Development Projects In today's app-driven world, application development is a top priority. Even so, 68% of enterprise application delivery projects fail. Designing and building applications that pay for themselves and adapt to future needs is incredibly difficult. Executing one successful project is lucky, but making it a repeatable process and strategic advantage? That's where the money is. With help from our most experienced project leads and software engineers, …

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds