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.
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.
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. |
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
<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
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
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
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)
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()
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.
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)
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
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)
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
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)
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
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.
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
#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
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
#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
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
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
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
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
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
Public Sub New(Optional ByVal blnLoad As Boolean = True) If blnLoad Then Load() End If End Sub
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
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
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
Public Sub Delete() Dim objIEmp As IEmployee objIEmp = CType(Activator.GetObject(GetType(IEmployee), _ AppConstants.REMOTEOBJECTS & REMENTRY), IEmployee) objIEmp.Delete(mintEmployeeID) objIEmp = Nothing End Sub
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
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
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
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
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.
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.
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
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
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.
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.
Figure 9-2: The Employee Edit form
To create the form, add the controls and set their properties as in Table 9-2.
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).
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
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
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
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
Listing 9-31 shows the code to add a photograph to your employee.
Listing 9-31: Associating a Photograph with an Employee
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
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
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
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
#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
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
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
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
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
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
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
As with our other forms, you simply check the status of our object so that the form does not close prematurely.