Visual Basic 6 Business Objects
Adding the Project
Let's put together a very simple form to act as an interface for a Person object. We'll build this in a new Standard EXE project called PersonDemo. We can add this project to the same Visual Basic session that we used to build the PersonObjects project: simply choose the File-Add Project... option, rather than File-New Project, and choose Standard EXE. Don't forget to change the name of this second project to PersonDemo using its Properties window.
When we run our project with other projects loaded inside the IDE, Visual Basic will break us into the debugger in any of the projects that are loaded, making this a very attractive feature for debugging. It hasn't always been this easy - in Visual Basic 4.0, we would have had to run a copy of Visual Basic for each project we wanted to debug interactively.
Creating the Form
The form has three text boxes so that the user can supply values for the social security number (txtSSN), name (txtName), and birth date (txtBirthDate). It also has a label set up to display the person's age (lblAge), as well as standard OK, Cancel and Apply buttons (cmdOK, cmdCancel and cmdApply). Set the Enabled properties of cmdOK and cmdApply to False, and set the BorderStyle property of lblAge to 1 - Fixed Single.
Referencing the PersonObjects Project
Before we start putting code behind the form, we need to make sure our new project has access to our Person class. Even though both projects are running within the same Visual Basic window, we still need to add a reference from our UI project back to the ActiveX DLL we created.
To do this, we need to choose the Project-References menu option to bring up the References dialog window. Then find the entry for PersonObjects and check the box to the left:
When we click OK, we'll establish a reference from our UI project back to our in-process server, PersonObjects. This will give our program access to all the public classes in that project. In this case, we'll get access to the Person class.
Adding the Code
Now let's put some code behind the Edit Person form:
Option Explicit Private WithEvents objPerson As Person Private Sub cmdApply_Click() ' save the object End Sub Private Sub cmdCancel_Click() ' do not save the object Unload Me End Sub Private Sub cmdOK_Click() ' save the object Unload Me End Sub Private Sub Form_Load() Set objPerson = New Person End Sub Private Sub objPerson_NewAge() lblAge = objPerson.Age End Sub Private Sub txtBirthdate_Change() If IsDate(txtBirthdate) Then objPerson.Birthdate = txtBirthdate End Sub Private Sub txtName_Change() objPerson.Name = txtName End Sub Private Sub txtSSN_Change() objPerson.SSN = txtSSN End Sub
There's nothing terribly complex going on here.
At the top, we declare a variable to hold our Person object and then, in the Form_Load event, we create a Person object and store a reference to it in the objPerson variable. The only point worth making, here, is that we're using the WithEvents keyword as we declare the objPerson variable. This is what allows our program to receive the NewAge event that the Person object might raise. We don't have to use the WithEvents keyword, but without it we won't be able to receive the event.
If we do receive a NewAge event, it will be handled in the objPerson_NewAge routine. In this case, we want to update the age value that's displayed in lblAge with the objPerson.Age property.
Each text box has a Change event subroutine that simply takes the value from the control and puts it into the corresponding property of our Person object. We are trusting the business object, Person, to handle any validation or business rules, so there's no reason to worry about that here in the UI.
While we could use the new Validate event that was introduced in Visual Basic 6.0, the Change event allows us to apply our business logic as the user enters each keystroke, providing the richest possible feedback to the user. Additionally, the Validate event suffers from the same limitation as the LostFocus event, in that it wont be fired if the user presses Enter to take the default button action or Escape to take the cancel button action. The Change event is the only way to ensure beyond all doubt that our business logic is applied to each field as the data is entered by the user.
The OK, Cancel and Apply button code is intentionally vague. How we support these buttons from within our business object is tricky. The solution we're going to see is both elegant and powerful, but we'll need to beef up our Person object a bit before we're ready to cover it.
Running the Program
Since it is from the PersonDemo project that our UI calls the business objects, we need to make sure that it's from the PersonDemo project that the program starts to run. To set this up, select the PersonDemo project in the Project Group window, right-click the mouse to get the context menu, and select Set as Start Up. Now, when we run the program, Visual Basic will begin execution within the PersonDemo project.
At this point, we should be able to choose the Run-Start menu option or press F5 to run the program and interact with our Person object.
This seems like a pretty nice, straightforward solution. However, we'll soon find that we have a couple problems with our implementation, so let's take a look at them.
Enforcing Field-Level Validation
Looking at the Person class, we see that it should only store 11 characters for the SSN and 50 characters for the Name - at least according to our user-defined type, PersonProps:
Private Type PersonProps SSN As String * 11 Name As String * 50 Birthdate As Date Age As Integer End Type
Here's the situation: if we run the program and enter text into the SSN or Name fields on the form, we'll find that we can enter as many characters as we like. This is obviously a problem.
While we could simply set the MaxLength property on the form's fields, we haven't solved the underlying problem. After all, suppose the next interface is an Excel spreadsheet, where we don't have a MaxLength property. This is a clear case where the object needs to protect itself.
Raising an Error from the Person Object
The easiest way for the object to indicate that it has a problem with a value is just to raise an error. So let's alter the SSN Property Let in our Person class as follows:
Public Property Let SSN(Value As String) If Len(Value) > 11 Then _ Err.Raise vbObjectError + 1001, "Person", "SSN too long" udtPerson.SSN = Value End Property
Now the form will be notified if the user's entry is invalid. We might still want to set the MaxLength property on the form's field but, if we don't, then at least there will be some indication that there was a problem - and the object will have protected itself.
Trapping the Error in the UI
Of course, the form will need some extra code to handle the error that's raised. How that error is handled is up to the individual UI designer. It could be handled as simply as sounding a beep and resetting the displayed value. Add this code to the txtSSN_Change routine in the EditPerson form:
Private Sub txtSSN_Change() On Error GoTo HandleError objPerson.SSN = txtSSN Exit Sub HandleError: Beep txtSSN = objPerson.SSN txtSSN.SelStart = Len(txtSSN) End Sub
Since we're handling our own error situation here, it's necessary to set the Visual Basic environment to break only on those errors that we aren't handling ourselves. To set this up, we need to make sure that the Visual Basic environment will only Break on Unhandled Errors. In Visual Basic, choose the Tools-Options dialog, select the General tab, and make sure that Break on Unhandled Errors is selected.
Also note that we didn't use On Error Resume Next, but instead used a labeled error handler. With Visual Basic 5.0 and later, this is an important performance consideration, since the native code compiler adds a lot of extra code to support On Error Resume Next, so a labeled error handler is much more efficient.
Running the Program
At this point, we should be able to run our program and see how this works. Go to the SSN field and try to enter a value longer than 11 characters. The code we just added should prevent us from entering any value longer than 11 characters.
The beauty of this solution is that the code in the form only deals with the presentation; it doesn't enforce any business rules. We could add extra checks in the Person class to prevent entry of alpha characters in the field, or change the maximum length, and there would be no impact on the code in the form.
Enforcing Object-Level Validation
By raising errors when a field is given an invalid value, we've provided field-level validation through our business object. It's also important to provide object-level validation to the largest degree possible.
Object-level validation checks the entire object - all the properties and internal variables - to make sure that everything is acceptable and all business rules have been met. This is important to the UI developer, since they may want to disable the OK and Apply buttons when the object is not valid and cannot be saved.
Object-level validation is more difficult to implement than field-level validation, since it depends upon more factors. An object might be valid only when all fields have values filled in; or perhaps a field is required only when another field contains a specific value. Virtually anything is possible, since the rules are dictated by the business requirements for the object.
Worse still, some object-level validation may not be possible in the business object on the client. Some business rules might really be relational rules in the database, so it's possible that we won't know they aren't being met until we try to save the object's data.
Granted, we can't guarantee that an object is valid according to rules enforced by the application services; but that shouldn't stop us from indicating whether the object meets all the conditions that can be checked right there on the client workstation. If we do that, then at least the UI can disable the OK and Apply buttons most of the time it's actually appropriate to do so, and we'll be moving towards a better interface.
An IsValid Property
To provide this functionality for the UI, let's just add a single Boolean property to the Person object, called IsValid. This property will return a True value if all the business rules are met (or at least, as far as our object can tell that they've all been met).
If our object has an IsValid property, then the form can check to see if the object is valid at any point. If the IsValid property returns False then the form knows that the object has at least one broken business rule, and the OK and Apply buttons can be disabled.
This implies that the IsValid property's value is based on all the business rules in the object. That can make things pretty complex, since an object might have a lot of business rules to check. For instance, we may have rules specifying which properties are required. We may also have rules that specify that a field must be blank if another has a value - or just about anything else we can think up.
There are a number of ways to implement an IsValid property. For instance, we could code all the business rules into the IsValid routine itself. Then, every time the property was checked, we'd just run through a series of checks to make sure all the conditions were met. This approach can be a serious performance problem, however, if we have a lot of rules - or if some of our rules are complex and hard to check.
Another possibility is to keep a Private variable to keep track of the number of rules that are broken at any given time. As property values change, we can check all the appropriate rules and change this counter as needed. This solution can provide better performance, but can be very difficult to implement.
In particular, when we check a rule we have no way of knowing if it was already broken or if it's newly broken due to some new data. If it was already broken then we don't want to increment our rule-broken count; but if it's a newly broken rule, then the rule-broken count needs to be upped by one.
One very good way to implement the IsValid property is to keep a collection of the rules that are broken within the object. If there are no broken rules in our collection, then we know the object is valid. As we check each rule, we can also use the collection to track whether the rule was already broken, is broken now, or is unbroken. This is a very useful concept, and one that we'll use in Chapter 5 - where we implement quite a number of business objects.
To make all this easier, let's create a BrokenRules class to help manage this collection for us. We'll then be able to use this class whenever we need to implement an object-level IsValid property in our business objects. We'll certainly be seeing it in action when we implement the objects for our video rental store application, in Chapter 5.
The BrokenRules Class
The BrokenRules class has one purpose, and that is to make it easy for our business objects to keep track of their business rules and how many are broken at any given time. If there are no broken business rules then our business object can return a True value from its IsValid property.
Creating the BrokenRules Class
Since this class will be used exclusively by our business objects as they keep track of their broken business rules, we'll need to add the BrokenRules class module to any business object projects we may be developing.
For instance, in the Person project that we've been developing in this chapter, we will need to add the BrokenRules class to our PersonObjects project - since that's where our business object resides in this example.
So add a new class module to the PersonObjects project, using the Project-Add Class Module menu option, and change the name of the new class module to BrokenRules using its Properties window.
Go ahead and enter the following code for the BrokenRules class, and then we'll walk through how it works.
Option Explicit Event BrokenRule() Event NoBrokenRules() Private colBroken As Collection Private Sub Class_Initialize() Set colBroken = New Collection End Sub Public Sub RuleBroken(Rule As String, IsBroken As Boolean) On Error GoTo HandleError If IsBroken Then colBroken.Add True, Rule RaiseEvent BrokenRule Else colBroken.Remove Rule If colBroken.Count = 0 Then RaiseEvent NoBrokenRules End If HandleError: End Sub Public Property Get Count() As Integer Count = colBroken.Count End Property
Once you've entered this code, make sure you save it, because we'll be using it throughout the development of our video rental project.
Our BrokenRules class first declares a collection variable and two events:
Event BrokenRule() Event NoBrokenRules() Private colBroken As Collection
The colBroken collection will be used to store exactly which rules have been broken; meanwhile, the events that we've declared will be raised when a rule is broken or when the broken-rule count goes to zero.
The real work in the class is done in the RuleBroken routine:
Public Sub RuleBroken(Rule As String, IsBroken As Boolean) On Error GoTo HandleError If IsBroken Then colBroken.Add True, Rule RaiseEvent BrokenRule Else colBroken.Remove Rule If colBroken.Count = 0 Then RaiseEvent NoBrokenRules End If HandleError: End Sub
The calling code, in our Person object, just passes this routine a label for the rule and a Boolean flag to indicate whether the rule was broken or not. Then, we just check that Boolean flag: if it's True then the rule was broken, so we make sure it's in the collection:
If IsBroken Then colBroken.Add True, Rule RaiseEvent BrokenRule
If it is already in the collection, we'll get an error and exit the routine via the HandleError label; but if it isn't already there, then we'll not only add it to the collection but also raise the BrokenRule event so the calling program knows that at least one rule has been broken.
Likewise, if IsBroken is False then we'll remove the entry from the collection:
Else colBroken.Remove Rule If colBroken.Count = 0 Then RaiseEvent NoBrokenRules
If the entry is not in the collection, an error will occur and we'll exit the routine via the HandleError label. If the entry was in the collection, then we'll see if the overall count of broken rules is down to zero. When the count reaches zero, the NoBrokenRules event will be fired so the calling code can tell that everything is valid.
Of course, events aren't universally supported: we can't get them just anywhere within Visual Basic itself, and we can't necessarily use them in all other environments. Other environments can still use this class, but if they don't support events then they'll need to check the Count property for these event conditions: when Count is zero, there are no broken rules and everything should be valid.
Using the BrokenRules Object within Our Person Object
Now let's see how we can use this BrokenRules class in our Person object. To keep this fairly simple, let's just enforce a rule that states that the SSN field is required and must be exactly 11 characters in length. We've already implemented code to prevent the user from entering more than 11 characters in this field; but now we're making the rules even more restrictive.
In order to use the BrokenRules object within our Person object, we need to declare and create a Private variable to hold the new object. Add the following line of code to the General Declarations section of our Person class module:
Private WithEvents objValid As BrokenRules
We also need to add code in the Person object's Class_Initialize routine to create an object for this variable:
Private Sub Class_Initialize() Set objValid = New BrokenRules objValid.RuleBroken "SSN", True End Sub
Notice, here, that we're forcing the "SSN" rule to be considered broken straight away. This is important, because, as the programmer, we know that the value is blank to begin with, so we need to make sure the business rule is enforced right from the start by indicating that it's broken.
An important note about events. An object can't raise events until it has been fully instantiated, and an object isn't instantiated until after the Class_Initialize routine is complete. This means that any RaiseEvent statements called during the Class_Initialize routine won't actually raise any events which is what wed really like to have happen.
In this example, we're indicating that a rule is broken, so the BrokenRule event should fire; but it won't, because we're still in the Class_Initialize routine.
Handling the BrokenRule and NoBrokenRules Events
Our Person object needs to handle the events that will be created by the BrokenRules object we just created. To make it easier for the UI developer, we'll also have our Person object raise an event to let the UI know when the Person object becomes valid or invalid.
This first line that we'll add to handle these events goes in the General Declarations section of the Person class module, and it declares the Valid event. We'll use this event to indicate when our Person object switches between being valid and invalid:
Event Valid(IsValid As Boolean)
As we've seen, our BrokenRules object raises two events of its own: BrokenRule and NoBrokenRules. The BrokenRule event will be fired whenever a new rule is broken, while the NoBrokenRules will be fired any time the number of broken rules reaches zero.
By adding the following code to our Person object, we'll be able to react to these events by raising our own Valid event to tell the UI whether the Person object is currently valid:
Private Sub objValid_BrokenRule() RaiseEvent Valid(False) End Sub Private Sub objValid_NoBrokenRules() RaiseEvent Valid(True) End Sub
If the BrokenRule event is fired then we know that at least one of our Person object's rules is broken. We can then raise our Valid event with a False parameter to indicate to the UI that the Person object is currently invalid. Likewise, when we receive a NoBrokenRules event, we can raise our Valid event with a True parameter to indicate to the UI that there are no broken rules and that our Person object is currently valid.
Implementing the IsValid Property
When we started this discussion, it was with the intent of creating an IsValid property on our Person object so that the UI could check to see if the object was valid at any given point. By implementing the Valid event in the previous section, we've actually provided a better solution; but we can't assume that the UI can actually receive events, since not all development tools provide support for them. The IsValid property therefore remains very important.
Fortunately, our BrokenRules object makes implementation of the IsValid property very trivial. The BrokenRules object provides us with a count of the number of broken rules, and if that count is zero then we know that there are no rules broken. No broken business rules translates very nicely into a valid Person object.
To implement our IsValid property, let's enter the following code into the Person class module:
Public Property Get IsValid() As Boolean IsValid = (objValid.Count = 0) End Property
This property simply returns a Boolean value that's based on whether the broken rule count equals zero or not.
Enforcing the SSN Business Rules
In our Person object's Class_Initialize routine, we added a line to indicate that our SSN field was invalid. Since we're making it a required field, and all fields in a brand new object are blank, we know that the rule is broken during the initialization of the class.
We can therefore finish the job by adding some code to check our business rules within the Person object's Property Let routine for our SSN property:
Public Property Let SSN(Value As String) If Len(Value) > 11 Then _Err.Raise vbObjectError + 1001, "Person", "SSN too long" udtPerson.SSN = Value objValid.RuleBroken "SSN", (Len(Trim$(udtPerson.SSN)) <> 11) End Property
What we've done, here, is simply call the RuleBroken method of our BrokenRules object, passing it the name of our rule "SSN" and a Boolean to indicate whether the rule is currently broken. In this case, the rule just checks to make sure the value is exactly 11 characters long.
Anywhere that we need to enforce a rule in our object, we just need to add a single line of code with the rule's name and a Boolean to indicate if it's broken or unbroken. All the other details are handled through the BrokenRules object and the events that it fires.
Using the Valid Event and IsValid Property in the UI
At this point, we've implemented the BrokenRules object to keep track of how many business rules are broken within a business object. We've also enhanced our business object, Person, to take advantage of the new BrokenRules object. All that remains is to enhance our user interface to take advantage of the Valid event and IsValid property that we just added to our Person object.
A rich user-interface should be able to enable or disable the OK and Apply buttons as appropriate, so that they're only available to the user if the object is actually valid at the time. Most users dislike clicking on a button only to be told that the requested operation can't be performed. Ideally, when an operation can't be performed, the associated buttons should be disabled.
In our case, the OK and Apply buttons are only valid if the business object itself is valid. If the business object isn't valid then we can't save it, so both the OK and the Apply button should be disabled.
Adding the EnableOK Subroutine
The easiest way to manage the enabling and disabling of these buttons is to put the code in a central routine in the form. In our case, we'll need to add the following routine to our EditPerson form in the PersonDemo project:
Private Sub EnableOK(IsOK As Boolean) cmdOK.Enabled = IsOK cmdApply.Enabled = IsOK End Sub
Using the IsValid Property
When the Edit Person form loads, the first thing it needs to do is make sure that the buttons are enabled properly. We need to check the Person object's IsValid property in our Form_Load routine to find out if the business object is valid at this point. Add this line of code to the EditPerson form's Form_Load routine:
Private Sub Form_Load() Set objPerson = New Person EnableOK objPerson.IsValid End Sub
All we need to do is call our EnableOK subroutine, passing the Person object's IsValid property value as a parameter. If the object is valid, we'll be passing a True value, which will indicate that the OK and Apply buttons are to be enabled.
It might look as if we could rely on the Person object's Valid event to fire as the object was created. We could then act on that event to enable or disable the two buttons on our form. Unfortunately, objects can't raise events as they are being created, so there is no way for our Person object to raise its Valid event while it's starting up. This means we can't rely on the Valid event to tell us whether the object is valid as our form is first loading.
Responding to the Valid Event
Once the EditPerson form is loaded and the user is interacting with it, we can rely on the Person object to raise its Valid event to tell the form whether it is currently valid. In our EditPerson form, we can add code to respond to this event, enabling and disabling the OK and Apply buttons as appropriate:
Private Sub objPerson_Valid(IsValid As Boolean) EnableOK IsValid End Sub
Since the object will raise this event any time it changes from valid to invalid or back, the UI developer can rely on this event to enable or disable the buttons for the life of the form. All we need to do is call our EnableOK subroutine, passing the IsValid parameter value to EnableOK to indicate whether to enable or disable the two buttons.
Removing the SSN Change Event Code
As our form currently stands, we have code in the Change event of the txtSSN control to trap any error raised by our business object. Now that weve enhanced the business object to utilize the BrokenRules object, we dont need this code in the form. Change the Change event code as shown:
Private Sub txtSSN_Change() objPerson.SSN = txtSSN End Sub
With this change were allowing the user to enter any value into the TextBox control and thus into our object. Were relying in the business logic in the object to raise the Valid event to inform the UI when the user has entered valid data.
Page 7 of 13