DekGenius.com
[ Team LiB ] Previous Section Next Section

Recipe 6.4 A Custom Trace Class that Outputs Information in an XML Format

Problem

You need to output trace information in an XML format. Unfortunately, the Trace and Debug classes are sealed and therefore cannot be inherited from in order to create more specialized classes. This limitation poses somewhat of a problem if you need to create a Trace or Debug class that outputs XML instead of plain text. You could start from scratch and build new Trace and Debug classes from the ground up, but you would have to handle configuration files, listener collections, and switch information, among other things. This way can become quite time-consuming; you need a better way.

Solution

You could use the Log4Net package found at the SourceForge web site (http://log4net.sourceforge.net); it is a complete logging system that can easily be added to your application. However, if you use the XML logging, you should realize that the XML output is not well-formed. This is done by design so that the XML fragments output from Log4Net can be included as external entities in a different XML file to create a well-formed XML file.

Another solution is to create a new trace listener class, such as XMLTraceListener, that inherits from the framework-provided TraceListener class. The XMLTraceListener class is defined as follows (note that the XMLTraceListener does create a well-formed XML document):

using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Xml;

public class XMLTraceListener : TraceListener, IDisposable
{
    // CTORS
    public XMLTraceListener( ) : this(null, "XMLTraceListener") {}

    // Required to be used by a *.config file
    public XMLTraceListener(string name) : this(null, name) {}

    public XMLTraceListener(Stream stream) : 
      this(stream, "XMLTraceListener") {}

    public XMLTraceListener(Stream stream, string name)
    {
        indentLevel = 0;

        if (stream == null)
        {
            string DirName = Environment.CurrentDirectory;

            if (DirName.EndsWith(Path.DirectorySeparatorChar.ToString( )) ||
                DirName.EndsWith(Path.AltDirectorySeparatorChar.ToString( )))
            {
                DirName += Process.GetCurrentProcess( ).ProcessName + ".xml";
            }
            else
            {
                DirName += @"\" + Process.GetCurrentProcess( ).ProcessName +
                  ".xml";
            }

            try
            {
                writer = new XmlTextWriter(File.OpenWrite(DirName), null);   
            }
            catch (Exception e)
            {
                Debugger.Log(0, "Initialization Error", 
                  "Could not create StreamWriter");
                Debugger.Log(0, null, e.ToString( ));

                // Re-throw exception
                throw;
            }
        }
        else
        {
            // Create XML writer
            writer = new XmlTextWriter(stream, null);
        }

        // Open the XML document, and write the root element
        writer.WriteStartDocument( );
        writer.WriteStartElement(XmlConvert.EncodeLocalName(
          "XMLDebugOutput"));
    }

    // FIELDS
    private Stack tagHierarchy = new Stack( );
    private int indentLevel = 0;
    private XmlTextWriter writer = null;

    // METHODS
    public override void Write(string message)
    {
        if (this.NeedIndent)
        {
            this.NeedIndent = true;
        }

        WriteData(message);
    }

    public override void Write(object obj)
    {
        if (obj != null)
        {
            this.Write(obj.ToString( ));
        }
        else
        {
            this.Write("");
        }
    }

    public override void Write(object obj, string message)
    {
        if (obj != null)
        {
            this.Write(obj.ToString( ), message);
        }
        else
        {
            this.Write("", message);
        }
    }

    public override void Write(string message, string category)
    {
        this.Write(message + ": " + category);
    }

    public override void WriteLine(string message)
    {
        this.Write(message + Environment.NewLine);
    }

    public override void WriteLine(object obj)
    {
        if (obj == null)
        {
            this.Write(obj.ToString( ) + Environment.NewLine);
        }
        else
        {
            // If the obj param is specified to be TraceTag.End
            //   we can close the last opened XML tag
            if (obj is TraceTag)
            {
                if (((TraceTag)obj) == TraceTag.End)
                {
                    WriteEndTag( );
                }
                else
                {
                    throw (new ArgumentException(
                             "This must be specified only " +
                             "as a TraceTag.End tag.", 
                             obj.ToString( )));
                }
            }
            else
            {
                this.Write(Environment.NewLine);
            }
        }
    }

    public override void WriteLine(object obj, string message)
    {
        if (obj == null)
        {
            this.Write(obj.ToString( ), message + Environment.NewLine);
        }
        else
        {
            // If the obj param is specified to be TraceTag.Start
            //   we can open the starting XML tag & record it
            if (obj is TraceTag)
            {
                if (((TraceTag)obj) == TraceTag.Start)
                {
                    WriteStartTag(message);
                }
                else
                {
                    throw (new ArgumentException(
                             "This must be specified only " +
                             "as a TraceTag.Start tag.", 
                             obj.ToString( )));
                }
            }
            else
            {
                this.Write("", message + Environment.NewLine);
            }
        }
    }

    public override void WriteLine(string message, string category)
    {
        this.Write(message, category + Environment.NewLine);
    }

    private new string WriteIndent( )
    {
        this.NeedIndent = false;

        string IndentChars = "";
        for (int Counter = 0; Counter < (this.indentLevel); Counter++)
        {
            IndentChars += "\t";
        }

        return (IndentChars);
    }

    private void WriteData(string message)
    {
        // Write to the debugger output
        if (Debugger.IsAttached && Debugger.IsLogging( ))
        {
            Debugger.Log(0, null, WriteIndent( ) + message);
        }

        // Write to the stream output
        writer.WriteString(message);
    }

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

    public override void Fail(string message, string detailedMessage)
    {
        WriteStartTag("FAIL");

        // Write to the debugger output
        if (Debugger.IsAttached && Debugger.IsLogging( ))
        {
            Debugger.Log(0, null, WriteIndent( ) + 
              "!!! Failure Message !!! ");
            Debugger.Log(0, null, message);
            if (detailedMessage != null)
            {
                Debugger.Log(0, null, ": " + detailedMessage);
            }
            Debugger.Log(0, null, Environment.NewLine);
        }

        // Write to the stream output
        writer.WriteString("!!! Failure Message !!!");
        if (message != null)
        {
            writer.WriteString(message);
        }
        if (detailedMessage != null)
        {
            writer.WriteString(": " + detailedMessage);
        }
        WriteEndTag( );
    }

    private void WriteStartTag(string tag)
    {
        // Test the tag param for correct xml tag syntax
        if (!System.Security.SecurityElement.IsValidTag(tag) )
        {
            throw (new ArgumentException("Invalid tag.", "tag"));
        }
        else if (tag.Length <= 0)
        {
            throw (new ArgumentException(
                     "Invalid tag, tag must be greater than zero " + 
                     "characters in length.", 
                     "tag"));
        }
        else if (char.IsNumber(tag[0]))
        {
            throw (new ArgumentException(
                     "Invalid tag, tag must not start with a " + 
                     "numeric character.", 
                     "tag"));
        }                                

        // Output the tag to both the debugger & XmlTextWriter
        Debugger.Log(0, null, WriteIndent( ) + "<" + tag + ">" + 
           Environment.NewLine);
        writer.WriteStartElement(XmlConvert.EncodeLocalName(tag));

        // Increase the indent level
        this.indentLevel++;

        // Push this tag onto the stack
        //   This stack element will be used again in 
        // the WriteEndTag method
        tagHierarchy.Push(tag);
    }

    private void WriteEndTag( )
    {
        writer.WriteEndElement( );

        this.indentLevel--;

        // Write out the ending tag for the next item to be popped
        // off the stack
        if (tagHierarchy.Count > 0)
        {
            Debugger.Log(0, null, WriteIndent( ) + @"</" + 
                         tagHierarchy.Pop( ).ToString( ) +
                         ">" + Environment.NewLine);
        }
        else
        {
            throw (new InvalidOperationException(
                     "Cannot close a tag that has not been created."));
        }
    }

    public override void Close( )
    {
        this.Dispose( );
    }

    public override void Flush( )
    {
        writer.Flush( );
        base.Flush( );
    }

    public new void Dispose( )
    {
        // Close all XmlTextWriter unclosed XML tags
        writer.WriteEndDocument( );

        // Close all Debugger.Log unclosed XML tags
        int tagCount = tagHierarchy.Count;
        for (int counter = 0; counter < tagCount; counter++)
        {
            this.indentLevel--;
            Debugger.Log(0, null, WriteIndent( ) + @"</" + 
                         tagHierarchy.Pop( ).ToString( ) +
                         ">" + Environment.NewLine);
        }

        writer.Close( );
        base.Close( );

        GC.SuppressFinalize(this);
    }
}

Here is the enumeration used to indicate to the XMLTraceListener object whether to write out a starting or an ending tag:

public enum TraceTag
{
    Start,
    End
}

Discussion

The Trace and Debug classes are sealed and therefore cannot be inherited from to create more specialized classes. This limitation poses somewhat of a problem if we need to create a specialized Trace or Debug class that outputs XML instead of plain text. As an alternative plan, we can inherit from the System.Diagnostics.TraceListener class to create a specialized listener that outputs XML. Our new XMLTraceListener class can then be added to the collection of listeners contained in either the Trace or Debug classes. Once the listener is added, we can use the Trace or Debug classes as normal.

The following example shows how the XMLTraceListener class could be used to output trace information as an XML document:

public void TestXMLTraceListener( )
{
    // The trace information will be displayed in the Output window of the IDE

    // Add our trace listener to the collection of listeners
    Trace.Listeners.Clear( );
    Trace.Listeners.Add(new XMLTraceListener( ));

    // Test output
    Trace.WriteLine(TraceTag.Start, "one");           // <one>
      Trace.WriteLine("The first element");
      Trace.Fail("FIRST FAIL");
      Trace.Fail("SECOND FAIL", "Details");
      Trace.WriteLine(TraceTag.Start, "two");         // <two>
        Trace.WriteLine("The second element");
        Trace.WriteLine(TraceTag.Start, "three");     // <three>
          Trace.WriteLine("The third element");
        Trace.WriteLine(TraceTag.End);                // </three>
        Trace.WriteLine(TraceTag.Start, "four");      // <four>
          Trace.WriteLine("The fourth element");
        Trace.WriteLine(TraceTag.End);                // </four>
        Trace.Assert(false, "FIRST ASSERTION", "Details");
        Trace.Assert(false, "SECOND ASSERTION");
        Trace.Assert(false);
      Trace.WriteLine(TraceTag.End);                  // </two>
    Trace.WriteLine(TraceTag.End);                    // </one>

    // Cleanup
    Trace.Flush( );
    Trace.Close( );
}

Note that for the Trace class to output any information, the TRACE directive must be defined, either in the project properties dialog box under Configuration Properties Build Conditional Compilation Constants or by using a #define directive at the beginning of the file:

#define TRACE

The main difference between using the XMLTraceListener and any other trace listener is the use of the TraceTag enumeration. This enumeration has two values, Start and End. Start signifies that the current text to be output will be output as a starting tag, and End signifies that the next tag output will close the most recently opened tag. Note that the closing tag text is automatically displayed; you do not have to keep track of this information.

The XMLTraceListener class contains two private methods of interest, WriteStartTag and WriteEndTag. The WriteStartTag method writes the starting tag for an XML block, after verifying that the tag is a valid XML tag. Note that these verification steps are not performed by the XmlTextWriter.WriteStartElement method. The WriteEndTag method writes the ending tag for the last XML tag that you opened. Notice that the WriteEndTag method does not accept any parameters. This method knows which closing tag to write to the Debugger.Log method by using the tagHierarchy Stack object. The last beginning tag written is placed on the top of this stack. All the WriteEndTag method has to do is pop off the last tag from this stack and write out its closing tag. The XmlTextWriter.WriteEndElement method automatically keeps track of the starting and ending tags so no special handling is required.

The WriteStartTag is indirectly called by using the WriteLine method, which accepts both an object and a string argument. Passing in the value TraceTag.Start for the object argument and a tag name for the string argument, you can create a beginning tag as shown here:

Trace.WriteLine(TraceTag.Start, "one");           // Displays:   <one>

To close this tag, call the overloaded WriteLine method, which accepts only an object argument. The value passed to this argument must be TraceTag.End in order to display an ending tag:

Trace.WriteLine(TraceTag.End);                    // Displays:   </one>

If the default constructor or the constructor that accepts only a string argument is called, a default XML file is created and passed to the first parameter of the XmlTextWriter constructor. The name of the file will be the process name for the application with the .xml extension. The file will be created in the current directory of the executing assembly.

See Also

See the "TraceListener Class" and "XmlTextWriter" topics in the MSDN documentation.

    [ Team LiB ] Previous Section Next Section