White Papers                  Home |  White Papers  |  Message Board |  Search |  Products |  Purchase | News |  

 

Handling long Web Requests with
Asynchronous Request Processing

 

by Rick Strahl

West Wind Technologies

http://www.west-wind.com/

 

Updated: 4/9/2001

 

 

Download for the wwAsyncWebRequest Class:

http://www.west-wind.com/presentations/wwAsyncWebRequest/wwAsyncWebRequest.zip

 

Amazon Honor System Click Here to Pay Learn More

 

 

 

 

Web Applications tend to be stateless and running long requests can be problematic for Web backends. Long running requests can tie up valuable Web server connections and resources. In this article Rick describes one approach that can be used to handle running long requests using a polling mechanism and an Event manager class that can be used to pass messages between a Web application and a processing server running the actual long task.

 

When designing a Web application the issue of how to handle long running requests invariably comes up. Almost any Web application will have at least one or two backend or admin task that take a fair amount of time to run. At first blush you may think, what's the big deal here? After all, Web Servers are multi-threaded and can run multiple requests simultaneously. Well, in many cases this arrangement is still problematic because the resource use involved on the Web server.

 

The problems with long requests run directly off a Web application are many:

 

The solution: Message based application servers

There are a number of ways that these issues can be addressed, but most of them have a simple concept in common: The client application submits a request to the Web Server and the Web server passes off the request for actual processing to another application or application server. The Web app then checks back occasionally to see if the process has completed and if it has retrieves the result to send back to the client.

 

There are many ways that this can be implemented. One solid approach is to use Microsoft Message Queue (MSMQ) to submit messages into a queue, and have another application pick up the incoming request to process. The result is then returned in a response message that the Web application can poll for.

 

I wrote an implementation of this type of Async manager a while back, but it ended up being too complex for many to implement and required Windows 2000 with Message Queue installed and configured properly.

 

So, I set out to create a simpler interface using a simple table based event mechanism that accomplishes the same functionality in a much simpler interface wrapped in a class. The result is the wwAsyncWebRequest class, which I'll talk about here. Before I dig into the details how the actual class implements this functionality let's take a closer look at what's required to build a sophisticated async event processing manager.

How it works

There are several components involved in this scenario. The client application running the browser that provides the user interface and basic process information. The Web Server application that provides the main Web processing. And finally a backend application service/server that handles processing the actual long running task in a separate process or even on a separate machine.

 

Figure 1 – Asynchronous events require coordination of the Web browser, Web Server application and a backend application that actually run the processing task. Note that the backend application can run either on the Web server or on a separate machine providing a means of scalability through this process.

The Browser

The end user will be accessing some functionality over the Web. Typically the user will initiate some operation such as running a long running report. Once the user clicks the link or form button to start the operation, he'll get a result page back that says that the request is still processing. This page is refreshed every so often to indicate progress by displaying some sort of updated status information. This progress can either be real progress as provided by the Application Server (more on that later), or something that the Web application simulates, such as an increasing number of dots or an animated gif that changes to give the user the impression that something is happening.


The browser can automatically refresh the page using the <META REFRESH> browser tag that causes the page to reload.

The Web Server Application

The Web Server application is responsible for actually submitting the Async request to the event queue when the user clicks the submit button to start processing. It then also handles each of the requests from the client to see whether the process is complete. If not complete, another update page containing a META tag is sent back to the browser to display to say that the process is still running. If it turns out that the process is complete then the Web application handler picks up the result value(s) and uses those values to build the final output that the user will see in his browser.

 

The format of data passed back and forth here is crucial – when a request is submitted and retrieved the format needs to be agreed upon and it must be something that can be stored in a database table. In many cases this will mean XML data inputs and outputs. The wwAsyncWebRequest class provides input and output data members as well as a simple property storage mechanism that makes it easy to pass this data between the Web application and the Application server.

The Backend Application

This is the actual application that handles the long running request. In most scenarios this will be a listener type application that looks for incoming requests in the event queue and picks up any events that are to be processed. The application then either processes this request itself or offloads processing to yet another application or server if necessary. The application server could be very, very simple and simply be directly fired from the Web server application via CreateProcess or Run command simply to offload. However, in most real world scenarios you probably have a listener application that polls the event queue for incoming requests and acts accordingly. For example a generic handler might run any COM object via a SOAP request stored in the event queue.

 

Backend Applications

Putting it all together

As you can see there is a bit of interaction involved to make this happen – running an asynchronous request is quite a bit more complex than running a straight request as you have to coordinate the client side, the Web server as well as the backend application.

 

To encapsulate the most common functionality and make it easy to perform the tasks related to the managing the communication process I built a Visual FoxPro class that I'll introduce here. The class handles event management via FoxPro or SQL Server tables that store information about each asynchronous event you want to fire. The basic concept is that of a queue where messages are passed in and returned out of either by specific message ID or in sequential first in/first out order.

 

At this point you might ask, why build a class like this when there are tools like MSMQ that handle queueing. There are a couple of reasons. Message queues tend to be somewhat limited in the amount of data they can provide about the messages that are sent within it. In particular one of the goals of the class I created is to provide inter application communication so that the client process can 'see' what the backend application is doing if so desired by passing messages back. A number of special fields on the event table make this very easy, where this same type of functionality with message queues would have to involve additional messages to be sent and retrieved. For this particular purpose a table based approach is actually much more flexible. For what it's worth it's possible to subclass this class to use MSMQ behind the scenes although this has not been provided as of yet. As of now VFP and SQL Server based classes are provided.

Introducing the wwAsyncRequest class

Let's take a look at the wwAsyncRequest class. The class provides a host of useful features that make it real easy to create message based applications that need to pass events from one application to another as well as passing messages between the two interoperating applications if so desired. The class provides:

 

 

 

To see how the class works let's start by looking at a simple example applet that demonstrates its use. The first step of the operation is the user actually clicking on a hyperlink to submit the request to start running a long running request – in this case a simulated query that generates an XML document output returned to the browser. After the initial click the user sees a page like the one shown in figure 2.

Figure 2 – Once the request has been submitted the browser 'pings' the server every few seconds for progress information. In this case the server application even provides information for how much longer the request will run.

 

The first request that comes back will not show any status information, because the request has just been submitted. The URL for the first request is:

 

AsyncWebRequest.wwd

 

which simply sets up the request and submits it into the event queue. For this simplistic example, the Web application posts a SQL statement into the Property manager, posts the event and then simply starts up a separate EXE file to process this event by passing the event id to it. The external program will pick up the event and process it, while the Web server app simply returns a status page that has no update info from the Application server yet. This first page and all subsequent status pages include a refresh header at the top:

 

<html>

<head>

<title>Running Report</title>

<META HTTP-EQUIV="Refresh" CONTENT="4;

      URL=AsyncWebRequest.wwd?Action=Check&RequestId=0CU0QTCAG1836">

</head>

<body>

…

 

The <META> refresh tag forces the page to automatically reload after 4 seconds in this case and go the following URL:

 

AsyncWebRequest.wwd?Action=Check&RequestID=SomeId

 

The request Id identifies this particular event that we're tracking and the action asks that we want to check for completion of the request. If the request is still pending the same kind of page is displayed again, with this same <META> header to continue refreshing after each page.

 

When the check occurs the Web application has the opportunity to show progress in some form. The wwAsyncWebRequest class provides several mechanisms to do this:

 

 

This example uses both of them – the dots you see in Figure 1 are lengthed each time the page is refreshed to the number of times we checked for completion. And the Done in xx seconds text is retrieved from the Status property that was set by the application server. In this case the server knows how long the request will take, but the more common scenario will be to provide the stages of processing that is occurring there like Running Accounting Report, Summarizing Totals and so on. Providing status strings with changing data is important to let the user know that his Web browser has not locked up and to not click refresh on his own every two seconds.

 

When the application server completes its task it writes the result – in this case an XML document -   into the ReturnData property of the object. This time when the Web application checks for completion it'll find the request completed and picks up the value stored in the ReturnData field and simply displays the XML document in the browser.

The Web server request

The following code demonstrates the Web application code using West Wind Web Connection to perform the Web server side task for this operation (note you can easily adapt this code to work with any implementation including COM objects or plain ASP pages using the wwAsyncWebRequest object as a COM object):

 

************************************************************

* wwDemo :: AsyncWebRequest

****************************************

FUNCTION AsyncWebRequest

 

SET PROCEDURE TO wwAsyncWebRequest ADDITIVE

SET PROCEDURE TO wwXMLState ADDITIVE

 

*** Refresh page every 8 seconds

lnPageRefresh = 4

lnPageTimeout = 15   && try 15 times to get result

 

*** Choose SQL or VFP tables

#IF WWC_USE_SQL_SYSTEMFILES

  loAsync = CREATEOBJECT("wwSQLAsyncWebRequest")

  loAsync.Connect(SERVER.oSQL)

#ELSE 

  loAsync = CREATEOBJECT("wwAsyncWebRequest")

#ENDIF 

 

*** Retrieve ID and Action

lcId = Request.QueryString("RequestId")

lcAction = lower(Request.QueryString("Action"))

IF empty(lcAction)

   lcAction = "submit"

ENDIF

 

DO CASE

   *** Place the event

   CASE lcAction = "submit"

      *** Create new event, but don't save yet (.T. parm)

      lcId = loAsync.SubmitEvent(,"wwDemo TestEvent",.T.)

      loAsync.SetProperty("Report","CustList")

      loAsync.SetProperty("SQL","select * from tt_Cust")

      loAsync.SaveEvent()

     

      *** Run the demo Handler Server

      lcExe = FULLPATH("wwasyncwebrequesthandler.exe") + ;

              " " + lcID + ;

              IIF(WWC_USE_SQL_SYSTEMFILES," SQL","")

      RUN /n4 &lcEXE

 

   *** Check for completion

   CASE lcAction = "check"

      lnResult = loAsync.CheckForCompletion(lcID)

      DO CASE

      CASE lnResult = 1

         *** Display result - XML doc return here

         Response.ContentTypeHeader("text/xml")

         Response.Write(loAsync.oEvent.ReturnData)

         RETURN

      CASE lnResult = -2  && No Event found

         THIS.ErrorMsg("Invalid Event ID",;

                        "Couldn't find a matching event.")

         RETURN

      CASE lnResult = -1  && Cancelled

         THIS.ErrorMsg("Event Cancelled",;

                       "The event has been cancelled.")

         RETURN

      ENDIF

   *** Cancel the Event by user

   CASE lcAction = "cancel"

      loAsync.CancelEvent(lcID)

      THIS.StandardPage("Async Request Cancelled")

      RETURN

ENDCASE

 

*** Check for timeout on the Event

IF loAsync.oEvent.chkCounter > lnPageTimeOut

   loAsync.CancelEvent(lcId)

   THIS.StandardPage("Sorry, this request timed out",;

                     "Timed out after " + TRANSFORM(lnPageTimeOut) + ;

                     " requests...")

   RETURN

ENDIF

 

*** Create the waiting output page

lcBody = "<hr><b>Waiting for report to complete" + ;

         REPLICATE(". ",loAsync.oEvent.ChkCounter + 1) + "</b>" +;

         IIF(!EMPTY(loAsync.oEvent.Status)," (" + ;

                    loAsync.oEvent.Status + ")","") +;

         "<hr><p>" + ;

         "This report,… <more text omitted here>"

 

*** Create the 'Waiting...' page. META refresh is generated

*** via the 4th and 5th parameters to refresh the page

THIS.StandardPage("Running Report",lcBody,,lnPageRefresh,;

                  "AsyncWebRequest.wwd?Action=Check&RequestId=" + lcId)

 

RETURN

 

There are two blocks of code that are important: The CASE statement with the handling of the Submit and Check actions and the call to StandardPage() which is responsible for generating the HTML for the refresh page. Web Connection's wwProcess::StandardPage() method includes support for <META> refresh via its 4th and 5th parameters by supplying the timeout value and URL to go to respectively.

 

The CASE statement's SUBMIT section demonstrates several features of the wwAsyncWebRequest class: SubmitEvent() is used to create a new event with which you can pass in a block of input data (XML inputs are often a great choice for this) and a title for the event. The final parameter of .T. in this case says to not submit this event to the queue just yet, because we'll want to set a few additional properties. At this point the event does not exist in the Event table yet.

 

The oAsync object has an oEvent member that maps to all the fields in the actual underlying fields of the Event table. So you have oAsync.oEvent.InputData and oAsync.oEvent.ReturnData for example. Other properties include status, chkCounter, userid, submitted, started, completed, expire, cancelled and a free form Properties field. The Properties property can be set with the Get/SetProperty methods of the oAsync object as is shown in the example. These methods set XML based keys that can be easily set and retrieved. When the oAsync object has been updated completely. You can use all of these properties to assign data to, to control operation your event handler.

 

Once you've set up the object completely and you're ready to submit, you call oAsync.Save() to actually write/update the event record. In this case the event record is written for the first time.

 

On the CHECK action in the CASE statement the key method is CheckForCompletion(), which checks whether a specific event has finished processing, has timed or has been cancelled. This method returns a numeric value that identifies the current event status:

 

1        – Completed

0        – Still processing

-1       – Cancelled

-2       – Invalid Event Id

 

In the above example the first check is made for completion and if the value is indeed 1 we simply retrieve the ReturnData property from the oAsync.oEvent member that is set by the CheckForCompletion() call. Here we simply echo back the XML by writing it out to the browser. In a more real world scenario you'd probably do something with the XML like write back to a cursor and perform further processing.

 

If the request is still processing (0) then we simply fall through the CASE statement and let the StandardPage() call at the end of the request handle the display of the status page.

 

Check out the way the Cancel operation is handled as well. The call to CancelEvent() sets the Cancel flag on the object which can then be picked up by the application server to potentially stop processing and abort. In this example it works because the server side code happens to run in a loop that can check for the cancel flag and simply get out. This is very powerful to allow users the ability to abort operations.

The Application server code

As I mentioned above the application server here is very basic and primarily used to demonstrate the operations that the server would use to handle requests. In this example the application server is simply launched from the Web Server application with a RUN command that passes the Event ID to the application server. The server then picks up the id, retrieves the inputs and goes off processing.

 

This is a specific handler, totally non-generic for this example that happens to compile a single operation into a standalone EXE file. Here's the code for this simple procedural function that makes up the EXE file:

 

*** wwAsyncWebRequestHandler

LPARAMETERS lcID, lcSQL

#INCLUDE WCONNECT.H

LOCAL lcID

 

IF EMPTY(lcID)

   RETURN

ENDIF

 

SET EXCLUSIVE OFF

SET DELETED OFF

SET SAFETY OFF

 

SET PROCEDURE TO wwUtils ADDITIVE

SET PROCEDURE TO wwXMLState Additive

SET PROCEDURE TO wwAsyncWebRequest Additive

SET CLASSLIB TO wwXML ADDITIVE

SET CLASSLIB TO wwSQL Additive

 

*** Make sure we can see the event file

DO PATH WITH "wwdemo\"

DO PATH WITH ".."

 

IF !EMPTY(lcSQL)

   loAsync = CREATEOBJECT("wwSQLAsyncWebRequest")

   loAsync.Connect("driver={SQL Server};server=(local);database=WestWind;uid=sa;pwd=")

ELSE

   loAsync = CREATEOBJECT("wwAsyncWebRequest")

ENDIF

 

IF !loAsync.LoadEvent(lcID)

   RETURN

ENDIF  

 

*** Update the Started Time Stamp

loAsync.oEvent.Started = DATETIME()

loAsync.SaveEvent()

 

FOR x=1 to 40

  lnSecsLeft = 40 - x

  WAIT WINDOW "Simulating long request taking " +;

              TRANS(lnSecsLeft) + " seconds..."  TIMEOUT 1

  IF !loAsync.LoadEvent(lcID)  && Get latest data!

     WAIT WINDOW "Failed reading " + lcId NOWAIT

     LOOP

  ENDIF

 

  *** Check for cancellation

  IF loAsyn.oEvent.Cancelled

     RETURN && Just exit and get out

  ENDIF

 

  loAsync.oEvent.Status = "Done in " +;

                          TRANS (lnSecsLeft) + " secs"

  loAsync.SaveEvent()

ENDFOR 

 

lcSQL = loAsync.GetProperty("SQL")

 

*** Run the SQL Statement

? lcSQL

&lcSQL INTO Cursor TTCustList

 

loXML = CREATEOBJECT("wwXML")

lcXML = loXML.CursorToXML()

lcXML = loXML.EncodeXML(lcXML)

 

*** Close out the request and pass the return data

*** into the ResultData property

loAsync.CompleteEvent(lcID,lcXML)

 

*** EXIT app

RETURN

 

The first thing that happens is that the event is loaded with LoadEvent() which is the base method used to access an event by ID. To let the client know that the application has started with set the Started property and call SaveEvent() to write the updated data to the event table.

 

The actual long running request here is totally simulated with a FOR loop and a timed WAIT window. Note the check for the Cancelled property inside of the loop to exit the operation. In this canceling is quite easy because we are running in a loop that executes a certain number of times. Most other applications will have specific commands or database operations that take a long time to run, so checking for Cancelled will have to be sprinkled throughout the code to get out and may not be so immediate.

 

The FOR loop also handles updating the Status property with the amount of remaining seconds for this request. Note the use of LoadEvent(), updating of properties and then calling SaveEvent() to update the table. You will want to call LoadEvent() to make sure you have a recent copy of the data – for things like the client's Cancel flag and his chKCounter property. And the client can also pass you information in some cases if that's required using any of the properties provided on the oAsync.oEvent object.

 

The 'actual' task performed by this handler is to run a SQL statement that was stored into a property with SetProperty("SQL",lcSQLStatement) on the WebServer. Here we pull out that property with GetProperty("SQL") and run the SQL statement, convert the result to XML and set the ReturnData property with this result string. Use SaveEvent() to write the data and this application server code is done.

 

As I mentioned above that's a really generic handler that is very specific to this request. To write a more generic handler that handles more than one type of request you can use wwAsyncWebRequest's GetNextEvent() method, which pulls the next waiting event out of the event queue:

 

DECLARE SLEEP IN WIN32API INTEGER

DO WHILE .T.

  IF !oAsync.GetNextEvent()

     Sleep(500)  && Wait half second

     LOOP

  ENDIF

 

  lcAction = oASync.GetProperty("Action")

  DO CASE

    CASE lcAction = "SQLQuery"

       …

    CASE lcAction = "COMObject"

      …

    CASE lcAction = "PRREPORT"

      …

    CASE lcAction = "EXIT"

      EXIT

  ENDCASE

ENDDO

 

Or this could run off a timer in a form. The handler can be totally generic. For example it could be set up to pass SOAP messages to the server and based on the SOAP message the server could run the SOAP request and return the result using the InputData and ReturnData properties to pass the SOAP packets around.

Taking a closer look at wwASyncWebRequest

The wwAsyncWebRequest class is built to be easy to use. It supports underlying event tables either in VFP or SQL Server tables. The SQL Server version of wwAsyncWebRequest uses a different subclass actually:

 

*** In server startup code

Server.oSQL = CREATE("wwSQL")

Server.oSQL.Connect("DSN=westwind;uid=sa;pwd=")

…

 

*** In Process Code

oAsync = CREATE("wwSQLAsyncWebRequest")

oAsync.Connect(oSQL)

* oAysnc.Connect("DSN=westwind;uid=sa;pwd=")

 

Use a separate oSQL object if the connection to the database is to be persisted across Web requests.

 

The key methods of the object are LoadEvent and SaveEvent which are low level and reused throughout the class's higher level methods like CheckForCompletion, GetNextEvent and CompleteEvent that your code typically will call.

 

************************************************************

* wwAsyncWebRequest :: LoadEvent

****************************************

***  Function: Loads a Event from the event table by ID

***      Pass: lcID

***    Return: .T. or .F.   oEvent set afterwards

************************************************************

FUNCTION LoadEvent(lcID)

 

IF EMPTY(lcId)

   THIS.ErrorMsg("No ID passed")

   RETURN .F.

ENDIF

 

THIS.Open()

  

IF lcID = "BLANK"

   SCATTER NAME THIS.oEvent MEMO BLANK

   THIS.oEvent.Expire = THIS.nDefaultExpire

   RETURN .T.

ENDIF 

 

*** Force a refresh always

REPLACE ID WITH ID

 

lcID = PADR(lcID,FSIZE("ID"))

LOCATE FOR ID = lcID

IF FOUND()

 

   SCATTER NAME THIS.oEvent MEMO

   RETURN .T.

ENDIF

 

SCATTER NAME THIS.oEvent MEMO BLANK

 

THIS.SetError("Event not found")

RETURN .F.  

ENDFUNC

*  wwAsyncWebRequest :: LoadEvent

 

************************************************************

* wwAsyncWebRequest :: SaveEvent

****************************************

***  Function: Saves the currently open Event object

***      Pass: nothing

***    Return: .T. or .F.

************************************************************

FUNCTION SaveEvent

LOCAL lcID

 

lcID = THIS.oEvent.id

 

THIS.Open()

LOCATE FOR ID == lcID

IF !FOUND()

   APPEND BLANK

ENDIF

  

GATHER NAME THIS.oEvent MEMO

 

RETURN .T.

ENDFUNC

*  wwAsyncWebRequest :: SaveEvent

 

You can see in these two methods that the oEvent member is key to the operation of this class. The member is created with a SCATTER NAME command, which creates an object with all of the fieldnames of the underlying table. The SQL version uses this same context but retrieves the data from SQL Server via SQLExec statements. Special Update and Insert statement builder code creates auto-update code to write the object content back to the SQL database in the overridden class.

 

Most other methods make use of the LoadEvent and SaveEvent methods. For example CheckForCompletion:

 

 

 

 

*************************************************************

wwAsyncWebRequest :: CheckForCompletion

*****************************************

***  Function: Checks to see if an event has been completed

***            and simply returns a result value of the status.

***    Assume: You can check oEvent for details

***      Pass: lcID

***    Return: 1 - Completed and oEvent is set

***            0 - Still running and oEvent is set

***           -1 - Cancelled and oEvent is set

***           -2 - Event ID is invalid

************************************************************

FUNCTION CheckForCompletion( lcID )

 

*** Invalid Event ID

IF !THIS.LoadEvent(lcID)

   THIS.SetError("Invalid Event ID")

   RETURN -2

ENDIF  

 

*** Cancelled Event

IF THIS.oEvent.Cancelled

   RETURN -1

ENDIF

 

*** Event is done

IF THIS.oEvent.Completed > {01/01/1990}

   RETURN 1

ENDIF

 

*** Increase the number of checks

THIS.oEvent.chkCounter = THIS.oEvent.chkCounter + 1

THIS.SaveEvent()

 

*** Still waiting

RETURN 0

ENDFUNC

*  wwAsyncWebRequest :: CheckForCompletion

 

loads an event checks for various object settings then updates the counter if still waiting for the server to complete the request.

 

The GetNextEvent() method is a little tricky in that it has to make sure that only one client retrieves an event at a single time. Using record locks this is easy to accomplish:

 

*************************************************************

wwAsyncWebRequest :: GetNextEvent

****************************************

***  Function: Sets the current event with the next event

***            in the queue.

***    Assume: sets the oEvent member

***      Pass: Optinal - the type of event to look for

***    Return: .T. if oEvent was set. .F. if no events pending.

************************************************************

FUNCTION GetNextEvent

LPARAMETERS lcType

 

THIS.Open()

 

IF EMPTY(lcType)

   lcType = "  "

ENDIF  

 

DO WHILE .T.

   LOCATE FOR STARTED = { : } AND COMPLETED = { : } and ;

              TYPE = PADR(lcType,FSIZE("type"))

   IF FOUND()

      IF RLOCK()  && Make sure we can lock it

         SCATTER NAME THIS.oEvent MEMO

         THIS.oEvent.Started = DATETIME()

         REPLACE Started WITH THIS.oEvent.Started

         UNLOCK

         RETURN .T.  && Got an event

      ENDIF

   ELSE

      RETURN .F.  && No Events

   ENDIF  

ENDDO

 

RETURN .F.

 

The SQL Server version uses a Stored Procedure to perform this task locking down a selected row via a SERIALIZABLE transaction.

Lots of uses – get to it

In this article I've shown you how the basic concepts behind building asynchronous request to handle Web requests. Asynchronous processing comes in handy in many places and it also allows you a way to scale processing off to other machines. The class I provided has used beyond these types of requests for any message based application that needs to pass messages between two applications. The mechanism used for this class is generic and can be applied to a variety of applications.

 

So, take a look at how you can apply these concepts into your application to improve performance, scalability and user experience with the tools I've provided you with here…

 

Resources

 

wwAsyncWebRequest Class

http://www.west-wind.com/presentations/wwAsyncWebRequest/wwAsyncWebRequest.zip

 

Free wwXML class
http://www.west-wind.com/wwxml.asp

 

Rick Strahl is president of West Wind Technologies on Maui, Hawaii. The company specializes in Internet application development and tools focused on Internet Information Server, ISAPI, C++ and Visual FoxPro. Rick is author of West Wind Web Connection, a powerful and widely used Web application framework for Visual FoxPro, West Wind HTML Help Builder, co-author of Visual WebBuilder, a Microsoft Most Valuable Professional, and a frequent contributor to FoxPro magazines and books. His book "Internet Applications with Visual FoxPro 6.0", was published in 1999 by Hentzenwerke Publishing. For more information please visit: http://www.west-wind.com/

 

 

Amazon Honor System Click Here to Pay Learn More

 

 

 
  White Papers                  Home |  White Papers  |  Message Board |  Search |  Products |  Purchase | News |