This section presents three tasks: adding code to make sure that data passed to an object matches the column properties in the database, adding code that validates complex data types and
Trang 19.6 Validate Data Passed to Properties and Communicate Errors to Developers
To make a class that wraps up access to a table, it is critical that the class-and not the developer who is using the class-makes sure that all data is valid before writing it to the database
For example, in the Customers table, the CustomerID field must be five characters in length-no more, no less-and, of course, it must be unique
Phone numbers and fax numbers must also be validated Although we don't necessarily know how many digits are in a phone number (France has eight-digit numbers, Spain has nine, and the U.S has ten), we do know that only numbers and characters such as
periods, parentheses, and hyphens are allowed
You also need to communicate with other developers when their data is not valid It is the data class's job to make sure that invalid data doesn't make it to the database
This section presents three tasks: adding code to make sure that data passed to an object matches the column properties in the database, adding code that validates complex data types and business rules, and communicating errors to the class's consumers
Technique
First, you need to pass errors back up the call stack In Visual Basic 6, the accepted
method was to use Err.Raise, which propagates an error number and an error message This technique is still available to Visual Basic developers, but it retains all the old
problems If you aren't careful, programmers who are working on different components can raise the same error numbers, making error handling a potential nightmare (If you aren't careful, you can end up raising the same error number in your own code.)
You learned how to use structured exception handling in earlier chapters, but the beauty
is that you can define your own exceptions as needed, with almost no code Also, because exceptions have a name as opposed to a number, it is far easier for developers to work with your code Finally, because exceptions are defined with a Namespace, even if two projects define an InvalidCustomerIDException, each exception will be unique
Second, you need to write code to check whether a value is valid For the CustomerID, you simply need to check the length of the string (Later on, you'll need to add code to check whether the ID already exists.) For phone numbers, you'll need to examine the string for invalid characters
Steps
Trang 2Because validating the maximum allowable length for all of our properties is simplest, tackle this task first
1 Define a new exception class by inheriting from System.ApplicationException As mentioned before, Microsoft recommends that all custom exceptions inherit from ApplicationException rather than Exception System.ApplicationException has most of the methods and properties that you will need to communicate an
exception to consumers of this class The only property that a consumer might find useful is one that exposes what the maximum length of the property is This would allow the consumer to communicate the error back to the user or truncate the string without having to hard-code the maximum length into his code Adding the name of the property and the value is also a good idea Some developers who are using your class might write one long Try Catch block, so this information will help debug going forward Paste the code in Listing 9.33 defining the
MaximumStringLengthExceededException into your code
Listing 9.33 frmHowTo9_6.vb: Class Declaration for the
MaximumStringLengthExceededException
Public Class MaximumStringLengthExceededException
Inherits System.ApplicationException
Private nMaxLen As Integer
Public Sub New(ByVal pMaxLen As Integer, ByVal pPropertyName As String, ByVal pValue As String)
' You need to initialize the base class of this exception
' If you do not specifically call a constructor of the base
' class, the default constructor (the one without parameters)
' will be called, if it exists MyBase must precede the call to
' the base class's constructor so that the NET runtime knows not
' to call a constructor in the derived class
MyBase.new("The value specified, " & pValue & _
", exceeds the maximum " &
"length of " & pMaxLen & " allowable by the " & _
pPropertyName & " property.")
End Sub
Public ReadOnly Property MaxLength() As Integer
Get
Return nMaxLen
End Get
End Property
Trang 3End Class
2 Next, modify the set block of each property in the class to check the length of the new value to see if it exceeds the maximum length of the column in the Customers table If the length of the new value does exceed the maximum length, throw a new instance of the MaximumStringLengthExceededException To do this, simply create an If Then block that checks the maximum length into your class
When you have modified all of your properties, they should look like the
ContactName property in Listing 9.34
Listing 9.34 frmHowTo9_6.vb: The ContactName Property Validates for the Maximum Length of the Column and Throws an Exception if the Value
Passed to the Property Exceeds That Maximum Value
Public Property ContactName() As String Implements
ICustomer9_6.ContactName
Get
Return mstrContactName
End Get
Set(ByVal Value As String)
If Value.Length <= 30 Then
mstrContactName = Value
Else
Throw New MaximumStringLengthExceededException(30,
"ContactName", Value)
End If
End Set
End Property
3 Validating a phone number or fax number requires more than just checking the maximum length of the column You need to make sure that only numbers and other allowable characters are in the value First, create a new exception for
invalid phone numbers by adding the code from Listing 9.35 to frmHowTo9_6.vb
Listing 9.35 frmHowTo9_6.vb: The InvalidPhoneNumberException
Public Class InvalidPhoneNumberException
Inherits System.ApplicationException
Public Sub New(ByVal pstrPhone As String)
MyBase.New("The phone number specified, " & pstrPhone & ", is not
valid.")
Trang 4End Sub
End Class
4 Next, add a private method called ValidatePhoneNumber to check a phone number string for invalid characters, such as letters or punctuation marks, as shown in Listing 9.36
Listing 9.36 frmHowTo9_6.vb: A Function That Validates Phone Numbers
Private Function ValidatePhoneNumber(ByVal pstrPhone As String) As Boolean
' Create a string array that contains the numbers 0 to 9, as well as
' a hyphen, period, space, and parentheses
Dim cValidChars() As String
cValidChars = New String(14) {"1", "2", "3", "4", "5", _
"6", "7", "8", "9", "0", "(", ")", "-", " ", "."}
Dim i As Integer = 0
Dim nUBound As Integer = cValidChars.GetUpperBound(0)
Dim nLBound As Integer = cValidChars.GetLowerBound(0)
' Loop through the array of valid characters and remove them
' from a phone number string If characters are left
' in the string, the phone number is invalid
For i = nLBound To nUBound Step 1
pstrPhone = pstrPhone.Replace(cValidChars(i), "")
Next
pstrPhone = pstrPhone.Trim()
If pstrPhone.Length > 0 Then
Return False
Else
Return True
End If
End Function
5 Modify the set blocks of the Fax and Phone properties to call the
ValidatePhoneNumber method and throw an InvalidPhoneNumberException if the phone number is not valid Your code should look like Listing 9.37
Listing 9.37 frmHowTo9_6.vb: The Phone Property That Validates Phone Numbers
Trang 5Public Property Phone() As String Implements ICustomer.Phone
Get
Return mstrPhone
End Get
Set(ByVal Value As String)
If Value.Length <= 24 Then
If ValidatePhoneNumber(Value) Then
mstrPhone = Value
Else
Throw New InvalidPhoneNumberException(Value)
End If
Else
Throw New MaximumStringLengthExceededException(24, "Phone", Value)
End If
End Set
End Property
6 The last piece of data that you need to validate is the CustomerID You need to validate for the string length, and for new customers, you need to validate for the uniqueness of the proposed CustomerID
Validating for the proper length of a CustomerID is simple First, add a new exception called InvalidCustomerIDException, as shown in Listing 9.38
Listing 9.38 frmHowTo9_6.vb: Declaration of the
InvalidCustomerIDException
Public Class InvalidCustomerIDException
Inherits System.ApplicationException
Public Sub New(ByVal pstrID As String)
MyBase.New("The customer ID specified, " & pstrID & ", is not valid") End Sub
End Class
7 Then add the method from Listing 9.39, which checks the length of a CustomerID string and ensures that the string has no whitespace
Listing 9.39 frmHowTo9_6.vb: The ValidateCustomerID Method
Private Function ValidateCustomerID(ByVal pstrID As String) As Boolean
Trang 6' Strip out any leading or trailing spaces
pstrID = pstrID.Trim
' A CustomerID must have five characters
If pstrID.Length = 5 Then
Return True
Else
Return False
End If
End Function
8 Now add code like that in Listing 9.40 to your constructor that validates the
CustomerID, and, if that value is invalid, throws an InvalidCustomerIDException
Listing 9.40 frmHowTo9_6.vb: An If Then Block to Wrap Around Your Constructor Code
If ValidateCustomerID(pCustomerIDToRetrieve) Then
' Your original constructor code goes here
Else
Throw New InvalidCustomerIDException(pCustomerIDToRetrieve)
End If
9 The final piece of validation code you need to add is a function that checks for the existence of a CustomerID before a new Customer object is instantiated You could use the data access objects you defined in CCustomerData, but for
performance purposes, you should create a new command object and use the ExecuteScalar method as shown in Listing 9.41; this method requires less
interaction with the database
Listing 9.41 frmHowTo9_6.vb: The DoesCustomerIDExist Function
Private Function DoesCustomerIDExist(ByVal pstrID As String) As Boolean
Dim strSQL As String = "SELECT COUNT(*) FROM Customers " & _
"WHERE CustomerID = '" &
pstrID & "'"
Dim cmd As New System.Data.OleDb.OleDbCommand(strSQL, oleCnn) Dim fExists As Boolean
Trang 7oleCnn.Open()
fExists = CBool(cmd.ExecuteScalar())
oleCnn.Close()
Return fExists
End Function
Add an extra If Then block in the constructor used to create a new Customer row and you're ready to start testing your new code
Your existing code for frmHowTo9_6 should suffice for testing You made sure to handle exceptions thrown from properties in section 9.2
How It Works
Much of the code in this section qualifies and extends the properties you have already defined and implemented This section has two key concepts
Validating data is a critical part of any application, although the examples in this section use validation techniques you should already be familiar with The next section will take data validation to a new level
The most important concept is declaring your own exceptions Communicating errors to other parts of the application is, perhaps, more important than the business logic you implement When it works, it works, but when something goes wrong, providing enough information about the error is far more important Creating new exceptions is an elegant and readable way to communicate and handle errors
Comments
Hardcoding the maximum column length into the property set block as recommended in this section isn't necessarily the best solution to this problem If you decide to increase or decrease the length of the column in the database, you must search through all of your source code to find every place that value was hard coded Using constants is one way around this, but your options still are fairly limited
The strongly typed dataset you created earlier has the potential to provide an elegant solution to this problem The XSD that underlies the dataset could define many of the data rules in your tables, including the maximum length of columns Also, the actual dataset exposes this information in its properties and methods The problem is that Visual Studio NET does not generate XSDs with such strict definitions