.NET for Visual FoxPro Developers

Chapter 13
Error Handling and Debugging in .NET

Error handling in .NET is quite a bit different than error handling found in previous versions of VFP. However, both .NET and Visual FoxPro 8 have try…catch…finally blocks that provide a far more powerful and flexible way to handle errors in your application. This chapter also discusses Visual Studio .NET’s debugging tools. As you’ll see, many are similar to Visual FoxPro’s; some are better, and some are not!

Errors are part and parcel of software applications. However, in weakly typed languages such as Visual FoxPro, you are more likely to create code containing bugs than in strongly typed languages that find far more errors at compile time. However, even the best of compilers can’t find all of the logic errors you have in your code. In addition, problems can occur at runtime that have nothing to do with bugs in your code. At times resources are locked or unavailable, files become corrupt, and remote servers can go down. Your code needs to be able to handle all of these scenarios.

This is where error handling and debugging tools come to the rescue. This chapter discusses .NET error handling as well as Visual Studio .NET’s debugging tools that help you identify errors in your .NET applications.

Common errors vs. exceptions

There are errors and there are exceptions. Errors are common problems that occur in an application from which the application can easily recover. For example, a user may enter an invalid value and your application can display a message warning them of this. A connection to a database may have been left open and the application can simply close it.

In contrast, exceptions, as suggested by their name, are uncommon or exceptional errors from which the application cannot recover. For example, you may not be able to connect to the application’s database—this is exceptional, because the application is not be able to run properly without access to the data. A programmer may have passed an invalid value to a method—this is exceptional (hopefully!) and the method is not able to run.

In the case of common error handling, you should write defensive code that expects the expected and gracefully handles the problem. In the case of exceptions, you need to do something more. The following sections describe exception handling in .NET.

.NET Exceptions

In the .NET world, an exception is an object created when an error occurs in your application (this is also known as throwing an exception). The .NET Framework contains hundreds of different exception classes created when specific types of errors occur. With so many different exception classes, it’s not practical that you become intimately familiar with each and every one. Figure 1 shows just a few of the exception classes you commonly encounter and Table 1 gives a brief description of each of these classes. As you get more experience writing .NET applications, you learn about the most common exceptions quickly enough!

Figure 1. Common .NET Exception classes.

Table 1. Common Exception class descriptions

Property

Description

System.Exception

The base class for all other exception classes.

System.ApplicationException

The base class for all custom exception classes you create.

System.SystemException

The base class for all predefined exceptions in the System namespace.

System.ArithmeticException

The base class for arithmetic, casting, or conversion operation errors.

System.OverflowException

The exception thrown when there is an overflow in arithmetic, casting, or conversion operation.

System.ArgumentException

The exception thrown when an invalid argument is passed to a method.

System.IndexOutOfRangeException

The exception thrown when an attempt is made to access an array element that is out of the bounds of the array.

System.StackOverflowException

The exception thrown when the stack overflows with too many pending method calls. This can occur if you have a recursive, infinite loop in your code.

System.IO.IOException

The base class for all I/O exceptions.

System.IO.DirectoryNotFoundException

The exception thrown when a directory cannot be found.

System.IO.FileNotFoundException

The exception thrown when a file cannot be found.

 

Exceptions can be thrown by either the .NET Common Language Runtime or by an executing application. Some examples of CLR exceptions include ExecutionEngineException, StackOverflowException, and OutOfMemoryException. These exceptions are usually of such a critical nature that the application cannot recover from them. For more information, see the .NET Help topics for each of these classes.

Catching exceptions with try…catch…finally

.NET provides structured exception handling in the form of try…catch…finally blocks in both C# and Visual Basic .NET that allow you to catch and respond to exceptions thrown in your application. These blocks should be used to catch exceptions—not common errors. Here is a template showing how these try…catch…finally blocks are structured.

In C#:

try

{

  // Contains regular application code

}

catch

{

  // Contains error handling code

}

finally

{

  // Contains any cleanup code needed to close resources such

  // as files and connections that were opened in the try block

}

And in Visual Basic. NET:

Try

  ' Contains regular application code

Catch

  ' Contains error handling code

Finally

  ' Contains any cleanup code needed to close resources such

  ' as files and connections that were opened in the try block

End Try

The following sections explain what the try, catch, and finally blocks are and how they can be used in your applications.

The try block

You can surround any code that has the potential to cause an exception with a try block. A good example is trying to create a connection to a database. For any number of reasons, including the possibility that the database server is down, you may encounter an error when you attempt to connect to a database.

The catch block

As mentioned previously, when an error occurs in your application, a corresponding exception class representing that specific error is instantiated. The catch block allows you to get a reference to this exception object and respond accordingly. The catch block only executes if an exception occurs.

The catch block is optional. However, if you don’t have a catch block, your try block must have an associated finally block, which is discussed in the next section. If you
don’t have a catch block associated with a try block, the runtime searches up the stack to find the next suitable catch block. See the “Nested try blocks” section later in this chapter for details.

The finally block

The finally block gives you a place to insert cleanup code that is executed whether or not an error is encountered in the try block. The finally block, although useful in many cases, is optional—if you don’t have any specific cleanup code that needs to be run, you can leave it out. However, if you leave out the finally block, your try block must have an associated catch block.

A real-world example

Here’s a real-world example showing how to use the try…catch…finally blocks when accessing data. This method connects to the SQL Server Northwind database and runs the “Ten Most Expensive Products” stored procedure. The result set is stored in a data reader that is iterated to build a string and displayed using the MessageBox class.

In C#:

public void TryCatchDemo()

{

  SqlDataReader dr = null;

 

  // Create the connection string & configure the connection and command objects

  string ConnectString = "server=(local);uid=sa;pwd=;database=NorthWind;";

  SqlConnection Conn = new SqlConnection(ConnectString);

  SqlCommand Cmd = new SqlCommand("Ten Most Expensive Products", Conn);

  Cmd.CommandType = CommandType.StoredProcedure;

 

  try

  {

       // Open the connection and execute the stored procedure

       Conn.Open();

       dr = Cmd.ExecuteReader();

 

       // Get all records in the result set

       string Products = "";

       while (dr.Read())

       {

          // Access the data in the current row

          Products += dr.GetString(0) + ", " + dr.GetDecimal(1) + "\n";

       }

       MessageBox.Show(Products, "try...catch...finally demo");

       }

  catch (Exception e)

  {

       // Log the Exception

       ExceptionLog.Log(e);

  }


  finally

  {

       // Close the data reader and connection

       if (dr != null)

       {

          dr.Close();

       }

       Conn.Close();

  }

}

And in Visual Basic .NET:

Public Sub TryCatchDemo()

    Dim dr As SqlDataReader = Nothing

 

    ' Create the connection string and configure

    ' the connection and command objects

    Dim ConnectString As String = _

        "server=(local);uid=sa;pwd=;database=NorthWind;"

    Dim Conn As New SqlConnection(ConnectString)

    Dim Cmd As New SqlCommand("Ten Most Expensive Products", Conn)

    Cmd.CommandType = CommandType.StoredProcedure

 

    Try

        ' Open the connection and execute the stored procedure

        Conn.Open()

        dr = Cmd.ExecuteReader()

 

        ' Get all records in the result set

        Dim Products As String = ""

        While dr.Read()

            ' Access the data in the current row

            Products += dr.GetString(0) + ", " & _

                dr.GetDecimal(1).ToString() & ControlChars.Lf

        End While

        MessageBox.Show(Products, "try...catch...finally demo")

 

    Catch e As Exception

        ' Log the Exception

        ExceptionLog.LogError(e)

    Finally

        ' Close the data reader and connection

        If Not (dr Is Nothing) Then

            dr.Close()

        End If

        Conn.Close()

    End Try

End Sub 'TryCatchDemo

There are several lines of code at the top of the method outside of the try…catch…finally blocks. These lines of code are not trouble spots, so they can safely reside outside of the try block. Within the try block is code with the potential for error. This is where a connection to SQL Server is opened, the stored procedure is executed, and the result set is processed.

If everything goes smoothly

If no errors are encountered in the try block, the finally block is executed, which performs cleanup by closing the data reader and the connection object.

If an error occurs

What if an error occurs (for example, the specified stored procedure doesn’t exist) on the line of code that executes the stored procedure?

In C#:

       dr = Cmd.ExecuteReader();

In Visual Basic .NET:

       dr = Cmd.ExecuteReader()

When the error occurs, control is immediately passed to the catch block. Remember the CLR instantiates an exception object when an error occurs—this is also known as “throwing an exception”. The catch block is the mechanism the CLR uses to pass a reference to this object. If you look at the code, you’ll see the catch block declares a parameter of the type “Exception” named “e”.

In C#:

  catch (Exception e)

In Visual Basic .NET:

  Catch e As Exception

This is how the CLR passes the exception object to the catch block.

Within the catch block is a single line of code that passes the exception object reference to the static Log method of the ExceptionLog class (one of your custom sample classes). This method logs the error for future reference. Check out the “Logging Errors” section later in this chapter for information on extracting and logging error information from this exception object.

Text Box: Ą

In reality, if a critical error occurred during an operation similar to the one in the sample, you probably wouldn’t catch the error at this level. Depending on the context where the code is called, you may want to let the client code catch the error instead and respond accordingly.

Remember, if an exception is thrown, the finally method is still executed. This means the cleanup code that closes the data reader and connection object is executed after the catch block is run. This ensures you don’t leave a connection to the database open. Notice the code performs a check to see if the data reader object dr is null.


In C#:

  finally

  {

       // Close the data reader and connection

       if (dr != null)

       {

          dr.Close();

       }

       Conn.Close();

  }

And in Visual Basic .NET:

    Finally

        ' Close the data reader and connection

        If Not (dr Is Nothing) Then

            dr.Close()

        End If

        Conn.Close()

This check is necessary, because if an error occurs while executing the stored procedure, the data reader may not be returned and the variable may be null.

After the finally block is executed, control is passed to the end of the finally block. This is what you want, because if the stored procedure did not execute correctly, you don’t want to continue processing. This is different from Visual FoxPro 7, where the Error event executes and control is passed back to the line immediately following the code that generated an error.

Text Box: Ą

Visual FoxPro 8 introduces Try…Catch structured exception handling to the FoxPro language. For details, see the VFP 8 Help file.

Catching specific exceptions

The previous example shows how you can use the catch block to catch all exceptions. However, there may be times where you would like to catch specific exceptions and handle them differently. The following code sample shows how to do this.

In C#:

public void CatchOverFlow()

{

long MyLong = 3000000000;

int MyInteger;

 


try

{

   // Store the value of MyDouble into MyInteger

   checked

   {

     MyInteger = (int)MyLong;

   }

}

catch(OverflowException e)

{

   // Do additional processing…

   MessageBox.Show("Caught an arithmetic overflow exception.", "Catch demo");

 

   // Catch a specific exception

   ExceptionLog.LogError(e);

}

catch(Exception e)

{

   // Catch the generic exceptions

   ExceptionLog.LogError(e);

}

finally

{

   // Cleanup code

}

}

In Visual Basic .NET:

Public Sub CatchOverFlow()

    Dim MyLong As Long = 3000000000

    Dim MyInteger As Integer

 

    Try

        ' Store the value of MyDouble into MyInteger

        MyInteger = CInt(MyLong)

 

    Catch e As OverflowException

        ' Do additional processing

        MessageBox.Show("Caught an arithmetic overflow exception.", "Catch demo")

 

        ' Catch a specific exception

        ExceptionLog.LogError(e)

 

    Catch e As Exception

        ' Catch the generic exceptions

        ExceptionLog.LogError(e)

 

    Finally

        ' Cleanup code

 

    End Try

 

End Sub 'CatchOverFlow

The code within the try block takes the value of MyLong and casts it to an integer. However, since the largest positive value an integer can hold is a 2,147,483,647, this code generates an exception.

Notice the C# code contains a checked block surrounding the code that stores the value of MyLong into MyInteger, but the Visual Basic .NET code does not:

checked

{

  MyInteger = (int)MyLong;

}

In C#, an arithmetic overflow condition does not automatically cause an exception to be thrown. If you want an exception to be thrown, you need to surround the code with a checked block. In Visual Basic .NET, you don’t have a choice—an exception is always thrown when an overflow condition is encountered. Alternately, you can tell the C# compiler you want all code checked by using the “/checked” compiler option.

Notice there are two catch blocks in this code. The first catch block is specifically checking for an OverflowException error and the second block is checking for a generic Exception. If code in the try block generates an exception other than an OverflowException, it is caught by the second catch block.

The OverflowException catch block has a placeholder for “Do additional processing”. In the real world, you could change the default exception message to something more informative. The technique for doing this is discussed later in this chapter under the “System.Exception” section.

Catching all exceptions

If an exception is not caught in a catch block, the Common Language Runtime catches it
for you. If this happens, an un-user friendly message is displayed and your program terminates. The best way to avoid a situation where your code does not catch an error is to
use nested try blocks.

Nested try blocks

Nested try blocks allow you to bracket one try block with another that sits at a higher level. Here is some sample code demonstrating how this works.

In C#:

public void NestedTry()

{

  try

  {

       try

       {

          int x;

          int y = 0;

          // Divide 100 by 0 (throws an exception)

          x = 100 / y;

       }

       catch (OverflowException e)


       {

          ExceptionLog.LogError(e);

       }

       finally

       {

          // cleanup

       }

  }

  catch (Exception e)

  {

       ExceptionLog.LogError(e);

  }

  finally

  {

       // cleanup

  }

}

And in Visual Basic .NET:

Public Sub NestedTry()

    Try

        Try

            Dim x As Integer

            Dim y As Integer = 0

            ' Divide 100 by 0 (throws an exception)

            x = CInt(100 / y)

        Catch e As OverflowException

            ExceptionLog.LogError(e)

        Finally

            ' cleanup

            MessageBox.Show("Running the inner finally block")

        End Try

    Catch e As Exception

        ExceptionLog.LogError(e)

    Finally

       ' cleanup

    End Try

End Sub 'NestedTry

The inner try block contains code that divides 100 by the variable y, which contains the value zero. Running this code throws a DivideByZeroException. Notice, however, the inner catch block only checks for an OverflowException. In this situation, the .NET runtime determines there is no catch block for the exception that was thrown and does the following:

1.       Runs the inner finally block associated with the inner catch.

2.       Searches for a suitable catch block further up the stack.

3.       When it finds the outer catch block that checks for any exception, it executes the code within it.

4.       Executes the outer finally block.

Although this sample code nests try blocks within a single method, you can nest them
at any level in your application calling chain. In fact, it’s a good idea to bracket your entire application in a try block as a final check for any unhandled exceptions within
your application.

For example, in C#:

static void Main()

{

   try

   {

       Application.Run(new MainAppWindow());

   }

   catch (Exception e)

   {

       ExceptionLog.LogError(e);

   }

   finally

   {

       // Cleanup

   }

}

And in Visual Basic .NET:

Shared Sub Main()

    Try

        Application.Run(New MainAppWindow())

    Catch e As Exception

        ExceptionDisplay.ShowError(e)

        ExceptionLog.LogError(e)

    Finally

        ' cleanup

    End Try

End Sub

 With your application bracketed this way, you can be sure that all uncaught exceptions are intercepted and logged. However, if an exception is not caught until it gets to this point, the application stops executing. In contrast, if you catch an exception at the point it occurs
(or some layer between the actual error and the topmost try…catch…finally block),
then application execution passes to the associated finally block and application
execution continues.

At times, you may not know how to handle an exception at the level where the error occurs. For example, you may have a library of classes used by a variety of applications. Because you don’t know the context where a particular class is used, you often don’t know how to properly respond to exceptions. In cases like this, you should allow the calling code to handle any exceptions that may occur. However, you may still want to catch generic exceptions and throw your own custom exception objects to provide the calling code with more specific information.

 


Throwing your own exceptions

Not all exceptions are automatically generated by the .NET runtime. Some are the result of perfectly valid .NET code that simply won’t work. For example, the contents of a file may have become corrupt, a particular file may not exist, or it may be locked. If you detect error conditions within your application, you can manually throw exceptions from within your code.

For example, the following class contains a method called IsBirthMonth, which accepts an integer and determines if it is my birth month (October). The code checks to see if the value of the month parameter is between 1 and 12. If it’s not, an exception is thrown—specifically an ArgumentOutOfRangeException, which is one of the .NET Framework’s exception classes.

In C#:

public class ThrowExceptionDemo

{

  public bool IsBirthMonth(int month)

  {

    bool BirthMonth = false;

 

    if (! (month >= 1 && month <= 12))

    {

       throw new ArgumentOutOfRangeException();

    }

 

    if (month == 10)

    {

       BirthMonth = true;

    }

 

    return BirthMonth;

  }

}

In Visual Basic .NET:

Public Class ThrowExceptionDemo

  

   Public Function IsBirthMonth(month As Integer) As Boolean

      Dim BirthMonth As Boolean = False

     

      If Not(month >= 1 And month <= 12) Then

         Throw New ArgumentOutOfRangeException()

      End If

     

      If month = 10 Then

         BirthMonth = True

      End If

     

      Return BirthMonth

   End Function 'IsBirthMonth

End Class 'ThrowExceptionDemo

If you have an application-level try…catch…finally block in place, the block would catch this exception.

Displaying and Logging Errors

Up to this point I’ve glossed over how to respond to exceptions that are caught by simply calling ExceptionLog.LogError. In this section I’ll show you how to retrieve error information from an Exception object and display or log the error. Before doing this, first take a closer look at the System.Exception class.

System.Exception

As mentioned previously, all exception classes—both the .NET Framework classes and your own custom exceptions—are derived from the System.Exception class. Table 2 contains a list of the Exception class’s public properties with a brief description of each.

Table 2. System.Exception properties

Property

Description

HelpLink

Specifies a Uniform Resource Name (URN) or Uniform Resource Locater (URL) link to the Help file associated with the exception.

Example: “file:///C:/Applications/MyHelp.html#MyError1

Message

The text of the error message.

Source

Specifies the name of the object that caused the error.

TargetSite

Specifies the method that threw the exception.

StackTrace

Contains a string detailing the call stack at the time the exception occurred.

InnerException

Specifies a previous exception that caused the current exception. If there is no previous exception, this property is null.

 

One of the most important properties of the Exception object is Message. This property contains the text of the error message. The .NET runtime supplies a default message that is stored in this property. For example, the default message displayed when the IsBirthMonth method throws its exception is:

"Specified argument was out of the range of valid values."

You can override this default message by specifying a different message in the constructor of the exception class. The ArgumentOutOfRangeException class has four different constructors you can choose. The constructor signature I’ve selected in this example expects three parameters:

·         Parameter Name

·         Actual Value

·         Message

Here’s the modified code in C#:

throw new ArgumentOutOfRangeException("month",

  month,"Values for month must be between 1-12");


And in Visual Basic .NET:

Throw New ArgumentOutOfRangeException("month", _

    month, "Values for month must be between 1-12")

Now when this exception is thrown, the default message is:

" Values for month must be between 1-12."

This is much more descriptive than the generic “argument out of range” message.

Displaying exception information

To see the value of an exception object’s message property (and other properties), the following sample code uses a class with a static method named “ShowError”.

In C#:

public class ExceptionDisplay

{

  public static void ShowError(Exception e)

  {

       MessageBox.Show("Error information\n\n" +

          "Message: " + e.Message + "\n" +

          "Source: " + e.Source + "\n" +

          "TargetSite: " + e.TargetSite + "\n" +

          "Stack trace: " + e.StackTrace,

          "Exception demo");

  }

}

And in Visual Basic .NET:

Public Class ExceptionDisplay

 

    Public Shared Sub ShowError(ByVal e As Exception)

        MessageBox.Show("Message: " & e.Message & ControlChars.Lf & _

            ControlChars.Lf & "Source: " & e.Source & ControlChars.Lf & _

            ControlChars.Lf & "TargetSite: " & e.TargetSite.ToString() & _

            ControlChars.Lf & ControlChars.Lf & "Stack trace: " & _

            ControlChars.Lf & e.StackTrace, "Exception demo")

    End Sub 'ShowError

 

End Class 'ExceptionDisplay

Here is a modified version of the IsBirthMonth method containing a catch block that calls this new static method.


In C#:

public bool IsBirthMonth(int month)

{

  bool BirthMonth = false;

 

  try

  {

    if (! (month >= 1 && month <= 12))

    {

       throw new ArgumentOutOfRangeException("month",

          month,"Values for month must be between 1-12");

    }

  }

 

  catch (ArgumentOutOfRangeException e)

  {

    ExceptionDisplay.ShowError(e);

  }

 

  finally

  {

    if (month == 10)

    {

       BirthMonth = true;

    }

  }

  return BirthMonth;

}

And in Visual Basic .NET:

Public Function IsBirthMonth(ByVal month As Integer) As Boolean

    Dim BirthMonth As Boolean = False

 

    Try

        If Not (month >= 1 And month <= 12) Then

            Throw New ArgumentOutOfRangeException("month", _

                month, "Values for month must be between 1-12")

        End If

 

    Catch e As ArgumentOutOfRangeException

        ExceptionDisplay.ShowError(e)

        ExceptionLog.LogError(e)

 

    Finally

        If month = 10 Then

            BirthMonth = True

        End If

    End Try

    Return BirthMonth

End Function 'IsBirthMonth

If you run this code from the book’s sample application, you’ll see the message box shown in Figure 2.

Figure 2. Important information can be gleaned from the properties of an exception object.

As you can see, the Message property contains a three-part string that includes the error message, the parameter name, and the actual value, each shown on their own line.

If you do not set the Source property, it defaults to the name of the assembly where the error occurred—in this case, “HW .Net Book Samples_CSharp”, or if you’re using Visual Basic .NET, “HW .Net Book Samples_Visual Basic”.

The TargetSite property contains a string specifying the method that threw the exception, in this case “IsBirthMonth”. As shown, it also includes the type of the parameter and the type of the return value.

The StackTrace property provides information regarding the methods in execution at the time the exception is thrown. As seen in Figure 2, the example only lists a single class and method—the ThrowExceptionDemo class and the IsBirthMonth method. It also lists the source code file and line number where the error was thrown.

Logging exception information

Typically, you should create a way to save exception information to some type of log file. In this section, you’ll see a few easy ways to do this.

Figure 3 contains a UML class diagram of an ExceptionLog class. This is a custom class found in this chapter’s sample code that allows you to save exception information to an XML log file. Saving exceptions as XML gives you the ability to easily read and manipulate the exception data using either .NET run time or design time tools.

Figure 3. The ExceptionLog class found in this chapter’s sample code provides a means to save errors to a log file.

The ExceptionLog’s LogFile property specifies the name of the XML log file. The ApplicationName property identifies the name of the application that generated the exception. Specifying the name of the application lets you save exception data from multiple applications into a single exception log.

The ExceptionLog’s CreateLogFile method creates a new log file with the name given in the LogFile property.

Here is the code for this method in C#:

public static void CreateLogFile()

{

  // Create an XmlTextWriter, specifying the name of the new XML log file

  XmlTextWriter xtw = new XmlTextWriter(LogFile, null);

 

  // Write the XML declaration at the top of the file

  xtw.WriteStartDocument();

 

  // Add a comment to the file indicating the date/time created

  xtw.WriteComment("Log file created: " + DateTime.Now);

 

  // Add an empty <EventLog> element

  xtw.WriteStartElement("EventLog");

  xtw.WriteEndElement();

 

  // Close the stream

  xtw.Close();

}

And in Visual Basic .NET:

Public Shared Sub CreateLogFile()

    ' Create an XmlTextWriter, specifying the name of the new XML log file

    Dim xtw As New XmlTextWriter(LogFile, Nothing)

 

    ' Write the XML declaration at the top of the file

    xtw.WriteStartDocument()

 

    ' Add a comment to the file indicating the date/time created

    xtw.WriteComment(("Log file created: " + DateTime.Now.ToString()))

 

    ' Add an empty <EventLog> element

    xtw.WriteStartElement("EventLog")

    xtw.WriteEndElement()

 

    ' Close the stream

    xtw.Close()

 

End Sub 'CreateLogFile

This method uses an XmlTextWriter to create the new XML log file, and then writes:

·          The XML declaration at the top of the file.

·         A comment indicating the date and time the file was created.

·         An empty <EventLog> element.

Text Box: ĄFor more information about using the XmlTextWriter class and other XML classes referenced in this chapter, see Chapter 11, “.NET XML”.

Now take a look at the code in the ExceptionLog’s LogError method.

In C#:

public static void LogError(Exception e)

{

  // If the log file doesn't exist, create it

  if (!File.Exists(LogFile))

  {

       CreateLogFile();

  }

 

  // Open the log file

  XmlDocument XmlDoc = new XmlDocument();

  XmlDoc.Load(LogFile);

 

  // Create a new Exception element

  XmlElement ExceptionEntry = XmlDoc.CreateElement("Exception");

 

  // Create a new DateTime element

  XmlElement DateTimeChild = XmlDoc.CreateElement("DateTime");

  DateTimeChild.InnerText = DateTime.Now.ToString();

  ExceptionEntry.AppendChild(DateTimeChild);

 

  // Create an Application element

  XmlElement ApplicationChild = XmlDoc.CreateElement("Application");

  ApplicationChild.InnerText = ApplicationName;

  ExceptionEntry.AppendChild(ApplicationChild);

 

  // Create a new Message child element

  XmlElement MessageChild = XmlDoc.CreateElement("Message");

  MessageChild.InnerText = e.Message;

  ExceptionEntry.AppendChild(MessageChild);

 

  // Create a new Source child element

  XmlElement SourceChild = XmlDoc.CreateElement("Source");

  SourceChild.InnerText = e.Source;

  ExceptionEntry.AppendChild(SourceChild);

 

  // Create a new TargetSite child element

  XmlElement TargetSiteChild = XmlDoc.CreateElement("TargetSite");

  TargetSiteChild.InnerText = e.TargetSite.ToString();

  ExceptionEntry.AppendChild(TargetSiteChild);

 

  // Create a new Stacktrace child element

  XmlElement StackTraceChild = XmlDoc.CreateElement("StackTrace");

  StackTraceChild.InnerText = e.StackTrace;

  ExceptionEntry.AppendChild(StackTraceChild);

 

  // Add the entire ExceptionEntry to the XML document

  XmlDoc.DocumentElement.AppendChild(ExceptionEntry);

 


  // Write out the updated XML file

  XmlTextWriter xtw = new XmlTextWriter(LogFile, null);

  xtw.Formatting = Formatting.Indented;

  XmlDoc.WriteContentTo(xtw);

  xtw.Close();

}

And in Visual Basic .NET:

Public Shared Sub LogError(ByVal e As Exception)

    ' If the log file doesn't exist, create it

    If Not File.Exists(LogFile) Then

        CreateLogFile()

    End If

 

    ' Open the log file

    Dim XmlDoc As New XmlDocument()

    XmlDoc.Load(LogFile)

 

    ' Create a new Exception element

    Dim ExceptionEntry As XmlElement = XmlDoc.CreateElement("Exception")

 

    ' Create a new DateTime element

    Dim DateTimeChild As XmlElement = XmlDoc.CreateElement("DateTime")

    DateTimeChild.InnerText = DateTime.Now.ToString()

    ExceptionEntry.AppendChild(DateTimeChild)

 

    ' Create an Application element

    Dim ApplicationChild As XmlElement = XmlDoc.CreateElement("Application")

    ApplicationChild.InnerText = ApplicationName

    ExceptionEntry.AppendChild(ApplicationChild)

 

    ' Create a new Message child element

    Dim MessageChild As XmlElement = XmlDoc.CreateElement("Message")

    MessageChild.InnerText = e.Message

    ExceptionEntry.AppendChild(MessageChild)

 

    ' Create a new Source child element

    Dim SourceChild As XmlElement = XmlDoc.CreateElement("Source")

    SourceChild.InnerText = e.Source

    ExceptionEntry.AppendChild(SourceChild)

 

    ' Create a new TargetSite child element

    Dim TargetSiteChild As XmlElement = XmlDoc.CreateElement("TargetSite")

    TargetSiteChild.InnerText = e.TargetSite.ToString()

    ExceptionEntry.AppendChild(TargetSiteChild)

 

    ' Create a new Stacktrace child element

    Dim StackTraceChild As XmlElement = XmlDoc.CreateElement("StackTrace")

    StackTraceChild.InnerText = e.StackTrace

    ExceptionEntry.AppendChild(StackTraceChild)

 

    ' Add the entire ExceptionEntry to the XML document

    XmlDoc.DocumentElement.AppendChild(ExceptionEntry)

 


    ' Write out the updated XML file

    Dim xtw As New XmlTextWriter(LogFile, Nothing)

    xtw.Formatting = Formatting.Indented

    XmlDoc.WriteContentTo(xtw)

    xtw.Close()

 

End Sub 'LogError

This method first checks to see if the exception log file exists. If the file doesn’t exist, it makes a call to the CreateLogFile method, which creates a new file. Next, it opens the log file in an instance of the XmlDocument class and creates a new <Exception> element. Afterwards, it adds DateTime, Application, Message, Source, TargetSite, and StackTrace child elements. Finally, it writes out the log file using an XmlTextWriter object in conjunction with the XmlDocument.WriteContentTo method.

Here’s an example of a single entry in the XML Exception log file:

<?xml version="1.0"?>

<!--Log file created: 6/30/2002 9:59:50 AM-->

  <Exception>

    <DateTime>6/30/2002 11:22:03 AM</DateTime>

    <Application>HW .NET Sample app</Application>

    <Message>Values for month must be between 1-12

Parameter name: month

Actual value was 13.</Message>

    <Source>HW .NET Book Samples_CSharp</Source>

    <TargetSite>Boolean IsBirthMonth(Int32)</TargetSite>

    <StackTrace>   at HW.NetBook.Samples.ThrowExceptionDemo.IsBirthMonth(Int32 month) in c:\net code sample\hw .net book samples_csharp\chapter13.cs:line 139</StackTrace>

  </Exception>

</EventLog>

In a real-world application, you may prefer to write your errors to a Windows 2000 event log on a local or remote computer rather than to an XML log file. One advantage of doing this is being able to use the Windows Event Viewer to view, search, archive, export, and maintain event logs. Check out the .NET Help topic “Logging Application, Server, and Security Events” for information on how to do this.

Creating your own custom exception classes

If there is an existing .NET Framework exception class that fits your needs, Microsoft recommends you use that class when throwing your own exceptions. For example, in the ThrowException demo class earlier in this chapter, the IsBirthMonth method used the .NET Framework’s ArgumentOutOfRangeException class to indicate if an invalid month was passed to the method. This class was used because it perfectly suited the situation. However, if you run into a situation where there isn’t an existing class that suits your needs, the .NET Framework provides the System.ApplicationException class specifically designed for the purpose of allowing you to create your own custom exception classes.

Deriving exceptions from the .NET System.ApplicationException class gives you a way to distinguish between exceptions defined in the .NET Framework and custom exceptions you define. ApplicationException has four different overloaded constructor methods as shown in Table 3.

Table 3. ApplicationException constructor methods

Method

Description

public ApplicationException()

Simple constructor

public ApplicationException(string);

Constructor with error message

public ApplicationException(string, Exception)

Constructor with an error message and a reference to the inner exception that caused the original exception

public ApplicationException(SerializationIfnfo, StreamingContext);

Deserialization constructor

 

The .NET Help topic, “Best Practices for handling exceptions”, states that when you create your own exception classes you should implement at least the first three constructors shown in Table 3. The fourth constructor is a deserialization constructor and is optional. Implementing this constructor allows your custom exceptions to be passed from one machine to another. In this chapter, you will only implement the first three. For information on implementing the serialization constructor, see the on-line article by Eric Gunnerson of Microsoft at the following URL:

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp08162001.asp

The following sample code demonstrates defining a custom exception class derived from ApplicationException.

Text Box: Ą

Microsoft recommends you always end exception class names with the word “Exception” as they have done in the .NET Framework. This provides consistency and makes it easy for others to identify your exception classes.

In C#:

public class ConfigurationFileNotFoundException : ApplicationException

{

  public ConfigurationFileNotFoundException()

  {

  }

 

  public ConfigurationFileNotFoundException(string message)

       : base(message)

  {

  }

 

  public ConfigurationFileNotFoundException(string message, Exception inner)

       : base(message, inner)

  {

  }

}

In Visual Basic .NET:

Public Class ConfigurationFileNotFoundException

    Inherits ApplicationException

 

    Public Sub New()

    End Sub 'New

 

 

    Public Sub New(ByVal message As String)

        MyBase.New(message)

    End Sub 'New

 

 

    Public Sub New(ByVal message As String, ByVal inner As Exception)

        MyBase.New(message, inner)

    End Sub 'New

End Class 'ConfigurationFileNotFoundException

Here is an example of code that throws this custom exception. It checks if a particular configuration file exists and if it doesn’t, the ConfigurationFileNotFoundException is thrown.

In C#:

public class CustomExceptionDemo

{

  public void OpenDbcConfigFile()

  {

    if (!File.Exists("mmconfigx.xml"))

    {

      throw new ConfigurationFileNotFoundException("Application configuration file not found.");

    }

  }

}

And in Visual Basic .NET:

Public Class CustomExceptionDemo

 

    Public Sub OpenAppConfigFile()

        If Not File.Exists("mmconfigx.xml") Then

            Throw New ConfigurationFileNotFoundException( _

                "Application configuration file not found.")

        End If

    End Sub 'OpenAppConfigFile

 

End Class 'CustomExceptionDemo

Again, if you can find an existing .NET exception class that suits your needs use it. Otherwise, feel free to create your own custom exceptions based on the ApplicationException class.

 

Text Box: ĄThroughout this chapter, there are several tips for exception handling “best practices”. In addition to these suggestions, I highly recommend the .NET Help topic “Best Practices for Handling Exceptions”.

Debugging your application

The rest of this chapter is devoted to the variety of tools available in Visual Studio .NET for debugging your applications. I’ll take a look at each of these tools and show you the basics of how to use each.

Setting and clearing breakpoints

One of the most important debugging tools is the ability to set breakpoints in your application. Visual Studio .NET makes it easy to set breakpoints and step through the code in your application. You’ll find .NET’s approach to breakpoints is similar to Visual FoxPro’s, but with some additional features.

The easiest way to set a breakpoint is clicking in the left column of the code-editing window next to the line where you want to break (Figure 4). You can easily clear the breakpoint by clicking again in the left column at the same location.

Figure 4. Clicking in the left column of the code-editing window allows you to easily set and clear breakpoints.

Another easy way to set a breakpoint is to right-click on a line of code and select Insert Breakpoint or New Breakpoint from the shortcut menu.

Using the Breakpoints window

The Breakpoints window contains a list of all breakpoints currently set in your application (Figure 5). To launch the Breakpoints window, select Debug | Windows | Breakpoints from the main menu or type Ctrl+Alt+B.

Figure 5. The Breakpoints window shows you all the breakpoints currently set in
your application.

By default, there are three columns displayed in the Breakpoints window—Name, Condition, and Hit Count. You can specify additional or different columns by clicking the Columns button at the top of the Breakpoints window and selecting or deselecting columns from the list. Notice the name column includes the source code file name, the line number and the character position. Although you don’t normally care about the character position, you do care if you have multiple commands on a single line and want to know which command the breakpoint refers to.

You can enable or disable a breakpoint by checking or clearing the check box in the Name column. To view the source code line associated with a breakpoint, double-click on the breakpoint or right-click on the breakpoint and select Go To Source Code from the shortcut menu. You can also clear all breakpoints or disable all breakpoints by clicking the corresponding buttons at the top of the Breakpoints window.

Creating new breakpoints

To add a new breakpoint using the Breakpoints window, click the New button at the top of the window to launch the New Breakpoint dialog (Figure 6).

The New Breakpoint dialog allows you to set breakpoints:

·         When program execution reaches a specified location in a function.

·         When program execution reaches a specified line number in a file.

·         When program execution reaches the instruction at the specified address.

·         When the value of a variable changes.

For additional information on each of these options, click the Help button on the corresponding page of the New Breakpoint dialog.

Figure 6. You can create new breakpoints for a function, file, address, or data (variables) using the New Breakpoint dialog.

If you click the Condition button, it launches a Breakpoint Condition dialog (Figure 7) allowing you to specify a break that occurs when a condition is true or has changed.

Figure 7. The Breakpoint Condition dialog allows you to specify a condition that is true or has changed before a breakpoint is hit.

If you click the Hit Count… button, it launches the Breakpoint Hit Count dialog (Figure 8). This dialog lets you specify how many times a breakpoint is hit before program execution breaks. You can set a specific count, a multiple of a count, or a hit count greater than or equal to a specified value.

Figure 8. The Breakpoint Hit Count dialog lets you specify a count for the number of times a breakpoint is hit before program execution breaks.

Editing existing breakpoints

To edit an existing breakpoint, select the breakpoint you want to edit, and click the Properties button (the button to the far right) at the top of the Breakpoints window. This launches the same dialog described when creating a new breakpoint. You can also right-click a breakpoint and select Properties from the shortcut menu.

Deleting existing breakpoints

To delete an existing breakpoint, select the breakpoint you want to delete, and click the Delete button (the second from the left) at the top of the Breakpoints window. You can also right-click on a breakpoint and select Delete from the shortcut menu.

Ignoring breakpoints

If you want to start your application but ignore all existing breakpoints, you can do so by pressing Ctrl+F5 or by selecting Debug | Start Without Debugging from the main menu.

Navigating through code in the debugger

When you run your application and hit a breakpoint, Visual Studio .NET automatically opens the associated source code file, highlights the line where the break occurred, and displays an arrow in the left column of the code editing window (Figure 9).

Figure 9. VS .NET shows you the line of code where a break occurs.

At this point, you can choose Step Into, Step Over, or Step Out from the Debug menu to navigate through your code. You can also use the shortcut keys, which are F11 (Step Into), F10 (Step Over), and Shift+F10 (Step Out). These have the same meaning as in the Visual FoxPro debugger.

To navigate to a different line, possibly even a line earlier in the stack, right-click on the line and select Set Next Statement. This feature is invaluable when you’re debugging your application and want to rerun or completely skip over code.

If you page up or down in the source code so you can’t see the stack pointer arrow in the left column, or if you’re editing another source code file and want to go back to the next line to be executed, simply right-click in the code-editing window and select Show Next Statement from the shortcut menu.

Another option that’s extremely handy is the ability to continue program execution up to a given line and then break—but without setting an actual breakpoint. To do this, right-click on the line of code you want to stop at, and select Run to Cursor from the shortcut menu. This causes application execution to continue until it hits the selected line, at which point it breaks again.

To resume normal program execution after stopping for a breakpoint, you can simply press F5 or click the Continue button (Figure 10) at the top of the Visual Studio .NET IDE.

Figure 10. Click Continue to resume program execution after hitting a breakpoint.

Examining values in the Watch window

When your program is running, you can use the Watch window to view the values of variables and expressions, as well as edit the value of a variable. The Watch window in VS .NET is similar to Visual FoxPro’s, but with some additional features.

To launch the Watch window, you must first run your application and be in break mode. Once your application is running, from the Debug menu choose Windows | Watch, and select Watch1, Watch2, Watch3, or Watch4. If you’ve ever filled up your Watch window in Visual FoxPro, you’ll be glad to see four different Watch windows in VS .NET you can use to avoid the clutter of too many watch values in a single window.

As with Visual FoxPro, the VS .NET Watch windows persist their values from one VS .NET session to another. This means if you specify a Watch value, close Visual Studio .NET, and restart, your values still show up in the Watch window.

Although you can manually type variables or expressions into a Watch window, you can also easily add watch values by dragging and dropping selected text in the code-editing window into a Watch window. For example, if you set a breakpoint in this chapter’s sample code (Chapter13.cs or Chapter13.vb) in the ThrowExceptionDemo.IsBirthMonth method at the line of code that calls ExceptionLog.LogError, you can drag the variable “e” from the source code, and drop it into a Watch window. If you do this, the variable “e” is displayed in the Watch window (Figure 11) along with its associated value and type. If you want to change the value of the variable “e”, simply click on the text in the Value column and enter a new value.

Figure 11. You can examine the values of variables and expressions and change the values of variables in one of VS .NET’s four Watch windows.

Notice there is a plus sign in the Name column next to the variable “e”. Whenever you enter the name of an object variable or array into the Watch window, a tree control appears next to the variable name. You expand and contract this tree control to show or hide additional values (such as properties) of the object.

In Visual FoxPro, you can double-click in the column to the left of a Watch value to specify program execution to break when the value of the variable or expression changes. In VS .NET, you do this through the Breakpoints window (see the “Using the Breakpoints window” section earlier in this chapter for details).

The QuickWatch window

Visual Studio .NET also has a QuickWatch window (Figure 12) you can use as an alternate to the Watch windows if you want to view or edit a variable or expression quickly. QuickWatch is a modal dialog, so if you want to look at a value and step through code to see how the value changes, you need to use the Watch window instead.

Figure 12. The QuickWatch window allows you to easily view variables and expressions and edit variable values.

To launch the QuickWatch window, the debugger must be in break mode, and then select Debug | QuickWatch from the menu or press Ctrl+Alt+Q.

When you click the Recalculate button, it evaluates the variable or expression you have entered in the Expression text box and displays it in the Current value. If you click the Add Watch button, the expression or variable is automatically added to the Watch window.

The Command window

Expecting Visual Studio .NET Command Window to be similar to Visual FoxPro’s Command Window, is like renting a Bruce Willis action-adventure DVD and finding out they’ve accidentally put “Pee-Wee’s Big Adventure” in the box instead! Unfortunately, the VS .NET Command Window is nothing in comparison to Visual FoxPro’s Command window. Take a closer look at its functionality (or lack thereof) and you’ll see what I mean.

The Command Window (Figure 13) is used to issue commands or to debug and evaluate expressions. To launch the Command Window, select View | Other Windows | Command Window from the main menu or simply press Ctrl+Alt+A.

Figure 13. The Visual Studio .NET Command Window has a long way to go to be as capable as the Visual FoxPro Command Window.

There are two different Command Window modes—Command mode and Immediate mode. You know you’re in Command mode if the greater than symbol appears as a command prompt. You know you’re in Immediate mode if the title of the window is Command Window – Immediate. If you’re in Command mode, you switch to Immediate mode by typing “immed” and pressing Enter. If you’re in Immediate mode you go to Command mode by entering “>cmd” in the Command Window.

Command mode

When working in Command mode, you can enter IDE commands directly into the Command Window, bypassing the VS .NET menu system. If you’re expecting to instantiate objects from the Command Window you’ll be sadly disappointed—this capability is non-existent.

There are a number of predefined command aliases you can enter in the Command Window to save time. For example, if you enter “nav” in Command mode, it automatically brings up the VS .NET browser in the IDE. If you enter the letter “k” in Command mode, it displays the call stack. If you want to evaluate expressions while in Command mode, you use the eval command. For example, entering the following command displays a string containing the current date and time:

eval DateTime.Today.ToString()

For a list of all pre-defined VS .NET command aliases, see the .NET Help topic “Pre-defined Visual Studio Command Aliases”. You can also type “alias” in the Command Window and it shows you all of the aliased commands. For more information on Visual Studio commands and instructions on creating your own aliases, see the .NET Help topic “Visual Studio Commands”.

Immediate mode

The Immediate mode of the Command Window allows you to issue commands, debug, and evaluate expressions, as well as view and change the values of variables while debugging.

For example, while debugging a program, you can enter the following command in Immediate mode to determine if the mmconfig.xml file exists in the current directory:

File.Exists("mmconfig.xml")

If the file exists, it displays “true” in the Command Window (Figure 14). Otherwise, it displays “false”.

Figure 14. You can enter commands to be executed in the Immediate mode of the Command window.

The Call Stack window

The Call Stack window (Figure 15) allows you to see functions on the call stack as well as any parameters and their values. For example, if you set a breakpoint in the sample code’s ThrowExceptionDemo.IsBirthMonth method on the line that calls ExceptionLog.LogError, the Call Stack window shows you the fully qualified class name, the method name, the type and value of the parameter (int month = 13), and the line number.

To see the Call Stack window, when your application is in Break mode, select Debug | Windows | Call Stack from the main menu or press Ctrl+Alt+C.

Figure 15. The Call Stack window allows you to see functions on the call stack as well as parameters and their values.

If you double-click on another function in the call stack, it highlights the associated source code in the code-editing window of the IDE (Figure 16).

Figure 16. Double-clicking on another function other than the next statement to be executed opens the associated source code in the IDE.

If you want to hide some of the information displayed, right-click on the Call Stack window and clear the checkbox next to the information you want to hide. For example, if you don’t want to see line numbers in the Call Stack window, clear the Show Line Numbers option.

The Autos window

In C#, the Autos window (Figure 17) displays variables used in the current statement or in the previous statement. The name “Autos” is derived from the fact that the debugger identifies these variables for you automatically. In Visual Basic .NET, the Autos window displays variables used in the current statement, as well as three statements before and after the current statement.

To launch the Autos window when your application is in Break mode, select Debug | Windows | Autos from the main menu or type Ctrl+Alt+V, A (press Ctrl+Alt+V, release, and press the letter “A”).

Figure 17. The Autos window shows variables used in the proximity of the current statement.

Note the variables in the Autos window change accordingly if you select a different function in the Call Stack window. You change the value of variables in the Autos window by double-clicking the Value column and entering the new value.

The Locals window

The Locals window (Figure 18) displays variables local to the current execution location.
To launch the Locals window, when your application is running, select Debug | Windows | Locals from the main menu or type Ctrl+Alt+V, L (press Ctrl+Alt+V, release, and press the letter “L”).

Figure 18. The Locals window displays all local variables visible in the current execution context and allows you to change their associated values.

As with other Debug windows, you change the value of variables in the Locals window by double-clicking the Value column and entering a new value. The variables displayed in the Locals window change accordingly if you select a different function in the Call Stack window.

The This/Me windows

The This/Me (Figure 19) windows allow you to view the members of the object associated with the current method. It’s called the This window in C# (and C++) and the Me window in VB .NET. To launch the window, when your application is running, select Debug | Windows | This (or Me) from the main menu or by typing Ctrl+Alt+V, T (press Ctrl+Alt+V, release, and press the letter “T”).

Figure 19. The This/Me windows allow you to view the members of the object associated with the current method.

The Modules window

The Modules window (Figure 20) provides detailed information about each DLL and EXE used by your program.

Figure 20. The Modules window provides a list of all DLLs and EXEs used by your program.

To view the Modules window, select Debug | Windows | Modules from the main menu, or type Ctrl+Alt+U. You click on any column to sort the items in the list.

Miscellaneous debug windows

There are a variety of other debug windows listed in Table 4 that are also available to you. For more information on each of these windows, check the .NET Help file.

Table 4. Miscellaneous debug windows

Method

Description

Memory window

Allows you to view large buffers, strings, and other data that do not display well in Watch or Variable windows.

Disassembly window

Allows you to view your source code in assembly language.

Registers window

Allows you to view the values stored in CPU registers.

Exceptions window

Allows you to change the way the debugger handles specific exceptions or categories of exceptions.

Threading window

Allows you to view and control threads in a multi-thread program.

 

Tracing and Instrumenting your applications

You can add tracing and debugging information to a .NET application that lets you monitor its execution while you’re developing the application and also after you’ve deployed it. The .NET Framework provides Trace and Debug classes with methods allowing you to get information about code coverage, performance profiling, and also allow you to create Assert statements, similar to ASSERTs in Visual FoxPro. These classes can be used in both .NET Windows applications as well as ASP.NET applications.

The Trace and Debug classes are the same except the procedures and functions of the Trace class are compiled into release builds of your application and those of the Debug class are not.

Code tracing and debugging

The Debug and Trace classes can be used during development to display messages in the Output window of Visual Studio .NET. For example, the following code uses the WriteLine method of the Debug and Trace classes.


In C#:

using system.diagnostics; 

 

public class DebugDemo

{

  public void MyMethod()

  {

       Debug.WriteLine("Debug.WriteLine output");

       Trace.WriteLine("Trace.WriteLine output");

  }

}

And in Visual Basic .NET:

Import System.Diagnostics 

 

Public Class DebugDemo

 

    Public Sub MyMethod()

        Debug.WriteLine("Debug.WriteLine output")

        Trace.WriteLine("Trace.WriteLine output")

    End Sub 'MyMethod

End Class 'DebugDemo

This code displays the messages “Debug.WriteLine output” and “Trace.WriteLine output” in the Output window (Figure 21).

Figure 21. The Debug and Trace WriteLine methods allow you to display messages in the VS .NET Output window.

When you compile a release build of your application, you can specify that you do not want to include debug information in your final executable. For more information, see the .NET Help topic “Compiling Conditionally with Trace and Debug”.

Trace listeners

When you are developing your application, all Debug and Trace information is written to the VS .NET Output window. However, in a deployed application, you need to specify an output target for this information called a trace listener.

Trace listeners come in the following flavors:


·         TextWriterTraceListener – Sends output to a TextWriter or other Stream class.

·         EventLogTraceListener – Sends output to an event log.

·         DefaultTraceListener (the default) – Sends output to Visual Studio .NET’s Output window.

The following code demonstrates how to implement a TextWriterTraceListener.

In C#:

// Creates the Trace Log text file (or open it if it already exists)

FileStream TraceLog = new FileStream("TraceLog.txt",

  FileMode.OpenOrCreate);

         

// Creates the Trace Listener

TextWriterTraceListener TextListener = new

  TextWriterTraceListener(TraceLog);

 

// Add the Listener to the Listeners collection

Trace.Listeners.Add(TextListener);

 

Debug.WriteLine("Debug.WriteLine TextWriterListener test: " +

  DateTime.Now);

Trace.WriteLine("Trace.WriteLine TextWriterListener test: " +

  DateTime.Now);

 

// Flush the output of the TextListener

TextListener.Flush();

         

// Remove the Listener from the Listeners collection

Trace.Listeners.Remove(TextListener);

In Visual Basic .NET:

' Creates the Trace Log text file (or open it if it already exists)

Dim TraceLog As New FileStream("TraceLog.txt", FileMode.OpenOrCreate)

 

' Creates the Trace Listener

Dim TextListener As New TextWriterTraceListener(TraceLog)

 

' Add the Listener to the Listeners collection

Trace.Listeners.Add(TextListener)

 

Debug.WriteLine(("Debug.WriteLine TextWriterListener test: " & _

    DateTime.Now.ToString()))

Trace.WriteLine(("Trace.WriteLine TextWriterListener test: " & _

    DateTime.Now.ToString()))

 

' Flush the output of the TextListener

TextListener.Flush()

 

' Remove the Listener from the Listeners collection

Trace.Listeners.Remove(TextListener)

 The first line of code creates a new FileStream for a text file named TraceLog.txt. The second parameter in the FileStream constructor uses the FileMode enumeration to indicate the TraceLog.txt file should be opened if it already exists or created if it does not. The second line of code creates a new TextWriterTraceListener object, passing a reference to the TraceLog FileStream. The third line adds the listener to the Trace object’s Listeners collection. Typically this initialization code occurs someplace in your application startup.

The next two lines of code write out Debug and Trace messages. In a real application, you can have any number of these calls to Debug or Trace throughout your application. These messages are written out to the TraceLog.txt file (Figure 22). In addition, they are still written to the VS .NET Output window. This is because VS .NET automatically adds a DefaultTraceListener to the Trace object’s Listener collection behind the scenes.

Figure 22. A TextWriterListener can output your Debug and Trace information to a text file.

The last two lines of code flush the TextListener and remove it from the Trace object’s Listener collection. You can do this at any point in the application when you want to stop writing messages to the trace log file.

Additional Debug and Trace methods

The Debug and Trace classes have additional methods you can use for writing output to listeners. Table 5 lists these different methods and how they are used.

Table 5. Miscellaneous debug windows

Method

Description

Assert

Checks for a condition and displays a message (or the call stack) if the condition is false.

Fail

Emits an error message.

Write

Writes information to the trace listeners in the Listener collection.

WriteIf

Writes information to the trace listeners in the Listener collection if a condition is true.

WriteLine

Writes information to the trace listeners in the Listener collection. Similar to the Write method, but writes out the message on a new line.

WriteLineIf

Writes information to the trace listeners in the Listener collection if a condition is true. Similar to WriteIf, but writes out the message on a new line.

 

For more information on using these methods, see the .NET Help topic “Adding Trace Statements to Application Code”.

Error handling in ASP.NET applications

When it comes to handling errors in ASP.NET applications, there are a few additional settings you should know about found in your application’s web.config file.

Debug mode

By default, when you create a new ASP.NET application, the web.config file Visual Studio .NET generates specifies that your application runs in debug mode. Here’s the debug setting contained within the compilation tag:

<compilation

     defaultLanguage="c#"

     debug="true"

/>

When debug mode is set to true, you get additional debugging information when your ASP.NET application encounters errors. Although setting this to true is useful for debugging, you also incur a huge performance penalty. So when you actually deploy your application, you should set debug to “false”.

Custom error messages

The web.config file also contains a setting that specifies the users who should see user-friendly messages and the users who should see error details that include a stack trace. This setting is found in the customErrors element:

<customErrors

mode="RemoteOnly"

/>

The three possible values for this setting are:

·         On – Always display user-friendly error messages.

·         Off – Always display detailed error messages.

·         RemoteOnly – Display user-friendly error messages to users not running on the local Web server, otherwise, display detailed error messages.

The default value for this setting is “RemoteOnly”.

 

ASP.NET tracing

In order to see trace output in an ASP.NET application, you need to turn on tracing, which is typically done at the application-level in the web.config file. The trace setting can be found within the trace element:

<trace

    enabled="false"

    requestLimit="10"

    pageOutput="false"

    traceMode="SortByTime"

    localOnly="true"

/>

To turn on tracing, set the enabled property to “true”. If the pageOutput property is set
to “true”, the trace output is displayed at the bottom of each ASP.NET page. If it’s set to “false”, trace output is saved in a file named “trace.axd” found in your Web application’s
root directory.

Conclusion

There are a number of advancements in .NET’s error handling and debugging capabilities making it easier to detect and fix bugs in your software. With the exception (no pun intended) of the Command Window, you’ll find Visual Studio .NET’s debugging tools more advanced and capable than those of Visual FoxPro. In the final analysis, it’s up to you as the developer to learn how to use these tools well to provide your end-users with solid, well-debugged code.

 


.NET for Visual FoxPro Developers