Better Debugging with the .NET Diagnostic Tools

I l @ ve RuBoard

I mentioned earlier that debuggers are not always helpful in diagnosing problems. This might seem counterintuitive ”after all, debuggers are all about diagnosing problems in your code. But at times debugging is reminiscent of a principle of quantum mechanics: you cannot observe a system without affecting the outcome. A example that comes to mind is threading. It is not unusual for timing to be a critical factor in a software failure (known as a race condition ). By attaching the debugger, you can slow the system down, and while you might avert the problem, you won't learn where and why the failure is occurring. You might be left scratching your head and asking, "Why does this work only when the debugger is attached?"

This is where many of the classes in the System.Diagnostics namespace come in handy. This namespace contains a bunch of utility classes that provide a whole host of features you can use to instrument an application (by adding debugging hooks, posting events to the event log, creating performance counters, and outputting status information) and debug its runtime behavior. The three features I'll focus on are communicating with a debugger, using the event logs, and using the Trace and Debug statements.

The Debugger Class

The Debugger class enables your application to communicate with an attached debugger. This can be useful when you have complex debugging conditions that you are having difficulty otherwise re-creating. Imagine a situation in which an exception has been generated by an application under stress. To re-create the problem, you might have to apply a large amount of stress (for example, simulate a large number of simultaneous clients ) while also attaching a debugger to wait for the error. The problem is that applications tend to throw exceptions quite frequently. If the type of exception you're interested in is quite frequently thrown, locating the actual failure point can be like looking for a needle in a haystack.

This is where the Debugger class can help out. Using the various shared members of this class, you can programmatically cause an attached debugger to break at a specific line of code, launch a new debugger, and send messages to the attached debugger. Table 10-2 describes the shared properties and methods of the Debugger class.

Table 10-2. Shared Members of the Debugger Class

Name

Member Type

Description

IsAttached

Property

Indicates whether a debugger is attached to the current process.

Break

Method

Signals the currently attached debugger that it should break into the code at the current line. If no debugger is attached, this has no effect.

IsLogging

Method

Indicates whether logging is currently enabled by the attached debugger.

Launch

Method

Causes a debugger to be launched and attached to the current process.

Log

Method

Logs information to the attached debugger.

All of the public members of the Debugger class are shared. This makes the features extremely easy to use, as shown in the following example:

 'Breakintoanattacheddebugger. IfDebugger.IsAttachedThenDebugger.Break() 'Sendamessagetoanattacheddebugger. Debugger.Log("HiThere!") 

Easy, right? The features of the Debugger class are easy to access, but they have a myriad of uses that are limited only by your imagination .

Event Logs

Windows event logging provides a centralized mechanism for applications to report important events. The Windows event-logging service stores events from various sources into a single event collection (known as an event log ). Event logs are commonly used by Windows services applications for the precise reason that they do not present a user interface. The event log provides a single location where a system administrator can check the status of a machine and all of the services running on a device. The contents of the available event logs can be viewed using the Event Viewer, which is shown in Figure 10-6.

Figure 10-6. The Event Viewer.

graphics/f10pn06.jpg

The Windows Event Log supports several important features. First, it supports logging different kinds of events: Info , Warning , Success , Failure , and Error events. This gives applications some degree of flexibility in what kind of status information they can report. It also lets a user or system administrator easily determine what, if any, failures are occurring. Second, the Event Log supports multiple logs. This enables an application to post its events to a specific log that is most appropriate. (The standard logs are Application, Security, and System).

The ability to work with event logs of all stripes is provided by the all-important EventLog class in the System.Diagnostics namespace. This class is used to create, manage, monitor, delete, and post to event logs. It enables flexible management of all types of logs. These logs can reside on the local system or on any system the current user has access to. The most typical use, however, is for accessing event logs on the local system.

Note

Every system has three standard event logs: Application, Security, and System. Only two of these can be written to by an application: Application and System. The Security log can be written to only by the operating system.


Creating New Event Log Entries

Creating new entries in an event log is simple. The EventLog class sports the WriteEntry method for just this purpose. Using this method (or one of its many overloads), you can easily write entries to the event log of your choice (except the Security log). There are, in fact, 10 overloads of the WriteEntry method. Five of these methods are Shared , allowing you to easily write an information to the Application log much like in the following example:

 EventLog.WriteEntry("MyApplicationName", "Thisisatest") 

You can think of this as an easy way to "one-off" a entry to an event log. The only caveat is that you must first register your own event source ( essentially your application's name) and the log associated with that source. Alternatively, you can create an instance of the EventLog class, providing the name of the log you want to write to. Then you can call one of the five WriteEntry instance members and write the entry to the log of your choice. In the following sample, I again write an information entry to the Application log:

 DimevtLogAsNewEventLog("Application") evtLog.Source= "MyApplication" evtLog.WriteEntry("Thisisaneventmessage") evtLog.Dispose() 

The previous two samples touched only lightly on posting an event to a log. There is much additional information you can specify, including information on the type of event, an event ID, an event category, and binary data about the event. The other overloads of the WriteEntry methods give you this capability.

Before we move on, we should discuss the benefits of the shared methods versus the instance methods of the WriteEntry method of the EventLog class. From a performance perspective, if you expect to make calls to an event log in an application that has strict performance requirements (an application that can't afford to spend any more time than is necessary to post an event), you should create an instance of the EventLog class and call one of the nonshared WriteEntry methods. When you call the shared WriteEntry methods, you are implicitly creating a new EventLog instance each time you write a new log entry.

Shared methods still have a use, however. If you're likely to report an event only when an error or exception occurs, it might be easiest to use the Shared methods ”you won't have to write the EventLog initialization code.

Creating Event Logs

The two major concepts related to the event log system are sources and logs. An event source is really just an application name. A source is a way of identifying the application that created an event. You can create and delete your own logs, and you can also define your own sources. When you create, or register, a new event source, you specify the log that the source is associated with. The following example creates a new event source called MySource and associates it with the Application event log:

 EventLog.CreateEventSource("MySource", "Application") 

Of course, this is nothing spectacular. The default behavior of the EventLog.WriteEntry method is to post all entries to the Application log. So this is really just duplicative. It gets interesting if I specify a different log name:

 EventLog.CreateEventSource("MySource", "MyLog") 

What happens here? The system not only registers my new source with the log named MyLog, but it also creates that log if it doesn't already exist. This is a simple, one-step way to register a new event source and create a custom event log. From now on, once I post events using the MySource identifier, all of the events will be written to the MyLog event log. Great! But there's a problem. What if the event source already exists? Registering an event source twice will cause an exception, so you need to be able to tell if the source already exists. The EventLog.SourceExists methods allow you to find out. Your code to register an event source would now look more like the following:

 IfNotEventLog.SourceExists("MySource")Then EventLog.CreateEventSource("MySource", "MyLog") EndIf 

Then there's the converse problem: how to unregister a source and delete a log. Deregistering a source is easy using the EventLog.DeleteEventSource shared method. Another shared method, EventLog.Delete , allows you to delete custom logs. The following example demonstrates how both of these methods work:

 IfEventLog.SourceExists("MySource")Then EventLog.DeleteEventSource("MySource") EndIf IfEventLog.Exists("MyLog")Then EventLog.Delete("MyLog") EndIf 

This is not the only way to create your own sources and event logs. Instead of using a runtime programmatic means of creating and deleting your logs, you can handle these tasks at install time using the EventLogInstaller class.

The EventLogInstaller class

Like the other installer classes we looked at in Chapter 7 and Chapter 8, the EventLogInstaller class makes it easy to install and uninstall event logs and sources. It provides a unified way to set up all of your application's install/uninstall issues.

The mechanics for creating your own event log installer should be familiar:

  1. Create an installer class derived from EventLogInstaller .

  2. Mark this class with the RunInstaller attribute.

  3. Set the inherited Source and Log properties of the class in the constructor.

That's it. Thanks to the magic of the installer utility (InstallUtil.exe), this minimal amount of code handles both the install and uninstall of your event logs. The following sample demonstrates this:

 ImportsSystem.Diagnostics ImportsSystem.ComponentModel <RunInstaller(True)>_ PublicClassMyEventLogInstaller InheritsEventLogInstaller PublicSubNew() MyBase.New() Me.Source= "EventLogSample" Me.Log= "MyEventLog" EndSub EndClass 

If I compile this code into an assembly and run the InstallUtil.exe application against it, a new event log, such as the one shown in Figure 10-7, will be created, as expected. That's pretty easy, and it neatly solves our install and uninstall issues. If this approach works for you, I highly recommend it.

Figure 10-7. The result of the MyEventLogInstaller class.

graphics/f10pn07.jpg

Monitoring an Event Log

Not only does the EventLog class allow you to create new logs and post events, but it also allows you to monitor the activity of any log. EventLog allows you to capture events as they are written to a log as well as access all of the current event entries in a log. Before we talk about how to do this, let's spend a little time talking about the EventLogEntry class, a subject I have avoided until now.

The EventLogEntry class

The EventLogEntry object represents a single event entry in an event log. When I called the EventLog.WriteEntry method earlier, I implicitly created these objects. The EventLogEntry class supports an array of properties that describe all of the information about a specific entry in an event log. Table 10-3 lists the properties of the EventLogEntry class.

Table 10-3. Properties of the EventLogEntry Class

Property

Description

Category

Retrieves text related to the CategoryNumber for this event log entry

CategoryNumber

The category number of this event log entry

Data

The binary data associated with the entry

EntryType

The event type of this entry

EventID

The application-specific event identifier of this event entry

Index

The index of this event entry in the event log

MachineName

The name of the computer where this entry was created

Message

The message contained within this entry

Source

The name of the application that created this event

TimeGenerated

The time when this entry was created

TimeWritten

The time when this entry was written to the log

UserName

The name of the user who's responsible for this event

You cannot directly create new entries of the EventLogEntry class. Only the system can do that. You can cause instances of the class to be generated by calling one of the EventLog.WriteEntry methods, but you can never alter the contents of any of the properties. This is quite purposeful. The entries in an event log are intended to be immutable. All you can do is inspect the individual properties.

Accessing the Event Log Entries

Accessing the entries of a log is simple. When you create an instance of the EventLog class, you can specify the log the object points to. When you do this, the Entries property, a collection of EventLogEntry objects, is populated . You can iterate this collection and look at each entries and process them however you want. Simple.

In addition to just reading the contents of a log, the EventLog class supports the EntryWritten event, which you can use to monitor entries as they are posted to the log. Furthermore, you can turn the monitoring for an event log on and off using the EnableRaisingEvents property of EventLog . The following sample demonstrates how you might do this:

 DimevtLogAsNewEventLog("Application") evtLog.EnableRaisingEvents=True AddHandlerevtLog.EntryWritten,AddressOfEventMethod ... PublicSubEventMethod(ByValsenderAsObject,_ ByValeAsEntryWrittenEventArgs) DimentryAsEventLogEntry=e.Entry Console.WriteLine(entry.Message) EndSub 

By default, the EventLog class does not raise the EntryWritten event, so you must explicitly enable it by setting the EnableRaisingEvents to True . You can just as easily disable this event by setting the EnableRaisingEvents property to False . From this sample you can also see how the EventLogEntry class is passed to the handler for the EntryWritten event. The EventLogEntry object is easily accessible, and you can access all of the information about that particular event.

The ability to monitor event log postings can have many uses. For example, you can create an application to monitor a set of services on a machine (or set of machines). By checking the Source and EntryType parameters, you can easily determine if there are problems with the applications you're monitoring (as long as they post to the event log).

The EventLog Sample Application

I created the EventLog Sample application, shown in Figure 10-8, to demonstrate a number of event logging concepts. This sample includes supporting an EventLogInstaller , writing entries to a log, and monitoring the entries posted to that log.

Figure 10-8. The EventLog Sample application.

graphics/f10pn08.jpg

Let's start at the top. First, I create an EventLogInstaller to create my event log. I set my source to the name of the application (" EventLogSample") and I call my new log " MyEventLog". Here's the code:

 ImportsSystem.Diagnostics ImportsSystem.ComponentModel <RunInstaller(True)>_ PublicClassMyEventLogInstaller InheritsEventLogInstaller PublicSubNew() MyBase.New() Me.Source= "EventLogSample" Me.Log= "MyEventLog" EndSub EndClass 

When the application is compiled, I must run the InstallUtil.exe application to set up the event log; otherwise, the application will, by design, fail when calling the form's Load event. You typically won't run into problems until you create the EventLog object or call one of the Shared WriteEntry methods, but I wanted to make sure the sample handled this cleanly ”you'll know right off the bat if you haven't run the installer. Let's take a look at the code in this form:

 PublicClassForm1 InheritsSystem.Windows.Forms.Form #Region " WindowsFormDesignergeneratedcode " DimWithEventsevtLogAsEventLog PrivateSubForm1_Load(ByValsenderAsSystem.Object,_ ByValeAsSystem.EventArgs)HandlesMyBase.Load 'Createthesourceandeventlog IfNotEventLog.SourceExists("EventLogSample")Then MsgBox("RunInstallUtil.exeonthisapplication " &_  "toinstalltheeventlogs") Me.Close() Return EndIf 'CreateanewinstanceoftheEventLogclassandattachtothe 'MyEventLoglog evtLog=NewEventLog("MyEventLog") evtLog.Source= "EventLogSample" 'Assignoureventloginstancetothenewlycreatedlog evtLog.EnableRaisingEvents=True 'InitializetheForm'sComboBox LogType.Items.AddRange(NewObject(){EventLogEntryType.Error,_ EventLogEntryType.Information,_ EventLogEntryType.FailureAudit,_ EventLogEntryType.SuccessAudit,_ EventLogEntryType.Warning}) EndSub PrivateSubForm1_Closing(ByValsenderAsObject,_ ByValeAsSystem.ComponentModel.CancelEventArgs)_ HandlesMyBase.Closing IfNotevtLogIsNothingThen evtLog.Dispose() EndIf EndSub PrivateSubPostButton_Click(ByValsenderAsSystem.Object,_ ByValeAsSystem.EventArgs_)HandlesPostButton.Click IfLogType.SelectedItemIsNothingThen MsgBox("Pleasespecifyaneventtype...") Return EndIf evtLog.WriteEntry(Message.Text,LogType.SelectedItem) EndSub PrivateSubevtLog_EntryWritten(ByValsenderAsObject,_ ByValeAsEntryWrittenEventArgs_)HandlesevtLog.EntryWritten DimevtAsEventLogEntry=e.Entry DimsAsString=String.Format("{0}-{1}-{2}",_ evt.TimeGenerated,_ evt.EntryType,evt.Message) EventLogActivity.AppendText(s&vbCrLf) EndSub EndClass 

You can see that the form's Load event handles a number of tasks. It not only creates an instance of EventLog and populates the Log and Source properties, but it also enables the EntryWritten event. The Form1_Load method's final task is to populate the form's ComboBox with the values of the EntryType enumeration. This allows the user to select from the available event entry types when posting a new event.

All of the real action takes place in the PostButton_Click and evtLog_EntryWritten methods. In PostButton_Click , I used the EventLog.Write ­Entry method to post a new event to the application's even log. I used an overload of the WriteEntry method so I could specify not only the message but also the event type. The evtLog_EntryWritten method is fired after every call to PostButton_Click . I captured some of the information about the event and appended it to the form's text field so you can see how the event is processed in the application. For additional verification, you can use the Event Viewer application to see where the events end up.

The Trace and Debug Classes

The Trace and Debug classes are closely related. Both enable the developer to post messages about the progress of an application (typically to an attached debugger). But these classes do serve different purposes. The Debug class is intended for use with debug builds of an assembly and the Trace class is for instrumenting your application.

Both classes are based on a more generic form of output. While the Debugger class allows direct communication to a debugger, the Debug and Trace classes care little about who is receiving the output. The general idea is that the Debug and Trace classes send their output to a collection of Listener objects. What these Listener objects do with the information is completely up to them. Neither of the classes really cares what, if anything, is done with the information provided. Developers therefore gain a lot of flexibility, including the ability to create custom Listener objects to deal with the output of the Debug and Trace classes.

Using Debug and Trace

The Debug and Trace classes sport exactly the same set of methods and properties (although, strangely, they don't share a common interface). All of the members of these classes are shared. This means that any modification to the output settings are global for the process. Table 10-4 lists all of the members supported by both classes. The purpose of the Write and WriteLine methods is fairly intuitive, but other methods are more involved. For instance, some of the methods ( Assert and WriteIf ) conditionally output messages, and others ( Indent and Unindent ) modify the output.

Table 10-4. Shared Members of the Debug and Trace Classes

Member

Type

Description

Assert

Method

Checks for a condition. If the condition is false, it writes information to the trace listeners in the Listeners collection.

Close

Method

Flushes the output buffer and then closes the Listener objects. This disables further output until new Listener objects are created.

Fail

Method

Displays an error message.

Flush

Method

Flushes the output buffer and causes buffered data to be written to the Listener objects.

Indent

Method

Increments the current IndentLevel .

Unindent

Method

Decrements the current IndentLevel .

Write

Method

Writes information about the trace to the trace listeners in the Listeners collection.

WriteIf

Method

Writes information about the trace to the trace listeners in the Listeners collection if a specified condition is true. (This condition is passed as a parameter to this method.)

WriteLine

Method

Writes information about the trace to the trace listeners in the Listeners collection.

WriteLineIf

Method

Writes information about the trace to the trace listeners in the Listeners collection if a specified condition is true. (This condition is passed as a parameter to this method.)

AutoFlush

Property

Gets or sets whether Flush should be called on the Listener objects after every write.

IndentLevel

Property

Gets or sets the indent level.

IndentSize

Property

Gets or sets the number of spaces in an indent.

Listeners

Property

Gets the collection of listeners that is monitoring the trace output.

All of these properties, with the exception of the Listeners collection, have to do with controlling the output of the Debug or Trace statements. The Listeners collection contains all of the registered listener objects. As I mentioned before, these objects are the recipients of the Debug and Trace statements' output.

The following example shows how easy these classes are to use:

 'Theol'standardWriteLinemethod Debug.WriteLine("ThisisaDebugtest") Trace.WriteLine("ThisisaTracetest") 'PlayingwiththeAssertstatements Debug.Assert(False, "Thisalwaysprints") Trace.Assert(True, "Thisneverprints") Debug.Fail("Thisisafailuremessage") Trace.Fail("Thisisanotherfailuremessage") 'Demonstratinghowindentingworks Debug.Indent() Debug.WriteLine("Thisisindented") Trace.WriteLine("Thisisnotindented") Debug.Unindent() Debug.WriteLine("Thisisnotindented") 

So you get the point: the Debug and Trace classes are equally easy to use. If you know how to use one, you know how to use the other ”they work exactly the same. The only difference is in the message's intended purpose.

Enabling and Disabling Debug and Trace

If the Debug and Trace classes are so similar, why have both? The main difference between these classes is in their purpose rather than their functionality. The Debug class should be used only to generate statements that are specifically related to debugging a project. You don't have to do it this way, but it is the recommended approach. The Trace class is intended to be included with deployed applications ”it lets you trace the execution of a code path within an application. Debug statements should never make it into deployed applications. You can clarify the distinction between the two classes by asking yourself whether this statement should be included in our deployed application. If not, it should probably be a Debug statement instead of a Trace statement.

Using compile-time switches, you can include or omit the Debug and Trace statements. On the Build Configuration property page for your Visual Basic .NET project (Figure 10-9), you can control whether Debug and Trace statements are included in a build. By default, a debug build includes both Debug and Trace statements. A retail build is configured, by default, to include only the Trace statements.

Figure 10-9. The Build Configuration property page.

graphics/f10pn09.jpg

Note that when you deselect one of these options ”say, Debug ”any calls to that class are completely omitted from the compiled application. This allows you to speed up your application's performance by omitting these statements and without having to manually remove or comment them out. When you think about it, this is a powerful feature. It allows you to be prolific with your debugging statements when you're developing an application and, with the flick of a compiler switch, generate an application completely free of these statements. Pretty darn cool.

TraceListener Objects

I've already mentioned Listener objects in passing. If you look at the Visual Studio .NET documentation, you'll see that the Listeners collections of both the Debug and Trace classes are of type TraceListenerCollection . A trace listener is a generic class that monitors an application's trace and debug output. All classes that implement this functionality derive from the TraceListener class. Contrary to what the name suggests, the TraceListener class is used to monitor output from both the Debug and Trace statements. It's pretty much source- agnostic .

Here's something else that is not intuitively obvious: the two apparently distinct Listener collections of Trace and Debug are in fact one and the same. The same collection is exposed on two different objects. This means that adding a TraceListener to the Trace.Listeners collection has the same effect as adding it to the Debug.Listeners collection. If you were to add a TraceListener to both collections, you would end up with duplicated output because you'd really be adding the same object to a collection twice.

Predefined TraceListener classes

The .NET Framework comes with a set of predefined TraceListener classes. The most commonly used listener is the DefaultTraceListener because it is ”surprise ”the default. When your application starts up, an instance of the DefaultTraceListener class is automatically created and added to the application's TraceListeners collection. The DefaultTraceListener class is designed to give the maximum flexibility for your debug and trace output. All program output through the tracing statements is passed to both the OutputDebugString API and the VisualBasic Log method. This ensures a default behavior that allows your application's debug output to be easily monitored .

In addition to the DefaultTraceListener , there are two additional predefined TraceListener objects: EventLogTraceListener and TextWriterTraceListener . Both provide additional functionality above and beyond the DefaultTraceListener class. The EventLogTraceListener can be used to send all trace and debug output to the event log. This frees the developer from having to attach a debugger just to see what is going on in an application. The following code demonstrates the use of the EventLogTraceListener class and how to add it to your application's Listeners collection.

 Dimetl1AsNewEventLogTraceListener() Dimetl2AsNewEventLogTraceListener("MyApplication") Dimetl3AsNewEventLogTraceListener(NewEventLog("Application")) 'Twodifferentwaystoaddthelisteners Debug.Listeners.Add(etl1) Trace.Listeners.AddRange(NewTraceListener(){etl2,etl3}) 

The TextWriterTraceListener also enables you to redirect your application's trace and debug output to a number of locations, including the console, a file, or a specific TextWriterStream . The following example demonstrates several ways to create a TextWriterTraceListener and add it to your application's Listeners collection:

 Dimtws1AsNewTextWriterTraceListener("myLog.log") Dimtws2AsNewTextWriterTraceListener(Console.Out) Dimtws3AsNewTextWriterTraceListener(NewStreamWriter("myLog2.log")) 'Twodifferentwaystoaddthelisteners Debug.Listeners.Add(tws1) Trace.Listeners.AddRange(NewTraceListener(){tws2,tws3}) 

One final piece of information about the built-in trace listeners. I mentioned that the DefaultTraceListener is created and added to both the Debug.Listeners collection and the Trace.Listeners collection. You can add as many additional listeners you please, but you might want to remove the default listener if it doesn't suit your purposes (to save on overhead). How do you do this? Easy. It's just in a collection. You can quickly handle this in a number of ways. You can clear the Listeners collection if you know that there won't be anything else in there you want to keep. Alternatively, you can pass a new instance of the DefaultTraceListener class to the Listeners.Remove method:

 'Quickanddirtywaytoclearthecollection Debug.Listeners.Clear() 'RemovingonlytheDefaultTraceListenerfromthecollection Trace.Listeners.Remove(newDefaultTraceListener()) 

Another way to customize your application's Listeners collection is through the application's configuration file. The following example demonstrates how this works. You can add and remove listeners without recompiling the application.

 <configuration> <system.diagnostics> <trace> <listeners> <removetype="System.Diagnostics.DefaultTraceListener,System"/> <addname="myEventListener" type="System.Diagnostics.EventLogTraceListener,System" initializeData="Application" /> <addname="myTextListener" type="System.Diagnostics.TextWriterTraceListener,System" initializeData="c:\myListener.log" /> </listeners> </trace> </system.diagnostics> </configuration> 

Any modifications to this configuration file must be made before an application starts (or restarts) to take effect. You can even add custom TraceListener classes (our next subject).

Creating a custom TraceListener

Creating your own TraceListener is not that difficult. In the simplest case, you can create a class that inherits from TraceListener . The only thing you're required to do is to override two base class methods: Write and WriteLine . The following example TraceListener raises a custom event whenever a call to Write or WriteLine is made:

 ImportsSystem.Diagnostics PublicClassTracer InheritsTraceListener PublicSubNew() MyBase.New() Trace.Listeners.Add(Me) EndSub PublicDelegateSubTraceHandler(ByValsAsString) PublicEventTraceEventAsTraceHandler PublicOverloadsOverridesSubWrite(ByValsAsString) RaiseEventTraceEvent(s) EndSub PublicOverloadsOverridesSubWriteLine(ByValsAsString) RaiseEventTraceEvent(s) EndSub EndClass 

You can take this sample even further. For example, there are a host of methods of the TraceListener class (including Close , Fail , and Flush ) that you can overload to provide the exact behaviors you're looking for.

Controlling Debug and Trace Output

You've seen two ways to customize the Debug and Trace output of an application. The first approach uses the compile-time flags to selectively enable or disable debug and trace output. The second approach modifies the application's TraceListener collection and develops custom TraceListener classes. But all of these approaches use brute force. Next, I want to demonstrate how you can fine-tune your Debug and Trace output using your application's configuration file and the Switch classes. The Switch classes provide you with a runtime mechanism for controlling Trace and Debug output. They give you control of your debug output through the configuration files. Instead of disabling your Trace and Debug output outright , you can control various levels of output.

A switch provides a mechanism for controlling tracing and debugging output at run time. Switch-based classes can read the configuration information from the application's configuration file, giving an application the flexibility to alter the amount of information, if any, generated by its tracing and debugging statements. Two switch classes are of immediate interest to us: BooleanSwitch and TraceSwitch .

The BooleanSwitch class

The BooleanSwitch class is pretty simple. It provide a basic on/off behavior for displaying output. All you need to do is create a new instance of the BooleanSwitch class and pass it the name of the switch. (This is looked up in the application's configuration file.) Then your code only needs to check its Enabled property. The following code demonstrates how you can do this:

 DimbsAsNewBooleanSwitch("mySwitch", "MySwitchDescription") Ifbs.EnabledThen Console.WriteLine("Theswitchisenabled") EndIf 

Of course, if you do nothing else, the Console.WriteLine statement will never execute. You must enable the switch somehow. This is easily done in the configuration file, as shown here:

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

When I specify the switch in the above XML, I have to provide a value. (Otherwise, it will always be false.) If I specify a value of 0 (or omit the value attribute) the BooleanSwitch will not be enabled. If I specify a value of 1 (or any other nonzero integer) the specified BooleanSwitch will be enabled. Pretty easy.

The TraceSwitch class

The TraceSwitch class is, as you might expect, somewhat more sophisticated than the Boolean switch. This class is really designed to provide a much more tightly controlled set of output options. Using the Trace ­Switch , you can specify what kind of output you're interested in. Five trace levels are supported. Table 10-5 lists these levels as well as the corresponding value that you can use in the application's configuration file to set this switch.

Table 10-5. Trace-Level Values

Level

Value

Description

Off

Disables output of Trace and Debug messages

Error

1

Outputs only error messages

Warning

2

Outputs only warnings and error messages

Info

3

Outputs informational, warning, and error messages

Verbose

4

Outputs all Trace and Debug messages

The TraceSwitch class supports a number of properties that allow you to work with the various configuration settings. The Level property reports the current value of the specified TraceSwitch instance. Four other properties provide a simpler approach to determining the currently allowed tracing level: TraceError , TraceInfo , TraceVerbose , and TraceWarning . To see how you can work with these properties, take a look at this example:

 DimtsAsNewTraceSwitch("myTraceSwitch", "ATraceSwitch") Console.WriteLine("Thetraceswitchlevel:{0}",ts.Level) Ifts.TraceErrorThenConsole.WriteLine("Tracelevelallowserrors") Ifts.TraceWarningThenConsole.WriteLine("Tracelevelallowswarnings") Ifts.TraceInfoThenConsole.WriteLine("Tracelevelallowsinformation") Ifts.TraceVerboseThenConsole.WriteLine("Tracelevelisverbose") 

Of course, you still need to specify the switch in the configuration file. The following example does just that. Using the value 1 from Table 10-5, this example enables the myTraceSwitch switch with an Error trace level.

 <configuration> <system.diagnostics> <switches> <addname="myTraceSwitch" value="1" /> </switches> </system.diagnostics> </configuration> 

I have included a sample console application among the book's sample files called SwitchExamples that allows you to play with both types of switches and the configuration file and quickly see the results.

The Switch classes

The last remaining question is how and when to use the Switch classes. Certainly, it is not highly desirable to litter your code with all sorts of If statements unless it is absolutely necessary. I would advocate creating a custom TraceListener and implement the TraceSwitch or BooleanSwitch in that class. That way, if you disable tracing, your application won't end up with If trace statements all over the place.

I l @ ve RuBoard


Designing Enterprise Applications with Microsoft Visual Basic .NET
Designing Enterprise Applications with Microsoft Visual Basic .NET (Pro-Developer)
ISBN: 073561721X
EAN: 2147483647
Year: 2002
Pages: 103

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net