.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);