Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning ASP.NET 2

.0.pdf
Скачиваний:
23
Добавлен:
17.08.2013
Размер:
24.67 Mб
Скачать

15

Dealing with Errors

This chapter covers another topic that you need to think about during the whole of site construction — that of how to deal with errors. In many ways this chapter could fit at the beginning of the book, because it’s highly likely you’ll get errors as you work through the book and what this chapter covers could be useful. But some of what’s discussed here uses code and depends upon other chapters, so we’ve left it until now.

It’s a fact that you will get errors when creating applications, and that’s okay. We all make mistakes, so this is nothing to be ashamed of or worried about. Some will be simply typing mistakes and some more complex, maybe due to lack of practice, but these go away with time (apart from my typing mistakes, but that’s another story!). So what this chapter looks at is a variety of topics covering all aspects of handling errors. In particular it examines the following:

How to write code so that it is error proof

What exceptions are and how they can be handled

How to centrally handle exceptions

How to use debugging and tracing to work out where errors are occurring

The first section looks at how to bulletproof code.

Defensive Coding

Defensive coding is all about anticipation; working out what could possibly go wrong and coding to prevent it. One of the precepts of defensive coding is that you should never assume anything, especially if user input is involved. Most users will be quite happy to use the site as intended, but hackers will search for ways to break into sites, so you have to do anything you can to minimize this risk.

Being hacked isn’t the only reason to code defensively. A coding bug or vulnerability may not be found by a user, but by yourself or a tester. Fixing this bug, then, involves resources — perhaps a project manager, a developer to fix the bug, a tester to retest the application, all of which take time and money. Also, any change to the code leads to potential bugs — there may not be any, but

Chapter 15

there’s always the possibility. As you add code to correct bugs, the original code becomes more complex and you occasionally end up with imperfect solutions because you had to code around existing code.

So what can you do to protect your code? Well, there are several techniques to defensive coding.

Parameter Checking

The first of these defensive coding techniques is checking the parameters of methods. When writing subroutines or functions you should never assume that a parameter has a valid value — you should check it yourself, especially if the content originates from outside of your code. Take the code for the shopping cart for an example — this is in App_Code\Shopping.vb. One of the methods of the cart allows the item to be updated, like so:

Public Sub Update(ByVal RowID As Integer, ByVal ProductID As Integer, _ ByVal Quantity As Integer, ByVal Price As Double)

Dim Item As CartItem = _items(RowID)

Item.ProductID = ProductID

Item.Quantity = Quantity

Item.Price = Price

_lastUpdate = DateTime.Now()

End Sub

This is fairly simple code, but it does no checking on the parameters that are passed in. The reason for this is that the code that uses this gets the parameter values from the database before passing them in. So although you can say that the code is okay, what happens if this code is reused in another project? What happens if someone circumvents the calling code to pass in an incorrect price, one lower than that stored in the database? Or more simply, what if the RowID passed in is invalid? With this as an example, you could modify the code like so:

Public Sub Update(ByVal RowID As Integer, ByVal ProductID As Integer, _ ByVal Quantity As Integer, ByVal Price As Double)

If RowID <= _items.Count Then

Dim Item As CartItem = _items(RowID)

Item.ProductID = ProductID

Item.Quantity = Quantity

Item.Price = Price

_lastUpdate = DateTime.Now()

End If

End Sub

You’ve now protected this against an incorrect RowID, so no errors will occur when this method is called. It’s a simple check, ensuring that the ID of the row to be updated isn’t larger than the number of rows.

In general, you should always check incoming parameters if the method is a public one; that is, called from outside the class. If it’s a method that isn’t accessible from outside of the class (Private or Protected) then this is less important because you’re probably supplying those parameters yourself, although this

560

Dealing with Errors

doesn’t necessarily mean the parameters will be correct — you might get the values from elsewhere before passing them into the method.

Avoiding Assumptions

As well as checking parameters, you should also avoid assumptions in your code. For example, consider an example that I actually had just now while formatting this document, where there was an error in my own code (yes, shocking isn’t it?). I have macros in Word to perform formatting for the styles of the book, one of which is for the grey code block. It’s a generic routine that accepts three strings, which are the Word styles to use for the first line, the intermediate lines, and the last line, as shown here:

Private Sub FormatParas(First As String, Middle As String, Last As String)

Dim iRow As Integer

Selection.Paragraphs(1).Style = First

For iRow = 2 To Selection.Paragraphs.Count - 1

Selection.Paragraphs(iRow).Style = Middle

Next

Selection.Paragraphs(iRow).Style = Last

End Sub

This code uses Word objects to format paragraphs and assumes that there will be more than one paragraph; this seems sensible because each line is a separate paragraph and there is a separate macro to format code that is only a single line. The code works by setting the first paragraph in the selection to the style defined by First. For subsequent lines (line two downward) the Middle style is used, and for the final line Last is used as the style. However, if there is only one paragraph in the selection, the For...Next loop doesn’t run (Selection.Paragraphs.Count being 1), but it does set iRow to 2. The final line fails because iRow is 2 and there is only one paragraph in the selection. I failed in this code because I didn’t defend it against user failure (me being the user in this case), where the selection was only a single paragraph.

The corrected code is as follows:

Private Sub FormatParas(First As String, Middle As String, Last As String)

If Selection.Paragraphs.Count = 1 Then

Selection.Paragraphs(1).Style = Last

Exit Sub

End If

Dim iRow As Integer

Selection.Paragraphs(1).Style = First

For iRow = 2 To Selection.Paragraphs.Count - 1

Selection.Paragraphs(iRow).Style = Middle

Next

Selection.Paragraphs(iRow).Style = Last

End Sub

561

Chapter 15

With this code, a check is made for the number of paragraphs before the loop is done, and if there is only a single paragraph, no looping is done.

This is a surprisingly common occurrence — errors made because of simple assumptions. This particular code has been in use for several years without failure, but today although it was user error that caused the problem, the failure is in the code for not protecting itself. The dumb user (me) had what looked like multiple paragraphs, but because of soft carriage returns in fact only had one. So the user was wrong, but the code didn’t protect against it.

Query Parameters

If you build your applications using the wizards and data source controls then you won’t run into this problem, because the generated SQL statements contain parameters. But if you manually construct SQL, or inherit an application, you may see code like this:

Dim SQL As String = “SELECT * FROM Products “ & _

“WHERE ProductID=” & Request.QueryString(“ProductID”)

Or like this:

Dim SQL As String = “SELECT * FROM Employee “ & _

“WHERE LastName=’” & LastName.Text & “‘“

Both of these dynamically construct a SQL statement, concatenating strings together; the first uses a value from the QueryString, the second from a TextBox. The problem here is that there’s no validation on the user input, so it’s easy to perform what’s known as SQL injection, where rogue SQL statements are injected into existing ones. With the second example, if the user enters a last name, the SQL would be

SELECT * FROM Employee WHERE LastName=’Paton’

However, the user could enter

Paton’ OR 1=1 --

This would turn the SQL into

SELECT * FROM Employee WHERE LastName=’Paton’ OR 1=1 --’

The first thing to know is that -- is a comment in SQL, so the trailing quote is ignored. The SQL statement matches any LastName that is Paton, which is the intended match, but also the 1=1 statement, which will always be true. The result is that all employees would be shown, which is definitely not the intended purpose. This is mild, however, compared to the user who enters this:

Paton’ ; DROP TABLE Employee –

The SQL statement is now

SELECT * FROM Employee WHERE LastName= ‘Paton’ ; DROP TABLE Employee --’

562

Dealing with Errors

The SELECT statement works as expected, but there’s now a second SQL statement — the semicolon separates statements in SQL. So the query runs, and then the table is dropped — no more employees. Uh-oh, serious trouble. This could easily have been any other SELECT statement to find out more information about the database, or even dropping the entire database.

The solution to SQL injection is to use parameters, because these automatically prevent this type of attack. If you’re using stored procedures, which you should be, then parameters are required for passing information into the procedure, but when building SQL dynamically you can still use parameters. So if you were running a SqlCommand, you could do this:

Dim SQL As String = “SELECT * FROM Employee “ & _ “WHERE LastName=@LastName”

Dim cmd As New SqlCommand(SQL, conn)

cmd.Parameters.Append(“@LastName”, SqlDbType.VarChar, 50)

cmd.Paramaters(“@LastName”).Value = LastName.Text

Here @LastName is the parameter name, and because the value is assigned via the parameter no SQL injection can take place. This is because ADO.NET protects against SQL injection attacks when using parameters — the values passed into parameters are checked for specific content that would signify an attack.

Validation

We’ve already mentioned that any user input is automatically suspicious, and one of the things that can be done to mitigate the risk is to validate that input; let’s stop the user from entering bad input in the first place. This is good for two reasons: first, it means the code is safer and second, users won’t have to deal with potentially confusing error messages. They might still get messages telling them that their input is incorrect, but at least they won’t get an obscure error message because your code failed.

To help with this problem there is a suite of five validation controls with ASP.NET 2.0 that can check user input before it even gets to your code.

RequiredFieldValidator, which ensures that a field is not left empty.

CompareValidator, which compares the entered text with a value, or with another control.

RangeValidator, which ensures the entered text is within a specified range.

RegularExpressionValidator, which matches the entered text against a regular expression.

CustomValidator, which runs custom code to validate the entered text.

These are standard ASP.NET controls that are placed on the page, generally alongside the control they are validating. There is also a ValidationSummary control that allows display of error messages in one place. You add these in the following Try It Out.

Try It Out

Validation Controls

1.In VWD in the Wrox United project for this chapter (C:\BegASPNET2\Chapters\Begin\ Chapter15\WroxUnited) open FanClub\FanClubMatchReport.aspx.

563

Chapter 15

2.Find FormView1 and find TextBox1 in the <EditItemTemplate>. Between TextBox1 and the

<br />, add the following:

<br />

<asp:RequiredFieldValidator id=”rfv1” runat=”server”

ControlToValidate=”TextBox1”

Text=”You haven’t entered the report!” />

3.Do the same for the <InsertItemTemplate> — enter exactly the same code between TextBox1 and the <br />.

4.Save the file and run the application, logging in as a fan club member (use Dave or Lou as the user — the passwords are dave@123 and lou@123).

5.Select the Add Match Report from the Fan Club menu, pick a match, and select the Add New Report link.

6.Don’t enter any text in the text area, but press the Save New Report link, and you’ll see an error message, shown in Figure 15-1.

Figure 15-1

7.Now enter some text and save the report.

8.Click the Add New Report link again to add another report.

9.Don’t enter any text, but click the Cancel New Report link instead. Notice that the error message is displayed again. You can’t cancel unless some text has been entered, so let’s correct that.

10.Stop the application and back in VWD edit the page. Find btnCancelReport (a LinkButton that is in both the <EditItemTemplate> and <InsertItemTemplate>), and for both occurrences add the following attribute:

CausesValidation=”False”

11.Save the file and repeat Steps 5 and 6. Notice that you can now cancel even if no text has been entered.

564

Dealing with Errors

How It Works

This works because the validation control is linked to the text box and won’t allow the page to be posted back to the server until some text has been entered. Here’s a look at the validator in more detail:

<asp:RequiredFieldValidator id=”rfv1” runat=”server” ControlToValidate=”TextBox1”

Text=”You haven’t entered the report!” />

The two properties you’re interested in are the ControlToValidate and the Text. The first is what links the controls together — in this case it’s TextBox1 that the validator will check. If no text is entered, the contents of the Text property are displayed. If text has been entered, the validator does nothing and no output is shown.

Validation occurs whenever a control would cause the page to be posted back to the server — this could be a Button, a LinkButton, and so forth, but what’s important to note is that by default no postback occurs because the validation is done client-side in the browser. This means that the user sees a responsive display without that refresh that can be so annoying. However, you don’t want the validation to take place when you’re trying to cancel an edit — if you’re canceling it doesn’t matter whether there’s anything in the text box. By default all means of posting back cause validation to occur, but by setting the CausesValidation property to False for the cancel link, no validation takes place.

Validation should always be done server-side, but can optionally be done client-side as well. You should never rely solely upon client-side validation, because it’s easy for hackers to post data directly to a page, bypassing the client-side validation. The default settings in ASP.NET 2.0 are for both clientand server-side validation.

Validating Multiple Controls

The previous example showed how to validate a single control, with the error message shown directly by the control. However, there are times when you want to validate several controls, and in these cases it’s not always sensible to have the error message presented directly by the control. For example, you might have a grid of data, or the data you are editing might be neatly aligned. Under these circumstances you might not want to have the message displayed, because it would change the layout of the screen. However you probably still want some form of indication as to which editable field failed the validation, as well as having the full error message displayed somewhere. For that you use the ValidationSummary control. The following Try It Out shows how this can be done.

Try It Out

The ValidationSummary Control

1.Open FanClub.aspx and add RequiredFieldValidator controls between the end of the text boxes and the </td>. You’ll need the following validators,

<asp:RequiredFieldValidator id=”rfv1” runat=”server” ControlToValidate=”txtName”

Text=”*” ErrorMessage=”You must enter a value for your name” /> <asp:RequiredFieldValidator id=”rfv2” runat=”server”

ControlToValidate=”txtAddress”

Text=”*” ErrorMessage=”You must enter a value for the address” /> <asp:RequiredFieldValidator id=”rfv3” runat=”server”

ControlToValidate=”txtCity”

565

Chapter 15

Text=”*” ErrorMessage=”You must enter a value for the city” /> <asp:RequiredFieldValidator id=”rfv4” runat=”server”

ControlToValidate=”txtCounty “

Text=”*” ErrorMessage=”You must enter a value for the county” /> <asp:RequiredFieldValidator id=”rfv5” runat=”server”

ControlToValidate=” txtPostCode “

Text=”*” ErrorMessage=”You must enter a value for the post code” /> <asp:RequiredFieldValidator id=”rfv6” runat=”server”

ControlToValidate=”txtCountry”

Text=”*” ErrorMessage=”You must enter a value for the country” /> <asp:RequiredFieldValidator id=”rfv7” runat=”server”

ControlToValidate=”txtEmail”

Text=”*” ErrorMessage=”You must enter a value for the email address” /> <asp:RequiredFieldValidator id=”rfv8” runat=”server”

ControlToValidate=”txtAlias”

Text=”*” ErrorMessage=”You must enter a value for the membership alias” />

For example, the Name would be

<tr>

<td>Name:</td>

<td><asp:TextBox id=”txtName” runat=”server” /> <asp:RequiredFieldValidator id=”rfv1” runat=”server”

ControlToValidate=”txtName”

Text=”*” ErrorMessage=”You must enter a value for the name” />

</td>

</tr>

2.After the validator for the e-mail address, add the following:

<asp:RegularExpressionValidator id=”rev1” runat=”server” ControlToValidate=”txtEmail”

Text=”*” ErrorMessage=”Please enter a valid email address” />

3.Switch the page into Design View and select the LoginView control, FCLoginView. From the Common Tasks menu select RoleGroup[0]–FanClubMember from the Views list (see Figure 15-2). This will show the controls in the RoleGroup for users in the FanClubMemberRole.

Figure 15-2

4.Scroll down the page to find the RegularExpressionValidator; this is the right-hand validator next to the Email text box. Select this validator and in the Properties area click the ValidationExpression property. Then click the button that appears, as shown in Figure 15-3.

566

Dealing with Errors

Figure 15-3

5.The window that appears is the Regular Expression Editor, which comes pre-filled with a selection of common expressions. Select Internet e-mail address and click OK, and the selected expression will be filled into the ValidationExpression property.

6.Switch the page back to Source View, and before the LinkButton (btnSaveChanges), add the following:

<asp:ValidationSummary id=”vs” runat=”server” DisplayMode=”BulletList” /><br />

7.Save the file and run the application. Log in as either Dave (using the password dave@123) or Lou (using the password lou@123), and navigate to the Fan Club home page.

8.Add text to the fields, but leave the City and Email fields blank. Click the Save Changes link and you’ll see Figure 15-4.

Figure 15-4

9.Now enter some text into the Email field, but make sure it isn’t a valid e-mail address. Click the Save Changes link again and notice how the error message has changed.

567

Chapter 15

10.Delete the text from the Name field and press the Tab key to move to the next field — notice how as soon as you tab out of the field a red star appears next to the field, indicating an error.

How It Works

Several differences exist between this example and the first one where you used only one RequiredFieldValidator. In this example there are several, each for different fields, as well as other validation controls. One thing you’ll have noticed about the required field validators is that no error message was displayed alongside the field they are validating. That’s because the Text property was set to * — just a simple marker to indicate an error in the field. The Text property is displayed inline — that is, wherever the validator is placed. The ErrorMessage property is used wherever the error is displayed, and in this case that is shown by the ValidationSummary.

The ValidationSummary is fairly obvious in its purpose, because it simply displays in one location all error messages from validators. This allows the validators to be discrete, showing just a simple asterisk to mark the field in error without causing displacement on the screen, but still to have a full error message. The DisplayMode for the summary can be a list, bulleted list, or a paragraph.

The other validator used was the RegularExpressionValidator, which checks the entered text against a regular expression — this is a format for defining character sequences. If you want to use regular expressions for other fields, such as postal codes, then the Regular Expression Editor has some expressions, otherwise you can find many others at www.regexlib.com, which has a searchable database of expressions freely available. You’ll have noticed that the Email field had two validators, one to ensure some text was entered, and one to ensure that the entered text was valid. Both validate the same control because they both have their ControlToValidate property set to txtEmail. You can mix and match the other validators in the same way, which gives great flexibility in ensuring that entered data is correct.

The other thing you’ll have noticed was that tabbing from field to field also showed the asterisk, but not the error message in the summary. Because validation is being done client-side, the client script knows that text is required for a field, so if left empty it can signal an error. But because the summary is intended to show all errors, this isn’t filled in unless a postback is tried. So you get the best of both worlds — dynamic error handling with full error messages.

Exception Handling

Exception handling is the term given to anything out of the ordinary that happens in your application — it is exceptional circumstances. Although handling exceptions is part of defensive coding, there’s a difference between programming for things that could quite easily go wrong, or are even expected to go wrong, such as incorrect passwords, and those things over which you have no control. For example, consider what happens if you can’t connect to the database — what would you do? How would you handle that situation? Now it’s likely that there’s nothing you can really do in your application, because it may well be dependent upon the database, but you still don’t want your users to see an ugly error message — you want to display your own message, maybe letting them know that there is a problem and they can try again later.

So the trick is to have a strategy, to work out in advance what you’re going to do when something goes wrong.

568