Nội dung Text: Control the Creation and Behavior of Classes Now that you have code that can prepare all of the

9.4 Control the Creation and Behavior of Classes
Now that you have code that can prepare all of the objects you need to access and update
data in the database, it's time to write code that can load information for the database into
your class. In Visual Basic 6, developers typically used one of two techniques to populate
a class from a database. One was to have a collection class with a LoadData method that
would open a recordset and iterate through the rows, creating instances of the class
representing the table and setting the object's properties with data from the row. The
second was to have a Retrieve method that would accept a primary key value and load the
data from that one row.
The problem with both of these techniques was that you could never be sure if a
developer who was using your component would call the methods in the proper order.
For example, that developer might attempt to access properties of the class before calling
the Retrieve method. If those properties were strings and numbers, the developer would
receive a 0-length string and a 0, respectively. These could both be valid values within
your database and would not raise an error. How do you control what is happening? You
could have an internal Boolean variable representing whether data was loaded into the
object, and add If...Then statements in every property and method that would raise an
error if that Boolean variable was False. This is not, however, a neat solution.
The Class_Initialize Event was no help either because it wouldn't accept parameters. You
could use a global variable that the initialize event would access, but this would only
work if a single instance of the class could exist in your application.
In short, in VB 6, you didn't have control over the creation of objects.
Technique
Visual Basic .NET does give you this kind of control with a new kind of method called a
constructor. A constructor is a method that is called in conjunction with the New
keyword when you're creating an object instance. It is similar to the Class_Initialize event
that is available in Visual Basic 6, except that it accepts parameters. If the parameters that
are passed to a constructor are not of the proper type, or if a developer neglects to specify
those parameters, the code does not compile.
In this section, you will learn how to write a constructor that accepts a primary key value
and uses that value to populate the properties of a CCustomer object.
Steps
First, the CCustomer class needs access to the data adapter code you created in the
previous section. You could paste all of the code you have written so far in the

CCustomer class into the CCustomerData class, but you may want to use the
CCustomerData objects elsewhere. That leaves you two options: you could have a
module-level CCustomerData variable within the CCustomer class, or you could have the
CCustomer class "inherit" all of the code in the CCustomerData class. You've probably
done the former before, so try the latter.
1. In the declaration of the CCustomer class, add "Inherits CCustomerData".
2. Add module-level variables of type dsCustomers and
dsCustomers.CustomersRow, and name them mdsCust and mdrCust, respectively.
3. Add the following constructor to the CCustomer class, as shown in Listing 9.23.
Listing 9.23 frmHowTo9_4.vb: A Constructor for the CCustomer Class
Public Class CCustomer
Inherits CCustomerData
Implements ICustomer
#Region "Constructors"
Public Sub New(ByVal pCustomerIDToRetrieve As String)
' This constructor takes a valid CustomerID as a parameter,
' and retrieves that row. If the row does not exist, an
' Exception is thrown.
' Create an instance of the Customers dataset
mdsCust = New dsCustomers()
Try
' Set the parameter for the select command so that we retrieve
' the proper row from the table.
' MyBase is used to refer to members of the inherited class.
' It is only required in certain circumstances, such as when
' calling a constructor in the inherited class.
MyBase.odaCustomers.SelectCommand.Parameters(0).Value = _
pCustomerIDToRetrieve
' A strongly typed dataset could contain multiple tables,
' so you must specify the table name to fill.
odaCustomers.Fill(mdsCust, "Customers")
Catch ex As Exception
Throw New ApplicationException("The customer row could not be
retrieved." & ex.Message)

End Try
If mdsCust.Customers.Rows.Count = 1 Then
' Option Strict disallows implicit type conversions.
' Even though the row returned by dsCustomers.Rows(0) is
' actually of the type CustomersRow, the method is declared
' as returning System.Data.DataRow.
mdrCust = CType(mdsCust.Customers.Rows(0),
dsCustomers.CustomersRow)
ReadValuesFromDataRow()
Else
' Throw an exception if no data was returned
Throw New ApplicationException("The customer " & _
pCustomerIDToRetrieve & " was not found.")
End If
End Sub
#End Region
Note
According to Microsoft, you should use ApplicationException for
all exceptions that custom applications throw. This is because
exceptions are typically defined as one of two groups: system
exceptions and application exceptions. System exceptions are
exceptions that are related to execution of code and the .NET run-
time, including things as standard as NullPointerExceptions as
well as more critical issues, such as OutOfMemoryExceptions.
ApplicationExceptions are designed for unexpected circumstances
in your code. If you only throw exceptions, it will be more
difficult for other developers to determine how severe the error is
and how to recover properly.
4. Finally, add the private method called ReadValuesFromDataRow from Listing
9.24 that reads the values from the data row and writes to the class properties.
ReadValuesFromDataRow translates null values from the database into values that
don't cause NullPointerExceptions in Visual Basic code.
Listing 9.24 frmHowTo9_4.vb: Excerpts from the ReadValuesFromDataRow
Method
Private Sub ReadValuesFromDataRow()
With mdrCust

' Because the CustomerID and the CompanyName are both required
' values, we will not have handling for zero-length strings.
mstrCustomerID = .CustomerID
Me.CompanyName = .CompanyName
If .IsContactNameNull Then
Me.ContactName = ""
Else
Me.ContactName = .ContactName
End If
If .IsContactTitleNull Then
Me.ContactTitle = ""
Else
Me.ContactTitle = .ContactTitle
End If
End With
5. To test this code, you need to add code to the click event of the Retrieve button to
use the constructor, as well as a method that copies the values of the object to the
form's text boxes. Listing 9.25 shows the code behind btnRetrieve, as well as the
GetProperties to write the object's properties to the text boxes.
Don't forget that you originally declared the mCustomer variable by creating a
new instance of the class. That declaration called the class's default, parameterless
constructor, which the .NET runtime creates for you if you do not declare a
constructor. As soon as you do declare a constructor, however, that default,
parameterless constructor will no longer exist. You will need to change the
declaration to exclude the instantiation of a new Ccustomer class, as written at the
beginning of Listing 9.25.
Listing 9.25 frmHowTo.vb: Testing the New Constructor
Private mCustomer As CCustomer
Private Sub btnRetrieve_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnRetrieve.Click
Try
mCustomer = New CCustomer(Me.txtCustomerID.Text)
GetProperties()
Catch ex As Exception

MsgBox(ex.Message)
End Try
End Sub
Private Sub GetProperties()
Me.txtAddress.Text = mCustomer.Address
Me.txtCity.Text = mCustomer.City
Me.txtCompanyName.Text = mCustomer.CompanyName
Me.txtContactName.Text = mCustomer.ContactName
Me.txtContactTitle.Text = mCustomer.ContactTitle
Me.txtCountry.Text = mCustomer.Country
Me.txtCustomerID.Text = mCustomer.CustomerID
Me.txtFax.Text = mCustomer.Fax
Me.txtPhone.Text = mCustomer.Phone
Me.txtRegion.Text = mCustomer.Region
Me.txtPostalCode.Text = mCustomer.PostalCode
End Sub
How It Works
By adding this constructor, other developers must provide a CustomerID when they
attempt to create an instance of the CCustomer class. If a corresponding row is in the
Customers table, the properties of the class are populated using data from the database. If
not, an exception is thrown that notifies the developer of the problem.
Of course, a developer could ignore your exception and attempt to access the properties
of the object, but you did give those other developers ample warning. Also, none of the
class-level variables would have been instantiated. In Visual Basic 6, this would not have
made a difference; base datatypes have a default value. (Strings are 0-length strings,
numbers are 0, and so on.) In Visual Basic .NET, however, base datatypes are objects just
like everything else, so a NullPointerException will be thrown at runtime if a developer
should churlishly ignore your previous exception.
Note
Exactly when a constructor executes can be a little confusing. Two steps
are involved in creating an object with a constructor. (Technically, all
objects have at least one constructor, even if you didn't implement one.)
First, the .NET runtime allocates space in memory for the object and
returns a pointer to your code. Second, before control is passed back to
your code, the constructor is executed. Why? Your constructor wouldn't

be able to initialize the properties of the object if memory hadn't been
allocated.
The important point is that, even if you throw an exception in the
constructor, the object already exists in memory and is accessible to the
client code.
Take a closer look at the ReadValuesFromDataRow method. The technique used here
allows consumers of this class to modify the object's properties (the class-level variables)
without modifying the values in the data row, and thus the dataset. In short, the data row
holds the original state of the Customers row, whereas the class-level variables hold the
current state. At the moment, the ReadValuesFromDataRow method is just used to
initialize the class, but, because it sets the class-level variables to the state of the data
row, you could also use it as the basis of a cancel or reset method.
Remember: The dataset stores data as XML, and it does not require an active database
connection. With ADO, retaining recordsets and rows in memory results in a heavy load
on your database servers. With ADO.NET, the cost is minimal.
In the next section, you will add a matching method that writes the current state of the
object to the data row just prior to saving changes to the database.
Comments
Inheritance is another OO term that simply means that all the members of one class are
now part of another class. Instead of copying and pasting code from one module into
another module like you would do with Visual Basic 6 (the copy-and-paste method is
sometimes referred to in the OO world, jokingly, as "Editor Inheritance"), you simply add
a reference to the class containing the code you want to use to the class declaration of
another class.
One of the most fundamental problems in Visual Basic database development is how to
interpret null values in the database within Visual Basic code, and viceversa. In the
database, a null value is a null value, and it won't cause database runtime errors (unless
nulls are not allowed). In Visual Basic, base datatypes, such as strings and dates, do not
have null states. You can, however, have a null pointer in Visual Basic .NET, which
means that the variable was never initialized. An uninitialized variable is quite different
from a database null value and could just as easily signify a runtime bug as a null value.
In either case, other developers would have to include error handling for
NullPointerExceptions.

In Listing 9.24, I used 0-length strings to represent null. This is, perhaps, a bad habit held
over from Visual Basic 6. The definition of a null value is, incongruously, undefined. A
null value represents nothing-not even a datatype. In other words, null represents the
absence of data. A 0-length string represents a specific datatype and a specific value-that
is, a string of no characters. In short, a null value and a 0-length string are two entirely
different things.
The issue becomes more complex when you add numbers, Booleans, dates, and so on.
Does 0 represent null? Does January 1, 1901 represent a null for a date? When you begin
to write production applications in .NET, you should make a choice about how you will
handle null values and stick to it. Microsoft has made the choice in .NET that a null is a
null and nothing else, and, as you can see from the ReadValuesFromDataRow method, all
the code that is generated in .NET provides methods to check if a value is null, as well as
methods to set a value to null. You might want to follow this recommendation, but to
keep the examples in this chapter as simple as possible, we will continue to use 0-length
strings to represent null values.