Skip to main content.

Web Based Programming Tutorials

Homepage | Forum - Join the forum to discuss anything related to programming! | Programming Resources

Special Edition Using Visual FoxPro 6

Special Edition Using Visual FoxPro 6 -- Ch 25 - Internet Support

Special Edition Using Visual FoxPro 6


Chapter 25

Internet Support




What's so Exciting About the Internet?

The Internet and the technologies that are driving it are hot these days! People are flocking to the Internet, and the World Wide Web in particular, by the millions, and new Web sites and other Internet-related businesses and institutions are keeping pace with the ever-increasing hook-up rates of new Web surfers. Building applications that are integrated with Internet technology and can connect databases to Web sites will likely become an important aspect in your software development, as companies like Microsoft integrate Web-based technology into every aspect of software from high-level applications down to the operating system.

Although traditionally the Internet has been made of mostly static content, there is a tremendous rush under way to make dynamic data available. Database connectivity is key to this concept, and Visual FoxPro is up to the task to provide the speed and flexibility to act as a database back end and integrate Internet functionality into existing applications.

There is a lot of hype about the Internet and, to be sure, some of the new technologies are not all that they are cracked up to be. The use of the Internet for business application development is still in its infancy, and there are quite a few limitations when compared to the application development facilities that you might be used to using.

Nevertheless, the Internet is bringing about a major shift in the way applications are deployed by making it much easier to build solutions that are open and widely distributed, even open to the public, over the World Wide Web. Over the last few years, you have probably heard a lot about client/server development. A client is normally a user application, such as an accounts payable program. A server is an active process, usually running on a separate computer, that provides a service for the client. In the case of database operations, a database server manages the databases and their indexes and keeps track of user resources. The Internet is providing the full promise of what client/server was always meant to be: a platform that enables you to build widely distributed applications that can be centrally maintained using a common front end provided by a Web browser.

The driving forces behind the popularity of the Internet and the World Wide Web in particular are

The following sections look at these points in more detail.

The Universal Client Web Browsers as a Common Interface

The World Wide Web and the Web browsers used to cruise it are changing the way we look at applications today. Web browsers have brought a universal interface to applications in a way that no other software interface before it has ever achieved.

Web browsers are easy to use and provide a very simple interface to the user. The use of simple controls for navigation (back and forward buttons, a list of the last places visited, a "favorites" list, and so on) make the browser interface just about self-explanatory. It also provides a consistent interface across applications developed with a browser in mind. A typical example of a dynamically generated Web page is shown in Figure 25.1.

Figure 25.1 : A Web page containing dynamically generated data. This order form shows items selected on a previous form, enabling the user to fill out order info for an online purchase using a secure transaction.

The hyperlink-based nature of Web pages, where you can simply click on a highlighted area and immediately be transported to the location of the relevant information, makes them an extremely powerful tool for integrating information from various sources, both internally from within a company and externally from the open Internet.

Ease of use is important, but even more compelling is the ubiquitous nature of Web browsers. Browsers exist for all the major computing platforms from Windows to Macintosh to UNIX. What this means is that an application interface developed for a Web browser will instantly run on all the platforms that support a Web browser. Cross-platform development has been an often-overlooked part of software development in the past because of its complexities, but with the Web browser interface, this feature is included for free in the one-time Web application development process.

Finally, Web browsers free developers from having to distribute huge application runtimes that need to be installed on each client system. Take a typical standalone Visual FoxPro application, for example. To distribute such an application across the company, you have to install the application runtimes on each of the machines. You distribute your .EXE, the runtime, and various system files that get installed on the client system. With a Web browser, none of this happens. There is a one-time installation for the browser after which all Web-based applications are accessible. As long as the client system is equipped with a Web browser and access is available to the network that runs the application over either the public or company internal Web, the application can be run without any special installation procedures. Furthermore, the application can be run from anywhere, whether the user is at the office, on the road, or on vacation, as long as she has access to an Internet connection.

Distributed Applications Over the Web

The last point is extremely important! The Web has made distributed computing a reality for the first time by making it relatively easy to install applications that can be accessed publicly over the Internet or privately over an internal network (called an intranet). Prior to the Internet explosion, building a publicly accessible application was extremely difficult, inconvenient for end users, and very costly because proprietary communications and network protocols needed to be set up.

The promise of client/server has always been to create a distributed environment where there is a distinct line between the application interface and the database and business logic. The Web is providing this environment by clearly separating the client (the Web browser) and the server (the Web server and the back-end applications tied to it) as well as providing an open platform (a network running the TCP/IP protocol) to connect the two.

Web applications make it possible to distribute applications widely, including the capability to enable public access at reasonable cost. But at the same time, the application is maintained centrally at the server with no pieces of the application actually residing on the client side. This means that updates to an application don't require updating of any part of the client's system. All that's required is a code change on the back-end application, and all clients are automatically updated.

Open Standards

The Internet is based on open standards. Although there are quite a few struggles to extend standards and push them into company-specific directions, by and large, all the protocols and tools used are standards-based and supported by a wide variety of products and vendors. This is extremely important because it enables different companies to build tools that can interact with one another.

In addition, the open nature of the Internet is forcing companies to try to extend standards in non-proprietary ways. A good example is the struggle to extend HTML (Hypertext Markup Language), which is used to display output in Web browsers. The two leading browser vendors, Microsoft and Netscape, are the standard-bearers in this field, and both are trying to extend the standard beyond its current limitations. But unlike the past, when extensions were often proprietary, both of these companies are making the specifications widely available for developers to start using them immediately, thereby encouraging other browser vendors to pick up on the new extensions for use in their own products.

Many of the protocols used on the Internet are modular and relatively simple to implement. This means it's easy to integrate many of the connectivity features that the Internet provides with readily available protocols. Specifications are publicly maintained and accessible over the Internet. Because most of the protocols used are simple to implement, a wide variety of tools are available to use with these protocols; you can easily roll your own if you need specialized functionality.

All of this openness adds up to better interoperability of tools as well as immediate accessibility to the tools that are required to build advanced Internet applications.

Limitations of Building Web-Based Applications

Although the previous sections point out the glowing advantages of building Internet-based applications, it's important to keep in mind that this is a very young and still developing field. Most of the dedicated Web-only development tools that are available today are fairly limited. There are lots of solutions available to hook just about any kind of data to the Web, but the complexity or limits of the tools can often get in the way of building applications that provide the full breadth of functionality that a traditional standalone application can provide.

In this chapter, you will learn how to use Visual FoxPro as a database back end that gets around some of the Web application development limitations. Still, there are limitations in interface design that will require a change from the way you might be accustomed to building applications with visual tools like Visual FoxPro.

For example, HTML and the distinct client/server interface that disallows direct access to the data from the Web browser requires building applications with the different mindset of server-based programming. HTML input forms are more reminiscent of the dBASE II days when you had to hand-code fields and had no immediate control over user input via field-level validation. All access to the data happens on the server end so that each request for a data update requires calling the Web server and requesting it to update the current HTML page with new data. In essence, this process requires reloading of the currently displayed page with the updated information. This is a somewhat slower process than what can be achieved with in-line field validation. However, it is a very sure process that is simple to develop and maintain.

Web-based form input is transaction-based, where you first capture all the information entered in the Web browser and then validate the input and return an error message relating to an entire input form on the Web server. Although new HTML extensions provide more control over input forms and active HTML display, the fact that the Web browser has no direct access to data makes it difficult to build truly interactive input forms that can immediately validate input against the database rules. To update the input form or even send back an error message, the original form has to be submitted, sent to the server, and then be redrawn from scratch with the updated information retrieved from the server.

Another limitation to consider is that Web applications cannot easily print reports. All you can do is display a page as HTML and then print the result; there's no full-fledged report writer to create banded and subtotaled reports. Instead of a report builder you have to hand-code.

Although these limitations are real and something you have to consider when building Web-based applications, Microsoft and Netscape are addressing these issues with extensions to HTML that are starting to look more like the fully event-driven forms that you are used to with tools like Visual FoxPro. The not-too-distant future will bring tools to paint input forms with a form designer and enable attaching of validation code directly to fields. However, the lack of direct data access will likely continue to be a major difference between Web and desktop applications.

Limitations or not, the Web is hot, and those who have taken the first step toward building Web-based applications rarely look back as the advantages of the distributed environment and the easy scalability that goes with it often outweigh the disadvantages just mentioned.

Database Development on the Web

With the popularity of the World Wide Web, the need for database application development on the Web is exploding as more and more companies are realizing that to make maximum use of their Web sites, dynamic display of database information is essential to provide interesting and up-to-date content.

This section discusses the logistics of building database applications that run over the Web, how Web deployment affects the application development process, and what's required to make it happen.

Tools you Need to Develop Web Applications

To build applications to run over the World Wide Web, you need the following components:

The preceding list provides a road map of tools you need to build Internet applications and test them on your local machine or over a local network. In addition, you must deal with the issue of connecting the application to the Internet. Several options are available:

Running Applications Over the Web

Database connectivity over the Internet is essentially a specialized form of client/server: You have a front end (the Web browser) that accesses a back end (the Web server), which, in turn, is connected to the application that provides the database access. The front end and the back end are connected via an Internet connection, which in most cases will be the World Wide Web and HTTP (Hypertext Transfer Protocol). Figure 25.2 shows the relationship between the Web browser and Web server.

Figure 25.2 : The Web server is responsible for data access using server extensions, and the browser is responsible for display of output.

When you look at the diagram, keep in mind the clear distinction between the client and server sides-note the distinct line between the two. The Web browser has no direct access to the data, and the Web server on the other end has no direct access to the interface presented to the user. Think of the Web browser as the interface and the Web server as the database/application server. All interaction between the two is accomplished via a predefined protocol and transaction-based requests. Database

Web Browsers Get Smart: Scripting and Active Controls  On the browser side of Figure 25.2, you can see the browser scripting and active controls options. Browser scripts are client-side browser extensions, such as JavaScript for Netscape and VBScript for Internet Explorer, that provide programmable control over the browser's interface. These scripting languages provide additional control over the user interface used for input as well as provide programmable features for generating the HTML output; in essence, browser-side scripting provides logic smarts to the browser. Remember, though, there's no access to data from the browser.

Active controls refers to external controls that can be called by HTML pages and controlled by browser scripts. These controls can be automatically downloaded over the Web. In this context, active controls include Java applets and ActiveX custom controls (special-purpose, lightweight OLE controls that contain the logic to download and install themselves over the Internet) that can be executed or called by the scripting engines provided in the Web browser. Scripting languages provide the basic interface programmability and the active controls provide high-performance, optimized, and operating system-specific features to a Web browser. Many multimedia-related tools and controls are implemented as active controls, and, in the future, you will likely see controls that match common GUI interface controls, such as pop-up calendars for date validations, shipping rate calculators, and so on.

Database But even these high-powered controls do not have direct access to the data that resides on the server.

The Web Server: Providing the Link to Application and Database Connectivity  On the other end of the Internet sits the Web server. When dealing with applications running over the Internet, the Web server acts as the intermediary that negotiates access to the application and database. The Web server on its own knows nothing about applications, but instead calls helper applications known as server scripts to perform this task for it.

The flow goes something like this: The Web browser requests access to data by sending a request to execute a script on the server side. A script is essentially a program that runs on the Web server machine in response to a hyperlink hit or the click on an HTML form button. Scripts can either be .EXE files following the CGI (Common Gateway Interface) protocol, or a server-side API interface such as ISAPI (Internet Server API), which is available with Microsoft's Internet Information Server (IIS). The API-level .DLLs that are called in response to script links provide much better performance than the .EXE-based CGI scripts because they are loaded in memory as in-process .DLLs that don't need to be reloaded on each hit.

Keep the server-side script concept in mind, as all dynamic Web access is accomplished by calling scripts. Don't confuse server-side scripts with scripting languages such as JavaScript or VBScript either on the client or server side. Scripts in this context are external applications called by the Web server. These scripts can either be fully self-contained or can be used as connectors to call other applications such as Visual FoxPro to handle the database access, application logic, and the output display.

With each Web request, the browser makes available some information about itself-the browser type, its Internet address, and any field values defined on an input form-and passes this information to the Web server. When calling a server-side script, this information is passed to the script along with additional information the Web server makes available about itself. The script is then responsible for running its processing task and generating Web server-compliant output. The processing task could be anything from running a database query, adding records to a table, or simply sending back a plain HTML response page that shows the time of day.

Whenever a server-side script executes, it runs its own custom processing, but it is always required to return a response to the Web server. In most cases, the response is an HTML document that displays the results of the request: The result of a query or a confirmation or error page stating the result status of the request that was just processed. Although HTML output is the most common, other responses can also be sent, such as a request to authenticate a user, a redirection to send a user to another location, or a trigger to display a standard Web server error message.

Typically, a server script provides the following three functions:

Keep these three points in mind because flexibility in handling each of these tasks is important when choosing a tool with which to build Web applications.

How Visual FoxPro Fits In

Visual FoxPro is an excellent platform for building back-end applications because of its extremely fast data retrieval speed against local data, its flexibility when accessing remote data, and its powerful object-oriented, database-oriented language, which is ideal for programming complex data access and business rule logic.

Please note that to standardize this text, the following examples all use IIS. Some of the tools described work with other Web servers; I will point out what servers are supported.

NOTE
The rest of this chapter will focus on connecting Visual FoxPro databases to IIS, which is part of the operating system in Windows NT Server. Although some of the tools that will be mentioned work with other Web servers, I chose to standardize this discussion on IIS because it is built into Windows NT Server, which provides the ideal platform for hosting Web servers. Version 4.0 of Windows NT Server ships with this Web server.

Following are several mechanisms available for accessing Visual FoxPro data over the Web:

ODBC-based tools are easy to set up and get started and provide integrated solutions that are closely tied to the Web server. However, ODBC is comparatively slow when running against local data such as Visual FoxPro and does not scale well if your server load gets heavy.

Using Visual FoxPro on its own for serving Web requests has distinct advantages over ODBC, as you get a significant database speed improvement and much better flexibility when building Web back ends using the full functionality of the FoxPro language.

To run Visual FoxPro as a standalone back-end tool, Visual FoxPro must act as a data server, an application that is preloaded in memory waiting for requests. This is necessary to provide timely response to requests without having to incur the overhead of loading the entire Visual FoxPro runtime on each incoming hit. A data server can be implemented in a variety of ways, whether it's as a standalone application, an OLE server, or a DDE server. I will discuss several methods in the following sections.

Why You Should or Shouldn't Use Visual FoxPro

There are a couple of things to keep in mind when using Visual FoxPro as a database back end.

The following are advantages of using Visual FoxPro:

The biggest advantage of using Visual FoxPro as the database back end is its flexibility. Visual FoxPro is a high-end database tool, and you can take advantage of all the language and database functionality provided in it to create your Web application logic. You won't be limited by a cryptic general purpose scripting language, and there's no learning curve for new syntax (well, okay, you'll have to learn a little about HTML no matter how you slice it).

On the other hand, keep in mind the following disadvantages of using Visual FoxPro as a Web back end:

At first glance, these limitations seem major. However, they are easy to overcome with proper implementation of the data server. Visual FoxPro's single-threaded nature can be handled by running multiple simultaneous sessions of the data server, which essentially simulates a multithreaded environment. Although speed will decrease for simultaneous requests occurring in this fashion, the same applies to true multitasked tools. The CPU load is the real performance factor; and whether you're running one or 10 simultaneous sessions or threads, the actual load that a single server can handle depends on the number and power of the CPUs available to handle that load.

The latter problem of maintenance is one to carefully consider. Running Visual FoxPro as a data server means that the application can respond to requests only while it's up and running; crash the server and you won't process requests. It's extremely important to build bulletproof code that can recover from any error and continue running.

Running Visual FoxPro as its own server also means that a separate startup procedure is required. It's easy enough to stick a shortcut to the data server into the system startup folder to have it load automatically when the system is rebooted, but unless you have the system log in automatically, some manual intervention for logging in is required. Unlike Web servers, Visual FoxPro cannot easily be run as a system service. Running a Visual FoxPro OLE Server provides some automation of this process as the OLE server is treated as a system component that's accessible directly from the system.

Updating code or data also translates to shutting down the data server, and that means either physical access to the machine or accessing it via remote-control software such as pcAnywhere to make the changes online. With OLE servers, the Web server might need to be shut down to handle code updates.

For in-house Web installations, the latter points won't be much of a problem. However, these issues can be especially problematic if you plan to install your application on a third-party ISP's network, as this will in essence mean that you need extensive security rights to access your data server. ISPs can be very touchy about what goes on their network and who is given access to network resources.

The Internet Database Connector

The first and most straightforward method to access FoxPro data is one of the ODBC-based tools that is available with IIS. IIS ships with the Internet Database Connector (IDC), which is a simple script-based tool that enables accessing Visual FoxPro tables or any other ODBC-accessible data source. Output is accomplished via a simple HTML-like scripting language that can be used to display the results from a SQL statement (SELECT, INSERT, UPDATE, DELETE, and so on).

NOTE
The IDC uses SQL syntax that follows the ODBC SQL guidelines, which vary slightly from the Visual FoxPro SQL implementations. For example, you cannot call FoxPro's built-in functions from the SQL command line; instead, you have to use the ODBC/Transact SQL equivalent syntax.

To set up the IDC for use with a FoxPro database or directory of data files, start by configuring an ODBC datasource using the Visual FoxPro ODBC driver:

  1. Bring up the 32-bit ODBC manager from your taskbar menu.
  2. Click System DSN (the button on the bottom of the list window of installed drivers). It's extremely important that you add the new ODBC datasource using the system datasource rather than a standard ODBC source, or else IIS will fail to find your datasource and exit with an error. Figure 25.3 shows the resulting dialog box.
  3. Add a new datasource for the Visual FoxPro ODBC driver. For the demo, name the new data source QueVFP and point the driver at the directory with the sample data.

Figure 25.3 : Create a new System DSN using the Visual FoxPro ODBC driver and store it in the directory that contains your DBC or directory containing free tables.

After the ODBC driver is installed, you're ready to create the scripts that enable you to access this data. Take a look at Figure 25.4 and see how the data travels from Web browser to Web server and back to the browser.

Figure 25.4 : An HTML form link accesses the .IDC script containing the query information. Results are displayed in the .HTX document.

All in all, a request generated via the IDC requires three files: an HTML file that contains the link or form that launches the script, the .IDC file responsible for defining the query parameters, and the .HTX HTML file template that is used to display the output. The example provided here is extremely simple but serves to illustrate the various pieces of the database connector.

The .IDC and .HTX files need to be placed into a directory that has been set up for execution rights in the IIS Service Manager application. By default, IIS assigns a /scripts directory for server scripts, but it's usually a good idea to create a separate directory for each of your applications. The example below uses a /que directory for this. To create this directory, follow these steps:

  1. Bring up the IIS Service Manager.
  2. Create a new Virtual Directory on the Directory tab of the Service Manager by clicking Add.
  3. Set the directory to the physical DOS path of the script directory that you are creating.
  4. Set the Alias to the name that you would like to use as part of your URL. For example, /que translates to a full URL of http://servername.com/que/.
  5. Make sure you set the Execute check box to enable the server to execute a script in this directory.
  6. Read access is optional. If you place HTML files in this directory as well, check the Read check box.

After you set up the script directory on the server, you can get to work and call the HTML page. The HTML form that captures input from the user looks like this:

<HTML><BODY>
<FORM ACTION="/cgi-win/QUE.IDC" METHOD="POST">
  Enter Name to Lookup: <input name=Search size=20><br>
  <input type=submit value="Retrieve Names"></FORM>
</BODY></HTML>

The script is fired off by the ACTION tag of the input HTML form. Here, you're retrieving input from the user and storing it to an HTML variable named Search.

Running Server Scripts
You can also run an IDC script from an HREF link:
<A HREF="QUE.IDC?">Run Query</a>
Notice the trailing question mark when running in this fashion. The ? is required to tell the browser that the link is a script. Without the question mark, some browsers might attempt to download the script and display it.
The question mark serves as a delimiter to signify the end of the executable and the beginning of the query string or the parameter list. You can pass additional parameters on the URL that can be evaluated inside of the script as if they were entered on a form:
<A HREF="QUE.IDC?UID=00001&Name=Rick">
These values can be retrieved in the .IDC file by using %UID% or %Name%.

When the user clicks the Retrieve Names button on this form, the QUE.IDC script is called. Behind the scenes, IIS calls an ISAPI DLL called httpodbc.dll that handles the routing, parameter translation, and evaluation of the .IDC and .HTX script files. The .IDC script file contains all the parameters that are related to the query to run:

Datasource: QUEVFP
Template: que.htx
SQLStatement:
+SELECT Company, Careof, Phone
+ FROM TT_Cust
+ WHERE Company Like "%Search%%"
+ ORDER BY COMPANY

You can specify a host of other parameters that enable you to limit the number of records returned from the query, specify the name of the user for database access, and provide default parameter values.

You can also run multiple SELECT statements by including multiple SQL statement clauses in the .IDC file (multiple queries work only with IIS 2.0, which ships with NT 4.0), but keep in mind that these run one after the other immediately without allowing you to tie logic to them. If you're using SQL server, you can also execute Transact SQL syntax here and execute stored procedures.

The most important options are used in the preceding .IDC file: datasource name, SQL statement, and name of the .HTX template file that is loaded when the query completes.

Output from this query is created with the .HTX template file, which is essentially an HTML document that contains embedded field values. Listing 25.1 shows what the .HTX file looks like.


Listing 25.1  25CODE01.HTX-The .HTX Template File That Generates the HTML Output
<html><body>
<title>Query Results</title>
<h2>Internet Database Connector Result</h2>
<HR>
Here are the results of your query for company search string: <b><%idc.search%></B>
<p>

<TABLE CELLPADDING=5 BORDER=2 WIDTH=95% ALIGN=CENTER>
<TR>Company</TH>Contact</TH>Phone</TH>

<%begindetail%>
<TR><TD><%Company%></TD><TD><%CareOf%></TD><TD><%Phone%></TD></TR>
<%enddetail%>

</TABLE>
</body></html>

The <%BeginDetail%> and <%EndDetail%> tags provide a looping structure that runs through the result cursor. Any HTML between these two tags is repeatedly generated for each of the records in the result set. Field names can be embedded inside the page using the <%FieldName%> syntax that is used in the previous example. There are additional constructs available, such as a conditional <%IF%> (which can't be nested, though). For example:

<%if idc.company eq "West Wind Technologies"%>
    <H2>Special Message for West Wind Technologies</H2>
<%else%>
   <H2>Standard Message for <%idc.company%> </H2>
<%endif%>

For more detailed information on the .IDC and .HTX format options, you can look in the \IIS\IISADMIN\HTMLDOCS directory and search the index on the Database Connector (the actual page that contains this info is in \IIS\IISADMIN\HTMLDOCS\08_IIS.HTM).

The Internet database connector provides an easy mechanism for simple access to your Visual FoxPro data. When using this mechanism for accessing your data over the Web, keep the following advantages and disadvantages in mind:

Advantages:

Disadvantages:

The central advantage of the IDC is that it is well-integrated with the Web server and provides an easy way to get started connecting databases to the Web. On the downside, the scripting mechanism is limited in the functionality that is provided when accessing data and creating dynamic HTML output. Furthermore, ODBC is slow compared to running Visual FoxPro natively. ODBC makes good sense when running against remote server data, but provides limited scalability against local data such as FoxPro tables.

Using Visual FoxPro as a Data Server

ODBC works well for small and simple Web applications, but for better performance and the ultimate in flexibility when creating applications based on FoxPro data, a Visual FoxPro data server is the ticket.

What exactly is a data server? The term implies that Visual FoxPro is used as a server that responds to requests rather than running as an interactive application. Although it's possible to run a FoxPro .EXE file directly in response to a Web server request, there are several problems with this approach. A Visual FoxPro .EXE file takes several seconds to load under the best of circumstances, and loading the .EXE directly in this manner causes the application to run invisibly on the desktop, which makes it next to impossible to debug your code should something go wrong. It's much more efficient for Visual FoxPro to be already loaded, waiting for incoming requests from the Web server and instantly springing to life when a request is received. This always-on state is a requirement for Web applications where fast response time is crucial. Using a data server, it is possible to return data-based page responses in sub-second times.

To provide the data server functionality, it's necessary to use an intermediary piece of software, called a connector, that passes requests from the Web server to Visual FoxPro. These connector applications are usually small library-type routines that are written in C to provide a messaging interface that communicates between the Web server and a Visual FoxPro server that is waiting for incoming requests. Following are descriptions of two different implementations of FoxPro data servers using ISAPI-based connector applications.

Using FoxISAPI and OLE Servers for Web Applications

NOTE
Using FoxISAPI with an OLE server for Web applications requires that the user have an ISAPI-based Web server, such as MS IIS, Commerce Builder, or Purveyor.

Visual FoxPro's new capability to create OLE servers has brought about another slick option for implementing Visual FoxPro-based Web applications. What if you could use an OLE server to respond to a request placed from a Web page to handle the data processing and HTML page generation? With a tool called FoxISAPI that's provided with Visual FoxPro, you can do just that. You can find all the required files and an interesting example of an OLE server that makes use of FoxISAPI on your installation disk in the directory ..\SAMPLES\SERVER\FOXISAPI.

Before trying out this mechanism, be sure to read this entire section, especially the areas on setting up and creating the OLE server. Configuration is critical in getting FoxISAPI to work correctly.

How FoxISAPI Works  FoxISAPI consists of a small connector script .DLL that is called directly from an HTML page using a link similar to this:

<a HREF="/scripts/foxisapi.dll/
oleserver.myclass.mymethod?UID=1111&Company=Que+Publications">

The first thing that happens on a script call is that the FoxISAPI.dll is accessed. This .DLL is implemented as an Internet Server API (ISAPI) extension, which is an API that extends Internet Information Server via an in-process .DLL interface. Because ISAPI extensions run in the same address space as the Web server, are multithreaded, and are coded in a low-level compiled language such as C, these extension scripts are extremely fast.

The task of the ISAPI DLL is to provide an OLE Automation client that makes calls to your Visual FoxPro OLE server using the class ID (server, class, method) that is passed as part of the URL. The .DLL parses out the class string and makes an OLE Automation call to your OLE server accessing your class method directly. In response, your code should return a compliant result, which in most cases should be an HTML document. Figure 25.5 shows how a request travels from the Web server to your OLE server and back. Notice how FoxISAPI.dll is the mediator that receives both the outgoing script call and the incoming HTML output, sending the output to the Web server for display or processing.

Figure 25.5 : FoxISAPI enables calling OLE server methods directly via an HTML script link or form submission.

A Simple Example Server  The FoxISAPI example provided by Microsoft in your \VFP\SAMPLES\SERVERS\FOXISAPI directory is a good way to check out some of the things you can do with a FoxPro-based data server. But a simpler example might be more adequate in showing how FoxISAPI works. Listing 25.2 demonstrates how FoxISAPI.dll calls a Visual FoxPro OLE server.


Listing 25.2  25CODE02.PRG-A Minimal Response OLE Server Using FoxISAPI
#DEFINE CR CHR(13)+CHR(10)

*************************************************************
DEFINE CLASS QueVFP AS FOXISAPI OLEPUBLIC
*************************************************************

************************************************************************
* QueVFP : HelloWorld
*********************************
*  Function: Minimal response method for handling FoxISAPI call.
************************************************************************
FUNCTION HelloWorld
LPARAMETER lcFormVars, lcIniFile, lnReleaseFlag
LOCAL lcOutput

* HTTP header - REQUIRED on each request!
lcOutput="HTTP/1.0 200 OK"+CR+;
         "Content-type: text/html"+;
         CR+CR

lcOutput=lcOutput+;
"<HTML><BODY>"+CR+;
"<H1>Hello World from Visual FoxPro</H1>"+CR+;
"<HR>The current time is: "+TIME()+CR+;
"This page was generated by Visual FoxPro...<HR>"+CR+;
"</HTML></BODY>"

RETURN lcOutput
ENDFUNC
* FoxISAPI

ENDDEFINE
* QueVFP

This example doesn't do much, but it demonstrates the basics of what's required of a method that responds to a call from the FoxISAPI.DLL. The idea is this: FoxISAPI calls your OLE server method with three input parameters (described in Table 25.1), so your method must always support these three parameters. The parameters provide all the information made available by the HTML input form as well as Web server and browser stats. You pull the appropriate information from these references in order to do your data processing. For example, you might retrieve a query parameter and, based on it, run a SQL SELECT statement. To complete the process, your code needs to then return an HTTP-compatible response to the Web server. In most cases, this response is an HTML document, as demonstrated by the preceding example code, which simply returns an HTML page along with the time, so you can assure yourself that the page is actually dynamically generated. Notice that the output must include an HTTP header that is created by the first assignment to lcOutput.

Just like the HelloWorld example method, every response method must have three parameters. If your code has fewer than three parameters, the OLE call will fail, generating a FoxISAPI-generated error.

Table 25.1  FoxISAPI Response Method Parameters
Parameter
Contents
cFormVarsThis parameter contains all variable names and their contents in encoded form.
CIniFileFoxISAPI creates an .INI file containing server/browser variables, which are stored in an .INI file; the name and path of this file is passed in this parameter.
NreleaseFlagPassed by reference, this variable determines whether FoxISAPI.dll releases the reference to the OLE server. You can set this value in your code and it's returned to the .DLL. 0 is the default and means the server is not unloaded; 1 means it is unloaded.

The first parameter is probably the most important, as it contains the name and values of any fields that were filled out on an HTML form. All key/value pairs from an HTML form are returned; fields that are empty simply return an empty value. In typical CGI fashion, and because browsers do not support certain characters on the URL line, the string is MIME-encoded using various characters to signify "extended" characters and spaces. Before you can use the string, you typically have to decode it. Here's an example of a string passed in lcFormVars:

UserId=000111&BookTitle=Using+Visual+FoxPro

Each of the key/value pairs is separated by an ampersand (&) and spaces are converted to plus (+) signs. In addition, lower ASCII characters are converted into a hex code preceded by an ampersand. For example, a carriage return would be included as %0D (hex 0D or decimal 13).

Listing 25.2 doesn't use the parameters passed to it, so let's look at another example that does. Listing 25.3 demonstrates how to retrieve the information provided by the Web server using a simple FoxISAPI class provided in 25CODE02.prg. The code retrieves a couple of form variables passed on the URL of the request and then displays the entire .INI file in a browser window for you to examine.


Listing 25.3  25CODE03.prg-How to Retrieve Information Provided by the Web Server
************************************************************************
* QueVFP : TestMethod
*********************************
FUNCTION TestMethod
LPARAMETER lcFormVars, lcIniFile, lnReleaseFlag
LOCAL lcOutput

* Decode the Form Vars and assign INI file to class property
THIS.StartRequest(lcFormVars,lcIniFile)

* Must always add a content Type Header to output first
THIS.HTMLContentTypeHeader()

lcUserId=THIS.GetFormVar("UserId")
lcName=THIS.GetFormVar("UserName")

THIS.SendLn("<HTML><BODY>")
THIS.SendLn("<H1>Hello World from Visual FoxPro</H1><HR>")
THIS.SendLn("This page was generated by Visual FoxPro using FOXISAPI. ")
THIS.SendLn("The current time is: "+time()+"<p>")

THIS.SendLn("<b>Encoded Form/URL variables:</b> "+lcFormVars+"<BR>")
THIS.SendLn("<b>Decoded UserId:</b> "+ THIS.GetFormVar("UserId")+"<br>")
THIS.SendLn("<b>Decoded UserName:</b> " +lcName+"<P>")

* Show the content of the FOXISAPI INI server/browser vars
IF !EMPTY(lcIniFile) AND FILE(lcIniFile)
   CREATE CURSOR TMemo (TFile M )
   APPEND BLANK
   APPEND MEMO TFile from (lcIniFile)
   THIS.SendLn("Here's the content of: <i>"+lcIniFile+"</i>."+;
               "You can retrieve any of these with <i>"+;
               THIS.GetCGIVar(cVarname,cSection)+"</i>:<p>")
   THIS.SendLn([For example to retrieve the Browser use ]+;
               [THIS.GetCGIVar("HTTP_USER_AGENT","ALL_HTTP"): ]+;
               THIS.GetCGIVar("HTTP_USER_AGENT","ALL_HTTP") )

   THIS.SendLn("<PRE>")
   THIS.SendLn(Tmemo.Tfile)
   THIS.SendLn("</PRE>")
   USE in TMemo
ENDIF

THIS.SendLn("<HR></HTML></BODY>")

RETURN THIS.cOutput

The code makes heavy use of the FoxISAPI class' internal methods to simplify retrieving information and generating the output. Table 25.2 shows the public interface to the FoxISAPI class from which the QueVFP class in the examples is derived.

Table 25.2  FoxISAPI Class Methods
Method
Function
Send(cOutput, llNoOutput)A low-level output routine that simplifies SendLn(cOutput, llNoOutput) creating HTML output by using a method call. This is also useful for abstracting the output interface in case you want to modify the way output is generated later on. SendLn is identical to Send, but adds a carriage return to the output.
StandardPage(cHeader, cBody)A simplified routine that creates a full HTML page by passing a header and body. The page created includes minimal formatting and a title directive. Both header and body can contain embedded HTML codes that are expanded when the page is displayed.
ContentTypeHeader(llNoOutput) Generates the HTTP header required by FoxISAPI. Generates a default header for HTML documents. Required for each output page.
StartRequest()Call this method to automatically decode the FormVariable string and set up the internal handling for retrieving form and server variables using the following two methods. Required for each request that retrieves form or CGI variables using the internal methods.
GetFormVar(cVarName)Returns the value for the form variable passed as the first parameter. Note that only single variables are returned; there's no support for multiselects.
GetCGIVar(cCGIVar,cSection)Returns variables contained in the .INI file that is passed by FoxISAPI. Pass the name of the variable and the section that it is contained in. The default section is FoxISAPI.
ReleaseServer()A full request method that takes the standard three request parameters and sets the lnReleaseFlag to 1, thus forcing the FoxISAPI .DLL to release the OLE server reference.

In the Testmethod code, notice the calls to THIS.StartRequest and THIS.ContentTypeHeader. StartRequest sets up the internal variable retrieval routines by assigning the input parameters to class properties so that they can be easily referenced by the internal methods such as GetCGIVar and GetFormVar. Both of these routines make it easy to retrieve information related to the current request from the Web server's provided information. ContentTypeHeader creates the required header that must be sent back to the Web server in the result output. An HTTP header tells the server what type of content to expect and ContentTypeHeader obliges by providing the proper identification for an HTML document.

HTML output is accomplished by using the class Send method, which abstracts the output. Using this method is easier than concatenating strings manually and also provides the capability to build more complex output mechanisms that are required when your output gets longer than a few thousand characters. Behind the scenes, Send() does nothing more than add the text to a string property of the class. Notice that at the exit point, a RETURN THIS.cOutput is used to return the final result text to the FoxISAPI DLL.

The next snippet outputs the original lcFormVars encoded string and then uses GetFormVar() to print the decoded values of the actual values that were passed on the URL. GetFormVar() takes the name of the key as a parameter and returns the decoded value. If the key does not exist or the key is blank, a null string ("") is returned.

CGI variables returned by the server provide information about the server, browser, and the environment. FoxISAPI.dll captures most of the relevant information into an .INI file and the code wrapped in the IF statement outputs this file to the HTML page. All the keys are accessible with the GetCGIVar() method, which takes a keyname and section as parameters. For example, to retrieve the name of the Web server in use, you can use the following code:

THIS.GetCGIVar("Server Software ","FOXISAPI")

Let's take another look at the customer list example you used with the IDC and see how to implement it with FoxISAPI. Listing 25.4 shows the method code that accomplishes the task.


Listing 25.4  25CODE04-This Sample Method Generates a Customer List Based on a Name Provided on an HTML Form
************************************************************************
* QueVFP : CustomerLookup
*********************************
FUNCTION CustomerLookup
LPARAMETER lcFormVars, lcIniFile, lnReleaseFlag

* Decode the Form Vars and assign INI file to global var
THIS.StartRequest(lcFormVars,lcIniFile)

lcName=THIS.GetFormVar("Name")
lcCompany=THIS.GetFormVar("Company")
lcWhere=""
IF !EMPTY(lcName)
   lcWhere="UPPER(Careof)='"+UPPER(lcName)+"'"
ENDIF
IF !EMPTY(lcCompany)
   IF !EMPTY(lcWhere)
      lcWhere=lcWhere+" AND "
    ENDIF
    lcWhere=lcWhere+"UPPER(Client)='"+UPPER(lcCompany)+"'"
ENDIF
IF !EMPTY(lcWhere)
   lcWhere="WHERE "+lcWhere
ENDIF

SELECT Careof, Company, Address, Phone ;
   FROM (DATAPATH+"TT_CUST") ;
   &lcWhere ;
   INTO Cursor TQuery

IF _Tally <1
   THIS.StandardPage("No matching records found",;
                     "Please enter another name or use a shorter search
                      string...")
    USE IN Tquery
    USE IN TT_Cust
    RETURN THIS.cOutput
ENDIF

THIS.HTMLContentTypeHeader()

THIS.SendLn([<HTML><BODY>])
THIS.SendLn([<H1>Customer Lookup</H1><HR>])

This.SendLn([Matching found: ]+STR(_Tally)+[<p>])

THIS.Send([<TABLE BGCOLOR=#EEEEEE CELLPADDING=4 BORDER=1 WIDTH=100%>]+CR+;
          [<TR BGCOLOR=#FFFFCC>Name</TH>Company</TH>Address</TH>;
                       </ TR>]+CR)
SCAN
   THIS.Send(;
          [<TR><TD>]+;
          TRIM(IIF(EMPTY(TQUery.Careof),"<BR>",Tquery.CareOf))+[</TD><TD>]+;
          TRIM(IIF(EMPTY(Tquery.Company),"<BR>",Tquery.Company))+[Company</
          TD><TD>]+;
          TRIM(IIF(EMPTY(Tquery.Phone),"<BR>",TQuery.Phone))+[</TD></TR>]+CR)
ENDSCAN

THIS.SendLn([</TABLE><HR>])
THIS.SendLn([</BODY></HTML>])

USE IN Tquery
USE IN TT_Cust

RETURN THIS.cOutput
 * CustomerLookup

Setting Up for FoxISAPI  It's extremely important to correctly set up the Web server, the FoxISAPI.dll script connector, and your Visual FoxPro OLE server to get them to properly run under Windows NT. Windows NT 4.0 especially requires special attention to OLE server access rights and user configuration rights in order to run OLE servers driven by the Web server.

Here are the configuration steps for using FoxISAPI with Internet Information Server under NT 4.0:

Deciding What OLE Server Instancing to Use  Whenever you build an OLE server, one of the important issues you need to deal with is server instancing. A server must provide a separate, identical process to handle each client request; each use of the process is an instance. In Web applications, instancing is even more critical as timing and freeing up of the server for quick handling of requests are crucial to provide adequate Web performance.

Visual FoxPro is a single-threaded application and as such can handle only one request at a time. OLE servers, even multiuse servers, are no different. If an OLE server is busy, it cannot handle another request until it finishes. Furthermore, although ISAPI DLLs are multithreaded, FoxISAPI blocks simultaneous OLE server access in its code to prevent Visual FoxPro from taking more than one request at a time.

Your instancing options are to use the following:

The bottom line is that you should stick to DLL or multiuse servers. DLLs provide the best speed but they are volatile because a crash in the OLE server can crash the entire Web server and will require a server shutdown. Multiuse servers probably offer the best compromise between performance and flexibility. Speed for these multiuse servers is excellent and it is possible to shut them down without shutting down the Web server.

One major limitation of FoxISAPI to keep in mind is that you are limited in scalability. If you outgrow a single instance of your OLE server, FoxISAPI can't easily offload requests to another server. FoxISAPI can handle only one OLE Automation call at a time. It is possible to call different OLE servers simultaneously by making copies of FoxISAPI.dll and using a different name to call specific multiuse OLE servers, which are essentially one ISAPI DLL per OLE server. But even using this workaround, you can't call the same server simultaneously.

As long as a single machine and a single OLE server called serially can serve your needs, FoxISAPI provides a fast, efficient, and easily implemented interface to your FoxPro applications.

Using Web Connection for Web Applications

NOTE
In order to use Web Connection, you must have ISAPI or a Windows CGI-based Web server (IIS, Commerce Builder, Website, Purveyor for NT).

FoxISAPI provides an easy, speedy interface for creating Web applications. But for maximum flexibility and scalability, a standalone Web application that runs as a data server can provide much better scalability, maintenance, and debugging functionality. FoxISAPI is limited to a single OLE server of the same type for handling like requests, which can be a serious limitation on busy sites. In contrast, when running a standalone Visual FoxPro application, it is possible to run multiple instances of Visual FoxPro to provide an imitation of multithreading for a Visual FoxPro-based Web application. Running only two sessions, others have tested up to 120,000 database requests per day on a single dual-processor Pentium Pro machine on a live site. With a tool like Web Connection, it's even possible to further scale the application to multiple machines across a network for even greater scalability by having remote nodes handle data processing over the network.

NOTE
To provide working examples of how you can hook FoxPro code to process requests from a Web server, this section describes how to build Web applications using a third-party tool called Web Connection. A shareware version of the software can be found at http://www.west-wind.com.

Web Connection is a developer tool that provides a framework for connecting a Web server to Visual FoxPro. The framework provides many important features for developing industrial-strength Web applications: The capability to create external HTML pages with embedded FoxPro expressions to enable working with HTML designers, robust error handling and logging, automatic logging of Web hits, and an easy interface to retrieve information from and output HTML to the Web server.

Unlike FoxISAPI, which uses OLE as its messaging medium, Web Connection uses a file-based connector approach to communicate with the Web server. Figure 25.6 shows how data flows from the Web browser to the Web server to Visual FoxPro and your code.

Figure 25.6 : With Web Connection, Visual FoxPro acts as an active server waiting for incoming requests from the Web server, responding by executing FoxPro code and generating an HTML page.

From Web Server to Visual FoxPro  A typical request starts off on the Web browser where the user clicks either a hyperlink or the submit button of an HTML input form:

<A HREF="/cgi-win/wwcgi.dll?Test_Page">Simple CGI Test</A>

The wwcgi.dll script captures the information that the Web server and Web browser make available. Like FoxISAPI, Web Connection captures the content information, including the HTML form variables, into an .INI file that can be easily accessed with the application framework provided.

Instead of using an OLE server as FoxISAPI does, Web Connection employs a Visual FoxPro application based on a set of framework classes that wait for an incoming message that is provided by the wwcgi.dll. The message comes in the form of a small file that contains the path to the content .INI file the .DLL creates for each request. Web Connection picks up the file, retrieves the .INI file path, and creates a CGI object that exposes all of the .INI file's content.

NOTE
If you would rather use OLE messaging similar to FoxISAPI's instead of Web Connection's standard file messaging, you can also use Web Connection's OLE connector, which uses all the Web Connection framework classes. You can access servers using the OLE object syntax and still use the CGI, HTML, and CGIProcess classes described here for generating your request code.

The data server, which is responsible for picking up these requests, is implemented as a form class running with a timer and retrieves the filename and creates an object that facilitates access to the .INI file via a simplified class interface provided by the wwCGI class. After the Web Connection server has received the message file from the Web server, it takes the newly created CGI object and calls a user-defined function using the object as a parameter. This function is the hook that acts as an entry point for your own custom Visual FoxPro code.

Now it's your program's turn to take the information available via the CGI object and create HTML output. Your code can run any available FoxPro commands and functions, access class libraries, the data dictionary, and views to remote data-the entire language is available to you at this point. Web Connection facilitates creation of the HTML output by providing a CGIProcess class that contains both the CGI object passed to the Process function as well as an HTML object that is preconfigured to output the HTML (in most cases the response is HTML, but you can actually return any HTTP-compliant result) result to the proper output file.

After the HTML output has been generated, your custom code terminates and control returns to the wwCGIserver object, which, in turn, notifies the Web server that processing is complete. The Web server now takes the output file generated by your code and sends it over the Web for display by the Web browser.

A Look at the Components and the Setup Code  Before you dig in to the code, let's describe the components that make up Web Connection:

In its simplest form, startup of the Web Connection CGI server requires only a handful of lines of code, as shown in Listing 25.5.


Listing 25.5  25CODE05.PRG-Simplified Web Connection Server Startup Code
************************************************************************
FUNCTION CGITEST
******************
*   Function: Web Connection server startup program.
************************************************************************
#INCLUDE WCONNECT.H
SET PROCEDURE TO CGIServ ADDITIVE
SET PROCEDURE TO CGI ADDITIVE
SET PROCEDURE TO HTML ADDITIVE
SET PROCEDURE TO CGIPROC ADDITIVE
SET PROCEDURE TO WWUTILS ADDITIVE

* Starts up the server and gets it ready to poll
* for CGI requests. Call Process UDF() on a request
oCGIServer=CREATE("wwCGIServer","Process")
IF TYPE("oCGIServer")#"O"
   =MessageBox("Unable to load the CGI Request Server",;
      MB_ICONEXCLAMATION,"Web Connection Error")
   RETURN
ENDIF

oCGIServer.SetCGIFilePath("c:\temp\")

* This actually puts the server into polling mode - Modal Window
oCGIServer.show()
RETURN

The preceding code loads all the required code-based class libraries and simply creates a new wwCGIServer object. All the actual CGI request retrieval logic is handled transparently by this server class. The only crucial item in this piece of code is the second parameter in the CREATE command. The second parameter, Process, specifies a function of your choice that is called with a wwCGI object parameter each time a request is generated by the Web server. Notice the call to the SetCGIFilePath() method. The path specified here is inserted by the SETUP.APP installation program and should point to the location of the CGI temp files generated by each Web request.

When a request from the Web server hits, the code shown in Listing 25.6 is called.


Listing 25.6  25CODE06.PRG-The Process Procedure Is Called by Any Incoming Request
************************************************************************
FUNCTION Process
****************
*  Function: This is the program called by the CGI Server that
*            handles processing of a CGI request.
*
*            This example creates a process class, which
*            simplifies error handling and validation of
*            success. However, you can use procedural
*            code if you prefer.
*      Pass: loCGI -       Object containing CGI information
************************************************************************
LPARAMETERS loCGI

* Now create a process object. It's not necessary
* to use an object here, but it makes error handling
* document and CGI handling much easier!
loCGIProcess=CREATE("MyCGIProcess",loCGI)

* Call the Process Method that routes request types
* to methods in the loCGIProcess class
loCGIProcess.Process

* Debug: See what the input and output files look like
* RELEASE loCGIProcess  && Must release first or file isn't closed
* COPY FILE (lcIniFile) TO TEMP.INI
* COPY FILE (lcOutFile) TO TEMP.HTM
RETURN

This procedure is the entry point of the custom FoxPro code that can be executed in response to a Web server request. Notice that this function expects a wwcgi parameter when it is called from the Web server.

Although this routine creates another layer of abstraction by creating an instance of the CGIProcess class, this step is strictly optional (though highly recommended for ease of use). You could, at this point, use logic to retrieve the information passed by the Web server and start processing and generating HTML output right here. For maximum ease of use and maintainability, however, the CGIProcess class provides preconfigured settings that let you get to work immediately.

The Process Class: Putting Your Code to Work  For maximum ease of use and maintainability, the CGIProcess class created in Listing 25.6 exposes a framework that provides development and debug mode error handling, an easy mechanism for routing requests to your code and preconfigured CGI and HTML objects. With this class, adding your own code becomes as easy as adding a method to a subclassed version of the class.

Let's see how this works. First, here's a typical URL that generates a request in the running Web Connection server:

<A HREF="wwcgi.dll?MethodToCall~Parameter1~Parameter2">Que Test Request</a>

Notice the use of "parameters" on the URL to identify which method in the wwCGIProcess class to call.

When this request runs, the Web Connection server passes the request on to the Process function, which, in turn, creates a subclassed object of the wwCGIServer class as seen in Listing 25.6. After the object is created, its Process method is called.

Listing 25.7 contains a skeleton class definition.


Listing 25.7  25CODE07.PRG-A Skeleton Class Definition for Setting Up Your Own Request Handlers
*************************************************************
DEFINE CLASS webConnectDemo AS wwCGIProcess
*************************************************************
*  Function: This class handles the requests generated by
*            the wconnect.htm form and its results. The
*            class implementation makes error and output
*            doc handling much cleaner
*            Subclassed from a generic wwCGIProcess class
*            handler which provides error handling and
*            HTML and CGI object setup.
*************************************************************
* Properties defined by wwCGIProcess Parent Class
* -----------------------
* oCGI=.NULL.
* oHTML=.NULL.
* Methods defined by wwCGIProcess Parent Class
* ----------------------
* Init(oCGI) && Initializes and checks HTML and CGI objects
* Process    && Virtual Method always overridden, used to route requests
* Error      && Handles errors that occur in the Process code
* ErrorMsg(cErrorHeader,cMessage)  && Quick Message Display
************************************************************************
* webConnectDemo : Process
***************************
*  Modified: 01/24/96
*  Function: This is the callback program file that handles
*            processing a CGI request
*      Pass: THIS.oCGI  Object containing CGI information
*    Return: .T. to erase Temp File .F. to keep it
************************************************************************
FUNCTION Process
LOCAL lcParameter

* Retrieve first 'parameter' off the URL
lcParameter=UPPER(THIS.oCGI.GetCGIParameter(1))

DO CASE
   * Call the method if it exists
   CASE !EMPTY(lcParameter) AND PEMSTATUS(THIS,lcParameter,5)
      =EVALUATE("THIS."+lcParameter+"()")
   OTHERWISE
     * Generate Error Response Page
     THIS.ErrorMsg("The server was unable to respond "+;
         "to the CGI request.<br>"+;
         "Parameter Passed: '"+PROPER(lcParameter)+"'...",;
         "This error page is automatically called when a "+;
         "Visual FoxPro code error occurs while processing "+;
         "CGI requests.<p>It uses the wwHTML:HTMLError() method to "+;
         "output two error strings and generic server information, "+;
         "as well as overwriting existing HTML output for this request.")
ENDCASE
RETURN .T.

Function CustomMethod1
 ... Your code here
EndFunc

Function CustomMethod2
 ... Your code here
EndFunc

ENDDEFINE

You'll always create a Process method, which is used to route incoming CGI requests to the appropriate processing method within the wwCGIProcess subclass. This class can process requests generated by HTML tags with the following format:

/cgi-win/wwcgi.dll?Method ~Optional+Parameter ~Optional+Parm2

The method is an identifier that is used in the CASE statement to decide which method to call to process the request. The optional parameters are any additional parameters that you need to pass when processing a request. Note that the ~ is used as a parameter separator that is recognized by the wwCGI:GetCGIParameter(ParmNo) method to separate parameters passed on the URL following the ?.

Because of the way the class is designed, it consists almost entirely of your own custom code. The Process method is provided here more for reference than anything else; the code is actually defined in the base class. However, you often will want to override the Process method to use a more complex parameter scheme that enables you to call different classes for request processing (see the CGIMAIN example in the Web Connection samples to see how to call multiple projects from one session).

The main task of the Process method is to route the request by figuring out which method to call. This logic is handled by retrieving the first parameter on the URL, and then checking whether a method of that name exists with PEMSTATUS(). If the method exists, the EVALUATE() goes out and executes the method.

NOTE
PEMSTATUS() is an extremely powerful function for writing generic code that checks for the existence of class properties and methods. The function provides a mechanism to query all aspects of properties or methods. You can determine Public/Protected status, whether the value was changed from the default, whether the property is read only, and what type a property is.

What all of this does is provide you with an easy mechanism to hook your own code: All you have to do is add a method to your subclassed version of the CGIProcess class, and the code is practically called directly from a URL link.

Lights, Camera, Action  So what does the actual code you write to respond to requests look like? Let's take a look at a couple of examples.

For starters, let's use the same simple example you used with the Internet Database Connector:

<HTML><BODY>
<FORM ACTION="/cgi-win/wwcgi.dll?CustomerList" METHOD="POST">
  Enter Name to Lookup: <input name=Search size=20><br>
  <input type=submit value="Retrieve Names">
</FORM></BODY></HTML>

To respond to this request, add a new method to the wwConnectDemo class started previously (see Listing 25.8).


Listing 25.8  25CODE08.PRG-The Customer List Example Using Web Connection
************************************************************************
* wwConnectDemo : CustomerList
*********************************
*  Function: Returns an HTML table customer list.
************************************************************************
FUNCTION CustomerList
LOCAL loCGI, loHTML

* Easier reference
loCGI=THIS.oCGI
loHTML=THIS.oHTML

* Retrieve the name the user entered - could be blank
lcCustname=loCGI.GetFormVar("Search")

* Get all entries that have time entries (expense=.F.)
SELECT tt_cust.Company, tt_cust.careof, tt_cust.phone ;
   FROM TT_Cust ;
   WHERE tt_cust.company=lcCustname ;
   ORDER BY Company ;
   INTO CURSOR TQuery

IF _TALLY < 1
   * Return an HTML response page
   * You can subclass ErrorMsg to create a customized 'error page'
   THIS.ErrorMsg("No Matching Records Found",;
                 "Please pick another name or use fewer letters "+;
                 "to identify the name to look up")
   RETURN
ENDIF

* Create HTML document header
* - Document header, a Browser title, Background Image
loHTML.HTMLHeader("Customer List","Web Connection Customer List",;
                  "/wconnect/whitwav.jpg")

loHTML.SendLn("<b>Returned Records: "+STR(_TALLY)+"</b>")
loHTML.SendPar()
loHTML.SendLn("<CENTER>")

* Show entire result set as an HTML table
loHTML.ShowCursor()

* Center the table
loHTML.SendLn("</CENTER>")

loHTML.HTMLFooter(PAGEFOOT)

USE IN TQuery

ENDFUNC
* CustomerList

The logic of this snippet is straightforward. The code retrieves the value entered on the HTML form, runs a query, and then displays as an HTML table.

The important pieces in this code snippet are the uses of the CGI and HTML objects. As you can see, you don't need to create these objects because they are instantiated automatically when the CGIProcess object is created.

The loCGI.GetFormVar() method retrieves the single-input field from the HTML form. You can retrieve any field from an HTML form in this manner. Note that method always returns a string. If the form variable is not found, a null string ("") is returned, so it's safe in the preceding example to simply use the result in the query without further checks. The CGI class provides a ton of useful functionality. Here's a list of the most commonly used methods:

wwCGI Method
Function
GetFormVar(cFormVar)Retrieves a field entered on an HTML form.
GetFormMultiple(aParams)Retrieves multi-select field selections into an array.
GetBrowser()Returns the Browser ID string.
IsHTML30()Does the browser support HTML 3.0 extensions like tables?
IsSecure()Does the browser support secure transactions?
GetPreviousURL()Name of the page that generated this link.
GetRemoteAddress()Returns the IP address of the user.
GetCGIVar(cKey,cSection)Low-level CGI retrieval routine that enables retrieval of key values that don't have predefined methods. The section name defaults to the CGI section in the content .INI file.

The HTML class provides a simple output mechanism for generating HTML code. The class consists of both high-level and low-level methods that aid in creating your result output. At the low level are the Send() and SendLn() methods, which enable you to send string output to the HTML output file. It's entirely possible to generate your HTML output entirely using these two commands.

However, some of the higher-level functions can make life a lot easier. Here are a few of the functions available:

wwHTML Method
Function
Send()Lowest-level function. All output must go through this method to enable for different output methods. All the methods in this class call this method for final output.
SendLn()Identical to Send except it adds a carriage return/linefeed at the end.
HTMLHeader()Creates a standard HTML header with a title line, browser window title, and background image, as well as providing an easy mechanism to control HTTP headers passed back to the Web server.
HTMLFooter()Adds <HTML><BODY> tags and enables for sending a standard HTML footer for pages.
ShowCursor()Displays all fields of the currently open cursor/table as either an HTML table, or a <PRE> formatted list including headers and a title.
ShowMemoPage()Displays HTML text from either disk file or a memo field contained in a system table (wwHTML.dbf). The file can contain embedded FoxPro character expressions. This function enables you to work with HTML designers for data-driven pages.
MergeText()Merges text by translating embedded text expressions and returning the result. This function is more low level than ShowMemoPage and can be used for partial pages.
HRef()Creates a hotlink.
List()Creates various HTML list types.
HTMLError()Creates an entire HTML page with a couple of text input parameters. Great for quick status displays or error messages.
SendMemoLn()Formats large text fields.

The preceding example uses ShowCursor() to display the result table with a single line of code. This powerful method takes the currently selected table and parses out the headers and fields, creating an HTML table as output. ShowCursor() has the capability to display custom headers passed into the method as an array and the capability to sum numeric fields.

Another extremely powerful method of the wwHTML class is ShowMemoPage(). This method makes it possible to build external HTML pages stored on disk or in a memo field that can contain embedded FoxPro expressions and even entire code snippets to be evaluated by the Web Connection engine.

Now look at the more complex example in Listing 25.9, which provides an interactive guestbook browser. This example centers around a single page that shows a guestbook entry form, which is implemented as a standalone HTML document that contains embedded FoxPro fields.


Listing 25.9  25CODE09.wc-The HTML Template Page for the Guestbook Application Contains Embedded FoxPro Expressions and Fields
<HTML>
<HEAD><TITLE>West Wind Guest Book Browser</TITLE></HEAD>
<BODY Background="/wconnect/whitwav.jpg">
<p>
<IMG src="/wconnect/toolbar.gif" USEMAP="#entry" border=0, ismap HSPACE=20>
<MAP NAME="entry">
  <!- Image Map Coordinates here ->
</MAP>
<FORM ACTION="wwcgi.dll?ShowGuest ~Save ~##pcCustId##" METHOD="POST">
<INPUT TYPE="SUBMIT" VALUE="##IIF(pcCustId="NEW_ID","Add Info to","Update")##
       Guestbook" WIDTH=40>
<p>

##IIF(!EMPTY(pcErrorMsg),[<hr><font color="#800000"><h3>]+pcErrorMsg+[</h3>
        </font><hr>],"")##

<PRE>
Entered on: ##IIF(EMPTY(guest.entered),DTOC(date()),DTOC(guest.entered))##

Name: <INPUT TYPE="TEXT" NAME="txtName" VALUE="##guest. name##" SIZE="39">
Cust Id: ##pcCustId##
Company: <INPUT TYPE="TEXT" NAME="txtCompany" VALUE="##guest.company##" SIZE
        ="39">
Email: <INPUT TYPE="TEXT" NAME="txtEmail" VALUE="##guest.email##" SIZE="54">
Checking in from: <INPUT TYPE="TEXT" NAME="txtLocation" VALUE=
        "##guest.location##" SIZE="54">
</PRE>
<b>Leave a note for fellow visitors if you like:</b><br>
<TEXTAREA  NAME="txtMessage" ROWS=5 COLS=75>##guest.message##</TextAREA>
<BLOCKQUOTE>
    <b>Password:</b> <INPUT TYPE="TEXT" NAME="txtPassword" VALUE=
     "##pcPassword##" SIZE="8" MAXLENGTH="8"> (required to change entry)
</BLOCKQUOTE>
<CENTER>
<b>##STR(RecCount())## visitors have signed the guestbook.</b><p>
</CENTER>
</FORM>
<CENTER>
[<A HREF="/cgi-win/wwcgi.dll?ShowGuest ~Top ~##pcCustId##">First</A>]
[<A HREF="/cgi-win/wwcgi.dll?ShowGuest ~Previous ~##pcCustId##">Previous</A>]
[<A HREF="/cgi-win/wwcgi.dll?ShowGuest ~Next ~##pcCustId##">Next</A>]
[<A HREF="/cgi-win/wwcgi.dll?ShowGuest ~Bottom ~##pcCustId##">Last</A>]
[<A HREF="/cgi-win/wwcgi.dll?ShowGuest ~Add ~##pcCustid##">Add Entry</A>]
[<A HREF="/cgi-win/wwcgi.dll?BrowseGuests">Browse Guests</A>]
</CENTER>
<hr>

<IMG SRC="/wconnect/wcpower.gif" ALIGN="LEFT" HSPACE=5 ALT="Powered by
       Web Connection">
<FONT SIZE=-1><I>Query created by <A HREF="mailto:rstrahl@west-wind.com">
       Rick Strahl</A><br>
<A HREF="/wconnect/wconnect.htm">Web Connection demo page</A>
</BODY>
</HTML>

You'll notice the use of double pound signs (##) as delimiters to indicate embedded expressions in this HTML page. Between these delimiters you can find FoxPro character expressions that are evaluated by the ShowMemoPage() method. To use ShowMemoPage() in this manner, a Web Connection routine locates the record pointer(s) to the proper locations and then embeds the fields directly into the HTML form. When ShowMemoPage() is then called from code, it evaluates the character expressions and inserts the evaluated string in its place. Errors in expressions are automatically handled with an error string inserted instead.

The expressions can be database fields, variables, FoxPro expressions, even class method calls and user-defined functions. By storing this page in an externally edited file, it's possible to edit this page visually using an HTML editor such as FrontPage or WebEdit. As you might expect, this is easier and more maintainable than making changes inside of the actual FoxPro code. Furthermore, it enables you to design pages that can be edited by HTML designers who don't know anything about database programming.

Listing 25.10 contains the entire code for the Guestbook application shown in Figure 25.7. The code is wrapped into two methods that are part of the wwConnectDemo process class started above. This example is lengthy and a bit more complex than the previous one, but it demonstrates a full range of features in a realistic example of a Web application (you can find the code for this and the previous examples in the Web Connection examples).

Figure 25.7 : The Guestbook browser demonstrates how you can build interactive Web pages that act a lot like standalone applications.


Listing 25.10  25CODE10.PRG-A Guestbook Application Implemented in Web Connection
************************************************************************
* wwConnectDemo : ShowGuest
*********************************
*  Function: Guest Book Interactive Browser. Note that all this code
*            is not related to creating HTML at all, but rather
*            consists of setting up the logic for navigation and
*            adding editing entries.
************************************************************************
FUNCTION ShowGuest
LOCAL lcCustId, lcMoveWhere, llError
PRIVATE pcErrorMsg, pcPassword

loHTML=THIS.oHTML
loCGI=THIS.oCGI

* Retrieve the Operation option (Next, Previous etc.)
lcMoveWhere=UPPER(loCGI.GetCgiParameter(2))

* Grab the commandline Customer Id
lcCustId=loCGI.GetCGIParameter(3)

pcPassword=""
pcErrorMsg=""
llError=.F.

IF!USED("Guest")
  USE GUEST IN 0
ENDIF
SELE GUEST

IF EMPTY(lcCustId)
  lcMoveWhere="BOTTOM"
ELSE
  IF lcCustId#"NEW_ID"
     LOCATE FOR CustId=lcCustId
     IF !FOUND()
        pcErrorMsg="Invalid Record. Going to bottom of file..."
        lcMoveWhere="BOTTOM"
     ENDIF
  ENDIF
ENDIF

DO CASE
  CASE lcMoveWhere="GO"
     * Do nothing - just display
  CASE lcMoveWhere="NEXT"
     IF !EOF()
        SKIP
        IF EOF()
           pcErrorMsg="Last Record in table..."
        ENDIF
     ELSE
        GO BOTTOM
     ENDIF

     IF EOF()
        GO BOTTOM
        pcErrorMsg="Last Record in table..."
     ENDIF
  CASE lcMoveWhere="PREVIOUS" AND !llError
     IF !BOF()
        SKIP -1
     ENDIF
     IF BOF()
        pcErrorMsg="Beginning of File..."
     ENDIF

  CASE lcMoveWhere="TOP"
    GO TOP
    DO WHILE EMPTY(guest.name) AND !EOF()
       SKIP
    ENDDO

  CASE lcMoveWhere="BOTTOM"
     GO BOTTOM
     DO WHILE EMPTY(guest.name) AND !BOF() AND !EOF()
        SKIP -1
     ENDDO

  CASE lcMoveWhere="ADD"
     * Don't add record - move to 'ghost rec' to show blank record
     GO BOTTOM
     SKIP

     pcErrorMsg="Please fill out the form below and click the Save button..."

  CASE lcMoveWhere="SAVE"
     IF EMPTY(loCGI.GetFormVar("txtName")) AND ;
EMPTY(loCGI.GetFormVar("txtCompany"))
        THIS.ErrorMsg("Incomplete Input",;
"You have to enter at least a name or company.")
        USE IN GUEST
        RETURN
     ENDIF

     IF lcCustId="NEW_ID"
         APPEND BLANK
         REPLACE custid with sys(3), ;
                 entered with datetime(), ;
                 password with loCGI.GetFormVar("txtPassWord")
     ELSE
         * Check password
         pcPassWord=PADR(loCGI.GetFormVar("txtPassWord"),8)
         IF UPPER(guest.password) # UPPER(pcPassword)
            pcErrorMsg="The password you typed does not allow you to "+;
                       "change the selected entry..."
            pcCustId=guest.custid
            pcPassword=""
            loHTML.ShowMemoPage(HTMLPAGEPATH+"Guest.wc",.T.,"FORCE RELOAD")
            RETURN
         ENDIF
     ENDIF

     REPLACE name with loCGI.GetFormVar("txtName"), ;
             company with loCGI.GetFormVar("txtCompany"),;
             location with loCGI.GetFormVar("txtLocation"),;
             Email with loCGI.GetFormVar("txtEmail"),;
             Message with loCGI.GetFormVar("txtMessage")


      pcErrorMsg="Record saved..."
ENDCASE

* Prime pcCustId for all links
IF lcMoveWhere#"ADD"
   pcCustId=guest.custid
ELSE
   pcCustId="NEW_ID"
ENDIF
pcPassword=""

pcHomePath=HOMEPATH

* Display GUEST.WC - This HTML form contains the fields and
* pcErrorMsg variable...
loHTML.ShowMemoPage(HTMLPAGEPATH+"Guest.wc",.T.,;
       IIF(ATC("MSIE",loCGI.GetBrowser())>0,"ForceReload","text/html"))

IF USED("Guest")
   USE IN Guest
ENDIF

ENDFUNC
* ShowGuest

************************************************************************
* wwConnectDemo : BrowseGuests
*******************************
*  Function: Shows a list of Guests in table form for the Guest
*            Sample application.
*            This example manually creates the Browse page.
************************************************************************
FUNCTION BrowseGuests
LOCAL loHTML, loCGI, lcOrder

loHTML=THIS.oHTML
loCGI=THIS.oCGI

* Retrieve the Order Radio Button Value - Name, Company, Location
lcOrderVal=TRIM(loCGI.GetFormVar("radOrder"))
IF EMPTY(lcOrderVal)
  lcOrderVal="Name"
ENDIF

* Build an Order By expression
lcOrder="UPPER("+lcOrderVal+")"

* Create Cursor of all Guests - Note the URL link is
* embedded in the SQL-SELECT
SELECT [<A HREF="wwcgi.dll?ShowGuest ~Go ~ ]+custid+[">]+Name+;
       [</a>] as Guest, ;
   company, location,;
   &lcOrder ;
   FROM Guest ;
   ORDER BY 4 ;
   INTO CURSOR TQuery

* Set up so we can use HTML tables
loHTML.SetAllowHTMLTables(loCGI.IsNetscape())

loHTML.HTMLHeader("Guest Book Browser",,BACKIMG,;
       IIF(ATC("MSIE",loCGI.GetBrowser())>0,"Force Reload","text/Âhtml"))
loHTML.SendLn([ <FORM ACTION="wwcgi.dll?BrowseGuests" METHOD="POST">])
loHTML.SendLn([ Sort by: <input type="radio" value="Name" name="radOrder" ;
    ]+IIF(lcOrderVal="Name","checked=true","")+[>Name ])
loHTML.SendLn([ <input type="radio" value="Company" name="radOrder" ;
    ]+IIF(lcOrderVal="Company","checked=true","")+[> Company  ])
loHTML.SendLn([ <input type="radio" value="Location" ; name="radOrder"
    ]+IIF(lcOrderVal="Location","checked=true","")+[> Location<br>])
loHTML.SendLn([ <input type="submit" value="Change Order">])
loHTML.SendLn([ </FORM> <p>])

* Explicitly set up headers so we only display first 3 cols
DIMENSION laHeaders[3]
laHeaders[1]="Name"
laHeaders[2]="Company"
laHeaders[3]="Location"

* Display the table
loHTML.ShowCursor(@laHeaders)

loHTML.HTMLFooter(PAGEFOOT)

IF USED("Guest")
   USE IN Guest
ENDIF

ENDFUNC
* BrowseGuests

Looking at this code, you can tell that the majority of the logic that takes place has to do with the navigation aspect of the application rather than the actual Web/HTML dynamics. Most of the display issues are wrapped up in the external HTML template page, which is displayed in Listing 25.9.

One difference from a typical Visual FoxPro application is the fact that the application is implemented as a transaction-based process: Every action is a request that is processed by a method call. There is no event-driven programming happening here, and all validation is happening at the FoxPro back end with error messages being sent back as a message on a regenerated HTML page. The actual error message is embedded on the HTML page with the following expression:

##IIF(!EMPTY(pcErrorMsg),[<hr><font
color="#800000"><h3>]+pcErrorMsg+[</h3></font><hr>],"")##

pcErrorMsg is a PRIVATE variable that is set in the code and then passed down to the evaluation engine in ShowMemoPage(). Because the variable is PRIVATE, ShowMemoPage() can still access the pcErrorMsg variable, as it is still in scope. Thus, any variables that are declared PRIVATE or PUBLIC in the processing method can be accessed in a subsequent call to evaluate an HTML page processed with ShowMemoPage(). ShowMemoPage() (and MergeText(), which is the actual workhorse routine that ShowMemoPage() calls) also makes available the CGI and HTML objects as poCGI and poHTML, respectively. So, you could do something like this inside the HTML page:

##poCGI.GetBrowser()##
##poHTML.ShowCursor(,,,.t.)##

The first expression displays the name of the browser used to access the current page. The latter expression displays a table of the currently selected cursor and embeds it inside the HTML page. Notice that all the HTML class methods can also be used to return a string rather than sending output directly to file so that HTML class methods can be nested inside of each other. All HTML class methods have a logical llNoOutput parameter, which, if set to .T., will return the output as string only.

Working with Web Browsers

Browsers are the interface to the user, and although there's no direct interaction from the Web server back-end application to the browser, the application does provide the interface in a non-interactive fashion by sending back an entire HTML page to display.

To give you an idea of just how important Web browsers are, Microsoft has integrated the Web browser interface directly into Windows 98. In Internet Explorer 4.0, there is no distinction between the Web browser and the other interface components for file browsing and even for the display of desktop applications. With your desktop using a browser interface that is fully customizable, any application on your local computer or on the Internet is only a click away at any time. The browser is fast becoming the all-encompassing application that provides the functionality required for navigating the Web, your local network, and even your own computer and its applications.

From the aspect of back-end Web applications, browsers are very frustrating beasts in that they are both intensely visual and seemingly interactive, yet at the same time have a mentality that is reminiscent of the dumb terminal of the mainframe days. I already discussed the issue of no direct data connectivity between the browser and the server, which makes for a purely transaction-based interface to the data.

Nevertheless, browsers are getting smarter; and although the data connectivity is an issue that will not likely get resolved for some time given the infrastructure of the Web, a lot of logic is moving to the browser. New scripting languages such as Java, VBScript, and JavaScript enable extending the browser interface beyond its limited display-only capabilities. With these scripting languages, it is possible to build some validation logic into the client-side HTML pages. For example, you can validate input as long as no data lookup is required (that is, checking a phone number field for proper formatting, a credit card number to be valid, a proper state abbreviation). In addition, these scripting languages are extending the form control interface to be a lot more like a full GUI-based screen in that event code can be attached to input fields so that validation can be triggered automatically.

Although Java is getting all the attention these days, the simpler scripting languages such as VBScript and JavaScript make it easy to move some of the simpler validation-based and simple calculation-oriented functionality and implement it on the browser.

Because these scripting languages are implemented as HTML-embedded text that executes on the client side, you can use your Visual FoxPro back-end application to actually generate script code that runs when the page is displayed on the Web browser. It's sort of like getting a double execution punch.

One thing to be aware of is that not all browsers support all the features. Netscape and Microsoft are the market leaders with both of their browsers providing multitudes of extensions. Although the leading browsers from both companies are reasonably compatible and comprise 90 percent of the market, browsers from other companies do not support some of the more advanced features. If your site is public, hard choices have to be made whether to support only the most basic HTML features, or build state-of-the-art pages at the risk of turning away a small percentage of users with incompatible browsers.

Figure 25.8 shows an example of how taking advantage of browser-specific features can enhance the user interface. This page can be displayed in two modes: one using frames for IE 3.0 or Netscape 2.0 or later, and one using plain pages for browsers that do not yet support frames. The frames version is much more visually appealing and provides a more functional interface for navigating the site than a similar page that doesn't use frames. Notice the option on the frames page to go to no frames-important for laptop and low-resolution users. This site is providing a dual interface to serve both the latest HTML extensions and the low-end browsers, but a fair amount of extra design effort is required to provide this functionality.

Figure 25.8 : Taking advantage of new HTML extensions can make your site easier to use.

As a Web developer, you have to weigh carefully which features you want to implement. When building internal intranet applications, it's likely that you have control over the browser to use, so you can choose the one that provides the most functionality for the job at hand. For public applications, though, there is a tradeoff between using the newest, coolest features and leaving some users who haven't upgraded to the latest and greatest in the dust.

One strategy is to develop two sets of pages to satisfy both the hottest new developments and the older browsers. For example, the following function (implemented with Web Connection) checks for browser support and returns a letter that identifies the browser type: "F" for frames-based pages, "M" for IE 2.0, and "" for all others:

************************************************************************
* SurplusProcess : PageType
*********************************
*  Function: Returns the PageType based on the Browser name
*      Pass: llNoFrames    -  Override flag to use or not use frames
*    Return: "F"rames for Netscape 1.2/IE 3 or higher, "M" for MSIE 2
*            "" for all other Browsers
************************************************************************
FUNCTION PageType
LPARAMETERS llNoFrames
LOCAL lcBrowser, lcType

lcBrowser=UPPER(THIS.oCGI.GetBrowser())
lcType=""       && Default to non-frames page
IF  INLIST(lcBrowser,"MOZILLA/2.","MOZILLA/3.") AND !llNoFrames
   lcType="F"
ENDIF

* Return "M" for MS Internet Explorer 2.0
* only on the Homepage
IF llHomePage AND ATC("MSIE 2.",lcBrowser)>0
  lcType="M"
ENDIF

RETURN lcType
* PageType

When a page is loaded for display, it is then loaded with the appropriate prefix:

loHTML.ShowMemoPage(HTMLPAGEPATH+THIS.PageType(THIS.lNoFra