Handling Errors - Program Statements - C# 24-Hour Trainer (2015)

C# 24-Hour Trainer (2015)

Section III

Program Statements

Lesson 21

Handling Errors

The best way to avoid user errors is to not give the user the ability to make them in the first place. For example, suppose a program can take purchase orders for between 1 and 100 reams of paper. If the program lets you specify the quantity by using a NumericUpDowncontrol with Minimum = 1 and Maximum = 100, you cannot accidentally enter invalid values like –5 or 10,000.

Sometimes, however, it's hard to build an interface that protects against all possible errors. For example, if the user needs to type in a numeric value, you need to worry about invalid inputs such as 1.2.3 and ten. If you write a program that works with files, you can't always be sure the file will be available when you need it. For example, it might be on a CD that has been removed, or it might be locked by another program.

In this lesson, you learn how to deal with these kinds of unexpected errors. You learn how to protect against invalid values, unavailable files, and other problems that are difficult or impossible to predict and prevent.

Errors and Exceptions

An error is a mistake. It occurs when the program does something incorrect. Sometimes an error is a bug, for example, if the code just doesn't do the right thing.

Sometimes an error is caused by circumstances outside of the program's control. If the program expects the user to enter a numeric value in a textbox but the user types “seven,” the program won't be able to continue its work until the user fixes the problem.

Sometimes you can predict when an error may occur. For example, if a program needs to open a file, there's a chance that the file won't exist. In predictable cases such as this one, the program should try to anticipate the error and protect itself. It should check to see if the file exists before it tries to open it. It can then display a message to the user and ask for help.

Other errors are hard or impossible to predict. Even if the file exists, it may be locked by another program. The user entering invalid data is another example. In those cases, the program may need to just try to do its job. If the program tries to do something seriously invalid, it will receive an exception.

An exception tells the program that something generally very bad occurred such as trying to divide by zero, trying to access an entry in an array that doesn't exist (for example, setting values[100] = 100 when values only holds 10 items), or trying to convert the text “pickle” into an integer.

In cases like these, the program must catch the exception and deal with it. Sometimes it can figure out what went wrong and fix the problem. Other times it might only be able to tell the user about the problem and hope the user can fix it.

NOTE

In C# terms, the code that has the problem throws the exception. Code higher up in the chain can catch the exception and try to handle it.

To catch an exception, a program uses a try-catch block.

try-catch Blocks

In C#, you can use a try-catch block to catch exceptions. One common form of this statement has the following syntax:

try

{

...codeToProtect...

}

catch (ExceptionType1 ex)

{

...exceptionCode1…

}

catch (ExceptionType2 ex)

{

...exceptionCode2…

}

finally

{

...finallyCode…

}

Where:

· codeToProtect is the code that might throw the exception.

· ExceptionType1, ExceptionType2 are exception types such as FormatException or DivideByZeroException. If this particular exception type occurs in the codeToProtect, the corresponding catch block executes.

· ex is a variable that has the same type as the exception. You pick the name for this variable just as you do when you declare any other variable. If an error occurs, you can use this variable to learn more about what happened.

· exceptionCode is the code that the program should execute if the corresponding exception occurs.

· finallyCode is code that always executes whether or not an error occurs.

A try-catch block can include any number of catch blocks with different exception types. If an error occurs, the program looks through the catch blocks in order until it finds one that matches the error. It then executes that block's code and jumps to the finallystatement if there is one.

If you use a catch statement without an exception type and variable, that block catches all exceptions.

NOTE

If you omit the catch statement's exception type and variable, the code cannot learn anything about the exception that occurred. Sometimes that's okay if you don't really care what went wrong as long as you know that something went wrong.

An alternative strategy is to catch a generic Exception object, which matches any kind of exception and provides more information. Then you can at least display an error message as shown in the following code, which tries to calculate a student's test score average assuming the variables totalScore and numTests are already initialized. If the code throws an exception, the catch block displays the exception's default description.

try

{

// Calculate the average.

int averageScore = totalScore / numTests;

// Display the student's average score.

MessageBox.Show("Average Score: " +

averageScore.ToString("0.00"));

}

catch (Exception ex)

{

// Display a message describing the exception.

MessageBox.Show("Error calculating average.\n" + ex.Message);

}

In this example the error that this code is most likely to encounter is a DivideByZeroException thrown if numTests is 0. Because that kind of error is predictable, the code should probably specifically look for DivideByZeroException. The best strategy is to catch the most specific type of exception possible to get the most information. Then catch more generic exceptions just in case. Better still, it should check numTests and not perform the calculation if numTests is 0. Then it can avoid the exception completely.

A try-catch block must include at least one catch block or the finally block, although none of them needs to contain any code. For example, the following code catches and ignores all exceptions:

try

{

...codeToProtect...

}

catch

{

}

The code in the finally block executes whether or not an exception occurs. If an error occurs, the program executes a catch block (if one matches the exception) and then executes the finally block. If no error occurs, the program executes the finally block after it finishes the codeToProtect code.

In fact, if the code inside the try or catch section executes a return statement, the finally block still executes before the program actually leaves the method! The finally block executes no matter how the code leaves the try-catch block.

TryParse

One place where problems are likely to occur is when a program parses text entered by the user. Even if users don't enter obviously ridiculous values such a “twelve,” they might enter values in a format that you don't expect. For example, you might expect the user to enter an integer dollar amount such as 1200 but the user enters $1,200.00. If you use the decimal data type's Parse method and don't allow the currency symbol, thousands separator, and decimal point, the Parse method will throw an exception.

You can use a try-catch block to handle the exception, but it's more efficient to detect the invalid format instead. To do that, you can use the decimal data type's TryParse method.

A data type's TryParse method attempts to parse some text and save the result in a parameter passed with the out keyword. The TryParse method returns true if it successfully parsed the text and false if it could not.

For example, the following code tries to parse a value entered by the user:

decimal amount;

if (!decimal.TryParse(amountTextBox.Text, out amount))

{

MessageBox.Show("Invalid format for amount: " +

amountTextBox.Text +

"\r\nThe amount should be an integer such as 12.");

return;

}

The code uses decimal.TryParse to try to parse the value in amountTextBox. If TryParse returns false, the code displays an error message and then uses a return statement to stop processing the value.

The TryParse methods can take a NumberStyles parameter just as the Parse methods can. For example, you can pass decimal.TryParse the parameter NumberStyles.Any to allow the user to enter values that include currency symbols and thousands separators.

To make things a bit more confusing, the version of TryParse that takes a NumberStyles parameter also takes a format provider that gives the method information about the culture it should use when parsing the text. If you set that parameter to null, the method uses the program's current culture information. For example, the following code is similar to the previous code except it allows thousands separators. The new code is highlighted in bold:

decimal amount;

if (!decimal.TryParse(amountTextBox.Text,

NumberStyles.AllowThousands, null, out amount))

{

MessageBox.Show("Invalid format for amount: " +

amountTextBox.Text +

"\r\nThe amount should be an integer such as 12.");

return;

}

It's generally considered good programming practice to look for the most predictable errors first and only use try-catch blocks as a last resort. That usually allows you to give the user the most meaningful error messages.

Throwing Exceptions

Occasionally it's useful to be able to throw your own exceptions. For example, consider the factorial method you wrote in Lesson 20 and suppose the program invokes the method passing it the value –10 for its parameter. The value –10! is not defined, so what should the method do? It could just declare that –10! is 1 and return that, but that approach could hide a bug in the rest of the program.

A better solution is to throw an exception telling the program what's wrong. The calling code can then use a try-catch block to catch the error and tell the user what's wrong.

The following code shows an improved version of the factorial method described in Lesson 20. Before calculating the factorial, the code checks its parameter and, if the parameter is less than zero, it throws a new ArgumentOutOfRangeException. The exception's constructor has several overloaded versions. The one used here takes as parameters the name of the parameter that caused the problem and a description of the error:

// Return value!

private long Factorial(long value)

{

// Check the parameter.

if (value < 0)

{

// This is invalid. Throw an exception.

throw new ArgumentOutOfRangeException(

"value",

"value must be at least 0.");

}

// Calculate the factorial.

long result = 1;

for (long i = 2; i <= value; i++)

{

result *= i;

}

return result;

}

The following code shows how the program might invoke the new version of the Factorial method. It uses a try-catch block to protect itself in case the Factorial method throws an exception. The block also protects against other errors such as the user entering garbage in the TextBox.

// Calculate the factorial.

private void calculateButton_Click(object sender, EventArgs e)

{

try

{

// Get the input value.

long number = long.Parse(numberTextBox.Text);

// Calculate the factorial.

long answer = Factorial(number);

// Display the factorial.

resultTextBox.Text = answer.ToString();

}

catch (Exception ex)

{

// Display an error message.

MessageBox.Show(ex.Message);

resultTextBox.Clear();

}

}

TIP

Exceptions take additional overhead and disrupt the natural flow of the code, making it harder to read, so only throw exceptions to signal exceptional conditions.

If a method needs to tell the calling code whether it succeeded or failed, that isn't an exceptional condition so use a return value. If a method has an invalid input parameter (such as a 0 in a parameter that cannot be 0), that's an error, so throw an exception.

Try It

In this Try It, you add validation and error handling code to the program you built for Exercise 19-4. When the user clicks the NewItemForm's Calculate and OK buttons, the program should verify that the values make sense and protect itself against garbage such as the user entering the quantity “one,” as shown in Figure 21.1.

Screenshot of Try It 13 window with Add Item button highlighted. One is entered in New Item form's quantity textbox with OK button highlighted. A message box saying “quantity must be an integer” pops up.

Figure 21.1

Lesson Requirements

In this lesson, you:

· Copy the program you built for Exercise 19-4 (or download the version on the book's website).

· Write a ValuesAreOk method to validate the values entered by the user. It should:

· Verify that Item, Price Each, and Quantity aren't blank.

· Use TryParse methods to get the Price Each and Quantity values.

· Verify that Price Each and Quantity are greater than zero.

· Calculate the product of Price Each and Quantity to see if the result is too large to fit in a decimal value.

· If ValuesAreOk finds a problem, it should:

· Tell the user.

· Set focus to the textbox that caused the problem.

· Return false.

· If ValuesAreOk finds that all of the values are okay, it should return true.

NOTE

You can download the code and resources for this lesson from the website at www.wrox.com/go/csharp24hourtrainer2e.

Hints

· If the user clicks the OK button, the form should close only if the user's inputs are valid. Be sure the OK button's DialogResult property doesn't automatically close the form.

Step-by-Step

· Copy the program you built for Exercise 19-4 (or download the version on the book's website).

1. This is straightforward.

· Write a ValuesAreOk method to validate the values entered by the user. It should:

· Verify that Item, Price Each, and Quantity aren't blank.

· Use TryParse methods to get the Price Each and Quantity values.

· Verify that Price Each and Quantity are greater than zero.

· Calculate the product of Price Each and Quantity to see if the result is too large to fit in a decimal value.

· If ValuesAreOk finds a problem, it should:

· Tell the user.

· Set focus to the textbox that caused the problem.

· Return false.

3. The current program only enables the OK button when the Item, Price Each, and Quantity are all non-blank, so you don't need to add any code to verify that they aren't blank. The user can't click the OK button unless they're non-blank.

4. The following code shows how you might try to parse Price Each:

5. // Try to parse PriceEach.

6. if (!decimal.TryParse(priceEachTextBox.Text,

7. NumberStyles.Any, null, out PriceEach))

8. {

9. MessageBox.Show("Price Each must be a currency value.");

10. priceEachTextBox.Focus();

11. return false;

}

When you parse quantity, you could use NumberStyles.Integer to require a plain integer, or you could use NumberStyles.AllowThousands to allow thousands separators.

12. The following code shows how you might verify that PriceEach is greater than zero:

13. // Verify that PriceEach is greater than zero.

14. if (PriceEach <= 0)

15. {

16. MessageBox.Show("Price each must be greater than 0.");

17. priceEachTextBox.Focus();

18. return false;

}

19.The following code shows how you might verify that the product of Price Each and Quantity fits in the decimal data type:

20. // See if Quantity * PriceEach is too big.

21. try

22. {

23. decimal total = Quantity * PriceEach;

24. }

25. catch (Exception ex)

26. {

27. MessageBox.Show(ex.Message);

28. return false;

}

You can test that part of the code by setting Price Each to 1e28 and Quantity to 1000.

· If ValuesAreOk finds that all of the values are okay, it should return true.

0. If the method makes it past all of the previous tests, it should use the statement ItemName = itemTextBox.Text to save the item name for the main program to read.

1. The method should then end with the statement return true.

Exercises

1. Copy the program you wrote for the Try It. That program still has one more problem (at least). If the sum of the values of the items is too big to fit in a decimal, the main program will crash. You can test this by entering two items with Price Each 1e28 and Quantity 7.

Use a try-catch block to protect the main program from this problem. Enclose the code that displays the NewItemForm in a loop that executes as long as the new item's values cause problems.

(Did you anticipate this problem? How about the problem of a new item having a price of $1e28 and quantity 1000? Anticipating and protecting against these kinds of problems is part of what makes programming challenging.)

2. The limits used by the program you wrote for Exercise 1 are ludicrous. You could use the program to order 1 million pencils or a notepad that cost $1e28. That's more money than there is in the entire world. (Probably more money than exists in the entire universe, depending on the currency exchange rate with the Andromeda galaxy.)

Copy the program you wrote for Exercise 1 and add sanity checks. Modify the ValuesAreOk method so it allows up to 100 items and Price Each up to $100.

3. Even if it's unusual for an item to have a price of more than $100 or for someone to order more than 100 of a particular item, it still may be possible. Copy the program you wrote for Exercise 2 and modify the sanity checks. If a value exceeds the normal limits, ask the user if the value is correct and continue if the user says Yes.

4. Copy the LCM program you built for Exercise 20-1 (or download the version on the book's website) and add error handling to it. If a value causes an error, display a message and set focus to its textbox. Hints: Validate both the GCD and LCM methods so they only allow inputs greater than 0. That way they're both covered if a different program uses GCD directly. Also use a try-catch block in the Calculate button's Click event handler to protect against format errors.

5. Copy the Fibonacci program you built for Exercise 19-2 (or download the version on the book's website) and add error handling and validation to it. Protect the program against format errors. Also move the calculation itself into a new method and make it throw an exception if its input is less than 0. (Hint: Test the program with the input 200 and make sure the result makes sense.)

6. [SimpleEdit] Copy the SimpleEdit program you built for Exercise 20-4 (or download the version on the book's website) and add error handling to the places where the program opens and saves files.

To test the program, run it, type some text, and then close the program. Then:

· Use Microsoft Word to open the file Test.rtf in the program's executable directory. Then try to use SimpleEdit to open the file.

· Close Word, open the file in SimpleEdit, and then open the file again in Word. Now make a change in SimpleEdit and try to save the file.

· With the file still open in Word, start a new file in SimpleEdit, type some text, and use the File menu's Save As command to try to save the new file as Test.rtf.

In all three tests, Word should have the Test.rtf file locked so SimpleEdit should display an error message.

7. The quadratic equation finds solutions to equations with the form ax2 + bx + c = 0 where a, b, and c are constants. The solutions to this equation (the values of x that make it true) are given by the quadratic formula:

equation

Build a program similar to the one shown in Figure 21.2 that calculates solutions to quadratic equations. Hints:

Screenshot of Quadratic Formula window with input fields for the values of a, b, and c to find solutions to equations with the form ax2 + bx + c = 0.

Figure 21.2

· Use TryParse to protect against format errors.

· Use Math.Sqrt to take square roots.

· The equation has zero, one, or two real solutions depending on whether the discriminant b2 – 4ac is less than, equal to, or greater than zero. Use if statements to avoid trying to take the square root of a negative number.

· If a is 0, then this is a linear equation not a quadratic, and the quadratic formula tries to divide by zero. Unfortunately C# doesn't consider that an error and just sets the result equal to the special value NaN (which stands for “not a number”). After performing the calculation, use double.IsNaN to see if the result is NaN and display “Not a quadratic” if it is.

8. Several of the programs you've built or described in this book so far enable a Button only when a TextBox contains non-blank text. If the user should enter a number, you can improve the program by only enabling the Button if the text has a valid format. Try this out by writing a program that calculates the area of a circle. Hints:

· Use TryParse to make the TextBox's TextChanged event handler enable the Calculate Button when the user has entered a valid double and that value is at least zero.

· Use the formula area = π × radius2.

· If the user enters a value that is too large (such as 1e200), display the message, “The radius is too big.”

9. Make a program that contains a TextBox for each of the basic data types byte, sbyte, ushort, short, uint, int, ulong, long, float, double, decimal, bool, and char. Use event handlers to set each TextBox's background color to white if the TextBox contains a valid value of the corresponding data type and pink if it does not.

NOTE

Please select the videos for Lesson 21 online at www.wrox.com/go/csharp24hourtrainer2evideos.