Visual Basic 6 Business Objects
One approach to persisting business objects is to create an object manager object for use by the UI developer. The UI code would simply ask the object manager to load and save objects on its behalf, typically passing the business objects themselves as parameters to the Load and Save methods of the object manager.
This design is illustrated in the following figure:
As shown, the presentation tier, or UI, will interact with both our UI-centric business objects, such as our Person object, and the object manager that knows how to retrieve, save, and delete our business objects.
Sticking with the Person object we've used so far, let's take a look at a PersonManager class. The PersonManager will handle all the details of retrieving, saving, and deleting Person objects from the database. The UI code will ask the PersonManager to retrieve a Person object when it wants one loaded from the database. The UI will also use it to save a Person object back to the database or to delete a Person object.
An Object Manager as an Out-of-Process Server
One of our goals is to be able to put the PersonManager object on a separate machine from the client workstation. This will allow us more flexibility in how we deploy our application, as we can put this object manager on an application server machine and increase the scalability of our application:
This figure shows where an object manager, such as PersonManager, would fit into the CSLA. It also shows how the PersonManager can be run on an application server machine separate from the client workstation.
We dont have a data-centric object in this case, as the Object Manager fills that role under this scenario. In many ways the Object Manager is analogous to a data-centric business object, but we use the Object Manager in a different manner than we would a data-centric business object.
For us to be able to run the PersonManager object on a separate machine, we'll need to implement this class in a separate Visual Basic project from the Person class.
As we discussed earlier in this chapter, there are serious performance concerns when communicating between processes or across a network. Since we're designing the PersonManager to be at least out-of-process, and very likely on another machine across the network, we need to take steps to make sure that our communication is very efficient.
Adding GetState and SetState methods to Person
To this end, we'll use the user-defined type and LSet technique discussed earlier in this chapter. In order to implement this in the Person object itself, we need to add a couple of new methods: GetState and SetState.
To use the LSet technique, we need to make sure our object's data is stored using a user-defined type. Then we need to create a second user-defined type of the same length to act as a buffer for all the detailed state data from our object.
We'll use the word state to describe the core data that makes up an object. An object can be viewed as an interface, an implementation of that interface (code), and state (data). An object could have a lot more data than is really required to define its state. The state data includes only that which must be saved and restored to return an object to its original state.
Our Person object already stores its state data in a user-defined type, PersonProps. However, this type will be needed by both the Person object and the PersonManager object, so we'll want to make it available to both. The easiest way to do this is to add a new code module to the PersonObjects project and move the PersonProps type from the Person class module into this code module. We'll call this new code module PersonUDTs:
Public Type PersonProps SSN As String * 11 Name As String * 50 Birthdate As Date Age As Integer End Type
Notice that we've also changed the scope of the user-defined type from Private to Public so that it will be available outside this code module.
Now we can add the following user-defined type to the PersonUDTs code module:
Public Type PersonData Buffer As String * 67 End Type
This type will act as a buffer for the PersonProps data, allowing us to use the LSet command to easily copy the detailed information from PersonProps into the simple string buffer declared here.
With the user-defined types set up, we're all ready to add the GetState and SetState methods to our Person class module:
Public Function GetState() As String Dim udtBuffer As PersonData LSet udtBuffer = udtPerson GetState = udtBuffer.Buffer End Function
To get the object's state, we just copy the detailed udtPerson variable into udtBuffer. The udtBuffer variable is just a String, so we can return it as the result of the function:
Public Sub SetState(ByVal Buffer As String) Dim udtBuffer As PersonData udtBuffer.Buffer = Buffer LSet udtPerson = udtBuffer CalculateAge End Sub
To set the object's state, we simply reverse the process: accepting a string buffer and copying it into udtBuffer. Then we just LSet udtBuffer's data into udtPerson, and the object is restored.
We also make a call to the CalculateAge method to ensure that the read-only Age property will return the correct value. Since the data that is passed into our object via the SetState method bypasses all our Property Let routines we need to make sure that any required processing (such as calculating the age) are performed as part of the SetState method.
Given these methods, our PersonManager object needs to make only one call to retrieve all of the data from our object, and make only one other call to send all the object's data back. As we discussed earlier, this is very fast and efficient, even over a network connection.
Cloning Business Objects with GetState and SetState
Since GetState and SetState simply copy and restore the object's state data, we can use them for other purposes than persistence. They make a built-in cloning capability for each object, since we can write code like this:
Dim objPerson1 As Person Dim objPerson2 As Person Set objPerson1 = New Person Set objPerson2 = New Person objPerson1.SetState objPerson2.GetState
The GetState method of objPerson2 simply converts that object's data into a string buffer. That buffer is then passed to the SetState method of objPerson1, which converts it back into detailed state data. We've moved all the detailed state data from objPerson2 to objPerson1 in one line of code.
The Person Object's ApplyEdit Method
In our original Person object's ApplyEdit method, we inserted a comment to indicate that this routine would be responsible for saving the object's data:
Public Sub ApplyEdit() If Not flgEditing Then Err.Raise 445 flgEditing = False flgNew = False ' data would be saved here End Sub
This isn't actually true if we intend to use an object manager like PersonManager. Instead, the PersonManager object itself will be responsible for saving the Person object's data. We don't need to make any changes to the ApplyEdit method, but it's important to recognize that the work involved in saving the Person object won't be done here and that it will be handled by the PersonManager.
It could be argued that it's possible to merge the code from ApplyEdit into the GetState method, since the GetState method will be called by PersonManager when it's saving the object - so the edit process must be complete. Unfortunately, this would introduce a side-effect into the GetState method that isn't intuitive. From the outside, just looking at the name GetState, you'd never guess that it also ends the editing process. To avoid confusion, methods should always be as descriptive as possible without unexpected side-effects, and merging these two routines could easily cause such confusion.
Creating the PersonManager Object
Now that we've got the Person object ready to go, let's build the PersonManager object. To start off, make sure you've saved the PersonObjects project and, with the File-New Project menu option, start a new ActiveX EXE project. Set the project's Project Name to PersonServer under the Project-Properties menu option.
Change the name of Class1 to PersonManager and make sure its Instancing property is set to 5-Multiuse.
Since we'll be sending data back and forth between our Person object and the PersonManager object through the use of the LSet technique, it's important that both objects have access to the PersonProps and PersonData user-defined types. Fortunately, we've put those types into the code module named PersonUDTs, so we can choose Project-Add File and add that code module to our new PersonServer project. Now both the PersonObjects and PersonServer projects have access to the exact same code module containing our user-defined types.
Now we're ready to add some code to the PersonManager class.
Adding a Load Method to PersonManager
The UI code will use the PersonManager object to load object data from the database into a new Person object. To do this, we'll implement a Load method on the PersonManager object for use by the UI developer. This is where things get interesting.
At the very least, we need to pass the Load method an identifier so that it can retrieve the right person. In this example, we'll pass the social security number, with the assumption that it provides a unique identifier for an individual.
Now we need to figure out how to get the data back to the client and into an object. Ideally, we'd like to make the Load method a function that returns a fully loaded Person object. This would mean our UI client code could look something like this:
Dim objPersonManager As PersonManager Dim objPerson As Person Set objPersonManager = New PersonManager Set objPerson = objPersonManager.Load(strSSN)
Unfortunately, this is difficult at best. In order to return an object reference, the Load method needs to create an object. When an object is created, using either New or CreateObject, it's instantiated in the same process, and on the same machine, as the code that creates it. This means that a Person object created by the PersonManager's Load method would be created, in the PersonManager object, on whatever machine that code is running.
We need the Person object to be created on the client machine, inside our client process. This means that the code to instantiate the object needs to be in that process as well. As a compromise, let's make our client code look something like this:
Dim objPersonManager As PersonManager Dim objPerson As Person Set objPersonManager = New PersonManager Set objPerson = New Person objPersonManager.Load strSSN, objPerson
This way, the object is created in the client, but we'll pass it as a reference to the PersonManager object to be loaded with data.
Given this approach, let's enter the following code, for the Load method itself, into the PersonManager class module:
Public Sub Load(ByVal SSN As String, Person As Object) Dim recPerson As Recordset Dim strConnect As String Dim strSQL As String Dim udtPerson As PersonProps Dim udtBuffer As PersonData strConnect = "Provider=Microsoft.Jet.OLEDB.3.51;" & _ "Persist Security Info=False;" & _ "Data Source=C:person.mdb" strSQL = "SELECT * FROM Person WHERE SSN='" & SSN & "'" Set recPerson = New Recordset recPerson.Open strSQL, strConnect With recPerson If Not .EOF And Not .BOF Then udtPerson.SSN = .Fields("SSN") udtPerson.Name = .Fields("Name") udtPerson.Birthdate = .Fields("Birthdate") LSet udtBuffer = udtPerson Person.SetState udtBuffer.Buffer Else recPerson.Close Err.Raise vbObjectError + 1002, "Person", "SSN not on file" End If End With recPerson.Close End Sub
Once again, this code makes reference to a Recordset object, so you may need to add a reference in your project to the ADO. Use the Project-References menu option and select the most up-to-date ADO reference, such as Microsoft ActiveX Data Objects 2.0 Library.
For the most part, this is pretty straightforward database programming, but let's walk through the routine to make sure everything is clear.
The code opens the database and builds a recordset based on a SQL statement using the social security number:
strConnect = "Provider=Microsoft.Jet.OLEDB.3.51;" & _ "Persist Security Info=False;" & _ "Data Source=C:person.mdb" strSQL = "SELECT * FROM Person WHERE SSN='" & SSN & "'" Set recPerson = New Recordset recPerson.Open strSQL, strConnect
If we successfully retrieve the data, we just load that data into our user-defined type and use LSet to copy the detailed data into a user-defined type that represents a single string buffer:
udtPerson.SSN = .Fields("SSN") udtPerson.Name = .Fields("Name") udtPerson.Birthdate = .Fields("Birthdate") LSet udtBuffer = udtPerson
Now that we've got all the data in a single string, we can just make a single call to the Person object's SetState method, as discussed above:
This technique does require that both the detail and buffer user-defined types be available to both the business object project and the PersonManager object. The best way to handle this is to put the UDT definitions in a BAS module and include that module in both projects. Better yet, if you're using source code control such as Visual SourceSafe then you can link the file across both projects and allow the source control software to keep them in sync.
Once we've called the Person object's SetState method to pass it the data from the database, the UI will have a reference to a fully loaded Person object. Then the UI code can use that Person object through its properties and methods.
Adding a Save Method to PersonManager
At some point, the UI will need to save a Person object's data to the database. To do this, it will use the PersonManager, so we'll add a Save method to the PersonManager object to handle the add and update functions.
The Save method can have a fairly simple interface, since all we really need to do is send down a reference to the object itself. The code can then directly call the GetState method of the Person object to retrieve its data. Here is the code:
Public Sub Save(Person As Object) Dim rsPerson As Recordset Dim strConnect As String Dim strSQL As String Dim udtPerson As PersonProps Dim udtBuffer As PersonData udtBuffer.Buffer = Person.GetState LSet udtPerson = udtBuffer strConnect = "Provider=Microsoft.Jet.OLEDB.3.51;" & _ "Persist Security Info=False;" & _ "Data Source=C:person.mdb" strSQL = "SELECT * FROM Person WHERE SSN='" & udtPerson.SSN & "'" Set rsPerson = New Recordset rsPerson.Open strSQL, strConnect, adLockOptimistic With rsPerson If Person.IsNew Then .AddNew .Fields("SSN") = udtPerson.SSN .Fields("Name") = udtPerson.Name .Fields("Birthdate") = udtPerson.Birthdate .Update End With rsPerson.Close End Sub
A good question, at this point, might be: why pass the object reference when we could just pass the state string returned by GetState? In this case, it would accomplish the same thing, but with one less out-of-process or network call.
Suppose, however, that the Person object also included a comment field, a dynamic string in the object, and a memo or long text field in the database. Since this variable would be dynamic in length, we couldn't put it into a user-defined type, and so we couldn't easily pass it within our state string.
In a case like this, the Save method may not only need to use the GetState method, but it may also have to use a GetComment method - which we'd implement in the Person object to return the comment string.
Basically, by passing the object reference, rather than just the state string, we've provided ourselves with virtually unlimited flexibility in terms of communication between the PersonManager and Person objects.
To save a Person object, the code in our form's cmdOK_Click and cmdApply_Click event routines will need to be updated. Open our PersonDemo project and bring up the forms code window. Change these two routines as shown:
Private Sub cmdApply_Click() Dim objPersonManager As PersonManager ' save the object Set objPersonManager = New PersonManager objPersonManager.Save objPerson objPerson.ApplyEdit objPerson.BeginEdit End Sub Private Sub cmdOK_Click() Dim objPersonManager As PersonManager ' save the object Set objPersonManager = New PersonManager objPersonManager.Save objPerson objPerson.ApplyEdit Unload 3Me End Sub
Since we're passing the Save method a reference to the objPerson object, it can retrieve the data from the Person object and write the data out to the database.
Adding a Delete Method to PersonManager
At this point, our UI code can use the PersonManager object's Load method to retrieve a Person object and the Save method to add or update a Person object into the database. The only remaining operation we need to support is removal of a Person object from the database.
To provide this support, we'll add a Delete method to the PersonManager object. We can use the same identity value for the Delete that we used for the Load; in this case, the social security number. And since we don't need the object's data, we don't need to worry about passing the object reference at all:
Public Sub Delete(SSN As String) Dim cnPerson As Connection Dim strConnect As String Dim strSQL As String strConnect = "Provider=Microsoft.Jet.OLEDB.3.51;" & _ "Persist Security Info=False;" & _ "Data Source=C:person.mdb" strSQL = "DELETE * FROM Person WHERE SSN='" & SSN & "'" Set cnPerson = New Connection cnPerson.Open strConnect cnPerson.Execute strSQL cnPerson.Close Set cnPerson = Nothing End Sub
To delete a Person object, the UI code would look like this:
Dim objPersonManager As PersonManager Set objPersonManager = New PersonManager objPersonManager.Delete objPerson.SSN
This might be implemented behind a Delete button or a menu option - whatever is appropriate for the specific user-interface.
Testing the Save Method
We should be able to immediately try out the PersonManager object's Save method. To do this, we'll need to compile our PersonServer project into an EXE. This is done using the File-Make PersonServer.exe menu option from within Visual Basic.
Once the PersonServer project has been compiled, we're almost ready to run our PersonDemo program. Load up the PersonDemo project again, and just add a reference to the PersonServer using the Project-References menu option.
Now run the PersonDemo program. The form will come up as always, allowing us to enter information into our Person object. However, with the changes we just made to the code behind the OK and Apply buttons, clicking either one should cause our Person object's data to be saved to the database by our PersonManager object.
Testing the Load Method
The Load method is a bit trickier, since we need to come up with some way to get the SSN value from the user before we can call the method. The UI code we looked at for calling the Load method assumed we already had the SSN value.
Enter the following lines into our EditPerson form's Form_Load routine; this way, we can load a Person object as the form loads:
Private Sub Form_Load() Dim objPersonManager As PersonManager Set objPerson = New Person Set objPersonManager = New PersonManager objPersonManager.Load strSSN, objPerson EnableOK objPerson.IsValid objPerson.BeginEdit End Sub
We can easily enhance this by using the InputBox$ function to ask the user for the SSN. In the PersonDemo project, add the following to the Form_Load method:
Private Sub Form_Load() Dim strSSN As String Dim objPersonManager As PersonManager Set objPerson = New Person Set objPersonManager = New PersonManager strSSN = InputBox$("Enter the SSN") objPersonManager.Load strSSN, objPerson EnableOK objPerson.IsValid objPerson.BeginEdit End Sub
This gets us almost there. If the user supplies a valid SSN value then our Person object will be loaded with the data from the database. All that remains is to update the display on the form, so let's add these lines to the Form_Load routine:
Private Sub Form_Load() Dim strSSN As String Dim objPersonManager As PersonManager Set objPerson = New Person Set objPersonManager = New PersonManager strSSN = InputBox$("Enter the SSN") objPersonManager.Load strSSN, objPerson flgLoading = True With objPerson txtSSN = .SSN txtName = .Name txtBirthDate = .BirthDate lblAge = .Age End With flgLoading = False EnableOK objPerson.IsValid objPerson.BeginEdit End Sub
Notice, here, that we're using the module-level variable trick we saw in an earlier discussion where we looked at objects that save themselves: we set flgLoading to True while we're loading information into our form, so that we can switch off the form's Change events of the text fields.
Therefore, we also need to declare this module-level variable in the General Declarations area of our EditPerson form:
Private flgLoading As Boolean
and we need to add this line to all the Change events in the EditPerson form:
Private Sub txtName_Change() If flgLoading Then Exit Sub objPerson.Name = txtName End Sub
If we run our PersonDemo program now, we'll be prompted for an SSN value. Entering a valid SSN should cause that Person object to be displayed. Of course, we don't have any error trapping for invalid SSN entries, but this demonstrates the basic concepts of saving and restoring an object from the database. We'll build a more robust application based on these general techniques starting in Chapter 5.
Page 11 of 13