Beginning ASP.NET 2
.0.pdfDealing with Errors
What Are Exceptions?
Exceptions are a range of classes, all deriving from the same base class called Exception. Many different exceptions exist such as FileNotFoundException, which would occur when trying to access a file that doesn’t exist; and SqlException, which would occur when there is a database problem. For example, at some stage while working through the examples in the book you may have seen something like Figure 15-5.
Figure 15-5
This occurred because of an incorrect SQL statement — one of the column names was wrong. This sort of exception is unlikely to get out into a live application, but other exceptions are, so you need to learn how to trap them.
The Exception Object
The Exception object is the base class for all exceptions and defines the base properties that all exceptions have. These properties are explained in the following table.
Property |
Description |
|
|
Data |
A collection of key/value pairs that provide |
|
additional details about the exception. |
HelpLink |
The link to the help file that contains a description |
|
of the exception. |
InnerException |
The underlying exception that caused the problem. |
Message |
Text that describes the exception. |
Source |
The name of the application or object that caused |
|
the problem. |
StackTrace |
The trace of method calls that lead up to the problem. |
TargetSite |
The method that threw the current exception. |
|
|
569
Chapter 15
One thing that that you might notice is that there is an InnerException property. This allows exceptions to be stacked, which is useful when you have layers of code. For example, an underlying part of the .NET Framework may raise an exception, but that might be wrapped within another exception to provide more information.
Other exceptions might define additional properties to describe the specifics on the exception. For example, SqlException also defines the following properties:
Property |
Description |
|
|
Class |
The severity of the error, as defined by SQL Server. |
Errors |
A collection of SqlError objects detailing the problem. |
LineNumber |
The line number within the SQL or stored procedure |
|
where the problem occurred. |
Number |
The error number. |
Procedure |
The name of the stored procedure where the problem |
|
occurred. |
Server |
The name of the SQL Server machine. |
Source |
The name of the data provider. |
State |
The error code from SQL Server. |
You can see that when exceptions occur you have a certain amount of information regarding the problem, but that if you use the correct type of exception you get more information. This is discussed later in the chapter.
How to Trap Exceptions
Trapping exceptions is crucial to maintaining control of your application, and is done by way of Try Catch statements. This is easiest to understand through an example, using some code you saw earlier in the book (Chapter 14) — the code that generates thumbnails for uploaded images:
Public Shared Sub GenerateThumbnail(ByVal SourceImagePath As String, _ ByVal TargetImagePath As String)
Dim newHeight As Short
Dim newWidth As Short
Using sourceImage As Image = Image.FromFile(SourceImagePath)
newHeight = CShort(sourceImage.Height * 0.25) newWidth = CShort(sourceImage.Width * 0.25)
Dim cb As New Image.GetThumbnailImageAbort(AddressOf ThumbnailCallback) Using targetImage As Image = _
sourceImage.GetThumbnailImage(newWidth, newHeight, cb, IntPtr.Zero) targetImage.Save(TargetImagePath, Imaging.ImageFormat.Gif)
570
Dealing with Errors
End Using
End Using
End Sub
At the moment this code doesn’t protect itself in any way, but you can add both defensive coding and exception handling. The source image, for example, must exist before it can be converted, so that can be checked first, but the target image doesn’t exist, so you need to protect against some problem when saving it. Here’s how the code could look:
Public Shared Sub GenerateThumbnail(ByVal SourceImagePath As String, _ ByVal TargetImagePath As String)
If Not File.Exists(sourceImagePath) Then
Return
End If
Dim newHeight As Short
Dim newWidth As Short
Using sourceImage As Image = Image.FromFile(SourceImagePath)
newHeight = CShort(sourceImage.Height * 0.25) newWidth = CShort(sourceImage.Width * 0.25)
Dim cb As New Image.GetThumbnailImageAbort(AddressOf ThumbnailCallback) Using targetImage As Image = _
sourceImage.GetThumbnailImage(newWidth, newHeight, cb, IntPtr.Zero)
Try
targetImage.Save(TargetImagePath, Imaging.ImageFormat.Gif) Catch ex As Exception
Tools.Log(“ImageHandling.GenerateThumbnail”, ex) End Try
End Using End Using
End Sub
The first thing to note is the defensive coding for the source image; this uses the Exists method of the File class to see if the file exists, and if it doesn’t you simply exit silently. The exception handling is wrapped around the saving of the target file and follows this general principle:
Try
Place the code here that you need to trap exceptions for
Catch ex As Exception
Place the code here to handle the exception
This could be displaying a tidy message or logging the exception
End Try
So in the code targetImage.Save is the line that could potentially cause an exception — it might not be possible to save the file for a variety of reasons: an incorrect file name, permissions, and so on. If an
571
Chapter 15
exception does occur, execution immediately transfers to the Catch block, so any lines within the Try that were after the line in fault would not be executed.
The Catch block is interesting because there can be more than one of them. For example:
Try
code to read from a database Catch sqlEx As SqlException
A SQL exception, so something is wrong with the database Catch ex As Exception
Any other error End Try
When there is more than one Catch block and an exception occurs, the exceptions are compared in the order in which they are declared. So if an exception was raised it would be tested to see if it was a SqlException — if it was, the SqlException Catch block would be run. If the exception wasn’t a SqlException, the next one down would be tried until a match was found or until the End Try was reached. Because all exceptions ultimately inherit from Exception, this is the last chance saloon — the default exception if you will.
It’s important that when using multiple Catch blocks you place the most granular first. For example, in the previous code example, consider if this had been done:
Try
code to read from a database Catch ex As Exception
Any other error
Catch sqlEx As SqlException
A SQL exception, so something is wrong with the database End Try
Even if a SqlException was raised it would never be trapped because the Catch for Exception would trap it. This is because Exception matches all exceptions, which is why you should always place it last when using multiple Catch blocks.
There is also another part to Try Catch, which is the Finally block. This is optional, and is a block of code that always runs, whether or not an exception is raised. Again, this is easier to see as code:
Try
code to read from a database Catch ex As Exception
Code to handle the exception Finally
This code will always run End Try
What happens here depends upon whether an exception is raised. If not, then all of the lines of code in the Try block are run, followed by the code in the Finally block. If an exception is raised, the appropriate Catch block is run, followed by the Finally block. This gives you a chance to perform any clean-up operations that might be required. A typical example of this is when performing some database action, and you want to close the database connection whether or not an exception was raised:
572
Dealing with Errors
Dim conn As SqlConnection
Try
conn = New SqlConnection(“ . . . “) conn.Open()
‘do some database action Catch sqlEx As Exception
‘log the exception
Tools.Log(“A database exception occurred”, sqlEx)
Finally
If conn IsNot Nothing Then
conn.Close() End If
End Try
In this code the connection is declared outside of the Try block, allowing it to be referenced from within all code blocks (such as the Catch and Finally blocks). In the Finally block you don’t just automatically close the connection because the exception might have been raised when trying to open the connection, so the connection object (conn) might not have a value. So conn is first checked to see if it has a value, and only then is it closed.
The following Try It Out gives you a better feel for how it can be used. You’re going to update the Checkout page for the Wrox United shop to ensure that any database errors are handled gracefully and that they don’t leave the database in an inconsistent state.
Try It Out |
Trapping Exceptions |
1.In the Wrox United application for the chapter, open the code file for Checkout.aspx and move to the Wizard1_FinishButtonClick event. This is the event that runs when the checkout wizard has finished collecting information from the user and will move the order from the shopping cart into the database.
2.Add the following variable declaration at the top of the method:
Dim trans As SqlTransaction = Nothing
3.Modify the first few lines after the variable declarations so it looks like the following code — the new lines to add are shaded:
Try
conn = New SqlConnection( . . . ) conn.Open()
trans = conn.BeginTransaction
cmd = New SqlCommand() cmd.Connection = conn cmd.Transaction = trans
. . .
573
Chapter 15
4.Now you need to do the same at the end of the method, so modify the code to look like this:
cmd.ExecuteNonQuery() Next
trans.Commit()
Catch SqlEx As SqlException
If trans IsNot Nothing Then
trans.Rollback()
End If
CreateOrderErrorLabel.Visible = True
Return
Finally
If conn IsNot Nothing Then
conn.Close()
End If
End Try
Profile.Cart.Items.Clear()
End Sub
5.To ensure that the exception is seen in practice there actually needs to be some error, so you’ll force one by making the SQL statement incorrect. Change the SQL statement that inserts into the Orders table to insert into no_Orders:
cmd.CommandText = “INSERT INTO no_ORDERS(MemberName ...”
6.Save the file and run the application.
7.Go to the shop and add some items to your cart and then go to the Checkout page. Step through the checkout wizard and after clicking the Finish button you’ll see the message stating that an error has occurred. Although it says that the administrator and shop know about this, you’ll be coding those bits later in the chapter.
How It Works
The code to trap any database exceptions is fairly straightforward, but you’ve added in something else to ensure the integrity of the database, and this is a transaction. A transaction, in database terms, ensures that either all commands succeed or none of them do. The problem is that an order is being created in the Orders table and the items being ordered are created in the Order Items table. So at least two commands are being run, one for each table, but there will be a command for each order line. With any database operation there is a chance of failure, and you need to ensure that both operations succeed, inserting the data into the Orders and Order Items table. You wouldn’t want the order to be created but no order items, nor would you want the reverse. Both are problems. With only an order line, the system thinks that an order is there, but there are no items. The reverse means there would be order lines, but no corresponding order, and thus no way to access those order lines.
Wrapping all of these commands within a transaction means that the order and order lines are not only directly written to their tables but they are also placed in a transaction log, a special table handled by the database. If there are no problems during the transaction, it is committed and all is as expected. If there are problems, however, the transaction is rolled back, which means that the rows that were added as part of the transaction are removed. So there are only two outcomes — all rows are added, or none are added.
574
Dealing with Errors
This means that an exception will leave the database in a consistent state, as it was when the transaction started. It’s a bit like time travel, but without all those weird states of seeing your parents when they were your age.
Look at the code to see how this fits in with the exception handling. You saw the creation of this code in Chapter 13, when looking at e-commerce and the creation of the checkout process, but the explanation of the exception handling was postponed until this chapter.
The first thing is the declaration of a new variable, of type SqlTransaction:
Dim trans As SqlTransaction = Nothing
It is assigned an initial value of Nothing to avoid potential runtime errors. In your code this isn’t a problem, but if you remove = Nothing from the declaration you can see that VWD displays a warning stating that Variable ‘trans’ is used before it has been assigned a value. A null reference exception could result at runtime. The reason is that the compiler cannot be sure that the variable will have an assigned value, so it gives a warning. You’ll see where this assigned value of Nothing comes in later.
After the declaration, the code is wrapped within a Try Catch block:
Try
conn = New SqlConnection( . . . ) conn.Open()
Once the connection is open, a transaction is started. This gives you the marker point for the database commands, and it’s to this state of the database that you will roll back to if an exception occurs. The
BeginTransaction method of the SqlConnection returns a SqTransaction object, so later in the code this can be used to commit or rollback the database changes:
trans = conn.BeginTransaction
With the transaction created it is then assigned to the command using the Transaction property:
cmd = New SqlCommand() cmd.Connection = conn cmd.Transaction = trans
Now that the transaction has been assigned, the code can insert the required data into the tables. Once that has happened the transaction can be committed:
trans.Commit()
That’s all the code for a successful operation, but if there is a problem the exception needs to be caught:
Catch SqlEx As SqlException
Within the Catch block the transaction needs to be rolled back because some problem arose. But, before the transaction can be rolled back the trans object needs to be checked to see if it contains a value. Remember that when the SqlTransaction object was declared it was given a value of Nothing. Within the Try statement, the first lines of code create and open a connection, and these two lines of code could raise an exception. If that happened, the SqlTransaction object wouldn’t have been created, so the
575
Chapter 15
trans variable would still have its default value of Nothing. That’s why the compiler gives a warning and why the default value is set. So you only want to roll back the transaction if it was actually started, in which case the trans variable would not be Nothing:
If trans IsNot Nothing Then trans.Rollback()
End If
Once the transaction has been rolled back an error message is displayed, by simply making a Label control visible. Once done, you can return from the method:
CreateOrderErrorLabel.Visible = True
Return
Before you return, though, the connection needs to be closed, and this needs to be done whether or not an exception occurred, so a Finally block is used. Remember that the Finally block is always run. Like the transaction, the connection also has an initial value of Nothing, and this is used to ensure that there is a connection before you try to close it:
Finally
If conn IsNot Nothing Then
conn.Close()
End If
End Try
The final thing to do is to clear the items from the cart, because they’ve now been added to the Orders and Order Items tables.
Profile.Cart.Items.Clear()
You can see that in use the Try Catch block is quite simple, and that in conjunction with transactions it allows the integrity of the database to remain. Displaying an error on the screen to let users know a problem occurred is a good thing, but the site administrators also need to know, so the next section looks at how these details could be logged to a file.
Logging Exceptions
Trapping exceptions is all very well, but you generally need to log them somehow. After all, if things go wrong you really want to know about it. Logging can be performed in many ways, with the log file being written to a variety of places, including a database table; the Event Log, either under Application events or a custom log; and a log file. There are advantages and disadvantages to all forms of logging. For example, you might not be able to write to the Event Log, because you might not have permission, or if you can write to the Event Log how would you then view the entries? Most web servers are tightly guarded and viewing the Event Log might not be allowed. The advantage of the Event Log is that exceptions are contained along with other application errors.
This section looks at the log file option, because it provides an easy way to log exceptions. A simple text file will be used to store details of exceptions, showing the time and the details of the exception. Each exception is appended to the existing file, or if no file exists it will be created. Although you are using a log file, the technique works for the other ways of logging; the difference is simply where you log the exception. Give this a try in the following Try It Out, modifying the exception code you added to the checkout.
576
Dealing with Errors
Try It Out |
Logging Exceptions |
1.In the Wrox United application for the chapter, open the code file for Checkout.aspx and move to the Wizard1_FinishButtonClick event.
2.In the section where the SqlException is caught, modify the code so that it looks like this:
Catch SqlEx As SqlException
If trans IsNot Nothing Then
trans.Rollback()
End If
Tools.Log(“An error occurred while creating the order”, SqlEx)
CreateOrderErrorLabel.Visible = True
Return
3.Save the file.
4.In the App_Code directory, create a new class called Tools (right-click App_Code and select Add New Item...).
5.In the new Tools.vb class file, add the following Imports statements after the initial one that imports the Microsoft.VisualBasic namespace:
Imports System.IO
Imports System.Web
6.Within the Tools class, add the following methods:
Public Shared Sub Log(ByVal Message As String)
Log(Message, Nothing)
End Sub
Public Shared Sub Log(ByVal Message As String, ByVal Ex As Exception)
Dim fileName As String = Path.Combine( _ HttpContext.Current.Request.PhysicalApplicationPath, “WroxUnited.log”)
Using logFile As New StreamWriter(fileName, True) logFile.WriteLine(“{0}: {1}”, DateTime.Now, Message) If Ex IsNot Nothing Then
logFile.WriteLine(Ex.ToString()) End If
logFile.Close() End Using
End Sub
7.Save the file and run the application. Log in and add some items to the shopping cart, then follow the checkout procedure. When you finish the checkout, the screen will say that an error occurred.
8.Navigate to the directory in which the web site is running and open WroxUnited.log and you’ll see the exception details logged.
577
Chapter 15
9.Don’t forget to change the SQL statement back to a correct one after you’ve run this example. Just remove the no_ from the table name:
cmd.CommandText = “INSERT INTO ORDERS(MemberName ...”
How It Works
The first code modification was to log the exception from within the Catch block in the Checkout page:
Tools.Log(“An error occurred while creating the order”, SqlEx)
This calls the Log method of the Tools class, passing into it a string describing the problem, and the exception SqlException object. The Log method is a Shared method so you haven’t had to create an instance of the Tools class.
The exception will now be logged, so the logging class is created. This is a class file and is placed in the App_Code directory, which means that it will automatically be compiled. Within the Tools class the first thing that is done is to add some namespaces:
Imports System.IO
Imports System.Web
System.IO is required because that is where the file handling routines are stored, and System.Web will allow you access to details of the current web site.
Next is the routine that trapped the exception, where the following was added:
Tools.Log(“An error occurred while creating the order”, SqlEx)
This calls the Log method of the Tools class, passing in an error message and the exception. Notice that it wasn’t necessary to create an instance of the Tools class; because the class is just a container for simple methods, those methods have been made Shared, so a class instance isn’t required – Shared methods were explained in Chapter 9. The code for these is fairly simple, too, and shows a good case of overloaded methods.
The first method accepts a single argument, the error message to log. It doesn’t actually do any logging itself, but calls another Log method, passing the message into that. It also passes Nothing as a second parameter:
Public Shared Sub Log(ByVal Message As String)
Log(Message, Nothing)
End Sub
The second method, also called Log, is where the real work is done. This method accepts two parameters. The first is the error message and the second is an Exception object:
Public Shared Sub Log(ByVal Message As String, ByVal Ex As Exception)
578