Building Client-Server Applications With Visual FoxPro
Building Client-Server Applications With Visual FoxPro
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.
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.
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 )
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.
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.
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
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.
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>
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>
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.