0% found this document useful (0 votes)
197 views

Building Client-Server Applications With Visual FoxPro

This document discusses building three-tier client-server applications with Visual FoxPro. It describes splitting work between the front-end, middle-tier, and back-end for improved performance and maintainability. The middle-tier provides business logic services and validations. An example uses a Visual FoxPro OLE server as the middle-tier to enforce rules for a library membership application with front-end, middle-tier, and SQL Server back-end tiers.
Copyright
© © All Rights Reserved
Available Formats
Download as DOC, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
197 views

Building Client-Server Applications With Visual FoxPro

This document discusses building three-tier client-server applications with Visual FoxPro. It describes splitting work between the front-end, middle-tier, and back-end for improved performance and maintainability. The middle-tier provides business logic services and validations. An example uses a Visual FoxPro OLE server as the middle-tier to enforce rules for a library membership application with front-end, middle-tier, and SQL Server back-end tiers.
Copyright
© © All Rights Reserved
Available Formats
Download as DOC, PDF, TXT or read online on Scribd
You are on page 1/ 20

Session BKO01

Building 3-tier Client-Server Applications


with Visual FoxPro
By Robert Green

Introduction
Your good friends on the FoxPro team at Microsoft spent a great deal of time to make
Visual FoxPro a robust and powerful front-end for client-server applications. Remote
views and SQL pass-through provide powerful tools to take advantage of SQL back-ends
such as Oracle and SQL Server via ODBC (Open Database Connectivity).
One of the great truisms of application development is that there are many ways to do
everything. One of the hardest things to do when building an application is to decide on an
approach and to know if it is better than the other approaches. In client-server development
this is compounded by the fact that you are dealing with two database engines, Visual
FoxPro on the front-end and a very powerful SQL database engine on the back-end.
Session BKO02 (Integrating SQL Server with Visual FoxPro) explored building two-tier
client-server applications using SQL pass-through. In this session an OLE server built with
Visual FoxPro is used to create three-tier client-server applications. Adding a middle layer
gives you an additional place to put code and perform validations.

The Fat Client Problem


The client-server application developer's dilemma is knowing how to split up the work
between the front and back-ends. Visual FoxPro and SQL Server both have very powerful
database engines. Both have powerful programming languages and are capable of
validating data and enforcing business rules.
Lately there has been much talk in the industry about fat clients and thin clients and the
effect they have on your client-server development efforts. As the available front-end tools
have grown in power there has been a tendency to rely on them more. This does ease the
burden on the back-end but it can also lead to problems.
The first problem with loading up the front-end is that it requires a more powerful
computer. Certainly if you are using Visual FoxPro as the front-end you would want at
least a 486 with 12 MB of RAM. This issue is what spawned the phrase "fat client". The
girth of the front-end is certainly a consideration if you are trying to choose which front-
end to use. You would weigh the strengths and weaknesses and resource requirements of
Visual FoxPro, Visual Basic, Access, Delphi, PowerBuilder, Java, and all of the others.
Java is exciting because it holds out the promise of a truly thin client that has enough
power to be a robust front-end. Visual FoxPro 5.0 is significantly less fat than version 3.0.
The second problem with having the front-end do a lot of the work is that if your
validations and business rules change you have to change the code in the front-end. This
Building 3-Tier Client-Server Applications with Visual FoxPro - Page 1
 1996 Robert Green. All rights reserved.
may or may not be a problem. Suppose you are developing the Visual FoxPro front-end
and you also happen to be in charge of the SQL Server back-end. If you are the one who
writes the validation and business rule code, you will presumably write it once, either in
Visual FoxPro or as SQL Server stored procedures or perhaps as some combination of the
two. If some logic has to be changed and you are the one who changes it, how much of an
issue is it where the code is located?
On the other hand, you may be writing the front-end and not have any say on the back-end.
You may not be allowed to write your own Selects, Inserts, Updates and Deletes. The
keepers of the back-end may limit you to only using stored procedures. The beauty of that
approach is that they are guaranteed that the front-end does things right. If your only access
to the data is through the stored procedures, you would be forced to do things in the correct
order and not leave out any steps. For instance, if you were deleting a member you could
not forget to remove the member's reservations. If you were adding a new member you
would not forget to put the new ID into the Adult table.
What if your front-end is just one of several front-ends all accessing the same back-end
data? In that case you might want all logic to be on the back-end. That way, if any of it
needs to change it only needs to be changed in one place.

Three-Tier Architecture
A recent advance in client-server development is the three-tier architecture scheme.
Traditional client-server is two-tier, consisting of a client and a server. As discussed above,
this can potentially lead to problems if too much work is concentrated in either the client or
the server. The three-tier architecture introduces a middle layer, which serves to ease the
processing burden of the two ends.
In a classic three-tier architecture, shown in Figure 1, each tier is responsible for providing
a service. The client provides user services, consisting mainly of the user interface. The
server provides data services, consisting of the data and the means of accessing and
maintaining it. The middle layer provides business services, consisting of data validations
and enforcement of business rules.

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 2


 1996 Robert Green. All rights reserved.
Figure 1: A Layered Application Architecture
To fully realize the potential of this architecture you should make the middle layer easily
accessible to multiple front-ends. If it is enforcing your business rules then all clients need
to be able to talk to it. This is true whether the clients are written in Visual FoxPro, Visual
Basic or some other language.
The middle tier should be running on a separate machine. This allows it to be accessible to
multiple clients and it provides for the maximum performance. Remember, the problem
with fat clients is that the client machine is overburdened. In a two tier architecture you
might have to provide each of the clients with at least a 16 MB (or more) Pentium machine.
In a three-tier architecture you might locate the middle layer on a 32 MB dual processor
Pentium machine and be happy with 12 MB 486s as the clients.
There are at least a couple of ways you could set up the middle layer. One approach would
be to use a SQL Server machine as both the middle layer and server. The approach taken in
this session uses an OLE server created in Visual FoxPro as the middle layer. The OLE
server is called from Visual FoxPro, or for that matter any OLE controller, using the
CreateObject() function, in the same manner as you would call Excel or Word as OLE
Automation servers.

Creating a Visual FoxPro OLE Server


If you don't yet know how to create an OLE server in Visual FoxPro check out sessions
ACX03 (Visual FoxPro as OLE Automation Server) and ACX04 (Custom OLE Servers).
You can also see how to do this in the Visual FoxPro Online Documentation (Chapter 16
of the Visual FoxPro Developers Guide).
The OLE server can be created as an InProc or OutOfProc server In the Win32 world
(Windows 95 and Windows NT) a process is a running application and can address up to 4
GB of memory, half of which is used to store the processes code and data and half of which
is used by the system. An InProc server is compiled as a DLL and runs in the same address
space as the process that called it. An OutOfProc server is compiled as an EXE and runs as
a separate process. An InProc server offers better performance because inter-process
communication is slower than intra-process communication. On the other hand, since an
OutOfProc server runs in a separate process you can take advantage of a multiprocessor
environment.
The OLE server can also be run locally or remotely. Typically a remote OLE server is
created as an OutOfProc server and sits on a powerful computer. That allows it to be
accessed by any number of client machines. The Remote Automation Connection Manager
can be used to configure the server and client machines to allow for remote automation.

The Sample Data


The data used in this session's examples is from a SQL Server 6.5 based library application.
The library wants to keep track of its members, its books and its loans. The library database
schema is shown in Figure 2.

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 3


 1996 Robert Green. All rights reserved.
Figure 2: Library Database Schema

Table Structures
One of the central tables in the Library application is the Member table, which contains one
row for every member of the library. An interesting twist here is that juveniles can only be
members of the library if they have a sponsoring adult. Since a juvenile lives, presumably,
in the same place as the adult there are separate tables for Adult and Juvenile. This saves
disk space because all of a juvenile's address information is redundant once you know who
the adult is. Also, the juvenile's expiration date is the same as the adult's. Further, you don't
care about the adult's birth date although you do care about a juvenile's birth date, but only
because on their 18th birthday he or she becomes an adult (at least as far as the tables are
concerned!).
The following code shows the SQL Server statements used to create the Member, Adult
and Juvenile tables:
CREATE TABLE member
( member_no member_no NOT NULL IDENTITY(1,1),
lastname shortstring NOT NULL ,
firstname shortstring NOT NULL ,
middleinitial letter NULL ,
photograph image NULL )

CREATE TABLE adult


( member_no member_no NOT NULL ,
street shortstring NOT NULL ,
city shortstring NOT NULL ,
state statecode NOT NULL ,
zip zipcode NOT NULL ,
phone_no phonenumber NULL ,
expr_date datetime NOT NULL )

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 4


 1996 Robert Green. All rights reserved.
CREATE TABLE juvenile
( member_no member_no NOT NULL ,
adult_member_no member_no NOT NULL ,
birth_date datetime NOT NULL )
The member_no field in the Member table is automatically generated by SQL Server when
a new row is added. This field is an Identity column. The seed value of 1 and the increment
value of 1 specify that the first row entered into the table should have a member_no of 1.
For every row inserted after that the member_no is incremented by 1. When adding a row
the client does not specify the member_no. SQL Server takes care of it automatically and
the client can ask what number was used.
The member_no field in the Adult and Juvenile tables is not an Identity column. The value
in that column has to match the member_no of the corresponding row in the Member table.
When a new member is added to the library, a row is first added to the Member table. The
SQL Server global variable @@Identity contains the automatically generated member_no.
A row is then added to the Adult or Juvenile table and the value for the member_no will be
whatever is in @@Identity.

Declarative Referential Integrity


In prior versions of SQL Server referential integrity was enforced through the use of
triggers, which is the same way Visual FoxPro enforces referential integrity. SQL Server
6.0 added declarative referential integrity, which allows you to define your RI rules as part
of the data structure. The first step is to create a Primary Key constraint in each table, as
shown in the following code:
ALTER TABLE member
ADD CONSTRAINT member_ident PRIMARY KEY CLUSTERED
(member_no)
ALTER TABLE adult
ADD CONSTRAINT adult_ident PRIMARY KEY CLUSTERED
(member_no)
ALTER TABLE juvenile
ADD CONSTRAINT juvenile_ident PRIMARY KEY CLUSTERED
(member_no)
The Primary Key constraint creates a Unique index, which enforces the uniqueness of the
member_no. In the examples here a clustered index, which physically sorts the data, is
created.
The second step in defining declarative referential integrity is to create Foreign Key
constraints between related tables, as shown in the following code:
ALTER TABLE adult
ADD CONSTRAINT adult_member_link FOREIGN KEY (member_no)
REFERENCES member (member_no)
ALTER TABLE juvenile
ADD CONSTRAINT juvenile_member_link FOREIGN KEY

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 5


 1996 Robert Green. All rights reserved.
(member_no) REFERENCES member (member_no)
ALTER TABLE juvenile
ADD CONSTRAINT juvenile_adult_link FOREIGN KEY
(adult_member_no) REFERENCES adult (member_no)
The first Alter Table defines a relationship between the Member and Adult tables. This is a
one-to-one relationship, although there is nothing in the code to indicate or enforce that
aspect of the relationship. The second Alter Table defines a relationship between the
Member and Juvenile tables. The final Alter Table defines a relationship between the Adult
and Juvenile tables. This is a one-to-many relationship.
Be aware that SQL Server currently does not support cascading updates or deletes. If you
wanted those you would use triggers instead of constraints.

The Library OLE Server


The Library OLE Server uses SQL pass-through to talk to the SQL Server Library
database. The server will contain methods to take actions such as retrieving information for
a member, adding a new member, changing a member's information and deleting a
member. The server exposes a few properties, including properties to determine what
errors have occurred.
The OLE Server project is the file LIBRARYSERVER.PJX. The project contains a custom
class called Members. The class has been identified as OLE Public by checking the
appropriate box in the Class Info dialog.
The methods and properties of the Members class, and hence the OLE Server, are listed in
Table 1 and Table 2, respectively.

Table 1: Library OLE Server Methods


Method Visibility Description
InitConnection Public Initiate a connection to SQL Server
CloseConnection Public Close the connection to SQL Server
GetMember Public Retrieve information for a member
AddMember Public Add a new member
UpdateMember Public Update a member's information
RemoveMember Public Remove a member
SetError Protected Store the SQL Server error
Convert Protected Convert values to strings

Table 2: Library OLE Server Properties


Property Visibility Description
nHandle Public Connection handle
NewID Public ID of newly added member
LastErrDesc Public Description of the last error that occurred
In the Project Info dialog, shown in Figure 3, the project has been given the name
VFPLibraryOLEServer. This, combined with the class name Members, yields the
registered OLE name of VFPLibraryOLEServer.Members. This can be found in the
HKEY_CLASSES_ROOT hive in the Registry, as shown in Figure 4.
Building 3-Tier Client-Server Applications with Visual FoxPro - Page 6
 1996 Robert Green. All rights reserved.
Figure 3: Libraryserver Project Info Dialog
The OLE server is automatically registered on the machine that built it. If you move it to
another machine you can register it by running REGSVR32.EXE, found in the System
directory under Windows 95 and in the System32 directory under Windows NT.

Figure 4: Windows Registry Showing OLE Server


To use the OLE server a client application would use something like the following line of
code.
oLibrary = CreateObject("VFPLibraryOLEServer.Members")
When Visual FoxPro comes across this code it first checks to see if the object name is a
Visual FoxPro class. It isn't, so the Registry is checked. The object name is found and the
CLSID, the object's unique identifier, is noted.
Also in the Registry's HKEY_CLASSES_ROOT hive is a key named CLSID, which
contains an entry for all objects that have CLSIDs. As you can see in Figure 5, the Registry
stores the executable file associated with each CLSID. That is how Windows knows to start
the OLE Server when Visual FoxPro executes its CreateObject command.

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 7


 1996 Robert Green. All rights reserved.
Figure 5: Registry CLSID Entries for Library OLE Server

Using the Library OLE Server


The form MEMBOLEFP.SCX uses the Visual FoxPro Library OLE server as the middle
layer to talk to the SQL Server Library database. After calling the server the form invokes
methods to retrieve member information, delete and update members, etc.

Loading the Form


To call the OLE server, the form uses the same CreateObject() function that would be used
to call Excel or Word. The official registered name of the OLE server as it appears in the
Windows Registry is vfpLibraryOLEServer.Members. The following code is located in the
Load event of the form.
This.oLibrary = CreateObject("vfpLibraryOLEServer.Members")

If Type("This.oLibrary") # "O" Or IsNull(This.oLibrary)


= MessageBox("Couldn't set up the OLE Server.", MB_ICONINFORMATION)
This.lConnected = .F.
Return
Else
If This.oLibrary.InitConnection("robertg","vfpguru")
= MessageBox("Welcome to the library", MB_ICONINFORMATION)
ThisForm.lConnected = .T.
Else
= MessageBox("Access to the library denied", MB_ICONINFORMATION)
This.lConnected = .F.
Return
Endif
Endif
The code here tries to create a reference to the OLE server. If it works, the form property
oLibrary will contain a reference to the OLE server. If the reference can't be created then
there is nothing left to do. If the object can be created, the form will invoke the
InitConnection method of the OLE server in an attempt to login to SQL Server. If that
doesn't work, there is nothing to do.
If the connection is made successfully the form creates a cursor and then sets optimistic
row buffering. The cursor will be used for adding and editing and allows you to use
familiar data entry functions such as GetFldState() and OldVal(), as you will see later.

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 8


 1996 Robert Green. All rights reserved.
Set Multilocks On
Create Cursor c_member ;
( member_no I, firstname C(50), middleinitial C(1), ;
lastname C(50), street C(50), city C(50), state C(10), ;
zip C(10), phone_no C(20), expr_date T, ;
birth_date T, adult_member_no I )
= CursorSetProp("Buffering", DB_BUFOPTRECORD, 'c_member')

Connecting to SQL Server


The InitConnection method of the OLE server takes the user ID and password as
parameters and then uses SQLConnect() to connect to SQL Server. The method returns
true if the connection worked and false if it didn't.
lParameters cUserID, cPassword
This.nHandle = SQLConnect('Library', cUserID, cPassword)
Return Iif(This.nHandle < 0, .F., .T.)

Locating a Member
The GetMember method of the OLE server is used to retrieve information for a member. In
the SQL pass-through examples from Session BKO02 the sample forms used SQLExec() to
either run a Select statement or to execute a stored procedure. In either case the results
were returned in a cursor, which was then used to populate the form.
Life is not so simple with the OLE server, which is specifically designed to be used with
any client, including Access and Visual Basic. So it can not simply return results in a
Visual FoxPro cursor.
The OLE server's GetMember method takes three parameters: a string that will hold a
delimited list of the retrieved data, the character you want to use as the delimiter and the ID
of the member you want retrieved. The Click event of the form's Locate button calls the
GetMember method of the OLE server.
The string is passed by reference because the GetMember method will fill it with the
member's information. The delimiter character will be used to separate the columns of
information. If GetMember returns 0, there is no member with the supplied ID. If it returns
a negative number, there was an error, which will be stored in the OLE server's
LastErrDesc property.
nRetVal = ThisForm.oLibrary.GetMember(@lcMemberInfo, "|", ;
ThisForm.txtMemberID.Value)

If nRetVal < 0
lcMessage = ThisForm.oLibrary.LastErrDesc
= MessageBox(Substr(lcMessage, RAt(']',lcMessage)+1), ;
MB_ICONINFORMATION)
<code intentionally left out>
If nRetVal = 0
= MessageBox("There is no member with this ID.", ;
MB_ICONINFORMATION)
<code intentionally left out>
If the member's information is found, the string needs to be parsed to read the individual
columns of information. The cursor created in the form's Load method is populated with
the information and the form is refreshed.

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 9


 1996 Robert Green. All rights reserved.
For i = 1 To 10
j = AllTrim(Str(i))
nPipe&j = At("|", lcMemberInfo, i)
Next
nPipe11 = Len(lcMemberInfo)

Select c_member
Append Blank
Replace firstname With Substr(lcMemberInfo, 1, nPipe1 - 1), ;
middleinitial With Substr(lcMemberInfo, nPipe1 + 1, ;
nPipe2 - nPipe1 - 1), ;
lastname With Substr(lcMemberInfo, nPipe2 + 1, ;
nPipe3 - nPipe2 - 1), ;
street With Substr(lcMemberInfo, nPipe3 + 1, ;
nPipe4 - nPipe3 - 1), ;
city With Substr(lcMemberInfo, nPipe4 + 1, ;
nPipe5 - nPipe4 - 1), ;
state With Substr(lcMemberInfo, nPipe5 + 1, ;
nPipe6 - nPipe5 - 1), ;
zip With Substr(lcMemberInfo, nPipe6 + 1, ;
nPipe7 - nPipe6 - 1), ;
phone_no With Substr(lcMemberInfo, nPipe7 + 1, ;
nPipe8 - nPipe7 - 1), ;
expr_date With Ctot(Substr(lcMemberInfo, nPipe8 + 1, ;
nPipe9 - nPipe8 - 1)), ;
birth_date With Ctot(Substr(lcMemberInfo, nPipe9 + 1, ;
nPipe10 - nPipe9 - 1)), ;
adult_member_no With Val(Substr(lcMemberInfo, nPipe10 + 1, ;
nPipe11 - nPipe10 - 1))
<code intentionally left out>
ThisForm.Refresh
<code intentionally left out>
The code in the For loop determines the location of each pipe delimiter. Everything up to
the first delimiter is the member's first name. Everything between the first delimiter and the
second is the member's middle initial. Everything between the second delimiter and the
third is the member's last name. And so on.

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 10


 1996 Robert Green. All rights reserved.
The OLE Server's GetMember Method
As mentioned above, the GetMember method of the OLE server takes three parameters, a
string which will contain a delimited list of the member's information, the delimiter and the
member ID of the desired member. A Select statement is constructed and sent to SQL
Server to retrieve the information. If the member is found, the string is constructed. Since
the string was passed by reference it will be sent back to the form. The GetMember method
itself returns 0 if the member was not found and 1 if the member was found.
LParameters cMemberInfo, cDelimiter, cSearchID
lcSQL = "Select firstname, middleinitial, lastname, street, " + ;
" city, state, zip, phone_no, expr_date, " + ;
" birth_date = null, adult_member_no = null " + ;
"From member, adult " + ;
"Where member.member_no = adult.member_no " + ;
" And member.member_no = " + AllTrim(cSearchID) + " " + ;
"Union " + ;
"Select firstname, middleinitial, lastname, street, " + ;
" city, state, zip, phone_no, expr_date, " + ;
" birth_date, adult_member_no " + ;
"From member, adult, juvenile " + ;
"Where member.member_no = juvenile.member_no " + ;
" And adult.member_no = juvenile.adult_member_no " + ;
" And member.member_no = " + AllTrim(cSearchID)
<code intentionally left out>
If RecCount("c_member") = 0
Return 0
Else
cMemberInfo = AllTrim(This.Convert(c_member.firstname)) + ; +
cDelimiter + ;
AllTrim(This.Convert(c_member.middleinitial)) + ;
cDelimiter + ;
AllTrim(This.Convert(c_member.lastname)) + ;
cDelimiter + ;
AllTrim(This.Convert(c_member.street)) + ;
cDelimiter + ;
AllTrim(This.Convert(c_member.city)) + cDelimiter + ;
AllTrim(This.Convert(c_member.state)) + ;
cDelimiter + ;
AllTrim(This.Convert(c_member.zip)) + cDelimiter + ;
AllTrim(This.Convert(c_member.phone_no)) + ;
cDelimiter + ;
AllTrim(This.Convert(c_member.expr_date)) + ;
cDelimiter + ;
AllTrim(This.Convert(c_member.birth_date)) + ;
cDelimiter + ;
AllTrim(This.Convert(c_member.adult_member_no)) + ;
cDelimiter
Return RecCount("c_member")
Endif

Adding an Adult
The AddMember method of the OLE server is used to add a member to the library. For
ease of use the method takes as a parameter a two dimensional array. The first column

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 11


 1996 Robert Green. All rights reserved.
contains field names and the second column contains the associated information. The
form's AddMember method is called from the Click event of the Save button and in turn
calls the GetMember method of the OLE server.
Dimension laMember[11,2]
laMember[1,1] = "firstname"
laMember[2,1] = "middleinitial"
laMember[3,1] = "lastname"
laMember[4,1] = "street"
laMember[5,1] = "city"
laMember[6,1] = "state"
laMember[7,1] = "zip"
laMember[8,1] = "phone_no"
laMember[9,1] = "expr_date"
laMember[10,1] = "birth_date"
laMember[11,1] = "adult_member_no"

* Some of the data is either left empty or supplied by the server


laMember[1,2] = AllTrim(ThisForm.txtFirstName.Value)
laMember[2,2] = AllTrim(ThisForm.txtMiddleInitial.Value)
laMember[3,2] = AllTrim(ThisForm.txtLastName.Value)
laMember[4,2] = AllTrim(ThisForm.txtStreet.Value)
laMember[5,2] = AllTrim(ThisForm.txtCity.Value)
laMember[6,2] = AllTrim(ThisForm.txtState.Value)
laMember[7,2] = AllTrim(ThisForm.txtZip.Value)
laMember[8,2] = AllTrim(ThisForm.txtPhoneNumber.Value)

If ThisForm.oLibrary.AddMember(@laMember) < 0
lcMessage = ThisForm.oLibrary.LastErrDesc
= MessageBox(Substr(lcMessage, RAt(']',lcMessage)+1), ;
MB_ICONINFORMATION)
Else
= MessageBox("This member has been added.", MB_ICONINFORMATION)
* Find out the member_no of the new member
ThisForm.txtMemberID.Value = AllTrim(Str(ThisForm.oLibrary.NewID))
<code intentionally left out>
The first column of the array laMember contains the names of the fields. The second
contains the actual data, which is taken from the controls on the form. The array is passed,
by reference to ensure it all goes, to the AddMember method, which returns 1 if the
member was added and -1 if he or she wasn't. The LastErrDesc property of the OLE server
contains the error message if the addition was unsuccessful.

The OLE Server's AddMember Method


As mentioned above, the AddMember method of the OLE server takes one parameter, an
array of field names and values.
LParameters aMemberInfo
Adding a member to the Library is a two step procedure. First a new row is added to the
Member table, which contains the member's name. The member_no field in that table has
been assigned the Identity property and SQL Server automatically determines the next
value to use. The second step is to add a corresponding row to the Adult table, which
contains the member's address. The same member_no must be used in both tables to
maintain the one-to-one relationship.

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 12


 1996 Robert Green. All rights reserved.
The two steps need to be part of a transaction so that either both rows go in correctly or
neither goes in.
= SQLSetProp(This.nHandle, "Transactions", 2)
Next, an Insert statement for the Member table is constructed and sent to SQL Server. If
the Insert fails the transaction is rolled back and the AddMember method returns -1,
alerting the client that the Insert failed.
cSQL = "Insert member (firstname, middleinitial, lastname, " + ;
"photograph) " + ;
"Values ('" + AllTrim(aMemberInfo[1,2]) + "', " + ;
"'" + AllTrim(aMemberInfo[2,2]) + "', " + ;
"'" + AllTrim(aMemberInfo[3,2]) + "', " + ;
"NULL)"
If SQLExec(This.nHandle, cSQL) < 0
This.SetError
* Rollback the transaction
= SQLRollback(This.nHandle)
Return -1
Endif
The SQL Server global variable @@Identity stores the most recent value inserted into an
Identity column. In this case it contains the new user's member_no. That number is stored
in the NewID property of the OLE server.
If SQLExec(This.nHandle, "Select @@identity") < 0
This.SetError
* Rollback the transaction
= SQLRollback(This.nHandle)
Return -1
Endif
This.NewID = sqlresult.exp
Next, the corresponding row in the Adult table is added. The value for member_no comes
from the value of @@Identity. As before, if this Insert fails, the whole transaction is rolled
back and AddMember returns -1.
cSQL = "Insert adult (member_no, street, city, state, zip, " + ;
"phone_no, expr_date) " + ;
"Values (" + AllTrim(Str(This.NewID)) + ", " + ;
"'" + AllTrim(aMemberInfo[4,2]) + "', " + ;
"'" + AllTrim(aMemberInfo[5,2]) + "', " + ;
"'" + AllTrim(aMemberInfo[6,2]) + "', " + ;
"'" + AllTrim(aMemberInfo[7,2]) + "', " + ;
"'" + AllTrim(aMemberInfo[8,2]) + "', " + ;
"'" + TToC(DToT(GoMonth(Date(),12))) + "' )"
If SQLExec(This.nHandle, cSQL) < 0
This.SetError
* Rollback the transaction
= SQLRollback(This.nHandle)
Return -1
Endif
The last step is to try to commit the transaction. If that fails, everything is rolled back. If
the Commit works, the new member has been added and AddMember returns 1, indicating
success.
If SQLCommit(This.nHandle) < 0
This.SetError
* Rollback the transaction
= SQLRollback(This.nHandle)

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 13


 1996 Robert Green. All rights reserved.
Return -1
Else
Return 1
Endif

Saving Changes
The UpdateMember method of the form calls the UpdateMember method of the OLE
server. The form passes to the OLE server a two dimensional array and the ID of the
member. As with the AddMember method above, the first column of the array contains the
names of fields to be updated, while the second column contains the new information for a
particular member.
When a member was added, the array contained a row for each field. In this case however,
it should contain a row for only those fields whose value has changed. There is no point in
making SQL Server update information that hasn't changed.
The form looks at each control to see if its value has changed. If so, a row is added to the
array. Notice the use of OldVal() to see if the field changed. The use of the buffered cursor
makes this possible.
i = 0
If c_member.firstname <> OldVal("c_member.firstname")
i = i + 1
Dimension laMember[i, 2]
laMember[i,1] = "firstname"
laMember[i,2] = AllTrim(ThisForm.txtFirstName.Value)
Endif
If c_member.lastname <> OldVal("c_member.lastname")
i = i + 1
Dimension laMember[i, 2]
laMember[i,1] = "lastname"
laMember[i,2] = AllTrim(ThisForm.txtLastName.Value)
Endif
<code intentionally left out>
If no fields were changed, there is nothing to do and therefore no point in bothering SQL
Server.
If i = 0
= MessageBox("There is nothing to save.", MB_ICONINFORMATION)
Return
Endif
If any fields have been changed, the form invokes the UpdateMember method of the OLE
server. The array is passed, as well as the member's ID. If UpdateMember returns -1 the
update failed and the reason is displayed to the user.
If ThisForm.oLibrary.UpdateMember(@laMember, ;
ThisForm.txtMemberID.Value) < 0
lcMessage = ThisForm.oLibrary.LastErrDesc
= MessageBox(Substr(lcMessage, RAt(']',lcMessage)+1), ;
MB_ICONINFORMATION)
Else
= MessageBox("This member's information has been saved.", ;
MB_ICONINFORMATION)
<code intentionally left out>

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 14


 1996 Robert Green. All rights reserved.
The OLE Server's UpdateMember Method
As mentioned above, the OLE server UpdateMember method takes two parameters, the
array of field names and data and the ID of the member to update.
LParameters aMemberInfo, cSearchID
Just as adding a member required two Inserts wrapped in a transaction, updating a member
requires two Updates wrapped in a transaction. To build each Update statement
UpdateMember searches each array for each field. For instance, if the string "firstname" is
found in the array and if it is in the first column, the firstname field should be updated in
the Member table. Two strings, cSQL1 and cSQL2 are constructed, each containing part of
an Update statement.
cSQL1 = ""
nPos = AScan(aMemberInfo, "firstname")
If Mod(nPos, 2) = 1
cSQL1 = cSQL1 + "firstname = '" + aMemberInfo[nPos + 1] + "', "
Endif
nPos = AScan(aMemberInfo, "lastname")
If Mod(nPos, 2) = 1
cSQL1 = cSQL1 + "lastname = '" + aMemberInfo[nPos + 1] + "', "
Endif
<code intentionally left out>
cSQL2 = ""
nPos = AScan(aMemberInfo, "street")
If Mod(nPos, 2) = 1
cSQL2 = cSQL2 + "street = '" + aMemberInfo[nPos + 1] + "', "
Endif
nPos = AScan(aMemberInfo, "city")
If Mod(nPos, 2) = 1
cSQL2 = cSQL2 + "city = '" + aMemberInfo[nPos + 1] + "', "
Endif
A transaction is then begun.
= SQLSetProp(This.nHandle, "Transactions", 2)
If anything has been stored to the cSQL1 variable the rest of the Update statement for the
Member table is constructed and passed to SQL Server. If it fails the transaction is rolled
back.
If Len(cSQL1) > 0
* Add the Update, strip off the last comma, add a Where clause
cSQL1 = "Update member Set " + Left(cSQL1, Len(cSQL1) - 2) + ;
"Where member_no = " + AllTrim(cSearchID)
If SQLExec(This.nHandle, cSQL1) < 0
This.SetError
* Rollback the transaction
= SQLRollback(This.nHandle)
Return -1
Endif
Endif
Similarly, if anything has been stored to the cSQL2 variable the rest of the Update
statement for the Adult table is constructed and passed to SQL Server. If it fails the
transaction is rolled back.
If Len(cSQL2) > 0
* Add the Update, strip off the last comma, add a Where clause
cSQL2 = "Update adult Set " + Left(cSQL2, Len(cSQL2) - 2) + ;
"Where member_no = " + AllTrim(cSearchID)

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 15


 1996 Robert Green. All rights reserved.
If SQLExec(This.nHandle, cSQL2) < 0
This.SetError
* Rollback the transaction
= SQLRollback(This.nHandle)
Return -1
Endif
Endif
The last step is to commit the transaction. If the Commit fails the transaction is rolled back.
If SQLCommit(This.nHandle) < 0
This.SetError
* Rollback the transaction
= SQLRollback(This.nHandle)
Return -1
Else
Return 1
Endif

Deleting a Member
The OLE Server's RemoveMember method is used to delete a member. The method takes
as a parameter the ID of the member to delete. RemoveMember returns 1 if the member
was deleted and -1 otherwise. If the deletion fails, the OLE server property LastErrDesc
contains the reason. The deletion could fail because referential integrity is violated, for
instance a member with outstanding loans or active juveniles cannot be deleted. It could
also fail because a SQLExec() failed. Either way, the form code assumes the reason will be
contained in oLibrary.LastErrDesc and can also be displayed to the user.
If ThisForm.oLibrary.RemoveMember(ThisForm.txtMemberID.Value) < 0
lcMessage = ThisForm.oLibrary.LastErrDesc
= MessageBox(Substr(lcMessage, RAt(']',lcMessage)+1), ;
MB_ICONINFORMATION)
Else
= MessageBox("This member has been deleted.", MB_ICONINFORMATION)
<code intentionally left out>

The OLE Server's RemoveMember Method


The OLE server's RemoveMember method takes one parameter, the member ID of the
member the user is attempted to delete.
LParameters cSearchID
There are several reasons a delete attempt might fail. With the data used here the two most
frequent are that the member has outstanding loans or is an adult sponsoring active
juveniles. If either of those is true the Delete will fail.
The RemoveMember method code could have been written to try the deletion and then
monitor failures to see if either of these is the reason. But this is inefficient, especially
compared to the alternative of checking these two conditions first. If the member has active
loans or is sponsoring juveniles then the Delete won't even be tried. This makes it easier to
trap for the error and also spares processing on the SQL Server.
* First check to see if this member has active loans
cSQL = "Select member_no From loan Where member_no = " + ;

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 16


 1996 Robert Green. All rights reserved.
AllTrim(cSearchID)
If SQLExec(This.nHandle, cSQL) < 0
This.SetError
Return -1
Else
If RecCount("sqlresult") <> 0
This.LastErrDesc = "This member can not be deleted. " + ;
"He/she has active loans."
Return -1
Endif
Endif

* Now check to see if this is an adult with active juveniles


cSQL = "Select member_no From juvenile Where adult_member_no = " + ;
AllTrim(cSearchID)
If SQLExec(This.nHandle, cSQL) < 0
This.SetError
Return -1
Else
If RecCount("sqlresult") <> 0
This.LastErrDesc = "This member can not be deleted. " + ;
"He/she is an adult with active juveniles."
Return -1
Endif
Endif
If there are other things to check, the code for them could follow the lines above. If none of
the obvious problems is present, a transaction is started and the deletion proceeds.
= SQLSetProp(This.nHandle, "Transactions", 2)
A member may have related records in the Loanhist table, which contains a row for every
borrowed book that was returned, or in the Reservation table, which contains a row for
every book on reserve. These related rows are deleted first. If not, referential integrity
would be violated when the member was deleted. If either of these Deletes fails the
transaction is rolled back.
* Delete the loan history records for this member
cSQL = "Delete loanhist Where member_no = " + AllTrim(cSearchID)
If SQLExec(This.nHandle, cSQL) < 0
This.SetError
* Rollback the transaction
= SQLRollback(This.nHandle)
Return -1
Endif

* Delete the loan reservation records for this member


cSQL = "Delete reservation Where member_no = " + AllTrim(cSearchID)
If SQLExec(This.nHandle, cSQL) < 0
This.SetError
* Rollback the transaction
= SQLRollback(This.nHandle)
Return -1
Endif
Any other related records would also be deleted at this point. Once all the related rows are
gone, the member can be deleted, first from the Adult table and then from the Member

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 17


 1996 Robert Green. All rights reserved.
table. The Deletes must be in this order to preserve referential integrity. Again, if either of
these Deletes fails, the transaction is rolled back.
* Delete the member's row in the Adult table
cSQL = "Delete adult Where member_no = " + AllTrim(cSearchID)
If SQLExec(This.nHandle, cSQL) < 0
This.SetError
* Rollback the transaction
= SQLRollback(This.nHandle)
Return -1
Endif

* Delete the member's row in the Member table


cSQL = "Delete member Where member_no = " + AllTrim(cSearchID)
If SQLExec(This.nHandle, cSQL) < 0
This.SetError
* Rollback the transaction
= SQLRollback(This.nHandle)
Return -1
Endif
If the next part is not familiar, you either didn't read the preceding parts of these notes or
you slept through them. Commit if go, rollback if no. The RemoveMember method returns
-1 if the delete failed and 1 if it succeeded.
If SQLCommit(This.nHandle) < 0
This.SetError
* Rollback the transaction
= SQLRollback(This.nHandle)
Return -1
Else
Return 1
Endif

Storing SQL Server Errors


The SetError method of the OLE server stores the SQL Server error to the LastErrDesc
property, which is then available to the client.
lnError = AError(laError)
This.LastErrDesc = Substr(laError[3], RAt(']',laError[3]) + 1)

Black Box Programming with OLE Servers


Session BKO02 covered using views and SQL pass-through to create two-tier client-server
applications. One of the examples used SQL Server stored procedures to add, update and
delete members. A benefit to that technique was that processing occurred on the server and
not the client. This not only avoided the fat client problem but also made it unlikely that
developers would mess up the data. For instance, you couldn't forget to remove the
member's related loan history records if all you did was invoke a stored procedure.
Using a three tier architecture is very similar to using stored procedures in terms of coding
difficulty (low) and the level of control you retain (also low). For instance, the code to
invoke the stored procedure to delete a member is
If SQLExec(ThisForm.nHandle, "Execute RemoveMember " + ;
ThisForm.txtMemberID.Value) < 0

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 18


 1996 Robert Green. All rights reserved.
while the code to invoke the equivalent OLE server method is
If ThisForm.oLibrary.RemoveMember(ThisForm.txtMemberID.Value) < 0
In both of these cases all the developer needs to know is the name of the method or
procedure, what parameters it takes and what it returns. Both the SQL Server stored
procedures and the OLE server methods become black boxes.
Someone who is only in charge of the front-end doesn't need to know or care what is in the
middle layer as opposed to what is in the back-end. He or she may just want to know how
to call the methods and how to tell whether they succeeded. This puts the rapid back in
rapid development.

Where is the Fat?


In one of the examples from session BKO02 SQLExec() was used from the client form to
handle all communications with SQL Server. Selects, Inserts, Updates and Deletes were
sent and the results were manipulated as necessary. Most of the work other than the actual
physical managing of data was done in the client. The risk there was winding up with a fat
client.
In another example from session BKO02 SQLExec() was used to call stored procedures
sitting on SQL Server. No work was done in the client, other than managing a local cursor
filled with one row at a time. Both data manipulation and validation were performed on the
server. The risk there was winding up with a fat server.
In the example used in this session the front-end does nothing but invoke methods of the
OLE server. In fact, in this example, which is admittedly rather simple, the middle layer
looks strikingly like the first SQLExec() example. In fact, much of the code for the OLE
server was taken from the form MEMBEXEC.SCX.
If all the data validations and business rule checking are put into the middle layer does this
lead to a problem of fat middles? Has the fat problem merely been moved up the
hierarchy? To be sure, it would be possible to overload the OLE server to the point where
it can't perform its role satisfactorily. However, since it runs on one machine it would be
easier to solve the fat issue by throwing hardware at it than if the problem were fat clients.
For a relatively small amount of money you could stick one or two more processors and 16
or 32 MB more RAM in the middle tier machine and, conceivably, see any performance
problems disappear. Of course, that works only if the processing power of the machine is
the bottleneck.
The important point is that because Visual FoxPro and SQL Server, and for that matter
Oracle and others, have both powerful data engines and powerful languages, you can fine
tune the fat. You can decide where to place code and where to put the processing burden
and you can tweak it and adjust it until performance is acceptable.

When Do You Need This?


It is not difficult to create a Visual FoxPro OLE server. It is also not difficult to call an
OLE server's methods from a form or other code. Setting up remote automation is not
difficult either. So writing a client-server application using a three-tier architecture is only
mildly more difficult than using a two-tier architecture.
So why would you do it? When do you need to move up to a three-tier architecture.
Certainly performance is a reason. Do it if your client machines are underpowered and if,
Building 3-Tier Client-Server Applications with Visual FoxPro - Page 19
 1996 Robert Green. All rights reserved.
by moving large chunks of code to a more powerful middle tier box, you can significantly
improve performance.
How likely is this? While it is true that the resource requirements for Visual FoxPro have
fallen with version 5.0, you still want at least a 486 with 12 MB of RAM. Pentiums with 32
MB are $2000 these days. Consider a company who is moving an application off the
mainframe and rewriting it using Visual FoxPro and SQL Server. They buy 100 brand new
machines and have a fairly speedy network. In that situation you may not realize a large
performance gain by using a three-tier as opposed to two-tier architecture. Note that might
is the operative word here.
Perhaps a more compelling reason to consider three-tier is the fact that you can move your
data validation and business rules code into one location. Suppose there is a rule that no
new orders can be placed if a customer has more than $20,000 60 days past due. In a two-
tier architecture you can easily code this into the Click event of the Add button and
immediately put up a message telling the user they can't place the order. The users love it
and everyone is happy.
Suppose the rule changes and the limit is raised to $30,000. How many client machines
have to be upgraded? Do you have to regenerate the APP or EXE file and redistribute it?
This can be quite a burden and a large cost. You may have designed the system so that
business rules are stored in a metatable somewhere. If so, good for you. If not, there is a lot
of work to do for what seems like a simple change.
If the business rule were in an OLE server however, the change is fairly simple. You would
change the code and recompile the one EXE. None of the client machines would have to
change. The rule would go into effect as soon as the EXE was done and the maintenance
burden is a fraction of what it could have been.
Even if a remote OLE server decreased overall performance slightly you might still use it
to take advantage of the decreased maintenance burden.

Conclusion
The OLE server middle layer approach provides several enticing benefits. The fact that it
runs on a separate machine allows you to run your validations and business rules on a
powerful machine without breaking your budget. You can purchase one ultra powerful
machine instead of having to beef up every client.
When it comes time to change the validations or business rules you would only need to
change the middle layer. You would change the Visual FoxPro code and remake the OLE
server. Every client would automatically use the new rules because they are located in a
single location. This dramatically simplifies the problem of distributing changes and also
guarantees that all clients are using the same, and updated, rules.

Building 3-Tier Client-Server Applications with Visual FoxPro - Page 20


 1996 Robert Green. All rights reserved.

You might also like