Building a distributed application
Building The Monolith

We'll start with a single PBL called 'pbaccess.pbl'. You can find this PBL in the code you downloaded under the /monolith directory. Here's what the PBL will contain when we are done building the monolithic application. -

azdpbmonolith.gif (2307 bytes)

Lets go over the building process step by step.

The Application

Lets start with creating a new application called pbaccess. The open event of the application contains typical code that connects to the database and then opens the w_employee window. The database disconnect is coded in the close event.

pbaccess::open

// Profile Powersoft Demo DB V6
SQLCA.DBMS = "ODBC"
SQLCA.Database = "Powersoft Demo DB V6"
SQLCA.AutoCommit = False
SQLCA.DBParm = "ConnectString='DSN=Powersoft Demo DB V6;UID=dba;PWD=sql'"

connect;

Open(w_employee)

pbaccess:close

Disconnect;

 

The Datawindow Objects

Next, lets create the two datawindows. Let take some time here and think about how we are going to tie-up the list and freeform datawindows. We know the way the user interface is required to work. The user hits the 'Refresh' button to retrieve the data into the left hand side datawindow. Then, when he clicks on a row in this datawindow the same row is shown to him in the adjacent freeform datawindow, where he can edit it.  The simplest solution would be to retrieve all the rows with all the columns in the list datawindow and then make the freeform datawindow share data with the list datawindow. Easy? Yes. But is it really efficient? Each employee record carries a lot of information and there will be hundreds of such records in the employee table. The user will want to edit just a few of them at any given time. Does it really justify bringing all the complete records to the client? On most networks bandwidth is on a premium. And our future plans to split the application will only aggravate the bandwidth situation, since typical distributed applications cause greater transfer of data across the network.

So we'll create a list datawindow, dw_employee_list, such that it retrieves all the records with only the emp_id, emp_fname, emp_lname columns from the employee table. Then we'll create the freeform datawindow that will select all the columns for a given emp_id as the retrieval argument. When we build the window we'll tie up the retrieval of the freeform datawindow with the current selection in the list datawindow.

When I advocate this strategy, people sometimes ask me - 'What if the user goes on a clicking spree? Won't that actually increase the number of requests for data?'. I must agree that it will. But in response, I ask them - do you really think that your users have nothing better to do than play with your application? Most users I've seen just want to get their work done and want to get it done without delays.When each row in the table carries a lot of information and there can be potentially a large number of rows, this approach definitely improves the user's perceived performance. I've seen it work well in PowerBuilder applications that access data across a low bandwidth WAN. This approach should also pay off when we go in for Web.PB/HTML clients.

Building the NVO

Next, we come the most important part of building the NVO 'nvo_employee'. As I said earlier this NVO will encapsulate the business logic as well as the database access logic. The code that handles presentation will need to exchange data and changes to that data with this NVO. Even though this sounds a little complex, with PowerBuilder 6.x this is a breeze cause we will be using the new datawindow state functions. I hope you are familiar with these functions. If not, check out the online help files, the online books.

First we need a method to retrieve a list of employees. Here it is -

nvo_employee::of_getlist() returns blob

Datastore    ds
Blob         blb_fullstate

ds = Create Datastore

ds.DataObject = 'dw_employee_list'

ds.SetTransObject(SQLCA)
ds.Retrieve()
ds.GetFullState(blb_fullstate)

Destroy ds

Return blb_fullstate

This code creates a datastore, assigns it the list datawindow object and then retrieves the datastore. The retrieved data is sent to the caller as a blob from the function GetFullState(). This blob contains all the retrieved rows, item status information as well the definition of the dw_employee_list dataobject.

Next, we need a function that will retrieve just a single row from the employee table given the emp_id. This is the data that the user will modify. Hence we will have to come up with some persistence mechanism here. Our persistence object is going to be a datastore 'ids_employee', declared as an instance variable. Whenever the NVO gets a request for a row, we'll retrieve the row and hold it in this datastore. We'll create the datastore in the constructor and destroy it in the destructor. Here's the code from the two events -

nvo_employee::constructor

ids_employee = Create Datastore
ids_employee.DataObject = 'dw_employee'
ids_employee.SetTransObject(SQLCA)

nvo_employee::destructor

Destroy ids_employee
Disconnect;

And, here's code for the function that will retrieve a single row into the internal datastore and then pass it to the caller as a blob-

nvo_employee::of_remoteretrieve_employee(integer id) returns blob

Blob             blb_fullstate

ids_employee.Retrieve(id)
ids_employee.GetFullState(blb_fullstate)

Return blb_fullstate

 

The code for validating salary is in -

nvo_employee::of_validateSalary(decimal adec_salary) returns boolean

//Check if the salary is between 10,000 and 200,000
If adec_salary >= 10000 And adec_salary <= 200000 Then
    Return True
Else
    Return False
End If

 

Finally, we need a function that will update the changes made by the user to the database. But the user makes the changes in a different datawindow. How will this NVO ever figure out what changes the user made? The datawindow state functions come to the rescue again. The presentation code will obtain the changes made by the user to the freeform datawindow as a blob using the GetChanges() function and pass will it to the NVO. The NVO will just apply those changes to the data in the internal datastore and then simply update it. Here's the function -

nvo_employee::of_remoteupdate(blob ablb_changes) returns integer

Integer rv

ids_employee.SetChanges(ablb_changes)

rv = ids_employee.Update()

If rv = 0 Then
    RollBack;
    Return -1
Else
    Commit;
    Return 0
End If

 

The Presentation Layer

We've built the persistence logic and business logic. Now we just have to build the presentation layer that will tie-up everything together. We start off with creating the window 'w_employee'. On this window place the two datawindows and the four command buttons. Don't assign any datawindow objects to the datawindow. Yeah, you read that right. You don't have to do that, because the blob returned by GetFullState() also contains the datawindow object definition. Isn't that simply fabulous?! It allows us to completely encapsulate the database access logic into a single object. In cases where you need only the datawindow object definition without the data, I would still discourage associating the datawindow object at design time. Instead, write another function in the business object that just returns the value for Object.DataWindow.Syntax.

The window declares an instance variable called 'invo_emp' for the business object 'nvo_employee' as. Lets go over the various events and functions I've coded in the window and it's controls.

w_employe::Open

myconn = Create nvo_employee
post event ue_refresh()

Nothing special here; just instantiated the business object and post the event that will retrieve data into the list datawindow.

w_employe::Close

Destroy invo_emp

w_employe::ue_refresh

Blob blb_fullstate
blb_fullstate = invo_emp.of_getlist()
dw_list.SetFullState(blb_fullstate)


//The user can only select a row, so make DW read only
dw_list.Object.DataWindow.ReadOnly = 'Yes'

This code calls the business object function to provide a list of employees. The blob returned is applied to the list datawindow. Note that first 3 lines are essentially just a replacement of your typical Retrieve() function in a 2-tiered application.

The code in the 'rowfocuschanged' event of dw_list retrieves the complete record for the currently selected employee into the freeform datawindow.

dw_1::rowfocuschanged

If currentrow < 1 Then Return

//select the current row
this.SelectRow(0, False)
this.SelectRow(currentrow, True)

//Set the redraw off to avoid flicker
dw_employee.SetRedraw(False)

Blob blb_fullstate
blb_fullstate = invo_emp.of_remoteretrieve_employee(this.Object.emp_id[currentrow])
dw_employee.SetFullState(blb_fullstate)

dw_employee.SetRedraw(True)

There can be a slight problem here. What if the user changes a record but selects another record without saving? These are the times when the new rowfocuschanged event is really handy. Here's the code for the event and related function that offers the user to save changes-

dw_1::rowfocuschanging

//If there are unsaved changes check if the user would like to save them
//before continuing
If Parent.of_checkChanges() Then

    Return 0
Else
    //User doesn't want to continue
    Return 1
End If

w_employee::of_checkChanges() returns boolean

//If there are unsaved changes ask the user if he would like to -
//Save the changes
//Continue without saving, or
//Cancel the whole operation

Integer    i_ans

If dw_employee.ModifiedCount() > 0 Then
    i_ans = MessageBox("Unsaved Changes", "Would like to save the changes to this record?", Question!, YesNoCancel!)
    If i_ans = 1 Then
        this.Event ue_save()    //save changes
        Return True
    ElseIf i_ans = 2 Then
        Return True              //Forget the changes and just go ahead
    Else
        Return False             //Cancel the operation
    End If
Else
    Return True    //No changes, go ahead
End If

The code in the buttons is self explanatory -

cb_refresh::clicked

Parent.event ue_refresh()

cb_add::clicked

//Save the changes if the user has any unsaved changes
If Parent.of_checkChanges() Then
    //Reset the datawindow and insert a row
    dw_employee.Reset()
    dw_employee.InsertRow(0)
End If

cb_delete::clicked

//Delete the current row and save immediately
Integer    i_ans

i_ans = MessageBox("Delete", "Are you sure you want to delete this record?", Question!, YesNo!)
If i_ans = 1 Then
    dw_list.DeleteRow(0)
    dw_employee.DeleteRow(1)
    Parent.Event ue_save()
Else
    Return //Cancel delete
End If

cb_save::clicked

parent.event ue_save()

The ue_save event of the window validates modified rows using the business objects of_validSalary() method. If you have other pre-save validations you can perform them here. The changes made to the datawindow are captured into a blob using GetChanges() and sent to the business object which takes care of updating the database.

If dw_employee.AcceptText() = -1 Then Return

Blob    blb_changes
Long    l_count,    l_row
Dec    dec_salary
//Validate changed rows
l_count = dw_employee.RowCount()

For l_row = 1 To l_count
    If dw_employee.GetItemStatus(l_row, 0, Primary!) = NewModified! Or &
        dw_employee.GetItemStatus(l_row, 0, Primary!) = DataModified! Then
       
        dec_salary = dw_employee.Object.salary[l_row]
        If Not invo_emp.of_validSalary(dec_salary) Then
            MessageBox(This.Title, "Invalid Salary")
            Return
        End If
    End If
Next
           
dw_employee.GetChanges(blb_changes)
If invo_emp.of_remoteupdate ( blb_changes ) = -1 Then
    MessageBox("Save", "Unable to save changes. Sorry! Sometimes things don't work out!")
Else
    dw_employee.ResetUpdate()
End If

Testing the monolith application

It's a good idea to test well the application we've just built. It's easier to debug and fix a 2-tiered application than a 3-tiered one. An important thing to remember is to make sure that the business objects are well tested and stable. That way you don't have to worry about them when they are deployed on a remote server. The stability of the server depends on the stability of these objects and it is possible that a single rogue object can bring the server crashing down.

Its time to split this monolith into server and client components. The next section explains that.

 



Introduction

Strategy
Building the monolith
Building the DPB Server
Building the DPB Client

Setting up the DPB Server
Deploying the PB Client

Points to Ponder
Where to go from here

 download.gif (351 bytes)Download the application

What's Next?

Previous  Next

Home
1