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.
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 += " "; } 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 }
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.