Building the Employee Objects


After the previous chapter, you can see that the code is designed in a cookie-cutter fashion. This makes it faster to develop new classes and to maintain old classes. It makes bringing new developers onto a project and bringing them up to speed far easier than everyone going off and doing things their own way.

start sidebar
Development Teams

I worked on a project less than a year ago that had a team of about 10 developers on it. I was brought on about a year and a half into the project and by the time I left the project there was only one member of the original team left. Do not think for an instant that on any large project you will finish with the same team with which you started. In most cases it just is not practical or possible. Design your code with this in mind as well. There must be a cohesive, understandable overall architecture to any project.

In the case of the project that I was working on, every developer had a different way of writing the same piece of code. A lot of developers, including the developer I replaced, did not even document the code on which they were working. No one working on one part of the application was able to work on or understand a different part of the application. This means that every time you needed to move someone to a different part of the application, there was a huge learning curve. This ended up eating into the resources of the other developers and was in part responsible for causing the project to overrun the original estimates. There were also other problems that came out of everyone writing code their own way—the developers never worked as a team. This statement alone should be enough to make any developer understand the importance of a cohesive, overall architecture.

end sidebar

As you start building these objects, realize that there is another relationship you need to worry about: the territory relationship. Although there is no key in the Employees table for the territory, the territory is still a pertinent piece of information in relationship to the employee. You do not have to reference this information, but you need a way to add it into the database. Although you have already created the Territory object, you need a way to associate the territory with the employee. The nature of the relationship in the table indicates there is a many-to-many relationship between the employees and territories table. You already have the stored procedure to return the TerritoryIDs of those territories that the employee is associated with, but you do not return anything else. You will see how you handle this when you start creating your objects.

Caution

Before you start building the Employee objects, you have to do one thing first. When Microsoft created the original Northwind database, the photos of the employees stored in the photo column were added by way of an OLE control in Visual Basic 5 or 6. For this reason, header information for this control was stored in the photo column and cannot be read and displayed by any other means. You can download a zip file from the Apress website (http://www.apress.com) that contains a small program that I wrote to replace the bitmaps with bitmaps that have had the header information removed and can be read and displayed by any program. Simply run the LoadEmployeePics.exe file. The database needs to be on the same machine on which the program is running. The program connects to the SQL Server default instance using integrated authentication. Should you want to make any changes, the source code for the program is included with the application.

Employee Shared Objects

Once the photos have been updated, you can start building the Employee objects. These objects are considerably more complicated than what you have built up to this point. However, these objects in themselves are straightforward but do require a little bit of additional explanation, so we will go through them a little at a time.

As before, you will start off by building the interfaces and structures so that you can serialize your data and make calls across the network. Start off by adding the structure shown in Listing 9-7 to the structures namespace in the NorthwindShared assembly.

Listing 9-7: The structEmployee Structure

start example
 <Serializable()> Public Structure structEmployee      Public EmployeeID As Integer      Public LastName As String      Public FirstName As String      Public Title As String      Public TitleOfCourtesy As String      Public BirthDate As Date      Public HireDate As Date      Public Address As String      Public City As String      Public Region As String      Public PostalCode As String      Public Country As String      Public HomePhone As String      Public Extension As String      Public Photo() As Byte      Public Notes As String      Public ReportsTo As Integer      Public ReportsToFirstName As String      Public ReportsToLastName As String      Public PhotoPath As String      Public Territories() As String End Structure 
end example

You will notice two differences in this structure from your previous structures. The first is the Photo variable, and the second is the Territories variable. In SQL Server, an image data type is returned as a byte array. The Territories variable will need to hold the IDs of all of the territories with which the employee is associated. In addition, you are holding two additional properties that are not part of the Employees table: ReportsToFirstName and ReportsToLastName. These are necessary so that you have enough information to load an Employee object that represents a manager and to return enough information to fill a control to display this information to the user. You will see how you make use of this in the "Employee User-Centric Objects" section.

Next, create the interface you will use to make calls to your remote objects. Add the interface in Listing 9-8 to the Interfaces.vb code module in the NorthwindShared assembly.

Listing 9-8: The IEmployee Interface

start example
 Public Interface IEmployee      Inherits IBaseInterface      Function LoadRecord(ByVal intID As Integer) As structEmployee      Function Save(ByVal sEmployee As structEmployee, _           ByRef intID As Integer) As BusinessErrors      Sub Delete(ByVal intID As Integer) End Interface 
end example

This interface is identical to the other interfaces you have seen before. The last thing you have to do in the NorthwindShared assembly is to add your new business errors to the Errors namespace. Add the following errors to the Errors.vb code module.

The AllowedValuesException will be used when a value does not meet the requirements that you define for a check constraint:

 Public Class AllowedValuesException      Inherits System.ApplicationException      Public Sub New(ByVal strValues As String)           MyBase.New("The value must be one of the following: " & strValues)      End Sub End Class 

The FutureDateException will be used for any date that cannot occur in the future:

 Public Class FutureDateException      Inherits System.ApplicationException      Public Sub New()           MyBase.New("This date can not occur in the future.")      End Sub End Class 

The UnderAgeException will be used to handle any employees who are younger than the minimum hiring age. Note that this is a nonspecific age exception, so you can change the minimum age in your business object and still throw the same error:

 Public Class UnderAgeException      Inherits System.ApplicationException      Public Sub New(ByVal intAge As Integer)           MyBase.New("This person must be over " & intAge.ToString & ".")      End Sub End Class 

Not to be discriminatory, but you have decided not to hire people older than a certain age, so you will create an error to handle this also:

 Public Class OverAgeException      Inherits System.ApplicationException      Public Sub New(ByVal intAge As Integer)           MyBase.New("This person cannot be over the age of " & intAge & ".")      End Sub End Class 

This next exception informs you if you are trying to hire an employee on a date before the company was created:

 Public Class BeforeCompanyCreatedException      Inherits System.ApplicationException      Public Sub New()           MyBase.New("This date cannot occur before the date the " _           & "company was created on (1 January 1976).")      End Sub End Class 

Finally, you have an exception that will handle any invalid date that occurs in the future. It is designed to be fairly flexible but not to meet any specific needs:

 Public Class SpecificFutureDateException      Inherits System.ApplicationException      Public Enum Unit           Days = 0           Weeks = 1           Months = 2           Years = 3      End Enum      Public Sub New(ByVal intPeriod As Integer, ByVal intUnit As Unit)           MyBase.New("This date may not occur more than " _           & intPeriod.ToString & " " & intUnit.ToString & ".")      End Sub End Class 

Employee Data-Centric Object

Now that you have all of your shared objects constructed, it is time to build the data-centric Employee object. As you are building this class, keep in mind that for the first time you have the possibility of different errors occurring in the server-side business objects than the client-side business objects. Remember that you are only checking basic database constraints in the client-side business objects.

Listing 9-9 shows the header and attribute code for the data-centric Employee object. You will look at specific differences from the previous classes afterward. To start this process, add a new class to the NorthwindDC assembly called EmployeeDC.vb.

Listing 9-9: The Header and Properties of the EmployeeDC Class

start example
 Option Strict On Option Explicit On Imports NorthwindTraders.NorthwindShared.Interfaces Imports NorthwindTraders.NorthwindShared.Structures Imports NorthwindTraders.NorthwindShared.Errors Imports System.Configuration Imports System.Data.SqlClient Public Class EmployeeDC      Inherits MarshalByRefObject      Implements IEmployee      Private mobjBusErr As BusinessErrors #Region " Private Attributes"      Private mintEmployeeID As Integer      Private mstrLastName As String      Private mstrFirstName As String      Private mstrTitle As String      Private mstrTitleOfCourtesy As String      Private mdteBirthDate As Date      Private mdteHireDate As Date      Private mstrAddress As String      Private mstrCity As String      Private mstrRegion As String      Private mstrPostalCode As String      Private mstrCountry As String      Private mstrHomePhone As String      Private mstrExtension As String      Private mbytPhoto() As Byte      Private mstrNotes As String      Private mintReportsTo As Integer      Private mstrPhotoPath As String      Private mstrTerritory() As String #End Region #Region " Public Attributes"      Public ReadOnly Property EmployeeID() As Integer           Get                Return mintEmployeeID           End Get      End Property      Public Property LastName() As String           Get                Return mstrLastName           End Get           Set(ByVal Value As String)                Try                     'Test for null value                     If Value Is Nothing Then                          Throw New ArgumentNullException("Last Name")                     End If                     'Test for empty string                     If Value.Length = 0 Then                          Throw New ZeroLengthException()                     End If                     'Test for max length                     If Value.Length > 20 Then                          Throw New MaximumLengthException(20)                     End If                     mstrLastName = Value                Catch exc As Exception                     mobjBusErr.Add("Last Name", exc.Message)                End Try           End Set      End Property      Public Property FirstName() As String           Get                Return mstrFirstName           End Get           Set(ByVal Value As String)                Try                     'Test for null value                     If Value Is Nothing Then                          Throw New ArgumentNullException("First Name")                     End If                     'Test for empty string                     If Value.Length = 0 Then                          Throw New ZeroLengthException()                     End If                     'Test for max length                     If Value.Length > 10 Then                          Throw New MaximumLengthException(10)                     End If                     mstrFirstName = Value                Catch exc As Exception                     mobjBusErr.Add("First Name", exc.Message)                End Try           End Set      End Property      Public Property Title() As String           Get                Return mstrTitle           End Get           Set(ByVal Value As String)                Try                     'Test for null value                     If Value Is Nothing Then                          Throw New ArgumentNullException("Title")                     End If                     'Test for empty string                     If Value.Length = 0 Then                          Throw New ZeroLengthException()                     End If                     'Test for max length                     If Value.Length > 30 Then                          Throw New MaximumLengthException(30)                     End If                     mstrTitle = Value                Catch exc As Exception                     mobjBusErr.Add("Title", exc.Message)                End Try           End Set      End Property      Public Property TitleOfCourtesy() As String           Get                Return mstrTitleOfCourtesy           End Get           Set(ByVal Value As String)                Try                     'Test for null value                     If Value Is Nothing Then                          Exit Property                     End If                     'Test for empty string                     If Value.Length = 0 Then                          mstrTitleOfCourtesy = Nothing                          Exit Property                     End If                     'Test for max length                     If Value.Length > 25 Then                          Throw New MaximumLengthException(25)                     End If                     'Test for specific values                     Select Case Value                          Case "Mr.", "Ms.", "Dr.", "Mrs."                               'Do nothing                          Case Else                               Throw New AllowedValuesException("Mr., Ms., " _                               & "Dr., Mrs.")                     End Select                     mstrTitleOfCourtesy = Value                Catch exc As Exception                     mobjBusErr.Add("Title Of Courtesy", exc.Message)                End Try           End Set      End Property      Public Property BirthDate() As Date           Get                Return mdteBirthDate           End Get           Set(ByVal Value As Date)                Try                     'Check for a future date                     If Value > Now Then                          Throw New FutureDateException()                     End If                     'Check for under 18                     If DateDiff(DateInterval.Day, Value, Now) < (18 * 365) _                     Then                          Throw New UnderAgeException(18)                     End If                     'Check for over 67                     If DateDiff(DateInterval.Year, Value, Now) > 67 Then                          Throw New OverAgeException(67)                     End If                     mdteBirthDate = Value                Catch exc As Exception                     mobjBusErr.Add("Birth Date", exc.Message)                End Try           End Set      End Property      Public Property HireDate() As Date           Get                Return mdteHireDate           End Get           Set(ByVal Value As Date)                Try                     'Check for a future date                     If Value > DateAdd(DateInterval.Day, 14, Now) Then                          Throw New SpecificFutureDateException(2, _                          SpecificFutureDateException.Unit.Weeks)                     End If                     'Check for before company creation date                     If Value < #1/1/1976# Then                          Throw New BeforeCompanyCreatedException()                     End If                     mdteHireDate = Value                Catch exc As Exception                     mobjBusErr.Add("Hire Date", exc.Message)                End Try           End Set      End Property      Public Property Address() As String           Get                Return mstrAddress           End Get           Set(ByVal Value As String)                Try                     'Test for null value                     If Value Is Nothing Then                          Throw New ArgumentNullException("Address")                     End If                     'Test for empty string                     If Value.Length = 0 Then                          Throw New ZeroLengthException()                     End If                     'Test for max length                     If Value.Length > 67 Then                          Throw New MaximumLengthException(67)                     End If                     mstrAddress = Value                Catch exc As Exception                     mobjBusErr.Add("Address", exc.Message)                End Try           End Set      End Property      Public Property City() As String           Get                Return mstrCity           End Get           Set(ByVal Value As String)                Try                     'Test for null value                     If Value Is Nothing Then                          Throw New ArgumentNullException("City")                     End If                     'Test for empty string                     If Value.Length = 0 Then                          Throw New ZeroLengthException()                     End If                     'Test for max length                     If Value.Length > 15 Then                          Throw New MaximumLengthException(15)                     End If                     mstrCity = Value                Catch exc As Exception                     mobjBusErr.Add("City", exc.Message)                End Try           End Set      End Property      Public Property Region() As String           Get                Return mstrRegion           End Get           Set(ByVal Value As String)                Try                     'Test for null value                     If Value Is Nothing Then Exit Property                     'Test for empty string                     If Value.Length = 0 Then                          mstrRegion = Nothing                          Exit Property                     End If                     'Test for max length                     If Value.Length > 15 Then                          Throw New MaximumLengthException(15)                     End If                     mstrRegion = Value                Catch exc As Exception                     mobjBusErr.Add("Region", exc.Message)                End Try           End Set      End Property      Public Property PostalCode() As String           Get                Return mstrPostalCode           End Get           Set(ByVal Value As String)                Try                     'Test for null value                     If Value Is Nothing Then                          Throw New ArgumentNullException("Postal Code")                     End If                     'Test for empty string                     If Value.Length = 0 Then                          Throw New ZeroLengthException()                     End If                     'Test for max length                     If Value.Length > 10 Then                          Throw New MaximumLengthException(10)                     End If                     mstrPostalCode = Value                Catch exc As Exception                     mobjBusErr.Add("Postal Code", exc.Message)                End Try           End Set      End Property      Public Property Country() As String           Get                Return mstrCountry           End Get           Set(ByVal Value As String)                Try                     'Test for null value                     If Value Is Nothing Then                          Throw New ArgumentNullException("Country")                     End If                     'Test for empty string                     If Value.Length = 0 Then                          Throw New ZeroLengthException()                     End If                     'Test for max length                     If Value.Length > 15 Then                          Throw New MaximumLengthException(15)                     End If                     mstrCountry = Value                Catch exc As Exception                     mobjBusErr.Add("Country", exc.Message)                End Try           End Set      End Property      Public Property HomePhone() As String           Get                Return mstrHomePhone           End Get           Set(ByVal Value As String)                Try                     'Test for null value                     If Value Is Nothing Then                          Throw New ArgumentNullException("Home Phone")                     End If                     'Test for empty string                     If Value.Length = 0 Then                          Throw New ZeroLengthException()                     End If                     'Test for max length                     If Value.Length > 24 Then                          Throw New MaximumLengthException(24)                     End If                     mstrHomePhone = Value                Catch exc As Exception                     mobjBusErr.Add("Home Phone", exc.Message)                End Try           End Set      End Property      Public Property Extension() As String           Get                Return mstrExtension           End Get           Set(ByVal Value As String)                Try                     'Test for null value                     If Value Is Nothing Then Exit Property                     'Test for empty string                     If Value.Length = 0 Then                          mstrExtension = Nothing                          Exit Property                     End If                     'Test for max length                     If Value.Length > 4 Then                          Throw New MaximumLengthException(4)                     End If                     mstrExtension = Value                Catch exc As Exception                     mobjBusErr.Add("Extension", exc.Message)                End Try           End Set      End Property      Public Property Photo() As Byte()           Get                Return mbytPhoto           End Get           Set(ByVal Value As Byte())                If Value Is Nothing Then Exit Property                mbytPhoto = Value           End Set      End Property      Public Property Notes() As String           Get                Return mstrNotes           End Get           Set(ByVal Value As String)                Try                     'Test for null value                     If Value Is Nothing Then Exit Property                     'Test for empty string                     If Value.Length = 0 Then                          mstrNotes = Nothing                          Exit Property                     End If                     mstrNotes = Value                Catch exc As Exception                     mobjBusErr.Add("Notes", exc.Message)                End Try           End Set      End Property      Public Property ReportsTo() As Integer           Get                Return mintReportsTo           End Get           Set(ByVal Value As Integer)                Try                     'Test for null value                     If Value = 0 Then                          mintReportsTo = Nothing                          Exit Property                     End If                     mintReportsTo = Value                Catch exc As Exception                     mobjBusErr.Add("Reports To", exc.Message)                End Try           End Set      End Property      Public Property PhotoPath() As String           Get                Return mstrPhotoPath           End Get           Set(ByVal Value As String)                Try                     'Test for null value                     If Value Is Nothing Then Exit Property                     'Test for empty string                     If Value.Length = 0 Then                          mstrPhotoPath = Nothing                          Exit Property                     End If                     'Test for max length                     If Value.Length > 255 Then                          Throw New MaximumLengthException(255)                     End If                     If FileSystem.FileLen(Value) > 0 Then                     'do nothing, if the file is not found, a                     'FileNotFoundException will be thrown automatically                     End If                     mstrPhotoPath = Value                Catch exc As Exception                     mobjBusErr.Add("Photo Path", exc.Message)                End Try           End Set      End Property      Public Property Territories() As String()           Get                Return mstrTerritory           End Get           Set(ByVal Value As String())                Try                     If Value Is Nothing Then                          Throw New Exception("An employee must be " _                          & "assigned to at least one territory.")                     Else                          If Value.Length = 0 Then                               Throw New Exception("An employee must " _                               & "be assigned to at least one territory.")                          End If                     End If                     mstrTerritory = Value                Catch exc As Exception                     mobjBusErr.Add("Territories", exc.Message)                End Try           End Set      End Property #End Region End Class 
end example

Note

To save typing, you can download the code for Chapter 9, which contains all the objects created in this chapter.

Note

For the purpose of the Windows interface you are creating, the photopath property is unnecessary and you will not be using it.

Now that you have entered (or at least reviewed) that incredibly long listing, let's review specific parts of the code to help clarify certain things you have not seen before.

This first section of code from the TitleOfCourtesy property demonstrates how to check for and allow only specific values to be assigned to the property. I have chosen to use a Select Case statement here for consistency and because it is usually far cleaner than using If..Then statements. It also gives you the opportunity to handle different values differently if you choose to do so. Using a Select Case statement does require one piece of code that I generally find sloppy—having a Case to which you do not respond. In this situation, it is necessary; otherwise the Case Else statement would trap everything:

 'Test for specific values Select Case Value      Case "Mr.", "Ms.", "Dr.", "Mrs."           'Do nothing      Case Else           Throw New AllowedValuesException("Mr., Ms., Dr., Mrs.") End Select 

Handling a date is always a tricky task. The next new piece of code is for handling the BirthDate property. The first check is to see if the employee is younger than 18. Notice that the check is by days and not by years. This is because of how the year check is made.

Caution

It is a simple method for checking the number of days that occur between the employee's date of birth and the current day. But this does not take into account leap years and other considerations, so this can spawn a whole other discussion on how to handle dates. One way is to perform a year difference calculation, then a month difference calculation, and then a day difference calculation. The choice is up to you. Your choice should generally be made based on the accuracy required of the calculation.

The next check is to see if the employee is older than 67. This check is not as precise as the underage check because I am making an assumption (a lousy assumption as it turns out) that you are not going to have employees nearly that old. As with the previous date check, there are many ways to make this check more accurate. These date checks are for demonstration purposes only:

 'Check for under 18 If DateDiff(DateInterval.Day, Value, Now) < (18 * 365) Then      Throw New UnderAgeException(18) End If 'Check for over 67 If DateDiff(DateInterval.Year, Value, Now) > 67 Then      Throw New OverAgeException(67) End If 

The hire date property is checking to see how far in the future a person is listed as being hired. It might be standard practice for a company to list a person as hired and wait for them to complete a security check or undergo some other type of waiting period. For this you throw a SpecificFutureDateException, passing in the value of two weeks for an allowable period. The company creation date is fairly simple—you cannot hire anybody before the company was created:

 'Check for a future date If Value > DateAdd(DateInterval.Day, 14, Now) Then      Throw New SpecificFutureDateException(2, _           SpecificFutureDateException.Unit.Weeks) End If 'Check for before company creation date If Value < #1/1/1976# Then      Throw New BeforeCompanyCreatedException() End If 

The extension property allows a value that can be null—that is, a value that is not required. If the value is nothing, you simply exit the property and leave the module-level variable unset. If the property was passed an empty string, you set it equal to nothing. If the value was set, then you check any additional constraints. Being able to have a null value has additional consequences as you will see when you go to save the object:

 'Test for null value If Value Is Nothing Then Exit Property 'Test for empty string If Value.Length = 0 Then      mstrExtension = Nothing      Exit Property End If 'Test for max length If Value.Length > 4 Then      Throw New MaximumLengthException(4) End If 

Next, you move on to your five, now-standard methods in the data-centric class. First is the LoadProxy method (shown in Listing 9-10). This is the same as the previous classes (and it will always be identical) except for the name of the stored procedure you are calling.

Listing 9-10: The Employee LoadProxy Method

start example
 Public Function LoadProxy() As DataSet Implements IEmployee.LoadProxy      Dim strCN As String = _           ConfigurationSettings.AppSettings("Northwind_DSN")      Dim cn As New SqlConnection(strCN)      Dim cmd As New SqlCommand()      Dim da As New SqlDataAdapter(cmd)      Dim ds As New DataSet()      cn.Open()      With cmd           .Connection = cn           .CommandType = CommandType.StoredProcedure           .CommandText = "usp_employee_getall"      End With      da.Fill(ds)      cmd = Nothing      cn.Close()      Return ds End Function 
end example

Second is the LoadRecord routine. There are several variations in this object from what you have seen before, so you will see those differences after you enter the LoadRecord method in Listing 9-11.

Listing 9-11: The Employee LoadRecord Method

start example
 Public Function LoadRecord(ByVal intID As Integer) As structEmployee _ Implements IEmployee.LoadRecord      Dim strCN As String = ConfigurationSettings.AppSettings("Northwind_DSN")      Dim cn As New SqlConnection(strCN)      Dim cmd As New SqlCommand()      Dim da As New SqlDataAdapter(cmd)      Dim ds As New DataSet()      Dim dRow As DataRow      Dim i As Integer      Dim sEmployee As structEmployee      cn.Open()      With cmd          .Connection = cn          .CommandType = CommandType.StoredProcedure          .CommandText = "usp_employee_getone"          .Parameters.Add("@id", intID)      End With      da.Fill(ds)      cmd = Nothing      cn.Close()      With ds.Tables(0).Rows(0)          sEmployee.EmployeeID = Convert.ToInt32(.Item("EmployeeID"))          sEmployee.LastName = Convert.ToString(.Item("LastName"))          sEmployee.FirstName = Convert.ToString(.Item("FirstName"))          sEmployee.Title = Convert.ToString(.Item("Title"))          sEmployee.TitleOfCourtesy = _                Convert.ToString(.Item("TitleOfCourtesy"))          sEmployee.BirthDate = Convert.ToDateTime(.Item("BirthDate"))          sEmployee.HireDate = Convert.ToDateTime(.Item("HireDate"))          sEmployee.Address = Convert.ToString(.Item("Address"))          sEmployee.City = Convert.ToString(.Item("City"))          If Not IsDBNull(.Item("Region")) Then               sEmployee.Region = Convert.ToString(.Item("Region"))          End If          sEmployee.PostalCode = Convert.ToString(.Item("PostalCode"))          sEmployee.Country = Convert.ToString(.Item("Country"))          sEmployee.HomePhone = Convert.ToString(.Item("HomePhone"))          If Not IsDBNull(.Item("Extension")) Then          sEmployee.Extension = Convert.ToString(.Item("Extension"))          End If          If Not IsDBNull(.Item("Photo")) Then                sEmployee.Photo = CType(.Item("Photo"), Byte())          End If          If Not IsDBNull(.Item("Notes")) Then               sEmployee.Notes = Convert.ToString(.Item("Notes"))          End If          If Not IsDBNull(.Item("ReportsTo")) Then               sEmployee.ReportsTo = Convert.ToInt32(.Item("ReportsTo"))                 sEmployee.ReportsToLastName = _                       Convert.ToString(.Item("ReportsToLastName"))               sEmployee.ReportsToFirstName = _                     Convert.ToString(.Item("ReportsToFirstName"))          End If          If Not IsDBNull(.Item("PhotoPath")) Then                sEmployee.PhotoPath = Convert.ToString(.Item("PhotoPath"))          End If      End With      ReDim sEmployee.Territories(ds.Tables(1).Rows.Count - 1)      For Each dRow In ds.Tables(1).Rows          sEmployee.Territories(i) = _                Convert.ToString(dRow.Item("TerritoryID"))          i += 1      Next      ds = Nothing      Return sEmployee End Function 
end example

The major difference here is that you now have the possibility of null values being returned from the database. For every value that can be null in the database, you need to check to see if the value is null. Instead of the IsNothing method in VB 6, you now have the IsDBNull method, which accepts an object as its parameter. The check is simple: If it is nothing, you skip it; otherwise you assign the property.

Another difference is the block of code to grab the TerritoryIDs. Notice that you are pulling the rows from table 1 in the dataset's table collection. For every Select statement in the stored procedure, a table is added to the dataset to contain the results of that Select statement. Here you simply loop through the table and add the values to the Territory array. This ensures that you are opening the connection to the database for the shortest possible time, which gives your application maximum efficiency and scalability.

The Delete method (shown in Listing 9-12) is the same as in the previous objects except for the stored procedure that you call.

Listing 9-12: The Employee Delete Method

start example
 Public Sub Delete(ByVal intID As Integer) Implements IEmployee.Delete      Dim strCN As String = ConfigurationSettings.AppSettings("Northwind_DSN")      Dim cn As New SqlConnection(strCN)      Dim cmd As New SqlCommand()      cn.Open()      With cmd           .Connection = cn           .CommandType = CommandType.StoredProcedure           .CommandText = "usp_employee_delete"           .Parameters.Add("@id", intID)           .ExecuteNonQuery()      End With      cmd = Nothing      cn.Close() End Sub 
end example

The GetBusinessRules method is identical to the previous classes, but as you can see (in Listing 9-13), it is a bit more extensive.

Listing 9-13: The Employee GetBusinessRules Method

start example
 Public Function GetBusinessRules() As BusinessErrors _ Implements IEmployee.GetBusinessRules      Dim objBusRules As New BusinessErrors()      With objBusRules           .Add("Last Name", "The value cannot be null.")           .Add("Last Name", "The value cannot be more than 20 characters " _           & "in length.")           .Add("First Name", "The value cannot be null.")           .Add("First Name", "The value cannot be more than 10 characters " _           & "in length.")           .Add("Title", "The value cannot be null.")           .Add("Title", "The value cannot be more than 30 characters " _           & "in length.")           .Add("Title Of Courtesy", "The value cannot be more than 25 " _           & "characters in length.")           .Add("Title Of Courtesy", "The value must one of the " _           & "following: Mr., Ms., Dr. or Mrs.")           .Add("Birth Date", "The value cannot be a date in the future.")           .Add("Birth Date", "The employee must be 18 years of age or " _           & "older.")           .Add("Birth Date", "The employee must be younger than 60 years " _           & "of age.")           .Add("Hire Date", "The value may not be more than two weeks in " _           & "the future.")           .Add("Hire Date", "The value may not be before the company " _           & "was created.")           .Add("Address", "The value cannot be null.")           .Add("Address", "The value cannot be more than 60 characters " _           & "in length.")           .Add("City", "The value cannot be null.")           .Add("City", "The value cannot be more than 15 characters in " _           & "length.")           .Add("Region", "The value cannot be more than 15 characters in " _           & "length.")           .Add("Postal Code", "The value cannot be more than 10 " _           & "characters in length.")           .Add("Country", "The value cannot be more than 15 characters " _           & "in length.")           .Add("Home Phone", "The value cannot be null.")           .Add("Home Phone", "The value cannot be more than 24 characters " _           & "in length.")           .Add("Extension", "The value cannot be more than 4 characters " _           & "in length.")           .Add("Photo Path", "The value cannot be more than 255 " _           & "characters in length.")      End With      Return objBusRules End Function 
end example

Note

This is a small number of business rules. I have worked on projects where objects have had hundreds of business rules each (fortunately those objects are few and far between). The choice to display the business rules to the users is a personal one (or maybe a mandatory one), but in general it saves a lot of calls to technical support.

In the next chapter, you will see how to report an object's business rules with only one line of code (at least, one line of code in this particular method)!

The last method to add is the Save method. This method is a little longer than you have seen before because there are more properties in the object. Also, this is the first time that you are using an ADO.NET transaction, which works slightly differently than in VB 6. You will work through the Save method a little at a time. Listing 9-14 shows the first part of the Save method.

Listing 9-14: The Employee Save Method (Part 1)

start example
 Public Function Save(ByVal sEmployee As structEmployee, ByRef intID As _ Integer) As BusinessErrors Implements IEmployee.Save      Dim strCN As String = ConfigurationSettings.AppSettings("Northwind_DSN")      Dim cn As New SqlConnection(strCN)      Dim cmd As New SqlCommand()      Dim trans As SqlTransaction      Dim i As Integer      Dim intTempID As Integer      intTempID = sEmployee.EmployeeID      mobjBusErr = New BusinessErrors()      With sEmployee           Me.mintEmployeeID = .EmployeeID           Me.LastName = .LastName           Me.FirstName = .FirstName           Me.Title = .Title           Me.TitleOfCourtesy = .TitleOfCourtesy           Me.BirthDate = .BirthDate           Me.HireDate = .HireDate           Me.Address = .Address           Me.City = .City           Me.Region = .Region           Me.PostalCode = .PostalCode           Me.Country = .Country           Me.HomePhone = .HomePhone           Me.Extension = .Extension           Me.Photo = .Photo           Me.Notes = .Notes           Me.ReportsTo = .ReportsTo           Me.PhotoPath = .PhotoPath           Me.Territories = .Territories      End With 
end example

There are two differences between this Save method and the previous Save methods. The first is the inclusion of the following declaration:

 Dim trans As SqlTransaction 

In .NET, a transaction is a separate object. However, there are some rules regarding transactions. You will learn about these after you see how the transaction is started, committed, and rolled back, as shown in Listing 9-15.

Listing 9-15: The Employee Save Method (Part 2)

start example
 If mobjBusErr.Count = 0 Then             Try                Dim prm As SqlParameter                cn.Open()                trans = cn.BeginTransaction()                With cmd                    .Connection = cn                    .Transaction = trans                    .CommandType = CommandType.StoredProcedure                    .CommandText = "usp_employee_save"                    .Parameters.Add("@id", mintEmployeeID)                    .Parameters.Add("@lname", mstrLastName)                    .Parameters.Add("@fname", mstrFirstName)                    .Parameters.Add("@title", mstrTitle)                    .Parameters.Add("@courtesy", mstrTitleOfCourtesy)                    .Parameters.Add("@birth", mdteBirthDate)                    .Parameters.Add("@hire", mdteHireDate)                    .Parameters.Add("@address", mstrAddress)                    .Parameters.Add("@city", mstrCity)                    .Parameters.Add("@region", mstrRegion)                    .Parameters.Add("@postal", mstrPostalCode)                    .Parameters.Add("@country", mstrCountry)                    .Parameters.Add("@phone", mstrHomePhone)                    If mstrExtension = "" Then                        .Parameters.Add("@extension", DBNull.Value)                    Else                        .Parameters.Add("@extension", mstrExtension)                    End If                    If mbytPhoto Is Nothing Then                        .Parameters.Add("@photo", DBNull.Value)                    Else                        prm = New SqlParameter("@photo", SqlDbType.Image, _                        mbytPhoto.Length, ParameterDirection.Input, False, _                        0, 0, Nothing, DataRowVersion.Current, _                        mbytPhoto)                        cmd.Parameters.Add(prm)                    End If                    If mstrNotes = "" Then                        .Parameters.Add("@notes", DBNull.Value)                    Else                        .Parameters.Add("@notes", mstrNotes)                    End If                    If mintReportsTo = 0 Then                        .Parameters.Add("@reports", DBNull.Value)                    Else                        .Parameters.Add("@reports", mintReportsTo)                    End If                    If mstrPhotoPath = "" Then                        .Parameters.Add("@photopath", DBNull.Value)                    Else                        .Parameters.Add("@photopath", mstrPhotoPath)                    End If                    cmd.Parameters.Add("@new_id", intID).Direction = _                    ParameterDirection.Output                End With                cmd.ExecuteNonQuery() 
end example

Because this is a lot of code and it is difficult to break up into smaller blocks of code, let's review some of the code that you entered previously (namely the code that you have not seen before). The first thing is this declaration:

 Dim prm As SqlParameter 

Although you have been adding parameters with the Parameter.Add method, you can also create a parameter object. In this case it is necessary because one of the constructors that is provided by the object offers you functionality that you cannot get any other way, as you will see when you get to the photo array. The next piece of new code is the following line:

 trans = cn.BeginTransaction() 

You begin the transaction on the connection object, but after that point, you can make any changes to the transaction state against the transaction object instead of the connection. Next is the following:

 .Transaction = trans 

You must assign the transaction to the command object explicitly. This is mandatory and is one of the rules for using a transaction. Every single command executed against the connection, once the transaction has been started, must be associated with the transaction. Failure to do so will cause a runtime error.

This block of code is a new one also:

 If mstrExtension = "" Then      .Parameters.Add("@extension", DBNull.Value) Else      .Parameters.Add("@extension", mstrExtension) End If 

You may ask the following questions: Why are you checking for an empty string if you never set the object equal to anything—wouldn't it be nothing? The answer is that because this is a string object, it is automatically initialized to an empty string so you can check for an empty string or nothing. Second, why do you have to pass the value of DBNull.Value if your object is equal to nothing? The answer is that failure to do this generates a rather unique error. Figure 9-1 shows the error that is generated.

click to expand
Figure 9-1: SQL Server null value error

Of course, you supplied a null value, but SQL Server will not see it. To pass a null value, you must pass DBNull.Value. So, every place where a null value can be inserted into the database, this check must be made.

Tip

It may be helpful to create some type of method that can return either the value or DBNull.value and overload it for every type of data. This saves a great deal of If..Then statements and makes the code more readable.

The last block of code you have not seen so far is the following:

 If mbytPhoto Is Nothing Then      .Parameters.Add("@photo", DBNull.Value) Else      prm = New SqlParameter("@photo", SqlDbType.Image, _      mbytPhoto.Length, ParameterDirection.Input, False, _      0, 0, Nothing, DataRowVersion.Current, _      mbytPhoto)      cmd.Parameters.Add(prm) End If 

Because the photo parameter is of type image, you have to do a little bit more work. You need to identify to SQL Server that you are passing in an image to the parameter, and you need to specify the length of the value being passed in and the actual object you are passing in. Only the overloaded constructor allows you to specify all of the needed values. So, you create the parameter object first and then add it to the parameters collection of the command object.

Next, you need to add the records that associate the employee with the territory. To do this, enter the code in Listing 9-16 into the Save method of the Employee class.

Listing 9-16: The Employee Save Method (Part 3)

start example
 If intTempID > 0 Then      cmd = New SqlCommand()      With cmd           .Connection = cn           .Transaction = trans           .CommandType = CommandType.StoredProcedure           .CommandText = "usp_employee_territory_delete"           .Parameters.Add("@employee_id", mintEmployeeID)           .ExecuteNonQuery()      End With      cmd = Nothing End If 
end example

First, you need to check to see if this is a new employee. If it is a new employee, you can skip this part. If it is not a new employee, you need to delete the employee/ territory relationships. It is easier to simply rewrite the values than to try to figure out what changes were made, especially in a table comprised of only foreign keys.

Note

An Identifying relationship is one in which the join table is comprised of two or more foreign keys, and these foreign keys make up the primary key of the join table. Updating the values in the table is much more difficult. You identify the row in the table with two or more columns and then you change one of the values in the same column you used to identify that row. That means you can no longer identify that row in the same way. Although you do have the new values in your object, you do not have the old values, so how do you find the record you need to update? That requires a lot more work.

After that, you assign the new employee ID to the module-level variable so that you can use it later. (You could use the value stored in intID, except it is more intuitive if you place that value in the mintEmployeeID variable.) This next block of code associates the employee with the territories. Add this code (in Listing 9-17) to the end of the Employee Save method. Notice that you need to loop through the Territory array and add each relationship one at a time. There is no really good way to do this, so simple is better and less likely to cause problems.

Listing 9-17: The Employee Save Method (Part 4)

start example
 If Not sEmployee.Territories Is Nothing Then      For i = 0 To sEmployee.Territories.Length - 1           cmd = New SqlCommand()           With cmd                .Connection = cn                .Transaction = trans                .CommandType = CommandType.StoredProcedure                .CommandText = "usp_employee_territory_insert"                .Parameters.Add("@employee_id", mintEmployeeID)                .Parameters.Add("@territory_id", sEmployee.Territories(i))                .ExecuteNonQuery()           End With           cmd = Nothing      Next End If 
end example

Finally, you need to close out the transaction and ensure the object is in a stable state. Listing 9-18 contains the remainder of the code needed for the Employee Save method.

Listing 9-18: The Employee Save Method (Part 5)

start example
                intID = mintEmployeeID                trans.Commit()                trans = Nothing                cn.Close()           Catch exc As Exception                trans.Rollback()                trans = Nothing                cn.Close()                Throw exc           End Try      Else           Return mobjBusErr      End If End Function 
end example

If everything goes as planned, you call the commit method of the transaction object and then close the database connection. If anything goes wrong, you roll back the transaction and close the connection. You also rethrow the exception so that it can be displayed to the user. If there were rule violations, you return those to the user-centric objects so they can be reported to the user.

Note

Remember that the error handling routine allows for a Finally block that always runs, even if an exception occurs. Although this is technically true, it does not happen if an exception is thrown in the Catch block because the code execution stops at that point because you do not have a Catch block that can handle this exception. This is why you do not close the connection in a Finally block.

Tip

One thing to note is that you are not displaying the contents of a sqlException; you are only using the methods and properties of a base exception. You can increase error reporting by checking the exception that was thrown to see if it is a type of sqlException. If it is, you can report the errors in more detail.

That is it for the data-centric class; you are finally done. Next you will move on to the user-centric objects.

Employee User-Centric Objects

As with the other user-centric classes you have created before, this is simply more of the same with some slight additions to take care of the object aggregation. Also, you will notice that you are not handling all of the business rules in this class that you handled in the data-centric class.

To start with, add a new class to the NorthwindUC project and call it Employee.vb. Add the standard heading and Import statements at the top of the code module:

 Option Strict On Option Explicit On Imports NorthwindTraders.NorthwindShared.Structures Imports NorthwindTraders.NorthwindShared.Interfaces Imports NorthwindTraders.NorthwindShared.Errors Imports NorthwindTraders.NorthwindShared 

Next, make sure that the class inherits from the business base class, declares your remote object constant, and declares the Errs event as in the following:

 Public Class Employee      Inherits BusinessBase      Private Const REMENTRY As String = "EmployeeDC.rem"      Public Event Errs(ByVal obj As BusinessErrors)      Private msEmployee As structEmployee 

As before, the module-level structure holds the Employee object in case you need to roll back a change. Now add the private variable declarations (these can, with two exceptions, be copied from the data-centric Employee class) as shown in Listing 9-19.

Listing 9-19: The User-Centric Employee Private Attributes

start example
 #Region " Private Attributes"     Private mintEmployeeID As Integer = 0     Private mstrLastName As String     Private mstrFirstName As String     Private mstrTitle As String     Private mstrTitleOfCourtesy As String     Private mdteBirthDate As Date = Now     Private mdteHireDate As Date = Now     Private mstrAddress As String     Private mstrCity As String     Private mstrRegion As String     Private mstrPostalCode As String     Private mstrCountry As String     Private mstrHomePhone As String     Private mstrExtension As String     Private mbytPhoto() As Byte     Private mstrNotes As String     Private mintReportsTo As Integer     Private mstrPhotoPath As String     Private mobjReportsTo As Employee     Private mobjTerritoryMgr As TerritoryMgr #End Region 
end example

The two exceptions are the mobjReportsTo variable and the mobjTerritoryMgr object. The mobjReportsTo variable holds an object of type Employee. This allows you to manipulate the employee's manager as a separate object and to retrieve all of the information about this individual by way of the LoadRecord method. Once you program the Employee object, you can use it however you see fit. The mobjTerritoryMgr object is only one solution to the issue of maintaining the territories with which an employee is associated. Remember that you chose not to use the Singleton pattern when you created the TerritoryMgr object so that you can use the manager to maintain the collection of territories specific to this one employee. Another solution would be to use the territory manager as a Singleton and just maintain an array of territory IDs that just reference the TerritoryMgr object when you need to retrieve territory information. The method you choose is up to you, and each has its pros and cons.

Note

These are only two possible solutions, but there are others as well. As a developer, you know that there is no one single solution to any given problem. You also know that there are, in some cases, as many solutions to a problem as there are developers who have had to face that problem. By using the ideas in this chapter as guides, you will be able to develop your own solutions that may work better for your particular situation.

You should also note that you have assigned default values of Now to the date variables. This is necessary so that when you create the user dates, you can set the DateTimePicker control to a default value based on what is in the object.

Now you will add the public member variables. These are checking a subset of the business rules in the data-centric objects. Listing 9-20 shows these properties (if you have not yet downloaded the code for this chapter, you may want to do so now—this is an extremely long listing).

Listing 9-20: The User-Centric Employee Public Properties

start example
 #Region " Public Attributes"     Public ReadOnly Property EmployeeID() As Integer         Get             Return mintEmployeeID         End Get     End Property     Public Property LastName() As String         Get             Return mstrLastName         End Get         Set(ByVal Value As String)             Try                 'Test for null value                 If Value Is Nothing Then                     Throw New ArgumentNullException("Last Name")                 End If                 'Test for empty string                 If Value.Length = 0 Then                     Throw New ZeroLengthException()                 End If                 'Test for max length                 If Value.Length > 20 Then                     Throw New MaximumLengthException(20)                 End If                 If mstrLastName <> Value Then                     mstrLastName = Value                     If Not Loading Then                         mobjRules.BrokenRule("Last Name", False)                         mblnDirty = True                     End If                 End If             Catch exc As Exception                 mobjRules.BrokenRule("Last Name", True)                 mstrLastName = Value                 mblnDirty = True                 Throw exc             End Try         End Set     End Property     Public Property FirstName() As String         Get             Return mstrFirstName         End Get         Set(ByVal Value As String)             Try                 'Test for null value                 If Value Is Nothing Then                     Throw New ArgumentNullException("First Name")                 End If                 'Test for empty string                 If Value.Length = 0 Then                     Throw New ZeroLengthException()                 End If                 'Test for max length                 If Value.Length > 10 Then                     Throw New MaximumLengthException(10)                 End If                 If mstrFirstName <> Value Then                     mstrFirstName = Value                     If Not Loading Then                         mobjRules.BrokenRule("First Name", False)                         mblnDirty = True                     End If                 End If             Catch exc As Exception                 mobjRules.BrokenRule("First Name", True)                 mstrFirstName = Value                 mblnDirty = True                 Throw exc             End Try         End Set     End Property     Public Property Title() As String         Get             Return mstrTitle         End Get         Set(ByVal Value As String)             Try                 'Test for null value                 If Value Is Nothing Then                     Throw New ArgumentNullException("Title")                 End If                 'Test for empty string                 If Value.Length = 0 Then                     Throw New ZeroLengthException()                 End If                 'Test for max length                 If Value.Length > 30 Then                     Throw New MaximumLengthException(30)                 End If                 If mstrTitle <> Value Then                     mstrTitle = Value                     If Not Loading Then                         mobjRules.BrokenRule("Title", False)                         mblnDirty = True                     End If                 End If             Catch exc As Exception                 mobjRules.BrokenRule("Title", True)                 mstrTitle = Value                 mblnDirty = True                 Throw exc             End Try         End Set     End Property     Public Property TitleOfCourtesy() As String         Get             Return mstrTitleOfCourtesy         End Get         Set(ByVal Value As String)             Try                 'Test for null value                 If Value Is Nothing Then                     Exit Property                 End If                 'Test for empty string                 If Value.Length = 0 Then                     mstrTitleOfCourtesy = Nothing                     Exit Property                 End If                 'Test for max length                 If Value.Length > 25 Then                     Throw New MaximumLengthException(25)                 End If                 'Test for specific values                 Select Case Value                     Case "Mr.", "Ms.", "Dr.", "Mrs."                         'Do nothing                     Case Else                         Throw New AllowedValuesException("Mr., Ms., " _                         & "Dr., Mrs.")                 End Select                 If mstrTitleOfCourtesy <> Value Then                     mstrTitleOfCourtesy = Value                     If Not Loading Then                         mobjRules.BrokenRule("Title Of Courtesy", False)                         mblnDirty = True                     End If                 End If             Catch exc As Exception                 mobjRules.BrokenRule("Title Of Courtesy", True)                 mstrTitleOfCourtesy = Value                 mblnDirty = True                 Throw exc             End Try         End Set     End Property     Public Property BirthDate() As Date         Get             Return mdteBirthDate         End Get         Set(ByVal Value As Date)             Try                 If mdteBirthDate <> Value Then                     mdteBirthDate = Value                     If Not Loading Then                         mobjRules.BrokenRule("Birth Date", False)                         mblnDirty = True                     End If                 End If             Catch exc As Exception                 mobjRules.BrokenRule("Birth Date", True)                 mdteBirthDate = Value                 mblnDirty = True                 Throw exc             End Try         End Set     End Property     Public Property HireDate() As Date         Get             Return mdteHireDate         End Get         Set(ByVal Value As Date)             Try                 If mdteHireDate <> Value Then                     mdteHireDate = Value                     If Not Loading Then                         mobjRules.BrokenRule("Hire Date", False)                         mblnDirty = True                     End If                 End If             Catch exc As Exception                 mobjRules.BrokenRule("Hire Date", True)                 mdteHireDate = Value                mblnDirty = True                Throw exc             End Try         End Set     End Property     Public Property Address() As String         Get             Return mstrAddress         End Get         Set(ByVal Value As String)             Try                 'Test for null value                 If Value Is Nothing Then                     Throw New ArgumentNullException("Address")                 End If                 'Test for empty string                 If Value.Length = 0 Then                     Throw New ZeroLengthException()                 End If                 'Test for max length                 If Value.Length > 60 Then                     Throw New MaximumLengthException(60)                 End If                 If mstrAddress <> Value Then                     mstrAddress = Value                     If Not Loading Then                         mobjRules.BrokenRule("Address", False)                         mblnDirty = True                     End If                 End If             Catch exc As Exception                 mobjRules.BrokenRule("Address", True)                 mstrAddress = Value                 mblnDirty = True                 Throw exc             End Try         End Set     End Property     Public Property City() As String         Get             Return mstrCity         End Get         Set(ByVal Value As String)             Try                 'Test for null value                 If Value Is Nothing Then                     Throw New ArgumentNullException("City")                 End If                 'Test for empty string                 If Value.Length = 0 Then                     Throw New ZeroLengthException()                 End If                 'Test for max length                 If Value.Length > 15 Then                     Throw New MaximumLengthException(15)                 End If                 If mstrCity <> Value Then                     mstrCity = Value                     If Not Loading Then                         mobjRules.BrokenRule("City", False)                         mblnDirty = True                     End If                 End If             Catch exc As Exception                 mobjRules.BrokenRule("City", True)                 mstrCity = Value                 mblnDirty = True                 Throw exc             End Try         End Set     End Property     Public Property Region() As String         Get             Return mstrRegion         End Get         Set(ByVal Value As String)             Try                 'Test for null value                 If Value Is Nothing Then                     Exit Property                 End If                 'Test for empty string                 If Value.Length = 0 Then                     mstrRegion = Nothing                     Exit Property                 End If                 'Test for max length                 If Value.Length > 15 Then                     Throw New MaximumLengthException(15)                 End If                 If mstrRegion <> Value Then                     mstrRegion = Value                     If Not Loading Then                         mobjRules.BrokenRule("Region", False)                         mblnDirty = True                     End If                 End If             Catch exc As Exception                 mobjRules.BrokenRule("Region", True)                 mstrRegion = Value                 mblnDirty = True                 Throw exc             End Try         End Set     End Property     Public Property PostalCode() As String         Get             Return mstrPostalCode         End Get         Set(ByVal Value As String)             Try                 'Test for null value                 If Value Is Nothing Then                     Throw New ArgumentNullException("Postal Code")                 End If                 'Test for empty string                 If Value.Length = 0 Then                     Throw New ZeroLengthException()                 End If                 'Test for max length                 If Value.Length > 10 Then                     Throw New MaximumLengthException(10)                 End If                 If mstrPostalCode <> Value Then                     mstrPostalCode = Value                     If Not Loading Then                         mobjRules.BrokenRule("Postal Code", False)                         mblnDirty = True                     End If                 End If             Catch exc As Exception                 mobjRules.BrokenRule("Postal Code", True)                 mstrPostalCode = Value                 mblnDirty = True                 Throw exc             End Try         End Set     End Property     Public Property Country() As String         Get             Return mstrCountry         End Get         Set(ByVal Value As String)             Try                 'Test for null value                 If Value Is Nothing Then                     Throw New ArgumentNullException("Country")                 End If                 'Test for empty string                 If Value.Length = 0 Then                     Throw New ZeroLengthException()                 End If                 'Test for max length                 If Value.Length > 15 Then                     Throw New MaximumLengthException(15)                 End If                 If mstrCountry <> Value Then                     mstrCountry = Value                     If Not Loading Then                         mobjRules.BrokenRule("Country", False)                         mblnDirty = True                     End If                 End If           Catch exc As Exception               mobjRules.BrokenRule("Country", True)               mstrCountry = Value               mblnDirty = True               Throw exc           End Try       End Set     End Property     Public Property HomePhone() As String         Get             Return mstrHomePhone         End Get         Set(ByVal Value As String)             Try                 'Test for null value                 If Value Is Nothing Then                     Throw New ArgumentNullException("Home Phone")                 End If                 'Test for empty string                 If Value.Length = 0 Then                     Throw New ZeroLengthException()                 End If                 'Test for max length                 If Value.Length > 24 Then                     Throw New MaximumLengthException(24)                 End If                 If mstrHomePhone <> Value Then                     mstrHomePhone = Value                     If Not Loading Then                         mobjRules.BrokenRule("Home Phone", False)                         mblnDirty = True                     End If                 End If             Catch exc As Exception                 mobjRules.BrokenRule("Home Phone", True)                 mstrHomePhone = Value                 mblnDirty = True                 Throw exc             End Try         End Set     End Property     Public Property Extension() As String         Get             Return mstrExtension         End Get         Set(ByVal Value As String)             Try             'Test for null value             If Value Is Nothing Then                 Exit Property             End If             'Test for empty string             If Value.Length = 0 Then                 mstrExtension = Nothing                 Exit Property             End If             'Test for max length             If Value.Length > 4 Then                 Throw New MaximumLengthException(4)             End If             If mstrExtension <> Value Then                 mstrExtension = Value                 If Not Loading Then                     mobjRules.BrokenRule("Extension", False)                     mblnDirty = True                 End If             End If             Catch exc As Exception                 mobjRules.BrokenRule("Extension", True)                 mstrExtension = Value                 mblnDirty = True                 Throw exc             End Try         End Set     End Property     Public Property Photo() As Byte()         Get             Return mbytPhoto         End Get         Set(ByVal Value As Byte())             If Not mbytPhoto Is Value Then                 mbytPhoto = Value                 If Not Loading Then                     mblnDirty = True                 End If             End If         End Set     End Property     Public Property Notes() As String         Get             Return mstrNotes         End Get         Set(ByVal Value As String)             If mstrNotes <> Value Then                 mstrNotes = Value                 If Not Loading Then                     mblnDirty = True                 End If             End If         End Set     End Property     Public Property PhotoPath() As String         Get             Return mstrPhotoPath         End Get         Set(ByVal Value As String)             Try                 'Test for null value                 If Value Is Nothing Then                     Exit Property                 End If                 'Test for empty string                 If Value.Length = 0 Then                     mstrPhotoPath = Nothing                     Exit Property                 End If                 'Test for max length                 If Value.Length > 255 Then                     Throw New MaximumLengthException(255)                 End If                 If FileSystem.FileLen(Value) > 0 Then                     'do nothing, if the file is not found, a                     'FileNotFoundException will be thrown automatically                 End If                 If mstrPhotoPath <> Value Then                     mstrPhotoPath = Value                     If Not Loading Then                         mobjRules.BrokenRule("Photo Path", False)                         mblnDirty = True                     End If                 End If             Catch exc As Exception                 mobjRules.BrokenRule("Photo Path", True)                 mstrPhotoPath = Value                 mblnDirty = True                 Throw exc             End Try         End Set     End Property     Public Property ReportsTo() As Employee         Get             Return mobjReportsTo         End Get         Set(ByVal Value As Employee)             If Not mobjReportsTo Is Value Then                 mobjReportsTo = Value                 If Not Loading Then                     mblnDirty = True                 End If             End If         End Set     End Property 
end example

The only property you have left out so far is the property involving the territory manager. That is because it is just a bit different than a regular property. Add the following property for the territory manager object:

     Public ReadOnly Property Territories() As TerritoryMgr         Get             Return mobjTerritoryMgr         End Get     End Property #End Region 

This may seem kind of strange because this looks like you can only read the territory manager object. However, this is not the case—what this does is return the territory manager and allow you to manipulate it however you want. So although this is a read-only method, once you get the territory manager you can do whatever you want with the manager object. This presents you with an additional problem, though. How do you know when a territory associated with the employee changed? The answer is that you do not. So why is this a problem and how do you solve it? It is a problem because you control the enabling and disabling of buttons based on whether the object is dirty. If the user only changes a territory, the employee would never be marked as dirty and would never be saved! Listing 9-21 shows an additional method you need to add that requires some explanation.

Listing 9-21: The Shadowed IsDirty Method

start example
 Public Shadows Function IsDirty() As Boolean      Dim i As Integer      Dim dictEnt As DictionaryEntry      Dim blnFound As Boolean      If Not mblndirty Then           If Not msEmployee.Territories Is Nothing Then                If mobjTerritoryMgr.Count <> msEmployee.Territories.Length Then                     mblndirty = True                Else                     For Each dictEnt In mobjTerritoryMgr                          Dim objTerritory As Territory = _                          CType(dictEnt.Value, Territory)                          blnFound = False                          For i = 0 To msEmployee.Territories.Length - 1                               If objTerritory.TerritoryID = _                               msEmployee.Territories(i) Then                                    blnFound = True                                    Exit For                                End If                          Next                          If Not blnFound Then                               mblndirty = True                               Exit For                          End If                     Next                End If           End If      End If      Return mblndirty End Function 
end example

The first thing to note about this method is the signature—specifically the Shadows keyword. In this particular instance you could also use the overrides keyword, but you have not marked the IsDirty method as overridable in the BusinessBase class. You have avoided a full-blown discussion of the different methods for using objects because there is a great deal more to discuss than could be included in this book. However, because you are using the Shadows keyword, I will briefly explain its use. You have a method called IsDirty in your BusinessBase class. But that method only returns the value of the mblnDirty variable. What the Shadow keyword does is that when you call the IsDirty method on an instance of the Employee class, you will be calling the method from the Employee class. But, if you call the IsDirty method on the Employee class if it has been instantiated as the BusinessBase class, then you will be calling the IsDirty method on the BusinessBase object. Although this may seem confusing, perhaps the following example code snippet will help:

 Dim objEmployee as Employee objEmployee = New Employee() This will call the isDirty method on the Employee object Dim blnValue as Boolean = objEmployee.IsDirty Dim objEmployee as Employee objEmployee = New BusinessBase() This will call the IsDirty method on the BusinessBase object Dim blnValue as Boolean = objEmployee.IsDirty 

Note

For more information on object-oriented design and keywords, refer to the list of references in Appendix B.

Caution

You should shadow a method as a last resort. The reason for this is that unless the author of a class marks a method as overridable, it can be inferred that the author did not intend for the behavior of the method to change. By using the Shadows keyword, you are overriding the behavior anyway. This can lead to some unexpected side effects depending on how the objects are being used.

The rest of this method simply compares your array of IDs with your TerritoryMgr object to look for differences. If it finds differences, then you mark the object as dirty.

Next, add the two constructors that you need for the Employee class (see Listing 9-22).

Listing 9-22: Employee Class Constructors

start example
 Public Sub New()         MyBase.New(REMENTRY)         mobjRules = New BrokenRules()         mobjTerritoryMgr = New TerritoryMgr(False)         With mobjrules             .BrokenRule("Last Name", True)             .BrokenRule("First Name", True)             .BrokenRule("Title", True)             .BrokenRule("Address", True)             .BrokenRule("City", True)             .BrokenRule("Postal Code", True)             .BrokenRule("Country", True)             .BrokenRule("Home Phone", True)         End With     End Sub     Public Sub New(ByVal intID As Integer)         MyBase.New(REMENTRY)         mobjRules = New BrokenRules()         mintEmployeeID = intID         mobjTerritoryMgr = New TerritoryMgr(False) End Sub 
end example

The only rules you break in the first constructor are for those properties that do not have default values (such as the BirthDate and HireDate properties) and those properties that cannot be null. You also instantiate any manager objects that the class will maintain. Now you have a small problem here—if you look at the TerritoryMgr class, you will notice that you have only one constructor and it calls the Load method. You do not want that to happen because you want an empty TerritoryMgr object! To alleviate this problem, modify the constructor in the TerritoryMgr object as shown in Listing 9-23.

Listing 9-23: The Modified TerritoryMgr Constructor

start example
 Public Sub New(Optional ByVal blnLoad As Boolean = True)      If blnLoad Then           Load()      End If End Sub 
end example

Now your original code can remain untouched, but you have the option of not loading up the TerritoryMgr object from the database.

As you did in previous classes, you will override the ToString method by adding the following code to the Employee class:

 Public Overrides Function ToString() As String      Return mstrLastName & ", " & mstrFirstName End Function 

In this case, your ToString method will return the whole name of the employee.

Now add the LoadRecord method and its associated methods as shown in Listing 9-24.

Listing 9-24: The Employee LoadRecord and Related Methods

start example
 Public Sub LoadRecord(ByRef objTerritoryMgr As TerritoryMgr)      Dim objIEmp As IEmployee      objIEmp = CType(Activator.GetObject(GetType(IEmployee), _      AppConstants.REMOTEOBJECTS & REMENTRY), IEmployee)      msEmployee = objIEmp.LoadRecord(mintEmployeeID)      objIEmp = Nothing      LoadObject(objTerritoryMgr) End Sub Private Sub LoadObject(ByRef objTerritoryMgr As TerritoryMgr)      Dim i As Integer      With msEmployee           Me.mintEmployeeID = .EmployeeID           Me.mstrLastName = .LastName           Me.mstrFirstName = .FirstName           Me.mstrTitle = .Title           Me.mstrTitleOfCourtesy = .TitleOfCourtesy           Me.mdteBirthDate = .BirthDate           Me.mdteHireDate = .HireDate           Me.mstrAddress = .Address           Me.mstrCity = .City           Me.mstrRegion = .Region           Me.mstrPostalCode = .PostalCode           Me.mstrCountry = .Country           Me.mstrHomePhone = .HomePhone           Me.mstrExtension = .Extension           Me.mbytPhoto = .Photo           Me.mstrNotes = .Notes           Me.mstrPhotoPath = .PhotoPath           If .ReportsTo > 0 Then                mobjReportsTo = New Employee(.ReportsTo)                mobjReportsTo.FirstName = .ReportsToFirstName                mobjReportsTo.LastName = .ReportsToLastName           End If           mobjTerritoryMgr.Clear()           If Not .Territories Is Nothing Then                For i = 0 To .Territories.Length - 1                     mobjTerritoryMgr.Add((objTerritoryMgr.Item(.Territories(i))))                Next           End If      End With End Sub Public Sub Rollback(ByRef objTerritoryMgr As TerritoryMgr)      LoadObject(objTerritoryMgr) End Sub 
end example

There are a couple of new items here and at least one warning note that you need to be aware of, so let's examine this code. As before, you have a LoadRecord method, but the LoadObject method does the real work. This method is also called by the rollback method (in case the user cancels an edit) as you have seen before.

You first instantiate the mobjReportsTo object (which is of type Employee) and assign the manager information to this class. One important thing to understand about the values that you are assigning to this class is that you have given it enough information to be able to load itself (the EmployeeID), and you have given it enough information so that the ToString method of the Employee Manager will return something. This is important if you want this object to be able to be placed in a listbox or combobox (as you will do when you build the user interface).

Caution

In a situation like this, do not fall into the trap of having one object load another object's state. This destroys the principle of encapsulation. The only time this rule should be broken is when one object is fully aggregated into another object. An example of this would be an Order Detail object. An Order Detail cannot stand on its own; it must be a part of an Order object. In this situation, it is perfectly acceptable to have the Order object instantiate the Order Detail object because the Order Detail object would not have enough information to instantiate itself.

The other item to note is how you get the TerritoryMgr object and assign values from this object to the territories collection object contained within the Employee object. You are now relying on an external object to give you some assistance in loading your Employee object. This breaks the "strict" object-oriented design principle of encapsulation, but on the other hand this is exactly what it means to have aggregated objects! Notice also that you have passed the manager ByRef because you need your Employee object's collection of territories to point to the same place as those territories currently in memory. The act of assigning territories to your internal manager in this particular way means that the objects are stored in the collection ByRef, so any changes to the territory object will be immediately visible in the Employee object.

Caution

This will propagate a problem mentioned earlier in this chapter concerning the use of identifying columns. The twist here is the following: The territory table uses a non-system-generated number, which means the user can change this number during the course of program execution. Because this field is the key that is used in the collections, this has the potential to be a huge problem because as soon as you change the Territory ID, you cannot reference that territory in the collection again! This is why I never use a non-system-generated key on any table in a real application.

Listing 9-25 shows the Delete method. This follows the same pattern as the earlier classes.

Listing 9-25: The Employee Object Delete Method

start example
 Public Sub Delete()      Dim objIEmp As IEmployee      objIEmp = CType(Activator.GetObject(GetType(IEmployee), _      AppConstants.REMOTEOBJECTS & REMENTRY), IEmployee)      objIEmp.Delete(mintEmployeeID)      objIEmp = Nothing End Sub 
end example

Last but certainly not least is the Save method, as shown in Listing 9-26. You will see the differences between this and previous Save methods next.

Listing 9-26: The Employee Class Save Method

start example
 Public Sub Save()      Dim objBusErr As BusinessErrors      If mobjRules.Count = 0 Then           If IsDirty() = True Then                Dim objIEmp As IEmployee                Dim intID As Integer                Dim sEmployee As structEmployee                Dim i As Integer = 0                Dim objTerritory As Territory                Dim dictEnt As DictionaryEntry                intID = mintEmployeeID                With sEmployee                    .EmployeeID = mintEmployeeID                    .LastName = Me.mstrLastName                    .FirstName = Me.mstrFirstName                    .Title = Me.mstrTitle                    .TitleOfCourtesy = Me.mstrTitleOfCourtesy                    .BirthDate = Me.mdteBirthDate                    .HireDate = Me.mdteHireDate                    .Address = Me.mstrAddress                    .City = Me.mstrCity                    .Region = Me.mstrRegion                    .PostalCode = Me.mstrPostalCode                    .Country = Me.mstrCountry                    .HomePhone = Me.mstrHomePhone                    .Extension = Me.mstrExtension                    .Photo = Me.mbytPhoto                    .Notes = Me.mstrNotes                    .PhotoPath = Me.mstrPhotoPath                    If Not mobjReportsTo Is Nothing Then                         .ReportsTo = mobjReportsTo.EmployeeID                    End If                  ReDim .Territories(mobjTerritoryMgr.Count - 1)                   i = 0                   For Each dictEnt In mobjTerritoryMgr                       objTerritory = CType(dictEnt.Value, Territory)                       .Territories(i) = objTerritory.TerritoryID                       i += 1                   Next             objIEmp = CType(Activator.GetObject(GetType(IEmployee), _             AppConstants.REMOTEOBJECTS & REMENTRY), IEmployee)             objBusErr = objIEmp.Save(sEmployee, mintEmployeeID)             objIEmp = Nothing             If Not objBusErr Is Nothing Then                 RaiseEvent Errs(objBusErr)             Else                 mblndirty = False                 msEmployee = sEmployee                 If intID = 0 Then                      CallChangedEvent(Me, New _                      ChangedEventArgs(ChangedEventArgs.eChange.Added))                 Else                      CallChangedEvent(Me, New _                      ChangedEventArgs(ChangedEventArgs.eChange.Updated))                 End If             End If         End If     End If End Sub 
end example

There are only two things that differ in this method from the previous Save methods, and both of them deal with your aggregated objects. The first difference is the ReportsTo property. Instead of assigning the object to your structure, you assign the ID only because that is the only information about the manager that you need to save when you save the Employee object to the database. The second difference are the Territory objects. You store only the ID in the Territory array because you only need this information to be able to enter information into the join table. Any updates to the individual territory objects can be handled by the Territory objects.

Finally, you need to add an employee manager collection class, as shown in Listing 9-27. Add this class to the end of the Employee code module.

Listing 9-27: The EmployeeMgr Class

start example
 Public Class EmployeeMgr     Inherits System.Collections.DictionaryBase     Private Shared mobjEmployeeMgr As EmployeeMgr     Public Shared Function GetInstance() As EmployeeMgr         If mobjEmployeeMgr Is Nothing Then             mobjEmployeeMgr = New EmployeeMgr()         End If         Return mobjEmployeeMgr     End Function     Protected Sub New()         Load()     End Sub     Public Sub Add(ByVal obj As Employee)         dictionary.Add(obj.EmployeeID, obj)     End Sub     Public Function Item(ByVal Key As Object) As Employee         Return CType(dictionary.Item(Key), Employee)     End Function     Public Sub Remove(ByVal Key As Object)         dictionary.Remove(Key)     End Sub     Private Sub Load()         Dim objIEmployee As IEmployee         Dim dRow As DataRow         Dim ds As DataSet         'Obtain a reference to the remote object         objIEmployee = CType(Activator.GetObject(GetType(IEmployee), _         AppConstants.REMOTEOBJECTS & "EmployeeDC.rem"), IEmployee)         ds = objIEmployee.LoadProxy()         objIEmployee = Nothing         'Loop through the dataset adding region objects to the collection         For Each dRow In ds.Tables(0).Rows             'Assign the ID in the constructor since we can't assign it             'afterwards             Dim objEmployee As New _             Employee(Convert.ToInt32(dRow.Item("EmployeeID")))             With objEmployee                 'Set the loading flag so the object isn't marked as edited                 .Loading = True                 .LastName = Convert.ToString(dRow.Item("LastName"))                 .FirstName = Convert.ToString(dRow.Item("FirstName"))                 .Title = Convert.ToString(dRow.Item("Title"))                 .Loading = False             End With             'Add the object to the collection             Me.Add(objEmployee)         Next         ds = Nothing     End Sub     Public Sub Refresh()         dictionary.Clear()         Load()     End Sub End Class 
end example

There is no difference between this manager class and the Region manager class you created before. However, you will note that this time your LoadProxy method does generate a partial object, not a full object because you have a lot more information than you had before and you do not want to load it all up at once—especially if the user does not need or want to see that information. This is why it is called a proxy—it is not complete, but it represents the full object.

Employee User Interface

The Employee List form is almost identical to the previous list forms, but the edit form is much more complicated. You will not review the list form, but you will see the edit form in detail because you will be examining some new techniques for displaying data.

Employee List Form

Because this now-standard list form is no different from any of the previous list forms (except for the objects that it operates on), I simply present the code in Listing 9-28 without further explanation. Add a new form that inherits from your frmListBase form and call it frmEmployeeList. Add all of the code in Listing 9-28 to the list form.

Listing 9-28: Employee List Form

start example
 Option Explicit On Option Strict On Imports NorthwindTraders.NorthwindUC Public Class frmEmployeeList     Inherits UserInterface.frmListBase     Private mobjEmployeeMgr As EmployeeMgr     Private WithEvents mfrmEdit As frmEmployeeEdit     Private WithEvents mobjEmployee As Employee     Private mobjTerritoryMgr As TerritoryMgr #Region " Windows Form Designer generated code "     Public Sub New()         MyBase.New("Employees")         'This call is required by the Windows Form Designer.         InitializeComponent()         'Add any initialization after the InitializeComponent() call         LoadList() End Sub 'Form overrides dispose to clean up the component list. Protected Overloads Overrides Sub Dispose(ByVal disposing As _ Boolean)     If disposing Then         If Not (components Is Nothing) Then             components.Dispose()         End If     End If     MyBase.Dispose(disposing) End Sub     'Required by the Windows Form Designer     Private components As System.ComponentModel.IContainer     'NOTE: The following procedure is required by the Windows Form     'Designer     'It can be modified using the Windows Form Designer.     'Do not modify it using the code editor.     <System.Diagnostics.DebuggerStepThrough()> Private Sub _ InitializeComponent()         components = New System.ComponentModel.Container()     End Sub #End Region     Private Sub LoadList()         Dim objEmployee As Employee         Dim objDictEnt As DictionaryEntry         Try             mobjTerritoryMgr = New TerritoryMgr(True)             lvwList.BeginUpdate()             If lvwList.Columns.Count = 0 Then                 With lvwList                     .Columns.Add("Last Name", CInt(.Size.Width / 3) - _                     8, HorizontalAlignment.Left)                     .Columns.Add("First Name", CInt(.Size.Width / 3) - _                     8, HorizontalAlignment.Left)                     .Columns.Add("Title", CInt(.Size.Width / 3) - 8, _                     HorizontalAlignment.Left)                 End With             End If             lvwList.Items.Clear()             mobjEmployeeMgr = mobjEmployeeMgr.GetInstance         For Each objDictEnt In mobjEmployeeMgr             objEmployee = CType(objDictEnt.Value, Employee)             Dim lst As New ListViewItem(objEmployee.LastName)             lst.Tag = objEmployee.EmployeeID             lst.SubItems.Add(objEmployee.FirstName)             lst.SubItems.Add(objEmployee.Title)             lvwList.Items.Add(lst)         Next         lvwList.EndUpdate()         lblRecordCount.Text = "Record Count: " & lvwList.Items.Count     Catch exc As Exception         LogException(exc)     End Try End Sub Protected Overrides Sub AddButton_Click(ByVal sender As Object, _ ByVal e As System.EventArgs)     Try         If mfrmEdit Is Nothing Then             mobjEmployee = New Employee()             mfrmEdit = New frmEmployeeEdit(mobjEmployee, _             mobjTerritoryMgr)             mfrmEdit.MdiParent = Me.MdiParent             mfrmEdit.Show()         End If     Catch exc As Exception         LogException(exc)     End Try End Sub Private Sub mfrmEdit_Closed(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles mfrmEdit.Closed     mfrmEdit = Nothing End Sub Private Sub mobjEmployee_ObjectChanged(ByVal sender As Object, _ ByVal e As ChangedEventArgs) _ Handles mobjEmployee.ObjectChanged     Try         Dim lst As ListViewItem         Dim objEmployee As Employee = CType(sender, Employee)         Select Case e.Change             Case ChangedEventArgs.eChange.Added                 mobjEmployeeMgr.Add(objEmployee)                 lst = New ListViewItem(objEmployee.LastName)                 lst.Tag = objEmployee.EmployeeID                 lst.SubItems.Add(objEmployee.FirstName)                 lst.SubItems.Add(objEmployee.Title)                 lvwList.Items.Add(lst)                 lblRecordCount.Text = "Record Count: " & _                 lvwList.Items.Count             Case ChangedEventArgs.eChange.Updated                 For Each lst In lvwList.Items                     If Convert.ToInt32(lst.Tag) = _                     objEmployee.EmployeeID Then                         lst.Text = objEmployee.LastName                         lst.SubItems(1).Text = objEmployee.FirstName                         lst.SubItems(2).Text = objEmployee.Title                         Exit For                     End If                 Next         End Select         lvwList.Sort()     Catch exc As Exception         LogException(exc)     End Try End Sub Protected Overrides Sub EditButton_Click(ByVal sender As Object, _ ByVal e As System.EventArgs)     Try         If mfrmEdit Is Nothing Then             If lvwList.SelectedItems.Count > 0 Then                 Cursor = Cursors.WaitCursor                 mobjEmployee = _                 mobjEmployeeMgr.Item(lvwList.SelectedItems(0).Tag)                 mobjEmployee.LoadRecord(mobjTerritoryMgr)                 mfrmEdit = New frmEmployeeEdit(mobjEmployee, _                 mobjTerritoryMgr)                 mfrmEdit.MdiParent = Me.MdiParent                 mfrmEdit.Show()             End If         End If         Catch exc As Exception             LogException(exc)         Finally             Cursor = Cursors.Default         End Try     End Sub     Protected Overrides Sub DeleteButton_Click(ByVal sender As Object, _     ByVal e As System.EventArgs)         Dim objEmployee As Employee         Dim dlgResult As DialogResult         Try             If lvwList.SelectedItems.Count > 0 Then                 objEmployee = _                 mobjEmployeeMgr.Item(lvwList.SelectedItems(0).Tag)                 dlgResult = MessageBox.Show("Do you want to delete " _                 & "employee: " & objEmployee.ToString & "?", _                 "Confirm Delete", MessageBoxButtons.YesNo, _                 MessageBoxIcon.Question)                 If dlgResult = DialogResult.Yes Then                     objEmployee.Delete()                     mobjEmployeeMgr.Remove(objEmployee.EmployeeID)                     lvwList.SelectedItems(0).Remove()                     lblRecordCount.Text = "Record Count: " _                     & lvwList.Items.Count                 End If             End If         Catch exc As Exception             LogException(exc)         End Try     End Sub End Class 
end example

The only difference I will mention is the creation of the TerritoryMgr object. This object must be passed to the edit form—you will see why momentarily. Do not forget to set the text property of the form to Employee List. Next you will create your Employee Edit form.

Employee Edit Form

The Employee Edit form is considerably more complicated than what you have seen before. It also introduces some new concepts and techniques that you have not covered yet. When you are done creating the Employee Edit form, it looks like the form in Figure 9-2. To begin, add a new inherited form called frmEmployeeEdit. This form inherits from the frmEditBase form.

click to expand
Figure 9-2: The Employee Edit form

To create the form, add the controls and set their properties as in Table 9-2.

Table 9-2: Employee Edit Form Controls

Control

Control Name

Control Text

TextBox

txtFirstName

TextBox

txtLastName

TextBox

txtTitle

TextBox

txtExtension

TextBox

txtHomePhone

TextBox

txtAddress

TextBox

txtCountry

TextBox

txtPostalCode

TextBox

txtRegion

TextBox

txtCity

ComboBox

cboCourtesy

ComboBox

cboReportsTo

PictureBox

picPhoto

DateTimePicker

dtpBirthDate

DateTimePicker

dtpHireDate

RichTextBox

rtbNotes

Listbox

lstTerritories

Listbox

lstAvailable

Button

btnAdd

<

Button

btnRemove

>

Button

btnPhoto

(Camera Image)

Label

lblFirstName

First Name

Label

lblLastName

Last Name

Label

lblTitle

Title

Label

lblTitleOfCourtesy

Courtesy

Label

lblAddress

Address

Label

lblHireDate

Hire Date

Label

lblBirthDate

Birth Date

Label

lblExtension

Extension

Label

lblHomePhone

Home Phone

Label

lblReportsTo

Reports To

Label

lblRegion

Region

Label

lblPostalCode

Postal Code

Label

lblCountry

Country

Label

lblNotes

Notes

Label

lblPhoto

Photo

Label

lblTerritories

Assigned Territories

Label

lblCity

City

Label

lblAvailable

Available Territories

Form

frmEmployeeEdit

Employee Edit

Remember to set the Icon Alignment on erpMain property to the middle left for all of the controls that can be edited (even if they currently cannot have a business rule violation). Several controls have special properties that need to be set or overridden to work correctly (see Table 9-3).

Table 9-3: Special Properties of the Employee Edit Form Controls

Control

Property

Value

Explanation

cboCourtesy

DropDownStyle

DropDownList

You only want users picking values from the combo box.

cboReportsTo

DropDownStyle

DropDownList

You only want users picking values from the combo box.

txtAddress

Multiline

True

The database stores both lines of the address in one field with a CR/ LF to get to the second line of the address.

txtAddress

Icon Alignment on erpMain

TopLeft

Because this is a multiline textbox, you want the icon to show up in the top left instead of the middle of a high text box.

rtbNotes

Icon Alignment on erpMain

TopLeft

Because this is a multiline textbox, you want the icon to show up in the top left instead of the middle of a high text box

cboCourtesy

Items Collection

"Mr.", "Mrs.", "Ms.", "Dr."

List of available values.

lstTerritories

Sorted

True

lstAvailable

Sorted

True

frmEmployeeEdit

AcceptButton

None

Any form that contains a multiline textbox or richtextbox control can have the acceptbutton property set, but then the user will not be able to press the Enter key while they are in the textbox or richtextbox to go down to the next line—note that you can change this programmatically if needed.

Also, remember to set the tab order to a logical order. The Windows standard is left to right, top to bottom. The last thing you have to do in the designer is add an OpenFileDialog control to the form. To do this, select and drag the OpenFileDialog from the Toolbox to the form and it will be placed in the nonvisible controls section of the designer. It will be named OpenFileDialog1, and you will use it to select a photo for an employee.

To start off with, your form's code will look similar to what you have seen before. Listing 9-29 shows the header and constructor code.

Listing 9-29: Employee Edit Header and Constructor Code

start example
 Option Strict On Option Explicit On Imports NorthwindTraders.NorthwindUC Public Class frmEmployeeEdit     Inherits UserInterface.frmEditBase     Private WithEvents mobjEmployee As Employee     Private mblnErrors As Boolean = False      Private mobjTerritoryMgr As TerritoryMgr #Region " Windows Form Designer generated code "     Public Sub New(ByRef objEmployee As Employee, _     ByRef objTerritoryMgr As TerritoryMgr)         MyBase.New()         'This call is required by the Windows Form Designer.         InitializeComponent()         'Add any initialization after the InitializeComponent() call         mobjEmployee = objEmployee         mobjTerritoryMgr = objTerritoryMgr         If Not mobjEmployee.IsValid Then             btnOK.Enabled = False         End If     End Sub 
end example

There are two small differences in this listing. The first is the inclusion of the mblnErrors variable. You will see the purpose for this variable shortly. The second is the TerritoryMgr object, which is passed ByRef. Because you are not using a Singleton object for the TerritoryMgr, you need to pass this value in to the Employee Edit form. The reason for this is that you need a master list of territories to place in the available territory list. Otherwise, everything is as straightforward as it was before.

The first routine to add is the frmEmployeeEdit_Load method. This method is somewhat more complicated than you have seen before, so let's walk through it here:

 Private Sub frmEmployeeEdit_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles MyBase.Load      Dim i As Integer      Dim strValue As String      Dim DictEnt As DictionaryEntry      Dim objEmployeeMgr As EmployeeMgr         Try             objEmployeeMgr = objEmployeeMgr.GetInstance             For Each DictEnt In objEmployeeMgr                 Dim objEmployee As Employee = CType(DictEnt.Value, Employee)                 cboReportsTo.Items.Add(objEmployee)             Next             cboReportsTo.Items.Remove(mobjEmployee) 

In this first section of code, the EmployeeMgr object fills the ReportsTo listbox. At the bottom of this block of code, you remove the employee that you are editing because they cannot report to themselves!

This block loads all of the territories into the available territories list box:

 For Each DictEnt In mobjTerritoryMgr     Dim objTerritory As Territory = CType(DictEnt.Value, _     Territory)     lstAvailable.Items.Add(objTerritory) Next 

Here you loop through the contents of the TitleOfCourtesy combo box and look for the courtesy that applies to this employee and then you select it:

 With mobjEmployee     For i = 0 To cboCourtesy.Items.Count - 1         strValue = CType(cboCourtesy.Items(i), String)         If strValue = .TitleOfCourtesy Then             cboCourtesy.SelectedIndex = i             Exit For         End If     Next 

Here you set the person who the employee reports to, just as in the code to set the title of courtesy:

 txtFirstName.Text = .FirstName txtLastName.Text = .LastName txtTitle.Text = .Title If Not .ReportsTo Is Nothing Then     For i = 0 To cboReportsTo.Items.Count - 1         Dim objEmployee As Employee = _         CType(cboReportsTo.Items(i), Employee)         If objEmployee.EmployeeID = .ReportsTo.EmployeeID _         Then             cboReportsTo.SelectedIndex = i             Exit For         End If     Next End If 

This next code assigns the object values to the controls. Note that for the DateTimePicker controls, you need to specify the Value property, not the text property:

 dtpBirthDate.Value = .BirthDate dtpHireDate.Value = .HireDate txtHomePhone.Text = .HomePhone txtExtension.Text = .Extension txtAddress.Text = .Address txtCity.Text = .City txtRegion.Text = .Region txtPostalCode.Text = .PostalCode txtCountry.Text = .Country rtbNotes.Text = .Notes 

The following code loads a photograph if there is one, using the Image.FromStream method. This is a radical change from how images had to be loaded in VB 6 and earlier. You can now take streams of data (assuming they have the correct format), assign them to a picture box, and the control will draw the data. Here you are taking the information from the photo byte array and reading it into a stream format:

 If Not .Photo Is Nothing Then     Dim mStream As New IO.MemoryStream(.Photo)     mStream.Write(.Photo, 0, .Photo.Length - 1)     picPhoto.Image = Image.FromStream(mStream) End If 

This code loads each territory that the employee is associated with into the territories list box and then removes that territory from the list of available territories:

             For Each DictEnt In .Territories                 Dim objT As Territory = CType(DictEnt.Value, Territory)                 lstTerritories.Items.Add(objT)                 lstAvailable.Items.Remove(objT)             Next         End With     Catch exc As Exception         LogException(exc)     End Try End Sub 

Now you need to add the code to take care of associating and removing an employee from an association with a territory (see Listing 9-30).

Listing 9-30: The btnAdd and btnRemove Methods

start example
 Private Sub btnAdd_Click(ByVal sender As System.Object, _     ByVal e As System.EventArgs) Handles btnAdd.Click        Try             If Not lstAvailable.SelectedItem Is Nothing Then                 lstTerritories.Items.Add(lstAvailable.SelectedItem)                 mobjEmployee.Territories.Add(CType(lstAvailable.SelectedItem, _                 Territory))                 lstAvailable.Items.Remove(lstAvailable.SelectedItem)             End If         Catch exc As Exception             LogException(exc)         End Try     End Sub     Private Sub btnRemove_Click(ByVal sender As System.Object, _     ByVal e As System.EventArgs) Handles btnRemove.Click         Try              If Not lstTerritories.SelectedItem Is Nothing Then                  Dim objTerritory As Territory = _                  CType(lstTerritories.SelectedItem, Territory)                  lstAvailable.Items.Add(objTerritory)                  mobjEmployee.Territories.Remove(objTerritory.TerritoryID)                  lstTerritories.Items.Remove(lstTerritories.SelectedItem)              End If         Catch exc As Exception             LogException(exc)         End Try End Sub 
end example

Listing 9-31 shows the code to add a photograph to your employee.

Listing 9-31: Associating a Photograph with an Employee

start example
 Private Sub btnPhoto_Click(ByVal sender As System.Object, _     ByVal e As System.EventArgs) Handles btnPhoto.Click         Dim bytArray() As Byte         Try             With OpenFileDialog1                 .Title = "Select Employee Photo"                 .ShowDialog()             End With         If OpenFileDialog1.FileName <> "" Then             Dim fs As New IO.FileStream(OpenFileDialog1.FileName, _             IO.FileMode.Open)             ReDim bytArray(Convert.ToInt32(fs.Length - 1))             fs.Read(bytArray, 0, bytArray.Length - 1)             mobjEmployee.Photo = bytArray             fs.Close()             picPhoto.Image = Image.FromFile(OpenFileDialog1.FileName)        End If         Catch exc As Exception             LogException(exc)         End Try     End Sub 
end example

The first part of the method simply displays an Open File dialog box so that the user can select a photograph. Next, you open the file using a filestream, then you initialize a byte array to the size of the file to hold the image, and then you read the contents of the filestream into the byte array. Finally, you assign the byte array to the photo property and load the picture from the file into the picturebox.

Caution

If you try to load the file into the picturebox first and then read it into a byte array, you will receive an error stating that the file is in use.

Listing 9-32 shows the code for the rules button and the broken rule event from your Employee object.

Listing 9-32: Displaying the Business Rules and Enabling/Disabling the OK Button

start example
 Private Sub btnRules_Click(ByVal sender As System.Object, _     ByVal e As System.EventArgs) Handles btnRules.Click         Dim frmRules As New frmBusinessRules(mobjEmployee.GetBusinessRules)         frmRules.ShowDialog()         frmRules = Nothing     End Sub     Private Sub mobjEmployee_BrokenRule(ByVal IsBroken As Boolean) _     Handles mobjEmployee.BrokenRule         If IsBroken Then             btnOK.Enabled = False         Else             btnOK.Enabled = True         End If     End Sub 
end example

Listing 9-33 is the block of code for the validation events for your properties. It is just a lot of code, and it is nothing you have not seen before.

Listing 9-33: The Employee Validation Events

start example
 #Region " Validate Events"     Private Sub txtFirstName_Validated(ByVal sender As Object, _     ByVal e As System.EventArgs) Handles txtFirstName.Validated         Dim txt As TextBox = CType(sender, TextBox)         Try             mobjEmployee.FirstName = txt.Text             erpmain.SetError(txt, "")         Catch exc As Exception             erpmain.SetError(txt, "")             erpmain.SetError(txt, exc.Message)         End Try     End Sub     Private Sub txtLastName_Validated(ByVal sender As Object, _     ByVal e As System.EventArgs) Handles txtLastName.Validated         Dim txt As TextBox = CType(sender, TextBox)         Try             mobjEmployee.LastName = txt.Text             erpmain.SetError(txt, "")         Catch exc As Exception             erpmain.SetError(txt, "")             erpmain.SetError(txt, exc.Message)         End Try     End Sub     Private Sub cboCourtesy_Click(ByVal sender As Object, _     ByVal e As System.EventArgs) Handles cboCourtesy.Validated         Dim cbo As ComboBox = CType(sender, ComboBox)         Try             mobjEmployee.TitleOfCourtesy = cbo.Text             erpmain.SetError(cbo, "")         Catch exc As Exception             erpmain.SetError(cbo, "")             erpmain.SetError(cbo, exc.Message)         End Try     End Sub     Private Sub txtTitle_TextChanged(ByVal sender As System.Object, _     ByVal e As System.EventArgs) Handles txtTitle.Validated         Dim txt As TextBox = CType(sender, TextBox)         Try             mobjEmployee.Title = txt.Text             erpmain.SetError(txt, "")         Catch exc As Exception             erpmain.SetError(txt, "")             erpmain.SetError(txt, exc.Message)         End Try     End Sub     Private Sub cboReportsTo_Click(ByVal sender As Object, _     ByVal e As System.EventArgs) Handles cboReportsTo.Validated         Dim cbo As ComboBox = CType(sender, ComboBox)         Try             mobjEmployee.ReportsTo = CType(cbo.SelectedItem, Employee)             erpmain.SetError(cbo, "")         Catch exc As Exception             erpmain.SetError(cbo, "")             erpmain.SetError(cbo, exc.Message)         End Try     End Sub     Private Sub dtpBirthDate_Validated(ByVal sender As Object, _     ByVal e As System.EventArgs) Handles dtpBirthDate.Validated         Dim dtp As DateTimePicker = CType(sender, DateTimePicker)         Try             mobjEmployee.BirthDate = dtp.Value             erpmain.SetError(dtp, "")         Catch exc As Exception             erpmain.SetError(dtp, "")             erpmain.SetError(dtp, exc.Message)         End Try     End Sub     Private Sub dtpHireDate_Validated(ByVal sender As Object, _     ByVal e As System.EventArgs) Handles dtpHireDate.Validated         Dim dtp As DateTimePicker = CType(sender, DateTimePicker)         Try             mobjEmployee.HireDate = dtp.Value             erpmain.SetError(dtp, "")         Catch exc As Exception             erpmain.SetError(dtp, "")             erpmain.SetError(dtp, exc.Message)         End Try     End Sub     Private Sub txtHomePhone_Validated(ByVal sender As Object, _     ByVal e As System.EventArgs) Handles txtHomePhone.Validated         Dim txt As TextBox = CType(sender, TextBox)         Try             mobjEmployee.HomePhone = txt.Text             erpmain.SetError(txt, "")         Catch exc As Exception             erpmain.SetError(txt, "")             erpmain.SetError(txt, exc.Message)         End Try     End Sub     Private Sub txtExtension_Validated(ByVal sender As Object, _     ByVal e As System.EventArgs) Handles txtExtension.Validated         Dim txt As TextBox = CType(sender, TextBox)         Try             mobjEmployee.Extension = txt.Text             erpmain.SetError(txt, "")         Catch exc As Exception             erpmain.SetError(txt, "")             erpmain.SetError(txt, exc.Message)         End Try     End Sub     Private Sub txtAddress_Validated(ByVal sender As Object, _     ByVal e As System.EventArgs) Handles txtAddress.Validated         Dim txt As TextBox = CType(sender, TextBox)         Try             mobjEmployee.Address = txt.Text             erpmain.SetError(txt, "")         Catch exc As Exception             erpmain.SetError(txt, "")             erpmain.SetError(txt, exc.Message)         End Try     End Sub     Private Sub txtCountry_Validated(ByVal sender As Object, _     ByVal e As System.EventArgs) Handles txtCountry.Validated         Dim txt As TextBox = CType(sender, TextBox)         Try             mobjEmployee.Country = txt.Text             erpmain.SetError(txt, "")         Catch exc As Exception             erpmain.SetError(txt, "")             erpmain.SetError(txt, exc.Message)         End Try     End Sub     Private Sub txtPostalCode_Validated(ByVal sender As Object, _     ByVal e As System.EventArgs) Handles txtPostalCode.Validated         Dim txt As TextBox = CType(sender, TextBox)         Try             mobjEmployee.PostalCode = txt.Text             erpmain.SetError(txt, "")         Catch exc As Exception             erpmain.SetError(txt, "")             erpmain.SetError(txt, exc.Message)         End Try     End Sub     Private Sub txtRegion_Validated(ByVal sender As Object, _     ByVal e As System.EventArgs) Handles txtRegion.Validated         Dim txt As TextBox = CType(sender, TextBox)         Try             mobjEmployee.Region = txt.Text             erpmain.SetError(txt, "")         Catch exc As Exception             erpmain.SetError(txt, "")             erpmain.SetError(txt, exc.Message)         End Try     End Sub     Private Sub txtCity_Validated(ByVal sender As Object, _     ByVal e As System.EventArgs) Handles txtCity.Validated         Dim txt As TextBox = CType(sender, TextBox)         Try             mobjEmployee.City = txt.Text             erpmain.SetError(txt, "")         Catch exc As Exception             erpmain.SetError(txt, "")             erpmain.SetError(txt, exc.Message)         End Try     End Sub     Private Sub rtbNotes_Validated(ByVal sender As Object, _     ByVal e As System.EventArgs) Handles rtbNotes.Validated         Dim rtb As RichTextBox = CType(sender, RichTextBox)         Try             mobjEmployee.Notes = rtb.Text             erpmain.SetError(rtb, "")         Catch exc As Exception             erpmain.SetError(rtb, "")             erpmain.SetError(rtb, exc.Message)         End Try     End Sub #End Region 
end example

Finally, you get to the OK button click event, which is shown in Listing 9-34. There is one difference in this method that you have not seen before.

Listing 9-34: The OK Button Click Event

start example
 Private Sub btnOK_Click(ByVal sender As System.Object, _     ByVal e As System.EventArgs) Handles btnOK.Click         Try             If mobjEmployee.IsDirty Then                 Cursor = Cursors.WaitCursor                 mblnErrors = False                 mobjEmployee.Save()             End If         If Not mblnErrors Then             Close()         End If     Catch exc As Exception         LogException(exc)     Finally         Cursor = Cursors.Default     End Try End Sub 
end example

This is the first time you are not just automatically closing the form, which you did on the two previous edit forms. This is because this is the first time that you can break a rule on the server that you cannot break on the workstation.

Note

This does not mean that the previous two edit forms should not be set up this way. In a real application, all edit forms, even the ones that share identical rules between the data-centric and user-centric classes, should be set up this way. What if you decide to add additional business logic later? Then you are in for a lot of rework.

So, where does the mblnErrors variable get set? The answer is in Listing 9-35, which you have also not implemented up to this point.

Listing 9-35: Trapping Employee Server-Side Errors

start example
 Private Sub mobjEmployee_Errs(ByVal obj As _ NorthwindTraders.NorthwindShared.Errors.BusinessErrors) Handles _ mobjEmployee.Errs      Try           Dim i As Integer           Dim ctl As Control           For Each ctl In Me.Controls                If TypeOf ctl Is TextBox Or TypeOf ctl Is ComboBox _                Or TypeOf ctl Is RichTextBox Then                     erpMain.SetError(ctl, "")                End If           Next           For i = 0 To obj.Count - 1                Select Case obj.Item(i).errProperty                     Case "Last Name"                          erpMain.SetError(Me.txtLastName, _                          obj.Item(i).errMessage)                     Case "First Name"                          erpMain.SetError(Me.txtFirstName, _                          obj.Item(i).errMessage)                     Case "Title"                          erpMain.SetError(Me.txtTitle, _                          obj.Item(i).errMessage)                     Case "Title Of Courtesy"                          erpMain.SetError(Me.cboCourtesy, _                          obj.Item(i).errMessage)                     Case "Birth Date"                          erpMain.SetError(Me.dtpBirthDate, _                          obj.Item(i).errMessage)                     Case "Hire Date"                          erpMain.SetError(Me.dtpHireDate, _                          obj.Item(i).errMessage)                     Case "Address"                          erpMain.SetError(Me.txtAddress, _                          obj.Item(i).errMessage)                     Case "City"                          erpMain.SetError(Me.txtCity, _                          obj.Item(i).errMessage)                     Case "Region"                          erpMain.SetError(Me.txtRegion, _                          obj.Item(i).errMessage)                     Case "Postal Code"                          erpMain.SetError(Me.txtPostalCode, _                          obj.Item(i).errMessage)                     Case "Country"                          erpMain.SetError(Me.txtCountry, _                          obj.Item(i).errMessage)                     Case "Home Phone"                          erpMain.SetError(Me.txtHomePhone, _                          obj.Item(i).errMessage)                     Case "Extension"                          erpMain.SetError(Me.txtExtension, _                          obj.Item(i).errMessage)                     Case "Notes"                          erpMain.SetError(Me.rtbNotes, _                          obj.Item(i).errMessage)                     Case "Territories"                          erpMain.SetError(Me.lstTerritories, _                          obj.Item(i).errMessage)                End Select           Next           mblnErrors = True         Catch exc As Exception             logexception(exc)         End Try End Sub 
end example

The For..Each block of code simply clears any existing errors from each of the controls. The Select..Case block of code is the one that some people may chastise me for because it sort of ties the data-centric objects to the user interface. The property name that the code is looking for is the property name that you gave to the attribute in the data-centric object, which means that the user interface needs to know what an object two layers away is doing with it. To create an efficient error handling routine that reports all of the errors at once, this is the only way to do it. Now, with .NET you can get really fancy and play some neat tricks with attributes on class members (which you will see in the next chapter), but you still need to know something about the class—period. The reason why I said this sort of ties the user interface to the data-centric objects is because your user interface does not have to do anything with this information.

Next you set the errors flag so that the form will not close. Listing 9-36 shows the code for the Closing event of the edit form.

Listing 9-36: Employee Edit Form Closing Event

start example
 Private Sub frmEmployeeEdit_Closing(ByVal sender As Object, ByVal e As _ System.ComponentModel.CancelEventArgs) Handles MyBase.Closing      Dim dlgResult As DialogResult      Try           If mobjEmployee.IsDirty Then                dlgResult = MessageBox.Show("The Employee information has " _                & "changed, do you want to exit without saving your " _                & "changes?", "Confirm Cancel", MessageBoxButtons.YesNo, _                MessageBoxIcon.Question)                If dlgResult = DialogResult.No Then                     e.Cancel = True                Else                     mobjEmployee.Rollback(mobjTerritoryMgr)                End If           End If      Catch exc As Exception           LogException(exc)      End Try End Sub 
end example

As with our other forms, you simply check the status of our object so that the form does not close prematurely.




Building Client/Server Applications with VB. NET(c) An Example-Driven Approach
Building Client/Server Applications Under VB .NET: An Example-Driven Approach
ISBN: 1590590708
EAN: 2147483647
Year: 2005
Pages: 148
Authors: Jeff Levinson

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net