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 th
Trang 19.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
Trang 2CCustomer 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)
Trang 3End 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
Trang 4' 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
Trang 5MsgBox(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
Trang 6be 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
Trang 7In 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