West Wind Web Connection


Web Connection is a complete Web application framework for developing and delivering scalable e-business solutions with Visual FoxPro. It is a proven development platform for integrating browser, server and database technologies into Web applications for Visual Studio and especially Visual FoxPro developers.

Make sure you read the following important topics:

This documentation is large, yes. Don't let it scare you off though, most of the information is here to help you with specific functionality, but for getting started you only need to know a few things.

Don't know where to start? Here are links to get you on your way:

Can't find it, or you're stuck? Try these support links:



What's new


Version 5.38

not released yet

Version 5.37

6/30/2008


Version 5.35

4/8/2008

Version 5.33

3/5/2008

Version 5.31

12/6/2007

Version 5.30

10/25/2007


Version 5.27

released 9/5/2007

Version 5.25

6/25/2007

Version 5.22

5/3/2007

Version 5.20

2/14/2007

Version 5.15

10/4/2006

Version 5.10

8/23/2006


Version 5.05

5/24/2006

Version 5.03

4/25/2006

Version 5.01

3/21/2006

Version 5.00

3/2/2006


Licensing


Web Connection is available in two licensing modes: Single server developer, and various sizes of runtime distributions. Developer Licensing for West Wind Web Connection is on a per physical server basis and per developer basis.

For current pricing for both developer and runtime licenses please check our product pricing page.

Single Developer License

The full developer license comes with a single developer license and a live server license so you can develop your application on your local machine and deploy it on a single live server. The developer license provides full source code to the FoxPro framework code and is required for any further runtime or developer licenses to be applied. Any additional developers require either another developer license or a runtime license as outlined below.

Runtime and Additional Developer Licensing

Runtime licensing is available by purchasing a runtime license pack which comes in sizes from 5 to unlimited distributions. A Runtime license can only be purchased in addition to at least one full developer license as the runtime licenses do not come with any distributables - it's just a license pack.

Runtime licenses may also be applied against additional developers on a team with one license per developer required.

A runtime license allows distribution of a compiled Web Connection application and distribution of any of the binary files included with Web Connection. No portions of the Web Connection framework may be re-distributed as source code without a full developer license. Runtime applications can make use of any of the framework classes supplied by the Web Connection framework including the HTML rendering classes and support tools as long as distributed in compiled form (APP/EXE).

Applications built with a distribution license must add significant value and cannot be in competition with the Web Connection development product. The Message Board and Offline Reader applications of Web Connection are excluded from any runtime agreement and may not be re-distributed in volume. A separate license agreement and pricing applies to redistribution of the message board application.

You may modify the source code and visual appearance of the Visual FoxPro Web Connection framework. Regardless of any changes made to the framework itself, it remains copyright of West Wind Technologies. Modification of any non Visual FoxPro binary files is not allowed.

Warranty Disclaimer: No Warranty!

IN NO EVENT SHALL THE AUTHOR, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THIS PROGRAM AND DOCUMENTATION, BE LIABLE FOR ANY COMMERCIAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM INCLUDING, BUT NOT LIMITED TO, LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR LOSSES SUSTAINED BY THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS, EVEN IF YOU OR OTHER PARTIES HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.


Upgrading from Version 4.0


Upgrades from Version 4.0 should be relatively painless as Web Connection 5.0 for the most part maintains backwards compatibility with Version 4.0. However, in order to take advantage of Web Connection 5.0 features there are a few things that you should do to optimize your applications and allow them to integrate with new tools and the new Web Control Framework.

The topics in this tree describe key changes in the framework that will affect existing applications.

Response.ExpandTemplate Eval Recursion no longer supported


Eval recursion has been turned off by default for Response.ExpandTemplate() and MergeText() and wwEval.MergeText(). This change is by design to minimize potential security breaches.

The main concern is that unchecked nested tags containing user input could easily execute code in your pages in unexpected situations which opens up a huge security hole. For this reason recursion is off by default, and we don't make it real easy to turn it back on.

With recursion on, imagine a user entering this into a textbox:

<%= Version() %>

If your template now echos that value with:

<%= lcInputValue %>

and recursion is on the page will display the FoxPro version number! At this point, effectively the user has executed code in YOUR FoxPro application. Version() is one of the milder problems of what can execute...

So for this reason we've turned off recursive expressions. If you really need recursive expressions, you can use:

<%= MergeText(lcValue) %>

which now explictly recurses the result value.

The wwEval object also contains a lAllowEvalRecursion flag that is off by default. If you have pages that you know explicitly require recursive expressions you can forego ExpandTemplate and instead use this equivalent code:

loEval = CREATE("wwEval") loEval.lAllowEvalRecursion = .T. lcResult = loEval.MergeText( FILE2VAR( Request.GetPhysicalPath() ) ) Response.Write(lcResult)


DEBUGMODE and Error Handling Changes


Web Connection 5.0 introduces a new Error management mechanism that is a bit more flexible and relies on TRY/CATCH exception handling. This new mechanism is more consistent and removes one potential problem issue related to the RETURN TO command in Visual FoxPro and the need to bracket Error methods with #IF define blocks. All of that is gone in favor of a simpler event based error mechanism that can simply be overridden in your code.

DEBUGMODE Flag - Gone

The first change is the removal of the DEBUGMODE flag. This flag was used internally by Web Connection to turn error handling on and off. Primarily it served to bracket error methods in wwServer, wwProcess and your wwProcess subclasses.

The DEBUGMODE flag is replaced primarily by the Server.lDebugMode flag now. This flag is now dynamic and can be changed in your appMain.ini file or even dynamically at runtime on the Server Status form.

Action Item:
You should check all of your code and find any DEBUGMODE usage. Especially check your custom wwProcess subclasses and make sure that there aren't any DEBUGMODE flags. If they wrap Error Methods see the next section on how to change your code.

Custom wwProcess Error Methods

Due to the changes to error handling Error methods should no longer be used in your custom wwProcess classes. If you have Error methods in your Process classes, instead change them to the following signature:

FUNCTION OnError(loException as Exception) * ... your custom error code here ENDFUNC

This method serves exactly the same functionality as the old Error method did, but as you can see it doesn't receive three parameters, but rather a FoxPro Exception object. Errors are caught in a TRY/CATCH as part of the Process class' Process() method and wrapped around the RouteRequest() method. This captures any error in your code and fires the OnError() method on your wwProcess subclass.

Keep in mind that overriding OnError is optional. The most common scenario is overriding how the error message is displayed and you can override that instead by overriding the ErrorMsg() function.

But if you need to explicitly manage your errors here's the default OnError implementation that you can emulate:

FUNCTION OnError(loException as Exception) *** Shut down this request with an error page - SAFE MESSAGE (doesn't rely on any objects) THIS.ErrorMsg("Application Error",; "An application error occurred while processing the current page. We apologize for the inconvenience. " + ; "The error has been logged and forwarded to the site administrator and we are working on fixing this problem as soon as we can.<p>" + ; "<p align='center'><table cellpadding='5' border='0' background='whitesmoke' width='550' " +; "style='font-size:10pt;border-collapse:collapse;border-width:2px;border-style:solid;border-color:navy'>" + CRLF +; "<tr><td colspan='2' class='gridheader' align='left' style='font-weight:bold;color:cornsilk;background:navy'>Error Information:</th></tr>" + CRLF +; "<tr><td align='right' width='150'>Error Message:</td><td>"+ loException.Message + "</td></tr>" + CRLF +; "<tr><td align='right'>Error Number:</td><td> " + TRANSFORM(loException.ErrorNo) + "</td></tr>" + CRLF +; "<tr><td align='right'>Running Method:</td><td> " + loException.Procedure + "</td></tr>"+CRLF+; "<tr><td align='right'>Current Code:</td><td> "+ loException.LineContents + "</td></tr>"+CRLF+; "<tr><td align='right'>Current Code Line:</td><td> " + TRANSFORM( loException.LineNo) + "</td></tr>"+ crlf +; "<tr><td align='right'>Exception Handled by:</td><td>" + THIS.CLASS + ".OnError()</td></tr></table></p>") *** Log the Request IF TYPE("THIS.oServer")="O" AND !ISNULL(THIS.oServer) this.LogError(loException) ENDIF *** We've completely handled the error! RETURN .T. ENDFUNC



Change Process Class Process() methods to OnProcessInit()


The wwProcess class has been reworked to be a bit more modular to provide for easier isolation of processing and improved error handling. In order to make this happen the Process() method has been relegated as a high level dispatch method only which delegates to lower level methods.

The typical flow of a request now is:

  1. wwProcess::Process() called
  2. wwProcess::OnProcessInit() called
  3. wwProcess::RouteRequest() called
  4. wwProcess::OnProcessComplete() called
  5. if an error occurs OnError is called (default implementation exists)

In Web Connection 4.0 you previously used the Process() to hook up any operation that applied to all requests that hit this process class - it fires on every request.

This behavior still exists, but the recommended hook point now is OnProcessInit().

Action Item: Rename Process() to OnProcessInit
In most cases you can simply rename your Process() method if you even have one to OnProcessInit() and you should be good to go. The only rare exception is if you need to create PRIVATE variables visible further down the call stack. For more info see the OnProcessInit() topic.

Action Item: Add PRIVATE defines at the top of the Process PRG file
If you use the new Wizards, Web Connection automatically generates a set of PRIVATE vars for the 'known' server objects like Request, Response, Server etc. at the top of the PRG file so that they are always visible. This makes it possible to make assignments to these classes at any time without worrying where you are in the FoxPro call stack.

LPARAMETER loServer LOCAL loProcess PRIVATE Request, Response, Server, Session, Process STORE .null. TO Request, Response, Server, Session, Process



Using the new wwPageResponse Class


This topic is an optional one and deals with converting to the new wwPageResponse class as opposed to the Version 4.0 wwResponse class. This class is more lightweight and provides the ability to build up your page content more consistently by allowing header creation at any time, so you can add Cookies and other header items at any time. It basically replaces the wwResponse and wwHTTPHeader class combination of classes.

Tip: Can't use the wwPageResponse class? Try wwPageResponse40
The wwPageResponse class removes many 'utility' methods of the wwResponse class. If you have legacy response method calls that don't work with the wwPageResponse class, there's another subclass of the class called wwPageResponse40 that adds back the various dropped methods like all of the Form???() methods, Send/SendLn and a few other methods that had been dropped.

Web Connection 5.0 works with the old Response classes by default, so you don't have to change anything to get your code to run. However if you want to use the new object you will have to make a few adjustments to your existing code.

Action Item: Enable the wwPageResponse class
In order to use the new wwPageResponse class you have to set a property in your wwProcess subclass definition.

************************************************************* DEFINE CLASS wwStore AS WWC_PROCESS ************************************************************* cResponseClass = [WWC_PAGERESPONSE] *** If you need to use the 4.0 compatibility class * cResponseClass = "wwPageResponse40" ... ENDDEFINE

The wwPageResponse class mixes wwHttpHeader and wwResponse functionality so anything that used to require separate headers can now in most cases use the wwResponse object directly. For example:

Response.AddCookie("TestCookie","MyValue","/wwstore") Response.Headers.Add("Expiration","-1") Response.AddForceReload()

The old way still works as well so you can still construct a wwHttpHeader object and pass it to ContentTypeHeader(), but this is not recommended because that mechanism will output the header immediately.

Action Item: Remove wwHttpHeader code and replace with Response methods
To optimize your code remove any wwHttpHeader related code and use the Response methods directly.

Make sure you test your code. If you should run into problems with headers they will most likely be related to duplicate headers. In those cases make sure that you are not explicitly writing out headers in your application code. Always try to use the wwPageResponse object directly.



Log and Session File Structure Updates


The format of the wwRequestLog and wwSession files have changed so if you're upgrading make sure that you delete both wwRequestLog.dbf and wwSession.dbf before running your application. If you're running the Setup application it will prompt to delete the DBF files for you.

If you're using SQL Server log and session data you will need delete the Log and Session tables and then run the Console SQL Wizard to recreate them or manually update the tables.

wwRequestLog Changes
The wwRequestLog has been updated to hold a unique Request Id that is passed from the ISAPI extension.

CREATE TABLE (THIS.cLogFile) FREE ; ( ; TIME T ,; REQID C(20),; Script c(50) ,; QueryStr M ,; REMOTEADDR C(16) ,; Duration N (5,2),; MemUsed C (8) ,; ERROR L ,; REQDATA M,; Browser M,; Respone M )

For SQL Server:

CREATE TABLE [dbo].[wwrequestlog] ( [time] [datetime] NOT NULL , [reqid] [varchar] (20) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [script] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [querystr] [varchar] (254) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [duration] [numeric](7, 3) NOT NULL , [memused] [char] (8) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [error] [bit] NOT NULL , [reqdata] [text] COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [browser] [varchar] (156) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]


wwSession Updates
The wwSession class has changed the way the Session table is created by widening the SessionID to 14 characters. The widening is to ensure unique session ids that create SYS(2015) plus the current ThreadID.

If you use FoxPro Table Session (wwSession) then you can simply delete your wwSession table. Web Connection will automatically recreate the file as needed. The update structure is:

CREATE TABLE ( THIS.cDataPath+THIS.cTableName ) FREE; (SESSIONID C (14),; USERID C (15),; FIRSTON T ,; LASTON T ,; VARS M ,; BROWSER M ,; IP M ,; HITS I )

If you forget to update your FoxPro table the code will still work but the new ThreadID is stripped. This is not really an issue - you get essentially the old behavior. This change really only affects high volume applications where many instances are running simultaneously.

For SQL Server you will need to modify the database and change the

CREATE TABLE [dbo].[wwsession] ( [SessionID] [char] (14) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [UserId] [char] (15) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [firston] [datetime] NOT NULL , [laston] [datetime] NOT NULL , [vars] [text] COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [browser] [text] COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL , [ip] [text] COLLATE SQL_Latin1_General_CP1_CI_AS NULL , [hits] [int] NOT NULL ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO

If you forget to update the SQL Session table you will get errors when trying to write a new session record.

Acknowledgements


West Wind Technologies would like the following people for their invaluable help with this product and otherwise lending valuable support.

West Wind Most Valuable Professionals
We're acknowledging the support many of the people in our community have given back to others on the message board and around shows and other events. We here at West Wind Technologies and the community at large thanks these individuals by awarding MVP awards. To find out more visit the MVP site.

Randy Pearson
Randy Pearson of Cycla Corporation has been an extremely active member in the Web Connection user community to the point that many people visiting the message board think he's a West Wind Employee, which he is not. His valuable contributions and thoughtful comments and suggestions can also be found in many aspects of Web Connection and had an impact on many of the features found in the product. Finally, Randy was in charge of the Migration topics found in this help file along with with the Migration Tool utility to move 2.x projects to 3.x. Randy is also co-author of the WebRad Web Connection book.

Lauren Clarke
Lauren has become one of the top resources on the West Wind Message Board and has been a huge help in testing various new developments in Web Connection. He's also been instrumental in a number of ideas and tossing around brainstorming ideas for new features in the product.

Markus Egger
Markus (EPS Software) and I have been business and development partners for some years now and Markus although not directly involved in this product has always been a great help in bouncing ideas and concepts off of. Many of these have found their way into Web Connection.

James Murez
For all of his help related to pushing Web Connection harder as a product and improving visibility both for WWWC as well as Visual FoxPro in general. He's also been very actively testing several not so often used components of Web Connection and provides tons of feedback both on the documentation as well as on bugs/inconsistencies.

Erik Moore
Erik has worked on some technologies that have been silently integrated into Web Connection and he's had valuable feedback and discussions on several topics ranging from XML, SQL and ADO to the WinInet functionality in wwIPStuff.

Ken Levy
Ken's been big help in providing info on XML and a link to Microsoft for bugs and odd behaviors. Even though he cut his hair and doesn't work in the Fox team anymore, he's still one of us. Ken's been invaluable in helping isolate a number of bugs in the DHTML and XML object models that have been fixed since.

Microsoft Visual FoxPro Team
For the obvious: Providing us with a tool that is so powerful and lets us do things so easily that it simply boggles the mind that you can't do many things in other more visible Web products. I want to specifically thank Calvin Hsia for his help on many COM related issues on the road to providing the scalability in Web Connection. Also, Randy Brown for his support and help with various WinDNA related issues in VFP.

The frequent Message Board Crew
Yeah, that's all of you who frequent the message board and help out others or ask pointed questions about features and (gasp) bugs. Without this feedback this product would not evolve as much as it does.

I thank all of you for your help and support of making this product such a success in the FoxPro market!

+++ Rick ---

Rick Strahl
West Wind Technologies



Shareware version limitations and issues


The shareware version contains the Web Connection libraries in a pre-compiled format and as such imposes a few limitations and special behaviors.

Alias name already in use Errors when running demos

This error can occur if you've loaded the Web Connection libraries and then run some of the demos that use code contained in CONSOLE.EXE. Both WCONNECT.APP (the Web Connection pre-compiled library) and CONSOLE.EXE contain the Web Connection classes and if a class library is already loaded it can cause the above error to occur.

This is not an issue with the full version as source code is available and VFP can properly
load the classes from the source code when run.

If you see this error there are two ways to deal with it:

It's good practice to assign the following to a Macro key in VFP:

CANCEL CLEAR ALL CLOSE ALL RELEASE ALL CLEAR PROGRAM CLEAR SET CLASSLIB TO SET PROCEDURE TO

and excercise this key frequently to clear the environment.

The shareware version cannot build a VFP Project

Because the shareware version is precompiled and you can't add .APP files to a project directly, the shareware version of Web Connection does not build a VFP project file when building a new Web Connection application.

However, you can stil run any Web Connection application simply by executing the main PRG file (ie. DO webDemoMain.prg).

The shareware version runs in debug mode

Because the shareware version is precompiled a number of Web Connection's compiler directives are precompiled into the application. One of these settings - DEBUGMODE - specifies how errors are handled in Web Connection. The shareware version runs with DEBUGMODE = .T. which stops on errors, so all errors actually stop the Web Connection server and pop up in the Debugger.

In the full version you can swap the flag to .F. which makes Web Connection handle errors and display an error page and send notification emails etc. but in the shareware version the behavior is fixed to break at the error.

The shareware version pops up a shareware form every half an hour

In shareware mode Web Connection pops up a form every half an hour to notify you that this is the shareware version. This screen also stop operation of the server until you accept the form.

Updating from the Shareware Version


To upgrade from the Shareware version of Web Connection to the full version, you can simply install the full version of Web Connection either to a new directory or on top of your existing shareware installation. If you install on top of an existing installation simply unzip the distribution files into the original directory.

If you've modified any of the sample files be aware that the sample files will be overwritten in the installation - hopefully you will have created new projects in their own separate files rather than modifying the existing files. If not, make sure you back up these files first before installing the update.

After you've installed the new files over the shareware installation, make sure you:

You can do the following from the VFP Command Window in the Web Connection startup directory:

RELEASE ALL DELETE WCONNECT.APP DELETE FILE classes\*.fxp DELETE FILE *.fxp COMPILE classes\*.prg COMPILE CLASS CLASSES\*.vcx



Requirements and Recommended downloads


West Wind Web Connection 5.0 has the following requirements:

These are the only software requirements that you have to have. Web Connection is developed and tested with IIS Web Servers and that's what works best. However there's also limited support for Apache, but the installation and configuration is not as automated as with IIS. Currently there are also issues with Apache 2.2 or later due to recent changes in the Apache Module architecture. Apache 2.1 and earlier are fully supported. For Apache 2.2 Web Connection has to run in CGI mode.

The following optional components are recommended:

Visual Studio 2008/2005 or Visual Web Developer
If you plan on using the new Web Connection 5.0 Web Control Framework you will want to use a version of IIS to layout and design the Web Control Pages in Visual Studio. Web Connection integrates with Visual Studio and provides FoxPro source code editing and browser previewing through the IDE. VIsual Web Developer is a free tool from Microsoft and it's sufficient for use with Web Connection. VS is not required however - Web Connection has no dependencies on Visual Studio or .NET at runtime and the Web Control Framework is based on pure text documents. Visual Studio is merely used to provide a rich design experience for the Web Controls. Any editor including NotePad or the VIsual FoxPro editor works.

The following components are useful but completely optional when running Web Connection:

Office Web Components for graphing
The Office Web Components are used on some of the graph demos and internally to generate graphics for the logs and a few other places. These components require an Office license (2000 or XP). It's recommended you download the latest version from the link below even if you have Office XP as the ClassIds for these components have changed.

http://office.microsoft.com/downloads/2002/owc10.aspx?HelpLCID=%HelpLang%1033


System, software and hosting requirements


Web Connection is designed as a server based application and thus requires a Microsoft Windows based server environment for deployment. It can however run just fine on Windows Vista, XP and Windows 2000 Desktop.

Server Software

For development you can use a client operating systems like Windows Vista, Windows XP or Windows 2000. For deployment a server version like Windows 2008, 2003 or 2000 operating system is required due to Microsoft's licensing policies.

Web Connection Supports the following Web Servers:

The product is designed primarily on Microsoft Internet Information Server (IIS), which provides best performance, stability and the full range of features. It also works well with Apache 2.0 and 2.2 and can be used with any any other ISAPI compatible server on Windows. We do recommend IIS especially IIS 6 or 7 as it provides the most stable and compatible platform for Web Connection on which Web Connection is developed and tested.

Development Software

Web Connection 5.0 is designed for Visual FoxPro 9.0, but also supports version 8.0. We highly recommend using Visual FoxPro 9.0 SP1 or later for best performance and taking advantage of all the latest features of the product.

If you plan on working with the Web Control Framework you'll want to use Visual Studio 2008 or the free Visual Web Developer 2008 (Visual Studio 2005 and VWD 2005 are also supported). Web Control Framework pages are plain text, but in order to get the full design experience a ASP.NET 2.0 compatible environment should be used. Click here for more info on whether you need Visual Studio.

Otherwise plain HTML editing tools like FrontPage and Dreamweaver can also be used for editing standard templates or even Web Control Framework templates (although you won't get full designer support for the controls). Of course you can edit any HTML templates including script and Web Control Framework templates with any text editor like Notepad or even the Visual FoxPro text editor.

Hardware

Web Connection can run on just about any hardware, but obviously a current, fast machine is preferrable. The software ran 10+ years ago on Pentium II machines and it can do so still today. Basically you'll want a machine that can run Visual FoxPro comfortably. More than anything memory is important - Web Connection instances take between 10-15 megs of memory each typically (although they can take more depending of what you do in your server code). Recommended minimum memory for an IIS server running Web Connection is 512 megs. It'll run in less, but it's not optimal.

For development we recommend the max amount of memory you can afford as it will make your dev environment work more smoothly. With memory under $50 a gigabyte, it's easy to justify running with 4 gigabytes of memory or more. Web development is very memory intensive especially if you use Visual Studio or Visual Web Developer. Also a fast machine won't hurt for development as you spend a lot of time flipping back and forth between different applications (VFP, Web Browser, HTML Development Tool etc.).

Web Hosting

Web Connection must run on a Web Server and getting the application online means that you are connected to the Internet. There are a variety of ways available to do this from in-house hosting to co-location (your server at an ISP's facility) or plain Web hosting.

The latter option tends to be the cheapest but unfortunately it's not likely that you will be able to use it for your Web Connection applications as a WWWC application requires binary executables which are usually not allowed by low end Web hosting services. However, there are special providers that specialize in hosting binary based application services and you may be able to talk to your ISP to get them to host you in this fashion.

Co-location lets you have your own machine or one provided by the ISP at their facility. The big advantage here is that you have full control over the server and can configure it as you choose. This usually includes the ability to add custom user accounts, set the security permissions for the server, add remote access services for pcAnyWhere or the like. It also makes sure that your application is the only one running on the server so that there's no interference or problems from these potentially dangerous applications running on the server at the same time. Co-location tends to be more pricey but for the piece of mind and control can save you lots of headaches. Pricing varies, but typically runs $200 a month and up.

Finally your company may already have an internal Internet connection and you can hook your server application into that network. In this situation you probably have to deal with your network administrator and any of the security policies configured for the company network. If your application is completely internal to your company (an Intranet) then a public Internet connection is not required. If your application is used both internally and externally (an Extranet) you will still need with the security issues of an open Internet connection through your IT staff.

For a list of providers that are Web Connection and Visual FoxPro friendly and provide related services, see http://www.west-wind.com/webconnection/webhosting.asp.



Visual FoxPro Environment Requirements


Visual FoxPro Versions Supported

Web Connection 5.0 requires Visual FoxPro 8.0 or 9.0 to run.

Visual FoxPro Environment Requirements and Settings



Documentation in Word Format


This document is also available in Word format at:

Documentation in Word Format

You can use Word to print the document into hard-copy. If you don't own Word 97 or Word 2000 or Word XP you can download the Word viewer from:

Word Viewer 2003

Note: The Word documentation may not be the very latest version as we only periodically update the Word docs. The Word file is also very large and you'll likely want to print only selected sections.


Support and reporting bugs


Support is available for free on our online West Wind Message Board. This site also links to a knowledgebase and makes a great first stop for any questions you might have. In addition to our staff, there's a great community of Web Connection developers and support questions are generally answered within a few hours - often less. To visit the Message Board go to:

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

Before you post:
Please, double check your code before posting to make sure you didn't overlook something obvious. When you post make sure you provide as much information as possible. Most importantly provide any error information that might be available for the Web Connection classes and Visual FoxPro. Most Web Connection classes have an lError flag and a cErrorMsg property which you can check for failure information. All of this will ensure we can answer your question more efficiently and get you up and running as quickly as possible

Additional Support:
If you require additional personalized support, you can contact West Wind Technologies for paid support via Email or Phone. We also provide customized on-site training and mentoring services. You can contact us for any of these services at:

West Wind Technologies
32 Kaiea Place
Paia, HI 96779
USA

Email: support@west-wind.com
Phone: (503) 914-6335
Fax: (815)572-0619

Please be aware that support is charged at regular consulting rates of $150/hr in half hour increments.

Message Reader

We also provide the very useful West West Wind Message Reader that allows you to download and read/write messages in a rich client environment. The offline reader also consolidates many of the message board tasks such as Chat, Knowledgebase Searches, Bug Reporting into a single organized interface.

The message board provides the following features:


These resources are very rich and provide you with both researchable material and the ability to ask questions about any problems you might have. The message board is monitored throughout the day and most questions are answered within an hour. Our official support response time is 24 hours weekdays, but most questions are answered within an hour during the day.

The West Wind Message Reader

In addition to the online Web application we encourage you to also use the Offline Message Reader which gives you access to all of these features via a local application. The reader makes it easier to write messages, format them and search and organize messages as well as providing a local searchable database of the message board content.

To run the Message Reader click on the West Wind Message Reader option from the Web Connection menu pad in Visual FoxPro or click on the shortcut in your Task Bar's Web Connection group.

The offline reader allows you to compose and read messages in the rich environment of a GUI application and provides support for creation of rich messages using standard editing features. As the name implies you can read and create messages offline and post them, get more messages when you reconnect. This is ideal for people while travelling or for those that have slow dial up connections. This environment is also much nicer to use than the Web application, so give it a shot.

The West Wind Knowledgebase

Go to the message board and hit the Knowledgebase Link to find our article databases, product information and knowledgebases for information about issues and solutions encountered before.

Reporting Bugs

If you have issues with the product that are outright bugs that cause failures and don't work as described you can use the bug reporting tool which is accessible either from the Message Reader or via the Web Connection menu in Visual FoxPro.

The bug report form asks you create a detailed error report to provide a reproducable scenario.

Please use this form only for problems that are reproducible or are outright bugs. If you're not sure how a feature or command works post a message on the message board. This mechanism is meant to be used as a basic bug tracking mechanism on the message board to allow tracing bugs from submission through resolution via message threads.

Web Connection Developer Registry


As a Web Connection developer you can sign up for the free Developer Registry that allows you to advertise your Web Connection related services in our registry. Fellow developers or prospective clients can search the registry and find your entry in their geographical area. The registry has been fairly popular and many developers and prospective clients have reported finding good matches for both developers and clients to get projects developed.

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

User Guide


The user guide outlines the basic operation of Web Connection.

What is Web Connection


Web Connection is a complete Web application framework for developing and delivering scalable e-business solutions with Visual FoxPro. It is a proven development platform with a 10 year history that has proven itself stable for a wide variety of Web and distributed applications from small to large scale. With Web Connection you can get your FoxPro applications online be it for HTML Based Web applications, or XML based distributed applications. It provides both the client and server tools to build powerful applications in either environment.

Web Connection provides a powerful modular framework that interfaces FoxPro with the Web Server. The core engine provides all the interfacing and server infrastructure with various modules and handlers plugging into the architecture. You can choose from building applications that use pure code to generate output, to using high level modules like the powerful Web Control Framework that provides a rich object oriented control and event based model to build applications more efficiently using a more familiar and productive desktop metaphor.

Web Connection provides you with lots of choices for generating output from your application. At the lowest level you can do everything in code - respond to requests, picking up request data with pure code and running your own FoxPro code to generate HTML output. Other mechanisms provide more highlevel generation tools. The base script support allows you to use external templates with a textmerge like mechanism to externalize HTML generation. You can generate output from FoxPro reports to PDF files and use many high level functions to generate HTML from data quickly.

Finally there's the powerful Web Control Framework which manages many aspects of Web Page display. It automatically manages page state, and re-assigns control content on postbacks, allows persisting data across requests and can generate HTML from easy to use controls that you can programmatically access in your code. You simply set the text property of a textbox, vs. generating HTML for the entire textbox for example, and communicate with the textbox's properties to set things like color alignment, fonts etc. The Web Framework in conjunction with Visual Studio or the free Visual Web Developer allows you use a visual drag and drop design surface and easy to use property editors to visually design your layouts and set up control layouts. This is the recommended mechanism for building Form Centric HTML Web applications with Web Connection 5.0.

What is Web Connection?

Web Connection consists of two major pieces of software that make up the overall product. A small C++ based connector application (ISAPI dll) that implements the communication interface between the Web Server and your Visual FoxPro application, and an extensive framework of Visual FoxPro classes that makes it easy to build application specific Web code. The Visual FoxPro framework is object oriented, extensible and open and is shipped with full source code when you purchase the product. You can see how it works and if necessary extend or modify the base code. No black box code here!

The Visual FoxPro framework consists of classes that manage communication with the Web connector ISAPI extension. They handle server management and administration, retrieving server and browser form variables from the server making them easily available to your code and providing a powerful set HTML classes and support tools that make short work of interactively building the HTML required to display your application on the Web. Web Connection provides access to all advanced HTTP based features including displaying HTML script pages containing FoxPro code and expressions from disk. Many advanced features like HTTP Authentication, HTTP Cookies, Sessions, custom HTTP headers, request logging, remote administration and configuration and server load balancing are built into the product and easily accessed through the framework.

And you can use rich design tools with Web Connection, so you can use FrontPage or Dreamweaver and the like to design your page templates, or if you're using the Web Control Framework using Visual Studio or Visual Web Developer to design your pages graphically in an interactive WYSIWYG environment.

Built for FoxPro and the way YOU like to work

Web Connection is built to work with Visual FoxPro from the ground up. The framework itself is written in FoxPro and anything you can do with FoxPro can be used in your Web applications. You can use it to access database tables, run queries against FoxPro or SQL Server or Oracle databases, you can use business objects or you can access the data directly. How you write your application is really up to you - Web Connection provides only the plumbing - you provide the application logic.

You can use existing business object frameworks with Web Connection. There are countless applications out there that use Mere Mortals, FoxExpress, Visual MaxFrame and others with Web Connection. And Web Connection itself also includes its own light weight business object framework that you can optionally use. While a Business Object Framework of some sort is recommended you don't have to use it of course - you can use FoxPro code and data access directly just as well.

Distributed Application Support

Web Connection can also built Fat Client applications let you use Visual FoxPro desktop applications communicating with a Web Connection (or other) Web Server application. Web Connection includes a rich set of client tools that can provide connectivity over HTTP using plain XML or the SOAP protocol for Web Services.

With Web Connection you can run SQL queries over the Web and create and call Web Services. You can create custom XML services or use more standard Web Services. Web Connection provides the client side HTTP tools as well as high level XML conversion tools that make XML integration a snap. The Web isn't all about HTML and these data messaging features let you build powerful distributed applications that aren't limited by HTML output. Several examples that demonstrate rich client applications are also provided in the box.

Chock full of tools, utilities and utlility functions

In addition Web Connection also provides tools for sending and receiving email (SMTP/POP3), sending and receiving files via FTP, low level TCP/IP access. There is an included light weight business object wwBusiness class that provides an easy way to get into building more structured applications. A wwSQL component helps interfacing with Database servers like SQL Server and Oracle and there are literally hundreds of utility classes that can be used in your application be they Web or desktop based.

Documentation

There's ample documentation of all aspects of the Web Connection framework, covering both the inner workings of the framework as well as detailed reference materials and a number of detailed tutorials for the different processing mechanisms. Web Connection comes with over 100 small samples that you can check out and look under the hood of to learn and plug into your own code. And there are a number of full scale sample applications that demonstrate many of the framework features in a more complete environment.

Some examples include a WebLog example, the Message Board application and a PhotoAlbum application to name a few.

Web Connection has been field tested on a number of high volume sites and even back in 1996 there were sites running in excess of 2 million hits a day. Imagine what you can do on today's hardware.

So read on and get ready to get Webbified! It's never been easier to build Visual FoxPro Web Applications!

…and what it isn't

Web Connection makes it easy to build Web applications, but it will not take an existing application and run it on the Web as is. Work is required to convert the interface to an HTML based design and if your application mixes interface and database logic tightly, existing code might not be easily integrated into your new Web application. On the other hand if your business logic is separated clearly from the interface then reusing business rules and other rule based classes or procedures will be easily integrated into your Web applications. Keep in mind you will have access to almost all of Visual FoxPro's wonderfully powerful features with the exception of VFP's GUI features!

Web applications are server based and respond to requests made from a Web page. Every action that occurs on a Web page generally fires back to the Web Server. In that sense Web applications are not truly event driven but follow a more rigid transaction based Request and Response model. Web Applications also are stateless and that imposes additional considerations. The short of it is that Web applications by nature are VERY different from Desktop applications.

The new Web Control Framework provides a programming model that is conceptually similar to Desktop Form development and is the recommended approach for first time developers as it provides a familiar control and event based metaphor for building Web applications. But even though the model is similar, understand that you are still bound by the limitations of HTML/HTTP based interfaces, so don't expect to be able to EXACTLY duplicate desktop application interfaces via HTML. This is neither always possible nor desirable as HTML interfaces often have different design targets.

The core feature that Web Connection provides is the ability to continue to use FoxPro code to build your Web applications. You can write code in the tool you are familiar with and you can likely reuse any non-User interface code in your Web application.

Another option for existing standalone applications is to build a distributed application that use the Web as a data transfer mechanism. If you go this route your UI code can potentially be reused using data pulled down from the server using tools in the wwIPStuff library. See the Online Distributed Application Demo for an idea of what you can do.

If you're new to the Web take a deep breath and read on - there's lots of info in this document that will clarify the way the Web works. Web Connection makes it easy to leverage your FoxPro skills. Once you understand the basic flow of information over the Web things will fall into place and allow you to be productive very quickly. You can crank out quality high performance applications without having to learn all of the Web technology up front and you'll learn about Web technology as you use Web Connection to build applications.


Roadmap to starting out with Web Connection


Looking at this documentation you may be feeling overwhelmed with information overload. Relax! You don't need to understand nor even read everything that's in this documentation, but when you want to dig deeper and understand some of the topics the information is there waiting for you. This topic serves as a roadmap for your startup with Web Connection.

This topic is designed to summarize the things that you should most definitely look at. It gives you an idea on where to start, what to try to get going and where to look for more information.

Use the Help File! It's got tons of information, and if you get stuck the answer is likely in here. It's keyworded and searchable in addition to the Content view, so take advantage of this very rich resource.

Setup and Installation

The first step in getting Web Connection installed is to install the product from the self-executing ZIP file. Run the file and the following installtion program. You can find more info on installation at this topic:

Setting up Web Connection

Checking out the demos

This step serves two purposes: It makes sure that the installation works and lets you see Web Connection in action with demos that you can look at and code you can step through if you choose. Make sure you run the Web Connection server (DO wcDemoMain.prg).

Go to the demo page

Most of the examples on the demo pages have links to show you the source code required to create that request by clicking on the [Show Code] link on the bottom of the page. At this time you can also start poking around in the code, making a few adjustments, maybe creating a new method and stepping through the code to get an idea what's involved in writing code for Web functionality.

There are two sets of samples: Core samples and Web Connection 5.0 samples, which sit on a separate page. To get started it may help to look at a few of the core samples first and see how Web Connection works in 'raw' code mode. Once you get the basic idea of how the Request and Response objects and the wwProcess class work, then you can take a look at the Web Control Framework (WCF) samples, which is the focus of Web Connection going forward. The WCF is conceptually a little more complex at first because there are many user controls to get familiar with, but it provides a much more interactive and flexible Web development approach to building Web applications. Both 'core' functionality and the new WCF features can co-exist in a single application.

Take the Step By Step Guide

The next step is to take the Core Step by Step Guide or The Web Control Framework Step by Step Guide which takes you through the motions of creating a new project, setting up custom requests of your own and writing code to handle a variety of tasks. This is a nice intro that shows you how to get started quickly with your own code. I suggest you follow the examples exactly creating the test project shown there to take you once through the process before building your own project in the next step. The Web Control Framework work through is also available as a Video walkthrough.

Digging a little deeper with the Framework Walkthrough

If now you're curious about some of the more advanced features of Web Connection and how things work behind the covers you can check out the How Web Connection workstopic. Here you find information on the architecture, how requests flow through the framework and another walk through that describes more advanced features that weren't covered in the Step by Step Guide.

This step is optional, but if you want to get the most out of Web Connection this topic tree will be well worth the time to read easpecially as you get more experienced with Web Connection.

Use the Web Connection Management Console to create a new project

Once you have familiarized a little bit with how Web Connection works you're probably ready to create your own requests for your own application. To do this you can use the New Project Wizard by running the Web Connection Management Console. To start the management Console type DO Console.exe into the command window or run the EXE from Explorer.

The new project will set up a new VFP application for you that you can start to write your own requests with. Now it's time to write access your business logic from within these Web requests and create some output to display back in the browser. As you start writing code you likely run into a few situations where you don't know how to do something. I suggest you go back to the demos and see if you can find something there that demonstrates what you're trying to do. Again, the message board is a great resource to post questions to if you get stuck or even if you want to bounce some ideas of other developers who have been down the path.

Use the Help File, Luke

Once you start your own development you will want to keep this help file handy. The extensive class reference will come in very handy as you start creating requests using the Web Connection framework classes. Most of your development work involves a couple of objects - wwRequest, wwResponse and wwProcess - take a look at those in detail to get up to speed quickly on what's available to you. Also, by now you may want some additional information on how things work and not just work by example from the demos.

The User Guide provides you with general discussion while the Class Reference (Framework Classes, Framework Support Classes, Utility Classes) provides detailed information on the actual classes in the framework. The class reference is laid out to provide an overview in the class header topic with examples and things to look out for with the actual class detail following class reference format showing the actual mechanics.

Configuration and Installation

Once you've built an application you will need to actually install it on a Web server or other server machine. Although this process is not difficult there are a fair number of files and components involved. The Web Connection Configuration topic describes many of the configuration components and the options available in them.

You can also resort to the Management Console and the Server Configuration Wizard to help you with configuring your Web server with the appropriate virtual directories and script maps as well as copying and registering the Web Connection components for your application.




Web Server Requirements


Web Connection requires a Web Server for building and testing your application and it supports both Microsoft Internet Information Server and Apache which are the two most popular Web servers. Out of the two IIS is the preferred choice again because it's our primary development platform on which we do primary development, testing and running live applications. Apache is also supported preferrable version 2.2 or later.

You can also work with the Visual Studio Web Server which makes it possible to work without a fully installed Web Server.

Microsoft Internet Information Server (IIS)
Most recent versions of Windows come with a version of IIS that can be used on your local machine.

IIS on Windows is installed as a Windows component and can be enabled through the Programs and Feature Control Panel applet | Windows Components.

There are three Windows versions that do not ship with IIS:

Visual Studio Web Server
If you'd rather not install a full Web Server or you are using Windows XP or Vista Home you can also use the Visual Studio Web Server with the Web Connection Managed Module. The Managed Module is a .NET implementation of the Web Connection connector that works with IIS 7 or ASP.NET 2.0 and can also work with Visual Studio's built in Web Server.

This option requires that Visual Studio or Visual Web Developer is up and running so typically you'll use this opt

There are some manual configuration steps involved in using this option which are described here:

Visual Studio Web Server and the Managed Module

Apache for Windows
Web Connection also work with Apache for Windows and provides basic configuration for Apache through the setup and new project wizards. Some custom configuration may be required.

Web Connection works best with Apache 2.2 or later. You can find the latest version of Apache here:

http://httpd.apache.org/




Setting Up Web Connection


The first step in getting going with Web Connection is to install the product. Web Connection ships as a self-extracting zip file that you either download from the Web or comes on a CD.

Unzip the Web Connection Install files

Click on the EXE file and unzip the application into a directory of your choice. The file is a WinZip self-extracting file, so it should ask you to either open the file or extract the content to a directory of your choice. I suggest you extract to a directory. This first step creates the Web Connection application directories, which are where all of the source and support files get installed.

Web Connection installs a hierarchy of directories as follows:

wconnect
	classes*	-	VFP source and direct support files for the framework (required for development)
	html		-	HTML sample pages
	scripts*	-	The Web Connection ISAPI DLL and CGI executable file
	templates	-	The Management Console File templates
	tools		-	several useful support tools and classes
	wwdemo		-	VFP source for the demos that ship with WWWC
	wwThreads	-	VFP Source for the Message Board
	wwReader	-	VFP Source for the Message Board Offline Reader application
	WebControl	-	The Web Control Framework Samples
	WebLog		-	The WebLog sample application

Run the Setup Program

The next vital step is to run the
Setup program which is installed into the Application root directory. The Setup.exe file should launch automatically when the unzipping completes - however if it does not make sure to run this file manually before proceeding. To do so, start Explorer, go to the Web Connection install directory and double click on Setup.Exe. The Setup program topic takes you through the installation steps - please read these topics as you step through the installation.

In case of problems

If something goes wrong during installation and your server won't work you can look at the Setup Troubleshooting topic, which describes a number of problem scenarios and attempts to take you through them. If these don't work for you you should post a message on the message board at http://www.west-wind.com/wwThreads/.

Checking out the demos

This step serves two purposes: It makes sure that the installation works and lets you see Web Connection in action with demos that you can look at and code you can step through if you choose. Make sure you run the Web Connection server (DO wcDemoMain.prg). You Should be able to navigate to http://localhost/wconnect/ to get started.

Most of the examples on the demo pages have links to show you the source code required to create that request by clicking on the [Show Code] link on the bottom of the page. At this time you can also start poking around in the code, making a few adjustments, maybe creating a new method and stepping through the code to get an idea what's involved in writing code for Web functionality.


Unzipping the self extracting Zip file


If you're reading this topic in the help file it means that you're past this step, but for completeness here is additional information on the installation process.

Web Connection is installed from a self-extracting EXE file that allows you to unzip the program files into a directory of your choice. The files extracted are the actual installation files so put these files into a permanent directory, not your temp directory.

Once unzipping is complete the program will automatically launch into the Web Connection Setup program. If this didn't occur automatically you can start the SETUP.EXE program from the Web Connection installation path. This program is required to configure Web Connection for your current Web server installation. Press F1 at any time for detailed instructions on the available options during the install process.

Understanding Directory Structures

Installation starts from the ZIP/EXE file you download. When that unzip program finishes you get a dialog that states that you should unzip the files into an application directory as you would with a full application installation. It gives you a default like c:\wconnect, which you can change, but should probably make it clear that the files shouldn't go into a temporary directory.

The files unzipped are the Web Connection framework files and your starting point for the installation. Web Connection requires two sets of directories:

These two paths should be completely separate - the Web path underneath the Web root and the application path whereever you choose to install it. Under no circumstances should these point to the same locations.

Other than first installation you don't need to know how this works. Use the defaults and all of this will be set up for you. If you change the defaults and you don't know what these values mean you can run into trouble, but then the issues are described for the help topics for the Setup Wizard.




The Setup program


Go to
Step 1


The setup program guides you through the first time configuration of Web Connection for your Web server. The setup process performs the following tasks automatically. For reference I also mention how you can configure these things manually.

These settings are the basic settings that must be performed for a Web Connection installation. The Setup program as well as the Web Connection Management Console New Project Wizard perform these task for you automatically, but it's important to understand what happens behind the scenes in case these settings do not work or are accidentally changed. If you or your administrator needs to know what happens with Microsoft Web servers or you want to manually configure settings check out the topic manually configuring Microsoft Web Server.

Step 1 - Picking the Web server


Next Step

The first step in configuring Web Connection is to pick the Web server to run on. This choice is very important as it will determine how to configure the server as well copying the appropriate files.

The server selections are straight forward. Web Connection should work fully with an ISAPI compatible Web Server and using file based operation with CGI based Web servers. It's highly recommended you use ISAPI for much improved performance. All Microsoft servers as well as Apache and Website are automatically configured - all other Web servers require manual configuration as outlined in the Setup Program step.


West Wind Web Connection works best Microsoft Internet Information Server 5 or later as well as Peer Web Server (also called Personal Web Server) on Windows NT Workstation. Other platforms and tools also work well, but the development of this product is focused on Internet Information Server and there may be features that don't fully work on other platforms. It's also highly recommended that you use Windows NT rather than Windows 98 to develop your application, both for stability of the dev environment as well as consistency between development and deployment environments.

If you're using IIS you can also select a Web site to install the Web Connection installation on. By default Web Connection uses the default Web site which should serve most situations. Advanced users or ISP's that have multiple sites can use the Advanced IIS Site Selection link to select a Web Site and domain to configure the site on:

The drop down will list all sites installed on the selected domain name or IP address.

Step 2 - Configuring the temp path


Next Step

When Web Connection runs in file based messaging mode it uses temporary files to pass messages between the Web server and the Visual FoxPro application server. These messages are picked up by the VFP server polling for the files in the temporary directory and it's important that both the ISAPI extension and the VFP server agree on this path. This option writes Path and TempFilePath values to wc.ini and wcDemo.ini (which is the demo application) respectively.

IMPORTANT

This directory must have FULL access for the SYSTEM and IUSR_ (on IIS) annonymous Web Server User Account. When a request comes in in file based messaging the Web server/wc.dll needs to create and write and read the temporary message files. This request runs under standard Web server security so this will typically be the IUSR_ account.



Step 3 - Configuring the virtual path


Next Step


This step configures a virtual directory where Web Connection installs its ISAPI extension and sample files. This serves as the home base directory for Web Connection. It's highly recommended that you stick with the default values.

Script File Path
You're asked to provide a physical disk location which can be anywhere. This should be a path that's part of the Web directory structure (ie. this is an HTML path not a Code path). The default will be the server's Web root plus wconnect and this is a consistent choice for this location.

Note
If you see 'Web Server not installed' in the script file path field, make sure that you have indeed selected the correct Web Server on the first page of the Wizard. If you are using IIS4 or 5 and you see this message, you can try explicitly selecting the Web site to install to on the first page by clicking on the Advanced tab and selecting the site.

Virtual Directory Name
This will be the virtual (logical) name for this directory in server relative terms. A virtual is similar to a drive mapping that logically describes an underlying path. It's highly recommended that you do not change this value from its wconnect default. If you do change this value the demos may not work since they use /wconnect as the server relative path to access the Web Connection DLL.

Step 4 - Finish and test the Server


Next Step


Ok, the final step of the Wizard asks you to click Finish to run the configuration process. Let 'er rip.

Important note for PWS on Windows 98
If you installed for Personal Web Server on a Windows 98 or 95 machine be sure to reboot your machine, since the registry settings made during install will not take until PWS is completely restarted. Rebooting is the only 'official' way to restart PWS, although you can also kill the process and then restart it.

Starting up the server

After the setup program is done it should pop up your new Web Connection server automatically as a standalone application running out of Visual FoxPro. It tries to run VFP in the current directory and simply runs the sample server immediately.

If you do this manually when you start up next time follow one of these steps:

Once you do this the server form pops up:

Click on the Status button to see the status information of the server that should display some of the information that you specified during the Wizard operation:

You can make additional configuration changes on this status form later on as well as retrieve information about individual requests.

Accessing the Web Connection Demos

The next step is to fire up your Web Browser and access the demo page. If you chose the default install options you should now be able to go to:

http://localhost/wconnect/default.htm

and access the Web Connection demo page. If this doesn't work please check the Troubleshooting section.

Scroll down to the first request Hello World with Web Connection.

If everything is working OK you should get a response page almost immediately that says Hello World from Visual FoxPro! followed by additional information about some of the server variables exposed by the server.

If this didn't work please see the setup troubleshooting topic.

Otherwise you're in business! Go ahead and browse around the demo page and check out some of the links. Note that most requests have a Show Code link on the bottom of the page that lets you quickly review the Visual FoxPro code that drives the request.

Back at the Web Connection Server

If you flip back to the Visual FoxPro instance that we started up at the beginning of this topic you'll find that there is now some request information in it like this:

00:07:33   wwDemo~ShowImage  - 0.022
00:18:26   wwdemo~TestPage~&Name=Rick&Company=West+Wind  - 0.040

Each request that hits the server shows in the main window along with the time that it took to run. At the same time the request gets logged into a log file where you can review and summarize the incoming requests. You can access this log file from the admin page at http://localhost/wconnect/admin.asp and the Show Web Connection Request Log request.

The admin page allows a number of administrative tasks from server monitoring to setting configuration options remotely over the Web to changing operational modes of the server. It's a good idea to shortcut this page in your Favorites list.



Step 5 - Checking out the code


While most of the demo page links contain the Show Code links that let you review the code for most requests, there's no substitute for getting your hands dirty and stepping through some code and maybe even modifying it a bit.

The Web Connection Server started with the DO WCDEMOMAIN command runs a VFP program. The demo application is also a PRG file that you can edit. So,

  1. Shut down the Web Connection Server by clicking on the Exit button.
  2. Open up wwDemo.prg with MODIFY COMMAND wwDemo.
  3. wwDemo.prg contains a large class called wwDemo with methods for each of the request links from the Web page.
  4. Find the ShowHours() method in the class (right click in the editor- Procedure|Function List and find wwDemo.ShowHours).
  5. At the very top add SET STEP ON into the code.
  6. Save the file.
  7. Restart the server with DO WCDEMOMAIN.

  8. Flip over the HTML Demo page
  9. Find the Simple Data Query Example and enter B to search for and a date range that's reasonably recent.
  10. Click on the Show Hours button

If you go back to Visual FoxPro now you should find the debugger popping up on the SET STEP code that we just inserted:

You can now happily step through the code line by and see the operation of each request as it happens. For example, you can now see the content of the request variables

lcOrigClient=Request.Form("Client")
lcDate1=Request.Form("StartDate")
lcDate2=Request.Form("EndDate")

being filled and the SQL WHERE clause being built and then rendering the code with ShowCursor().

As you just saw making a code change is very easy - you simply change the PRG file, save it and start up the server again. You don't need to recompile (although you could recompile the WCDEMO project everytime) and the changes take immediately.

Error Handling in Web Connection

Now go back into that code and take out the SET STEP ON. Instead put in some bogus code like:

o=CREATEOBJECT("NonExistingObject")

Guess what will happen here? The code will fail because this object doesn't exist and a VFP error occurs. VFP pops up an error dialog ontop of the editor with the code highlighted:

This is great for debugging - certainly beats typical COM debugging that doesn't allow you to debug a compiled object at runtime, right.

While this is nice for debugging if this occurs on your live Web site, this would be a problem though. I know you don't make coding mistakes ever <s>, but there are system circumstances or typos etc that do slip into production code. A stop error as we're seeing above would lock the Web Connection server dead in its tracks - it wouldn't be able to take any more requests until the dialog is closed.

In order to work around this Web Connection has a configuration setting that allows you to switch between debug and live modes. You can find this flag in WCONNECT.h:

#DEFINE DEBUGMODE      .T.

By default Web Connection ships with this flag set to .T. which makes errors basically stop on the offending line of code, since this is great for debugging. In order to switch this flag into deployment mode set the flag .F. and recompile your project. It's very important that you recompile the project completely with:

BUILD EXE <projname> FROM <projname> RECOMPILE

or by using the Recompile All option from the Build dialog.

Rerun the program now by running the EXE file:

DO WCDEMO

Now re-run the request. You'll find that Web Connection now handles the error and passes back an error page that describes the error on the Web page:

This is obviously much nicer than the server crashing and hanging situation we encountered before. What happens behind the scenes is that the Web Connection error methods kick in and generate this HTML output page. The page can be customized by overriding the wwProcess::ErrorMsg() method in your subclass of it. In fact you can also override the error behavior to perform other tasks on errors, but there's not a lot you can do but display some error info. Once the error page is generated the code returns back to the mainline and the error page is returned to the Web browser.

Note: The Shareware version is precompiled, so the #DEBUGMODE flag cannot be set and recompiled. The Shareware version will always stop on any VFP errors in your code.



Setup troubleshooting


The setup program will handle all configuration issues for all Microsoft Web servers as well as Apache. However, it's possible that due to particular system settings several options may not be available to get all of the settings installed resulting in the server not operating correctly.

This section describes several possible problem scenarios.

Troubleshooting a Filebased Server Install


When you install Web Connection it defaults into File based communication and a file based server requires that it can communicate with the Web Server via message files. There are a few things that can go wrong here either because a step was missed during setup or because a service is not running.

Is your Web Server working?
First of all make sure that you can indeed get to the Web Connection demo page by typing
http://localhost/wconnect/default.htm (or use the IP address or machine name instead of localhost). If this doesn't work your Web server is not functioning correctly. Check your Web server install docs to get the server working first.

Temp Directory Permission
If you get an error that states that you don't have permissions for your temp directory immediately, make sure that the temp path you've chose supports FULL access for the IUSR_ anonymous Web account. wc.dll requires this in order to write temporary message files when running in file based messaging.

Is the Web Connection Server running?
Next, make sure that the Web Connection server is running in a Visual FoxPro session. The demos are set up to run file based and they require that the Web Connection server is manually started and running in the background. You can do so by starting Web Connection from the desktop or start menu shortcut and using the Web Connection menu to click on Web Connection Demo Server. See Step 4 of the Setup program for details.

Is the ISAPI DLL firing?
If the server is running and still no requests are hitting the server try the following URL:

http://localhost/wconnect/wc.dll?_maintain~ShowStatus

If this URL is not working there's a fundamental problem on the Web Server in accessing the DLL. Follow this link to Manual Server Configuration for more details.

It's still not working - now what?

Ok, the above makes sure that the platform is properly configured. Let's make sure Web Connection is properly configured. To do this we want to make sure that the server's configuration file (YourApp.Ini or in the case of the demo WcDemo.Ini) and the Web Connection DLL INI file (wc.ini) are synched up properly.

Start by running your Web Connection Server with Do <yourAppMain.prg> and bring up the Status form as shown in the image below. Make sure that the startup path shown in the form matches your actual startup path - if for some reason it doesn't change it to the install directory and Save Settings.

Next bring up the Admin page for your the demo or your application by going to:

http://localhost/wconnect/admin.asp

(or go to your application's admin.asp page). Click on Show and Manage ISAPI settings. We can now compare settings between the Web Connection server and the wc.dll setting the Web Connection DLL is running.

Pay attention the Temp File path and Templates - they should match in both places. Case won't matter, but make sure they are otherwise the same. If they are not we need to sync them.

Changing the your server's settings
To make changes to the application's INI file simply change the value on the Status form and click the Save Server Settings button. When you exist the form these settings should be live. These settings are written into your application's ini file (<yourApp.ini or wcDemo.ini for the demo app).

[Main]
Tempfilepath=d:\temp\wc\
Template=WC_

Changing the wc.ini settings
To make changes to the Web Connection ISAPI INI file find wc.ini in your Web directory or the bin directory. Open the file with a text editor or the Visual FoxPro editor.

[wwcgi]
Path=d:\temp\wc\
Template=wc_

You'll want to change the Path and Template keys to match the value from the server.

Once this change has been made, go back to the ISAPI DLL Admin page and click on the Re-Read Configuration button, which loads the new settings. Verify that the settings are changed in the status display.



Authentication Dialog with NT and Windows 2000


If you see an authentication dialog pop up for non-protected Web Connection DLL or script links it most likely means one of the following:

The Web Connection Setup, New Project, New Process and Configuration Wizards all automatically apply these settings for IIS.


Request does not support this method... Error Page


This is a Web Connection generated error message and means that you're getting to Web Connection's code. This error means that you're not set up to handle a specific request parameter and most likely means you typed in the URL manually. What you're actually seeing is a Web Connection error handler that kicks in on invalid or non-existant requests.

These parameters are passed (by the demo at least as: wc.dll?Project~Method~Parameters). The Project name is added in the wcDemoServer :: Process() method in the CASE statement. The second parameter is handled by your processing code in the wwProcess :: Process() method and needs to correspond to a method in the class that handles requests. If the class does not exist you get the error.

To fix make sure you have set up an entry in the Server's Process CASE statement and you have a matching method in your custom Process() class. For more info, see the section on Your First Web Request.

Note to Apache Users:
If you switched to Apache from IIS make sure you add the following to your ServerMain.prg file's server class:
DEFINE CLASS wcDemoServer AS WWC_SERVER OLEPUBLIC cRequestClass = "wwApacheRequest" ...
Without this setting Apache returns invalid script map path information which the above class fixes.




Save As.. or Download dialog instead of request result


This typically occurs if the Web server is not configured correctly either because Setup.exe was not run, the wrong Web Server was selected or because simply went wrong during the installation process.

Double check the installation in the Web server's configuration. With IIS bring up the IIS Management Console and check that the /wconnect virtual directory was created. Make sure that the directory has Execute (Script and Execute) rights enabled.

Manually configuring Microsoft Web Servers


Web Connection includes a host of tools for configuring your Web Site they consist of:

The setup program and the various Project and Process Wizards should create all the Web site configuration settings for you automatically. But under certain circumstances it's possible that the new settings don't take. In particular you may find that virtual directory and script map settings didn't take.

For those of you that want to know or don't trust utilities here's how to do the dirty work by hand.

For manual installation on IIS 7 or later please see:
IIS 7 Configuration

For manual installation on IIS 6 and Windows 2003
Windows 2003 Configuration

Manually configuring IIS under Windows NT/2000/XP/Windows Server

Making the appropriate entries in IIS is fairly straightforward. You can use the IIS Management Console to perform these task via an easy to use GUI interface.

Note:
IIS is not automatically installed on client operating systems. To do so you have use the Windows Components installer and explicitly install IIS and IIS Management Console.

Creating a virtual directory

Creating Script Maps

Script maps by default are installed on the new virtual directory only, but you can optionally install them at the root of the server in which case they are visible throughout the entire Web site. Typically you'll want local script maps only, global scriptmaps can be useful if your application is site-wide or wants to run in various different virtual directories.


Make sure your temp directory has full rights for IUSR_
It's vital that you configure the directory that you choose for Web Connection temp file creation (set in wc.ini with PATH= and your app's INI file as TEMPFILEPATH=) has FULL rights configured for the IUSR_ account. If you're running NTFS use the NT Directory permissions to configure this. With FAT set up the TEMP path for sharing and make sure that IUSR_ or Everyone is included in the list.

Note: The temp path does not have to be your system TEMP path, although that's the logical place to put this file. I personally prefer to use a path such as d:\temp\wc so the directory is isolated and this is the installation default.

Using the wwWebServer Class to create configurations programmatically

Web Connection ships with a class library called WebServer.vcx which lets you programmatically configure the Web server. To do so you can use like the following from the VFP Command Window:

DO CONSOLE WITH "SPLASH"
o=CREATE("wwWebServer","IIS4")
o.CreateVirtual("wconnect","c:\inetput\wwwroot\wconnect\")
o.CreateScriptMap(".wc","c:\inetput\wwwroot\wconnect\wc.dll")

The following server types are supported:

IIS6 - IIS6 is used under Windows Server and sets up a Web Connection Application Pool
IIS4 - IIS4 and IIS5 and PWS 4 and 5 under Windows NT
IIS3 - IIS 3 and PWS 3 under Windows NT
PWS4 - Personal Web Server 4.0 Windows 98
PWS3 - Personal Web Server 1.0 and 3.0 Windows 95

Using the Server Configuration Wizard

You can also use the Management Console's Server Configuration Wizard to create Virtual directories and script maps.

These are the tools the Web Connection Setup uses internally to configure the server, so if Setup fails to install settings it's possible that the wwWebserver class and the Console will not do the trick and you have to follow the manual steps outlined above.



Windows 2003 Server Configuration


Web Connection intrinsicly supports Windows Server 2003 with special setup options for IIS 6. Make sure you select IIS 6 when creating applications and configuring Web sites as operation under COM requires certain additional settings to be set namely a custom Application Pool configuration. If you are creating applications with any of the Wizards and tools Web Connection provides, the appropriate configuration settings are made automatically when choosing IIS 6 and later.

I highly recommend you use the Server Configuration Wizard for these tasks as it is the best way to make sure all the necessary settings are made.

If you have an existing application and you want to configure it for IIS 6 and later you can:

Important Note:
Windows Server 2003 is locked down by default to not allow any external extensions or applications to run. Instead you get 404 file not found errors. If this is the case open the IIS Management console and either allow 'Allow all unknown ISAPI extensions' to allowed or add the Web Connection DLL:

Manually configuring Windows 2003 Server specific settings (Application Pools)

Web Connection actually works directly with Windows 2003 Server without any configuration options. However due to the new security environment that IIS 6 imposes there are a few features that will not work the same way as they do under previous versions of IIS. Specifically Web Connection requires SYSTEM level permissions to perform certain tasks (shutting down servers, hot swapping, accessing configuration etc.). IIS 6 by default doesn't run under SYSTEM permissions and instead uses a NETWORK SERVICE account which has far fewer rights.

Rather than changing the way IIS works by default the best choice for configuration is to use a custom Application Pool and configure it for our Web Connection applictions. An Application Pool is a new feature in IIS 6 that isolates a Web application or more than one into a separate process. These highly efficient processes (which work like Daemon services on Unix) are managed by the core IIS service and provide a number of advanced features such as metrics and recycling that are very useful for guaranteeing server uptime.

Each application domain is configured individually and there are many options health features. The fetaure that is most important for Web Connection is the Identity that the AppPool runs under. By default this is NETWORK SERVICE, but we need to change this to Local System. To do this:

  • Go to the IISAdmin console
  • Select Application Pools on the local computer
  • Create a new Application Pool and name it West Wind Web Connection (or whatever you choose)
  • Go to the Identity tab and set the account to Predefined - Local System.

    Next you go to or create your virtual directory for your your application and select the West Wind Web Connection Pool.

    Scriptmaps

    You create scriptmaps using the Configuration tab on the Virtual or Root Web's Home Directory or Directory Tabs. Click Configuration | Mappings Page then click on Add. Enter the full path to the Web Connection DLL and the extension name.

    Make sure that 'Verify that file exists' is not checked!

    More information about Windows 2003 Server

    IIS 6 improves operation of Web Connection considerably, doing away with some of the nagging problems with the IIS Management Console that have been a problem in IIS 5. Due to a brand new design of IIS 6 to run in a completely separate process (not through COM+), operation of Web Connection is actually more stable than it was previously. The MMC problems are gone, since WWWC no longer runs in the same process as IIS itself. Also shutdowns are more reliable and the new IIS 6 auto-cycling and recovery features work with no problem both in COM and file modes.

    IIS 6 uses a concept called application pools which is basically a worker process that handles each incoming request. IIS 6 uses Kernel mode HTTP drivers that talk directly to these processes improves performance of IIS considerably. Use of the separate worker processes in IIS 6 means that COM+ is no longer involved which has been the core problem for firing the Web Connection servers (DCOM and COM+ incompatibilities). It's also much easier to debug the ISAPI code now than was previously possible resulting in an easier development environment for the wc.dll ISPAPI extension moving forward.

    There are a number of new features in Windows 2003 that deal with making sure that the service or pool keeps on running. This is really useful for automatically cycling the Web Server process and a few other settings. The Application can be set up to automatically restart itself after x number of hits, after a certain time, or if a certain memory limit has been hit.

    The IIS Metabase in IIS 6 is an XML file - much easier to configure this way and easier to see what's actually available for configuration. ADSI still works as before although new features like the AppPools aren't documented at this time.



  • Windows Vista and Windows Server 2008 Configuration


    Web Connection 5.0 is ready for operation under Windows Vista and Windows Longhorn Server and when installed will automatically configure itself for operation under these OSs. There are also tools that provide configuration for new projects and existing installations.

    Windows Components required for IIS

    In order to get Web Connection to run the Web server on Vista needs to be explicitly installed and correctly configured as IIS on Vista is installed with very limited feature functionality.

    Note:
    It's crucially important that you have these components installed before you start installing Web Connection!

    Here are the required components for Web Connection:

    The figure above highlights the critical components that are used by Web Connection and are absolutely required. THere are a few additional checks in the above that will be useful in the future which is ASP.NET and .NET Extensibility.

    The most important settings are:

    The ISAPI extension support enables the Web Connection ISAPI extension to run - without this setting nothing will work. IIS Metabase support ensures that the COM based configuration of the Web Server can be performed through the Web Connection Management Console for the New Project and Server Configuration Wizards.

    I'd also recommend installing ASP.NET support as there will be more integration between ASP.NET and Web Connection in future versions.

    Automatic Configuration

    Once the base components have been installed you can use Web Connection's automatic configuration to install Web Connection, run the New Project Wizard to create new projects or run the Server Configuration Wizard to configure an existing site.

    In all cases you should use the IIS 7 server setting from the server type drop down.

    From there forward all the configuration options will work as any other of the Wizards for virtual directory configuration etc. This should be the easiest way to configure a Web application quickly as all of these mechanisms allow creationg of a virtual directory, configuring script maps and adding the Web Connection ISAPI extension into the allow application server application list.

    Programmatic Tools

    In addition to the fully automatic configuration tools you can also use programmatic tools to create a virtual and script maps. You can also use these programmatic tools:

    Create Virtuals:

    *** Using the Console - last parameter is the IIS Admin path under which virtual is created DO Console WITH "Virtual", "WebDemo","c:\westwind\webdemo",.F.,"IIS7","IIS://localhost/W3SVC/1/ROOT" *** Interactive DO Console WITH "Virtual","UI"

    Create Scriptmaps:

    *** Using the Console DO Console WITH "ScriptMap", ".wxx","c:\westwind\webdemo\bin\wc.dll",.F.,"IIS7","IIS://localhost/W3SVC/1/ROOT/WebDemo" *** Interactive DO Console WITH "ScriptMap", "UI"

    Note scriptmap creation will also register the ISAPI extension with the ISAPI restriction list.

    Manual Configuration

    If you think you don't want any stinking tools configure your application for you, or you really want to understand what settings are required to run your application, the following topics below take you through manual configuration for each of the configuration settings.

    Create an Application Pool

    Any virtual directory in IIS is hosted in what is known as an Application Pool. An Application pool is the host process for one or more Web applications (or virtual directories) and you can typically see this process running as w3wp.exe on your machine. There maybe multiple of these processes - one for each active Application Pool. Application Pools allow physical separation of one or more applications from another.

    Although a separate Web Connection Application Pool is not required it is recommended. The main reason is that Web Connection should be run in the System security context rather than the default Network Service security context and this setting is configured at the Application Pool level. This setting isn't required but if you don't run in System Context additional configuration may be required to ensure that Network Serivce (or whatever account you choose) has rights in several locations (Temp File directory, DCOM permissions for COM operation etc.).

    To create an Application Pool in IIS 7:

    Once you've created the Application Pool select it in the list and click on Advanced Settings. In the property sheet that appears set the Identity for the Application Pool to LocalSystem.

    You can also configure various other settings such as the process recycling, idle timeout and various other flags. Note that if you are running on a 64 Bit machine you should also set the Enable 32 Bit applications flag to True to enable the Web Connection ISAPI DLL which is a 32 bit application.

    Virtual Directory Configuration

    In order to run a Web Connection application you will need to create a virtual directory. The virtual can be configured in the IIS Servies Manager. To create a new virtual directory:

    Once the virtual has been created select the Authentication option in the virtual's configuration.

    Enable:

    Setting the ISAPI Restrictions for each copy of wc.dll

    Due to the security settings in IIS 6 and 7 generic ISAPI and CGI extensions (and ASP and ASP.NET even) are not allowed to execute. Without this setting any direct access or access through scriptmaps (MyPage.wcsx) to the dll will generate a 404.1 (File not found) error.

    The extension needs to be explicitly enabled:

    Changed behavior: BIN/wc.dll execution is not allowed

    IIS 7 does not allow direct URL access to any code in a BIN directory. For Web Connection this means that URLs that point at something like this:

    http://www.west-wind.com/wconnect/bin/wc.dll?wwDemo~TestPage

    are not allowed. This can be a problem for backwards compatibility if you use the bin path directly.

    There are relatively easy workarounds for this problem:

    Creating Script Mappings

    You'll also want to configure various script maps for your Web Connection application and you do this by creating mappings between certain extensions (like .wc, .wcsx, or .yourmap) to the ISAPI wc.dll extension. This allows you to serve pages like MyRequest.wc which automatically can route to a MyRequest process method in Web Connection.

    Script maps can be configured in the service manager as follows:

    Web.Config Configuration

    Note that all Virtual directory specific configuration settings are written to the web.config file in the Web application's Web root path by IIS, and you can also make setting changes in this configuration file.

    Here's what a Web.config for the WebDemo project looks like:

    <?xml version="1.0" encoding="UTF-8"?> <configuration> ... omitted <system.webServer> <handlers> <add name="WebDemo-wcsx" path="*.wcsx" verb="GET,POST" modules="IsapiModule" scriptProcessor="c:\westwind\webdemo\bin\wc.dll" resourceType="Unspecified" requireAccess="Script" responseBufferLimit="0" /> <add name="WebDemo-wp" path="*.wp" verb="GET,POST" modules="IsapiModule" scriptProcessor="c:\westwind\webdemo\bin\wc.dll" resourceType="Unspecified" requireAccess="Script" responseBufferLimit="0" /> <add name="WebDemo-wc" path="*.wc" verb="GET,POST" modules="IsapiModule" scriptProcessor="c:\westwind\webdemo\bin\wc.dll" resourceType="Unspecified" requireAccess="Script" responseBufferLimit="0" /> <add name="WebDemo-wwsoap" path="*.wwsoap" verb="GET,POST" modules="IsapiModule" scriptProcessor="c:\westwind\webdemo\bin\wc.dll" resourceType="Unspecified" requireAccess="Script" responseBufferLimit="0" /> </handlers> </system.webServer> </configuration>

    This can be especially useful for copying script map entries if you need to service a bunch of different script map extensions and it's much quicker to cut and paste these entries than adding them individually in the Service Manager.



    Running Web Connection on 64 Bit Windows


    When running in 64 bit versions of Windows with IIS there are special configuration issues to consider. Web Connection can run both 32 bit and 64 bit modes using two different approaches:

    Here's a quick review of the issues involved:

    Web Connection ISAPI DLL

    If you try to run the Web Connection ISAPI DLL on a 64 bit server without 32 bit compatibility mode enabled any request to the Web Connection DLL will fail with a 500 Server error and this error:

    %1 is not a valid Win32 application.

    (note that if you use IE default error reporting it will not actually show this error because the error message is too short - you'll only see the 500 Internal Error Page)

    When running IIS 7 you will get a server based exception and if debugging is enable you will get an Internal Server Error with an ExecuteHandlerRequestHandler error message which looks like a security violation.

    This indicates that the ISAPI extension is called from a 64 bit server instance. To fix this issue you need to do the following:

    Enable 32 Bit operation on Windows 2008

    IIS 7 supports the ability to isolate the 32 bit enabling flag on an Application Pool Level which means you can change the 32 bit flag specifically for your App Pool that Web Connection runs in - typically the West Wind Web Connection AppPool. Unfortunately, this flag setting on the AppPool is new in IIS 7 and is not supported through the IISAdmin tools, so for now this has to be set manually.

    The flag is set in the Application Pool Settings Manager with the Advanced Options:

    Enable 32 Bit operation on Windows 2003 Server/XP

    You can set the 64 bit option using the IIS Admin tools using the following code in VFP:

    DO CONSOLE WITH "ENABLE64BIT"

    to turn it off:

    DO CONSOLE WITH "ENABLE64BIT","OFF"

    You can also run CONSOLE.EXE from the Windows Command prompt:

    CONSOLE.EXE ENABLE64BIT
    CONSOLE.EXE ENABLE64BIT OFF

    Reconfigure ASP.NET for the proper 32 or 64 bit version
    In addition you may have to fix ASP.NET if it is installed on the server. ASP.NET 2.0 installs an ISAPI filter and that filter needs to be tied to either the 32 bit .NET runtime or the 64 bit version. If the wrong filter is installed you will get a Service is Unavailable error as the Application Pool crashes basically on any request and shuts down.

    To set up ASP.NET for 32 bit:

    C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727>aspnet_iisreg -i

    To set up ASP.NET for 64 bit:

    C:\WINDOWS\Microsoft.NET\Framework64\v2.0.50727>aspnet_iisreg -i

    Note that this ASP.NET configuration is REQUIRED even if you don't use ASP.NET, but if it is enabled. To check for this go to:

    IIS Service Manager | Web Sites | ISAPI Filters

    In the list you should see the ASP.NET ISAPI filter if it's active. If it is there you will need to run the above command line appropriate for this version. Or if you don't use ASP.NET at all you can just remove the filter.

    The Web Connection .NET Managed Module can run in 64 bit Mode

    Web Connection 5.20 and later also ships with a .NET HttpHandler module that is capable of running in 64 bit mode natively without requiring any changes to the Web Server. The module supports operation both in file and COM modes.

    The module works with ASP.NET 2.0 on IIS 6 or as a native HttpHandler in IIS 7.0 and it works without any configuration changes both in 32 and 64 bit mode.

    For more information please check out Using the Web Connection Managed Module.




    Apache for Windows Manual Configuration


    Web Connection supports Apache 2.0 and 2.2 (and later) in full featured ISAPI mode. Prior versions are supported only with the somewhat limited CGI mode and this is not recommended. You'll want to use the ISAPI mode for best performance and the ability to run Web Connection in COM mode.

    Web Connection provides a custom Apache ISAPI module (mod_webconnection_isapi.so and mod_webconnection_isapi_20.so) that can be found in the \scripts directory of your Web Connection installation. There are two versions: The default 2.2 compatible version and a 2.0 specific version (mod_webconnection_isapi_20.so). The Web Connection console will pick the right module based on the version of Apache installed. If you manually install make sure you pick the correct version of these module files for copying into the Apache Modules directory.

    This custom Apache provides the following:

    Please note that if you plan on using Apache, you should be familiar with Apache Configuration and security. It is your responsibility to lock down Apache server and your application and provide common customization features. The Web Connection installer only provides basic hook ups for Apache operation of Web Connection.

    Automatic Configuration

    The process of configuration is integrated into Web Connection's Configuration utilities so if you Create a new Project or use the Configure your Server Console applications they automatically configure your Apache server for the application. To do this simply choose Apache as your Web Server on the first page of each of the Wizards.

    The Wizards will handle the following:


    All of this automatic and we recommend that you take advantage of this functionality using the Console application both for first time configuration (New Project Wizard) and for server configuration (Configure your Server Wizard).

    Manual Configuration

    Manual configuration of Apache is accomplished by:

    Copying the Web Connection Apache Module
    The Web Connection module is found in the /scripts directory of the Web Connection installation. You should always copy this directory with your application to the server so the scripts directory is available to copy the files from within it to the server. To copy the file:

    COPY FILE "<webconectionInstallFolder>\scripts\mod_webconnection_isapi.so" TO; "<ApacheRoot>\modules\mod_webconnection_isapi.so"

    If you're using a version of Apache prior to 2.2 use:

    COPY FILE "<webconectionInstallFolder>\scripts\mod_webconnection_isapi_20.so" TO; "<ApacheRoot>\modules\mod_webconnection_isapi.so"

    MSVCR7.DLL Required
    Apache doesn't ship with recent Microsoft CRT runtimes, so you have to copy MSVCR7.DLL from the Web Connection install root, into your Windows directory if it's not already there (servers often won't have it). Automatic configuration copies this file automatically if not present, manual configuration requires you to do it.

    Create an Apache virtual directory
    Typically virtual directories are created underneath the Apache\htdocs directory. This Web path will hold all of your Web related files - HTML files, images, script pages etc.

    Into this directory you should copy the standard Web Connection related project files which are stored under <wcInstall>\HTML which is the Web template for the project.

    Copying the Web Connection DLL
    The most important file for the Web installation is the Web Connection ISAPI DLL (wc.dll). If you copied from a template this file will already be there otherwise you can find it here:

    COPY FILE TO "<webconnectionInstallFolder\scripts\wc.*" TO ; "<WebDirectory>\BIN\wc.*"

    This copies both wc.dll and wc.ini configuration file. wc.ini holds Web Connection configuration information in an INI file format and you'll need to confgure this file to match the settings in your server file. Specifically make sure you set the path (which is the temp file path for file based messaging) and the Template which needs to match the settings configured in your Web Connection server (<yourapp>.ini)

    ;*** THIS DIRECTORY MUST HAVE READ AND WRITE ACCESS FOR THE
    Path=d:\temp\wcapache\
    
    ;*** Message File Template (1st 3 letters)
    ;*** Default is "wc_"          Only needed if using a different template
    Template=WC_
    

    Modifying httpd.conf
    Apache configuration is handled through the httpd.conf file which lives in the /conf folder of the Apache Configuration. Web Connection adds a few entries to this file. There are a couple of global settings and a host of settings specific to each virtual directory/application that you configure.

    At the bottom of the file add the following:

    #*** WEB CONNECTION MODULE CONFIGURATION
    LoadModule webconnection_isapi_module modules/mod_webconnection_isapi.so
    #*** END WEB CONNECTION MODULE CONFIGURATION
    
    #*** WEB CONNECTION VIRTUAL - wconnect
    
    #*** WEB CONNECTION SCRIPT ALIAS
    ScriptAliasMatch (?i)^/wconnect/.*\.(wc|wcs|wcsx|wwsoap|wwd|blog|pho|wwr)$ "C:\Program Files\Apache2.0\Apache2\htdocs\wconnect\wc.dll"
    #*** END WEB CONNECTION SCRIPT ALIAS
    
    Alias /wconnect/ "C:/Program Files/Apache2.0/Apache2/htdocs/wconnect/"
    
    <directory "C:/Program Files/Apache2.0/Apache2/htdocs/wconnect/">
    DirectoryIndex default.htm
    Options ExecCGI
    # AddHandler isapi-handler dll
    AddHandler webconnection-isapi-handler dll
    #*** WEB CONNECTION VIRTUAL SCRIPT MAPS
    AddType application/webconnection-scriptmap .wc .wcs .wcsx .wwsoap .wwd .blog .pho .wwr
    Action application/webconnection-scriptmap "/wconnect/wc.dll"
    #*** END WEB CONNECTION VIRTUAL SCRIPT MAPS
    </directory>
    #*** END WEB CONNECTION VIRTUAL - wconnect
    
    
    #*** WEB CONNECTION VIRTUAL - wwthreads
    
    #*** WEB CONNECTION SCRIPT ALIAS
    ScriptAliasMatch (?i)^/wwthreads/.*\.(wwt|wc)$ "C:\Program Files\Apache2.0\Apache2\htdocs\wconnect\wc.dll"
    #*** END WEB CONNECTION SCRIPT ALIAS
    
    Alias /wwthreads/ "C:/Program Files/Apache2.0/Apache2/htdocs/wwthreads/"
    
    <directory "C:/Program Files/Apache2.0/Apache2/htdocs/wwthreads/">
    DirectoryIndex default.htm
    Options ExecCGI
    # AddHandler isapi-handler dll
    AddHandler webconnection-isapi-handler dll
    #*** WEB CONNECTION VIRTUAL SCRIPT MAPS
    AddType application/webconnection-scriptmap .wwt .wc
    Action application/webconnection-scriptmap "/wwthreads/wc.dll"
    #*** END WEB CONNECTION VIRTUAL SCRIPT MAPS
    </directory>
    #*** END WEB CONNECTION VIRTUAL - wwthreads
    

    The comment lines (#) are optional but recommended as they are placeholders for the Web Connection configuration routines that allow you to update the settings using the Wizards or programmatic tools to configure the server. If you add the comments for auto-configuration make sure the comment lines are EXACTLY as above (ie. cut and paste!).

    The key features are:

    LoadModule loads the Web Connection Apache module. This is a global setting and should only be specified once in the file.

    ScriptAliasMatch is used to deal with script mapping. It routes any custom extension you'd like to configure to the Web Connection ISAPI DLL. NOTE: it's crucial that this directive is declared before the Alias for the virtual is defined - otherwise you will get File Not Found errors on any requests that access non-file backed 'scripts'.

    Alias creates a Virtual directory. It maps a logical/virtual path to a physical path.

    The <directory> tag contains configuration settings for the specific directory on disk. It specifies the default document(s), and that this directory supports dynamic script operation (ExecCGI). It also sets up the Web Connection ISAPI DLL as a script handler for any files that map to the extensions specified in the AddType command. Action then maps the type specified to the Web Connection ISAPI DLL as the script handler. These script handlers are actually not required if you use a ScriptAliasMatch as above, but they are left in here in case ScriptAliasMatch is missing or misconfigured - AddType will catch any scriptmapped requests that have a backing file.


    Programmatically setting Configuration Settings

    In addition to manual configuration you can also programmatically configure Apache. You can use the wwWebServer Class to perform these tasks:

    DO WCONNECT SET CLASSLIB TO WebServer ADDITIVE oWeb = CREATEOBJECT("wwWebServer") oWeb.cServerType = "APACHE" *** Create a virtual directory oWeb.CreateVirtual("WebDemo","d:\westwind\webdemo") oWeb.CreateScriptMap(".wp","d:\westwind\webdemo\bin\wc.dll","WebDemo") oWeb.CreateScriptMap(".wpp","d:\westwind\webdemo\bin\wc.dll","WebDemo") *** Edit the .Config file lcConfigFile = oWeb.GetWebRoot() + "conf\httpd.conf" GoUrl(lcConfigFile) && Opens in Notepad for editing

    Using wwApacheRequest instead of wwRequest

    Let's move on to the Web Connection FoxPro configuration of the server. Again this step is automatically performed for you when you create your project with the New Project wizard and choose Apache as your Web server.

    Apache returns some server variables slightly differently than IIS does and so a special subclass of the wwRequest class called wwApacheRequest is used to handle these differences. IIS returns some platform specific server variables that are not returned by Apache - but most of these are truly platform specific and rarely used so this should not be a problem.

    The wwApacheRequest class handles path fixup for requests by explicitly checking paths and trying to automatically fix up the physical path to match the true physical path of a given script.

    To use the wwApacheRequest class you can simply assign the cRequestClass in the your main wwServer class which is contained in <yourApp>Main.prg. The following code demonstrates:

    DEFINE CLASS WebdemoServer AS WWC_SERVER OLEPUBLIC cRequestClass='wwApacheRequest' ...

    Alternately you can also override this globally using the following flag in your WCONNECT_OVERRIDE.H file:

    #UNDEFINE WWC_REQUEST
    #DEFINE WWC_REQUEST wwApacheRequest

    The former is a little less intrusive and isolates the change to the current application, while the latter can be easier if you need to do this with multiple applications as the flag is global to all Web Connection applications.

    Set the CallCoInitialize flag in wc.ini
    Starting with Version 4.50 Web Connetion no longer calls CoInitialize by default on COM requests by default. This fixed a number of issues with IIS as IIS had some issues with multiple calls to CoInitialize and potential mismatched CoUninitialize calls. IIS automatically calls CoInitialize for each thread, so there's no need to do it again.

    Apache however does not do this, so we need to tell the Web Connection ISAPI dll to explicitly to call CoInitialize/CoUninitialize. To do so we set the CallCoInitialize flag in the wc.ini file:

    [Automation Servers]
    ;*** Severloading - 0 - Normal  1 - Round Robin
    ServerLoading=1
    
    ;*** KeepAlive  0 - Normal  1 - Force extra COM reference to keep alive
    KeepAlive=1
    
    Server1=webDemo.webDemoServer
    ;Server2=webDemo.webDemoServer
    
    ;*** Determines whether CoInitialize for COM objects
    ;*** Set this option to 1 only if your servers do not
    ;*** load and given an error message to the effect 
    ;*** that COM is not initialized. Should only be needed
    ;*** on ancient or non-Microsoft Web Servers.
    CallCoInitialize=1
    

    Your wc.ini file is found in the same directory as wc.dll which will be your Web virtual directory usually in the BIN path.

    Configuring Apache Security for Web Connection

    Apache does not support application driven Basic Authentication so Web Connection's default mechanism for accessing admin links and the like won't work. Your only choice to work around this limitation is to configure Apache for directory security and create script files for admin operations in a separate, protected directory.

    To work around these issues and provide the basic security for locking down administration functions in the Web Connection DLL, the Web Connection Apache ISAPI Module provides Windows based Basic Authentication. When using the custom module, programmatic Basic Authentication works just like it does in IIS by authenticating against Windows User accounts rather than using Apache's password files.

    This lets you set security in the wc.ini configuration file:

    ;*** Account for Admin tasks    REQUIRED FOR ADMIN TASKS
    ;***       NT User Account   -  The specified user must log in
    ;***       Any               -  Any logged in user
    ;***                         -  Blank - no Authentication
    AdminAccount=Any
    

    Apache Security Note
    Web Connection will override security with the custom behavior only if Apache has not provided its own security implementation. If you lock down directories and Apache can authenticate users prior to hitting the Web Connection module, the Apache authentication takes precendence. To avoid confusion in this scenario you should avoid using Basic Authentication outside of programmatic use within your Web Connection applications. Instead choose Digest or other custom security scheme for directory based rights in Apache - this will avoid any conflicts.

    Check with your Apache Administrator for more information on setting up Web Server authentication for requests.


    Netscape and other CGI Servers


    Starting with version 2.80 Web Connection also supports Netscape Web servers or any other Web Server that can operate via CGI. CGI configuration by the Setup program is only partial with only the files being copied, but no configuration settings made on the Web server itself. The following steps describe Netscape installation, but they apply equally for other CGI servers.



    Using the Web Connection Managed Module


    Starting with Web Connection 5.25 Web Connection ships a .NET managed handler/module that completely duplicates - and enhances - the ISAPI DLL that has been Web Connection's main stay. The module provides a much cleaner design for the connector interface and much easier development which in turn allows us to provide more advanced features in the module. The new module also turns out to be more efficient than the ISAPI module in COM operation.

    Here is a list of advantages of using the module:

    There's also one downside to using the module which relates to the way that ASP.NET and IIS 7 separate IIS applications:

    Servers are tied to a single Web IIS Application

    The latter issue shouldn't be a problem for new development, but it can be an issue for existing applications that share multiple virtual directories for a single set of servers only in COM mode. For COM based servers the COM objects are tied specifically to an Application/Virtual directory. This is due to ASP.NET's application boundary which is tied to a virtual directory/IIS Application.

    This means if you have two separate applications that both use the same COM server they will create two sets of servers rather than a single set as the Web Connection ISAPI DLL did as long as script maps pointed to the same DLL.

    There's a workaround for this scenario: You can create a hierarchy of directories that are based on a physical disk layout and so don't need to rely virtual directories for separation. So if the root is configured for the module any non-virtual directories below it share that ASP.NET AppDomain. It requires some forethought and organization which is easy to do knowing this issue exists. However, it can be potentially difficult to achieve if you already have existing shared applications in place.

    IIS 7 and the Web Connection Managed Module


    Setting up IIS 7 for using with the managed module is easy by using the Integrated pipeline. With IIS 7 this model permits true XCOPY deployment with no additional configuration needs - you can simply copy the WebConnectionModule.dll in your BIN directory as part of your Web directory and make a couple of changes in web.config to enable to the module.

    Here's how to set up a new project and switch to the Managed Module:

    By default the web.config file that installs with the new project has the module hook ups commented out which in turn results in the ISAPI DLL being used. To switch to using the module open web.config and edit the following section:

    <?xml version="1.0"?> <configuration> <system.webServer> <handlers accessPolicy="Script, Execute, Read"> <!-- IIS 7 in Integrated Mode --> <add name="*.wp_wconnect" path="*.wp" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule" preCondition="integratedMode,runtimeVersionv2.0" /> <add name="*.wc_wconnect" path="*.wc" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule" preCondition="integratedMode,runtimeVersionv2.0" /> <add name="*.wcs_wconnect" path="*.wcs" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule" preCondition="integratedMode,runtimeVersionv2.0" /> <add name="*.wcsx_wconnect" path="*.wcsx" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule" preCondition="integratedMode,runtimeVersionv2.0" /> <add name="*.wwsoap_wconnect" path="*.wwsoap" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule" preCondition="integratedMode,runtimeVersionv2.0" /> <add name="*.blog_wconnect" path="*.blog" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule" preCondition="integratedMode,runtimeVersionv2.0" /> <add name="*.wwd_wconnect" path="*.wwd" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule" preCondition="integratedMode,runtimeVersionv2.0" /> <!-- end IIS 7 in Integrated Mode --> </handlers> </system.webServer> </configuration>

    Note there are additional entries in web.config that also need to be there, but these items will be already set. The above section nees to be uncommented in the XML (just remove the <!-- before the <add and after the last />).

    This section basically maps each script map to the managed module. This setting overrides any ISAPI handler mappings, so you don't need to do anything else. If you want to switch back to the ISAPI handler, simply uncomment these mappings.




    IIS 6/5 and the Web Connection Managed Module


    Setting up the managed module for IIS 5 or 6 requires a bit of work at the moment as the process is not yet automated. The process basically involves disconnecting any of the wc.dll script maps and re-attaching them to the ASP.NET ISAPI extension instead so that requests run through ASP.NET. In addition a couple of changes in the web.config file are required by uncommenting some entries

    You will also need to ensure that the .NET 2.0 runtime is installed.

    Here's how to set up a new project and switch to the Managed Module:

    This basically maps the Web Connection extensions to ASP.NET in your virtual directory, so if the script map is used it will fire through ASP.NET 2.0. Next we'll need to hook up the Web Connection Connector Handler by modifying a setting in web.config (in your Web directory's root).

    <?xml version="1.0"?> <configuration> <system.web> <httpHandlers> <!-- pre IIS 7 or IIS 7 in non-integrated mode NOTE: you still need to set up scriptmaps to c:\windows\Microsoft .NET\v2.0.50727\aspnet_isapi.dll --> <add verb="*" path="*.wp" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule"/> <add verb="*" path="*.wc" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule"/> <add verb="*" path="*.wcsx" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule"/> <add verb="*" path="*.wcs" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule"/> <add verb="*" path="*.wwsoap" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule"/> <!-- End pre IIS 7 --> </httpHandlers> </system.web> </configuration>



    Visual Studio Web Server, Cassini and the Managed Module


    The Web Connection Module can be used in combination with the Visual Studio Web Server as well as with a customized and redistributable version of the Cassini Web server that is essentially the same code base. Use of these servers requires that .NET 2.0 is installed and requires use of the Web Connection Managed Module instead of the ISAPI extension. By using these local only servers you can develop without requiring a full Web Server installation like IIS or Apache.

    Bundling the Cassini Web Server
    Microsoft also provides a standalone version of the Visual Studio Web Server in the form of a source code distributed
    Cassini server that is essentially the same server (though unsupported). We have bundled a customized version of this server with Web Connection and you can start this server from the Web Connection menu in FoxPro or from the VisualStudio/InternalServer directory. To register the server you have to run GAC.BAT to register the required Cassini component into the registry.

    The VS Web Server can also be run without Visual Studio actually running optionally.

    Automatic Installation and running in Visual Studio

    The Visual Studio Web Server can be used with the Setup and New Project Wizards. Because the server is standalone it will choose a fixed path (c:\websites\<yourVirtual>) by default for the Web path, but you can put the Web directory anywhere you choose. Since it's only file based the only configuration made is configuring the scriptmap you specified.

    Once set you can simply open the project in Visual Studio and press Run to execute the Web application (or View In Browser on the Default.htm page) or press Ctrl-F5 to start the Visual Studio Web server on your Web project. You will likely get a compilation error due to the fact that you have Visual FoxPro code in your markup pages which won't compile - ignore the error and set the checkbox to to not show the dialog next time.

    From thereon in everything should just work. Note if you Run the Web application you will get many errors potentially - because these projects aren't really ASP.NET code this is normal and you should simply ignore the errors and choose Yes to run anyway to start the site.

    The server stays running in the task tray until you explicitly shut it down or close Visual Studio. You can see the port number when hovering over it or clicking on the icon which brings up a small configuration form.

    Please note that Web Connection's Show in Web Browser will not work unless you modify the AppSettings key in web.config:

    <appSettings> <add key="FoxProjectBasePath" value="c:\wwapps\wc3\"/> <add key="WebProjectBasePath" value="c:\westwind\wconnect\webcontrols\"/> <add key="WebProjectVirtual" value="http://localhost:63841/wconnect/webcontrols/"/> <add key="WebBrowser" value=""/> </appSettings>

    But keep in mind that you'll have to change the port each time you restart the VS Web Server. This will be addressed in Visual Studio 2008 which includes an option to set a fixed port for the Web server.

    Manually starting the Web Connection Cassini Server

    Possibly a better way to start a standalone server is to start it explicitly via the RUN command or a batch file that you launch. If .NET 2.0 is installed you can use the following code from within Visual FoxPro:

    DO CONSOLE with "LAUNCHCASSINI","c:\websites\webdemo","81","/WebDemo"

    or if you want to manually set these settings:

    DO CONSOLE with "LAUNCHCASSINI"

    which brings up the server form:

    You can click on the link to bring up a browser at this location. You can minimize the appication to the task tray so the Web Server window stays out of your way.

    A Url to the server will be:

    http://localhost:81/WebDemo/Default.htm

    and you should specify whatever path you choose in your web.config configuration for the WebProjectVirtual:

    < add key="WebProjectVirtual" value="http://localhost:81/wconnect/webcontrols/"/>

    I recommend you create a small PRG file - maybe called LaunchWebServer.prg - and add:

    DO CONSOLE with "LAUNCHCASSINI","c:\websites\webdemo","81","/WebDemo"

    to start your server quickly and easily.

    Manual Setup and Configuration

    Automatic configuration is straight forward, but if you already have an app running and want to start using the VS Web Server

    Configure the Start Page
    The settings in the figure above should be the default except for the default.htm homepage but you can specify any page that you like. The start page can be invoked if you run the site or press Ctlr-F5.

    Configure web.config to use the Managed Module
    Finally we need to tell the VS Web Server that we want our application extensions to be mapped to the Web Connection module. Add the following to web.config in the project:

    <?xml version="1.0"?> <configuration> <configSections> <system.web> <compilation defaultLanguage="C#" debug="false"> <buildProviders> <add extension=".wcsx" type="System.Web.Compilation.PageBuildProvider"/> <add extension=".wcsctl" type="System.Web.Compilation.PageBuildProvider"/> </buildProviders> </compilation> <httpHandlers> <add path="*.wcsx" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule"/> <add path="*.tt" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule"/> <add path="*.wc" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule"/> <add path="*.wwsoap" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule"/> </httpHandlers> </system.web> </configuration>

    Both the build providers and the HttpHandlers are required to make this work. Add any script maps you might require in the httpHandlers section. Note that using either Cassini or the Visual Studio Server requires no further configuration as the server runs ALL content through its ASP.NET pipeline. So unlike IIS configuration which requires addition script mapping for ASP.NET the above is all that's needed.

    Differences with the Visual Studio Web Server

    The Visual Studio Web Server works well enough but it's a fairly limited server. It only knows how to run static content and ASP.NET content. So it can't for example run the Web Connection ISAPI DLL, which is why the managed module is used instead. The server also doesn't support basic authentication which is a problem for testing authentication in Web Connection because the admin permissions are using basic authentication.

    To work around this make sure you set the AdminAccount key in web.config to blank ("") so no authentication is applied.

    Web Connection Managed Module Configuration


    The Web Connection Managed Module is configured through settings in the Web.Config file in the root directory of your Web application.

    A full Web.Config looks something like this:

    <?xml version="1.0"?> <configuration> <configSections> <section name="webConnectionConfiguration" type="System.Configuration.NameValueSectionHandler,System,Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> <section name="webConnectionErrorPages" type="System.Configuration.NameValueSectionHandler,System,Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> </configSections> <appSettings> <add key="FoxProjectBasePath" value="c:\wwapps\wc3\" /> <add key="WebProjectBasePath" value="C:\westwind\WebDemo\" /> <add key="WebProjectVirtual" value="http://localhost/WebDemo" /> <!-- The efault browser used. Blank IE Automation, otherwise specify browser path --> <add key="WebBrowser" value="" /> <add key="xWebBrowser" value="c:\program files\firefox\firefox.exe" /> </appSettings> <system.web> <compilation defaultLanguage="c#" debug="false"> <buildProviders> <add extension=".wcsx" type="System.Web.Compilation.PageBuildProvider" /> <add extension=".wp" type="System.Web.Compilation.PageBuildProvider" /> </buildProviders> </compilation> <trust level="Full" /> <httpHandlers> <!-- pre IIS 7 or IIS 7 in non-integrated mode NOTE: you still need to set up scriptmaps to c:\windows\Microsoft .NET\v2.0.50727\aspnet_isapi.dll --> <!--<add verb="*" path="*.wp" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule"/> <add verb="*" path="*.wc" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule"/> <add verb="*" path="*.wcsx" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule"/> <add verb="*" path="*.wcs" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule"/> <add verb="*" path="*.wwsoap" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule"/>--> <!-- End pre IIS 7 --> </httpHandlers> </system.web> <system.webServer> <handlers accessPolicy="Script, Execute, Read"> <!-- IIS 7 in Integrated Mode --> <add name="*.wp_wconnect" path="*.wp" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule" preCondition="integratedMode,runtimeVersionv2.0" /> <add name="*.wc_wconnect" path="*.wc" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule" preCondition="integratedMode,runtimeVersionv2.0" /> <add name="*.wcsx_wconnect" path="*.wcsx" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule" preCondition="integratedMode,runtimeVersionv2.0" /> <add name="*.wcs_wconnect" path="*.wcs" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule" preCondition="integratedMode,runtimeVersionv2.0" /> <add name="*.wwsoap_wconnect" path="*.wwsoap" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule" preCondition="integratedMode,runtimeVersionv2.0" /> <!-- end IIS 7 in Integrated Mode --> <!-- IIS 7 non-integrated mode ScriptMappings --> <!--<add name="wp_ISAPI" path="*.wp" verb="*" modules="IsapiModule" scriptProcessor="C:\Windows\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" resourceType="Unspecified" /> <add name="wc_ISAPI" path="*.wc" verb="*" modules="IsapiModule" scriptProcessor="C:\Windows\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" resourceType="Unspecified" /> <add name="wcsx_ISAPI" path="*.wcsx" verb="*" modules="IsapiModule" scriptProcessor="C:\Windows\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" resourceType="Unspecified" /> <add name="blog_ISAPI" path="*.blog" verb="*" modules="IsapiModule" scriptProcessor="C:\Windows\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" resourceType="Unspecified" /> <add name="wwd_ISAPI" path="*.wwd" verb="*" modules="IsapiModule" scriptProcessor="C:\Windows\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" resourceType="Unspecified" />--> <!-- end IIS 7 non-integrated mode ScriptMappings --> </handlers> </system.webServer> <webConnectionConfiguration> <!-- NOTE: These settings apply only to the Web Connection Managed Module! --> <add key="Timeout" value="60" /> <add key="PollTimeout" value="100" /> <add key="InputPostBufferSize" value="65356" /> <add key="PostBufferLimit" value="0" /> <add key="TempPath" value="c:\temp\wc\" /> <add key="TempFilePrefix" value="WC_" /> <add key="MessagingMechanism" value="File" /> <add key="AdminAccount" value="ANY" /> <add key="AdminPage" value="~/admin/admin.asp" /> <add key="ExeFile" value="c:\wwapps\wc3\WebDemo.exe" /> <add key="UpdateFile" value="" /> <add key="LogDetail" value="False" /> <add key="ValidateRequest" value="False" /> <add key="ComServerProgId" value="WebDemo.WebDemoServer" /> <add key="ComServerLoadingMode" value="LoadBased" /> <add key="ServerCount" value="2" /> <add key="AutoStartServers" value="False" /> <add key="MessageDisplayFooter" value="<small>Error generated by Web Connection IIS Connector Module</small>" /> </webConnectionConfiguration> <webConnectionErrorPages> <!-- NOTE: These settings apply only to the Web Connection Managed Module! --> <add key="Exception" value="" /> <add key="OleError" value="" /> <add key="Timeout" value="" /> <add key="NoOutput" value="" /> <add key="Busy" value="" /> <add key="Maintenance" value="" /> <add key="InvalidRequestId" value="" /> <add key="TranmitFileFailure" value="" /> <add key="PostBufferSize" value="" /> </webConnectionErrorPages> </configuration>

    configSections

    Required to let ASP.NET know that there's a couple of custom configuration sections. These two sections contain the Web Connection Configuration settings and a set of error page templates.

    AppSettings

    These settings are actually not used by the module but rather by the Web Connection Add-in and they hold several settings that determine how the addin operates and where it can find and store files.

    Compilation

    The compilation setting sets the language to C# which is important only for the editor. Typically you'll want to set debug to false as well as it provides optimal performance for the ASP.NET application.

    BuildProvider

    These settings are not used by the module by but by Visual Studio which allows you to edit the pages visually.

    Trust Level

    The Web Connection Module performs a number of high trust operations like writing files to non-Web paths, calling COM objects, starting and stopping processes and so requires Full Trust. This is not an optional setting. Along the same lines the module requires that the app runs under LocalSystem or an Admin account.

    Handler Mappings

    The Handler mappings map a script map to the Web Connection module. The set up varies depending on whether IIS 7 in integrated mode or previous versions are used. You can review the
    specific version topics.

    webConnectionConfiguration

    This section contains Web Connection's main configuration settings that determine how this application operates. See webConnectionConfiguration Settings.

    webConnectionConfiguration Settings


    Main configuration class for the Web Connection Module that holds operational settings.

    These settings are persisted in the WebConnectionConfiguration section of the Web.config file and be set there.

    <webConnectionConfiguration> <!-- NOTE: These settings apply only to the Web Connection Managed Module! --> <add key="Timeout" value="60" /> <add key="PostBufferLimit" value="0" /> <add key="TempPath" value="c:\temp\wc\" /> <add key="TempFilePrefix" value="WC_" /> <add key="MessagingMechanism" value="File" /> <add key="AdminAccount" value="ANY" /> <add key="AdminPage" value="~/admin/admin.asp" /> <add key="ExeFile" value="c:\wwapps\wc3\WebDemo.exe" /> <add key="UpdateFile" value="c:\temp\updates\WebDemo.exe" /> <add key="LogDetail" value="False" /> <add key="ValidateRequest" value="False" /> <add key="ComServerProgId" value="WebDemo.WebDemoServer" /> <add key="ComServerLoadingMode" value="LoadBased" /> <add key="ServerCount" value="2" /> <add key="AutoStartServers" value="False" /> <add key="MessageDisplayFooter" value="<small>Error generated by Web Connection IIS Connector Module</small>" /> </webConnectionConfiguration>


    System.Object
      Westwind.Tools.wwAppConfiguration
        Westwind.WebConnection.AppConfiguration

    public class AppConfiguration : wwAppConfiguration
    

    Class Members

    MemberDescription
    AdminAccount The account that is allowed access to the administration functions when authenticated.
    AdminPage The adminstration page in the application. Allows use of ~ for the application path.
    AutoStartServers Determines whether servers are automatically started when the first hit comes into the module. Useful primarily for file based operation which starts up the EXE server
    ComServerLoadingMode Deterimines how servers are processing requests. Round Robin simply goes through each of the servers one after the other while LoadBased always starts with the first server.
    ComServerProgId The ProgId of the COM server to be loaded
    ExeFile Name of the EXE file for the server application.
    LogDetail Determines whether request data is logged in detail.
    MessageDisplayFooter A default footer message displayed on the bottom of the Module's generic messages
    MessagingMechanism Determines how messaging works in Web Connection
    PostBufferLimit Max size of the POST buffer - if bigger request is aborted
    ServerCount Determines how many Com instance of the server are loaded in Com messaging mode.
    TempFilePrefix The temp file prefix for file based message files
    TempPath The Path where message files are written for file based messaging
    Timeout The request timeout in seconds
    UpdateFile The Update EXE file from which the Exe file can be hot swapped

    Requirements

    Namespace: Westwind.WebConnection
    Assembly: webconnectionmodule.dll

    AppConfiguration.AdminAccount


    The account that is allowed access to the administration functions when authenticated.

    Can be any account name, or ANY for any authenticated user. You can also specify a list of authenticated users in a comma delimited list.

    public string AdminAccount
    

    AppConfiguration.AdminPage


    The adminstration page in the application. Allows use of ~ for the application path.

    public string AdminPage
    

    AppConfiguration.AutoStartServers


    Determines whether servers are automatically started when the first hit comes into the module. Useful primarily for file based operation which starts up the EXE server

    public bool AutoStartServers
    

    AppConfiguration.ComServerLoadingMode


    Deterimines how servers are processing requests. Round Robin simply goes through each of the servers one after the other while LoadBased always starts with the first server.

    public ComServerLoadingModes ComServerLoadingMode
    

    AppConfiguration.ComServerProgId


    The ProgId of the COM server to be loaded

    public string ComServerProgId
    

    AppConfiguration.ExeFile


    Name of the EXE file for the server application.

    Used for hot swapping functionality and killing the servers when shutting down. The module also uses the file name to retrieve version information as well for loading file based instances. It's fairly crucial that this value is set correctly.

    public string ExeFile
    

    AppConfiguration.LogDetail


    Determines whether request data is logged in detail.

    public bool LogDetail
    

    AppConfiguration.MessageDisplayFooter


    A default footer message displayed on the bottom of the Module's generic messages

    public string MessageDisplayFooter
    

    AppConfiguration.MessagingMechanism


    Determines how messaging works in Web Connection

    File Com

    public MessagingMechanisms MessagingMechanism
    

    AppConfiguration.PostBufferLimit


    Max size of the POST buffer - if bigger request is aborted

    0 - means no checks are performed. Note that ASP.NET settings may override

    public int PostBufferLimit
    

    AppConfiguration.ServerCount


    Determines how many Com instance of the server are loaded in Com messaging mode.

    public int ServerCount
    

    AppConfiguration.TempFilePrefix


    The temp file prefix for file based message files

    public string TempFilePrefix
    

    AppConfiguration.TempPath


    The Path where message files are written for file based messaging

    public string TempPath
    

    AppConfiguration.Timeout


    The request timeout in seconds

    public int Timeout
    

    AppConfiguration.UpdateFile


    The Update EXE file from which the Exe file can be hot swapped

    public string UpdateFile
    

    Class AppErrorMessagePages


    Configuration class that holds the Error Page mappings. Each of these properties can point at an external page that can be used to display a custom error page for specific errors.

    The filenames can either be expressed as relative Web Paths: messages/Maintenance.htm ~/messages/Maintenance.htm

    or as absolute physical paths: c:\westwind\wconnect\messages\maintenance.htm

    Whatever path files are read from requires that the account running the application (typically SYSTEM) has read access.

    System.Object
      Westwind.Tools.wwAppConfiguration
        Westwind.WebConnection.AppErrorMessagePages

    public class AppErrorMessagePages : wwAppConfiguration
    

    Class Members

    MemberDescription
    Busy Page displayed only for COM messaging when no server instance can be retrieved when all instances are already busy processing requests.
    Exception Page to display when an Execption occurs during the server call. Used for file based messaging.
    InvalidRequestId Page to display when the request id doesn't match
    Maintenance Page displayed when the server is in maintenance mode and not accepting requests while the MaintMode flag is set.
    NoOutput Page displayed when the server application returned no output to the module.
    OleError Exception fired when a COM Server call fails. This is specific to a failure in the COM server operation - typically this will be an Invokation error or a COM security error.
    PostBufferSize Error displayed if the PostBufferSize is exceeded
    Timeout Page displayed when a request times out for taking too long to process
    TranmitFileFailure Page displayed if TransmitFile fails to find or access the requested file

    Requirements

    Namespace: Westwind.WebConnection
    Assembly: webconnectionmodule.dll

    AppErrorMessagePages.Busy


    Page displayed only for COM messaging when no server instance can be retrieved when all instances are already busy processing requests.

    public string Busy
    

    AppErrorMessagePages.Exception


    Page to display when an Execption occurs during the server call. Used for file based messaging.

    Exception is also the 'generic' failure message that is displayed if an internal error occurs in the module processing.

    public string Exception
    

    AppErrorMessagePages.InvalidRequestId


    Page to display when the request id doesn't match

    public string InvalidRequestId
    

    AppErrorMessagePages.Maintenance


    Page displayed when the server is in maintenance mode and not accepting requests while the MaintMode flag is set.

    public string Maintenance
    

    AppErrorMessagePages.NoOutput


    Page displayed when the server application returned no output to the module.

    public string NoOutput
    

    AppErrorMessagePages.OleError


    Exception fired when a COM Server call fails. This is specific to a failure in the COM server operation - typically this will be an Invokation error or a COM security error.

    public string OleError
    

    AppErrorMessagePages.PostBufferSize


    Error displayed if the PostBufferSize is exceeded

    public string PostBufferSize
    

    AppErrorMessagePages.Timeout


    Page displayed when a request times out for taking too long to process

    public string Timeout
    

    AppErrorMessagePages.TranmitFileFailure


    Page displayed if TransmitFile fails to find or access the requested file

    public string TranmitFileFailure
    

    Web Connection Configuration and Deployment


    This section discusses a number of features of Web Connection in detail. Many of these topics were introduced in other places - this section serves as a more detailed and complete reference for specific features and operations.

    Deploying your Web Connection Application


    This section covers topics of the issues involved in installing a Web Connection application on a live Web Server. This process usually consists of:

    After you've deployed files the easiest way to perform the remaining tasks is by using the Server Configuration Wizard, which lets you do the following:


    Compiling your Server


    In order to deploy your server the first step will be compilation.

    If you created your application with the New Project Wizard you can simply do:

    DO BLD_<yourProject>

    This will compile the server and register it for interactive DCOM use. Alternately you can just compile your EXE file directly:

    BUILD EXE <yourProject> FROM <yourProject>

    Once compiled your server can now be run either from Explorer or is accessible as a COM object. To quickly test COM operation try this from the Command window:

    oServer = CREATEOBJECT("<yourProject>.<yourProject>Server") ? oServer.ProcessHit("query_string=wwMaint~FastHit")

    The first line should bring up your server as a window. The second line will simulate a request in the server and should return an HTTP response string.

    Make sure your COM server is marked for SingleUse
    One thing you should check if you are running a VFP version prior to 8.0 is to make sure that your server is compiled as a SingleUse COM server. To check this:


    You don't have to do this for VFP 8 and later since these versions default to SingleUse for EXE COM servers.



    Deploying an application on a live Web server


    Once you've developed your application and you've tested and debugged it on your development machine, the next step is to take that application online.

    Summary:

    Copying files to the server

    This process involves moving the application and HTML files to the Web server. This is a manual process that you need to perform on your own and will vary depending on your application.

    This can involve installing from an installer, simply copying files to a server or FTP'ing files to a server.

    Note that a first time installation requires someone at the server to run the installation.

    The following need to be moved to the server:

    Project Creation considerations

    When you create a new project, especially using the
    New Project Wizard, the Wizard creates both the project file in the application directory and some startup HTML files in a Web directory. It's generally a good idea to let the Wizard create a copy of wc.dll into your project's BIN directory. This will allow you to use the DLL independently of an existing Web Connection installation which requires wc.dll in the /wconnect virtual directory.

    Use the script maps
    The New Project Wizard also creates a scriptmap for your application automatically - it's highly recommended that you use that scriptmap extension instead of referencing the DLL directly so that the application is more portable.

    Tip!
    Whenever possible try to set up your project in such a way that it mimicks the final setup on the server. Use the same directory structures and data paths for example. Although this is not required this setup can make it much easier to fix problems and synchronize a development and live installation. Always make sure that all paths (both application and Web paths) are relative to some base path or the current path. This will ensure your application is portable when moved to different directory.

    Take note early on on how the application will be run on the server. For example, your app may be designed in a

    The Project Wizard tends to set up applications in a virtual directory, but you can easily move the application to the root if you choose. You can run applications either out of the virtual or the root if you keep to strict relative pathing for images and other related file! Don't hardcode paths or your app will not be portable!

    Copying files

    Ok, once the app is running on a development box it's time to move it to the server. This involves copying the application and HTML files. For Web files you have to copy all of your HTML files and images and other files that are located in the Web paths.

    For application files again copy all files that are related to your project including the executable and data files. In addition copy the following:

    Create a Temp directory

    You also need to create a temporary directory where Web Connection looks for message files in file based message mode and writes error log entries for any operational mode.

    This directory needs to be accessible to the SYSTEM account (or whatever account your Web Server is running under) with full access.

    Run the Server Configuration Wizard

    Run the Server Configuration Wizard on the server and set up any virtual directories, script maps to your web connection DLL and register your server as a COM object on the remote machine. Please see, Server Configuration Wizard for more info.

    COM Server Configuration

    The easiest way to configure your COM server is to run the Management Console and use the Configure COM settings step to automatically configure your server for DCOM configuration. You can also perform these steps manually, but I'd highly recommend you use the Wizard since it does the job quicker and more reliably.



    Using UpdateExe to hotswap COM Servers


    The Web Connection ISAPI DLL includes a mechanism for hot swapping COM servers while the server is running. This mechanism allows updating to a new executable very quickly - about 3 seconds per running instance. The mechanism performs these tasks:

    The URL to perform the tasks is:

    wc.dll?_maintain~UpdateExe



    Server Configuration Wizard


    Goto
    Step 1


    This wizard is responsible for configuring Web Connection applications online. Typically you'll run this Wizard after you've created an application locally and now need to move it to a server. This Wizard allows you to set up virtual directories, scriptmaps and configure and synch the Web Connection INI files and register COM servers.

    This Wizard is selective and allows partial operations to be performed. For example, you can only create a scriptmap or only create a virtual directory, or you can do both. In other words, this Wizard is very versatile and can be used for many common Web server and registration tasks, which is especially useful when installing Web application on a live server or passing on steps to perform by an ISP.

    Note that the configuration Wizard will not copy any files from your client to the server - it is purely a server configuration utility that must be physically run on the machine to be configured. This means you need physical access to the machine in question or some mechanism like pcAnywhere to run it.

    Requirements

    This wizard unlike the others does not generate any code nor configures any Web Connection specific features. This Wizard is purposefully generic so you can run it on a server. You will need the following in order to have full functionality:


    Step 1 - Create virtual directory


    Goto
    Step 2


    This step deals with configuring a virtual directory. You can choose a path and make it a Web virtual directory.

    Pick your Web Server

    This step is always required, so make sure you select the correct Web Server from the list of servers.

    Creating the Virutal Directorty

    In order to create a virtual directory you have to specify the virtual directory as well as the path that this directory is to refer to. Virtual directories in general are used to provide logical access to a physical path on the hard disk - you can map a non-Web path (like d:\somepath) logically into the Web so that it can be accessed as http://localhost/myvirtual/. Virtuals also serve as an application entity - Cookies, Authentication and rights always apply from a given virtual downward, so in general it's a good idea to create virtuals for an application's base path (if it's not running at the root of the Web server).

    If you're installing Web Connection for the first time and you will always run out of the root directory you will probably want to create a wconnect directory to hold the wc.dll and config files.


    Step 2 - Synch ISAPI DLL and the Web Connection Server


    Goto
    Step 3


    This step allows to copy a dedicated copy of wc.dll into the virtual created in step 1. The DLL is then configured.

    The configuration options on this page tell where to look for the server's INI file to get its settings (in this case WebDemo.ini) and pulls those values out if found. These values can be reset to new ones and overwritten. The final values are then synched in both the WebDemo.ini and wc.ini files.

    Step 3 - Create scriptmap


    Go to
    Step 4


    This step allows you to configure a custom scriptmap for your server.

    Creating a script map

    The Wizard also allows you to configure a scriptmap that maps to this sub-application. This allows for script based request routing where the extension (.wpfor WebProcess) is always routed to the new class and the page name (Helloworld.wp goes to WebProcess::HelloWorld). The default wwProcess handler makes this type of request routing automatic.

    Specify the name of the script map (this is a file extension so keep it to 2-4 characters - no period). Then point it to the Web Connection dll (wc.dll) of the application that this process class will be hooked to. If you were copying the DLL in the second step you will get a warning that the file doesn't exist which is OK because it will be copied when you finish.

    You can also specify whether the script maps are local to the virtual directory or global to the Web site. It's recommended you create local scriptmaps to avoid confusion over which DLL is handling requests at the server level.


    Step 4 - Configure COM settings


    This step allows you to register your COM server and set DCOM permission so that the server can load and call your COM object from within the Web Server.

    Server Exe
    Pick the EXE file that you want to register. This file will be registered by running WebDemo /regserver to register the COM object.

    ProgID
    The progid of the server such as WebDemo.WebDemoServer. Note: No checks are made that this is correct, so make sure you know what the server's ProgID is. You can check the project settings. Typically the progid is the name of the project, dot, name of the class. If you let WC generate the project it'll be the project name, dot, project name + Server. Hence, WebDemo.exe results in WebDemo.WebDemoServer.

    Server Impersonation
    This option specifies the user account and password that your COM server will run under. This username can be one of the fixed Windows accounts like INTERACTIVE USER, SYSTEM, NETWORK SERVICE which require no passwords, or a specific Windows Account that exists on the machine. If you specify a user account you also have to specify the password.

    This setting is equivalent to the DCOMCNFG Impersonation setting.

    DCOM Launch Permissions
    This option configures the DCOM Launch and Access permissions for your COM object. These settings add users to the permission list of the server so that applications and users can launch your COM object. Starting with Web Connection 5.0 typically you only need to set the System or NETWORK SERVICE account (depending on which account your Web Server or Application Pool is running under).

    Web Connection by default adds these accounts to the list:

    SYSTEM
    NETWORK SERVICE ( IIS 6 only)

    This setting is equivalent to the DCOMCNFG Default Launch and Access and Computer Launch and Access rights (if Add to Machine is checked).

    Note:
    DCOMPermissions.exe in your .\TOOLS directory is required in order to set the various DCOM settings automatically from the Wizard. Make sure you copy this file to your installation either in the .\TOOLS directory or in the same path as the CONSOLE.EXE file.

    wc.ini Configuration Note:
    This Wizard does not add the ProgId to your server wc.ini file if you are not copying a new wc.dll to your project. In other words - it assume your wc.ini file is already configured unless you are creating a new one. Therefore make sure that when you deploy your Web Connection DLL/INI that the ProgID matches:
    ...
    [Automation Servers]
    ;*** Severloading - 0 - Normal  1 - Round Robin
    ServerLoading=1
    
    ;*** KeepAlive  0 - Normal  1 - Force extra COM reference to keep alive
    KeepAlive=1
    
    Server1=webDemo.webDemoServer
    Server2=webDemo.webDemoServer
    ;Server3=webDemo.webDemoServer
    ...


    Manual Server Configuration


    The easiest way to configure your Web Connection application once it has been copied to the server is to run the
    Server Configuration Wizard. The Wizard can provide everything that is need to configure the Web settings, set permissions and copy Web Connection related files.

    Of course you can also manually configure your server and this topic shows you how to manually configure an IIS Web server. This topic should be followed up by:

    Manual Configuration

    Assuming you have copied your application to the Web server and you want to manually configure the server you have to do the following:

    Create a Virtual Directory

    The first step is to create a virtual directory on your Web Server. A virtual directory is a directory mapping that lets the Web Server see your directory beneath the Web root even if that directory is not located immediately underneath your Web root path. A virtual directory is also used to host specific directory settings that set the security and operational aspects of this directory that acts as a host to your application.

    In IIS to create a virtual directory:

    1. Start the IIS Management Console
    2. Select the Web site and right click for the context menu
    3. Select New | New Virtual Directory
    4. Enter the name of the virtual directory and the physical path on disk
    5. Select Read | Run | Execute options

    Once you've done this you should see a dialog like this:

    Create a Scriptmap

    Next you might want to create one or more scriptmaps. Scriptmaps map an extension to a copy of wc.dll so you can reference any file with the mapped extension and have it run through the Web Connection application.

    To set up the scriptmap:

    1. Go to the Virtual Directory
    2. Click on Configuration to see all script maps defined
    3. Click on Add... to a add a new script map
    4. Enter the path to wc.dll for your application
    5. Enter the script map extension name - no need to type the '.'
    6. Make sure that the 'Verify that File exists' checkbox is unchecked!



    Manual COM Server Configuration


    The new Management Console and New Project Wizard make short work of creating a COM object and running this object through COM from the Web Connection ISAPI extension. For Project generated COM server configuration see
    Step 5 of the New Project Wizard.

    The Server Configuration Wizard is the easiest way to get COM up and running with Web Connection. However, if something goes wrong during installation it's important that you understand what's actually happening when you build a Web Connection COM server. This topic takes you through the manual steps.

    If you go through the steps manually for the first installation you need to:

    When to use COM

    Since Web Connection can run as a standalone EXE you might ask yourself why bother with COM. After all COM is a little more difficult to set up (although much easier than it used to be) and configure. However, COM operation with WWWC has some distinct advantages:

    The downside of COM operation is that it's more difficult to set up for the first time. Once installed you have to be careful how you build your servers - COM servers are very touchy about mismatched ClassIDs. If you read this topic carefully and follow a few simple rules the process is straight forward, but it is important you that you follow these steps carefully!

    No code changes required

    COM operation is supported transparently by Web Connection. The wwServer class has a lComObject flag that determines whether the server is running as a standalone or a COM object. As a COM object the call interface to the server is different and output is generated differently than with file based messaging. All of this happens entirely behind the scenes as part of the framework. Your application code doesn't need to worry about whether it's running under COM or as a standalone app as the objects provide you with the right tools to read and write from the appropriate services.

    DEBUGMODE in wconnect.h

    Before building your COM server it's important that the server is built bullet proof. Make sure everything works properly in file mode before tyring to run in COM mode.

    The most important thing is that all error handling be enabled by setting the DEBUGMODE flag in wconnect.h prior to compiling your project. This is vital so that Web Connection's error handlers can kick in on any non-handled error in your application and framework code.

    Compiling the server

    First thing you need to do is build your project into an EXE file. Web Connection servers can run in the Web Connection pool as Out of Process servers. The pool is managed and Out Of Process servers allow the control necessary to provide crash protection and live code updates which would not be possible with In Process applications.

    Note
    You can build DLL servers if you like as long as they are hosted through Microsoft Transaction Server - when you do, note that many of the admin features will no longer be functional.

    Web Connection makes it easy to build your server interactively using file based messaging and an interactive Visual FoxPro session which allows you to thouroughly debug and test your code. That gets you 99% of the way.

    Once the code works with file based messaging follow these steps.

    1. Make sure that your server works without errors in File Based operation.

    2. Modify wconnect_override.h to make sure the DEBUGMODE flag is set to .F.

    3. Open your Server Project file and build it into an EXE file. This will create a COM capable EXE server.

    4. In VFP 8 or earlier, after you've built the project for the first time, go into the Project Info dialog and select the Servers tab. Set the Instancing combo to SingleUse. This is very important!

    5. Test the COM object which should generate some simple HTML:
      o=CREATE("WebDemo.WebDemoServer")
      ? o.ProcessHit("query_string=wwMaint~FastHit")
      

      If you don't VFP on the machine you're installing on you can create a small .VBS script file. Assuming the machine allows running VBScript files and/or you're an Administrator you can use the following script in a .VBS file:
      set oServer = CREATEOBJECT("webDemo.webDemoServer")
      lcHTML =  oServer.ProcessHit("query_string=wwMaint~FastHit")
      MsgBox(lcHtml)
      
      You should see the server form pop up and then some HTML printed to the VFP desktop.

    Moving the COM object to the server

    If you're building the COM component on the Web server the object is automatically registered and ready for operation.

    However, if you build your component on a development machine and then copy the object to the Web server you have to manually register the component from the command prompt:

    <yourserver>.exe /regserver

    Make sure that at least the Visual FoxPro runtime is installed on the server. See VFP documentation for required files and how to create an install for COM applications using the Setup Wizard.

    VFP 6/7 Note:
    If you are using VFP 7 or 6 you also need to copy <yourserver>.tlb to the server as these versions do not compile the type library into the EXE.

    Once the object has been registered I'd recommend you try to test it on the server as well. If you have VFP installed you can use the code above. If you don't, you can use a VBScript file or any application that contains VBA to test the server using code similar to the code presented in the last paragraph.

    Configure the COM object with DCOM

    Once the above works you need to configure the COM object properly so that the Web server can instantiate it. Every EXE server on a system must be configured to allow restricted accounts like the Internet Guest Account to access it.

    To do this you use the DCOMCNFG utility (part of Component Services in Windows XP/2003 Server and later):

    1. Go to the NT RUN box and type DCOMCNFG.


      Note: this image is for Windows XP and Windows Server 2003. For Win2000 and earlier this dialog looks different and you'll see the list of servers in a plain list.

    2. Scroll through the list and find your server name in the list usually (<yourProject>.<yourproject>Server) or if you used the Fox Project's server naming features the name of the server.
      Note:
      If you have other objects that are marked OLEPUBLIC in your project it's possible that the name of this object will pop up instead as <yourProject>.YourOtherCOMServer.

    3. Once you find your server select it and double click.

    4. Go to the Identity Tab and set the Impersonation to The Interactive User, which is the currently logged on user.

      This sets the server to run through whatever account is currently logged on and makes it possible to have a visible Web Connection server on the desktop. Essentially DCOM creates an Interactive Logon for the current user session and runs the COM Server in your current Windows desktop environment.

      Running without a Windows Logon
      If you want to run without a Windows logon you can't use the Interactive User and you have to use a specific account instead. To do this choose This User and specify a username and password of a specific Windows user account. Make sure this account has the proper rights to run your application, so it can access data files, configuration files, can read script files out of your Web directory and has rights to SQL databases etc. It's up to you to make sure the account you pick has the proper permissions. Generally this account will be some sort of Admin account similar or the same as the Interactive account you use for testing. This account should be a LOCAL account rather than a Domain account.

      Non Interactive Accounts run invisibly
      Note that when you use an account other than Interactive, Web Connection will run invisibly - there will be no server form showing on the desktop. For more information on startup options see Autostarting your Web Connection Server. For testing we recommend that you use Interactive first to make sure everything works before switching to a specific account.

    5. Go to the Security Tab. You should make sure that hte Launch and Access Permissions on this page are set to 'Use Default' which means they are inherited from the Global DCOM settings on the machine. This is the default so usually this doesn't need to be changed.

    6. Ensure Global DCOM Security allows execution. Again this should be set by default so usually you don't need to change these settings, but let's make sure that they didn't get changed.

      Go to the My Computer node of the tree in Component Manager (for Win2000 and earlier this setting can found on the main DCOMCNFG form under the Security tab). Open up this page and find the Access and Launch permissions.

      By default the Web Connection DLL runs under the default IIS Web Server account which is usually SYSTEM, so your COM servers get launched from this account. The actual account is determined by which account IIS or your IIS Application Pool runs under. If you used standard Web Connection configuration tools and setup steps this account is always SYSTEM.

      Find your Web Connection User Account
      When running IIS, Web Connection usually runs under the SYSTEM account, but depending on your version of IIS and your server is configured another account might be in use. To find the account Web Connection runs under, go to the ISAPI Administration page from Admin.asp. On the status page look at the Current Login value which is the account the Web Connection ISAPI DLL runs under. This is the account you should set Launch and Access permissions for.

      Non Default Configurations
      If you are using IIS 5 make sure you run your Web Connection virtual directory in Low Isolation to guarantee SYSTEM account usage. Otherwise you will need to add the IWAM_ account that the medium or high isolation processes use. This applies to IIS 5 only.

      In IIS 6 you can configure the account used to run an Application Pool process. It's recommended that you use the Local System account (SYSTEM) , but you can use the default NETWORK SERVICE or any other account. If you do, make sure you reflect that account here and add it to the Launch and Access permissions. If you use the Wizards Web Connection automatically configures a Web Connection Application pool, sets the Impersonation of the pool to SYSTEM and adds your virtual(s) to this pool. It's recommended you do the same if you manually configure your server. Remember that if you use a non-SYSTEM account.

      If non-System accounts are used make sure the account has rights to READ access in the directory where wc.dll lives (to access wc.ini) and READ/WRITE access in the Web Connection TEMP directory (to write log files).

      Non-IIS Web Servers
      Other Web Servers run under different accounts and the same mechanism can be used to find the operating user account. Apache varies depending on how it was launched. If launched as a service it will use the account the Service is configured under (usually SYSTEM). If Apache is launched interactively it will use the current desktop login.

      Make sure that the SYSTEM account is in the Access and Launch Permission dialogs. Under IIS 6 it's also a good idea to add NETWORK SERVICE just in case you forgot to set the Application Pool to use the Local System account.

      You need to add these accounts to both to the Launch and Access permission dialogs. Once you've set the permissions here you can click OK and exit the server configuration for your COM server.

      (On Windows 2000 and earlier go back to the main DCOMCNFG form, then click Default Security to get to this dialog).

    You should now be able to try executing your Web request and see the server(s) popup.

    Programmatically configuring DCOM permissions

    The above isn't exactly trivial and it's easy to miss a step. For this reason we've provided you some tools that make this easier. On a lower level you can also perform these tasks programmatically either via the commandline or via functions provided by Web Connection.

    Note:
    All the DCOM configuration options require that the DCOMPERMISSIONS.EXE file from the Tools directory is deployed to the same directory (or the TOOLS dir) of your server.

    Using the the Server Configuration Wizard
    As mentioned above the Server Configuration Wizard provides a visual UI for preparing your server installation. Step 4 provides you with the ability to register a COM server and set DCOM permissions through the user interface. You'll notice that we're repeating ourselves - the Wizard is the easiest and most reliable way.

    CONSOLE.EXE "DCOM"
    You can also use the CONSOLE as a command line utility either from within Visual FoxPro or from the DOS window:

    DOS Window:

    *** Configure the Impersonation to Interactive
    CONSOLE "DCOMIMPERSONATION" "webdemo.WebDemoServer" "Interactive"
    
    *** Add Launch and Access Permissions
    CONSOLE "DCOMPERMISSION" "webDemo.WebDemoServer" "SYSTEM"
    
    *** These two are not really required but a good idea for dev environments
    CONSOLE "DCOMPERMISSION" "webDemo.WebDemoServer" "Interactive"
    CONSOLE "DCOMPERMISSION" "webDemo.WebDemoServer" "Administrators"
    
    *** If you need to add a specific account
    CONSOLE "DCOMPERMISSION" "webDemo.WebDemoServer" "rstrahl","supersecretpassword"
    

    From the Command window:

    DO CONSOLE WITH "DCOM","webDemo.WebDemoServer","IUSR_RASNOTEBOOK" DO CONSOLE WITH "DCOM","webDemo.WebDemoServer","Administrators"
    etc.

    Using pure code
    This maybe useful if you want to build your own installer:

    DO WCONNECT *** Set Impersonation User DCOMCnfgServer("wcDemo.wcDemoServer","Interactive") && Interactive User loAPI = CREATE("wwAPI") lcMachineName = loAPI.GetComputerName() *** Set Access rights DCOMLaunchPermissions("webdemo.webDemoServer","Administrators") DCOMLaunchPermissions("webdemo.webDemoServer","SYSTEM") DCOMLaunchPermissions("webdemo.webDemoServer","INTERACTIVE")

    Note that you have to have DCOMPermissions.exe available in the FoxPath in order for these functions to execute properly! This command block effectively mimicks the DCOMCNFG steps outlined above.

    If you do this you may still need to configure the Access and Launch permissions for the server! You still have to add or at least check for the Default Users in the Default Security tab the first time you register a server though!

    Setting up the ISAPI extension for COM operation

    Click on the Load Servers link to get your server(s) to load. If this succeeds you should see your server popping up on the desktop and you should see the server(s) display in the list now. Now go back to the Admin page and try hitting one of the links to the server - the links should be served from your new server(s).

    wc.ini COM configuration options

    You can configure the COM servers with a couple of options in wc.ini. A typical section looks like this:

    [Automation Servers]
    Server1=wcDemo.wcDemoServer
    Server2=wcDemo.wcDemoServer
    ;Server3=wcDemo.wcDemoServer,OFFICESERVER
    ServerLoading=1
    KeepAlive=1
    COMLoadLockout=0
    
    ; Set to 1 Apache and other non-IIS Web Servers, and IIS 4 or older
    CallCoInitialize=0
    

    Server List
    The server list simply contains the server instances that are to be loaded. They should all point at the same type of server. You can load as many as 32 server instances of your object. It's important to understand that all of these server references must point at the same executable - IOW, each server provides exactly the same functionality. To provide a different server with different operations you have to set up another Web Connection project with a separate copy of wc.dll.

    Remote Objects
    Server3 demonstrates how you can access a COM object on a remote machine by simply seperating the COM object name with the name of the server that you want to run the object on. Note that the object must exist on the remote machine, must be registered and accessible via DCOM from the same user that is running your application locally. The name must follow standard NT server name conventions and can include IP addresses, domain names, netbios names and UNC type names.

    Although the option to do this is available and it works, configuration and administration of this feature is complex and there are issues with DCOM remote lifetime management. For more info on this feature and other loadbalancing options see Scaling Web Connection Servers across the network.

    ServerLoading
    Web Connection's pool manager allows you to run up to 32 instances of your server simultaneously. This flag determines how servers are loaded when requests come in. By default requests are processed by the first available server in the pool manager. If the first is busy the second one is checked - if it's busy the next and so on. This typically means that the first server in the pool gets much more activity than the last one.

    If this flag is set to 1 the servers are loaded in round robin fashion. Web Connection will keep track of the last server hit and them move on to the next one. If that one's busy it keeps going around until it finds one that's available. Round Robin works better in high volume environments as the load is balanced across active apps and the servers have some wind-down time between requests.

    KeepAlive
    There's a quirk in the DCOM subsystem of Windows NT that causes EXE COM objects loaded by a client and idle for more than 8 minutes to unload automatically. This feature was built into DCOM as a crude mechanism for controlling hung servers. The side effect is that it also kills valid, yet idle applications.

    For your applications this behavior may actually be useful in order to preserve server resources - you can load a larger number of servers and keep them idle so only one or two in the group will run. However, the unloading that occurs when DCOM yanks the server is very crude as well - it simply terminates your server much like an app that GPFs. This can on occasion lead to corruption of the DCOM subsystem resulting in occasional error messages related to resource exhaustion.

    To work around this problem Web Connection includes the KeepAlive flag. This flag forces the Web Connection ISAPI extension to do an extra AddRef() on the server which keeps the server alive indefinitely.

    CallCoInitialize
    Determines whether CoInitialize is called for COM operation. IIS 5 and later feeds ISAPI threads that already have CoInitialize called and thus it is redundant to call this function again. The value is off by default and should be turned on only if running on IIS 3 or 4 or when running non-Microsoft Web servers.

    If you are running Apache or other non-IIS Web Server in COM mode make sure CallCoInitialize is set to 1.

    ComLoadLockout
    This is a flag that controls whether requests are queued before the Web Connection servers have fully initialized. If this flag is set to 1 Web Connection returns a message to clients that the servers are still loading. This might be required in very high volume scenarios to avoid overloading the request queue on server startup. The default is 0 (off) which simply queues requests until the servers are loaded and then processes them.

    Some COM operation tips



    Manual File Server Configuration


    The Web Connection Management Console (CONSOLE.EXE) includes a
    Server Configuration Wizard which lets you configure a Web application and we recommend that you take advantage of this tool to configure your application.

    However, if you want to manually configure your file based server after you've copied all files to the server here's how you do it.

    Overview
    Assuming you have copied your application to the Web server and you want to manually configure the server for File Based operation you have to configure the Web Connection application INI files so that the server and the wc.ini file can communicate with each other.

    When working in Filebased mode the key thing is to make sure that your applications INI file (YourApp.ini or wcdemo.ini) is synched up properly with the Web Connection ISAPI INI file (wc.ini). Both need to point to the same directories for the temporary files so that they can communicate and the permissions on these directories need to be right.

    Changing the your FoxPro server's settings
    Your FoxPro Web Connection Server uses an INI file with the same name as the project to hold a number of startup parameters. The easiest way to change the most common settings is to start the server and use the Status form.

    To make changes to the application's INI file simply change the value on the Status form and click the Save Server Settings button. When you exist the form these settings should be live. These settings are written into your application's ini file (<yourApp>.ini or wcDemo.ini for the demo app). You can also set these settings manually in the INI file of course.

    The key settings for file based communication are:

    [Main]
    Tempfilepath=d:\temp\wc\
    Template=WC_
    

    Changing the wc.ini settings
    The Web Connection ISAPI INI file (wc.ini) contain configuration settings that tell the Web Connection web interface how to handle requests. This drives the C++ code and is separate from the INI settings above.

    To make changes to the Web Connection ISAPI INI file find wc.ini in your Web directory or the bin directory of your Web application (ie. \inetpub\wwwroot\webdemo\bin\wc.ini). Open the file with a text editor or the Visual FoxPro editor.

    [wwcgi]
    Path=d:\temp\wc\
    Template=wc_
    

    You'll want to change the Path and Template keys to match the value from your FoxPro server's INI file mentioned above.

    Setting Permissions on the Temp folder
    You need to make sure that SYSTEM, the Internet Guest Account (IUSR_<machinename> or whatever you have configured in IIS as the Anonymous User) and Administrators have FULL rights in the temp directory used in the entries above.

    Starting up and checking settings
    Once this is done you should be able to start up your Web Connection Server from Visual FoxPro or as an EXE. Try a request - it should work fine from here.


    But it's not working

    To trouble shoot a file based installation please check out the Troubleshooting a Filebase Server Installation topic.


    Copying Files to the Server


    When deploying your Web Connection application the first step is likely to be getting your application copied to the server. This includes:

    This process can take many forms and there really are no hard and fast rules. Copying files tends to be a one time task and so it doesn't necessarily need to be an automated process.

    However, it might be useful to organize your applications in a certain way to facilitate this copying process by specifically creating an application hierarchy for deployment that reflects the live environment so that you can copy the files from staging/development environment easily to the server.

    The following is just a file and folder arrangement deployment suggestion, but it's one that's served me particularily well in the course of many installations.

    It's helpful if your development setup matches the deployment setup, but that's not strictly necessary. Either way I'd recommend the intermediate step of creating a deployment installation and testing that installation before sending files to the server. The reason for this is simple - dev installs tend to include all source files so even if something in your project file is missing and not getting compiled into a final EXE it may still work because the source files are still available. A standalone EXE file may not have this luxury and fail unceremoniously.

    Further I recommend using a deployment hierarchy of folders that include both the Application and data files as well as the Web specific file under a common non-IIS rooted directory. The folder structure for this arrangement then looks something like this:

    AppRoot
    	ApplicationFiles	(EXE,Ini files, Web Connection Tools/html/Scripts/Template folders)
    	Web       		(the Web Virtual directory where WCSX etc. scripts and templates live)	
    	Data			(Optional data directory if you're using VFP data)
    

    Note that in this scenario the web folder is NOT underneath the actual Web root (ie. c:\inetpub\wwwroot) which tends to be the default for the Wizard setups (because that's 'standard' place to put sites) - AppRoot can be any arbitrary folder. But using a non wwwroot based folder works perfectly fine for Web paths although it's crucial that permissions are set appropriately to allow the IIS anonymous user explicit Read/Execute access - this user by default exists only under wwwroot, but not under an arbitrary path.

    The data path is optional - you can also stick data below your application folder, or in some situations your data may be in different paths on the machine or on the network depending on your security policy.

    One advantage of the above layout though is that you have a single directory structure that can be sent to the server via FTP in one shot instead of having to pick files out of multiple paths scattered across the machine.

    Staging Servers or Directories

    The other advantage of creating this sort of deployment hierarchy is that it allows you to create a separate testing environment that can be used to test the installation before sending it to a live server. It's a useful tool if you need to test
    installation scripts or simply for testing the application outside of your development environment before 'going live'. I highly recommend that this be done - preferrably on a separate machine if available as this often lets you find any last minute server bugs or omitted compilation files as well as giving you dry run the installation steps. In almost all situations this will make for much quicker live installation experience.

    Web Connection Application Files Required (AppRoot)

    If you are installing Web Connection for the first time on the server you'll need to ensure that certain files are copied and installed.

    The following need to be moved to the server:

    Web Files

    The Web files for your application will include anything that is accessed through the Web interface. This includes all Web Connection script files and/or templates including .WCSX or other script mapped pages. It also includes all static HTML files, CSS and script resources - basically anything that might be served through IIS.

    In addition the Web Folder should include a BIN directory that holds the application's Web script engine.

    If you're using the Web Connection module instead of the above files you should have:

    wc.ini and web.config hold the configuration for the respective script engines and may need some custom configuration settings to reflect the live development environment. Specifically the temp file path and template need to be set and matched to the same setting.


    Programmatic Server Configuration


    Web Connection includes all the tools to programmatically configure a Web application once you have copied all the files to the server.
    A number of programmatic functions and tools are available to create a post-installation script using FoxPro code that can:

    Here's an example script that demonstrates a typical installation:

    DO WCONNECT SET CLASSLIB TO WEBSERVER ADDITIVE lcApplication = "MyApplication" *** Hardcoded values - these would normally come *** from some sort of UI (popup a form?) *** Deployment Path lcWebPath = LOWER(FULLPATH("..\web\")) lcServerType = "IIS6" *** Name of the virtual Web directory for IIS lcVirtual = lcApplication lcServerExe = lcApplication + ".exe" lcTempPath = ADDBS(SYS(2023)) + "wc" **** COM Configuration lcProgId = lcApplication + "." + lcApplication + "Server" lcDCOMUserName = "Interactive User" lcDCOMPassword = "" && Fill in if you're using a real account *** End stock parameters *** Prompt for a few of the main parameters - ideally this should be a form to *** prompt for all the inputs lcServerName = "LOCALHOST" lcServerName = InputForm(lcServerName,"The local IP Address or domain of the server to configure","IIS Configuration - Server Address",,,"") IF EMPTY(lcServerName) return ENDIF lcServerType = InputForm(lcServerType,"IIS Version (IIS5, IIS6, IIS7)","IIS Configuration - IIS Server Type",,,"") IF EMPTY(lcServerType) return ENDIF lcWebPath = InputForm(lcWebPath,"Location of the Web Directory","IIS Configuration - Web Directory",,,"") IF EMPTY(lcWebPath) return ENDIF *** Create Virtual Directory oIIS = CREATEOBJECT("wwWebServer") oIIS.cServerType = lcServerType oIIS.cIISVirtualPath = "IIS://" + lcServerName + "/W3SVC/1/ROOT" && IIS Schema path oIIS.cApplicationPool = "West Wind Web Connection" IF ISNULL(GETOBJECT(oIIS.cIIsVirtualPath)) showStatus( "Unable to connect to IIS Administration COM object." ) return ENDIF showStatus("creating virtual directory") IF !oIIS.CreateVirtual(lcVirtual,lcWebPath) showStatus("Unable to create virtual directory") RETURN ENDIF *** Do some custom work on the new virtual *** Turn off Anonymous Permissions to FORCE LOGINS for EVERY REQUEST * loVirtual = GETOBJECT(oIIs.cIISVirtualPath + "/" + lcVirtual) * loVirtual.AuthAnonymous = .F. * loVirtual.SetInfo() *** Assume wc.dll lives in BIN directory lcScriptDLL = lcWebPath + 'bin\wc.dll' showStatus("creating script maps") lcIISVirtual = oIIS.cIISVirtualPath + "/" + lcVirtual *** Create script maps to the DLL oIIS.CreateScriptMap('wc',lcScriptDll, lcIISVirtual) oIIS.CreateScriptMap('wcsx',lcScriptDll, lcIISVirtual) oIIS.CreateScriptMap('wwsoap',lcScriptDll, lcIISVirtual) oIIS.CreateScriptMap('aspx',lcScriptDll, lcIISVirtual) showStatus("registering ISAPI dll in IIS") *** In IIS6 and later we have to register any DLLs with IIS IF LOWER(lcServerType) = "iis" AND LOWER(lcServerType) > "iis5" *** Add the DLL as a registered loIIS = CREATEOBJECT("wwIISAdmin") loIIS.cPath = "IIS://" + lcServerName + "/W3SVC" loIIS.AddRegisteredExtension(lcScriptDll,"West Wind Web Connection") loIIS = .f. ENDIF *** Create Temp Directory IF !ISDIR(lcTempPath) MD (lcTempPath) ENDIF *** Set permissions for IUSR_ Virtual loVirtual = GETOBJECT(oIIS.cIISVirtualPath) lcAnonymousUserName = loVirtual.AnonymousUserName loVirtual = .f. *!* *** Set access on the Web directory IF !EMPTY(lcWebPath) *** Read writes - wwUtils.SetAcl() llResult = SetAcl(lcWebPath,lcAnonymousUserName,"R",.t.) ENDIF *** Not required for Web Connection 5.x - wc.dll runs as SYSTEM * IF lcTempPath * *** Full rights in the temp directory * llResult = SetAcl(lcTempPath,lcAnonymousUserName,"F",.t.) * ENDIF showStatus("Register Com object") *** Register COM object and configure DCOM programmatically IF !EMPTY(lcProgId) wait window "Registering COM server" nowait lcCommand = "run " + lcServerExe + " /regserver" &lcCommand IF ISWinNT() IF !EMPTY(lcDCOMPassword) AND !FILE("DCOMPermissions.exe") MESSAGEBOX("DCOMPermissions.exe file is missing" + CHR(13) +; "Can't configure DCOM settings. Please configure manually.",48,"DCOM Settings") ELSE DO DCOMCNFGServer WITH lcProgId, lcDCOMUserName, lcDCOMPassword IF FILE("DCOMPermissions.exe") loAPI = CREATE("wwAPI") lcMachineName = loAPI.GetComputerName() DCOMLaunchPermissions(lcProgId,"Administrators") DCOMLaunchPermissions(lcProgId,"SYSTEM") DCOMLaunchPermissions(lcProgId,"INTERACTIVE") ENDIF ENDIF ENDIF ENDIF FUNCTION showStatus(lcMessage) WAIT WINDOW NOWAIT (lcMessage) ENDFUNC

    This script is not generic obviously, but it can be customized quite easily in a few places for most configurations. You might want to add or remove some script maps (like ASPX which was used for this particular project) or you might need additional settings applied say to the virtual directory. Note that the code even does some custom ADSI configuration for setting security - in this case it's removing Anonymous user access from the app so that every user is forced to log in with Windows credentials.

    The beauty of this sort of script is that once you have your configuration set up it's very easy to run it for a first time config or even to reconfigure if something should get accidentally removed.

    If you want to deploy this you can either choose to build an EXE out of this small program by adding to a project and compiling into an EXE, or maybe even easier by adding it to your main server EXE itself with code like the following in the startup program (like wcDemoMain.prg):

    ************************************************************************ * MyApplicationMain ****************************** *** Created: 06.05.2008 *** Function: Web Connection Mainline program. Responsible for setting *** up the Web Connection Server and get it ready to *** receive requests in file messaging mode. ************************************************************************ LPARAMETERS lcAction IF !EMPTY(lcAction) IF UPPER(lcAction) == "CONFIG" DO configurationScript RETURN ENDIF ENDIF *** This is the file based start up code that gets *** the server form up and running #INCLUDE WCONNECT.H …

    With this code the configuration script is now part of the server and you can simply do:

    YourServer.exe Config

    To run the configuration code. Simple and self contained and makes the configuration script available along with your EXE always.

    File Copying

    One thing that helps is to create your deployment structure something like this:

    AppRoot
    	ApplicationFiles	(EXE,Ini files, Web Connection Tools/html/Scripts/Template folders)
    	Web       		(the Web Virtual directory where WCSX etc. scripts and templates live)	
    	Data			(Optional data directory if you're using VFP data)
    

    Note that in this scenario the web folder is NOT underneath the actual Web root (ie. c:\inetpub\wwwroot), but this works perfectly fine as long as the path is configured properly and permissions are set appropriately specifically for the IIS anonymous user (ie. IIS_YourMachine) to have read/execute access. The configuration script above handles this automatically by looking up the Anonymous account and setting permissions on the Web folder for this account.

    For a more elaborate example of a front end you can take a look at the wwAppWizard class (especially the Configure Server method) and the ConfigureServer class in wcSetup.vcx. The source code for these classes is provided and you can build your own wizards based on them if you choose. Or you can build a much simpler form interface to customize with just the 4 or 5 configurable options to present to the user. In your own applications most options other than the install path are probably fixed so the user interface can be pretty simple for a front end.

    Note that source code for the Setup, New Project and Configuration Wizards is also available - you can customize those Wizards with custom logos and customized code. The actual configuration code resides in wwAppWizard and you can override any of the individual methods as you see fit to customize setup behaviors.

    Autostarting Web Connection on System Startup


    Running Web Connection as a Service or prior to NT Logon

    Symptom: I'm trying to run Web Connection as a Service or at least run it so that no user needs to be logged on.

    One topic that frequently comes up is how to set up Web Connection so that servers automatically start when the system boots up. Most people want WC to run as a service as a result of that. While it is possible to run WC as a service (using the NT ResKit's SvrAny program) I don't recommend this operation because the server cannot be controlled using the Web Connection server management features.

    However, there are other more efficient ways to accomplish this task. Depending on whether you run COM or File based there are several mechanisms available:

    Com Messaging

    When running COM, Web Connection brings up Automation servers automatically when a link that requires a WC server is hit.  You can even have Web Connection servers autoamtically load before anyone is logged onto the system by not using the Interactive account for Impersonation of the COM server. This is done using the Windows DCOMCnfg (part of Component Services) utility.

    If you're always logged on under NT
    If you're logged on under NT you should configure your Automation server to the Impersonate the Interactive User using the DCOMCnfg options (as described in OLE Automation Setup). Using this account allows the best operation of WC Automation servers that live visibly on the NT Desktop.

    AutoLogon to NT/2000/.Net Servers
    A simple way to allow proper operation of WC is to use the above settings and force NT to automatically log in at boot up.

    Web Connection provides a utility in the via the Web Connection menu under Tools to create an AutoLogon entry in the registry for you or you can use the Management Console with the following code:

    DO Console WITH "AUTOLOGON"

    To manually configure this option in the registry set the following registry keys:

    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon

    AutoAdminLogon	REG_SZ       1 - AutoLogon   0 - Manual Logon
    DefaultPassword	REG_SZ       The accout password
    DefaultUsername	REG_SZ       The account name  

    Running without an NT Logon
    COM servers can run without having an NT logon. The default configuration is to run under Interactive which logs on to the local Console. However, you can configure your server very simply to either use the calling processes security context or by configuring a specific acccount which allows running without any login. The account you choose should have sufficient rights to access all resource your application might require.

    The easiest is to simply use pass through security from the calling process, which typically will be the IIS worker process or Application Pool (w3wp.exe). It's optimal for Web Connection to run using the local system account and if this account is used it's the best choice for running without a logon. One limitation of the System account is that it has no network rights so if you need to access files on network shares or possibly connect to a remote SQL server system may not work.

    You can also set up your server to run under a specific account in which case DCOM will impersontate that specific account on every request. To do this go into DCOMCNFG and find your COM server in the list of servers and assign the specific user and password.

    This user should have rights to access your data directories, TEMP and any remote SQL or network connections that your app may need (since this user is tied to your server it's OK to use an Admin account here!). Select your specific server within the DCOMCnfg and click on the Identity tab. Choose This User: and enter a valid account and password. I recommend you use an Admin account so you have full access to the machine/network via the Automation server. 

    Test the server with your custom DCOM account with a logon first to make sure it works! The server will not be visible because it now runs as part of the IIS Service context. However, it should still serve requests just fine. Since the server is invisible you might also want to adjust the startup INI settings to surpress the server form (ShowServerForm=Off) - no need for this overhead. Once you're sure the server runs with this account, restart the Web Service (to unload everything) and log off the machine. The server should continue to work at this point. If these settings work your application is ready for running as part of the IIS Service and acts like a system service.

    Note: When your server is configured as using a specific account or Launching User the Web Connection server runs invisibly regardless of the Interact with Desktop flag of the Web service.

    File Based

    When running file based servers do not start up automatically as the Web Connection servers are standalone Visual FoxPro applications.

    AutoLogon to NT and Startup Group entries
    The easiest way to force WC servers to start up is to use Autologon (as described above) and server entries in the startup group. When the machine boots the startup group fires off servers automatically.

    The StartFileInstances flag in wc.ini
    You can automatically start Web Connection server instances the first time a call to wc.dll is made by specifying the following two keys in wc.ini:

       ExeFile=c:\wwapps\wc2\wcdemo.exe
       FileStartInstances=2

    Servers started in this fashion can be auto-started before NT logon, but the servers will run invisibly. When you do log on the servers will become visible.



    Find and configure the Account Web Server is running under


    In order to find which account your Web Server is running under you can use the Web Connection ISAPI Adminstration page.

    1. Go to the Admin.asp page for your project.
    2. Click on Show and Manage ISAPI Settings
    3. Look at the Current Login setting

    The value in the Current Login setting is the user account that the Web Server is running. Usually this account will be your SYSTEM account, but it can also be NETWORK SERVICE, or any account you configured for the service or IIS 6 Application Pool.

    Access needed by the Web Server account

    This account is the account the Web Connection ISAPI DLL runs under. The DLL needs rights using this account for the following tasks:



    The Server User Interface


    For the most part Web Connection is a developer tool where you write code. However, there are a few administrative features that are provided through several user interface services:



    The main server process form


    The server form provides visual input to your operational server. This form is optonial, but serves a functional role in providing visual feedback on your operating server.

    The main part of the form is made up of the process display which shows the current request running and the time that it took to run it.

    The Status button on this form takes you to the Server Status form that shows the server's current operational parameters and allows you to adjust these settings and save them for startup.




    The server status form


    The server status form serves as the configuration console of the currently running server application. It shows the current settings for the server and allows you to modify them on the fly and save the values persistently.

    Here's what the various fields mean:

    Startup Path
    The startup path shows the location that the server is currently running from. This path is set the first time the server is run based on the server's physical disk location and then stored in the registry. Everytime the server starts this value is read from the registry and the server path (SET DEFAULT/CD) is then changed to the specified directory.

    Typically this path will always match the server's physical location on the disk, but in some special situations you'll want to override this path to a different location. For example, when running a file based Web Connection server on another machine it makes sense to change the path to the remote machine so that all the configuration and data files can be found with relative paths.

    In any case you can change this setting to re-write the value into the registry.

    Maps to wwServer::cTempFilePath

    Temp Files, Timer Interval and File Template - File based operation only
    These settings are specific to operating Web Connection in file based mode and determine where and how the file messages are processed by WWWC. The temp file path shows the path where the server expects request files to coming in from the Web server. This setting should match what the Path= setting says in wc.ini. The timer interval determines how often the file based server polls for new request files. The shorter the interval the faster the turnaround. The default is 200 milliseconds. It's not recommended that you set this value smaller than 75 unless your server is super busy all the time. The Template identifies what type of files the server is polling for in the temp directory. Again this value must match the active wc.ini Template setting.

    Show Status
    This flag determines whether the server window displays each request in its window.

    Maps to wwServer::lShowStatus

    Log to File
    Determines whether Web Conection's request logging is turned on. By default Web Connection logs every request to a DBF file, RequestLog.dbf by default. This flag enables or disables this logging.

    Maps to: wwServer::lLogToFile

    Script Mode
    The script mode determines how Web Connection interprets WCS script files. You can choose between interpreted and compiled operation. Scripts can be compiled on the Admin page.

    Maps to: wwServer::nScriptMode


    Capturing Request Information

    A very useful feature of this form is the ability to cause Web Connection to capture request output and then display it to you for review. You can capture both the Request input (form and server variables) as well as the complete HTML output generated by the server.

    To do so use the options on the bottom of the status form:

    The Save Request Files checkbox causes every request to save its inputs and outputs to a static text file in your temp directory. The files are static and are called TEMP.HTM and TEMP.INI. This feature is meant as a debugging mechanism so it's not advisable to have this option enabled in a production environment.

    Once the checkbox is set run a request. Come back to this form and click on the Display Request button. The resulting popup allows you to view the full HTTP output from the last request including the HTTP header, a parsed version of the the Request input, which displays form variables and Server Variables in a key value display, and a raw version of the Request input which shows the raw request data in its URLEncoded format.

    Viewing the request data provides extremely useful debug information that tells you exactly what a client request posted to the server. This is very useful for debugging HTML form problems as well as things like Cookie and Authentication issues where logins apparently fail. This data is provided straight from the Web server so if it's not here, it didn't get sent!

    Use to make sure your app is getting what you think it is!



    Configuration Files


    Web Connection is configured through a few configuration files. These files configure each of Web Connection's components for communication with each other. The following files are used:

    The wc.ini and application INI files can be configured through the server's status form. The application settings can be interactively edited on the Server Status form, while the wc.ini settings are always set in the INI file directly.

    The wc.ini configuration file


    The wc.ini file contains settings that determine the operation of the Web Connection ISAPI interface. The values set in the INI file affect the operation of the low level interface that runs inside of IIS under the security context configured for IIS or the specific IIS Application Pool that hosts the ISAPI DLL.

    NOTE:
    Note the wc.ini file applies to operation with the ISAPI extension (wc.dll). When operating with the Web Connection IIS Module the configuration file settings are stored in web.config in the root of your Web application.

    This file contains several runtime options that are used by the ISAPI DLL to determine how to operate. A typical wc.ini file looks like this:

    [wwcgi]
    ;*** THIS DIRECTORY MUST HAVE READ AND WRITE ACCESS FOR THE
    Path=c:\TEMP\
    
    ;*** Time to allow request to finish
    ;*** Process will be terminated after number of secs
    ;*** specified here.           REQUIRED 
    Timeout=50
    
    ;*** Specify how often wc.dll polls for 'completion' message
    ;*** Specify in milliseconds.  REQUIRED FOR FILE BASED
    PollTime=100
    
    ;*** Max size allowed for the POST buffer. If the post buffer is bigger
    *** than this value in bytes the data is not posted. 0 - means no checks.
    PostBufferLimit=0
    
    ;*** Message File Template (1st 3 letters)
    ;*** Default is "wc_"          Only needed if using a different template
    Template=wc_
    
    ;*** Messaging Mechanism  of the DLL:    REQUIRED
    ;*** File       -   Original Web Connection Logic of 
    ;                   file based messaging
    ;*** Automation  -  Use OLE Automation Server Interface
    ;*** Interactive -  Call up the VFP development environment via Automation
    Mechanism=File
    
    ; PostMethod   -  URLEncoded or INI   -  Match POSTMETHOD in WCONNECT.H
    PostMethod=UrlEncoded
    
    ; Determines whether Web Connection DLL uses Impersonation (1) of the
    ; Web user account (IUSR_ or logged in user) or whether it uses the
    ; underlying IIS account (0)(SYSTEM/NETWORK SERVICE or Application Pool
    ; configured value.
    ; 0 - No Impersonation (default) 1 - Impersonation
    Impersonation=0
    
    ;*** Account for Admin tasks    REQUIRED FOR ADMIN TASKS
    ;***       NT User Account   -  The specified user must log in
    ;***       Any               -  Any logged in user
    ;***                         -  Blank - no Authentication
    AdminAccount=gonzo,ricks,maxhead
    
    ;Admin Page that is used for Backlinks from various internal pages
    ;Use a full server relative Web path!
    AdminPage=/wconnect/Admin.asp
    
    ;*** You can update an EXE on the fly from the UpdateFile
    ;*** With File base messaging you can also use StartEXE to start the
    ;*** ExeFile running
    ExeFile=d:\wwapps\wconnect\wcCOMdemo.exe
    UpdateFile=c:\temp\wcComdemo.exe
    FileStartInstances=0
    
    [Automation Servers]
    ;*** Severloading - 0 - Normal  1 - Round Robin
    ServerLoading=1
    
    ;*** KeepAlive  0 - Normal  1 - Force extra COM reference to keep alive
    KeepAlive=1
    
    ;*** Force users to see message while servers are loading
    ;*** Set to one if you have problems getting servers loaded
    ;*** in high volume environments to reduce thread backups
    COMLoadLockout=0
    
    ServerObject=0
    Server1=wcDemo.wcDemoServer
    Server2=wcDemo.wcDemoServer
    ;Server3=wcDemo.wcDemoServer,WestWindServer2
    ;Server4=wcDemo.wcDemoServer,WestWindServer2
    
    [Extra Server Variables]
    Var1=LOCAL_ADDR
    Var2=APPL_MD_PATH
    Var3=HTTP_CACHE_CONTROL
    
    [HTML PAGES]
    ;*** Use these to override DLL messages
    DLLStatusHeaderText=
    Busy=
    NoOutput=
    OleError=
    
    Timeout=
    
    ;Maintenance=c:\westwind\wconnect\dllerror.htm
    Exception=
    Maintenance=
    NoLoad=
    
    Busy=
    NoOutput=
    OleError=
    Execption=
    Maintenance=
    NoLoad=
    Timeout=
    PostBufferSize=
    

    Here's a description of what these values do:

    Key Description

    Path

    Determines where the DLL sends the server request file that contains the content of a particular request. Typically this will be your system Temp directory

    Timeout

    Determines how long a request can take before it is timed out and an error page is returned to the Web server. If you plan on requests taking more than 60 seconds each youll need to bump this value up.

    Default: 60     (seconds)

    PollTime

    Applies to file based only and determines how often the DLL polls for a return result.

    Default: 200   (millisecs)

    Template

    Applies to file based only and determines the 3 letter prefix that is used for the messaging files.

    Default: WC_

    Mechanism

    Determines whether file based or Automation based messaging is used. Values are: Automation or File

    PostMethod

    Determines how the Request data is encoded. Older versions of Web Connection used an INI file newer version pass URLEncoded strings. If using the default of URLEncoded wconnect.h must have #DEFINE POSTDATA .T.

    Values are: URLEncoded or INI.

    Impersonation Determines under which user context wc.dll executes internal ISAPI requests.

    0 (default)
    No impersonation. wc.dll runs under the underlying IIS account (SYSTEM/NETWORK SERVICE or Application Pool defined user)

    1
    wc.dll uses user impersonation and runs under the account that is currently logged in. This means IUSR_ for anonymous requests or the specific user if Basic Auth or Windows is applied against the request.

    AdminAccount

    Allows setting of the Administrative account that is allowed to access the DLLs internal Maintenance functions that can start and stop servers. If an account is specified only that account will be allowed access. If not logged into the Web server a login will be prompted. Values:

    AccountName The account to check for
    Account List - Comma delimited list of accounts
    A
    ny Any account as long as the user is logged in

    Or leave the key blank to disable account checking.

    AdminPage

    Full path to the admin page you use. This is used to provide a back link from the various Maintenance functions built into the DLL. Example: c:\http\wconnect\admin.asp.

    ExeFile

    UpdateFile

    These two entries allow you to update EXE files online while the server is running. You can set up the EXE file of the server and provide a name for another file that serves as an update file. The Maintain?UpdateExe allows you to upload a file to a server and hot swap server EXEs without stopping the Web service.

    For file based messaging you need to make sure all servers are shut down first or else the update will fail. You can also use StartEXE to restart a stalled server or to restart after an update.

    ExeFile=c:\wwapps\wwdemo\wwdemoole.exe

    UpdateFile=c:\ftp\uploads\wwdemoole.exe

    FileStartInstances

    This flag allows you to automatically launch Web Connection servers when the Web Connection DLL is first loaded. This key uses the value in ExeFile to determine which EXE to launch.

    Make sure you test operation of this feature first by using the wc.dll/maintain?StartExe function to load your EXE, since this function returns error information.

    If FileStartInstances cannot load your servers no indication is given of the failure.

    [Automation Servers]

    This section of the INI file determines which servers are loaded on requests. Note the ability to access remote servers with the last example.

    [Automation Servers]
    Serverloading=0
    
    
    Server1=wwDemoOle.wwDemoServer Server2=wwDemoOle.wwDemoServer ;Server3=wwDemoOle.wwDemoServer,\\MachineName

    ServerLoading

    Used only in the Automation Server section this option determines how requests cycle through the loaded servers.

    0 -Load Based
    Server only gets called when the previous one is busy. Typically the first server is worked heaviest and the last server the least.
    1 -Round Robin
    Request always cycle through servers sequentially. All servers are loaded equally.

    KeepAlive

    This flag allows getting around a DCOM bug that causes COM objects loaded by IIS threads to unload after 8 minutes of idle time. KeepAlive forces an extra reference on the server making COM keep the server locked and making it not unloadable.

    0 Normal
    Servers can unload. Use this if your servers are lightly loaded and run with short requests and you want the system to unload these servers to preserve resources.

    1 Keep Alive

    Keeps an extra COM reference to force servers to stay alive at all times. Servers unload only when IIS unloads or when you use Web Connection's own unload links.

    [Extra Server Variables]

    Use this section to add additional HTTP Server variables that IIS provides to your incoming Request object. Web Connection provides the most common varialbe - this option allows you to retrieve any additional ones that aren't natively provided. Use Var1, Var2, Var3 etc. keys to specify each of the server vars to add.



    Adding Extra Server Variables

    Web Connection provides most of IIS Server Variables by default. However it selectively pulls these variables to optimize performance so some server variables may not be available via the native Request.ServerVariables() method. You can easily see what server variables are pulled on each request by using the Show Status button after a request has been fired with Save Request Data checkbox checked. Look Request Data.

    However, Web Servers evolve and some servers other than IIS might expose additional server variables that don't get pulled by default. For this instance you can override the default behavior to allow adding any custom HTTP Servervariables explicitly by specifying the Server variable names in the [Extra Server Variables] section of the wc.ini file. To add any server variables not provided use the following syntax:

    [Extra Server Variables]
    Var1=LOCAL_ADDR
    Var2=APPL_MD_PATH
    

    To find out about the server variables IIS exposes see MSDN online.

    Custom DLL Error Messages

    The Web Connection ISAPI extension can throw several errors internally. By default these errors generate error messages that are displayed as HTML generated to a simple template inside of the DLL. Since these errors never hit the Web Connection Visual FoxPro server, this means you can't change the error message directly. In some instances this may not be appropriate. While errors are rare and usually point to a problem in the Visual FoxPro server code, it's sometimes necessary to provide an error message that is suitable for end users rather than the technical message that the DLL pops up. So, to do this you can provide an override form for each error message via settings inside of wc.ini. The following entries and error messages are available for customization: Ini Entry [HTML PAGES] Conditions:

    If entries are left blank the default error messages are brought up generated by the DLL. You can assign files to each of these keys that identify the page that you would like to load. Pages are loaded from the specified file - pages should not exceed 16k in size.
    [HTML PAGES]
    DLLStatusHeaderText=Custom Web Connection Status DLL Text
    Exception=C:\westwind\wconnect\error.htm
    Maintenance=C:\westwind\wconnect\error.htm
    NoLoad=C:\westwind\wconnect\error.htm
    Busy=C:\westwind\wconnect\error.htm
    
    Timeout=
    PostBufferSize=
    TransmitFileFailure=
    
    The special DLLStatusHeaderText key allows you to override the DLL Status page header. The default value you see is Web Connection DLL Status. You can override this header with your own to remove references to Web Connection.

    Displaying Error information in custom pages
    This mechanism is very low tech, but you can also embed two special %s keys into the page to match the error message that the Web Connection request creates. Embed the %s string into the page and the first encountered will expand to the error header, the second to the error message.

    <html> <body> <h1>%s</h1> <hr> The following error occurred: %s </body> </html>

    Note that you can hide the first parameter with something like this:

    <html> <body> <!-- %s --> <h1>A request has timed out on the server</h1> <p> The following error occurred: %s </body> </html>

     



    The Application ini file and the wwServerConfig class


    The application INI file contains settings that the Web Connection server and your individual process modules need for operation. It can also contain custom settings that you want to store for your own application specific purposes.

    The server settings are required while the process module settings are optional and can be defined by you as you need them. By default Web Connection manages the INI file settings through a custom implementation of the wwServerConfig class (defined in wwServer.prg and subclasses from the wwConfig class) which dynamically manages the settings via an object that persists its properties into the INI file.

    The application INI file has the same name as the project. So the demo application is wcDemo and the ini file is wcDemo.ini. The Ini file looks like this:

    [Main]
    tempfilepath=c:\temp\
    template=wc_
    logtofile=On
    saverequestfiles=Off
    showrequestdata=Off
    showserverform=On
    showstatus=On
    usemts=Off
    scriptmode=3
    timerinterval=200
    adminemail=rstrahl@west-wind.com
    adminmailserver=mail.gorge.net
    adminsenderroremail=Off
    
    [wwdemo]
    datapath=d:\wwapps\wc3\wwDemo\
    htmlpagepath=d:\westwind\wconnect\
    
    [http]
    datapath=d:\wwapps\wc3\wwDemo\
    htmlpagepath=d:\westwind\wconnect\
    serverport=80
    adminaccount=rstrahl
    

    The [Main] section contains server settings. Any other sections like [wwdemo] and [http] map to process classes that you implement. You can add any custom values to these INI files as you see fit. Please see the wwServerConfig object for more detailed information on the individual keys.

    The wwConfig object

    The Ini file is driven through the wwConfig object, which is an object persistance class. Basically wwConfig can persist all of its members into XML, an INI file or a registry string. wwConfig is used to hold all the server configuration settings and a subclass of this object is automatically created for you in your mainline program like wcDemoMain.

    Skip to the bottom of wcDemoMain.prg to find this code:

    DEFINE CLASS wcDemoConfig as wwServerConfig
    
    owwDemo = .NULL.
    owwMaint = .NULL.
    oWebHits = .NULL.
    oHTTP = .NULL.
    
    FUNCTION Init
    
    THIS.owwDemo = CREATE("wwDemoConfig")
    THIS.owwMaint = CREATE("wwDemoConfig")
    THIS.oHTTP = CREATE("HTTPConfig")
    THIS.oWebHits = CREATE("WebHitsConfig")
    
    ENDFUNC
    
    ENDDEFINE
    
    DEFINE CLASS wwDemoConfig as RELATION
    
    cHTMLPagePath = "d:\westwind\wconnect\"
    cDATAPath = "d:\wwapps\wc3\wwDemo\"
    
    ENDDEFINE
    
    DEFINE CLASS httpConfig as RELATION
    
    cHTMLPagePath = "d:\westwind\wconnect\"
    cDATAPath = "d:\wwapps\wc3\wwDemo\"
    cServerPort = "80"
    cAdminAccount = "rstrahl"
    
    ENDDEFINE
    

    The main wcDemoConfig object is the 'server' config object which becomes accessible as Server.oConfig (it's based on wwServerConfig which you can find in wwServer.prg). It contains server start up settings like the temp path, templates, timeouts and so on that are a required part of the Web Connection server. You can add additional properties if you want them to be available on the server object.

    If you change a value in the INI file, the value is read on server startup and the class value is changed to the INI file value - if the INI value doesn't exist the default property value is used.

    Notice that each of the sub-process classes get a custom object that is attached to the main server config object. For example, oHTTP is simply a new object with properties that match your INI file settings you want to create. Every property you add becomes a key value.

    To add any other sections simply create another class with the properties you want to use and the wwConfig class will take care of the rest.

    Note that the New Project and New Process Wizards handle creating of the basic objects for you automatically. All you have to do is add your custom properties to persist in the INI file.

    Important: All properties you create should be created with a type prefix like cServerPort, cDataPath, nSeconds. The prefix is dropped when written out to the INI file. If you omit the prefix you'll run into truncated values in the INI file - it'll still work, but it sure will look funny.



    The wconnect.h file



    The wconnect.h file contains a large variety of settings that control how Web Connection works. wconnect.h is required by almost every code module in the framework. It's highly recommended that you include wconnect.h with your code as well.

    Tip:


    DO NOT CHANGE SETTINGS IN THIS FILE! This file will be updated every time Web Connection is updated so any changes you make here will be overridden. Instead you can make changes in wconnect_override.h as described in the
    Customizing wconnect.h settings topic.

    wconnect.h values

    The following settings are important or require occasional overriding in your wconnect.h file.


    #DEFINE DEBUGMODE .T.

    The DEBUGMODE switch is used to switch between development mode and deployment mode. When set to .T. all errors inside the framework or your Web code causes a FoxPro error and the code to stop running on the line of code the error occurred. When the flag is .F. all errors are handled through the framework's Error method handlers. Typically an error results in an error result page.

    #DEFINE SERVER_IN_DESKTOP	.F.

    Determines whether the Web Connection server runs inside of the VFP frame or on the Desktop when running in file based operation. For COM servers this switch is ignored - COM server don't need a server form and when they do use it it's always a desktop form.

    #DEFINE WWXML_USE_VFP_XMLTOCURSOR   .F.

    Determines whether wwXML uses VFP 7 and later's XMLTOCURSOR and CURSORTOXML to create and parse XML. If this flag is .T. in general wwXML will perform better.

    #DEFINE WWWC_FILTER_UNSAFECOMMANDS	.F.

    This flag determines whether wwRequest::Form() and QueryString() filter commands for VFP methods that might be dangerous. Blocks things like EVAL, EXECSCRIPT etc. to prevent execution from hijacking of user interput.

    #DEFINE DEFAULT_HTTP_VERSION	"1.0"
    #DEFINE DEFAULT_CONTENTTYPE_HEADER ;
        "HTTP/1.0 200 OK" + CR + ;
    	"Content-type: text/html" + CR

    These two settings are used to set the default HTTP header that Web Connection uses if you don't create a full custom header of your own. You can add any custom tags here, such as forced page expirations and they will be appended to every HTTP header that goes out. Note that if you make any changes to the header in your application code this default is not used.

    #DEFINE MAX_STRINGSIZE  			10000

    Web Connection in many cases creates HTTP output as strings which are passed back to the Web server. But it's actually much faster writing data out to a temporary file and then reading it back in rather than building strings by concatenation once strings get lengthy. The value above determines when Web Connection starts dumping strings to file rather than keep concatenating them. 10000 bytes is about the break even point where file processing becomes faster. Larger numbers mean less file buffering and larger memory buffers. Faster machines do better with bigger buffers. If you have a real old machine you might benefit by reducing the string size to 5000 (which was optimal for P2 machines at the time).

    #DEFINE WWC_SERVER				wwServer
    #DEFINE WWC_SERVERFORM 			wwServerForm
    #DEFINE WWC_SERVERFORM_VFPFRAME wwServerFormVFPFrame
    
    #DEFINE WWC_PROCESS         	wwProcess
    #DEFINE WWC_WEBSERVICE			wwWebService
    
    #DEFINE WWC_SESSION 			wwSession
    #DEFINE WWC_SQLSESSION 			wwSessionSQL
    
    #DEFINE WWC_REQUEST				wwRequest
    #DEFINE WWC_REQUESTASP			wwASPRequest
    
    #DEFINE WWC_RESPONSE			wwResponse
    #DEFINE WWC_RESPONSEFILE    	wwResponseFile
    #DEFINE WWC_RESPONSESTRING		wwResponseString
    #DEFINE WWC_RESPONSEASP			wwASPResponse
    #DEFINE WWC_RESPONSEBEHAVIOR 	wwResponseFileBehavior
    #DEFINE WWC_HTTPHEADER			wwHTTPHeader
    
    #DEFINE WWC_wwEval 				wwEval
    #DEFINE WWC_wwHTMLControl 		wwHTMLControl
    #DEFINE WWC_WWVFPSCRIPT     	wwVFPScript
    #DEFINE WWC_WWPDF				wwPDF
    #DEFINE WWC_WWSOAP				wwSOAP
    

    This group holds all of the framework classnames. Internally Web Connection never references any class names explicitly but instead creates classes using these #DEFINEs. This allows you to subclass the framework classes and replace the classes here with yours to get your functionality into the framework.


    #DEFINE WWC_LOAD_DYNAMICHTML_FORMRENDERING  .T.
    #DEFINE WWC_LOAD_WWSESSION 					.T.
    #DEFINE WWC_LOAD_WWBANNER 					.T.
    #DEFINE WWC_LOAD_WWDBFPOPUP 				.T.
    #DEFINE WWC_LOAD_WWIPSTUFF 					.T.
    #DEFINE WWC_LOAD_WWVFPSCRIPT 				.T.
    #DEFINE WWC_LOAD_WWSQL						.T.
    #DEFINE WWC_LOAD_WWPDF						.T.
    #DEFINE WWC_LOAD_WWXML						.T.  && Don't change! Required!
    #DEFINE WWC_LOAD_WWMSMQ						.F.
    #DEFINE WWC_LOAD_WWSOAP						.T.
    

    This set of flags is checked when Web Connection loads. Basically, these are flags that determine whether some of the support classes are loaded on startup. This is here mainly to keep down project size by excluding classes you don't use. Do not remove the wwXML component - it's required and here only for backwards compatibility.

    The remainder of settings in wconnect.h are system defines and values that are used internally in various classes.

    Additional useful flags:

    #DEFINE WWC_CACHE_TEMPLATES 0
    Determines whether templates called with Response.ExpandTemplate() are cached in a table rather than being read from disk each time. If you have applications that use lots of templates this approach may provide a significant performance boost. Number specifies the number of seconds that a template is cached - 0 means that no caching occurs.

    #DEFINE WWC_EXTENDED_LOGGING_FORMAT .F.
    This option when set to .T. causes additional information to be logged into the Web Connection Request log. When set, the POST data and Browser string get logged in addition to the querystring, script, and client IP address. Turning this option on can quickly generate a very large log file so please use this option with caution and if you do use frequently clean out your log!

    #DEFINE MAX_TABLE_CELLS 15000
    Determines the number of table cells that the wwShowCursor class can render in a single table before changing output format to a text based list. Since tables can get too large to comfortably render as tables this maximum can be applied.



    Customizing wconnect.h settings the easy way


    A new Web Connection installation tends to overwrite your wconnect.h file along with any custom settings you've made to it, so it's usually a good idea to store changed settings separately from wconnect.h. DO NOT CHANGE SETTINGS DIRECTLY IN WCONNECT.h!

    wconnect.h includes a reference to an override header file that you can use for this purpose with the following line:

    #IF FILE("WCONNECT_OVERRIDE.H")
    	#INCLUDE WCONNECT_OVERRIDE.H
    #ENDIF
    

    wconnect_override.h should then contain #UNDEFINE statements for all constants you want to change along with #DEFINE statements for the new values. You can do this as follows:

    #UNDEFINE DEBUGMODE
    #DEFINE DEBUGMODE 					.F.
    
    #UNDEFINE MAX_TABLE_CELLS
    #DEFINE MAX_TABLE_CELLS				20000
    
    #UNDEFINE WWC_CACHE_TEMPLATES
    #DEFINE WWC_CACHE_TEMPLATES			0
    
    #UNDEFINE VISUALWEBBUILDER
    #DEFINE VISUALWEBBUILDER 			.F.
    
    #UNDEFINE WWC_USE_SQL_SYSTEMFILES    
    #DEFINE WWC_USE_SQL_SYSTEMFILES     .F.
    
    #UNDEFINE WWSTORE_USE_SQL_TABLES      
    #DEFINE WWSTORE_USE_SQL_TABLES      .F.
    
    #UNDEFINE WWMSGBOARD_USE_SQL_TABLES      
    #DEFINE WWMSGBOARD_USE_SQL_TABLES      .F.
    

    When an upgrade rolls around Web Connection will overwrite your wconnect.h, but the settings in wconnect_override.h remain intact.


    The Web Connection HTML Administration Page


    Most administrative features of Web Connection are accessible through an HTML form that provides links to various system tasks.

    The admin page can be reached with http://localhost/wconnect/admin.asp and looks as follows:

    Adminstrative links break down into two groups:


    Note:
    The admin page contains a process list componenent using the WMI (Windows Management Instrumentation) component that displays a list of processes that match the filters. This code runs in the ASP portion of the document and can be customized to include custom processes to view and kill if necessary. The default is inetinfo and anything wc*.*. In order to view this components output you have to be logged in (IUSR_ doesn't have rights to it) and you have to be running Windows 2000 or Windows NT 4.0 SP4 or later. For Windows 98 or NT pre SP4 you can download the WMI components from the Microsoft Web site. If the component is not available or the authentication is not in place the error handler skips over the display code and nothing displays. In order to set up authentication, set NTFS permissions on the ADMIN.ASP page for an admin account.


    ISAPI DLL Maintainence requests


    The Web Connection wc.dll provides a number of built in functions for managing Web Connection servers. These maintainence functions are available only through the Web interface of an application and follow a very specific format.

    All of the maintainence requests are accessed by using a special syntax with the ISAPI DLL. For example to release all servers you would access:

    http://localhost/wconnect/wc.dll?_Maintain~Release

    You use _maintain to let the ISAPI DLL know that it's to expect a maintainence request. The second parameter then specify the operation to perform - in this case Release.

    Security
    Access to all maintainence functions of the DLL is controlled via Basic Authentication (on NT/2000 accounts) on the Web server. A special key in wc.ini AdminAccount determines which account has access to maintainence functions. This key can be blank to not check for Authentication, Any to allow any logged in account (ie. anything but the anonymous IUSR_ account) or a valid NT user account name. For more details see the wc.ini section under AdminAccount.

    Maintenance DLL Status Page
    To facilitate the process of the maintenance functions, a special link on the admin page called Show and Manage DLL settings allows access to most of the DLL maintenance functionality via an HTML interface. The page is accessed with wc.dll?_maintain~ShowStatus and looks like this:

    This page shows the current status of most of Web Connection's settings that are loaded from the wc.ini file at startup. You can use the Re-read configuration link to reload the settings from the INI file. This link is extremely useful for debugging Web Connection problems as it shows you the real settings that the Web Connection ISAPI is running right now.

    This page also allows you to switch between file based messaging and COM based messaging, put the server on temporary hold (Hold Requests) and lets you load and unload the currently running servers when running in COM mode. The COM server list shows the currently running servers and their status. Hits shows the cumulative number of hits against your Web Connection Server, Active requests shows the number of seconds the currently active request has been running if any (this will rarely show anything unless you have a long running request), and Cumulative shows the cumulative seconds that your server has spent processing requests since it was started.

    The _maintain ISAPI DLL request parameters
    The following table lists all of the maintainence features available inside of the ISAPI DLL. You access these by specifying wc.dll (or a script map thereof) and addressing it like this:

    wc.dll?_maintain~MaintRequest

    where MaintRequest is one of the parameters from the table below:

    ISAPI Command

    What it does

    COM

    File

    ShowStatus

    Displays information of the current settings of the DLL. For Automation servers this display also shows which servers are loaded and if they are currently busy. This link summarizes the most important settings in wc.ini and you should use it to make sure you have all expected settings correctly set.

    u

    u

    ShowStatusXml

    Displays the same information that ShowStatus displays but in XML format for remote checks of status.

    u

    u

    Load

    Loads all the specified Automation servers into memory from the DLL startup INI file.

    u

     

    Release

    Releases all Automation servers from memory.

    u

     

    StartEXE

    Starts an EXE specified in ExeFile in the DLL Ini file for file based messaging. The EXE is started in the System context so it will run invisibly when started from a Service. You can make the session visible by allowing the service to 'Interact with Desktop' in the Service manager. Use this only if you've crashed the server, or if you've switched from Automation to File based and you need to get one server started to manage additionals.

     

    You can also use the FileStartInstances key in wc.ini to force instances to start up when the DLL first loads simulating behavior of an NT service and OLE Automation with file based operation.

     

    u

    UpdateExe

    Updates the EXE file as specified by the INI file EXEFile and UpdateFile keys. With Automation the process is automatic: Servers are unload, the Hold RequestFlag is set and the EXE file is copied. The servers are then reloaded. With file based messaging you are responsible for unloading all running sessions first using the Session links (see next section). Once unloaded the update operation is identical.

    u

    u

    MaintMode

    Releases all but one of the Automation servers from memory. This is very useful for doing maintainence tasks that require exclusive access to tables when running more than 1 server instance for a particular application.

    u

     

    HoldRequests

    Forces the DLL to return 'Please wait...' message page for all users and unloading all COM servers if running in COM mode, essentially locking down the Web Connection server except for users logged in under the AdminAccount list.  This can be used to update files on the server including Automation Server executables. This flag is a toggle that switches between on/off modes.

    u

    u

    MaintHoldRequests

    Works just like HoldRequests except it also loads a single COM Server instance so you can perform maintenance operations using the Web Connection server. When through use HoldRequests to toggle the HoldRequest flag back to off.

    u

     

    RecoverDeadLock

    Manually overrides the HoldRequests flag in case you set it and can't access the maintainence page when not logged in. This setting also resets the spin and lock counts shown in the DLL Status page in the rare event that these value stay above 0 for extended periods.

    u

     

    ReadSetupIni

    Re-reads the settings from the DLL startup INI file.

    u

    u

    SetFileMechanism

    Switch the DLL from Automation to File based message processing.

    u

     

    SetAutoMechanism

    Switch the DLL from File based to Automation message processing.

     

    u

    KeepAlive

    This request fires all of the Automation servers currently loaded. You can use this link to keep alive servers from Web Monitor by hitting it every 2 minutes. IIS unloads low usage threads after approximately 8 minutes and this allows keeping servers running. Servers are hit only if idle for more than 1 minute.

    u

     

     

    Online Code Updates

    Updating code online without shutting down the Web server is possible with Web Connection when operating under COM with the Web Connection Pool Manager. The idea is that you upload your new executable to the server into a temporary location and then use an update link to actually copy the new file over the existing version. It's not quite so easy however, as you have to make sure all other sessions of the EXE are terminated before updating the file. This is easy to do with COM operation, but quite messy and potentially risky with file based operation.

    The first thing you have to do is set up the wc.ini keys EXEFile and UpdateExe. The EXEfile should point at your executable file, while the UpdateExe should point at the new EXE file that you will upload to the Web server. It's very important that the Admin account that you use when you click the link (and is set up as the AdminAccount key in wc.ini) has access rights to read the file from the source directory and write access in the target.

    To actually update the files:

    COM Messaging
    With Automation the process is actually automated. Assuming you have the files in the correct location all you have to do is click Update Code on the maintainence page and all servers are shut down for you and the files copied. Once complete the servers are restarted for you. You use the UpdateExe and ExeFile keys in the wc.ini file to swap out files in real time.

    File Based Messaging
    File based is more difficult as the ISAPI DLL has no control over the server instances. Instead you have to first kill all server instances by using wc.dll?wwMaint~Sessions~KillUnconditional until ALL sessions have been killed from the server. You can then click the Update EXE link. Once the files have been updated you now have no sessions running. In order to start a session you have run the Emergency Restart Exe file link on the maintainence page. This will start up the EXE specified in the EXEFile key of wc.ini. Note that this server will run invisibly on the NT desktop – it will be visible only in task manager (or if you have your Web server set up in the Service Manager to 'Interact with Desktop').

     



    The wwMaint Maintainance requests


    Update Web Connection Web Resources

    When you update Web Connection on your server you need to replace a number of files in the various Web directories. To facilitate this process Web Connection can automatically copy resources to the Web directory for you using a Url like the following:

    http://localhost/yourVirtual/WebResourceUpdate.wc?wwMaint~WebResourceUpdate

    In order for this to work however you have to ensure that the following directories exist in your Web Connection installation (whereever the the EXE lives):

    This operation updates:

    Web Connection Request Log

    By default the Web Connection server automatically logs each request that hits the server. The wwServer's LogEntry() method automatically logs the following into RequestLog.DBF (customizable with the wwServer::cLogFile property) located in the Web Connection startup directory:

    In addition you can set the following in WCONNECT_OVERRIDE.H:

    #UNDEFINE WWC_EXTENDED_LOGGING_FORMAT #DEFINE WWC_EXTENDED_LOGGING_FORMAT .T.

    To force Web Connection to use an extended logging format. In this mode it also logs:

    Note that using the extended format collects significantly more data and should be used cautiously - primarily for debugging purposes if you have problems or need to track down a malicious client.

    Viewing the Log

    To show the contents of the Log from an HTML page do:

    wc.dll?wwMaint~ShowLog
    Note that you'll only see the last 400 records. This is done to keep the request short and not overload the browser's HTML table display. If you have the Office Web Components installed on the server Web Connection will also generate a chart of traffic over the last 25 hours.

    You can override the number of records to show by providing a LogSize parameter:
    wc.dll?wwMaint~ShowLog~&LogSize=1000

    By default the log displays in normal, non-extended mode. To show extended log information including browser and POST data use:

    wc.dll?wwMaint~ShowLog~&ExtendedLog=True&LogSize=500

    wc.dll?wwMaint~LogSummary
    You can also view a summary of hits tallied request. This works only for numbered parameters (wwDemo~TestPage for example). You can group by each parameter number. So, application would be 1, request would be 2 etc. Currently there's no support for scripts - script pages will not be counted.

    wc.dll?wwMaint~ClearLog~NOBACKUP
    In order to clear the log only one session can be running as exclusive access to the log file is required. When the log is cleared only the data up to the current day is cleared - today's data always stays in the file. When clearing the data in the log is appended to LOGBACK.DBF for archive purposes. Ideally, you want to run a daily log and keep the archive for reporting purposes. This file can get big, and clearing can get slow because of the APPENDs to the LOGBACK file. If you don't want a backup use the NOBACKUP parameter.

    Displaying the ISAPI DLL Error Log

    In addition to viewing the Web Connection Server server generated DBF file log you can also view the ISAPI DLL log of errors by using the Show DLL Errors link. DLL show any problems that may be occurring with the ISAPI DLL from minor notifications to serious exceptions. For description of common errors and what they mean see the previous section on wc.dll settings and commands. You can also clear the log from this link. The error log file lives in <wctemppath>\wcerrors.txt.

    Management of Multiple Sessions for File Based Messaging

    Since Visual FoxPro is a single threaded application (actually, it isn't - several threads run, but only one thread is available to application code) the only way to provide support for simultaneous requests is to run multiple instances of Visual FoxPro and Web Connection. Running multiple sessions can provide your application a better feel although the overall response time is probably not improved much by running multiple sessions. Rather multiple sessions should be used to balance request loads so that longer requests are not hogging the CPU and tying up the pipe while short requests have to wait for long requests to complete. On a single processor box two simultaneous sessions provide good performance without noticable slowdown - more sessions will see requests slowing down considerably when running simultaneously. On a dual processor box the cutoff point comes at 4-5 sessions. If you need more sessions the next step is to move off to different machines and run them across the network (see previous section).

    Running multiple sessions can provide the scalability needed to run simultaneous requests, but it also causes some maintenance headaches. If you're running multiple sessions it becomes crucial that you can shut down sessions so you can perform maintenance tasks that might require exclusive access to your data files. How often this occurs depends on your site, but if you are running a site that's running offline from another database application with data being shuffled back and forth maintenance tasks occur frequently.

    Session management for Automation servers is handled via the DLL Maintain functions described in the previous section. For file based messaging the DLL has no control over the standalone VFP EXE files and thus can't manage them. The wwMaint module handles some of the chores by using the RUN and QUIT commands to manage running sessions using its Sessions method.

    Note: In order to start a new session of Visual FoxPro development you need to set the following key in the wcmain.ini file in the Web Connection root:
    [wwMaint]
    RestartExePath=c:\vfp\vfp.exe -t wcmain.prg
    or
    ; RestartExePath=c:\wconnect\wcdemo.exe

    The URL syntax for the available functionality is:


    wc.dll?wwMaint~Sessions~KILL
    Kills a session that's running by executing a QUIT from the server that's hit.

    wc.dll?wwMaint~Sessions~START
    Starts a new session. Note that at least one session must already be running for this to work since Web Connection implements this mechanism. Web Connection actually issues a RUN command to start another session. If all sessions are dead you can use the next link to start a new instance.

    wc.dll?_maintain~StartExe
    This URL will restart a Web Connection server as a standalone EXE file from the ISAPI extension. The ISAPI extension reads the location of the EXE file from wc.ini in the EXEFILE key.
    NOTE: the server will start invisibly (no UI). Make sure you test starting servers in this fashion. Note that servers started in this fashion act differently than servers started from the Desktop. Servers are started using the SYSTEM account so set your permissions accordingly.

    Note:
    File based server sessions are not recommended. If you need to manage multiple Web Connection instances it's highly recommended you use COM messaging, which allows full control over loading and unloading of servers including status information and crash recovery.

    Update Code Online

    This option allows you to copy a new EXE file from a specified location. This feature is most useful with COM operation where you can upload a file to a pre-specified location and then cause a hotswap of the Server Exe files. How you get the file up, is up to you. You can FTP the file or use the BLD_<YOURAPP>.PRG that gets created with a Web Connection project which FTPs programmatically. Just make sure you configure the FTP paths and login names.

    You can also use the Upload File: input box to upload with HTTP directly from this page. The File gets copied to the server based on the UpdateFile key in wc.ini. Another key ExeFile specifies the target file of a Code update.

    When you click the button the Web Connection ISAPI DLL will try to delete the ExeFile first. This ensures that the file can be unloaded and is not running on the server. It then copies the UpdateFile to the ExeFile location. In COM mode, COM servers are unloaded before the delete and copy operation occurs. In file mode, no special action occurs - you're responsible for unloading servers on the Web Server using the Admin commands or other mechanism.

    Warning:
    This operation is based entirely on the keys defined in wc.ini. If you have either of these keys incorrectly set it's possible that a running file is deleted and not copied back. Make 100% sure you test this before making critical live site code updates!

    Server Statistics

    There's also a simple link to display basic statistics about the Web server itself. Check out the following link:

    wc.dll?wwMaint~ServerStatus
    wc.dll?wwMaint~ShowStatus
    These pages report settings for the server. The first requires IE 4.0 and allows editing the same server settings you can interactively change on the server's status form. The latter only displays stats about the running application.

    Warning! It's highly recommended that you add Authentication to the requests discussed here or else risk the consequences of people messing with your site. If you check out the wwMaint.prg file Process method you'll find some commented out code that enables security based on a password defined in a #DEFINE that you set up. In addition it's a good idea to set security on the Maintain.htm page that contains all the links to these requests.

    wc.dll?wwMaint~EditConfig
    It's also possible to edit the Web Connection Configuration files remotely. This link displays a page with the server's config files for the DLL Ini file (wc.ini) or the Web Connection Server (wcMain.ini or whatever your main program is). The files are displayed in textboxes and can be edited and updated via a edit box and button.

    Note: Updates work only if the Web Connection server resides on the same box as the Web Server. This request will not work if the INI files reside on remote machines due to the paths that are returned by the Web Server for the DLL and INI files. You can however modify the code in wwMaint.prg to hardcode the appropriate server path.

    The actual update links for the INI files are:

    wc.dll?wwMaint~EditConfig~SaveDLLIni
    wc.dll?wwMaint~EditConfig~SaveWCIni

    If you're running COM/Automation you can use the [Automation Servers] section to add and remove servers from the poo


    Web Connection ISAPI Server Settings Web Page


    The ShowStatus page is accessible via:

    http://localhost/wconnect/wc.dll?_maintain~ShowStatus

    (or wc.dll in your own application path).

    This page provides status information about the current status of your running Web Connection Application. This page reads the live status of the ISAPI dll that's currently running on the server and echos back the common values. The page looks like this:

    The status on this page is very useful for debugging any problems you might be having with your server as well as telling you most of the important settings of the server. The values are as follows:

    DLL Version
    This is the version of the Web Connection ISAPI dll (wc.dll). This version id is compiled into the DLL and echoed back. Note that this is the official way to check the WWWC version, even though there's a version resource compiled into the DLL as well. The version resource may sometimes be out of date while this value is always up to date.

    Current Ini File
    This is the location of the configuration file the wc.dll is currently using to read values from. This INI file should be in the same directory as your wc.dll. If you're seeing a different directory here than you are expecting it's possible that IIS is mismapped or a scriptmap is pointing to the wrong copy of wc.dll!

    File Template
    This is the template prefix used for file based messaging. This value is retrieved from wc.ini at startup.

    Temp File Path
    The location of the temp file path where file based messaging is depositing message files. This is also the directory where the error log sits.

    Admin Account
    This is the account or accounts that is required to access admin functionality in Web Connection. If this value is blank you should immediately assign this key a value in the wc.ini file.

    Current Login
    Shows you your current login name. This value should read your current user account or SYSTEM. If it reads IUSR_ or IWAM_ you have a security problem as this page would be openly accessible.

    Messaging Mechanism
    Determines whether COM or File based Messaging is used at the moment.

    Post Mechanism
    Determines how messages are posted. Supports UrlEncoded which is the default and INI based which is based on the outdated Windows CGI specification.

    COM Server Loading
    Determines how COM Servers are accessed. Sequentially (normal in wc.ini) uses the default order of servers, so the first available server is used. Round Robin attempts to go to the next server in the round. Sequential makes it easier to see how loaded the server are, but Round Robin spreads the load more evenly among the active servers.

    ISAPI ECB Object
    Determines whether a reference to the ISAPI COM object is passed to a COM Server. The ECB object allows access to writing out content immediately to the output stream.

    Hold Requests
    Determines whether the server is currently Running or On Hold. The Switch link allows to swap operation.

    Lock, Recursion and Spin Count
    These settings are internal debug settings that determine the state of Critical Sections in the ISAPI DLL. The lock count should be -1 (no locks) or a low number which shows the number of threads waiting on locks. Recursion and Spin are related and these values should be 0.

    The COM Server Grid
    The grid

    ISAPI Settngs by XML

    All of the status in

    <?xml version="1.0"?>
    <webconnectionconfig>
    <version>Web Connection 4.35 (32 servers)</version>
    <inifile>d:\westwind\wconnect\wc.INI</inifile>
    <template>wc_</template>
    <scripttimeout>120</scripttimeout>
    <temppath>d:\temp\wc\</temppath>
    <adminaccount>rstrahl</adminaccount>
    <messagingmechanism>COM</messagingmechanism>
    <comserverloading>Round Robin</comserverloading>
    <comkeepalive>Force</comkeepalive>
    <holdrequests>RUNNING</holdrequests>
    <lockcount>-1</lockcount>
    <recursioncount>0</recursioncount>
    <spincount>0</spincount>
    <comservers>
    <server>
    <progid>wcDemo.wcDemoServer</progid>
    <hits>774</hits>
    <currentseconds>0</currentseconds>
    <cumulativeseconds>31</cumulativeseconds>
    <started>17:40 - 4/5/2003</started>
    <processid>1708</processid>
    <serverid>0</serverid>
    <terminationurl>/wconnect/wc.dll?_maintain~KILL~0</terminationurl>
    </server>
    <server>
    <progid>wcDemo.wcDemoServer</progid>
    <hits>774</hits>
    <currentseconds>0</currentseconds>
    <cumulativeseconds>38</cumulativeseconds>
    <started>17:40 - 4/5/2003</started>
    <processid>2264</processid>
    <serverid>1</serverid>
    <terminationurl>/wconnect/wc.dll?_maintain~KILL~1</terminationurl>
    </server>
    </comservers>
    </webco

    Web Connection ISAPI Error log (wcErrors.txt)


    Web Connection is an ISAPI DLL that runs inside of IIS. Since it's a system level component errors don't show on the console, so all errors are logged into a file that you can examine. The file is called wcErrors.txt and goes into the Web Connection temporary file path. Note IUSR_ must have Full Access in this directory in order to create the log and write to it.

    The maintenance page contains a Show DLL Errors link you can use to view the log.

    There are a number of errors that are generated in this log that are not critical - they're reported for your information and debugging purposes. Harmless errors include:

    All servers are busy
    If your server pool is too busy to take further requests in the allocated timeout period you'll see this error message. This is not really an error, but merely a notification that your server is maxed out.

    Forced Release of Server
    If you're running Automation servers are unloaded by releasing the COM references which release the object when the reference count goes to 0. In some instances related to the multithreaded nature that Web Connection objects are called from the reference counts alone do not allow the objects to unload. In these cases the Web Connection DLL forces the objects to unload by terminating the process explicitly. This is normal for servers that have been running for long periods of time, especially if you use the KeepAlive flag on your server. This information is logged merely for informational purposes.

    Web Connection Request timed out
    This means that a request took longer than the allotted timeout period as specified in wc.ini. This either means you had a very slow request or possibly your server hung on an error or some unexpected UI operation. If the Web server is busy the offending server was probably unloaded automatically and reloaded. This error is also common when working with file based operation and testing development installations. Anytime you hit a WC link and don't have the server running one of these messages will eventually be generated when the request times out.

    GetIDsOfNames or QueryInterface Failed due to thread Time Out
    Web Connection Server has been unloaded (RPC Server Unavailable)
    These errors relate to crashed or killed Web Connection servers that unloaded while they were idle. When Web Connection tries to access these server these errors occur. These errors are actually trapped and the server are automatically reloaded at that time so that the client never sees an error in most cases.

    The errors above are all benign and are nothing to worry about even if you see a lot of them. There are also permission errors related to maintenance tasks which also are not crucial. Errors to watch out for are:

    Exception Errors
    Unhandled exception errors are caused by system errors (like heap corruption, corrupted memory) and believe it or not code errors <s>. If you have isolated exception errors that occur very rarely it's nothing to be worried about - IIS has been known to get unstable and memory/COM subsystem errors are something that will occur from time to time. In my experience these errors are very few indeed and most don't manifest themselves as serious to require even a Web server shutdown/restart.

    If you run into consistent exception errors make a note of the error message (the nature of exceptions makes this necessarily vague but some will report a little info) and try to get as much context as you can to report the issue. What is the server doing, what types of requests are you running when the problems occurred etc.


    Updating and Migrating



    This section kindly created and provided by Randy Pearson, Cycla Corporation

    Note:
    upgrades from version 3.x to 4.x don't require any special upgrade steps

    Keeping your applications current when Web Connection changes is an important activity. This section covers two very different areas:

    Updating consists of the process of moving your Web Connection 3.x applications from one version to the next.

    Migration is a one-time process to move version 2.x applications to Web Connection 3.x. Once migrated, all future version releases are handled by updating.

    These two subjects are covered in detail as follows:


    Updating an existing application on a live Web server


    When updating a new version of Web Connection to your server you need to update a number of files. A Web Connection version update will require a restart of the Web Service to update the loaded binary dlls.

    The best way to handle this is to copy the following files to the server from your local Web Connection installation to your Exe's application directory:

    This only copies the updated files to your 'installation' path. To update the files in your web application you need to perform an additional step. You can do this manually or use an automated link.

    Update Web Connection Web Resources with a Web Link

    Before you perform the automated Web update ensure that you have copied the required files to the Web Connection installation path. Once these files have been copied to the server you can run a link like the following on your server:

    http://localhost/yourVirtual/WebResourceUpdate.wc?wwMaint~WebResourceUpdate

    This operation updates:

    Note that westwind.css is not explicitly overridden unless you click on the link that is presented. This is done because frequently developers change westwind.css for their applications themselves and we don't want to override your changes.

    Note that this routine updates your wc.dll and webconnectionmodule.dll files one of which is likely going to be loaded in memory at the time of the update. This means that even if there's no error updating the active DLLs is not actually updated and running until you restart the Web service.

    Manual Copy

    You can also opt to manually update your Web files.

    Deploying with a new version of Web Connection




    Debugging Requests


    Debugging is one of the strengths of the Web Connection framework when compared to a script or pure COM based architecture because the framework allows you to operate your Web backend server inside of the VFP development environment.

    Standard VFP Debugging

    The key to debugging your application is that if you're running File Based operation you can the server inside of VFP and you can simply use standard VFP Debugging features like SET STEP ON and using the VFP debugger to step through your request code.

    The figure above demonstrates that you can set breakpoints and step through live Web requests in the VFP debugger.

    Error Handling

    Web Connection can run in DEBUGMODE which is a flag configurable in wconnect.h. When DEBUGMODE is .T. no error handling is in effect, which causes your program to simply stop on the offending line of code. During development this is a huge timesaver in helping you locate and fix errors quickly at their source.

    Come deployment time you can set DEBUGMODE to .F. and all of Web Connection's Error handlers kick in to provide bullet proof trapping of errors so that your server doesn't crash or hang. With error handling enabled an error message is displayed (calling the wwProcess::ErrorMsg method to display the error) and logging the error into the RequestLog file. This message can be customized by overriding the ErrorMsg method for display purposes and the Error method for custom error handling.

    The DEBUGMODE flag is contained in wconnect.h. It's a constant in a header file and in order to change this flag you have to recompile your server application. For more info See the Error Handling topic.

    Checking Web Request and Response Input and Output

    An extremely useful mechanism for debugging problems that deal with incoming request data such as HTML Form Variables, Client IP, Authentication, Cookies and so on as well as checking for output issues is to use the Status Form's Save Request file option. When this checkbox is checked the last HTML result and the Request input are saved into your temp path specified in the Server setup window as TEMP.HTM and TEMP.INI respectively. You can view this output by clicking on the Display button. The pop up menu lets you view the last request as well as the last HTML output.

    Use this feature to check output and make sure that you are returning exactly what you're expecting to return. One important thing to look for is the HTTP header of the resulting HTML document. The Request data file input can be very useful in seeing whether all the form variables from a form are retrieved. This lets you see exactly what the server is sending you on each request and lets you access some of the keys that are not exposed directly by the wwRequest class methods. Other things you can use the request data for is verifying that input contains expected cookies and authentication tokens which would otherwise be difficult to debug and troubleshoot.

    Make sure you use this feature whenever you see inconsistent inputs in your application. The request input data comes directly from the Web server and passed straight through the ISAPI extension - if it's not there the Web server didn't send it!

    Using the wwProcess::lShowRequestData flag
    You can use the wwProcess::lShowRequestData flag to force the full request data to be appended to the end of a HTTP Response (actually it work correctly only with HTML content because any other content type might get corrupted by the data at the end). You can do this either at the request level by setting it in the method you're calling or at the process level.

    Method level:

    Function DoSomeThing
    
    THIS.lShowRequestData = .T.
    Response.HTMLHeader("Test Page")
    ...
    Response.HTMLFooter()
    ENDFUNC
    

    Process level:

    ************************************************************************
    *PROCEDURE wwDemo
    ******************
    LPARAMETER loServer
    LOCAL loProcess 
    
    #INCLUDE WCONNECT.H
    
    loProcess=CREATE("wwDemo",loServer)
    loProcess.lShowRequestData = .T.   && loServer.lShowRequestData
    loProcess.Process()
    
    *** Class definition follows here
    

    Note that you can optionally read the loServer.lShowRequestData flag which is loaded from the application's INI file using the ShowRequestData flag.

    Error Handling


    Web Connection provides several built in facilities for error handling via TRY/CATCH exception handlers. In order to efficiently debug Web Connection applications a debug mode is provided that bypasses the error handlers and allow you to stop code operation at the source of an error.

    The Server.lDebugMode flag

    The wwServer class - referenced usually with Server in Process level code - exposes an lDebugMode flag that determines how errors are handled in the framework.

    The DebugMode flag can be set in the following places:


    Changes made to the debug mode flag are dynamic and take effect immediately.

    Essentially when debug mode is .T. errors stop at the source of the error with Web Connection removing it's core error handlers from the error chain, so that you can fail at the point of error and fix the problem any way that makes sense. If lDebugMode is set to .F. Web Connection's error handlers kick in and errors are handled and routed to predefined error handlers of the framework.

    Default error handling behavior is provided and you can override this behavior. Error handlers exist in:

    Note that the Server.lDebugMode flag is new for Web Connection 5.0 and superceeds the #DEFINE DEBUGMODE flag. The DEBUGMODE flag from WCONNECT.H is no longer used by the Web Connection framework.

    Using Error Method instead to capture more detailed Error Information

    Notice that you can also still use Web Connection 4.x style Error() methods by using the
    wwProcess::lUseErrorMethodErrorHandling property and implementing a custom Error() method. The benefit of this approach is that the error is fired in the current CallStack context whereas the TRY/CATCH handler returns control to OnError() only in the RouteRequest() context. The latter doesn't make it possible to capture current variables and ACALLSTACK() info of the actual error. Using an error method provides all the current information of the application at the time of the error.

    The documentation on wwProcess::lUseErrorMethodErrorHandling provides more information on how to configure this more complicated mechanism that requires use of #DEBUGMODE compilation flag as in Version 4.x.

    Error Trapping in Template and Script Pages

    When debugging embedded FoxPro expressions in HTML code things get a little more tricky. Expression errors are always handled regardless of the Server.lDebugMode flag. This makes it more difficult to debug script and template page errors.

    Templates simply evaluate expressions and code and if an error occurs they embed an error message into the document instead of the result that should have been there:

    < % Error: ErrorExpression  % >

    Expressions are run through the wwEval object and errors are trapped through this mechanism. However, no indication is given as to what caused the error at this point. This method works well for catching errors without doing damage to the system even when evaluating unknown code, but it's not terribly easy to see what the problem is especially if the failure occurs in a code block.

    Hanging the Server?

    When error handling is enabled it's next to impossible to crash the wwServer process. There are a few exceptions though: Any operation that brings up a dialog will hang the server. The most annoying one can be a File Open dialog if a file is not found! To avoid File Open dialogs use UNCs rather than drive mappings that might disappear, check for files before opening them or before using them in a SQL statement.


    Checking HTML output and the INI File input

    An extremely useful mechanism for debugging HTTP Request problems is to use the Status Form's Save Request file option, which saves the last request run. Or you can use the wwProcess::lShowRequestData flag to display the content right on the bottom of your HTML page. When this checkbox is checked the last HTML result and the request input are saved into your temp path specified in the Server setup window as TEMP.HTM and TEMP.INI respectively. You can view this output by clicking on the Display button. The pop up menu lets you view the last request as well as the last HTML output.

    Use this feature to check output and make sure that you are returning exactly what you're expecting to return. One important thing to look for is the HTTP header of the resulting HTML document. The INI file input can be very useful in seeing whether all the form variables from a form are retrieved. This lets you see exactly what the server is sending you on each request and lets you access some of the keys that are not exposed directly by the wwCGI class methods.

    Logging and Tracking Requests


    Web Connection supports two kinds of request Logging:

    Both mechanisms log a unique RequestId which identifies each request.

    The RequestId ServerVariable and the ISAPI Extension


    The Web Connection ISAPI DLL creates a unique request ID for every request that is fired against the DLL. This ID is used for logging and is also passed to your Web Connection FoxPro Server via a REQUESTID ServerVariable which you can retrieve like this:

    lcID = Request.ServerVariables("REQUESTID")

    You can use this ID to track requests through the entire Web Connection processing cycle. The DLL uses this id for all logging purposes and you can write out the request ID as part of any custom logging your application does.

    Web Connection also uses this ID to safeguard any possible corruption in requests and provide request tracking for logging purposes from your own application and the ISAPI extension. To enable this checking (which creates a tiny bit of overhead) set the following key in wc.ini:

    ValidateRequestId=1

    The ISAPI Dll sends the ID to your server, which then returns a RequestId header as part of the response. The ISAPI DLL then checks for the header, extracts the ID and validates it against the original ID generated for the request before the output is sent back to the client. If the IDs don't match - and this should never happen - the response is not sent to the client and an error message is displayed instead. This guarantees beyond any possible doubt that there might be any possible request mixups.

    Why this ID

    There have been a few instances in the past where customers have had problems with their hardware or their server configuration that has caused problems with requests returning output for the wrong request to a different user. In the history of IIS there were several known bugs in IIS 3 and IIS 5 that would cause COM server corruption on rare occasions that have resulted in COM servers returning output to another IIS connection. This situation is very, very rare and has never been tracked to Web Connection itself, but when this situation arises there's usually a lot of finger pointing, and having extra request ID tracking can lay to rest any fears that the wrong data is returned from your FoxPro Web Connection Server implementation.

    To ensure our customers of the security of the Web Connection framework, Web Connection uses an explicit check for the Request ID after the result has been returned from the FoxPro server to ensure that the request integrety is fully intact. If the RequestId HTTP header is returned from the Fox server this header is checked and compared against the original request id to ensure the ids match. If for whatever reason they don't an error page is displayed instead of displaying potentially incorrect data.

    Starting with Web Connection 5.10 the header is automatically added with any request that uses the wwPageResponse class or the wwHTTPHeader class for explicit or implicit headers with earlier Request class versions if the ValidateRequest key is enabled.

    On failure an error message will display. On IIS 6 or later if the problem should occur Web Connection will shut down the worker process and restart it automatically as the cause of this sort of mismatch is almost certainly due to COM object corruption which cannot be resolved without a restart.




    Fox Server Application Logging


    Web Connection includes a Request log that logs every request that is processed through your FoxPro server application. The logging is managed through the wwServer class and occurs at the end of the request.

    The Log is turned on via a switch in <YourServer>.ini file with:

    [Main]
    Logtofile=On
    

    which is mapped to the wwServer.lLogToFile. When .t. at the end of the request wwServer.LogRequest is called to log the request into the file specified in wwServer.cLogFile which defaults to wwRequestLog.

    Logging can occur in two formats - simple and extended.

    Simple Logging log the current script and querysting, timestamp, remote IP, duration and memory used. The extended logging also adds the browser user agent and POST data if any. Note that using extended logging can become very verbose quickly so make sure you clear out these files frequently or copy them off.

    You can view the log either as a DBF file directly or you can view it from the Server Status form. Click Status and Browse Log.

    Logging Requests in ISAPI and your Fox Server


    Web Connection supports several logging mechanisms including the Web Connection Server log and starting with version 5.0 and optional ISAPI logging mechanism. The ISAPI Log is a new feature that can be used for debugging requests and mapping ISAPI requests to requests running through your FoxPro server.

    The ISAPI request creates a unique RequestId at the beginning of the request which consists of an ISAPI threadID and a GUID. This value is passed through the entire request and to your FoxPro server application where it becomes available as Request.ServerVariables("REQUESTID"). The FoxPro Server also picks up this value and logs it in the request log if logging is enabled.

    The ISAPI extension can also log every request into it by setting the wc.ini LogDetail key to 1. If set to 1 every request is logged when it starts and when it exits the DLL. Here's a sample of what this looks like:

    2006.02.6 02:50:02:359 - 2428_D8152FCDDCE5 - Request Started - /wconnect/wc.dll?_maintain~ReadSetupIni - 122
    2006.02.6 02:50:02:359 - 2428_D8152FCDDCE5 - Request Completed (0) - /wconnect/wc.dll?_maintain~ReadSetupIni - 3
    2006.02.6 03:12:47:421 - 2428_5B5EC48F1980 - Request Started - /wconnect/wc.dll?wwDemo~ShowImage - 122
    2006.02.6 03:12:47:531 - 2428_5B5EC48F1980 - Request Completed (109) - /wconnect/wc.dll?wwDemo~ShowImage - 3
    2006.02.6 03:12:58:218 - 4188_ABAD0B86AE31 - Request Started - /wconnect/weblog/default.blog? - 122
    2006.02.6 03:12:59:750 - 4188_ABAD0B86AE31 - Request Completed (1531) - /wconnect/weblog/default.blog? - 3
    2006.02.6 03:13:06:671 - 4188_96F7C8213A96 - Request Started - /wconnect/weblog/default.blog? - 122
    2006.02.6 03:13:07:437 - 4188_96F7C8213A96 - Request Completed (765) - /wconnect/weblog/default.blog? - 3
    2006.02.6 03:13:08:734 - 4188_14A1C723EAC7 - Request Started - /wconnect/weblog/admin/default.blog? - 122
    2006.02.6 03:13:09:171 - 4188_14A1C723EAC7 - Request Completed (438) - /wconnect/weblog/admin/default.blog? - 3

    Each entry logs a timestamp, the request Id and a message - in this case start and stop messages. Stop messages include a tick count in parenthesis which is the time it took to process the request.

    End messages can sometimes not get logged if a hard exception occurs. In that case you should see a different kind of message for a specific id logged instead.

    Scaling Web Connection Servers across the network



    It's possible to run Web Connection over a network to move the CPU load of processing Request requests off the machine that is running the HTTP Web server. By offloading the database processing off the Web server CPU overhead on the Web server is minimal allowing the server to process more simultaneous requests without loss of response. Using this approach it's possible to scale your VFP processing server's to any Request request load simply by adding VFP sessions and/or machines on the network, without adversely affecting Web server performance.

    Web Connection's internal mechanisms to do this include:

    Standard scalability mechanisms available include:

    Remember, large scale application require a fair amount of hardware and network know how - make sure you have staff or support available that understands scalability, infrastructure, security and deployment issues in large scale environments.
    For more information on scalability scenarios see:

    Building Large Scale Web applications with Visual FoxPro.
    Building a Web Farm with Windows 2000's Network Load Balancing service

    My recommendation for scalability for large application is with the latter approach - it's more reliable and provides full redundancy as well as more efficiently balancing resources across machines than Web Connection built-in mechanisms. It's also an accepted standard for scaling Web applications in a tiered environment.

    DCOM is a good choice for a quick fix, but as you'll see below configuration can be daunting especially if you don't have a firm grasp on how COM and the distributed architecture works. More info is available in the Large Scale Web apps article described above.

    The following sections delve into the details of configuring Web Connection's internal mechanisms for moving out over the network.

    Dealing with Network Pathing

    The following applies only if you use DBF files rather than a SQL backend.

    When building a server that will run on multiple machines simultaneously it's important to set up your pathing correctly so that a single EXE file can be run on every one of these machines and still point at common data accessed on a network path. The following tips are suggestions only - you may configure your servers differently to be able to share data, but the following steps have worked well for me here:

    These files are optional and can be turned off. For SQL applications these files can be ported into a SQL database with some modifications to the wwSession and wwServer classes.

    The above suggestions apply to any network servers built regardless of whether you use Automation or File Based messaging.


    File based Messaging on Multiple Machines

    Setting up Web Connection to work over a network link involves setting it up so that it looks for the temporary message files generated by requests across the network and mapping the filenames properly to locally addressable paths, which is handled internally by Web Connection. Your setup is mainly concerned with identifying the remote path that Web Connection will scan for incoming request message files.

    No special setup is required on the Web server or with wc.dll/exe/ini. The server operates as before placing the files onto its local drive. The VFP Web Connection server on the other hand is now looking across the network, polling for incoming requests on the Web server's drive. So for the remote server it's TEMPFILEPATH is going to be z:\temp\ for example.

    All filenames returned by the wwRequest class methods are automatically mapped to this from the Web server's path (ie. c:\temp\) to the network path (ie. z:\temp\). The actual filenames will be Web server pathed - the translation is necessary to properly translate the path into the network path, so that no code changes are required internally when moving a Request process off the local machine onto the network.


    Using DCOM to remote Servers across the network

    Running Automation servers across the network is a bit trickier. Please note that these complications are not an issue with Web Connection as much as they are NT's DCOM and security issues that apply to porting any Automation objects for simultaneous use on multiple machines. The main issue here is that the remote server must allow the Web server's anonymous user account to access the remote COM object on the processing server.

    As with file based messaging make sure all your application paths are pointing across the network at the appropriate remote files if data is shared. Establish a Network startup path. This means any common data files (such as the Web Connection Log or Session tables) as well as any other file based resources such as INI files that you might want to access.

    In order to run Automation remotely you run Distributed COM (DCOM) across the network. The steps to remote a server rather complex to set up.



    Tuning Web Connection for high volume operations


    Web Connection is capable of running applications that are very high volume and the framework has been run in live applications that serviced in excess of 5 million requests daily on a single dual processor machine (PIII 700s). Operation for this kind of load or even bigger loads requires tuning and stress testing to find the best combinations for the number of servers to load.

    The first step for high volume applications should be to tune the application so it is optimized to perform as fast and efficiently as possible. For a number of hints on how to tune your Web application, the Web server, the machine, database operations and code check out:

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

    This article describes a number of things to optimize performance.

    The next step, which is also described in the above article is tune your application for the hardware it is running under. This process involves stress testing the Web application with a Web Stress tool such as Microsoft's Web Application Stress Tool (WAST). You can find out more about stress testing and how to use this tool at the following URL:

    http://www.west-wind.com/presentations/webstress/webstress.htm

    This article describes the process of stress testing as well as general concepts of what it takes to balance machine resources against the running application. The concepts in the article are fairly general, but apply to a Web Connection application as well.

    Understand that performance is a relative thing and depends entirely on your application. For example, in simple test scenerios we've run Web Connection with over 350 requests/second, but this is an unrealistic expectation for an application that actually performs something useful. The above is sort of a raw throughput number, which will drastically decrease once you do actual work in your code. So a typical transaction based business application with short (.10 second or less) requests will generally run somewhere around 50-100 requests a second. Your data, business and output generation logic will determine exactly how scalable your application is. Remember that long running requests are the killer of scalability!

    For Web Connection applications specifically, the key factor is balancing CPU usage against the number of instance that are run. As a general rule for high performance applications use these settings as a starting point:



    Configuring Web Connection for use with SQL Server


    Web Connection by default works with Visual FoxPro data, but since the Visual FoxPro language is used you can of course access SQL Server directly from your Web Connection using either Remote Views or SQL Passthrough. You can do this easily from within the wwProcess class methods that handle your application code.

    Support for logging and Sessions to SQL Server

    However, internally Web Connection also accesses VFP tables for logging and the wwSession functionality.With version 3.30 and later you can now set up logging and session activity to be directed to SQL Server by using the
    Create SQL Server Tables Wizard. The Wizard can set up the log and session tables for you and points you to additional manual configuration steps required for this setup in the help file.

    wwSQL class wraps SQL Passthrough for error handling and connection management

    Web Connection also includes a wwSQL class which is a wrapper around SQL Passthrough that works for any ODBC connection. This class manages SQL connections in form of an object with methods like Connect and Execute to manage running SQL passthrough operations. The wrapper provides error handling for requests (using Web Connection's familiar lError and cErrorMsg properties) and connection management and re-connection on errors that occur on the connection.

    Typical Web applications talking to a SQL backend will want to maintain a permanent connection to the SQL backend. This is easily accomplished by using a wwSQL object instance and attaching it to the wwServer object. The logging and Session features do this using a special oSQL property which is available, but you can also use AddProperty to create a custom object:

    FUNCTION SetServerEnvironment ... THIS.AddProperty("oSQLConn",CREATEOBJECT("wwSQL")) THIS.oSQLConn.Connect("DSN=Pubs;uid=sa") ... ENDFUNC

    Once this object is initialized it can be easily accessed in the application's wwProcess methods:

    FUNCTION SQLTest Server.oSQLConn.cSQLCursor = "TResult" Server.oSQLConn.Execute("Select * from Authors") IF Server.loSQLConn.Error THIS.ErrorMsg("SQL Error occurred",Server.oSQLConn.cErrorMsg) RETURN ENDIF *** Display an HTML table from SQL result Response.HTMLHeader("SQL Demo") Response.ShowCursor() Response.HTMLFooter()

    If you are sending logging and session data to your database as well you can use the Server.oSQL (wwProcess::oServer::oSQL) property to keep everything on a single connection. If you have your own SQL objects and would like have the Web Connection logging and session features share the connection to your existing database you can assign the oSQL.nSQLHandle and oSQL.cConnectString properties once the connection has been made. This will allow Web Connection to reuse your existing connection without requiring an additional connection.

    wwSQL is a very simple object - it doesn't help with proper design of a SQL application, but I would recommend using it or a wrapper like it to handle SQL connection errors which is messy to deal with in mainline code. wwSQL is fairly lightweight and supports all that SQLPassthrough provides.


    Core Step By Step Guide


    The step by step guide takes you through creating a new project and then creating several request handler methods that respond to Web requests. This guide is an introduction to some of the features of Web Connection and uses primarily FoxPro data language commands directly in code to access data and display it.

    After you've worked through this guide we recommend you check out the Step By Step with the wwBusiness Object guide, which uses the wwBusiness object to handle data access. Besides using the business object and showing how to utilize a business object in a Web based application, it also shows a few more advanced techniques for generating HTML interfaces in your applications.

    Web Control Framework Note:
    Web Connection 5.0 features a new Web Control Framework which provides a different mechanism for creating pages and processing requests. If this control based approach is more appealing to you, you should take the Web Control Framework Walk Through. We still recommend looking at this Step by Step Guide first unless you are already familiar with Web Connection as this walkthough demonstrates many core featues, like using the Request and Response objects, Sessions and providing a general overview of the framework that also applies to Web Control Framework applications.


    Step 1 - Creating a new project


    The first step to create a brand new application is by creating a new project for your application. To do so start the West Wind Management Console from the VFP command window:

    Click on the Create New Project button to create a new Visual FoxPro project that includes the base Web Connection framework classes and the startup code.

    Note to shareware users:
    The Shareware version cannot build a Visual FoxPro project or EXE file due to the fact that the shareware version is precompiled. The full version comes with source code and allows to create a 'real' project. However, you can still run the application by simply running the PRG files as mentioned below.

    In the Wizard that pops up start by naming the project and the main process class. The project name should be the filename of the project you want to create. Use a single string value without spaces for this setting. In this case it's WebDemo.

    The filled out page now looks like this:

    A Process is the actual request handler class, where you will write your logic to handle Web requests. A single project can contain several Process classes. In this case we're going to create a new project and add our first Process class called WebDemo. You can later add additional Process classes using the New Process Wizard.

    To make it real obvious which pieces I'm talking about in this demo I'll name the project WebDemo and the process WebProcess.

    I also need to specify the Web server I'm planning on using, in this case IIS 6. Note that it's important that you pick the correct Web Server, especially in the case of IIS 6. IIS 6 is heavily locked down and this Wizard sets up a number of settings to make sure that your new project can run.

    Step 2 of the Wizard now asks you to set up the Web directory for this application by creating a virtual directory, copying the Web Connection ISAPI connector to it and configuring the application settings for the new application.

    The filled out dialog looks like this:

    Note that the Web path can be anywhere but should point somewhere into your Web directory structure. Above the default \inetpub\wwwroot\ basepath is chosen with the virtual created off that. The Temp file path is open to your choice, but make sure that the directory allows FULL access user rights to the IUSR_ account or Everyone so that the ISAPI extension can read and write files to this directory.

    Step 3 lets you configure a script map for your application. A script map is a file extension that maps to the Web Connection ISAPI DLL that makes it easier to reference requests. Once mapped any request to a 'file' or URL with this extensions maps to the Web Connection DLL.

    Start with the extension you want to use for the script map. WP is good for example of WebProcess. The path to the DLL is automatically filled and defaults to the BIN directory of your new virtual directory. You can change the path, but generally you'll want to leave this setting as is.

    Go to the last page of the Wizard and click on the finish button to let the Wizard generate the project for you now. When you're done the Wizard will popup the project for you to review.

    Very Important
    A Project file will not be built when you run CONSOLE.EXE from Explorer as the VFP runtime cannot create a project. You have to run this process from within Visual FoxPro to create a new project. If you run the EXE directly you can manually create the project after the fact by adding <yourapp>Main.prg to a new project called <myApp>.pjx and recompiling all files from the project.

    The project may not compile completely at this time due to some files being in use. You may see a few errors for files that are used by the currently running program especially files like File2Var, IsDir, OpenExclusive etc. These files are located in wwUtils.prg and will be pulled when the project is recompiled. After the Wizard completes exit the Management Console and issue BUILD EXE <project> FROM <project> from the VFP Command Window to recompile your project or reopen and compile the project.

    Shareware version users
    The shareware version does not build a project file as it is precompiled and can't add the Web Connection source files to the projects. Therefore you can't build an EXE. However you can still run the project successfully by running the main PRG file for the application: DO <yourproject>Main.prg. In fact, this is how we usually debug our applications anyway - there's no need to build an EXE until you're ready to deploy the application.

    As the project is being generated each of the files is pulled into the project and compiled. When the compilation step is completed a dialog will pop up asking whether you want to register the component with DCOMCNFG. If you're running on Windows NT choose yes, otherwise no. This operation sets the configuration of the COM object to Interactive User impersonation to allow the COM object to be visible on the desktop.


    Moving your new Project

    The New Project Wizard creates all of the generated files in the current directory which is the Web Connection install directory. If you're only building one or a few separate applications this is probably fine, but if you create a lot of applications, you probably want to break out the project into a separate directory.

    You can move the files to another location if you choose as long as you make sure that you can still access them from the WC root via the FoxPro Path (SET PATH TO). For example, I typically stick new projects into a subdirectory of the WC root so for example I have a \webdemo directory off the WC root. It's up to you what you move - I tend to leave the project and main file (WebDemoMain.prg) in the WC root so I can start the project easily without having to set the path first. But I move the Process class and any data files and other support files the application uses into a separate directory (I do this for separate process classes as well) so that each of these applications is isolated and self-maintainable. The only files that stay in the root are the project related files and the main program file, so that the file can be started up simply by typing DO <yourproject>main.prg, instead of having to prefix a path first.

    It's very important that if you do move the application you add a SET PATH TO (<moved path>) into your SetServerProperties method of the server so that the running server application will be able to find your moved program files and any data you may also put in the subdirectories. SetServerProperties can be found in your generated <yourproject>main.prg file and contains application specific setting such as SET PATH, SET CLASSLIB and so on that affect the environment that the application runs in. Here you should add your new moved application path.


    Step 2 - Testing the installation


    Let's take a quick look at what the Wizard actually generated. The Visual FoxPro project looks like this:

    The Wizard pulls all of the Web Connection framework files into the project. Most importantly though it also created two new source files customized to our new application.

    As you may have noticed by now the servers follow a simple naming convention. Server related classes all start out with the project name and get additional text appended: WebDemoServer, WebDemoConfig, WebDemoMain for the mainline PRG file. The Wizard also generates a build file called Bld_WebDemo.prg that recompiles the project and re-registers the COM object with DCOM as needed.

    WebProcess.prg generates the base class again with the same name as the PRG file. The class also contains a dummy method called TestPage, which we are going to use in a second to test the server's operation.

    To start the server go back into Visual FoxPro were we left off and:

    DO WebDemoMain.prg 

    Note that you could also run the compiled WebDemo.EXE file, but here I'm choosing to run the source files directly as we're going to be editing and adding code in a second. Once you run the code you should see the server pop up.

    The server window is a display that shows requests running through it. The large area of the format will show requests as they come through with the time that each request took in seconds. Here I ran a maintainence test request to show what the server looks like while it's in process.

    The server form also gives you access to the Status configuration form by clicking on the Status button:


    The settings in this window should look familar - the settings match the settings I made in the first step when running the New Project Wizard. The most important values are the Startup directory which should be the current directory, the temp directory and the template setting, which are used for the file based messaging that we are currently using to run our server.

    Whenever the server is run as a standalone EXE or from within the VFP environment the server uses file based messaging. We can also run the server as a COM object in which case the Web server will instantiate the server itself.

    The Web Configuration

    Ok, so now our Web Connection server is ready to receive requests and waiting for our first incoming request.

    The Wizard also generated a new virtual directory on the Web server with script rights set as well as a scriptmap for the wp extension and points it at the wc.dll.

    All of these settings allow you now to access a URL on your Web server as follows:

    http://localhost/WebDemo/default.htm

    The resulting page from this request is just a test page that the Wizard sets up for you that allows you test the server's operation. It looks like this:

    Click on the HelloWorld link which takes you to the following URL:

    http://localhost/WebDemo/wc.dll?WebProcess~TestPage

    You should get a response page that basically tells you that you got there.

    You can actually access that URL a number of different ways because we chose to create a script map in the Wizard setup. The script map we added is for the .wp extension, which now allows us to access requests as follows:

    http://localhost/TestPage.wp

    The script map allows you to access .wp anywhere in the Web space without having to have an explicit file that matches the URL. There's no file called helloworld.wp in the server's root directory, but the server passes the request on to the Web Connection DLL, which in turn passes it forward to your Web Connection server to process. Here the .wp extension triggers the request to get routed to that same request class as the previous URL that was passed and we end up actually performing the exact same code in the process class.

    Why is this useful? It frees you from hardcoding paths to a fixed DLL file, and it allows you to have short URLs that don't contain any routing information on the querystring. Also as we'll see later, the script maps can be used to actually execute HTML scripts containing Visual FoxPro code.


    Step 3 - Adding our own Request Method



    So how does it work this far? The URLs we looked at in Step 2 are actually telling your Web Connection server that the request wants to call the HelloWorld method of WebProcess class. The two parameters following the ? on the URL, known as the QueryString, are used for routing the request to the method in your newly created class. Remember, the Wizard generated this stub test method so we can easily test the class before adding our own code as we'll do in a sec.

    The process class is the one that actually handles the incoming request. The Wizard generated this class, which is essentially a template to which we can add new methods, for us. The generated class looks like this. Note the HelloWorld method:

    ************************************************************************ *PROCEDURE WebProcess **************************** LPARAMETER loServer LOCAL loProcess PRIVATE Request, Response, Server, Session, Process STORE .null. TO Request, Response, Server, Session, Process #INCLUDE WCONNECT.H loProcess=CREATE("WebProcess",loServer) loProcess.lShowRequestData = loServer.lShowRequestData IF VARTYPE(loProcess)#"O" *** All we can do is return... WAIT WINDOW NOWAIT "Unable to create Process object..." RETURN .F. ENDIF *** Call the Process Method that handles the request loProcess.Process() RETURN ************************************************************* DEFINE CLASS WebProcess AS WWC_PROCESS ************************************************************* cResponseClass = [wwPageResponse40] && Original generated: [WWC_PAGERESPONSE] ********************************************************************* FUNCTION TestPage() ************************ THIS.StandardPage("Hello World from the WebProcess process",; "If you got here, everything should be working fine.<p>" + ; "Time: <b>" + TIME()+ "</b>") ENDFUNC * EOF WebProcess::TestPage ENDDEFINE

    I actually stripped out a couple of things that aren't important right now. Basically what we have here, is our request class with a HelloWorld method that's called when the request hits. Every method that you create can be accessed through the Web page by using either the Method.wp or wc.dll?WebProcess~Method syntax.

    Web Connection Version Notice
    These samples use the older classic way of doing things in Web Connection which are more low level. These samples include a few Response method functions that are no longer available by default in the wwPageResponse class. To make sure all the demos work correctly add the following at the top of your generated Process class:

    *** Use Classic Response object functions for this old class cResponseClass = "wwPageResponse40"

    This covers methods Response.FormTextbox, Response.FormHeader etc. which no longer existing with the wwPageResponse class.

    Adding a new request handler method

    So, to add a new request all we have to do is add a method to this class, so let's give that a shot. Open up WebProcess.prg jump to the bottom of the file just before the ENDDEFINE and add the following code. This request runs a query against a VFP table and displays the data back as HTML:

    ************************************************************************ * WebDemo :: CustomerList **************************************** FUNCTION CustomerList() *** Run a static query SELECT company, careof as Name, address, phone ; FROM wwDemo\TT_CUST ; ORDER BY Company ; INTO CURSOR TQuery *** Create HTTP Header, HEAD section, HTML/BODY tags and header text Response.HTMLHeader("Customer Data Accces") Response.Write("This demo displays data from a customer file.<p>") loSC = CREATEOBJECT("wwShowCursor") loSC.ShowCursor() *** Write the output from the ShowCursor Object to the Web Response.Write( loSC.GetOutput() ) *** Generate </body></html> Response.HTMLFooter() RETURN

    Then run:

    http://localhost/CustomerList.wp
    http://localhost/WebDemo/wc.dll?WebProcess~CustomerList

    Make sure that the Web Connection server is running as before. When you do this you'll get a result like this:

    Pretty cool, right? With a couple of lines of code, we've run a request on the server and have displayed the result of a Web query, and we really haven't written any HTML specific code. Note that you can however, write raw HTML code very easily, simply by using the Response.Write() method.

    Let's look a little close at what that code does. We start with a plain VFP SQL query that generates a cursor. Next we start to build an HTML document using code. The Response object is used to handle all HTTP/HTML output to the Web server. The Write() method is the lowest level method that sends output directly to the output stream. Above I use the Write method to write out a simple text string that's displayed in the document:

    Response.Write("This demo displays data from a customer file.<p>")

    All the other methods used in this request are higher level methods that generate compound HTML with single method calls. HTMLHeader() is a routine that creates a typical header for an HTML document. It generates a default HTTP Header, an HTML <HEAD> section containing a title, the <HTML><BODY> tags and some HTML that writes out a string in big text as the display header of the HTML document.

    The most interesting code of the request above is the ShowCursor object and calls, which are responsible for taking the currently open cursor or table and rendering it into an HTML document. It basically runs a SCAN loop through the data in TQuery (the open cursor) and generates an HTML table row for every record it finds. The result is the HTML table output above. One line of code can be very productive in generating a lot of HTML - ShowCursor() is great for quickly prototyping data output and with some query manipulation can be used for almost all list display purpose. More on this in a moment.

    HTMLFooter() then closes out the HTML document by putting end </body></html> tags at the bottom.

    It's important to understand at this point that Web Connection offers a number of ways to generate output. This example uses a handful of high level methods to perform all the output generation. As we'll see later on, you can generate HTML in a number of other ways as well.

    The next step will be to add user input to your request so the request becomes truly dynamic.

    Step 4 - Adding simple User Input with QueryStrings


    The demo from the previous step is cool because it take very little code. But it's also very limited because it's all static. We're running a static SQL statement that's returning all records. It would be nice to actually be able to specify a 'parameter' to search for, right.

    Using parameters via the QueryString

    Ok, let's change the code above as follows to allow us to specify a 'parameter' via the QueryString.

    *** Retrieve the Company from the QueryString
    lcCompany = Request.QueryString("Company")
    
    *** Run a static query - this time with the Company parameter
    SELECT company, careof as Name, address, phone ;
       FROM wwDemo\TT_CUST ;
       WHERE UPPER(Company) = UPPER(lcCompany) ;   
       ORDER BY Company ;
       INTO CURSOR TQuery
    

    Now try the following URLs in your browser:

    http://localhost/WebDemo/CustomerList.wp?Company=A
    http://localhost/WebDemo/wc.dll?WebProcess~CustomerList~&Company=A

    Note that we're passing the parameter Company on the URL. The syntax is slightly different whether you use the full path or the script map syntax. In the full syntax the ~ character is used to separate positional parameters and an & is used to separate named parameters. Note the ~& to signal the end of the positional parameters and moving into named parameters.

    In both cases the result from this request is now only the A's of the customer list. We now have a limited form of user input - we can simply change the URL to pass the Company A down to your request method to run. You can add this into URLs like I did above, or you can have users type this stuff directly into the browser.

    The QueryString() method retrieves parameters from the query string and it can do so both for postional parameters and named parameters:

    lnMethod = Request.QueryString(1)
    lcCompany = Request.QueryString("Company")

    You can pass parameters either way. Multiple named parameters must be separated by &'s. For example:

    CustomerList.wp?Company=Brim+Healthcare&Action=Delete

    Here two named parameters are passed Company and Action. BTW, notice that the first parameter looks a little weird. Instead of a space it has a + sign instead. This is because a query string must be UrlEncoded. UrlEncoding replaces any non keyboard characters with encoded characters. Spaces turn to + and any non-keyboard characters are turned into hex values like %0D for CHR(13) for example.

    Note that when you need to embed URL information into HTML output you can use the UrlEncode() method (in wwUtils.prg) to turn a string into a URLEncoded string:

    lcValue = UrlEncode(lcValue)

    to encode a URL so it is properly formatted when clicked on.

    The QueryString is useful for 'parameterized' queries - you will utilize querystring parameters primarily for embedded links that get generated by code. They're a great programmatic tool to send users to specific links. Usually a querystring parameter will be a primary key or other identifier that uniquely identifies what you want to lookup.

    Posting the data

    But for user interaction querystrings are a bit clumsy. After all you wouldn't want to have your visitors to the Web Site key in their search parameters on querystring, right? Instead you probably want to present users with fields to input the data into. Although you can get data from HTML fields returned on the querystring, in general data from HTML Forms are 'POST'ed to the Web Server. POST data doesn't go on the querystring, but is instead sent to the server in a special HTTP buffer in an encoded format that the browser generates for you.

    Using HTML forms then is a mutli-step process:

    1. Setting up the HTML form
    2. Having the user fill out the form and submit it
    3. Capturing the Form data and do something with it

    Depending on the situation, this process can be handled by multiple pages or a single one. In the simple example, of providing a single Company Search box we're going to use a single Web Connection request method to both generate the form and also retrieve the value.

    Let's modify the CustomerList method one more time by adding the following to the top:

    ************************************************************************ * WebDemo :: CustomerList **************************************** FUNCTION CustomerList() lcCompany = Request.QUeryString("Company") IF EMPTY(lcCompany) lcCompany = Request.Form("txtCompany") ENDIF *** Run a static query - this time with the Company parameter SELECT company, careof as Name, address, phone ; FROM wwDemo\TT_CUST ; WHERE UPPER(Company) = UPPER(lcCompany) ; ORDER BY Company ; INTO CURSOR TQuery Response.HTMLHeader("CustomerList") TEXT TO lcHtml NOSHOW TEXTMERGE <form action="CustomerList.wp" method="POST"> Company Name: <input name="txtCompany" value=""> <input type="Submit" name="btnSubmit" value="Search"> </form> ENDTEXT Response.Write(lcHtml) *** Generate the Cursor HTML Display loSC = CREATEOBJECT("wwShowCursor") loSC.lAlternateRows = .T. loSC.ShowCursor() *** Write the HTML into the HTTP stream Response.Write( loSC.GetOutput() ) Response.HTMLFooter() RETURN


    This code generates the following output that now lets you filter your list:

    Take a look at the Form code. First notice that we are 'posting back' to the same page that we're coming from. We started on CustomerList.wp and we're going back to it when we post back. If you look at the TEXTMERGE block you can see that the lcCompany value is expanded into the value field of the textbox, which forces the value to be displayed.

    This is an important point about HTML - it doesn't automatically retain its value, so something has to explicitly set the values of controls when the page returns. Note that the Web Control Framework manages these semantics for you automatically - but if you're writing low level code like this you are responsible for making the HTML work using raw HTML generation code in all its gory details.



    Step 5 - Drilling down into the customer list


    Ok, now that we know how to display a basic list, let's snazz it up a little bit by adding some hyperlinks to it so we can drill down into the data.

    Change the CustomerList query to:

    *** Run a static query - this time with the Company parameter SELECT HREF([ShowCustomer.wp?ID=] +TRANSFORM(pk),Company) as Company,; careof as Customer_Name ; from tt_cust ; WHERE UPPER(company) = UPPER(lcCompany) ; INTO CURSOR TQuery ; ORDER BY Company

    This makes the list look a little nicer by displaying less information and more importantly adds hypelinks for each of the customer entries. Note that this essentially embeds a hyperlink into the query - the HREF() function outputs an <a href="url">text</a> tag as output. You can create any kind of formatted string that contains HTML. In this case it's a hyperlink.

    Each hyperlink then should link to display each of the customers and their customer detail. In order to do this I use a SQL statement that embeds HTML HREF links directly into the query output. The key thing about the generated HREF is the ID= querystring value to which I assign the customer's ID, which in this case is the PK.

    Showing the Customer entry

    To display the customer we need a new method ShowCustomer in the WebProcess class. This method should look like this:

    FUNCTION ShowCustomer lnPK = VAL( Request.QUeryString("ID") ) SELECT Company,careof as Name,Phone, Email, BillRate, Entered ; from TT_CUST ; WHERE PK = lnPK ; INTO CURSOR TQUery Response.HTMLHeader("Customer Info for: " + TQUery.Company ) loSc = CREATEOBJECT("wwShowCursor") loSC.cTableWidth = "450" loSC.ShowRecord() Response.Write( loSC.GetOutput() ) Response.Write('<hr>[<a href="customerlist.wp">Customer List</a>]') Response.HTMLFOOTER() ENDFUNC

    The output from this wwShowCursor generated record display looks like this:

    This output is very basic, but once again it gives you a quick way to display data or to even capture the HTML and stick it into an HTML editor for fix-up and potential reuse later with templates. More on this in a bit. Notice again that a SQL statement is used here to filter the display data - ShowRecord() uses the fields of the record in the current cursor to determine what gets displayed.

    You could also use a LOCATE on the actual table (tt_cust) and then apply the cRecordFieldList property like this:

    SELECT TT_CUST LOCATE FOR PK = lnPK loSc = CREATEOBJECT("wwShowCursor") loSC.cTableRecordFieldList = "Company,Careof,BillRate" loSC.ShowRecord() Response.Write( loSc.GetOutput() )

    There are field list properties for each of the various display functions (ShowCursor, ShowRecord, EditRecord). But be aware that this property causes another query to be run to retrieve the data with a different field list, so there's additional overhead here. If you can format the cursor on your own you will always get better performance and usually better formatting capabilities.

    Step 6 - Capturing HTML Form Data


    Ok, now that we know how to look at data, let's capture it too. As mentioned in Step 4, you can POST data from an HTML form and capture the data using the Request.Form input.

    To demonstrate let's set up a very simple HTML customer entry form and submit that form to save a new record. Let's start by creating the HTML page for entering a few fields (there are couple of other changes in here such as usage of an image and a style sheet to make the display a little nicer:

    <html> <head> <title>Customer display</title> <link rel="stylesheet" type="text/css" href="westwind.css"> </head> <body style="font: normal normal 10pt Verdana" topmargin="0" leftmargin="0"> <h2> <img border="0" src="../images/newwave.jpg" align="left" width="158" height="864"><br> <font color="#800000">Customer Detail</font></h2> <hr> <form method="POST" action="AddCustomer.wp"> <input type="Submit" value="Save Customer" name="btnSubmit"><p> <table bgcolor="#EEEEEE" cellpadding="5" border="1" style="border: solid 2px Darkblue;"> <tr> <td valign="TOP" class="blockheader"> <b>Company:</b></td> <td> <input type="text" name="Company" style="width: 300px"></td> </tr> <tr> <td valign="TOP" class="blockheader"> <b>Name:</b></td> <td> <input type="text" name="Careof" style="width: 300px"></td> </tr> <tr> <td valign="TOP" class="blockheader"> <b>Address:</b></td> <td> <textarea name="Addres" style="width: 300px;height:80px"></textarea></td> </tr> <tr> <td valign="TOP" class="blockheader"> <b>Phone:</b></td> <td> <input type="text" name="phone" style="width: 300px"></td> </tr> </table> </form> <hr> </body> </html>

    which looks like this:

    As you can see in the example above the HTML form consists of the <form> tag which tells the form which link to run when the user submits the form and a bunch of <INPUT> and <TEXTAREA> fields that make up the data fields that the user types data into. The data is POSTed to the server when the user clicks on the Add button.

    Capturing the input

    The ACTION in the <form> tag determines which link receives the request. On the server side we can now set up a request that handles the input from this form and can save the data to the database.

    ********************************************************************* FUNCTION AddCustomer ******************** lcCompany = Request.Form("company") lcCareof = Request.Form("careof") lcPhone = Request.Form("phone") IF EMPTY(lcCompany) or EMPTY(lcPhone) THIS.ErrorMsg("Incomplete input","Company name and phone number are required.") RETURN ENDIF *** you'd have to do dupe checking here... INSERT INTO TT_CUST (custno,company, careof, phone) VALUES ; (SYS(2015),lcCompany,lcCareOf,lcPhone) THIS.StandardPage("New Customer Saved",; "The record has been stored into the database.") ENDFUNC

    To retrieve values submitted you simply use the Request.Form() method with the name of the form variable as defined on the HTML page to retrieve the content. In this example, the HTML Form field names happen to match the database field names (a good idea if you have control over this), but the field name is whatever the NAME= tag of the INPUT fields on the HTML form are set to.

    Tip:
    To quickly generate an HTML for all the fields in a VFP table, you can use the wwShowCursor object's EditRecord method as follows:

    >USE TT_CUST && Table to work with SET FIELDS TO Custno,Company,CareOf, Phone && Limit fields oSC = CREATE("wwShowCursor") oSC.EditRecord() && Gen the HTML SET FIELDS TO Response.Write("<form action='AddCustomer.wp' method='POST'>") *** Display the form - Getoutput returns HTML as a string ShowHTML( oSC.GetOutput() ) Response.Write("</form>")

    You still have to add the <form> wrapper and any submit buttons but the hard part is done. The above will generate the data from the current record. To get a blank form jump to the end of the file with LOCATE FOR .F..


    This is obviously an overly simple example that doesn't take into account error checking or even providing all the fields for editing here, but it gives you an idea of how forms and user input are handled. You can obviously use HTML form fields for many more things such as asking the user for query parameters or other information that you can then use in your code to perform business operations on. Request.Form() provides you with user input just as a VFP form field would in a standalone application.

    Step 7 - Dynamic Editing with Templates


    In the previous example I showed you how to create an HTML form and capture input directly. This is alright, except that in the previous example the form was completely static and stored in a .htm file. This works fine for blank forms you want to display, but is not so nice if you need to display dynamic data in the form.

    Enter HTML Templates in Web Connection. Templates provide a mechanism for storing HTML content externally. So rather than writing HTML output in code we can embed content such as expressions (fields, variables, properties, function and method calls, UDF()s etc) into the templates. Let me demonstrate this point with two examples.

    Let's start with a very simple example by once again redoing the customer list to support for editing records. To do this we'll change the SQL statement to add another column that has the Edit link. I'll also use a template to display the base page, into which I will then embed the wwShowCursor generated HTML list of customers:

    FUNCTION CustomerList() lcCompany = Request.QUeryString("Company") IF EMPTY(lcCompany) lcCompany = Request.Form("Company") ENDIF *** Run a static query - this time with the Company parameter SELECT [<a href="ShowCustomer.wp?ID=] +TRANSFORM(pk) + [">] + Company + [</a>] as Company,; careof as Customer_Name, ; [<a href="EditCustomer.wp?ID=] +TRANSFORM(pk) + [">Edit</a>] as Action ; from tt_cust ; WHERE UPPER(company) = UPPER(lcCompany) ; INTO CURSOR TQuery ; ORDER BY Company loSC = CREATEOBJECT("wwShowCursor",Response) loSC.lAlternateRows = .T. loSC.cExtraTableTags = [style="font:normal normal 8pt Tahoma"] loSC.ShowCursor() *** Vars in template must be PRIVATE - they cannot be LOCAL!!! * PRIVATE pcCompany, pcCustomerList && not really required pcCompany = lcCompany pcCustomerList = loSC.GetOutput() *** This will cause rendering of a CustomerList.wp script file Response.ExpandTemplate( Request.GetPhysicalPath() ) RETURN ENDFUNC

    Note that this time the company field is a dynamic HREF link to EditCustomer.wp and we're passing that request a parameter of our customer number that we want to edit. When you run this it should look like this:

    Where is the HTML coming from here? It's not being generated in code, but rather comes from template HTML page that lives in the Web directory (it can live anywhere however). Response.ExpandTemplate() basically loads this template page and merges any expressions that are in scope into it. ExpandTemplate finds the template via a OS path like d:\inetput\wwwroot\webdemo\CustomerList.wp, which in this case is represented by Request.GetPhysicalPath(). GetPhysicalPath translates the current URL into a physical path which is the same as the one just shown. This works well as long as the Process method and external page name are the same which is not always the case especially if you use pages that are used in multiple places.

    You can also do this:

    Response.ExpandTemplate(Server.oConfig.oWebProcess.cHTMLPagePath + "CustomerList.wp")

    which uses Web Connection's configuration settings from the INI file for the currently active Process class. oWebProcess is the config object for the project we created here and it's always 'o' plus the name of the Process class. Each of the properties of the class exist in the application's INI file in the WebProcess section in this case. HTMLPath will be set to d:\inetpub\wwwroot\webprocess\ and this value is retrieved from the Config object.

    Hint:
    At this point you'd normally create your own templates. But for this example, you can cheat and copy the templates from the html/wwDevregistry directory into your WebDemo project and work with these tempaltes to start with. They may require a little adjustment for some variables but overall they should be close.

    So, once we know where to place the template how do we edit it? Any way you like to edit HTML. Use your favorite HTML editor or if you choose use Notepad or even the VFP editor. I like to use FrontPage for visual pages. Here's what this page looks like:

    Notice that you can't see the customer list - that's because the customer list is embedded as an expression:

    ... Header stuff above
    <h2>Customer List</h2>
    <hr>
    <form action="CustomerList.wp" method="POST" >
    Company Name: 
    <INPUT TYPE="INPUT" NAME="Company" VALUE="<%= pcCompany %>" SIZE="20">
    <INPUT TYPE="SUBMIT" NAME="btnSubmit" VALUE="Search" SIZE="20"></form>
    <%=  pcCustomerList %>
    <p> 
    ... footer stuff below

    The key to make this template work are the use of the <%= Expression %> tags. Here I'm simply embedding two variables pcCompany for the value of the search textbox and the actual HTML string of the customer list embedded into this page. Sweet and simple, yes? You now can visually design your HTML layout and generate the dynamic pieces into - a perfect way to isolate your HTML generation and Fox code cleanly.

    Editing the data

    This is a very basic example of template usage - it becomes much more useful once you start editing data. The following code shows the EditCustomer method that handles both display of the customer data as well as posting the data back to the server for editing:

    FUNCTION EditCustomer()
    
    lnPK = VAL(  Request.QUeryString("ID") )
    
    *** Are we posting back?
    IF !EMPTY(Request.Form("btnSubmit"))
       IF !USED("TT_CUST")
          USE TT_CUST IN 0
       ENDIF
       SELECT TT_Cust
       
       LOCATE FOR pk = lnPK
       IF !FOUND()
          THIS.ErrorMsg("Customer not available","Get it right, man!")
          RETURN
       ENDIF
       
       REPLACE Company WITH Request.Form("Company"),;
               Careof WITH Request.Form("Name"),;
               Phone WITH Request.Form("Phone"),;
               Email WITH REquest.Form("Email")
    
       THIS.ShowCustomer(lnPK)
       RETURN
    ENDIF
    
    *** Show Entry
    SELECT Company,careof,Phone, Address,pk ;
        from TT_CUST ;
       WHERE PK = lnPK ;
       INTO CURSOR TQUery 
    
    SCATTER NAME poQuery MEMO
    
    Response.ExpandTemplate( Request.GetPhysicalPath() )   
    

    Two things are important here for displaying the data. Notice the query that runs to retrieve the single customer record and the SCATTER NAME to create an object. Although this isn't necessary I like the idea of having an object in the HTML code as opposed to working with a cursor directly. Inside of the HTML template the template expressions are bound to:

    <input name="Company" value="<%= poQuery.Company %>">

    and so on. I could have used TQuery.Company to acces the fields directly as well. This mechanism provides a basic implementation of data binding that makes it real easy to expose data from your VFP code to the HTML page. You can see in FrontPage that each of the fields is bound to the HTML text box fields for display. Here's what this looks like in FrontPage.

    When the form is POSTed back to the server it's posted back to the same EditCustomer.wp page/request. The code checks to see if we're posting back the form with:

    IF !EMPTY(Request.Form("btnSubmit"))

    and then loads the customer and updates the data by retrieving each of the form variable fields with Request.Form(). The code to do all of this is minimal, but keep in mind the error checking code is also missing here.

    If your field names match the name of the HTML form variables the save code can be simplified quite a bit by using code like the following:

       LOCATE FOR pk = lnPK
       IF !FOUND()
          THIS.ErrorMsg("Customer not available","Get it right, man!")
          RETURN
       ENDIF
       
       SCATTER NAME oRec MEMO
       
       Request.FormVarsToObject(oRec)
       
       GATHER NAME oRec MEMO
    

    FormVarsToObject looks at the current object and tries to find matching HTML form variables and retrieves that data into an object that you provide. This object can be a record object as above or it can be a custom business object (see the Step By Step with the wwBusiness Object topic for more info).

    Directly accessing templates

    Ok, you can use a small code module to fire your logic, but you can also put the code directly into the template page! Believe it or not you can take the code we put into our server process method and stick it directly into the template page. Templates support the concept of code blocks which can execute as the page is evaluated. Each block is executed top to bottom alongside the expressions.

    Start by editing the WebProcess and renaming the EditCustomer method to xEditCustomer, so that the method is no longer accessible. Also add a SET STEP ON at the top of the renamed method so you can see when and if it fires.

    Then add the following to the template above:

    <html>
    <body>
    <br>
    <%
    PUBLIC pcErrorMsg, poQuery
    pcErrorMsg = ""
    
    lnPK= VAL(Request.QueryString("ID"))
    
    IF !USED("TT_CUST")
       USE TT_CUST IN 0
    ENDIF
       
    SELE TT_CUST   
    
    LOCATE FOR PK = lnPK
    IF !FOUND()
       pcErrorMsg = "Invalid Customer. Please select a customer from the list")
    ENDIF
    
    SCATTER NAME poQUery MEMO
    %>

    Then run the request again.

    Wow! It still works and no debugger popped up which means the code fired in the template page. Don't believe me? Add the following as the last line in the code block we just added:

    RETURN "Hello from the editcust.wp code block"

    Returning a value from a code block causes that value to be displayed in the HTML document and you'll see it when you re-run the request.

    Tip:
    I consider running long blocks of code in templates and scripts bad coding practice as it mixes the User Interface with the business logic. It's really much cleaner to use a Process method using ExpandTemplate to call out to the HTML page to perform the computational part (as shown in the first example on this page) and then leave just the display issues to the template page. With the ability to embed expressions that can represent fields, properties of objects and PRIVATE variables it's easy to configure everything you need from within the process code so that all you have to is embed expressions in template pages. Another reason code in templates is not a good idea is that you can't debug it - if code fails it will show an error in the page, but will not give you a clear indication of what code actually failed. There's also no way to step through the script code in debug mode. All of these issues add up to a best practice of using Process code as much as possible and using templates only to display the results.

    Template Summary
    Powerful, isn't it? You have the basic ability to add code to HTML pages without having to compile anything. Change some text upload the file and the changes are there immediately. No other files to update, no code to recompile. Easy.

    But there's a downside. Script pages are not trivial to debug if something goes wrong. You can't step through the code if an error occurs and you get no error information beyond the code block which failed. So, for complex code I wouldn't recommend using this mechanism, but for simple things it works great.

    Codeblocks are also slow because they are evaluated line by line using macro-expansion and the actual code block has to be parsed first so there's a fair amount of overhead. Code like the above is no problem - but looping constructs running through a lot of records will be slow and better left for external code maintained in your server.

    Expression expansion on the other hand is fairly fast so <%= %> tags are efficient.




    Core Step by Step with the wwBusiness Object


    Ok, up to this point I've shown you how to use VFP's data language to create cursors and then display that data. This works well, but it's not really great application design and most applications do or should use business object for the data access. Web Connection includes a light weight wwBusiness object class which can facilitate the process, so let's review how to build another application that performs similar functionality to the previous example, but use the business objects instead.

    You can find the sample templates and PRG file in the html\devregistry directory of your installation.

    Step 1 - Setting up a business object



    In this example I'll use the wwBusiness object to build a simple editing application that lets us view entries in developer registry. Operationally this will be very similar to the straight file access example I used in the first Step by Step guide, but we'll greatly reduce code by using the business object. And for kicks I'll upsize the application once we're done and run it with SQL Server without making any major code changes. Ready?

    First we'll need to create a business object. In this case we'll keep it really simple with a single table application that uses the wwDevRegistry table in your wwDemo directory. First thing we want to do is create a business object that maps to this table. To this run the Web Connection Management Console (DO CONSOLE) and click on the Create New Business Object link. You'll see the following Wizard:

    You specify a class to create and the name of the file that you'd like to bind it to. Binding to a file is optional, but in most cases business objects bind to an underlying file or at least a controlling file. Some objects like invoices are aggregate objects, and even though the invoice object is more complex and made up of multiple file relations it would still bind to the invoice master table.

    In this case we're dealing with a single developer list table, so we choose that. Note that you can pick the file from the file dialog. I changed the value from the dialog to a relative path (.\) instead of using the full path to make the path more portable. Next I need to specify an ID file that is used to generate new PKs for my business object. If this file doesn't exist it's created for you. Each business object gets a record in this table with an increment count that generates the new ids.

    Ok, click the Go button and new business object is created for you that should look like this now:

    Simple Querying data

    First, lets just check out how the business object works by trying some simple querying:

    DO wconnect
    SET CLASSLIB to wwBusiness Additive
    SET CLASSLIB TO wwDeveloper ADDITIVE
    
    loDev = CREATEOBJECT("cDeveloper")
    
    loDev.Query()
    
    BROWSE
    

    This retrieved the entire contents of the table in a cursor to us. If you want to filter the query or retrieve only a few fields you can do something like this:

    loDev.Query("company,Name where company < 'D'")

    The Query method retrieves data with a SQL statement into a cursor. You can also specify a full SQL statement without an INTO clause like this:

    loDev.Query("select company,Name where company < 'D' ORDER BY company","Developers")

    The second parameter is the name of the cursor that's created. If you'd like to return data in XML format you can change the command to:

    loDev.Query("select company,Name where company < 'D' ORDER BY company","Developers",3)
    ShowXML(loDev.cResultXML)

    The third parameter determines the output type for the query. When returning XML the cursor is created and parsed into the cResultXML property. The cursor is closed. If you want to create XML and also keep the cursor you can use:

    loDev.Query("select company,Name where company < 'D' ORDER BY company","Developers")
    loDev.ConvertData(3)
    ShowXML(loDev.cResultXML)
    BROWSE
    

    There's much more to the object of course, but I'm going to leave that for later. Next let's create a new process in our Web Connection Step by Step WebDemo server.



    Step 2 - Create new Process class



    If you didn't do the first
    Step by Step guide go through Step 1 and 2 of that guide first to create the new project then come back here to add a new process.

    Start up the Management Console and click on the Create New Process Class link. Fill out the Wizard as follows:

    This will add a new process class called DevProcess to our existing Web Connection application. We'll create a new PRG file DevProcess with a DevProcess class that we'll use to handle our requests with.

    We'll also want to create a new Web virtual directory and script map (.dp) for this applet. Point the wc.dll back to the same wc.dll created in the previous Step by Step example. Each Web Connection server should use a single wc.dll instance.

    Click Finish to let the Wizard create the class, the virtual directory and copy the files into it.

    Let's make sure to test the new applet by:

    1. Start the WebDemo server with DO WEBDEMOMAIN.PRG
    2. Browse to http://localhost/DevProcess/default.htm

    You should see the demo page. Click on the Helloworld.wp script map link which brings up the default test page for this application.


    Step 3 - Querying developers with the business object


    Ready to access some data with the business object?

    To use the business object we need to do a couple of admin tasks first before we can start creating data access code. We need to make sure that the wwBusiness and wwDeveloper classes are visible from our process class. To do this we need to SET CLASSLIB to in the server's startup code. We can do this in the SetServerProperties method of the WebDemoServer class. Add the following to the bottom of the SetServerProperties method in WebDemoMain:

    *** Add any SET CLASSLIB or SET PROCEDURE code here
    SET CLASSLIB TO wwBusiness ADDIT
    SET CLASSLIB TO wwDeveloper ADDIT
    

    Next let's think about what we want to do here. We want to retrieve some records from the developer registry and display them. Although this query will be very simple let's first create a business object method to retrieve this data rather than explicitly using a SQL statement from our Web method. To do so let's:

    MODI CLASS cDeveloper of wwDeveloper

    Add a method called DeveloperList:

    LPARAMETERS lcCountry,lnMode
    
    IF EMPTY(lcCountry)
       lcCountry = "United States"
    ENDIF
    
    RETURN THIS.Query("SELECT company,name as contact,state,City,Country from " +  THIS.cFileName + ;
                      " WHERE country like '" + lcCountry +"%' " + ;
                      "ORDER BY State,City,Company","TDevelopers",lnMode)
    

    Why add a method for this simple SQL statement? Simple - it allows you to isolate all data access in the business object. If changes need to be made you can always come back here to do it in this single location. This becomes more important with more important query operations where you can pass parameters or property values into the method to set up for complex tasks. Using the Query method (or the lower level Execute() method) rather than a Fox SQL DML statement will allow this code to migrate to SQL Server later on.

    Ok let's use the code in a Web method and display it on the Web. For kicks let's display the list in a paged mode to something a little bit different than before.

    Open DevProcess.prg and add a method ShowDevelopers:

    FUNCTION ShowDevelopers()
    
    Response.HTMLHeader("Developer List")
    
    loDev = CREATEOBJECT("cDeveloper")
    lnCount = loDev.DeveloperList()
    
    IF lnCount < 1
       THIS.ErrorMsg("No matches","No developers match your query")
       RETURN
    ENDIF
    
    loSC = CREATEOBJECT("wwShowCursor")
    loSC.lAlternateRows = .T.
    
    *** Size each page to 10 items
    loSC.nPage_ItemsPerPage = 7
    loSC.cPage_PageUrl = "ShowDevelopers.dp?"
    
    loSC.ShowCursor()
    
    Response.Write( loSC.GetOutput() )
    
    Response.HTMLFooter(PAGEFOOT)
    
    ENDFUNC
    

    I made two additional changes in DevProcess. I added:

    #DEFINE PAGEFOOT [<hr><small><a href="ShowDevelopers.dp">List</a></small>]

    to add a page footer to my pages. I also added:

    FUNCTION Process
    
    THIS.oResponse.cStyleSheet = "/wconnect/westwind.css"
    DODEFAULT()
    
    RETURN .T.
    ENDFUNC
    

    To use a consistent style sheet with my pages so things look nice right from the get go. The result is:

    Notice that the list is pageable - the query is re-run on each hit as you click on different pages and wwShowCursor automatically handles the paging for you. Nice with very little effort isn't it?

    The whole list is cool but to filter we need to do a little more work. Let's allow fltering by country. Change the method as follows:

    FUNCTION ShowDevelopers()
    
    *** Retrieve the country entered
    lcCountry = Request.QueryString("Country")
    IF EMPTY(lcCountry)
       lcCountry = Request.Form("Country")
    ENDIF
    
    *** We have to persist the Country in order for paging to work
    THIS.InitSession()
    IF EMPTY(lcCountry)
       lcCountry = THIS.oSession.GetSessionVar("ShowCursor_Country")
    ELSE
       THIS.oSession.SetSessionVar("ShowCursor_Country",lcCountry)
    ENDIF
    
    
    loDev = CREATEOBJECT("cDeveloper")
    lnCount = loDev.DeveloperList(lcCountry)
    
    IF lnCount < 1
       THIS.ErrorMsg("No matches","No developers match your query")
       RETURN
    ENDIF
    
    loSC = CREATEOBJECT("wwShowCursor")
    loSC.lAlternateRows = .T.
    
    *** Size each page to 10 items
    loSC.nPage_ItemsPerPage = 7
    loSC.cPage_PageUrl = "ShowDevelopers.dp?"
    
    loSC.ShowCursor()
    
    Response.HTMLHeader("Developer List")
    
    *** Display Country dialog
    Response.FormHeader("ShowDevelopers.dp")
    Response.Write("Country: ")
    Response.FormTextBox("Country",lcCountry,20)
    Response.FormButton("btnSubmit","Go")
    Response.Write("</form><hr>")
    
    Response.Write( loSC.GetOutput() )
    
    Response.HTMLFooter(PAGEFOOT)
    
    ENDFUNC
    *  DevProcess :: ShowDevelopers
    

    The result now looks like this:



    Step 4 - Selecting and displaying data


    In order to display individual entries we need to drill down into the data. Note that when I set up the query in the last step with the business object rule I chose the fields I needed for display portion. I left out some important fields in that query because I was concerned with my display output <s>. This is bad design to for a business object to say the least. Business object methods should be flexible and be designed to allow return data only without having to worry about display issues.

    Most importantly we need the PK for each developer. But in order to make this method more flexible we can pass a parameter that's a field list on the fields to return. In order to get our cursor nicely formatted for wwShowCursor() we'll need to post parse the data with a secondary SQL statement. Let's start by fixing the DeveloperList method:

    LPARAMETERS lcCountry, lcFields, lnMode
    
    IF EMPTY(lcCountry)
       lcCountry = "United States"
    ENDIF
    IF EMPTY(lcFields)
       lcFields = "company,name,state,City,Country,pk"
    ENDIF
    
    RETURN THIS.Query("SELECT " + lcFields  + ;
                      "WHERE country like '" + lcCountry +"%' " + ;
                      "ORDER BY State,City,Company","TDevelopers",lnMode)

    Now we can return fields more cleanly for our queries with a typical default. Note that the original code in DevProcess::ShowDeveloper still works without any changes because the default parameters provide the data just fine.

    The next step for us is to add the ability to display and edit developer records. In order to do this and still use wwShowCursor we'll need to post process the SQL statement to create the hyper links as part of the Company field:

    loDev = CREATEOBJECT("cDeveloper")
    lnCount = loDev.DeveloperList(lcCountry,"Company,Name,State,City,Country,pk")
    
    *** Fixup the SQL statement
    SELECT [<a href="Showdeveloper.dp?Id=] + TRANSFORM(pk) + [">] + Company  + [</a>] as Company, ;
           Name as Contact, State, City, Country ;
       FROM TDevelopers ;
       INTO CURSOR TQuery
    

    We now have links:

    To handle the links we need to create a new method called ShowDeveloper. What we'll do here is load a customer business object using the PK we embedded into the link and then use an external template page to display the developer information.

    The ShowDeveloper method has next to no code because the HTML display is handled externally:

    FUNCTION ShowDeveloper()
    
    lnPk = VAL(Request.QueryString("ID"))
    
    loDev = CREATEOBJECT("cDeveloper")
    
    IF !loDev.Load(lnPK)
       THIS.ErrorMsg("Invalid Developer",loDev.cErrorMsg)
       RETURN
    ENDIF
    
    poDev = loDev.oData
    
    *** Load ShowDeveloper.dp page
    Response.ExpandTemplate(Request.GetPhysicalPath())
    RETURN

    The wwBusiness object base class loads an oData member with the data from the underlying table. In the code above you can reference the company with loDev.oData.Company for example. poDev is assigned for easier reference (and slightly faster operation) when displaying these fields inside of the template.

    The template page is ShowDeveloper.dp which is maintained with FrontPage and looks like this:

    The HTML inside of this document contains embedded ASP like tags that reference the poDev object. For example:

         
         <tr>
           <td width="131" valign="top" bgcolor="#00008B" align="right">
           <font color="#FFFFFF"><b>Web Site:</b></font></td>
           <td valign="top" width="453"><a href="<%= iif(lower(poDev.WebSite) # "http://","http://","") + TRIM( poDev.WebSite) %>">
           <%= poDev.WebSite %></a> </td>
         </tr>
    
        <tr>
           <td width="131" valign="top" bgcolor="#00008B" align="right">
           <font color="#FFFFFF"><b>Services
           offered:</b></font></td>
           <td valign="top" width="453">
           <%= IIF(poDev.Dev=1,"<img src='images/checkbox.gif'>Development    ","") %>
           <%= IIF(poDev.Training=1,"<img src='images/checkbox.gif'>Training    ","") %>
           <%= IIF(poDev.Support=1,"<img src='images/checkbox.gif'>Support    ","") %>
           <hr>
           <%= DisplayMemo(poDev.Services) %> </td>
         </tr>
    

    Note that we're using VFP functions here as well as using the properties of the poDev object reference to our fields. loDev is also available in this page - you could call a method of the business object if that would make sense.

    BTW, I actually created the table used here with wwShowCursor, captured the string to the clipboard and pasted it into this page. I then went through the field values and replaced them with the ASP tags of the object to replace the values. This is a quick way to pre-create content and then fix it up with a few table formatting options to make it look nicer and add in the dynamic data.

    When we run this form now by clicking in the ShowDevelopers list on any company we'll see:

    Look how little code it's taken to write all of this to this point. Using the business object keeps the Web Connection code really minimal. For the lookup code here we didn't even have to write any data access code because the business object provided us the services to retrieve the object. And once the object is loaded we can immediately plug it into the page.



    Step 5 - Editing data


    Let's provide the functionality to edit, delete and add developers. Hey, you already know the bulk of this code - it's very similar to the code we just dealt with when displaying the data. The business object will do all the work for us and a template page can handle the HTML display for this entry. We always come back to this page and we'll use the business object to hold state for us through requests - the data is read into the object from the form vars into the object each time, but we only write the data to disk if it validates Ok. We'll use the business object to validate and display error info right on the page along with the data the user entered.

    Again we have to start with the developer list to allow editing an entry. I'll add a field to the table called Action which will contain links to the Edit and Delete operations.

    Change the SQL Statement to:

    SELECT [<a href="Showdeveloper.dp?Id=] + TRANSFORM(pk) + [">] + Company  + [</a>] as Company, ;
           Name as Contact, State, City, Country,  ;
           [<a href="Editdeveloper.dp?Id=] + TRANSFORM(pk) + [">Edit</a> | ] + ;
           [<a href="DeleteDeveloper.dp?Id=] + TRANSFORM(pk) + [">Delete</a>] as Action ;
       FROM TDevelopers ;
       INTO CURSOR TQuery
    

    which now gives you:

    Displaying the information for the developer is a piece of cake using a template page in front page. Again we use FrontPage to handle this for us:

    <input type="text" name="txtCompany" size="40" value="<%= poDev.Company %>"></td>

    Note that the name of the field is prefixed with txt - this is important when we capture the data. Each of the fields on the form is formatted this way and we'll use FormVarsToObject to capture these form vars directly back into our business object.

    There are a couple of extra elements we need to deal with in editing that weren't an issue in display. First we need to validate and we'll handle this by filling a <%= pcErrorMsg %> variable with an error message. Validation is handled in the business object via a Validate() method which by default is blank. So let's fill it in with some basic error checks:

    * cDeveloper::Validate
    LOCAL loDev
    
    loDev = THIS.oData
    
    lcErrors = ""
    
    IF EMPTY(loDev.Company)
       lcErrors = lcErrors + "A company name is required." + CHR(13)
    ENDIF
    IF EMPTY(loDev.Name)
       lcErrors = lcErrors + "A contact name is required." + CHR(13)
    ENDIF
    IF LEN(loDev.Services) < 200
       lcErrors = lcErrors + "The service description is too short. At least 200 characters are required." + CHR(13)
    ENDIF
    
    IF !EMPTY(lcErrors)
       THIS.cErrorMsg = lcErrors
       RETURN .F.
    ENDIF
    
    RETURN .T.

    The other issue is that we can handle both existing entries and new entries in the same method. We'll assume that if we get a PK passed to us we'll Load the entry, otherwise we'll call the business object's New method to create a new object for us. In order for this to work we need to pass the PK forward for each page and we'll do this with the form submission by passing the PK on the URL in the HTML form definition:

    <form method="POST" action="EditDeveloper.dp?id=<%= poDev.pk %>">

    So if we call EditDeveloper without a PK we create a new entry, call it with a PK we're updating an existing entry.

    Ok, one last thing that's useful here since we're talking about new entries. The business object allows you to override behaviors and the New method is a good place to hook in things like default values. I want to default the country to United States and to have new developers a default setting of Development services. To do this I can add this to the New() method providing my defaults.

    * cDeveloper::New
    DODEFAULT()
    
    THIS.oData.dev = 1
    THIS.oData.Country = "United States"

    Now we're ready to deal with the Web handling code. Here it is, short and sweet:

    FUNCTION EditDeveloper()
    
    lnPk = VAL(Request.QueryString("ID"))
    
    pcErrorMsg = ""
    
    loDev = CREATEOBJECT("cDeveloper")
    
    IF !loDev.Load(lnPK)
       *** Create a new Developer
       loDev.New()
    ENDIF
    
    *** Easier Reference
    poDev = loDev.oData
    
    *** Save Operation
    IF !EMPTY(Request.Form("btnSubmit"))
       Request.FormVarsToObject(loDev.oData,"txt")
    
       *** Now fix up checkboxes
       lcVal = Request.Form("txtDev")
       IF !EMPTY(lcVal)
          loDev.oData.Dev = 1
       ELSE
          loDev.oData.Dev = 0
       ENDIF
       lcVal = Request.Form("txtTraining")
       IF !EMPTY(lcVal)
          loDev.oData.Training = 1
       ELSE
          loDev.oData.Training = 0
       ENDIF
       lcVal = Request.Form("txtSupport")
       IF !EMPTY(lcVal)
          loDev.oData.Support = 1
       ELSE
          loDev.oData.Support = 0
       ENDIF
    
       IF loDev.Validate()   
          loDev.Save()
          THIS.Showdevelopers()
          RETURN
      ENDIF
       
       pcErrorMsg = STRTRAN(loDev.cErrorMsg,CHR(13),"<br>")
    ENDIF
    
    poDev = loDev.oData
    
    *** Load ShowDeveloper.dp page
    Response.ExpandTemplate(Request.GetPhysicalPath())
    ENDFUNC
    

    The code starts by trying to load the developer object. If not found we assume a new developer is to be added. On first entry the btnSubmit form variable is empty because we didn't submit the form so the business object is simply displayed using ExpandTemplate.

    When we save the entry btnSubmit is not empty and we capture the form variables back into the business object. Checkboxes are problematic for auto updates like this because they only have form vars if checked so you can't tell the difference whether they were unchecked or simply not included on the form. Hence we have to explicitly check those values. On the HTML page too a little work is required to get these guys to display correctly:

          <input type="checkbox" value="1" name="txtDev" <%= iif(poDev.Dev = 1,"checked","") %> >Development           
          <input type="checkbox" value="1" name="txtSupport" <%= iif(poDev.Training = 1,"checked","") %> >Support<br>
          <input type="checkbox" value="1" name="txtTraining" <%= iif(poDev.Support = 1,"checked","") %> >Training</td>
    

    Once we have the values in the object we can validate. If validation goes through - great, we call the Save() method of the business object and the data gets written to disk. If it fails we get an error message back from the validate method in the cErrorMsg property. I choose to use CHR(13) as my error message delimiter, and I replace those with <br> HTML line breaks so that the error message displays correctly on the top of the page.

    Note now that this single edit page can handle all of the following:

    Each one of the form fields is embedded into the page like this:

    All with about 30 lines of code. because the business object is doing all of the hard work. Note that this really separates the layers of the application into the user interface (templates), the middle tier (Web Handling/front end) and the business layer (business object). The latter doesn't know or care anything about the Web and can be plugged in anywhere now.

    Step 6 - Updating the List Page


    For consistency's sake we should update the Developer list page to look nice too. The following steps demonstrate how you can mix code and templates to keep your code in your classes where it belongs while leaving most if not all of the HTML rendering external in a template page. We'll also discuss using Sessions to do multi-page operations and I'll throw in some discussion of security and logins to keep people from editing and deleting data.

    I'm going to add a new method to our server called default which is going to display our developer list similar to ShowDevelopers, but it will use a template and provide some query capabilities. This request will be a bit more complex than the ones we've been coding because we'll deal with two sets of data we need to manage: The query result as well as the query parameters.

    Let's start in FrontPage by designing the page:

    You can see the fields of the query form filled by a poQuery object. This is a tempoary object that is used to track the form variables and echo them back to the form when read.

    When the user clicks the Search for developers button the query is displayed between the two horizontal lines you see on the bottom of the form with a simple:

    <hr>
    <%= pcDeveloperList %>
    <hr>
    

    pcDeveloperList is actually updated in the Fox code and contains the output from wwShowCursor() which will be the same list we used before.

    Now searching becomes a little more difficult in this scenario, because we have various dynamic parameters that the user provides. You have a choice here of how you deal with this in terms of the business object. You can either decide that since this is indeed a fairly dynamic query that involves most of the fields as possible query parameters in the query that it's easier to simply build the Query and its SQL statement directly into the form. Or you can choose to go ahead and build a business object method that takes the various result values as parameters.We'll do the latter to keep the query creation code out of the Web application.

    So let's add a method to the cDeveloper class called DeveloperListQuery:

    LPARAMETERS lcCompany, lcName, lcCity, lcState, ;
                lcZip, lcZip2, lcCountry, ;
                lnDev, lnSupport, lnTraining, lcOrder,;
                lcFields, lnMode
    
    IF EMPTY(lcCountry)
       lcCountry = "United States"
    ENDIF
    IF EMPTY(lcFields)
       lcFields = "company,name,state,City,Country,pk"
    ENDIF
    
    
    lcWhere = "Approved=1"
    IF !EMPTY(lcName)
        lcWhere = lcWhere + " AND LOWER(Name) like '" + LOWER(lcName) + "%'"
    ENDIF
    IF !EMPTY(lcCompany)
        lcWhere = lcWhere + " AND LOWER(Company) like '" + LOWER(lcCompany) + "%'"
    ENDIF
    IF !EMPTY(lcCity)
        lcWhere = lcWhere + " AND LOWER(City) LIKE '" + LOWER(lcCity) + "%'"
    ENDIF
    IF !EMPTY(lcState)
        lcWhere = lcWhere + " AND LOWER(state) LIKE '" + LOWER(lcState) + "%'"
    ENDIF
    IF !EMPTY(lcZip) AND EMPTY(lcZip2)
        lcWhere = lcWhere + " AND Zip like '" + lcZip + "%'"
    ENDIF
    IF !EMPTY(lcZip) AND !EMPTY(lcZip2)
        lcWhere = lcWhere + " AND Zip>='" + lcZip + "' and Zip<='" + lcZip2 +"'"
    ENDIF
    IF !EMPTY(lcCountry)
        lcWhere = lcWhere + " AND LOWER(Country) like '" + LOWER(lcCountry) + "%'"
    ENDIF
    
    IF NOT (lnDev=0 AND lnTraining=0 AND lnSupport=0)
        IF lnDev = 1
            lcWhere = lcWhere + " AND dev=" + TRANSFORM(lnDev)
        ENDIF
        IF lnSupport = 1
            lcWhere = lcWhere + " AND support=" + TRANSFORM(lnSupport)
        ENDIF
        IF lnTraining = 1
            lcWhere = lcWhere + " AND training=" + TRANSFORM(lnTraining)
        ENDIF
    ENDIF
    
    IF EMPTY(lcOrder)
       lcOrder = "Company"
    ENDIF
    
    lcOrder = "ORDER BY " +lcOrder
    
    THIS.cSQLcursor = "TDevelopers"
    THIS.cSQL = "select " + lcFields + " from " + THIS.cFileName + ;
                 " WHERE " + lcWhere + " " + lcOrder
    
    lnResult = THIS.QUERY(THIS.cSQL)
    
    THIS.cSQLCursor = "TQuery"
    
    RETURN lnResult
    

    Notice those 10 (count 'em) parameters. Ugggh. An alternative would be to create a Query object that allows you to set properties on an object and pass that in, but that's actually even more code. This code just takes the parameters and converts them into SQL filters in the WHERE clause plus the order by that determines how records are ordered.

    With this method in place we can now easily query the data, which will reduce the amount of code in the Web process method. I'll call this one default (Default.dp) because this will be our home page and if you'll remember the HTML table data will generate into pcDeveloperList variable (at the bottom of this code), which embeds and displays the list on the template. Here's the code:

    FUNCTION Default
    LOCAL loSc as wwShowCursor, loDev as wwDevRegistry
    PRIVATE pcDeveloperList as String
    
    *** Object that'll hold the search vars as properties
    *** PRIVATE so it's visible to the external template
    PRIVATE poQuery
    poQuery = CREATEOBJECT("Relation")
    poQuery.AddProperty("DevName","")
    poQuery.AddProperty("Company","")
    poQuery.AddProperty("City","")
    poQuery.AddProperty("State","")
    poQuery.AddProperty("Zip","")
    poQuery.AddProperty("Zip2","")
    poQuery.AddProperty("Country","United States")
    poQuery.AddProperty("Sortby",1)
    poQuery.AddProperty("Dev",0)
    poQuery.AddProperty("Training",0)
    poQuery.AddProperty("Support",0)
     
    *** Collect all the form vars into this object
    Request.FormVarsToObject(poQuery,"txt")
    
    IF poQuery.SortBy = 1
       lcOrder = "Company"
    ELSE
       lcOrder = "Country,State,City"
    ENDIF
    
    loDev = CREATEOBJECT("cDeveloper")
    loDev.DeveloperListQuery(poQuery.Company,poQuery.DevName,;
                                      poQuery.City,poQuery.State,;
                                      poQuery.Zip,poQuery.Zip2,;
                                      poQuery.Country,;
                                      poQuery.Dev,poQuery.Support,poQuery.Training,;
                                      lcOrder)
    
    *** Post process for links
    SELECT [<a href="Showdeveloper.dp?Id=] + TRANSFORM(pk) + [">] + Company  + [</a>] as Company, ;
           Name as Contact, State, City, Country ;
           FROM TDevelopers ;
       INTO CURSOR TQuery
    
    loSC = CREATEOBJECT("wwShowCursor")
    loSC.lAlternateRows = .T.
    loSC.cExtraTableTags = " style='font:normal normal 8pt tahoma'"
    
    loSC.ShowCursor()
    pcDeveloperList = loSC.GetOutput()
    
    IF RECCOUNT() = 0
      pcDeveloperList = pcDeveloperList + ;
        "<p><center><b style='color:darkred'>No entries matched your search</b></center></p>"
    ENDIF
    
    Response.ExpandTemplate( Request.GetPhysicalPath() )
    ENDFUNC
    

    Notice the use of Query object to hold the form variables from the query form. Why do I do this? Well, on the Web page I'd like to echo back the values of the input form and using the object is an easy way to do this. If you look back on the FrontPage image you'll see that the query form has <%= poQuery.Company %> for the company name value - using the object makes it easy to display this value with out calling Request.Form("txtCompany") for each field. In addition, the parameters we pass to the DeveloperListQuery() method also requires those same values and here again we can simply use the object value instead of calling Request.Form() for each of the fields.

    Notice the call to Request.FormVarsToObject() which pulls all the form variables that match variable names into the object. The field names have a txt prefix (txtCompany, txtDevName etc) and that prefix is adjusted for. Using the poQuery object simplifies the job of passing this data around.

    If we run this form we now see the developer list as expected and it looks like this:

    Handling Query State between requests

    Cool. Play around with this and notice that the query is working correctly. But also notice that I cheated a little bit here! When we built the original developer list it was a paged list that only displayed a few records at a time - the list now is displaying all developers. To add that functionality back we'll have to do a little bit more work because we now have a dynamic query we're running with input provided by the users. Think about it: The user entered some data and if we see the link to go to page 2 the application has now lost the form variables that were entered to create the query. poQuery is going to have all blank values which means the paging mechanism without state keeping won't work.

    What we have to do is persist the query information between requests. To do this we need to use a Session object. We'll need a couple of thing to get this to work. First we need to add Session support. I'll add the call to InitSession into this method only since this is the only method that'll require it. If your app relies on session data in more than a few methods you probably should put the call to InitSession into the Process() method. We'll add this at the top of the method.

    THIS.InitSession()
    Session = THIS.oSession
    

    But what do we save there? All the search parameters? That's an option - but there's an easier way. We can simply take the poQuery object, persist into XML and store that in the session using wwXML::ObjectToXML(). Add this code:

    loXML = CREATEOBJECT("wwXML")
    
    IF EMPTY(Request.Form("btnSubmit"))
       *** Not a form submission - we're paging or first time access
       lcXML = Session.GetSessionVar("poQuery")
    
       IF !EMPTY(lcXML)  && Empty - first time access: Do nothing
          *** Load poQuery from the stored data of the last query
          loXML.XMLToObject(lcXML,poQuery)  && restore the persisted data
       ENDIF
    ELSE
       Session.SetSessionVar("poQuery",loXML.ObjectToXML(poQuery))
    ENDIF
    

    Finally we can add the paging support to ShowCursor by adding these two lines after loSC has been created:

    loSC.nPage_itemsPerpage = 8
    loSc.cPage_PageUrl = "Default.dp?"

    Ok, now let's test this again. The list should now page to 8 pages a piece. To test make sure you pick a query that returns more than 8 items such as Zip between 000 and 500 maybe. Notice now as you page through the list that your query stays intact and that the query search box remains filled out on each hit. This is because even though the user didn't fill out these values on subsequent hits these values come from the poQuery object that was persisted in the Session object.

    Pretty slick, huh? Using XML in this fashion allows you to very easily save complex data in a session and pull it back out. Objects are a great way to make this work because ObjectToXML() can with a single line of code persist an object to the session.




    Step 7 - Adding Basic Security


    Time to get realistic here - in this app so far anybody can do anything, which is Ok for a sample app, but not so OK for an online app. I'll show you a couple of very simple ways that you can prevent all but authenticated users to have access to certain parts of the application.

    Let's assume for a minute that all users have access to the application to view info and add information. But editing and deleting is allowed only for certain users that are logged in. To do this let's add edit and delete functionality to the ShowDeveloperPage by adding some links on the bottom of the page:

    We'll do the same for editing the developer. Let's start with the Delete operation since we haven't written that yet. Create a new method like so:

    FUNCTION DeleteDeveloper()
    
    THIS.InitSession()
    Session = THIS.oSession
    
    *** Allow only users with a password to delete this entry
    IF !THIS.UserLogin()
       RETURN
    ENDIF
    
    lnPK = VAL( Request.QueryString("ID") )
    
    loDev = CREATEOBJECT("cDeveloper")
    IF !loDev.Delete( lnPK )
       THIS.ErrorMsg("Error deleting entry",loDev.cErrorMsg)
       RETURN
    ENDIF
    
    THIS.Default()
    ENDFUNC
    

    We'll need to make one more change to the Default method to allow this to work. The template to display is referenced through Request.GetPhysicalPath(). We can't use the Physical Path in this case because the physical path for this request would be DeleteDeveloper().

    Instead we have to specify the path of the page explicitly. How do we do this generically? Conveniently when we created the Process class Web Connection created a configuration object for us which contains an HTMLPagePath reference which is stored in the application INI file (WebDemo.ini). We can use this as follows:

    Response.ExpandTemplate( Server.oConfig.oDevprocess.cHTMLPagePath + "default.dp" )

    The oConfig object is the main Web Connection configuration object. All process classes created with the Wizard also create a private config class off this main config object. Here you can add custom properties that are application specific. Each of the properties is persisted into the INI file. This is great to store application specific configuration information that is dynamic and needs to be changed depending on location of the app. oDevProcess is the name of the config object for our sample process that we've been building and the HTMLPagePath points at our HTML directory. If we look at webDemo.ini you'll find:

    [Devprocess]
    Datapath=
    Htmlpagepath=D:\inetpub\wwwroot\DevProcess\

    which is indeed the correct path to our Web pages.

    Note the call to the Login method which uses the Web Server's/Windows Basic Authentication mechanism to validate users. Make sure you enable Basic Authentication on the Web Server or the virutal directory that you're working with - Web Connection does this automatically for the virtual when it created the virtual directory.

    Now when we run the form we'll see:

    The ANY parameter on the Login() method specifies that any logged in user in the system will be allowed access so as long as you put in a valid username or password you'll get in.

    You can also specify a specific username or a comma delimited list of usernames that are allowed access. If you fail to login - no access, plain and simple.

    If I wanted to only allow myself I'd specify:

    IF !Login("ricks")
       RETURN
    ENDIF
    

    Basic Authentication is very simple to use and allows you to control access to an application neatly.

    Many times however you'll find you'll want to use FoxPro tables to validate access. To do this we'll have to do a little more work. Assume you have a user table called users with two fields username and password in them. You can then use the following code:

    FUNCTION UserLogin
    
    *** Session must be available for this to work
    lcUser = Session.GetSessionVar("LogonUser")
    IF !EMPTY(lcUser)
       RETURN .T.
    ENDIF
    
    lcuser = Request.Form("txtUserName")
    lcPass = Request.Form("txtPassword")
    
    IF !USED("Users")
       USE USERS IN 0
    ENDIF
    SELECT Users
    
    IF !EMPTY(lcUser) 
       LOCATE FOR LOWER(username) = LOWER(PADR(lcUser,20)) AND ;
                  LOWER(password) = LOWER(PADR(lcPass,15))
       IF FOUND()
          Session.SetSessionVar("LogonUser",lcUser)
          RETURN .T.
       ENDIF 
    ENDIF   
    
    
    *** User Input form   
    Response.HTMLHeader("Customer Login")
    Response.Write([<form action="" method="POST">])
    Response.Write([<table align=Center border=0 cellpadding=0 cellspacing=0 style="background:LightGrey;font:normal normal 8pt Tahoma">] + ;
                   [<tr style="background:Navy;color:White;font:normal bold 8pt Tahoma"><td colspan=2>User Login</td>] + ;
                   [<tr><td>Username:</td>] + ;
                   [<td><input type="text" size=20 name="txtUserName"></td></tr>] +;
                   [<tr><td>Password:</td><td><input type="password" size=15 name="txtPassword"></td></tr>] +;
                   [<tr><td><td><input type="submit" value="Log in"</td></tr>] + ;
                   [</table></form>])   
    
    RETURN .F.
    ENDFUNC
    *  WebProcess :: UserLogin
    

    This request will present an HTML based login dialog that validates against a Fox table and it works pretty much the same way as the Login() method in wwProcess work by default. Note that in order for this to work you a Session that is active. There's actually a little quirk relating to Sessions that occurred when I wrote this code. We need to add InitSession to the DeleteDeveloper call so that the session is available. But since we're redirecting to Default() at the end we run into trouble because Default also initializes a Session object. To avoid this problem I used a different approach using an indirect redirect (using an HTML <META REFRESH> tag):

    FUNCTION DeleteDeveloper()
    
    THIS.InitSession()
    Session = THIS.oSession
    
    *** Allow only users with a password to delete this entry
    IF !THIS.UserLogin()
       RETURN
    ENDIF
    
    lnPK = VAL( Request.QueryString("ID") )
    
    loDev = CREATEOBJECT("cDeveloper")
    IF !loDev.Delete( lnPK )
       THIS.ErrorMsg("Error deleting entry",loDev.cErrorMsg)
       RETURN
    ENDIF
    
    *** Just re-run the list now
    THIS.StandardPage(loDev.oData.Company + " deleted",,,2,"default.dp")
    

    Not the call toStandardPage() which includes a 4th and 5th parameter which specify when to redirect and to what link. This works better than a Response.Redirect() because you can write out HTTP headers (like the Cookie for the session for example) using this approach where Response.Redirect() does not support headers. Now the page displays an intermediate HTML page which in turn loads the default page.

    There's actually an easier and more logical way to handle this Session problem and that is to simply load the Session in the Process() method and make it available to all methods thus avoiding any duplication of InitSession. If you do use InitSession selective just be careful that code relying on sessions doesn't call InitSession twice or you'll blow away the original session content.

    Ok so we have security that's based on access rights. For editing developers we want to have similar functionality for 'admin' type users, but we also want the actual developer to edit their own entries. We won't implement this here, but basically what you want to do is assign the developer an ID in an external table that maps him to a site id. I do this on the West Wind site by using a master customer table that users are bound to when they come to the site. Users can reattach to their profile and they need that profile ID in order to access it. The way this works is that there's a user specific cookie, which maps to a user Id in my customer table. The customer PK is then a foreign key in the developer table. I deliberately left out this part of the application because the management of the customer in this scenario is fairly involved and not really very educational in terms of using business objects or providing a glimpse at any special technology. So I leave this particular excercise for you to complete and bind user profiles. It's not difficult - it just takes a fair amount of application specific code.



    Step 8 - Switching the app to SQL Server Data


    So now this little applet is more or less complete, so now lets move the app to SQL Server, shall we? ( you can use MSDE if you don't have SQL Server - MSDE ships with VFP7 and can be downloaded from the Microsoft Web Site)

    The first step in this excercise is to upsize the data to SQL Server using the Visual FoxPro Upsizing Wizard. Please see the VFP documentation on upsizing a VFP database to SQL Server for more details - this outline only gives you rough steps.

    1. Start by adding the wwDevRegistry table to a temporary database container (it's a free table)
    2. Make sure you have a SQL Server Connection setup either by creating a Connection in VFP
      or by creating an ODBC datasource that you can connect to.
    3. Click on the Tools|Wizards|Upsizing Wizard menu option to start the upsizing Wizard.
    4. Choose the tables to upsize and select any other options that the Wizard pops up.
    5. Create a New database and call it wwDeveloper.
    6. Step through the rest of the Wizard and click Finish to upsize

    This has created a new database for you and upsized the single wwdevRegistry table from our installation to the server.

    The next step is to set up Web Connection's SQL Server support:

    1. Start the Management Console with DO CONSOLE
    2. Select the Create SQL Server Tables option
    3. Select Add tables and Stored Procedures to existing database and type wwDeveloper for the database name.
    4. Provide a connection string, which defaults to wwDeveloper with username of sa and no password. Change
      the string if this is not correct.
    5. Click Finish.

    This creates several new tables and a few stored procedures that are used by Web Connection. The wwBusiness specific features handled by this operation are creation of a sp_ww_NewID stored procedure which is used to create PKs for our business objects.

    Next we need to add SQL Server support to our business object. To do this:

    1. Start the Management Console with DO CONSOLE
    2. Click on the Create New Business Object
    3. The settings from the previous build operation should still be in place.
    4. Note that your path for the tables is probably .\wwdemo instead of .\wwDevRegistry.
    5. Now set the SQL Server flag and add a connection string.
    6. IMPORTANT: Make sure the generate class is checked - don't worry it won't override your class it'll just update the properties.
    7. Click Go to complete the generation.
    8. Exit the Wizard and MODI CLASS cDeveloper of wwdeveloper if it isn't already popped up.
    9. Notice that the SQL connectstring is filled in. Change the nDataMode property to 2 for SQL Server.

    This operation created a wws_id table in your SQL Server database. If you look at the table you'll find a single record in it with wwDevRegistry and an ID number for it set to the next highest number of the PK that will be assigned.

    Congratulations you've fully upsized this table and are ready to use your business object. Let's check it out! First though you might want to rename or move your wwDevRegistry table to something else to make sure you notice when you're not accessing the SQL data. The best thing to do is create a small program and type in the following:

    DO WCONNECT
    SET CLASSLIB TO wwDeveloper addit
    
    oDev = CREATEOBJECT("cDeveloper")
    odev.Query
    
    BROWSE
    
    RETURN
    

    Congrats! You've just queried some data from SQL Server. Let's update a single record:

    oDev = CREATEOBJECT("cDeveloper")
    odev.Query
    
    ? oDev.Load(3)
    ? oDev.oData.Company && West Wind Technologies
    
    oDev.oData.Company = "East Wind Technologies"
    oDev.Save()
    
    oDev.Query()
    BROWSE
    

    And let's add a new record to make sure the PK generation works too:

    oDev = CREATEOBJECT("cDeveloper")
    odev.Query
    
    oDev.New()
    
    oDev.oData.Name = "Whil Hentzen"
    oDev.oData.Company = "Hentzenwerke"
    oDev.oData.City = "Milwaukee"
    oDev.oData.Services = REPLICATE("Books are good food!",25)
    IF !oDev.Validate()
       ? oDev.cErrorMsg
       RETURN
    ENDIF
    
    oDev.Save()
    
    oDev.Query()
    
    BROWSE
    
    RETURN
    

    Try this without the replicate first, then again with it. Now if you do this a few times you'll notice that Hentzenwerke is getting in there again and again - not all that cool. You can add some logic to the Validate() method that deals with this:

    *** Check if already existing
    IF THIS.nUpdateMode = 2  && Only do this on New entries
       lnResult = THIS.Query("select pk from " + THIS.cFileName + ;
                             " where company='" + THIS.oData.Company + "' AND name='" + THIS.oData.Name + "'")
       IF lnResult > 0
          lcErrors = lcErrors + "This entry exists already. Pk: " + TRANSFORM(pk) + CHR(13)
       ENDIF   
    ENDIF
    

    Delete all the new the entries we just created:

    oDev = CREATEOBJECT("cDeveloper")
    oDev.Execute("delete from " + oDev.cFileName + " where company='Hentzenwerke'")
    

    And then re-run the code we had before. Now only a single entry gets added.

    Ok so the business object works. Let's set up our application to use this SQL Server table setup. Just start 'er up and let's go.

    How about that? Isn't that pretty slick? The application works as is. If you followed the examples here the code should run without any changes now. I say *should* because SQL compatibility will not usually be this easy because of differring SQL syntax. In this example the queries are kept very simple and use Like instead of = and so on to provide proximity matches etc. In real world applications some SQL Statements will likely require bracketing between Fox and SQL versions. Still keep in mind that all record level operations will be performed automatically for you so they will always work as is without changes. Only SQL statements that use special syntax will require bracketing.

    What's bracketing? Inside of the business object you can do things like this

    IF THIS.nDataMode = 2 && SQL Server
       RETURN THIS.Query(  ... custom SQL here... )
    ELSE
       ... Custom Fox SQL here...
       RETURN _Tally
    ENDIF
    

    You can look at the wwBusiness class to get an idea what's involved in bracketing.


    Dealing with Connections

    I've oversimplified the porting issues too, because in this example here we're using a canned static connection string that's written into the object. This works because it happens to match the database connection settings, but most likely this value will be dynamic. Furthermore, as the app runs now, each time a business object is created a new connection is opened for that object. If you have 10 or 20 business objects running in a single request you're wasting a lot of resources on the SQL connections.

    First to specify a connection string manually you'd do:

    oDev = CREATEOBJECT("cDeveloper")
    oDev.cConnectString = "driver={sql server};server=(local);database=wwdeveloper;uid=sa;pwd=;"
    oDev.nDataMode = 2
    
    oDev.Open()  && Open the connection - not required with anyting but Execute()
    oDev.Execute(..SQL..)
    

    But to really do this right we don't want to set connection strings each time. We need to use a single connection and share it around. We do this with a wwSQL object. Like this:

    oSQL = CREATEOBJECT("wwSQL")
    oDev = CREATEOBJECT("cDeveloper")
    oSQL.Connect( oDev.cConnectString) 
    oDev.SetSQLObject(oSQL)
    
    oDev.Query()
    BROWSE NOWAIT
    
    oDev2 = CREATEOBJECT("cDeveloper")
    oDev2.SetSqlObject(oSQL)
    oDev2.cSQLCursor = "TQuery2"
    
    oDev.Query("select company, name")
    BROWSE NOWAIT

    Both of these queries are using the same SQL connection contained in the wwSQL object reference.

    So, in order to use the SQL application we need a connection and we need to make it permanent in the Web Connection server. To do so let's add it to the WebDemoServer object (as an oDPSQL object) and initialize it in SetServerProperties:

    THIS.AddProperty("oDPSQL", CREATEOBJECT("wwSQL"))
    IF !THIS.oDPSQL.Connect(THIS.oConfig.oDevProcess.cSQLConnectString)
       MESSAGEBOX("Couldn't connect to SQL Service. Check your SQL Connect string in the INI file.",48,"Web Connection")
       CANCEL
    ENDIF
    

    Notice the cSQLString property on the Config object which requires one more change in WebDemoMain on the bottom:

    *** Configuration class for the DevProcess Process class
    DEFINE CLASS DevProcessConfig as wwConfig
    
    cHTMLPagePath = "D:\WestWind\DevProcess\"
    cDATAPath = ""
    cSQLConnectString = "{sql server};server=(local);database=wwdeveloper;uid=sa;pwd=;"
    ENDDEFINE
    

    Add the connectstring there. It'll be written to the webDemo.ini file where you can change it as needed.

    [Devprocess]
    Datapath=
    Htmlpagepath=D:\WestWind\DevProcess\
    Sqlconnectstring=driver={sql server};server=(local);database=wwdeveloper;uid=sa;pwd=;

    Ok, that sets up the SQL Connection. Now to use the SQL object in our code. If you want to switch back and forth I suggest you set up a flag for it - WWC_USE_DPSQL and add it to wconnect_override.h like so:

    #DEFINE WWC_USE_DPSQL  .T.

    Now we need to tell our business objects to use the SQL connection. This means finding all places where the cDeveloper object is used and adding code like this:

    loDev = CREATEOBJECT("cDeveloper")
    #IF WWC_USE_DPSQL
    loDev.SetSQLObject(Server.oDPSql)
    #ENDIF

    Actually the #IF stuff is optional - if oDPSQL is NULL the connection isn't set and the datamode is not switched to SQL, but if you're not running SQL based there's no need to waste the overhead in the extra method call. So now it's a search and replace operation to find cDeveloper references and add the code above for each. To make sure you catch all of these change the default connection string in the cDeveloper class to an invalid string. If you start now without calling SetSQLObject now a connection dialog will pop up to let you know you have more work to do <bg>...

    It's a good idea to plan ahead and put that code in as you build your app right from the beginning. It's much easier at design time than at maintenance time later on as I've done here...




    Step 9 - Accessing the data over the Web from a Fat client


    wwBusiness also includes support for serving data over the Web to a business object, essentially providing you the utilility of using data located on a Web Server in a Fat client application. If you've already built your business object and the backend uses SQL Server you need to make no changes to the business object for this to work.

    In the following example I'll build a Visual FoxPro form that uses the cDeveloper class to retrieve the data from SQL Server over the Web. This involves a quick three step process:

    1. Setting up the server side application to handle our SQL requests
    2. Making a couple of property changes on the cDeveloper class
    3. Building the client side application using the cDeveloper class (form example here)

    Setting up the server side

    The first step is to set up the server side so that the wwBusiness object subclsass can access the remote database. The server side piece basically needs to be configured to determine which database to access and set up the connection to the database in order to provide the security rather than providing a generic access mechanism. To do this we'll need to set up a new Web Connection Process method - you can set this up in the DevProcess class we've worked on so far or put it into a completely different location. For demo purposes I'll put this data into my remote access demo process class which is located in wwdemo\http.prg. I create a method in there as follows:

    ************************************************************************
    * HTTP :: HTTPSQL
    ****************************************
    FUNCTION HTTPSQL_wwDevRegistry()
    
    *** Create Data Object and call Server Side Execute method (wrapper for Process Method)
    SET PROCEDURE TO wwHTTPSQLServer ADDITIVE
    loData = CREATE("wwHTTPSQLServer")
    loData.cConnectString = "server=(local);driver={SQL Server};database=wwDeveloper;pwd=sa;uid=;"
    loData.cAllowedCommands = "select,execute,insert,update,delete,method,"
    
    *** Retrieve XML input and then try to execute the SQL
    loData.S_Execute(Request.FormXML())   
    
    *** Send the output back to the client
    loHeader = CREATEOBJECT("wwHTTPHeader")
    loHeader.SetProtocol()
    loHeader.SetContentType("text/xml")
    loHeader.AddForceReload()
    loHeader.AddHeader("Content-length",TRANSFORM(LEN(loData.cResponseXML)))
    Response.Write( loHeader.GetOutput() )
    
    Response.Write( loData.cResponseXML )
    ENDFUNC
    *  HTTP :: HTTPSQL_wwDevRegistry
    

    That's all it really takes.

    For more detailed info on how to configure the server side for reusable SQL connections and security check out How wwHTTPSQLServer works.

    Testing the business object from the client side

    Ok, now that the server is in place we can use the business object to actually hit the server. What happens here is that we'll have two instances of VFP running - one to run the Web Connection server, the other to host the client application using the wwBusiness subclass (cDeveloper). We can now try this with code like this:

    DO WCONNECT
    SET CLASSLIB TO wwDeveloper Additive
    SET PROCEDURE TO wwHTTPSQL Additive
    
    oDev = CREATEOBJECT("cDeveloper")
    oDev.nDataMode = 4
    oDev.cServerUrl = "http://localhost/wconnect/wc.dll?http~HTTPSQL_wwDevRegistry"
    
    *** Sets up the HTTP object so we can configure it (optional)
    oDev.Open()
    oDev.oHTTPSQL.nConnectTimeout = 40
    *oDev.oHTTPSQL.cUsername = "rick"
    *oDev.oHTTPSQL.cPassword = "keepguessingbuddy"
    
    ? oDev.Query()   && Retrieve all records
    ? oDev.cErrorMsg
    
    BROWSE
    

    This should show you all the records from the server. Note that this data was retrieved from the Web Server, not from the local SQL Server. All operations that you could perform on the business object before still work as you would expect with data coming over the Web:

    *** Load one object
    oDev.Load(8)
    ? oDev.oData.Company
    ? oDev.oData.Name
    oDev.oData.Company = "West Wind Technologies"
    ? oDev.Save()
    ? oDev.cErrorMsg
    
    *** Create a new entry
     ? oDev.New(), "TEst"
    
     loData = oDev.oData
    
     loData.Company = "TEST COMPANY"
     loData.Name = "Rick Sttrahl"
     ? oDev.Save()
    
    ? oDev.Execute("select * from " + oDev.cFileName )
    BROWSE
    

    Ok, so this works just fine here, let's use the business object in a Fox form.

    Using the wwBusiness Object in a Fox form with Web Data

    (code for this example can be found in wwBusSample.scx)

    We'll build the following form based applet using the wwBusiness object with data that is retrieved over the Web. Note, that you can also switch operation of the application easily to pull data either from VFP or SQL Server tables locally simply by changing the properties on the instance of the class on the form.


    This form uses a wwBusiness object instance to retrieve all data from a remote data source over the Web. This form contains very little code and requires no changes to pull data from local VFP or SQL data, or by pulling data over down over the Web; voila, the power of business objects!

    1. Start out by creating a new Form and add the listbox named oList to the form.
    2. Add a page frame with the two tabs show above to the form.
    3. Add an instance of the the cDeveloper class (wwDeveloper in the sample SCX) and name it oDeveloper.
      Set the following properties:
      nDataMode=4
      cServerUrl=http://localhost/wconnect/wc.dll?http~HTTPSQL_wwDevRegistry
      lValidateOnSave=.T.
      Finally add THIS.Load(0) to the Init event - this makes sure the oData member is loaded with empty data that the form fields can bind to before any explict developer has been loaded.
    4. Add a Statusbar ActiveX control and name it oStatus. Add two panels to it.
    5. Now add all the fields to display onto the first page frame page. As you add each field set the ControlSource to the appropriate field of the business object's oData member like this:
      THISFORM.oDeveloper.oData.Company
      THISFORM.oDeveloper.oData.Name
    6. Add the following method to load the list box:
      LPARAMETERS lnListValue
      
      IF EMPTY(lnListValue)
         lnListValue = 1
      ENDIF
      
      THISFORM.Showstatus("Loading Developer List...")
      
      loDev = THIS.oDeveloper
      loDev.Query("select company,pk from wwDevRegistry ORDER BY Company","TDevelopers") 
      IF loDev.lError 
         MESSAGEBOX("Can't load customer data" + CHR(13) + ;
                    loDev.cErrorMsg,48,"wwBusiness Web Data Sample")
         RETURN .F.
      ENDIF
      
      THISFORM.oList.RowSourceType= 2
      THISFORM.oList.RowSource = "TDevelopers.Company"
      THISFORM.oList.Value = lnListValue
      
      THISFORM.ShowStatus("",RECCOUNT("TDevelopers"))

      THis is the most code we're going to write for this application in a single method!

      ShowStatus simply updates the two panels of the statusbar:

      LPARAMETERS lcPanelText, lnRecordCount
      IF EMPTY(lcPanelText)
        lcPanelText = "Ready"
      ENDIF
        
      THISFORM.oStatus.Panels(1).Text = lcPanelText
      
      IF !EMPTY(lnRecordCount)
         THISFORM.oStatus.Panels(2).Text = TRANSFORM(lnRecordCount) + " records "
      ENDIF

    7. Handle the movement through the list with the WHEN event like this:
      IF TDevelopers.Pk < 1
         RETURN
      ENDIF
      
      THISFORM.LoadDeveloper(TDevelopers.pk)
      

      and then implement LoadDeveloper like this:

      *** LoadDeveloper()
      LPARAMETERS lnPK
      
      THIS.ShowStatus("Loading Developer...")
      loDev = THIS.oDeveloper
      
      IF !loDev.Load(lnPK)
         MESSAGEBOX("Couldn't load this developer",48)
         RETURN .F. 
      ENDIF
      
      THISFORM.Refresh()
      
      THIS.ShowStatus()
      RETURN .T.

    8. Now implement each one of the methods that are called from the buttons:

      *** New
      THISFORM.oDeveloper.New()
      THISFORM.Refresh()

      *** Save
      THIS.ShowStatus("Saving...")
      IF !THISFORM.oDeveloper.Save()
         MESSAGEBOX("Unable to save the developer entry." + CHR(13) + CHR(13) + ;
                    THISFORM.oDeveloper.cErrormsg,48)
         THIS.ShowStatus()              
         RETURN              
      ENDIF   
      
      THIS.ShowStatus("Developer Entry Saved...") && Update the list by requerying
      

      *** Delete
      IF THISFORM.oDeveloper.Delete()
         WAIT WINDOW NOWAIT "Developer entry deleted..."
         THISFORM.LoadDeveloperList(THISFORM.oList.Value)
         WAIT CLEAR
      ELSE   
         MESSAGEBOX("Couldn't delete developer entry" + CHR(13) +;
                    THISFORM.oDeveloper.cErrorMsg,48)
      ENDIF
      

      Now point each of the buttons at these methods of the form: THISFORM.New(), THISFORM.Save(), THISFORM.Delete().

    9. Implement the Search Page

      Simply add the fields shown and name them as appropriate based on the following handler snippet you can attach to the Search button:
      loPage = THIS.Parent
      lcCompany = TRIM(loPage.txtCompany.Value)
      lcState = UPPER(TRIM(loPage.txtstate.Value))
      lcZIp = TRIM(loPage.txtZip.value)
      lcCountry = TRIM(loPage.txtCountry.value)
      
      THISFORM.oDeveloper.cSQLCursor = "TDevelopers"
      lnResult = THISFORM.oDeveloper.Developerlistquery(lcCompany,,,lcState,lcZip,"",lcCountry,0,0,0,"Company","Company,pk")
      IF THISFORM.oDeveloper.lError
         MESSAGEBOX("Query failed" +CHR(13) + CHR(13) + ;
                    THISFORM.oDeveloper.cErrorMsg)
         RETURN
      ENDIF
      
      IF lnResult < 1
         MESSAGEBOX("No matches found...",64)
         THISFORM.ShowStatus()
         RETURN
      ENDIF   
      
      THISFORM.oList.Requery()
      

      Again we're reusing the business object method DeveloperlistQuery() reusing existing functionality to reduce the amount of code we have to write. This code simply queries the data again and returns a new resultset which gets re-bound to the listbox. The Search All button on the other hand simply calls the LoadDeveloperList() method to refresh the listbox with all entries from the server.

    That's pretty much it! Note that when you save validation occurs if you don't fill in the form completely or make the service description too short. All the rules of the business object work just as they did before except we are now pulling the data from the Web!

    If you wanted to pull the data from your local SQL Server instead, change nDataMode to 2 and add a cSQLConnectString for the connection and off you'd go against SQL Server. Change the nDataMode to 0 and set the cDataPath appropriately and off you go against Fox data!

    I hope this example has demonstrated the power and flexibilty you have with this simple business object.



    How Web Connection works


    This section of the documentation explains the Web Connection architecture in detail. Although it's not required that you understand exactly how the framework works it will help you be more efficient with Web Connection and gives you the potential to change the base behavior to fit your needs. And of course, some people just have to know. This section is for you!

    One thing that you'll find as you read through this section is that there's a lot of information here and it can sound potentially overwhelming at first. Web Connection is a very powerful tool and it has lots of ways to accomplish tasks, but you don't have to use or even understand all of them or how they work to be productive. This section drills into the architecture for those of you that want to understand how Web Connection works under the covers.

    The Server Architecture


    Web Connection is made of two separate components that comprise the full Web Connection framework: An ISAPI connector (as well as an optional CGI component for those Web servers that don't support ISAPI) that communicates with the Web server and a Visual FoxPro based framework that knows how to communicate with this ISAPI extension.

    The ISAPI connector's job is to take the request information that a Web request provides - HTML Form variables (HTTP POST data), Server Information (server port, Software etc.), client information (Client IP address, Browser etc.) and Request state information (such as cookies and authentication info) - and pass it forward to a Visual FoxPro server application that waits for incoming requests. As such the connector is an Application Service Provider that provides the interface between Web Server and Visual FoxPro.

    The Visual FoxPro server application can run in a number of different ways - as COM server for deployed applications, as file server for development - and there can be multiple instances of this server running to process simultanous requests. Because Visual FoxPro is a single threaded environment that can process only one request at a time, this pool of instances is used to provide support for simultaneous request processing.

    The server or servers receive the incoming request data as input to individual request processing. The server is built using pure Visual FoxPro code and it contains the Web Connection Framework and your application specific implementation code. The code that fires is responsible for generating HTTP output for an incoming request. In most cases this output will be HTML, but it can also be XML, or binary content such as a data file, a Word or PDF document for example. The result is returned through the Web Connection Framework - typically the Response object - and returns essentially a string that is then displayed in the browser or served to an HTTP client.

    The above diagram demonstrates how requests travel from client to server. The request starts with the HTTP client which tends to be a Web browser, but it could also be a Fat Client HTTP application (for example a VFP application using the wwIPStuff class) to request data from the Web server over the HTTP protocol. There are two common mechanisms that the client can use to request data: HTTP GET or HTTP POST. Get simply requests data, while POST can request data as a result, but also has the ability to receive data back.

    The client makes a request over the Web through either an HTTP GET or POST operation. HTTP GET's are typically hyperlinks accessed in a browser, POST operations occur when you submit an HTML form in a browser which 'posts' the input field values to the server. Both operations can return data to the client but only POST can push data up to the server.

    When the request hits the server it hits the server with a link against the Web Connection DLL:

    http://localhost/wconnect/wc.dll?wwDemo~Testpage

    This causes the Web server to load the ISAPI extension into memory - once it's loaded it stays loaded and the single instance of the DLL is recycled. The ISAPI extension is written in C++ and is a true multi-threaded ISAPI extension, which means it can take multiple requests simultaneously on multiple processor threads.

    The ISAPI DLL receives the incoming request and picks up all of the server's request information and encodes it into string and passes this information forward to the Visual FoxPro Web Connection server. The server picks up the request information and uses this information as the request input much like you would fields on a form or parameters.

    At this point your server code has full control and can run any Visual FoxPro code using the Request object to retrieve input and the Response object to send output into the HTTP output stream. You can use FoxPro tables and cursors, you can use SQL Server if you like, you can call COM objects - just about anything that the FoxPro language allows except user interface operations which for obvious reasons won't work in a Web environment.

    The mechanism and syntax for the actual request processing varies depending on the method you use to generate your output. You can use low level code that uses the Response object directly and allows raw access to the output generated directly, or you can use high level tools like the Web Control Framework that abstract the output processing using an object oriented and control based model. Other tools allow generation of PDF documents from reports, parsing Visual FoxPro forms to HTML and to provide various Web Service type interfaces.

    In all cases the output is channelled through the Response object and is then passed back to the ISAPI extension, which in turn sends the output back to the Web server.

    The mechanism used to pass data between the ISAPI connector and the Visual FoxPro server depends on the messaging mechanism used: File based, COM based or ASP.

    Messaging Modes - File Based, COM and ASP


    The communication layer between the ISAPI extension and the Web Connection VFP server depends on which mechanism is used. The data passed back and forth using strings, but the data may be either a physical string (in COM messaging), message files (file based messaging) or via ASP objects (ASP messaging).

    All of these modes support simultaneous request processing by managing multiple instances of your FoxPro server application to work around the limitations of Visual FoxPro as a single threaded environment.

    File Based Messaging

    This mechanism is the default mechanism and is used primarily when developing and debugging applications. It uses message files to pass information between the Web Server and your FoxPro Web Connection application.

    In this scenario the ISAPI extension sends a message file that is picked up by the Web Connection VFP server application to retrieve its input. The HTTP result is returned to the server as an output file. While this mechanism exists primarily for debugging, you can also run this mode as a standalone EXE. File based is easy to set up and work with and requires no configuration. Because of the files created there's some overhead involved, which is critical only when the site running it becomes very busy where the number of message files can cause directory congestion.

    You can run multiple file based instances to allow for simultaneous request processing.

    COM Messaging

    COM messaging uses your Web Connection application as an EXE COM Server for communication between the Web Server and your FoxPro Web Connection application. It uses a plain parameter and return value interface to pass information between the two.

    The VFP server is compiled into a COM object and receives the request input as a parameter of one of the COM methods. The HTTP result is returned as a string return value of the method call. COM messaging performs better than file based operation, but is more difficult to set up and configure. Additional advantages include usage of the ISAPI pool manager for scalability, remote server management, auto server restart.

    You can configure a pool of COM instances (up to 32) to allow for simultaneous request processing. The Pool is managed as part of the ISAPI extension, which starts the servers and manages their lifetime. The big benefits of COM are better performance, the ability to automatically start servers on the first hit and to automatically recover from any hangs or crashes inside of your FoxPro code.

    ASP Messaging

    ASP messaging allows a Web Connection COM server to be run as part of an ASP request.

    The VFP server must be compiled as an MTDLL (multi-threaded DLL) and is called from an ASP page to generate a complete or partial HTTP response. The request input is retrieved from the ASP Request, Session, Cookies and Server objects and output is sent to the ASP Response object. You should create a separate project from your EXE servers if you plan to mix and match between ASP and either COM or file based operation since compiling a project for different types of servers tends to screw up the ClassIDs in the registry.

    Simultaneous request processing is managed through COM and ASP or ASP.NET which will internally load multiple instances of your application. Although InProcess servers can be faster, they also suffer from greater instability and the inability to be effective managed (shut down, restarted, reconfigured) due to the inprocess nature of thes MTDLL COM servers.

    It's all transparent

    The Visual FoxPro Web Connection framework handles all these messaging mechanisms transparently, automatically adjusting inputs and outputs to the appropriate types. Your application user code does not need to differentiate between these operational modes and should work without any modifications in either mechanism.

    The Visual FoxPro Framework


    On the Visual FoxPro side a server application runs that can communicate with the ISAPI extension. The Web Connection framework, which consists of a number of classes, provides an easy interface of passing requests to your user code.

    The idea is that the framework handles all the Web abstraction so that your code can concentrate on the business task at hand. Your code basically implements a custom class with methods for each incoming request. When your method is called you receive a ready made Request and Response object that you can use to start writing your Web application code using plain Visual FoxPro code.

    Framework objects

    The Visual FoxPro framework consists of a number of objects that interact with each other to provide you the information you need to create an HTTP response. The following diagram shows how the core objects interact:

    Incoming requests are picked up by the wwServer class. In file based messaging a timer waits for files with a certain file template. In COM messaging a COM object is instantiated the first time and then recycled on each hit. In both scenarios the request data is passed in encoded format to the server, which depending on the mechanism picks up the data and creates a Request object from it.

    All requests are funnelled through the wwServer::ProcessHit() method which serves as the entry point for a server request. ProcessHit() configures the wwRequest object instance and then calls the wwServer::Process() method. Process() is a request router method that looks at the incoming parameters to determine which application receives the request since you can create multiple process classes in a single Server application with each process serving as a sub application or grouping of services. Process() consists of a large CASE statement that handles routing to each of these process classes. This CASE statement is usually autoconfigured when you use one of the Wizards in Web Connection to create a new application or add sub-Process to an existing application.

    The routing sends the request off into a wwProcess class, instantiating the class. The new object receives a copy of the Request object and instructions on how to handle the outgoing HTTP response (a file, or string) and sets up these objects internally. Then the wwProcess::Process() method is called which serves as the common entry point into this application class. Remember this class acts as your user code class with each request serving a specific Web request/URL.

    The Process() method has a default implementation, so you don't have to override it by default. However, most applications will override this method to handle operations that must occur on every Web hit - namely things like Authentication or user verification, or potentially some sort of state or user management. By overriding this method you can perform these tasks easily.

    Process()'s responsibility is to route the request to the appropriate method of the class and it does so by looking at the 'parameters' or QueryString or name of the requested page. And so your custom code gets called in this class method! Once you get called in this fashion all of Visual FoxPro's features.

    Your code at this point also has access to the input and output objects as:

    If you created a Process class called MyCode and a data access method like the following:

    Function CustList lcName = Request.QueryString("Name") *** Run a static query SELECT company, careof as Name, address, phone ; FROM wwDemo\TT_CUST ; WHERE UPPER(Company) = UPPER(lcName) ; ORDER BY Company ; INTO CURSOR TQuery *** Create HTTP Header, HEAD section, HTML/BODY tags and header text Response.HTMLHeader("Customer Data Accces") Response.Write("This demo displays data from a customer file.<p>") Response.ShowCursor() Response.HTMLFooter()

    To access this URL you could use:

    http://localhost/wconnect/wc.dll?MyCode~CustList

    or if you have set up a script map called myScript:

    http://localhost/wconnect/CustList.myScript

    Summary

    At first glance it appears that there's a lot happening, and that this whole scheme is very complex unless you have a grasp of the architecture. However, what's happening is that you actually have only two major objects (wwServer and wwProcess) communicating with each other via message objects (wwRequest and wwResponse). The entire process is basically hidden and provided for you in the Web Connection Visual FoxPro framework code, which you don't have to understand or manipulate directly.

    The mechanism described here is the core Web Connection engine. All other functionality is built ontop of this small core engine which is relatively simple and easily extended at all levels. The framework has been carefully designed to provide maximum flexibility and extensibility. All classes are open and can be easily subclassed and replaced with your own classes that extend the functionality of the framework. As far as framework code goes Web Connection implements a very flat hierarchy that relies on a few key hook points and subclassing and #DEFINE class hooks to provide easy built in extensibility that is easy to work with and understand.

    Finally, Web Connection's Management Console Wizards greatly simplify setting up and configuring a new project and new sub-process classes added to an existing applications so that in most cases you never need to worry about this low level logic at all.


    Implementing the Server


    The wwServer class serves as the entry point of your application. It handles access via the various mechanisms of File Based, COM and ASP. It sets up the Request object for the request and knows how to deal with the outgoing HTTP stream.

    To create a new application you create a new instance of the wwServer class. When running COM or ASP the server itself is the startup code since the COM object is directly invoked. When running File Based however a small loader program must first load the server into memory by creating the object and then waiting for incoming requests:

    #INCLUDE WCONNECT.H PUBLIC glExitServer, goWCServer *** Load the Web Connection class libraries DO WCONNECT *** Load the server - wc3DemoServer class below goWCServer = CREATE("wcDemoServer") *** Make the server live - Show puts the server online and in polling mode READ EVENTS RETURN

    This instantiates the server in File Based mode, and waits for incoming requests which are checked in a timer event. When a request comes in it's processed and the output returned. The server goes idle again then simply waiting at the READ EVENTS. The file based stub is the startup required for a file server since it can't 'stand on its own'.

    Using COM the server needs no startup code since the server is called directly. Instead of a form's timer firing the ProcessHit() method is called directly and off the server goes. When the COM Server is done it returns to idle status in the pool waiting for the next request.

    Web Connection can automatically manage figuring out how to run the server based on which mode the server is set up for.

    The wwServer class implentation

    The wwServer class handles all of this conversion. Each application you build has to subclass the wwServer class with your own custom version. This is required so you can configure the application for your environment and your application classes. Actually the New Project Wizard creates a wwServer subclass for you and the generated code looks something like this (plus a few customizations):

    #INCLUDE WCONNECT.H ************************************************************** DEFINE CLASS wcDemoServer AS WWC_SERVER OLEPUBLIC ************************************************************* *** Add any custom properties here ************************************************************************ * wcDemoServer :: OnInit ************************ PROTECTED FUNCTION OnInit *** Location of the startup INI file THIS.cAppIniFile = addbs(THIS.cAppStartPath) + "wcDemo.ini" THIS.cAppName = "Web Connection Demo" *** Main Config for this class. Class below in this PRG file THIS.oConfig = CREATE("wcDemoConfig") THIS.oConfig.cFileName = THIS.cAppIniFile SET CENTURY ON ENDFUNC * SetServerEnvironment ************************************************************************ * wcDemoServer :: OnLoad ************************ PROTECTED FUNCTION OnLoad *** Any settings you want to make to the server IF THIS.lShowServerForm THIS.oServerForm.Caption =THIS.cServerId + " - Web Connection " + WWVERSION ENDIF *** Add any application paths that I might to access *** Remeber these may not be relative in COM object *** hence the full path! DO PATH WITH THIS.cAppStartpath && Required when running as InProc COM object DO PATH WITH THIS.cAppStartPath + "CLASSES\" DO PATH WITH THIS.cAppStartPath +"WWDEMO\" DO PATH WITH THIS.cAppStartPath + "WEBCONTROLS\" *** Add any data paths - SET DEFAULT has already occurred so this is safe! DO PATH WITH THIS.cAppStartPath + "WWTHREADS\" SET PROCEDURE TO wwtClasses ADDITIVE SET PROCEDURE TO wwtList ADDITIVE SET CLASSLIB TO wwStore ADDITIVE ENDFUNC * SetServerProperties ************************************************************************ * wcDemoServer :: Process ************************* PROTECTED FUNCTION Process LOCAL lcParameter, lcExtension, lcPhysicalPath *** Retrieve first parameter lcParameter=UPPER(THIS.oRequest.Querystring(1)) *** Set up project types and call external processing programs: DO CASE CASE lcParameter == "WWTHREADS" DO wwThreads with THIS CASE lcParameter == "WWDEMO" DO wwDemo with THIS *** HTTP Client Demos CASE lcParameter == "HTTP" DO HTTP with THIS CASE lcParameter =="WWSTORE" DO WWSTORE WITH THIS *** SUB APPLETS ADDED ABOVE - DO NOT MOVE THIS LINE *** CASE lcParameter == "WWMAINT" DO wwMaint with THIS OTHERWISE *** Check for Script Mapped files for: .WC, .WCS, .FXP lcPhysicalPath=THIS.oRequest.GetPhysicalPath() lcExtension = Upper(JustExt(lcPhysicalPath)) DO CASE CASE lcExtension == "WWS" DO wwStore with THIS *** ADD SCRIPTMAP EXTENSIONS ABOVE - DO NOT MOVE THIS LINE *** CASE lcExtension = "WWT" DO wwThreads with THIS *** Web Connection Demo handling CASE lcExtension = "WWD" DO wwDemo with THIS CASE lcExtension = "WC" OR lcExtension == "FXP" DO wwScriptMaps with THIS OTHERWISE *** Error - No handler available. Create custom Response=CREATE([WWC_RESPONSESTRING]) Response.StandardPage("Unhandled Request",; "The server is not setup to handle this type of Request: "+lcParameter) IF THIS.oConfig.lAdminSendErrorEmail LOCAL loIP loIP = CREATE("wwIPStuff") loIP.cMailServer = THIS.oConfig.cAdminMailServer loIP.cSenderEmail = THIS.oConfig.cAdminEmail loIP.cRecipient = THIS.oConfig.cAdminEmail loIP.cSubject = "Web Connection Error Message - Unhandled request" loIP.cMessage = CRLF + ; "The request Query String is: " +THIS.oRequest.QueryString() + CR +; " DLL or Script: " +THIS.oRequest.ServerVariables("Executable Path") + CR+; " Server Name: " + THIS.oRequest.GetServerName() *** Send and immediately return loIP.SendMailAsync() ENDIF IF THIS.lCOMObject *** Simply assign to output property THIS.cOutput=Response.GetOutput() ELSE *** FileBased - must output to file File2Var(THIS.oRequest.GetOutputFile(),Response.GetOutput()) ENDIF ENDCASE ENDCASE RETURN * EOF wc3DemoServer::Process ENDDEFINE * EOC wcDemoServer DEFINE CLASS wcDemoConfig as wwServerConfig owwStore = .NULL. FUNCTION Init THIS.owwStore = CREATE("wwStoreConfig") ENDFUNC ENDDEFINE DEFINE CLASS wwStoreConfig as RELATION cHTMLPagePath = "d:\westwind\wwStore\" cDATAPath = ".\wwStore\" cXMLDocRoot = "wwstore" cAdminUser = "Any" ENDDEFINE

    Most of this code is boilerplate, which means you can simply cut and paste it for each full application server. The New Project Wizard will set up a default configuration for your server with the appropriate parts filled in. The project should be ready to run when the Wizard completes.

    The key things that you will change for each fully self contained server are:

    OnInit

    This method fires very early in the creation process of the server and is used to configure server settings that are crucial to how the server loads itself. Here you should override server properties and file locations like the startup INI file if you need to override it. This is a key feature actually - reading the application's INI file determines most of the startup values the server uses. The startup INI file is read after this method completes. You can override the settings loaded if desired in OnLoad().

    OnLoad

    OnLoad() fires after the server has initialized itself with the configuration settings from the INI file and internally has set its environment. When OnLoad() fires the server is basically ready to receive requests. This is your hook to set application specific settings in the server environment and override any Server properties that were loaded by default from the INI file or otherwise are set on the server.

    The most common things to do in OnLoad() is to load class libraries and add additional paths to the FoxPro path so data and support files can be found.

    Process

    The Process method is called on every hit of the server and is responsible for routing requests to the appropriate application class. This method handles routing by looking at the URL. It looks for routing info by default in two locations:

    In the example above a first parameter of wwDemo and .wwd extension would route to the same class so the following URLs are actually doing the same thing:

    /wconnect/wc.dll?wwDemo~TestPage
    /wconnect/TestPage.wwd

    The CASE statement tries to find a matching 'process' signature and the calls the appropriate Process class to actually handle the request. The Process Prg file contains a small stub that loads the appropriate process class and executes its Process() method which does all the work of creating the output. The Process class manages output generation. All of this is handled by this single line of code in the Process CASE statement:

    DO MyProcess.prg with THIS

    Configuration Objects

    For every server created a custom Configuration class is created. In the code above we have a wcDemo server which has a wcDemoConfig class that inherits from wwServerConfig. The Server Config has a set of default properties that get persisted into the wcDemo.ini file and are updated from the file when the server loads.

    These custom config classes are added to the Server file so that you may extend the Config class with additional 'global' settings, simply by adding custom properties. These settings then get persisted to the INI file and are also read at startup. They are always accessible then as (from within a Process class):

    Server.oConfig.cCustomProperty

    In addition each Process class also creates a custom configuration object that is specific to each Process class. In the example below there's a custom configuration for a wwStore Process with a host of custom properties. By convention these custom objects get attached to the server's oConfig object with the name of the process prefixed by an o. So to use it would look like this:

    lcSqlConn = Server.oConfig.owwStore.cSqlConnection

    Here's what the config looks like for this scenario:

    DEFINE CLASS wcDemoConfig as wwServerConfig owwStore = .NULL. FUNCTION Init THIS.owwStore = CREATE("wwStoreConfig") ENDFUNC ENDDEFINE DEFINE CLASS wwStoreConfig as RELATION cHTMLPagePath = "d:\westwind\wwStore\" cDATAPath = ".\wwStore\" cXMLDocRoot = "wwstore" cAdminUser = "Any" cVirtualPath = "/wwstore/" cStoreName = "West Wind Web Store" cStoreSqlConnection = "driver={Sql Server};server=(local);database=WebStore;" ENDDEFINE

    This configuration object is very powerful - simply add a property and any settings are persisted to the INI file and can then be changed in the INI file for configuration purposes.


    Setting up a Process Class


    Up to this point we've only discussed the server object, whose job it is to grab a Web request from the Web server and pass it on to you. Now we're gettin' to the meat of things. The wwProcess subclasses you create are the heart of your Web Connection applications as these classes represent your entry point for each of your requests.

    wwProcess is works by getting called from the wwServer class and executing a method in your server based on the URL passed. The URL contains information regarding which method to call.

    To review the server does the following:

    Creating your wwProcess Subclass

    So, how do we create this wwProcess class?

    The easiest way is to use the Wizards. Both the new Project Wizard and the New Process Wizard create wwProcess subclasses that manage the entire process for you.

    Behind the scenes, the wwProcess implementation works like this:

    Looking at a wwProcess subclass

    The wwProcess class is mostly a container for other objects in order to simplify access to the other objects throughout the framework. As such only a few custom methods and properties are defined in this class. The object references are used to create a new Process object which builds references to them and uses them internally making them available for your own routines. The setup code for the UDF and class creation looks like this:

    * PROCEDURE wwDemo LPARAMETER loServer #INCLUDE WCONNECT.H loProcess=CREATE("wwDemo",loServer loProcess.Process() RETURN

    The code above basically receives the two objects as parameters then creates a Process object and calls its Process() method which is its entry point for Web request processing.

    The actual wwProcess class implementation needs to be subclassed by you in order to attach custom processing methods to the code. The minimal wwProcess subclass looks like this:

    ************************************************************* DEFINE CLASS wwDemo AS WWC_PROCESS ************************************************************* ************************************************************************ * wwDemo :: HelloWorld ********************** * URL to arrive here: wc.dll?MyPrg~HelloWorld * or with a scriptmap: Helloworld.myScript ************************************************************************ FUNCTION HelloWorld THIS.StandardPage("Hello World","Hello from Visual FoxPro. "+; "The current time is: <b>"+Time()+"</b>") RETURN *EOF TestPage FUNCTION OtherRequest Response.HTMLHeader("Another Request") For x = 1 to 10 Response.Write("line " + TRANS(x) ) ENDFOR ENDFUNC ENDDEFINE

    WWC_PROCESS is a constant defined in wconnect.h that defaults to wwProcess. This #define allows you to override a common subclass used for all of your Process subclasses.

    Each additional request you create with a link from an HTML page (link or Form button that calls wc.dll) needs to have a corresponding method in this class. Your subclass of wwProcess can contain as many methods as you see fit, although it's best to logically break up the size of classes in separate program files or classes. In order to do so you'd pass a different first 'parameter' on the URL (in the example above MyPrg - MySecondPrg for example) to route to another PRG file that can handle another set of requests using another subclass of the wwProcess object. Remember each additional PRG file will need an entry in the wwYourServer::Process() method's CASE statement. The parameter can either be in the form the first URL parameter in a ~ separated list or preferrably a custom script map for your process (Helloworld.myScript).

    Code to fire on every request to your Process class

    Often times you'll want to have some code fire on every request to your page. Common things are initializing the Session object or authenticating users on every request. Instead of adding the code to do this to every request or a large number of selected requests you can use the OnProcessInit method which fires before a request is fired against the actual implementation method.

    Here's an example that initializes the Session object and checks all requests against an Admin directory to authenticate:

    ************************************************************************ * webConnectDemo :: OnProcessInit ********************************* FUNCTION OnProcessInit *** Use Session in this application THIS.InitSession("wwDemo") *** Check for user login - no login/no access IF ATC("/admin/",Request.GetLogicalPath()) > 0 THIS.cAuthenticationMode = "UserSecurity" IF !THIS.Authenticate() RETURN .F. ENDIF * THIS.cAuthenticationMode = "Basic" * IF !THIS.Authenticate("ANY") * RETURN .F. * ENDIF ENDIF RETURN .T. ENDFUNC

    Error Handling

    How errors are handled in the Process class depends on the Server.lDebugMode flag. This flag determines whether errors are handled or cause VFP to stop in the debugger if an error occurs. During development you'll generally will want the lDebugMode flag to .T. so you indeed stop on the code where an error occurs. For deployment you'll want to set Server.lDebugMode to .F. so that errors are handled and display an error page instead.

    The Server.lDebugMode flag can be set in the YourApplication.INI file as DebugMode, or on the Server Status form using the DebugMode Checkbox.

    Errors are handled by the base Process() method through a TRY/CATCH construct which is conditionally enabled/disabled based on the Server.lDebugMode flag. When an error is trapped by the TRY/CATCH it fires the OnError() method on your Process class with an Exception object as a parameter. You can override this method to provide custom error behavior.

    The stock behavior does the following:

    Here's what the stock error handler looks like:

    ************************************************************************ * wwProcess :: OnError **************************************** *** Function: Called when an error occurs and Server.lDebugMode is off. *** Assume: *** Pass: loException *** Return: .T. if you completely handled the error including output *** generation. Else return .F. ************************************************************************ FUNCTION OnError(loException as Exception) *** Shut down this request with an error page - SAFE MESSAGE (doesn't rely on any objects) THIS.ErrorMsg("Application Error",; "An application error occurred while processing the current page. We apologize for the inconvenience. " + ; "The error has been logged and forwarded to the site administrator and we are working on fixing this problem as soon as we can.<p>" + ; "<p align='center'><table cellpadding='5' border='0' background='whitesmoke' width='550' " +; "style='font-size:10pt;border-collapse:collapse;border-width:2px;border-style:solid;border-color:navy'>" + CRLF +; "<tr><td colspan='2' class='gridheader' align='left' style='font-weight:bold;color:cornsilk;background:navy'>Error Information:</th></tr>" + CRLF +; "<tr><td align='right' width='150'>Error Message:</td><td>"+ loException.Message + "</td></tr>" + CRLF +; "<tr><td align='right'>Error Number:</td><td> " + TRANSFORM(loException.ErrorNo) + "</td></tr>" + CRLF +; "<tr><td align='right'>Running Method:</td><td> " + loException.Procedure + "</td></tr>"+CRLF+; "<tr><td align='right'>Current Code:</td><td> "+ loException.LineContents + "</td></tr>"+CRLF+; "<tr><td align='right'>Current Code Line:</td><td> " + TRANSFORM( loException.LineNo) + "</td></tr>"+ crlf +; "<tr><td align='right'>Exception Handled by:</td><td>" + THIS.CLASS + ".OnError()</td></tr></table></p>") *** Log the Request IF TYPE("THIS.oServer")="O" AND !ISNULL(THIS.oServer) this.LogError(loException) ENDIF *** We've completely handled the error! RETURN .T. ENDFUNC * wwProcess :: OnError

    The call to LogError() logs and also emails if email configuration is setup. Note the default implementation uses the wwProcess::ErrorMsg() function to display the error message on an HTML page. As you can see the method is simple and easy to override to provide your own error customization as are the ErrorMsg() method which provides the stock display format for error messages.

    Using the Request and Response objects

    When your class method fires you will have available to you the Request and Response objects which Web Connection creates for you as part of its default wwProcess::Process() method. Since that method calls into your method these PRIVATE scoped objects are in scope everywhere in your request processing code and you can access them directly.

    The example above uses the Response object to Write() a few values to the HTTP output stream. Mostly this will be HTML, but it can be anything.

    Along the same lines you can use the Request object to retrieve input from the user and the environment. Request.Form() lets you retrieve HTML form variables, Request.QueryString() lets you retrieve parameters and Request.ServerVariables() lets you retrieve any of the system variables the server makes available. Request also has many custom methods that are wrapped around ServerVariables to retrieve values with more memorable names than the Server Variables.


    For a more hands on example on how this all works see the
    Step by Step Guide.

    Mechanisms for generating HTML


    Web Connection includes a number of very different ways of generating HTML/HTTP output from raw output of low level strings to very high level mechanisms that can render entire forms as DHTML. Keep in mind you don't need to use all of these mechanisms - choose whatever will work best for you application.

    Here are the choices available:

    With the flexibility that these options provide come choices that you have to make.

    Best Performance:
    Raw HTML output

    Maximum Flexibility:
    Web Control Framework
    Fox Class code plus Templates or Scripts
    Raw HTML output
    both in combination (like Guest application and wwThreads)

    Easiest migration from Desktop:
    Distributed XML Application (Fat Client -> Web Server)
    DHTML form rendering
    PDF Report Generation
    Web Control Framework (conceptual similiarity to desktop)

    Frequent changes by non-programmers:
    Web Control Framework
    Scripting and templates

    Restrictive Server environment where compilation is a problem
    Scripting and templates

    Distributed Applications
    wwXML generation
    wwIPStuff DBF encoding
    Raw HTTP output

    Summary - My choices

    Starting with Web Connection 5.0 our recommendation is to use the Web Control Framework wherever possible as it provides maximum flexibility and quick development plus a rich design environment. However, the framework is a little more complex to get started with and to really take advantage of it requires Visual Studio 2005 or Visual Web Developer. The Web Control Framework is likely to be the main focus of future extensions to Web Connection as well.

    The Web Control Framework has a fair amount of processing overhead compared to raw HTML output or using an ExpandTemplate() type approach. Keep in mind though that you can mix and match - it's quite possible to have some requests fire raw HTML processing with others going against the Web Control Framework.

    If complex reports or exact printing is required for the application PDF rendering is a huge timesaver and I'll take advantage of the ability to quickly get output generated in a rich format. PDF is the only reliable choice if precise client printing for forms is required.

    In distributed applications speed tends to be the most important factor as well as flexibility to convert data into formats like XML or encoded DBF files so that they can travel over the wire. Here tools like wwXML and wwIPStuff handle the data conversion mixed with Raw HTML generated typically with the Response.Write() method to write out the raw data into the HTTP output stream.

    If you're not sure which approach to use stop by the message board and solicit some input specific to your scenario. Lots of folks visit there with experience and insight that can provide valuable feedback.

    Scripting Support in Web Connection


    Scripting support in Web Connection provides the ability to move the display formatting of pages to external files rather than having your Fox code generate the user interface. In typical Web applications, these external script files become your user interface layer of the application. Moving files externally:

    Web Connection supports three scripting mechanisms of which the first is a special case:

    Scripting 101

    Scripts are little programs that are based on VFP's TEXTMERGE mechanism. Web Connection converts the scripts you create into a real runnable PRG file that outputs into a TEXTMERGE file which is captured and returned to the Web browser.

    Here's a simple example script page:

    <html>
    <body style="font:normal normal 10pt Verdana">
    <h1>Customer Table Display</h1>
    <hr>
    <%
    SELECT * FROM TT_CUST INTO Cursor TQuery 
    lnReccount = RECCOUNT()
    %>
    Customer Table has <%= Reccount() %> records.<p>
    
    <table border=1 style="font:normal normal 8pt Tahoma">
    <%
    SCAN
    %>
    <tr><td><%= TQuery.Company %></td><td><%= TQuery.Address %></td></tr>
    <% ENDSCAN %>
    </table>
    
    </body></html>

    To run this page save it to a .WCS (Web Connection Script) extension and call it via Web URL:

    http://localhost/wconnect/CustomerTable.wcs

    Notice that you use <% %> for blocks of script code that run like a program, and <%= %> to output expressions that return a value. <%= %> should only be used with a single expression while <% %> can host multiple commands (translated into TEXTMERGE terms <%= %> are TextMerge expressions, while <% %> is considered pure code. Anything else (the static HTML) is what goes between TEXT...ENDTEXT directives).

    If you have an error in your code the page will bring back an error. For example if I misspell Reccount() above as Recount() you get a Scripting Error Page that says:

    Error:  File 'recount.prg' does not exist. 
    Code: Recount() 
    Error Number: 1 
    Line No: 9 
    

    If you're in debug mode (DEBUGMODE .T. in wconnect.h) Web Connection will also try to find the line of code that caused the error and position the cursor on it. This is not always possible due to the nature of the error, but in most cases and especially those were a simple typo occurred you can edit the change save and re-run the request immediately.

    Fixing up the example
    Let's change the example around a little more to allow a little more flexibility. Let's fix up the display by adding the WestWind stylesheet, clean up the table display and add another field (phone) and finally support for asking the user which records he wants to see:

    <html>
    <head>
    <title>Customer Table Scripting Demo</title>
    <LINK rel="stylesheet" type="text/css" href="westwind.css">
    </head>
    <body>
    <h1>Customer Table Display</h1>
    <hr>
    <%
    lcCompany = UPPER(Request.Form("txtCompany"))
    lcWhere = ""
    IF !EMPTY(lcCompany)
       lcWhere = "WHERE UPPER(company) = lcCompany"
    ENDIF
    
    
    SELECT * FROM TT_CUST &lcWhere ;
       INTO Cursor TQuery ;
       ORDER BY Company
       
    lnReccount = RECCOUNT()
    %>
    
    <form method="POST" action="customertable.wcs">
    Company Filter: <input type="text" name="txtCompany" value="<%= lcCompany %>">
    <input type="Submit" value=" Get Customers">
    </form>
    
    <hr>
    Selected  <b><%= Reccount() %></b> records.<p>
    
    <table border="1">
    <tr bgcolor="#EEEEEE"><th>Company</th><th>Address</th><th>Phone</th></tr>
    <% SCAN %>
    <tr>
    	<td  valign="top"><%= TQuery.Company %></td>
        <td><%= IIF(EMPTY(TQuery.Address),[<br>],DisplayMemo(TQuery.Address)) %></td>
        <td><%= IIF(EMPTY(TQuery.Phone),[<br>],DisplayMemo(TQuery.Phone)) %></td>
    </tr>
    <% ENDSCAN %>
    </table>
    <hr>
    <HR>
    </body>
    </html>
    

    Note that this form has now become both an input and output form at the same time because we're using the form field (txtCompany) in a <FORM> tag to ask the user to enter the company filter for the list to create. On each request we then retrieve the user's input using the Request.Form method into a variable (which must be PRIVATE not LOCAL in order to be visible anywhere but in the current <% %> block) lcCompany. We use the variable to also echo it back in the edit box by using <%= lcCompany %> to set the VALUE= attribute of the txtCompany field. This way the field always displays the user's last choice.

    Script Modes
    Web Connection's scripting engine can use 2 modes to run scripts:

    Objects available in scripts
    Scripts have the following Web Connection objects available:


    These objects are the standard Web Connection objects and you can look up their properties and methods separately in the various class references.


    For additional examples of how to use scripting see the scripting samples on the Web Connection Demo page.

    Templates 101

    This topic is incomplete
    Templates are similar to Scripts using very similar ASP like syntax with <% %> and <%= %> tags. The main difference between scripts and templates is that templates are not programs but as the name implies templates that are evaluated. Web Connection uses a set of MergeText functions to walk through the document top to bottom and evaluate each expression or codeblock. Each codeblock or expression is evaluated exactly once which means you can't create blocks like the following:

    <% scan %>
    Some HTML <%= datafield %>
    <% endscan %>

    Since each expression is evaluated once the SCAN and ENDSCAN would be interpreted as individual commands and would cause an error. A template can however run complex code - it just has to happen in a single script block.

    You can however use any combination of expressions, UDF calls, object properties and methods and any PRIVATE variables that are in scope from the calling code.

    <HTML>
    <BODY>
    <%
    PUBLIC oCust
    oCust = CREATE("cCustomer")
    oCust.Load( VAL(Request.QueryString("PK")) )
    %>
    
    Welcome back, <%= oCust.cFirstName %>. Your credit limit is <%= oCust.GetCreditLimit() %>.
    
    <hr>
    Time created: <%= TIME() %>
    
    <%
    RELEASE oCust
    %>
    </BODY>
    </HTML>
    

    Notice that you can use code blocks, but because templates are not self-contained programs any variables that you want to use throughout the page must be declared as PUBLIC (oCust here). However, any regular expression can be accessed as needed.

    One thing to understand about Codeblocks is that they are evaluated using Randy Pearson's CodeBlock, which basically evaluates each line of code individually. This means loops etc. are very slow. Codeblock performs a lot of checking and parsing and thus codeblocks are fairly slow. For this reason we recommend that you don't use them excessively in template pages, but rather put your business logic into Process class code and then call the template from there.

    * Process method
    FUNCTION CallTemplate
    PRIVATE oCust
    oCust = CREATE("cCustomer")
    oCust.Load( VAL(Request.QueryString("PK")) )
    
    Response.ExpandTemplate( "\inetput\wwwroot\wwdemo\CallTemplate.wc")
    ENDFUNC
    

    We'll talk more about this process at the end of this topic, but this approach is much cleaner as the business logic that deals with setting up the request now runs in a easily debuggable and efficient VFP class and separates the business logic from the User Interface in the template page.

    Objects available in Templates
    Templates have the following Web Connection objects available to them:


    Note that the Response object is in parenthesis here! You can use the Response object, but only if and only if you use the llNoOutput flag on its various methods.
    Templates are evaluated into a string and that string is then sent to output all at once after parsing is complete, so using the Response object directly would cause output sent to go before any content in the actual template.

    If you need to modify the behavior of the Response object such as changing the HTTP header you need to first clear the object, and then reassign the custom header:

    <%
    Response.Clear()
    Response.ContentTypeHeader("text/xml")
    %>
    <?xml version="1.0"?>
    <docroot>
    <test>test value</test>
    <%= Response.Write("</docroot>",.T.) %>
    

    (note the use of the .T. parameter (llNoOutput) on the Response.Write method call! Most response method include this parameter)

    If you do this frequently you should consider using Process class methods to handle the header.

    oHeader = CREATE("wwHTTPHeader")
    oHeader.DefaultHeader()
    oHeader.AddCookie("TestCookie","Rick")
    
    Response.ExpandTemplate(Request.GetPhysicalPath(), oHeader)

    Scripts like templates can use expressions simply by embedding an expression into the HTML text with <%= %>. Templates should use expressions as much as possible for optimal performance as these are simply evaluated on the fly. You can of course call UDF functions, as well as method of any class that's in scope.


    Tip: Editing .wcs and .wc files in FrontPage or Visual Interdev
    By default these tools don't know about .wc and .wcs and won't allow you to edit these files as HTML files. In FrontPage you can select the file and right click and use the Open With... option to open the file manually. For a permanent mapping of the extensions to the FrontPage editor use the Tools|Options|Configure Editors dialog box to attach the extensions to the FrontPage editor. Simply copy the settings for the .HTM extension. In Visual Interdev you can open the file in the editor which will come up as an unknown (text) document at first. Close the file and then go to the Project Explorer pane and select the file still in the list. Right click and select Open With... and select the HTML Editor. The dialog that pops up also has a Set As Default button which creates a permanent mapping in VI to bring up the HTML editor for the templates.

    FrontPage Themes and special Bot extensions can't be used with foreign extensions. However, you can fool FrontPage, but choosing a scriptmap other than WCS or WC that it does support. Since IIS uses a number of scriptmaps that you won't need (like the old IDC/HTC extensions) you can map script or templates to those instead. All of FrontPage's features will work then correctly. The same can be applied with custom scriptmaps you create.


    How to call scripts and templates

    Scripts and templates can be invoked in two ways:

    If you call scripts directly via scriptmap the pages must be fully self-contained and provide and handle all inputs and outputs.

    But you can also run a Web Connection process method first and then from witin it call out to the script page. The advantage of this approach is that you can separate the user interface (the script page) from the business logic (the Process class method). In addition you can set up objects and create private variables that will also be in scope in the script page.

    * Simple Process method that calls a template
    Function CallTemplate
    PRIVATE oCust
    
    
    oCust = CREATE("cCust")
    oCust.Load( VAL(Request.QueryString("PK")) )
    
    ... additional processing against customer object
    
    
    *** Now display the template
    Response.ExpandTemplate( "\inetpub\wwwroot\myapp\CallTemplate.wc" )
    
    ENDFUNC

    The template can now use the oCust object as part of the script as long as oCust was declared as PRIVATE (the default if you don't declare it).

    <HTML>
    <BODY>
    
    Welcome back <%= oCust.cFirstName %>
    
    Feel free to shop around. Your current credit-limit is: <%= oCust.CalcCreditLimit() %>
    
    </BODY>
    </HTML>
    

    Notice that any PRIVATE variables you declare will be in scope and can be called from the template or script. Any Classes or UDF functions that are in scope or the call stack also can be called directly.

    Paths for ExpandTemplate and ExpandScript
    Notice that in the example above I hard coded the path of the template into the call to ExpandTemplate. In general that's a very bad idea. Instead you should do one of two things:



    Creating ASP Components from a Web Connection Application


    To create an ASP component from a running Web Connection Application you should create a new project with a different name so as to not disturb the existing Out Of Process COM component registration.

    Assume for a second that you have an existing Web Connection project WebDemo (see Step by Step Guide). To convert the project into an ASP project follow these steps:

    There are two ways that you can call your WC components from an ASP page:


    Note
    Because ASP is already a script map, you cannot use the script map syntax feature of Web Connection to route requests. So requests like HelloWorld.wp don't work. Instead the full URL syntax must be used:

    wc.asp?WebProcess~HelloWorld~positionalparm~&namedparm1=value1&NamedParm2=100

    Note that you can still use either positional or named parameters - just make sure you delimit the named parameters and positional parameters with both a terminating ~ and starting &.

    A few notes:


    A note about HTTP Headers

    This works and it actually works rather well. You can push data to the server easily here and the majority of requests work perfectly well. It even works in most cases if a custom WC HTTP header is created as long as the Response::ContentTypeHeader method is called at some point (which includes implicit calls to methods like HTMLHeader, ExpandTemplate/Script,StandardPage etc.).

    The problem is with headers that don't follow this latter rule. Basically, Web Connection overrides ContentTypeHeader() for ASP and captures the outbound HTTP header and then writes the header out using the ASP object using Response.ContentType and Response.AddHeader. This requires that HTTPHeader() is always stored as a string and never passed an output wwResponse object that writes to file. This unfortunately is required for ASP, because ASP cannot write a full raw HTTP response or even raw headers as Web Connection does.


    The Web Connection Management Console


    The Web Connection Management control allows you create new Web Connection project and update existing applications with new process classes. You can also use the Console to configure a server for operation with an already built Web Connection application.

    This mechanism is the preferred way to perform these tasks as it automates several manual steps into simple Wizards that can perform the tasks in a few seconds. In this section we'll show how the Wizards work and also explain what the they do behind the scenes so if you have to perform the tasks manually you will be able to do it manually.

    To start the Management Console from within VFP do:

    DO CONSOLE

    from the Web Connection install directory either from within Visual FoxPro or by clicking on the EXE in Explorer. Console.exe is free standing, but some features that create VFP code will not work from the EXE. You can also start the console from the Web Connection Menu.

    Note to VFP 7.0 users
    CONSOLE.EXE is compiled with VFP 8.0 by default unless you have run the VFP70.bat which swaps CONSOLE70.exe to CONSOLE.EXE.

    The Management Console's main screen is little more than a menu to goes off to the supported tasks:


    Important: All of the processes above require that you run out of the Web Connection installation directory and require support files in the following directories!

    The Configure server option requires the following subdirectories:


    The Create process and project options require the following subdirectories:

    The Setup option requires the following subdirectories:


    Management Console Command Line Interface


    The Management Console can also activate a number of options via commandline so you can by pass the main menu and perform various tasks directly through the management interface.

    The syntax is the key word, plus in some cases additional parameters you can pass.

    For example:

    DO Console WITH "SQLCONFIG"
    DO CONSOLE WITH "VIRTUAL","wconnect","D:\web\wconnect"
    DO CONSOLE WITH "AUTOLOGON"
    

    The following useful utility functions are available through the console:

    SCRIPTMAP
    Allows you to create a scriptmap for the appropriate Web Server. Run without additional parameters to get a list of parameters and options or with "UI" to get prompted for options.

    VIRTUAL
    Creates a virtual directory. Run without additional parameters to get a list of parameters and options or with "UI" to get prompted for options.

    DCOMIMPERSONATION
    Sets the impersonation for a COM server

    CONSOLE "DCOMIMPERSONATION","<ProgId>","<UserName>","<password>"

    Note that the username is not provided for accounts like Interactive or SYSTEM. If username is omitted Interactive is used.

    Note:
    This option requires that the DCOMPERMISSIONS.EXE file from the Tools directory is either in the current path or the TOOLS directory.

    DCOMPERMISSION
    Sets a single DCOM permission for a COM server for a single account you specify.

    CONSOLE "DCOMPERMISSION","<ProgId>","<Username>"

    Username can be either a System username (SYSTEM,IUSR_RASNOTEBOOK), Group (Administrators) or normal username.

    Note:
    This option requires that the DCOMPERMISSIONS.EXE file from the Tools directory

    AUTOLOGON
    Creates an Autologon entry in the Windows Registry. Warning: Use this feature with care as it can be a potential security problem to have your Windows system automatically log on.

    INSTALLPRINTER
    Allows you to install one of Windows' default printer drivers automatically. You can use this feature to easily install the "Apple Color LW 12/660 PS" Postscript driver that can be used with the wwDistiller and wwGhostscript classes to generate PDF output. This option will default to the above printer and allow you to type in a known Windows printer name.

    You can optionally pass in the name of a driver:

    CONSOLE.EXE "INSTALLPRINTER" ""Apple Color LW 12/660 PS"

    GOURL
    Runs any URL or Windows file association and displays the content in the Web browser.

    The following options access the various user interface wizards and helpers directly.

    MESSAGEREADER
    Starts the Message Reader application. Assumes you're running out of the Web Connection root folder.

    SQLCONFIG
    Starts the SQL Configuration Wizard

    CONFIGURE
    Starts the Site Configuration Wizard

    SETUP
    Starts the new Web Connection Setup routine.

    NEWPROJECT
    Starts the new project Wizard.

    NEWPROCESS
    Starts the New Process Wizard



    New Project Wizard



    The new project Wizard allows you to create a brand new Visual FoxPro project that contains all of the required Web Connection base files and a starter main process class that handles Web requests. This process is designed so you can quickly get up to speed creating a new server application skeleton framework within minutes.

    Once the Wizard completes you'll have a project and a main process class to which you can add your own code immediately. The Wizard also allows you to configure a new virtual directory as well as a scriptmap specific to your application.

    Step 1 - Create the project and process names


    Goto
    Step 2 | New Project

    In this step you specify the name of the project and the name of your main process class that will handle requests.

    In Web Connection terms these two options will create (the example assumes WebDemo as the project and WebProcess as the process name) the following:




    Goto Step 2


    Step 2 - Configure a virtual directory


    Goto
    Step 1 | Step 3


    This step deals with configuring a virtual directory. You can choose a path and make it a Web virtual directory. You can also copy a dedicated copy of the Web Connection DLL, which allows you to custom configure the server application. In general every Web Connection server application (a separate EXE/DLL) should get its own copy of wc.dll along with the configuration files that go with it.

    Creating the Virutal Directorty

    In order to create a virtual directory you have to specify the virtual directory as well as the path that this directory is to refer to. Virtual directories in general are used to provide logical access to a physical path on the hard disk - you can map a non-Web path (like d:\somepath) logically into the Web so that it can be accessed as http://localhost/myvirtual/. Virtuals also serve as an application entity - Cookies, Authentication and rights always apply from a given virtual downward, so in general it's a good idea to create virtuals for an application's base path (if it's not running at the root of the Web server).

    If you're installing Web Connection for the first time and you will always run out of the root directory you will probably want to create a wconnect directory to hold the wc.dll and config files.

    Copy wc.dll

    Each Web Connection server application should get its own copy of wc.dll and the second checkbox on the form lets you specify that the DLL should be copied into this virtual directory. A new copy of wc.ini which configures the ISAPI extension on startup is also copied and the temp path template field values are written into this ini file. These will be matched to the application ini file (WebDemo.ini) automatically.



    Goto Step 1 | Step 3


    Step 3 - Create a scriptmap


    This step allows you to configure a scriptmap for the new server application (or the new process class).

    Creating a script map

    The Wizard also allows you to configure a scriptmap that maps to this sub-application. This allows for script based request routing where the extension (.wpfor WebProcess) is always routed to the new class and the page name (Helloworld.wp goes to WebProcess::HelloWorld). The default wwProcess handler makes this type of request routing automatic.

    Specify the name of the script map (this is a file extension so keep it to 2-4 characters - no period). Then point it to the Web Connection dll (wc.dll) of the application that this process class will be hooked to.

    You also need to select your Web server so that the Wizard can properly configure the scriptmap and virtuals for you particular environment.


    Step 4 - Finish and create the project


    The final step actually goes and creates the appropriate files and a project. Click on Finish to let the Wizard do its thing.

    Note:
    Windows 98 Microsoft Personal Web Server Users will have to reboot their machine after the Wizard completes, if you created either a virtual directory or a new script map. Without the reboot, the virutal directory and script map will not be functional.

    Note to Shareware version users
    Since the Shareware version is precompiled you cannot successfully build an EXE file from a project. Therefore with the shareware version no project is built and you will see a dialog that points out this fact instead. Even though no project is built, you can still run the project successfully by running the main PRG file for the application: DO <yourproject>Main.prg. Other than missing the PJX and EXE files, everything else will be configured as described below.

    Once the wizard completes a new project window will pop up for. Note that the Wizard tries to compile the project, but in most cases is not fully able to do so because some files may be in use. The project pops up looking something like this:

    The following files are generated for choices of WebDemo Project and WebProcess Process:

    Recompile the Project

    Due to the way that the project is generated from within Web Connection's wizards some of the files may have been in use when the project was built resulting in a project that didn't get fully compiled. So, the first thing you should do is to recompile the project:

    1. Click the Build button
    2. Select Win32 executable/COM Server (exe)
    3. Check Recompile all Files
    4. Click OK and build the project
    5. Once the project has built go back into the project and click on the menu Project|Project Info
    6. Click on the Servers tab and set the Instancing Popup to Single Use. This is required for COM operation to make sure that each instance of Web Connection can be controlled individually from the ISAPI extension.

    You can recompile the project at any time with one of the following:

    1. DO BLD_WebDemo

    2. BUILD EXE WebDemo FROM WebDemo RECOMPILE

    3. Use the VFP Project Manager with MODI PROJECT WebDemo and then compile from there

    The former recompiles the project and also sets the DCOM configurations for the server - in general this is required only when you are ready to deploy your server.


    Run the Server

    You should now have a fully working project and EXE file. To start the server as an EXE file:

    DO WebDemo

    or to start it as a PRG file without compiling the project each time:

    DO WebDemoMain

    This lets you edit code and develop with SET DEVELOPMENT ON so that you can make instant changes to your code without having to recompile each time. For development this is probably the best approach as it make.

    Shareware Version Note
    Shareware version users can only run the latter PRG file, since no project nor EXE file will be generated by the Wizard.


    Step 5 - Setting up the server as a COM object (Optional)


    Web Connection supports servers as standalone applications and as COM objects. When running Web Connection under COM you gain slightly better performance and much better administrative control over your servers. COM objects can run without an NT logon, auto start, handle server crashes, support automatic code updates and much more.

    Note
    COM operation is optional. This is considered an advanced step, but it's provided here for logical continuation of the project building process.

    Creating a COM object from your server is rather simple. Follow these steps (using WebDemo as the project name here):

    1. Edit wconnect.h (or your custom app specific .h file) and set DEBUGMODE to .F. to enable all error handling in the framework.

    2. Run the BLD_Webemo.PRG file to rebuild the project. Make sure no errors occur during build.

    3. This should have rebuild your server into an EXE file.
      Note I'm using WebDemo as our sample server as before - you'll have to substitute your own servername in the code snippets below.

    4. Test the server with the following code from the command prompt:
      o=CREATE("WebDemo.WebDemoServer")
      ? o.ProcessHit("query_string=wwMaint~FastHit")
      

      This should generate some HTML output and a hit in the server window.

    5. If this looks good bring up the server's admin page in the browser with:

      http://localhost/webdemo/admin.asp

    6. Start your File Based server from VFP and then click on the Edit Configuration Settings on the Admin page. Scroll the top edit box down to the [Automation Server] section. Make sure that the ProgId name for the server properly reflects your server. It should be: ProjectName.ProjectNameServer. This is done automatically for you by the project Wizard. Return to the Admin page.

    7. Click on the Switch to Automation Messaging link in the top right column.

    8. Go back to the Admin Page and try one of the links. Quick View of Settings is a nice simple one.

    9. You should now see the COM object pop up on the desktop and processing the hit. Your Web page should show the result.

    Congratulations - you've just created your Web Connection server COM object for your application.

    For more details on what's involved behind the scenes see Manual COM Server Configuration.

    New Process Wizard


    Goto
    Step 1


    The new process wizard adds a new process class to an existing application. It does so by adding some code into a main startup file (WebDemoMain.prg) for example, and hooking it to a new PRG file containing the new process class template.

    The Wizard also allows you to configure a scriptmap that maps to this sub-application. This allows for script based request routing where the extension (.wpfor WebProcess) is always routed to the new class and the page name (Helloworld.wp goes to WebProcess::HelloWorld). The default wwProcess handler makes this type of request routing automatic.

    Requirements

    Web Connection's code insertion requires a specifc format in the main program files in order to be able to insert new process class hooks into the main program file. If the project was generated using the new project wizard or you're using the wcDemoMain.prg file the this will already be the case. If you're using an older version of Web Connection you will have to add the following lines in the your server's main Process class (WebDemo::Process for example):


          
     DO CASE
    
          CASE lcParameter == "WEBPROCESS"
             DO WebProcess with THIS
    
          *** SUB APPLETS ADDED ABOVE - DO NOT MOVE THIS LINE ***
    
          CASE lcParameter == "WWMAINT"
    	      DO wwMaint with  THIS
      OTHERWISE
         *** Check for Script Mapped files for: .WC, .WCS, .FXP
         lcPhysicalPath=THIS.oRequest.GetPhysicalPath()
         lcExtension = Upper(JustExt(lcPhysicalPath))
    
         DO CASE
    
         CASE lcExtension == "WP"
            DO WebProcess with THIS
    
         *** ADD SCRIPTMAP EXTENSIONS ABOVE - DO NOT MOVE THIS LINE ***
    
         *** Default Web Connection handling
         CASE lcExtension == "WC" OR lcExtension == "FXP"
            DO wwScriptMaps with THIS
            ...
         ENDCASE
    
         ...
      ENDCASE
    

    Once these comments are in the document, the wizard will insert routing code above those lines for the plain parameters (wc.dll?webDemo~HelloWorld) as well as script mapped parsing (HelloWorld.wp).


    Note you should also add any SET PROCEDURE,CLASSLIB, PATH and other commands that you expect to require in your custom code to the SetServerProperties method.

    Code Generation

    The Wizard relies on templates for generating the actual code of your classes and INI file settings. These templates can be found in the .\templates directory off the Web Connection root. The Process.prg template is used to generate a new Process class, which gets initialized to the name that you specify for the process class. You may customize these templates for your own needs although I recommend to stick to the minimal approach that is provided by default.

    The process class generated looks as follows:

    ************************************************************************
    *PROCEDURE WebProcess
    ****************************
    LPARAMETER loServer
    LOCAL loProcess
    #INCLUDE WCONNECT.H
    
    loProcess=CREATE("WebProcess",loServer)
    
    IF VARTYPE(loProcess)#"O"
       *** All we can do is return...
       WAIT WINDOW NOWAIT "Unable to create Process object..."
       RETURN .F.
    ENDIF
    
    *** Call the Process Method that handles the request
    loProcess.Process()
    
    RETURN
    
    *************************************************************
    DEFINE CLASS WebProcess AS WWC_PROCESS
    *************************************************************
    
    
    *********************************************************************
    * Function WebProcess :: Process
    ************************************
    *** If you need to hook up generic functionality that occurs on
    *** every hit, implement this method then call DoDefault() to
    *** get the default Request Processing functionality. See docs
    *** for more info on how to customize wwProcess::Process behavior.
    *********************************************************************
    *!*			FUNCTION Process
    *!*
    *!*			THIS.InitSession("wwDemo")
    *!*
    *!*			IF !THIS.Login("any")
    *!*			   RETURN .F.
    *!*			ENDIF
    *!*
    *!*			DODEFAULT()
    *!*
    *!*			RETURN .T.
    *!*			ENDFUNC
    
    
    *********************************************************************
    FUNCTION HelloWorld()
    ************************
    
    THIS.StandardPage("Hello World from the WebProcess process",;
                      "If you got here, everything should be working fine")
                      
    ENDFUNC
    * EOF WebProcess::HelloWorld
    
    
    *** Recommend you override the following methods:
    
    *** ErrorMsg
    *** StandardPage
    *** Error
    
    ENDDEFINE

    When you generate this process class you should be able to immediately access it with either:

    Looking at the code you can see that the Process method is generated but commented out. By default the wwProcess's Process method is used to handle all request processing. You should uncomment the custom process code if you need to provide any generic handling that needs to occur on every hit as the Process method is called before any other method in the class fires from a Web hit. This method is perfect for handling things like authentication, cookie checks and assignment, logging etc. Make sure that if you implement this method you add a call to DoDefault to call the default behavior, which is responsible for routing the request to the appropriate method of the class (like HelloWorld).


    Step 1 - Pick the project and process


    Goto
    Step 2 | New Process Wizard

    The first step is to select a main project file to which the process class is to be added. This page also lets you pick a name of the process to create.

    Keeping with the WebDemo example the main program is going to be WebDemoMain.prg. This file is always going to be a Web Connection mainline file. The Process name will be the PRG file that is to be created with a skeleton class also described in the above topic.

    Select your Web Server so the Management Console can create virtual directories and scriptmaps as needed.




    Step 2 - Configure optional Virtual and Scriptmap


    In this step you can set up a virtual directory and scriptmap for the new sub application you're about to create.

    Creating the Virutal Directorty

    In order to create a virtual directory you have to specify the virtual directory as well as the path that this directory is to refer to. Virtual directories in general are used to provide logical access to a physical path on the hard disk - you can map a non-Web path (like d:\somepath) logically into the Web so that it can be accessed as http://localhost/myvirtual/. Virtuals also serve as an application entity - Cookies, Authentication and rights always apply from a given virtual downward, so in general it's a good idea to create virtuals for an application's base path (if it's not running at the root of the Web server).

    Creating a script map

    The Wizard also allows you to configure a scriptmap that maps to this sub-application. This allows for script based request routing where the extension (.wpfor WebProcess) is always routed to the new class and the page name (Helloworld.wp goes to WebProcess::HelloWorld). The default wwProcess handler makes this type of request routing automatic.

    Specify the name of the script map (this is a file extension so keep it to 2-4 characters - no period). Then point it to the Web Connection dll (wc.dll) of the application that this process class will be hooked to.


    Create SQL Server Tables


    Web Connection supports creation of SQL Server tables to hold logging, session and async request information. This can be useful for systems that are otherwise using SQL Server to hold datafiles to remove any dependencies to local VFP tables. This may be especially important for large installations that are running on load balanced systems where multiple servers must share these session and log files.

    This Wizard takes you through creating a database or using an existing one to add the wwRequestLog and wwSession table to. Once the tables are created additional manual steps are required to actually configure Web Connection to use those tables, which is described in the last steps of this topic tree.

    Note to Shareware Version users:
    This feature is not available for shareware version users, since the implementation requires a recompile of the classes. You can create the databases, but the actual tables will never be accessed or read from.


    Step 1 - Select Database


    The first step takes you through specifying a database. You can either create a new database or add the files to an existing database.

    In most cases you'll want to add the tables to an existing database so you can use a single connection to access your own data as well as let Web Connection share that data connection.

    There are two connection options depending on how you want to create the database:



    Step 2 - Set Connection String


    Once you've decided whether to add tables to an existing SQL Server database or whether to create a new database, you have to let the Wizard know how to connect to the database.

    There are two connection options depending on how you want to create the database. In the previous step, if you chose:

    If you used an existing database you can also use a DSN connection as long as the required login information is provided. If left out, you'll get a SQL server logon dialog.

    If you create a new database you'll want to connect to the master database since the new database won't exist yet. Once created the Wizard will connect to the new database.

    Note, you can create the database on any server desired by specifying the Server= key in the connection string. The only requirement is that the login information is valid and allows to create the database and tables.

    Step 3 - Finish and configure Web Connection for SQL tables


    On the last page of the Wizard click Finish to create the database. If you encounter any connection problems the SQL Connection will prompt you for logon information. If the logon fails an error message will be provided.

    Once the creation has completed you need to configure Web Connection properly to use the newly created SQL tables.

    Add a SQL Object to your server's mainline code

    If you have a project that was created prior to version 3.30 you need to add the following block to your mainline server program which will usually be named <yourproject>Main.prg:

    #IF WWC_USE_SQL_SYSTEMFILES
    	SET PROCEDURE TO wwSessionSQL ADDIT
    	THIS.oSQL = CREATE("wwSQL")
    	IF !THIS.oSQL.Connect(THIS.oConfig.cSQLConnectString)
    	   MESSAGEBOX("Couldn't connect to system SQL Service. Check your SQL Connect string",48,"Web Connection")
    	   CANCEL
        ENDIF
    #ENDIF	
    

    This block establishes a persistent connection with the SQL Server.

    Tip for existing SQL Server users:
    If you already use some other mechanism to manage a SQL connection and stored the Web Connection system files into this database, you can create the SQL object and rather than connect to it, set the nSQLHandle property. Although not required you should also set the cConnectString property so in case of a connection failure the wwSQL object can retry the connection.

    Set the SQLConnectString property in your server's INI file

    Finally, the SQL connection needs a connect string in order to connect to the database. You need to use a full connection string or DSN definition. As shown above Web Connection reads this value from the server's INI file via the server's Config object. The config object persists its data in the server's INI file (<yourproject>.INI in the [Main] section) .

    Sqlconnectstring=driver={SQL Server};server=(local);database=WestWindTest;uid=sa;pwd=

    If you prefer to not store a configuration string in the INI file you can also hardcode the string in the SetServerEnvironment code above instead of reading it from the Server.oConfig object.

    Set the WWC_USE_SQL_SYSTEMFILES constant in WCONNECT.H

    In order to for Web Connection use the object set up in the block above you need to set the following switch in WCONNECT.h:

    #DEFINE WWC_USE_SQL_SYSTEMFILES    .T.

    This flag is used in several places in the Web Connection framework that deal with logging and the session. In particular the following places are affected:

    Fix any manual Session object usage (not using InitSession())

    The wwServer and logging is fully self-contained. Session access is self-contained only if you use the wwProcess::InitSession method to set up sessions. If your applications use manual wwSession objects you'll have to adjust your instantiation code slightly to accomodate the wwSQLSession object:

    #IF WWC_USE_SQL_SYSTEMFILES
       THIS.oSession=CREATE([WWC_SQLSESSION])
       THIS.oSession.oSQL = THIS.oServer.oSQL
    #ELSE   
       THIS.oSession=CREATE([WWC_SESSION])
    #ENDIF
    

    The oSQL property is persistent and is reused on all hits and used to perform all SQL Execute commands performed over a SQL Passthrough connection to the SQL Server.

    Make sure you recompile everything after making these changes so the change of the WWC_SQL_SYSTEMFILES flag is properly applied.

    Once you've done so, all of your logging information will go to wwRequestLog and the Session data will go to wwSession in the database you specified using the Wizard.

    Security


    When building database Web applications, security is important because confidential data might be traveling over the wire. You wouldn't want to capture orders online, including credit card numbers, and then have somebody hijack the entire order/customer file with that sensitive information. Security comes in many flavors and applies to different aspects of a Web site. Is the information passed over the Web safe? Are the resources on your server secure from outside access? How do you keep people from accessing certain parts of your application? Are passwords kept secure across the network?

    Windows NT provides excellent, though somewhat complex, security features that should address the majority of your security needs. NT allows configuration of files at the file level as well as the directory level. NT Security is extended to Web applications through NT Challenge Response (file-level access security) and Basic Authentication (HTTP application security, controllable via code).

    NT uses a special account called IUSR_MachineName (where MachineName is your computer's name) to identify anonymous users to the Web site, and rights must be given to this user for any public areas. Public areas include your Web root and any virtual directories that are accessed through the Web. The basic configuration is handled directly through the IIS service manager, which assigns the appropriate NT file rights without you having to mess with directory rights.

    In all other places, make sure you remove any IUSR_ references and the Everyone account (which shouldn't be there in the first place) to disallow unchallenged access to these non-private areas. If a user tries to access any of these restricted areas over TCP/IP or the Web, a password dialog will pop up, which allows authentication through NT Security just like you'd get through local access from the server.

    The IUSR_ account is key to Web security, so be careful when changing the rights of the IUSR_ account in the User Manager. While developing applications with IIS and COM, it's easy to give the IUSR_ account Admin rights to get some security issues resolved while debugging applications. That's fine for debugging, but in an online environment an IUSR_ account with Admin rights lets anybody get at all aspects of your site. Don't forget to set your IUSR_ account as a guest account before you put your site online.

    You can enforce security through several mechanisms on Windows Web systems:



    Windows Directory & File Security


    When running on IIS and most other Windows based Web servers Windows NT/2000 provides file based security. Standard security practices are extended over the Web by IIS in that file permissions are validated by the Web server and authentication is requested when the user does not have rights to access a file. Anonymous Web users come in through the IUSR_ account so any public content must allow access to this user.

    The IIS Microsoft Management Console handles most of this for you automatically - when you set up a Web directory it automatically adds IUSR_ into the access list with the file attributes for reading and script access that you provide through the virtual directory dialog. If you need to protect individual file, you can go into Explorer and remote the IUSR_ account and add special accounts as you see fit.

    Keep data in an unmapped path

    Web applications typically break into multiple parts-the Web pages, the application code (if it's extended via COM or plain Fox code) and the data. If you keep sensitive data on your Web server, make sure that the data is not accessible via a relative path over the Web. Ideally the data should reside in an off-limits area away from the Web site in an unmapped path. If the data can sit on another machine and be accessed over a non-TCP/IP network connection, you can just about eliminate your risk for data piracy (at the cost of overhead for the network access). Of course if you use a SQL Server, either locally or on a remote machine, this won't be an issue as the built-in security will control access to the data. The same goes for code, if you keep it on the same box as the server.

    If you want to be really secure and you have worries about people hacking into your box (doesn't happen, but paranoia is common, ya?) you can even move data off to another machine and then lock that machine down by not allowing access to it via TCP/IP. Use a different protocol to get at the data - NetBios or IPX, which makes it impossible for Web users to access this machine.

    Use NTFS partitions

    Use NTFS partitions on your hard drives if you want to set rights on directories and files directly. FAT partitions are a lot more difficult to configure for security because you have to set up shares. FAT is also slower.

    If you must have data in a Web-relative path so that the data can be updated online via FTP, make sure you set the proper password rights on these directories to disallow anonymous access by Web users. Web and FTP access rights work through NT security, so you can set them directly from Explorer by right-clicking and using the Directory or File Security dialogs. Note that Web directories typically have Read and Execute rights set (Special), and all publicly accessible directories include the IUSR_ account. Any private directories should remove the IUSR_ and Everyone accounts, and add only those users or groups that should have access.

    NT supports NT Challenge Response for access to files, which means that if you're accessing a page and IUSR_ doesn't have rights, NT will try to validate your user account through the local machine or domain if you have IIS configured to run through a specific domain server. If you are a user of the local network, you might not be prompted for a password. If you aren't, NT will request a login dialog and validate you against the server's local machine or domain accounts (depending on how you have IIS configured-by default, only local server machine accounts are used for login validation). If you type the correct password, you're allowed access. This type of security works both at the directory level (which really just delegates down to the file level) and the file level.


    HTTPS/SSL Encryption


    By default requests sent over the Web via HTTP are not encrypted in any way. Neither the data returned from the Web Server nor the data sent via POST data by the browser are encrypted, so if intercepted in anyway the data is easily readable. Many applications require secure communication between client and server in order to be able to transmit sensitive data. Examples include Credit card and contact information in e-commerce applications, login information, or sensitive medical data when dealing with an insurance company.

    In order to encrypt Web content, HTTP provides an extension of the HTTP prototocol - HTTPS - which provides secure encryption of data on the wire. HTTPS encodes the content using SSL which is the encryption protocol used.

    HTTPS/SSL use with Web Connection is completely transparent - SSL installation is entirely a server configuration feature.

    SSL is implemented at the Web Server by installing a secure certificate. Certificates must be purchased from a Certificate Authority (CA) reseller such as DirectNic (www.directnic.com). The big CAs are Network Associates, Verisign and Thawte among others, which are usually much more expensive than the smaller providers without providing any significant additional value.

    In order to install a secure certificate you need to apply for the cert, prove that your business is who you say your are (read: paper work that takes a few days), and a fee. Rates vary greatly so shop around - we use DirectNic, but there are many other certificate authorities available for comparable pricing. For installation instructions check your Web server documentation (IIS has exact steps of how to generate a request file and send it to the Certificate Auth) as well as the instructions by the Certificate Authority.

    Most certificate authorities provided very detailed instructions on how to generate certificate keys, send them to the provider, and then install the final SSL certificate public key. No need to do research up front, simply follow the directions provided by the Certificate Authority reseller.

    SSL in Web Connection

    Once installed, HTTPS usage is very transparent to your application. All you have to do to run securely is change your links to HTTPS:// instead of HTTP://. When the request data is captured everything looks just like a plain HTTP request. You can check for a secure link by checking the Server port with wwRequest::ServerVariables("SERVER_PORT") which by default is 443 for an HTTPS/SSL request. You can also use wwRequest::IsLinkSecure(cPortnumber) to check for a secure request.


    Automatically switching into HTTPS/SSL mode

    If you have an application that must be accessed in SSL mode you can automatically switch users into SSL mode by redirecting the current URL into HTTPS. Use the following code in your Process method of your Process subclass:

    lcPort =THIS.oRequest.ServerVariables("SERVER_PORT") 
    IF  lcPort #  "443"
       THIS.oResponse.Redirect( THIS.oRequest.GetRelativeSecureLink(THIS.oRequest.GetCurrentUrl()) )
       RETURN   
    ENDIF
    




    Application Level Authentication


    Web Connection 5.0 provides a number of ways to authenticate users through your application code:

    Basic Authentication


    Basic Authentication provides a built-in mechanism tied to the operating system and the HTTP protocol that validates users through application code. This allows your application to request and check for authentication dynamically, which is a common feature for non-public access applications.

    Basic Authentication works against Windows User accounts so users must exist as Windows Accounts in order to be used for Authentication.

    Turn on Basic Authentication

    First and most importantly make sure that you turn on Basic Authentication on your Web Server. On IIS go to the default Web site and select the Security tab. Enable Basic Authentication there.

    Using Basic Authentication in code

    Dealing with authenticating users is a two step process of asking for authentication and then checking for the authentication.

    The easiest way to do Authentication in Web Connection is to use the wwProcess::Authenticate() method, which handles both in a single method call. Login must be called in a central place or on every request that requires authentication. The most common place to use Login is in the wwProcess::OnProcessInit() method so that every request can be checked for a login:

    *** Check to see if ANY valid login entered IF !THIS.AUTHENTICATE("ANY") *** access denied - Auth Dialog pops up RETURN ENDIF ... access allowed - code continues

    This code should be called early on in a request either at the top of Process method, or in OnLoad() of Web Control page or - if the login can be globalized in some way - in wwProcess::OnProcessInit().

    If I log in with rstrahl, the first time this request is accessed rstrahl is not logged in so Authenticate() generates a request to authenticate the user through Basic Authentication. The Web Browser pops up an Authentication dialog. The user enters user name and password and they are sent to the server which validates them against the Windows Users set up on the server. If a match is found that username is returned as part of the HTTP request.

    If the user types in the correct Windows user information the same request that triggered this authentication request is re-run and the user is considered authenticated. Once the right credentials were entered IIS and the browser both will continue to pass the username forward without requiring logging in again.

    Logging out note:
    Note that with Basic Authentication there's no way to log out other than shutting down the browser. The browser and Web Server share a token that is passed back and forth and unless you shut down this token keeps on getting passed. The credential token is tied to a specific virtual directory.

    If you need to retrieve the username logged in on the server for logging or other operational purposes you can retrieve it with Request.GetAuthenticatedUser().

    wwProcess::Authenticate - high level logins

    Authenticate() basically manages the entire login process by checking for a login and if found letting code go on, and if not forcing the Authentication request back to the server. By the time Authenticate succeeds you're guaranteed that the request passed Authentication. You can use the username from GetAuthenticatedUser() if you need to prefill username information in an app or you need to log the user's name.

    The Authenticate() method takes a username or user identity to validate against:

    Once authenticated the user's credentials are passed forward from the client on every Web request until the browser is shut down or you force another login. This means you'll see the login dialog once, and subsequent hits simply read the valid username and continue on.

    Basic Authentication uses Clear Text Passwords

    Keep in mind that Basic Authentication sends a password over the wire! Therefore, login dialogs such as this are inherently insecure. If you're worried about security beyond password information, it's recommended that you also use an SSL/HTTPS request to force the password authentication. That way the request information will be encrypted on the way to the server.


    UserSecurity Authentication


    Web Connection 5.0 introduces a more sophisticated model for authentication based on the wwUserSecurity class. This class provides username and password validation as well as basic user info storage through a FoxPro table. Using the wwProcess::Authenticate method users can authenticate against this table. The Authenticate method is self contains and manages intercepting the HTTP request and displaying a login page that also validates the user info. If correct the user is forwarded to the required page - otherwise the Login page is redisplayed.

    The wwUserSecurity Class

    The wwUserSecurity class provides a simple class and cursor based lookup mechanism for retrieving username/password combinations. It supports creation of the table, adding and deleting of records and the familiar business object approach that Web Connection uses to hold registration data. The wwUserSecurity class is a base feature for the following enhancements.

    The class is very simple and the key method is Authenticate which is passed a username and password. If Authenticate succeeds - or you call GetUser or GetUserByUserName - an internal oUser member is set with the user information. oUser contains username, password, FullName, email, admin and a notes field. It also supports Get and SetProperty methods to add additional information. You can extend the underlying table with new fields and these fields show up in the oUser member. You can override the table name and the wwUserSecurity class can be overridden for customizations of using different tables or even completely overriding the implementation of how authentication occurs and user data is stored. As long as the interface of the class is maintained you can override anyway you like.

    wwProcess::Authenticate

    There's a new Authenticate method on the process class that deals with Authentication. Authenticate is a one stop method meant to be a one liner in any code and provide automatic user validation based on the Authentication mode in use. The method can use either Basic Authentication or the new UserSecurity approach.

    In this mode Web Connection displays a login dialog and validates the input against the table used by the UserSecurity class. You can override this class to use a different table or completely override the behavior for Authentication and operation of the authentication features.

    To authenticate is as simple as this:

    *** In the class header cAuthenticationMode = "UserSecurity" * cAuthenticationUserSecurityClass = "MyUserSecurity" FUNCTION TestFunction IF !THIS.Authenticate() RETURN ENDIF this.StandardPage("You've Authenticated as " + this.cAuthenticatedUser + " " + ; "Full User name: " + this.oUserSecurity.oUser.FullName) ENDFUNC

    Here's what it looks like:



    In order for this to work you need to add usernames and passwords and any additional information such as Admin status and fullname into a UserSecurity table. Web Connection then validates against this table.

    There a are a few new properties on the wwProcess class that provide for Authentication functionality:

    *** Basic UserSecurity cAuthenticationMode = "Basic" *** Class used for UserSecurity style authentication cAuthenticationUserSecurityClass = "wwUserSecurity" *** A user object for the authenticated user oUserSecurity = null *** The name of the user that was authenticated cAuthenticatedUser = ""

    You can specify the Authentication Mode (Basic, UserSecurity), the name of the authentication class if using UserSecurity (defaulting to the stock wwUserSecurity using a Fox UserSecurity table). After Authentication is successful the cAuthenticatedUser property is set to the user name and you can optionally access the oUserSecurity object including it's oUser member that contains the user details (username, password, name, admin flag etc.).

    The idea is that you can customize the operation of Authentication on your custom Process class. You can override the class if necessary to use custom tables or even override the functional behavior. For example the WebLog sample does the following in its custom Process class:

    DEFINE CLASS WebLogPageBase AS wwWebPage EnableSessionState = .T. *** Custom Properties oBlogConfig = null nBlogId = 1 && for now *** Stock Property Overrides cAuthenticationUserSecurityClass = "WebLogUserSecurity" cAuthenticationMode = "UserSecurity" ENDDEFINE DEFINE CLASS WebLogUserSecurity AS wwUserSecurity OF wwUserSecurity.prg cAlias = "WebLogUserSecurity" cFilename = "Weblog\data\WeblogUserSecurity" ENDDEFINE

    Once set up like this any calls the wwProcess::Authenticate automatically use these new settings.

    To use the Process in Web Control Pages is very easy as well. Remember that the Process class is always available as an instance variable Process. So you can do the following in the Page_Load for example:

    FUNCTION OnLoad() IF !Process.Authenticate() RETURN ENDIF this.lblMessage.Text = Process.cAuthenticatedUser + " " + ; Process.oUserSecurity.oUser.Fullname ENDFUNC

    Global Authentication

    If you want to manage on a more global level rather than at the page level, you can do that as well by hooking the Authentication into the OnProcessInit. The following example demonstrates how to authenticate any request made against an admin directory:

    FUNCTION OnProcessInit LOCAL lcParameter, lcOutFile, lcIniFile, lcOldError *** Add a global stylesheet to common HTML generation THIS.oResponse.cStyleSheet = this.ResolveUrl("~/westwind.css") IF ATC("/admin/",Request.GetLogicalPath()) > 0 IF !THIS.Authenticate() RETURN .F. && doesn't continue processing ENDIF ENDIF RETURN .T. ENDFUNC

    wwWebLogin Control

    The Page framework also includes a wwWebLogin control that can be dropped onto a page and you can use the control as a page level access validator. When not logged in the control displays as login display:



    and you can enter username and password into it. The control has a LoggedIn property you can query from the page:

    IF !THIS.Login.LoggedIn this.panelAdminContent.Visible =.F. ENDIF

    Based on the on the LoggedIn flag or the IsAdmin flag you can show or hide content as needed based on the users level of authentication. Once you're logged in the control displays as a tag that shows the user's login name:

    Of course you can also hide the control from the page with:

    this.Login.Visible = .F.

    after Authentication() succeeded.

    wwWebLogin as a Non-Visual Component

    The control can also be used via code directly. For example, in the WebLog sample there's a generic Authentication routine which looks like this:

    ************************************************************************ * WebLog_Routines :: IsAdminLogin **************************************** *** Function: Generic Admin Security routine that can be generically *** called from the admin pages to validate users and force *** a login. *** Assume: *** Pass: *** Return: ************************************************************************ FUNCTION IsAdminLogin() loLogin = CREATEOBJECT("wwWebLogin") loLogin.UserSecurityClass = "WebLogUserSecurity" IF loLogin.Login() AND loLogin.IsAdmin RETURN .T. ENDIF Process.ErrorMsg("Access Denied",; "<blockquote>The Administrative features require that you log in first. " +; "Please return to main page of the Web Log form first and log on from there.<p></blockquote><hr>",,; 3,Process.ResolveUrl("~/Default.blog#WebLogin") ) *** Shut down Web Control Framework Response Object Response.End() RETURN .F. ENDFUNC

    The WebLogin class's Login method manages all aspects of the login process from authentication to the retrieving and setting Session variables. Here based on the result of the login we display an error message in case the login fails.


    The wwUserSecurity, wwProcess and wwWebLogin classes all work together to provide a common Authentication experience.


    Windows Authentication and Scriptmaps


    This mechanism allows you to use Windows Authentication based on file permissions on the physical disk. The permissions on the physical file of a page determine how Authentication occurs and who's allowed access to a page. This option requires that EVERY request in the virtual directory has a physical page on disk - any non-page backed requests will result in a 404 error.

    In Web Connection the typical scenario for this is to use script map pages such as Web Control Framework pages or ExpandTemplate/ExpandScript pages. With pages on disk Windows will validate the file permissions against the user's permissions and validate or reject the user that way.

    Setting up script maps

    The key setup setting for this option is to make sure that you create one or more script maps for your application and make sure that you check the 'Check that File Exists' option. This option enables Windows to authenticate the page file permissions.

    Note that this is a non-default setting. Web Connection by default doesn't check this flag in order to allow non-page backed methods to be fired. Once the flag is set you MUST have a backing page or the request will fail outright.

    Tip:
    If you must have both Page backed and non-Page backed requests in a single application create two scriptmaps and map them both to the same process class. Set up on to require the page, the other not.

    When the flag is set IIS will check the directory permissions and provide authentication information. If the user is not anonymous and authenticates you can check the LOGON_USER server variable which can be retrieved with Request.GetAuthenticatedUser().

    In summary:


    Administration Requests

    Windows Authentication also works against requests of the wc.dll, but you can't access the DLL directly to do this because you cannot remove anonymous rights from dll - it needs those because it may serve non authenticated requests.

    So, you need to use a script map for those requests as well. it for admin uses. YOU HAVE TO USE A SCRIPT MAP WITH A FILE ON DISK OR Windows AUTH will not work.

    Steps:

    You don't have to create a separate scriptmap for administration - you can use the same one described above for the main application.

    At this point Authentication should work against any admin requests:

    http://localhost/wconnect/wc.Admin?_maintain~ShowRequest

    Note that if you are on Localhost or an Intranet, the login may just happen automatically without a login login dialog popping up. To verify you're logged in go to the link above and look Logged in username on the bottom of the form.

    Note:
    Windows Auth returns usernames usually as domainormachinename\account as opposed to just returning the Account like Basic Authentication does. So if you set permissions for specific accounts you might want to do use:

    AdminAccount=rstrahl,rasnotebook\rstrahl




    Securing your Web Connection Installation


    In addition to dealing with application based security you also need to thinK about securing your Web Connection installation. In particular, Web Connection ships with maintenance modules that are accessible through HTML interfaces which are Web accessible. If these interfaces are publically exposed they can cause serious security risks to your online application.

    The first step is to understand that Web Connection consists of two components, which both expose administrative functions:

    Enable Basic Authentication

    In order to use Web Connecion's internal security blocks against access you should enable Basic Authentication on the Web Server either for the entire Web site or for the virtual directory that your application lives in. Setting the Admin passwords will use Basic Authentication and NT Security for checking access to these functions.

    The ADMIN.ASP page

    The admin page (ADMIN.ASP) serves as a central administrative page to all the administrative features available. To secure admin functions your first step should be to restrict access to this page via NT file permissions. Allow only Administrators (or individual users) access to this page. To do this find the page with Explorer in your Web directory and use the Security settings and remove the IUSR_ account from access to this page and add in Administrators or the specific users you want to have access to this page.

    The wc.ini file and the AdminAccount key

    wc.ini (or a renamed version thereof that matches the name of your DLL) contains a number of important settings that control how your Web Connection server interacts with the Web server.

    The most important setting in terms of security in this file is the AdminAccount key which determines who has access to the wc.dll admin function. Admin functions are typically accessed through wc.dll?_maintain~Command syntax through the URL interface. These requests never hit your FoxPro Web Connection application - they're processed directly by the DLL and not passed any further.

    It's important that you secure the AdminAccount to an Administrative account or a list of accounts that will be granted access. For example, on my site I allow myself and Markus Egger admin access. So I have an entry like this:

    AdminAccount=ricks,megger

    Now anybody accessing any of the admin links will be prompted for a password and username. If you don't authenticate properly access will be denied to any of the admin links.

    Note
    If your security is extremely sensitive I recommend you only access the ADMIN.ASP securely through HTTPS. This will prevent possible hacking of passwords via basic authentication that sends passwords in clear (or easily cracked hashed format) over the wire. The additional HTTPS security protects your passwords from hackers.

    The wc.ini file AdminAccount setting is also the default security account used for the Web Connection server admin features. The following code in wwMaint.prg validates users using Basic Authentication:

       *** Only allow Admin account from WC.INI
       *** and authenticate. If no success don't let on
       IF !THIS.Login("WCINI")
          RETURN 
       ENDIF   
    

    The above code also uses the WC.INI admin account setting to validate access to any wc.dll?wwmaint~Command links. You can override this behavior to any that you choose, but the default is a good mechanism to provide the same security for both the wc.dll security as well as the Web Connection server security.

    Securing WC.INI

    In addition you can also lock down wc.ini itself so that nobody can view the file over the Web which is possible with the default IUSR_ permissions. This file contains semi-sensitive information, which by default can be openly viewed over the Web. If you do http://localhost/wconnect/wc.ini you will actually get the contents of the INI file in the browser. I don't consider this a big security risk as long as you followed the above steps, but some admins will want to lock down this file as well. You can do this by removing rights for all users but Administrators and the SYSTEM account. Make sure the SYSTEM account has access to the file, because that's the context wc.dll uses to access the file for various maintenance requests.

    Scripts and input forms

    If you rely heavily on template forms and ASP like expansion tags be aware that it may be possible for a remote user to post a message that containing script tags that may get evaluated on the server. Those script tags can potentially execute damaging code as the server side parses this code (this BTW is a problem for any script based engine like WWWC, ASP, Cold Fusion etc.).

    For example, if you take user input in any way and echo it back to the user and the user enters a script tag like <%= Version() %> you can potentially see the actual version embedded instead of the HTML markup! Obviously more dangerous commands and blocks of code could be used instead. The solution to this problem is to always translate posted code into display only HTML by using functions like FixHTMLForDisplay() which takes any HTML tags and converts them into display only text. Failure to do so can cause serious security issues as potentially all of the power of VFP is available to an outside user.

    Form and QueryString security

    If you are running dynamic queries and/or execute commands directly from user input you have to be very careful about the input you receive. It's possible that input can be formatted in such a way that the command that you are running - such as a SQL statement with a WHERE clause constructed with the Form() or QueryString() input - is actually formatted to execute a command. For example, it might be possible to smuggle in a call to EVAL, EXECSCRIPT, STRTOFILE or other VFP command. To prevent this Web Connection includes a security feature that filters Form() and QueryString() requests by checking for common dangerous expressions.

    To use this set the WCONNECT.H flag WWWC_FILTER_UNSAFECOMMANDS, or use the wwRequest::lFilterUnsafeCommands property to provide basic filtering of the input string. This filter function is limited at best but it provides a good baseline to protect for basic hack attempts - unless somebody is extremely familiar with the way Web Connection and VFP works and has some knowledge of the code beneath the request it'll be unlikely to be exposed to attack. For ultimate security, we recommend that you carefully filter any parameters that you pass to SQL statements or other macro or EVAL() type processes that execute user input directly.

    Web Services

    Generic Web services such as the HTTP SQL service (wwHTTPData) and the HTTP Remote COM method call interface (wwHTTPCOM) are potential security openings if left unchecked. By default these services use passwords to force logins. By default these won't work. New projects also don't install the hooks to use these services - they must be hooked up manually. However, the demo server has them open and connected.

    To disable these services check your main program's Process method and make sure the the wwhttpdata processing is commented out in the CASE statement.

    These services can be configured to allow access by specific users (cUsername and cPassword properties) and the SQL service can be configured to allow only certain SQL commands like SELECT or EXECUTE. The COM service allows inclusion or exclusion lists of objects so you get flexibility in what's allowed. View the appropriate docs for these objects for details.



    Implementing FoxPro based User Logins


    How do I implement user validation/login operations without using NT Basic Authentication using my own FoxPro tables/classes?

    The following Web Connection Process class demonstrates how you can implement a FoxPro based security system into a Web Application replacing Web Connection's built in NT Authentication Login method in the wwProcess class.

    The following uses a UserSecurity class which provides the ability to check a file on disk for logon information. You will need to replace this class with your own routines that handle displaying the HTML for the login dialog and validating users against records in your user database.

    The following example also uses Web Connection Sessions to save and verify users once logged in. Sessions provide additional security because they hide any fixed values behind a somewhat random Session ID which is not easy to spoof.

    The core of the code below happens in the Login() method, which handles checking whether a user is already logged in (using a Session variable in this case), displaying a login form if not logged in and also validating a user upon running the login form.

    The code below assumes you're running a URL like:

    wc.dll?usersecurity~HelloWorld

    When you do this, you'll see a login dialog first. On success you're then redirected to the appropriate URL. On failure the login dialog is displayed again with an error message - you won't get past the login unless you actually log in.

    Here's the code:


    *************************************************************
    DEFINE CLASS UserSecurity_Process AS WWC_PROCESS
    *************************************************************
    
    
    *********************************************************************
    * Function WebProcess :: Process
    ************************************
    *** If you need to hook up generic functionality that occurs on
    *** every hit, implement this method then call DoDefault() to
    *** get the default Request Processing functionality. See docs
    *** for more info on how to customize wwProcess::Process behavior.
    *********************************************************************
    FUNCTION Process
    
    *** Add this to the mainline program - here for demo purpose only
    *** No need to reload each time
    SET CLASSLIB TO UserSecurity Additive
    
    
    THIS.InitSession()
    
    Session = THIS.oSession
    Request = THIS.oRequest
    Response = THIS.oResponse
    
    IF !THIS.Login()
       RETURN
    ENDIF
    
    DODEFAULT()
    
    RETURN .T.
    ENDFUNC
    
    ************************************************************************
    * UserSecurity :: Login
    *********************************
    ***  Function: Demonstrates how a Login can be handled by using a 
    ***            Fox class using FoxPro tables to verify users.
    ***    Assume: This approach uses Sessions to store the final username
    ***            Also uses Sessions to save the original URL the user
    ***            wanted to go to.
    ***            Sessions are good for this because you can avoid
    ***            potential spoofing that could occur with plain cookies
    ************************************************************************
    FUNCTION Login()
    
    *** Put your own validation rules here 
    *** In this example we just check whether the cookie exists
    IF !EMPTY(Session.GetSessionVar("Username"))
       RETURN .T.
    ENDIF
    
    *** Remember where user wanted to go before login!
    lcFirstPage = Session.GetSessionVar("FirstPage")
    IF EMPTY(lcFirstPage)
       Session.SetSessionVar("Firstpage",Request.GetCurrentUrl())
    ENDIF
    
    
    pcErrorMsg = ""
    *** Now let's see if we submitted the login form
    lcUsername = Request.Form("txtUserName")
    lcPassword = Request.Form("txtPassword")
    
    IF !EMPTY(lcUsername)
       *** Check the user credentials
       oUser = CREATEOBJECT("UserSecurity")
       oUser.cFileName = ".\tools\usersecurity"
       oUser.cAlias = "usersecurity"
    
       IF oUser.Authenticate(lcUserName, lcPassword )
          *** Write the user into the session and then redirect
          Session.SetSessionVar("Username",lcUserName)
          Response.Redirect(Session.GetSessionVar("FirstPage"))
          RETURN .T.
          
          *** NOTE: IF YOU WANT TO USE A COOKIE INSTEAD OF SESSIONS!   
          *** Note: We cannot use Response.Redirect, since it won't allow
          ***       a Cookie to be written. So we go to an intermediate page
          ***       that sets the cookie and then automatically goes to the
          ***       originally specified page.
          * THIS.StandardPage("Logged on","moving on to your destination",,;
          *                  0,Session.GetSessionVar("FirstPage") ) 
          RETURN .T.
       ELSE
          pcErrorMsg = "Invalid Login"
       ENDIF
    ENDIF
    
    *** This code presents username and password dialog for user
    *** Expects txtUsername and txtPassword HTML form fields
    oUserDialog = CREATEOBJECT("wwLoginDialog")
    
    Response.HTMLHeader("Login Form")
    
    Response.Write([<p><b style="color:red;font:bold bold 12pt Verdana">] +;
                   pcErrorMsg + [</b>])
    
    *** User Dialog is returned as a centered table string
    Response.Write(oUserDialog.HTMLDialog(THIS,"wc.dll?Usersecurity~Login"))
    
    RETURN .F. 
    ENDFUNC
    
    
    Function HelloWorld
    
    THIS.StandardPage("Hello World","If you got here, you're logged in...")
    
    ENDFUNC
    
    ENDDEFINE


    Knowledge Base Topics


    This section of the documentation introduces several How To topics that are frequently asked about.

    What are HTTP Cookies?


    HTTP Cookies are a useful mechanism for tracking a client through a Web site. Cookies are a small state token that is stored in the client's browser that allows your server side Web application to keep track of the user on the other end. The cookie is stored on the client and sent as part of every request made against the Web server, so the server side application has access to this cookie.

    This is very useful for applications, like online stores that must track users so that the application can keep track of for example items dropped into a shopping cart on a per user basis.

    Cookie Persistance

    By default Cookies persist for the duration of the browser session - when the user shuts down the browser the cookie is released and goes away. You can also create persistant cookies which can have an expiration date in the future or with Web Connection simply 'NEVER'. When you set a cookie for future expiration shutting down the browser will cause the cookie to be written into a special cookie file on the client machine storing the state until the cookie expires. Next time the user visits the Web site that generated the cookie, the cookie will be sent to that server again.

    It's important to understand Cookies are always specific to the Web server they were created on. In other words, if Yahoo gave you a Cookie, Excite can't use or access that cookie in any way. Cookies are stamped with their target domain and virtual directory and can only be retrieved by the matching domain/virtual. However, on your local machine, cookies are stored in files so somebody spying around your machine can find the cookies and figure out where you hang out. You may not want your boss knowing about your porn site visits <s>...

    Overall the hype and paranoia about cookie security are unfounded. People are worried about being 'tracked' but cookies are just a vehicle that can be performed by other mechanisms just as easily. Cookies just make it easier for site developers to do their job and in many cases state keeping is simply required. You simply cannot create a shopping site without somehow tracking the user!

    What should be in a Cookie

    Cookies are typically used to maintain user state through a site. Ideally the actual cookie should be no more than a simple ID - this is mainly so users don't get paranoid about the information kept about them. You can use this key to store the real information that you need to keep track of in a database. Real information typically will consist of user information like customer name, address, order information and current order process such as item in a shopping cart. In all these cases the Cookie serves as the Id that identifies the user and based on that the current information of his visit.

    Who can use a Cookie

    HTTP Cookies require a browser that supports cookies and a willing user that will accept the cookie - browsers do have the capability to refuse cookies either with or without user prompting. By default most modern browsers accept cookies but options in these browsers let users turn off cookies, or prompt for acceptance. Because Cookies can be blocked and because they have gotten a bad name in the press some users are paranoid of cookies and refuse to take them so consider this in your application.

    Cookie Alternatives

    In lieu of Cookies other mechanisms can be used to implement Cookie like behavior, but they tend to be a lot more work. The most common approach used by most Web sites is known as 'license plating' which basically provides key information identifying a user as part of every URL used in the Web site. You may have seen these kinds of URLS:

    http://www.shopme.com/item.asp?sku=SKU1212&Id=312312kl12klj123

    The ID in this case identifies the user. While the above works, it's required that every link in the site pass this ID forward. If one link doesn't pass it forward the ID and the user's context or state is lost. Thus using license plates are more work to implement and difficult to debug once an ID is lost. Furthermore license plates are passed on the URL and can thus be spoofed. It's absolutely vital that IDs are unique and then IDs are not generated in a predictable manner - sequential numbering would be a really bad call. With the right ID it might be possible to highjack somebody's shopping cart.

    Jump to Implementing HTTP Cookies

    Implementing HTTP Cookies


    Web Connection provides built-in support support for setting and retrieving HTTP Cookies via two methods in separate objects:

    Creating the Cookie

    To create a cookie with Web Connection you have to use the wwHTTPHeader object:
    lcID = SYS(3)  && or whatever value you want to store.
    
    oHeader = CREATE("wwHTTPHeader")
    oHeader.DefaultHeader()
    oHeader.AddCookie("wwuserid",lcID,"/wconnect")
    
    *** Cause the Header to be written
    Response.ContentTypeHeader(oHeader)
    
    *** HTML document here:
    Response.Write("<HTML>Hidi ho</HTML>")
    

    This creates the header described above. There are actually several ways that you can write out the header created with the wwHTTPHeader object including using it's GetOutput() method or directly passing a Response object to the wwHTTPHeader object's Init method. See the wwHTTPHeader class docs for more details.

    The key thing is the AddHeader method which adds the Set-Cookie string to the browser. Not that you can create multiple cookies by making several calls to Addheader. It's best to not create more than one cookie per site - use data stored in tables about the user to retrieve and store any additional data. In short, minimize the data stored in cookies to avoid user apprehension.

    Reading a Cookie

    To read a Cookie you simply use the wwRequest::GetCookie() method and pass the name of the cookie. Keep in mind that the cookie must have been created on the client first. In other words, a cookie cannot be created and read in the same pass. The Cookie must first reach the client, then can be read on any successive requests. The cookie must also be in scope with the appropriate path on the Web server as described above. If you create a cookie for use in the /wconnect directory GetCookie() can only retrieve that cookie when the request actually fired in the /wconnect directory either via a request against wc.dll or a script page in that path or downward.

    Typical operation on the server side usually involves checking for a cookie and writing the cookie only if the cookie doesn't already exist. Basically, it's a write once, read many situation:

    *** Try to retrieve the cookie...
    lcId=Request.GetCookie("WWUSERID")
    
    *** Create Standard Header
    loHeader=CREATEOBJECT("wwHTTPHeader")
    loHeader.DefaultHeader()
    
    *** If not Found create the cookie
    IF EMPTY(lcId)
       *** Create the cookie
       lcId=SYS(3)
       
       loHeader.AddCookie("WWUSERID",lcId,"/wconnect")
       
       *** To specify a permanent cookie supply NEVER or a specific expiration date
       *** loHeader.AddCookie("WWUSERID",lcId,"/","NEVER")ENDIF            
    ENDIF
    
    *** Send Header and make sure to pass the Content Type (loHeader)
    Response.ContentTypeHeader(loHeader)
    
    ... more HTML generation here
    

    Note that the cookie is created only once (although you could create a new one on each hit - bad form though), when lcID is blank. Thus when the cookie already exists the IF block is bypassed and no cookie is written. Remember, once the cookie has been created on the browser, there's no reason to re-write it because the value comes down on every request until the browser shuts down or the Cookie expires.

    What does a Cookie look like?

    An HTTP Cookie is nothing more than an HTTP header fragment that is sent back from the server to the browser. A header containing a Cookie looks like this:

    HTTP/1.0 200 OK
    Content-type: text/html
    Set-Cookie: WWUSERID=43491556; path=/wconnect

    The Set-Cookie command instructs the browser to create the client side cookie. A Cookie can contain path information (in this case /wconnect) and expiration information which in this case is omitted. If the expiration is in the future the cookie is persisted to disk and can persist past a browser shut down. If like above, no expiration is provided the Cookie is considered session specific and goes away when the browser shuts down.


    Creating a permanent Cookie

    To create a permanent Cookie you have to specify a date in the future. The easiest way to to do this is with:

    oHeader.AddCookie("wwuserid",lcID,"/wconnect","NEVER")

    NEVER in this case is translated automatically into a date in the far future. You can also specify a specific date instead of never, but it must be a real date and it must follow GMT naming. For example:

    Sun, 27-Dec-2009 01:01:01 GMT

    Note:
    Dates in the far future beyond this date may cause problems on some browsers. This date works on all tested browsers.

    Cookie Paths

    Cookies are specific to the paths that they are created for. Above I set the cookie to be valid only in the /wconnect virtual directory and down. If you don't specify a path / or the root directory will be used by default. By using the root the Cookie is visible on the entire site.

    Why deal with specific paths? Some sites use lots of cookies and cookie strings are by spec limited to 256 character lengths although both IE and Netscape implement 1k strings or more. By partitioning Cookies into their virtuals you're avoiding overload of cookies and only retrieve the data you need in the appropriate location.

    Deleting Persisted Cookies

    There's no special command to delete a cookie. Instead to delete a cookie you simply have to re-create the cookie and not assign an expiration date or an expiration date in the past.


    Where should I put my Cookie Code?

    Placement of cookie check/creation code should be central, since most likely it has to apply on every hit of the application or sub-applet. Hence the best place for this is in your wwProcess::Process subclass, so that this type of check can be performed on every hit to your application. What I typically do is check the Cookie in the Process method and then call a validation method that does whatever it needs to to create the Cookie and validate the user. For e-Commerce sites this is typically something like wwMyApp::HomePage.

    FUNCTION Process
    
    lcID = THIS.oRequest.GetCookie("wwuserid")
    
    *** No Cookie - user must go to homepage/login page
    IF EMPTY(lcID)
       THIS.HomePage(lcID)
       RETURN
    ENDIF
    
    DoDefault()
    
    ENDFUNC
    
    
    FUNCTION HomePage
    LPARAMETER lcID
    
    IF EMPTY(lcID)
      lcID = THIS.oRequest.GetCookie("wwuserid")
    ENDIF
    
    *** Create Standard Header
    loHeader=CREATEOBJECT("wwHTTPHeader")
    loHeader.DefaultHeader()
    
    IF EMPTY(lcID)
       *** Create the cookie
       lcId=SYS(2015)
       loHeader.AddCookie("WWUSERID",lcId,"/wconnect")
    ENDIF
    
    *** Show HomePage from template and add header
    THIS.ExpandTemplate(THIS.oServer.oConfig.oMyApp.cHTMLPagePath + "homepage.wc",loHeader)
    
    ENDFUNC

    Redirect links and Cookies

    Many application require redirects to allow people to login and then go to another page after login is complete. Often times the first idea that comes to mind is to use HTTP Redirection to send the user off to a new page. Unfortunately, Redirection - or any other non standard (200 OK) output page HTTP result - does not allow additional HTTP headers to be added to the request, which effectively means you cannot Redirect and send a cookie on the same request.

    Instead you have to create an intermediate page contains redirect link in a META tag, or a phyical HREF link the user clicks on to go to the next page.

    HTML pages and browsers support the META Refresh tag which makes this possible. The refresh can be set up to refresh a page after x seconds and go to a specified URL at that time or refresh the current page. You can do this with HTML code like the following:

    <html>
    <head>
    <title>Attaching to your profile</title>
    <META HTTP-EQUIV="Refresh" CONTENT="1; URL=profile.wws">
    </head>
    <body>
    <a href="orderprofile.wws">Click here</a> if your browser isn't navigating to the profile form automatically.
    </BODY>
    </HTML>
    

    You can generate this HTML either manually or use the StandardPage or ErrorMsg wwProcess methods in your Web Connection request handler methods:

    THIS.StandardPage([Login Successful],;
      [<a href="orderprofile.wws">Click here</a>...],loHeader,5,[orderprofile.wws])



    Checking whether cookies are enabled in browser


    My application uses cookies. Is there some way I can check whether the user is accepting my cookies?

    Unfortunately no - there's no direct way that you can check to see whether the user has cookies enabled and more importantly whether he has accepted your cookie.

    However there are a couple of ways you can check for cookie existance in your application. Both ways require that you have a central entry point page for your site and if users access the site without using that sets either a cookie or persistent session var.

    Cookie Check
    If you use only cookies in an application you can simply check inside of the process method whether a cookie was returned on a request. If the request is not the entry page where no cookie would exist yet, you simply let the user know that cookies are required.

    FUNCTION Process
    
    lcCookie = THIS.oRequest.GetCookie("WestWindUser")
    lcMethod = LOWER(JUSSTEM(THIS.oRequest.GetPhysicalPath()))
    
    *** if cookie's not set for anything but entry point method
    IF EMPTY(lcCookie) AND lcMethod # "default"    
       THIS.ErrorMsg("This site requires HTTP Cookies","describe situation here...")
       RETURN
    ENDIF
    
    
    *** Call default handler
    DODEFAULT()
    RETURN

    Note that this works only if you have a central entry point for your application so that this check can be performed on every hit. If your app doesn't have a central entry point, then you need to check for this in each of the methods that you need cookies for.

    Cookie check via Session Variables
    If you're using the Session object which also relies on Cookies, things are a little more tricky because you can't check the cookie directly as the Session object abstracts this process. The way around this is to set a value in the Session and then check for this value on any subsequent hits. If the value doesn't exist the user has cookies disabled.


    FUNCTION Process
    
    THIS.InitSession()
    
    *** Make sure Cookies are on before proceeding!!!
    IF EMPTY(THIS.oSession.GetSessionVar("CookiesOn")) 
       lcMethod = LOWER(JUSTSTEM(REQUEST.GetPhysicalPath()))
       IF  lcMethod # "default"
          THIS.ErrorMsg("This site requires HTTP Cookies","describe situation here...")
          RETURN
       ELSE
          THIS.oSession.SetSessionVar("CookiesOn","True")
       ENDIF
    ENDIF
    
    DoDefault()
    
    RETURN

    The app will now automatically check whether cookies are enabled on every hit to the site except on the hit to the default page in this scenario.

    You can also move this code into individual methods. For example, I perform this check only in my ShoppingCart display routine in the Web store application, so that users can browse around the store without cookies, but can't add anything to the shopping cart. If you do this you have to be very careful to set the session var in all locations where the session is enabled - in my case a link back on the Cookie warning page that takes the user back to the home page.



    Understanding HTTP Headers in Web Connection


    HTTP uses headers to to communicate request information from the client to the server and the server to the client. Typically headers are hidden from you as the server and browser parses them behind the scenes.

    To get an idea of what an HTTP header looks like try the following from the command window:

    DO WCONNECT
    oip = CREATE("wwIPStuff")
    oIP.HTTPConnect("www.microsoft.com")
    lcData = "
    oIP.HTTPGetEx("/",@lcData)
    CLEAR
    ? oIP.cHTTPHeaders

    What you should see is something like this:

    HTTP/1.1 200 OK
    Server: Microsoft-IIS/5.0
    Content-Location: http://www.microsoft.com/Default.htm
    Date: Mon, 24 Apr 2000 20:47:26 GMT
    Content-Type: text/html
    Accept-Ranges: bytes
    Last-Modified: Mon, 24 Apr 2000 18:01:16 GMT
    ETag: "1057b71517aebf1:61bb"
    Content-Length: 15824

    <html>
    document content goes here...
    </html>

    An HTTP header is always made up of individual lines separated by CHR(13) + CHR(10) each and an empty line that separates the HTTP header from the document's content. The content of the document is described in the Content-Type header - in the example above text/html. The content type is extremely important as it tells the client what kind of content to expect. A browser for example looks at the content type to see at how to display the data. So you can send text/xml and be able to view XML or application/pdf to see an Adobe Acrobat document in its viewer.

    Every server generated request should output an HTTP header although at minimum it only needs to include a status code and content type:

    HTTP/1.1 200 OK
    Content-Type: text/html

    Content Length can also be useful if you're sending binary data to a client. Clients can use the content length value to figure out how much data they're downloading and potentially provide status information. For Web pages binary downloads require a content length to show you a progress dialog (downloading x of y bytes).

    How Web Connection handles HTTP headers

    In Web Connection headers can be generated in a number of different ways.

    When you see an HTTP header in your HTML output

    If you see an HTTP header in the HTML output of your page it means that you generated two headers instead of one. Most likely you used the wwHTTPHeader object to physically create your own HTTP header and output it into the HTML page. You then most likely called another method such as ExpandScript to output yet another header. The way to fix this is to use the wwHTTPHeader object and pass it to the header generating method:

    loHeader = CREATE("wwHTTPHeader")
    loHeader.DefaultHeader()
    loHeader.AddCookie("testcookie","rick")
    
    Response.ExpandTemplate("somepage.wc",loHeader)
    

    This causes ExpandScript to use the header you passed instead of the default header it would generate without the parameter.




    What's the difference between a template and a script?


    What is the template and a script since the syntax for both looks very similar?

    A template is like a full HTML page that includes VFP expressions and blocks of self-contained code. A script can contain much more flexible structures with pure HTML inserted between structural components.

    A template is evaluated from top to bottom by looking at each expression in the document and evaluating each and embedding hte content into the HTTP stream. A script on the other hand is converted into a full program that executes top to bottom. Scripts have the ability to easily switch between HTML and code in the same document including between structured statements while templates must accomplish their task within the individual expressions and tasks. In a template every expression or code block is an island of code, where a script acts as a comprehensive program.

    A really clear example of the difference involves outputting a VFP cursor to an HTML table. In a template, any block of structured code must be self-contained. Thus:

    <TABLE>
    <%  SCAN
         Response.Write( '<TR><TD>' + Field_1 + '</TD><TD>' + Field_2 + '</TD></TR>' )
        ENDSCAN
    %>
    </TABLE>
    

    Notice how, once in the code, you have to manually produce the HTML via Response.Write. Now look at the script version:

    <TABLE>
    <%  SCAN %>
         <TR>
           <TD><%=Field_1%></TD>
           <TD><%=Field_2%></TD>
         </TR>
    <%  ENDSCAN %>
    </TABLE>
    

    This is much more like true HTML. In fact you could easily imagine laying out the table with a visual editor, and then just inserting the SCAN loop.

    You can do anything with a script that you can do with a template, and much more. However, non-compiled scripts tend to run a lot slower than templates because they are created and then executed on the fly through Fox interpreter. For better performance of scripts you can compile scripts into native VFP code, but at that point you loose the ability to edit files without recompiling.

    The syntax for templates and scripts is similar, so it's easy to switch between the two both from the calling end as well as from the script end.

    For most applications I write I use templates with occasional scripts if the logic requires complex conditional or looping structures. Your preferences may vary.




    Run the server without User Interface


    It's very easy to have your server's run invisibly on the desktop by setting the ShowServerForm key in the application's startup INI file (wcDemo.ini for example).

    ShowServerForm=Off

    You're effectively removing any UI from the server. If you plan on running your server as an InProcess COM component through MTS this setting will be automatically enforced as the server loads. The property on wwServer is lShowServerForm.

    Why bother? First of VFP forms are notoriously memory hungry and even the small window increases VFP's resource use by almost a meg as VFP has to load the form engine.

    Second performance - the new wwServer class is no longer based on a form but on the RELATION class which is much more light-weight. The display update mechanism in WC 3.0 is actually calling into the form object via a method call. In version 2.x the display was simply updated - the current version that updates the display is slightly less efficient because of the indirect object access. Performance is nothing to loose sleep over with this, but if your app needs every ounce of horsepower to process hits, there's no reason to waste it on the UI. On my machine which is a P200 Notebook turning off the form results in a savings of between 1-2 hundreds of a second per request. Considering that the fastest request runs in .03 seconds this is not insignificant.

    Note
    Even with the form off the logging functionality still continues so if you need to check up on your server the Admin page's status lets you see whether the server's still running.


    Updating your Server Executable online



    Updating your Web Connection server online can be accomplished in a number of different ways. The basic procedure is the same although there are some ways that can automate this process. All of the procedures have one thing in common: You have to somehow shut down the executable to replace the application EXE file.

    Using the Update EXE Maintainence operation

    This is the preferred mechanism for updating EXE files on the server, but this mechanism works well only if you're running your Web Connection Server as a COM object. It will also work for standalone applications, but the process is not fully automatic.

    This mechanism relies on a couple of entries in the wc.ini file to tell it where to find the application executable and the location of an file to update it with using the following keys:

    ExeFile=d:\wwapps\wc\wcDemo.exe
    UpdateFile=c:\temp\servers\wcdemo.exe
    

    COM Operation


    When you click the Update Exe link on the maintainence page under COM operation all servers are shut down first and all incoming requests are blocked for the duration of the update process. Then the new file is copied over the old one and the block is released. This operation takes only a few seconds if everything is set up correctly so you can do this with minimal downtime.

    Warning
    The COM server that is updated on the server must have the same ClassIds as the server it is replacing. The Update procedure does not re-register the server, so if the ClassIds are different the new server will not be found and the server will not run. To avoid this make sure you build your COM objects on the same machine from the same project. If ClassIDs are changed for whatever reason, perform a manual code update and re-register the COM object on the server by running it there with the /regserver switch (From the DOS box: MyServer.exe /regserver)

    File Based Operation

    You can use this mechanism with File Based operation as well, but you will need to manage shutting down the running Web Connection server applications. You can use the Administration Page links to shut down the servers by repeatedly killing each server or by using the Process Viewer component on the bottom of the Admin page. You can then run the Update Exe link to update the files as above.

    The key difference here is that once the Web Connection servers are dead in File Based they won't automatically restart. You need to restart them by using the StartExe link on the Admin page which must be properly configured. The server will run invisibly and it will run under the SYSTEM account when it starts. This security context has several implications - SYSTEM generally doesn't have network rights to access files over the network. SYSTEM by default does have access to most local resource however.

    I highly recommend you experiment with the StartExe option before shutting down all of your servers on an online server! Make sure StartExe works before relying on it to restart your apps preferrably when you can sit in front of the box.

    Running long requests in Single COM mode


    When running long requests that could potentially hold up your server pool you can use a special COM operation mode which allows you to instantiate a server once, run the request and unload the server after completion of the task. This allows for long running operations like reports to process independently of the Web Connection Server pool.

    Some applications may be things like credit card processing, communicating with a terminal program, running a complex query, or communicating with another server over HTTP.

    How it works

    In order to use this feature your application must be able to function in COM messaging mode. You can either use an existing COM object to handle these requests or set up a special server altogether to handle the request and potentially run this server even on a remote machine.

    This feature is implemented as an querystring parameter on the url. For example, to hit the following URL:

    http://localhost/wconnect/wc.dll?wwdemo~TestPage

    in single request mode you'd change the URL to:

    http://localhost/wconnect/wc.dll?wwdemo~TestPage~&instancing=single

    or on a script map page:

    http://localhost/wconnect/testpage.wwd?name=Rick&company=west+wind&instancing=single

    or without any querystring parameters:

    http://localhost/wconnect/testpage.wwd?instancing=single

    Note: This key value pair must be typed all lower case to work correctly!

    When you run the latter link you'll see an additional instance of your Web Connection server pop up, process the request and then release and disappear.

    Note
    This feature is available only when running COM messaging. If running filebased the instancing querystring variable is simply ignored and the request is processed as normal.


    Running multiple Web Connection Apps on a single server


    Web Connection servers are designed to run as self-contained Visual FoxPro EXE files that can run independently of each other. Because each server interacts with the Web server through the Web Connection ISAPI extension each application requires a separate copy of wc.dll in a separate directory on the server.

    The easiest way to create a new project is to use the Web Connection Management Console which takes you through the process. Make sure you pick the correct Web server and then proceed to specify a virtual directory for the application. Make sure you pick a separate directory, separate copy of wc.dll and a unique directory for your temp files:

    It's very important that you select the 'Copy separate copy of wc.dll into virtual' and pick a temp directory/template combination different from any existing Web Connection application to avoid having the two applications pick up message files from each other.

    While the TEMP directory affects only file based operation, COM operation derives it's distinguising characteristics from different ProgId for the COM object created in your project. COM objects are generated with the name of hte project: Project.ProjectServer. COM applications thus distinguish themselves directly from each other through this class ID. Howver, a separate copy of wc.dll is still required in order to hold the instantiation information for these COM objects.

    If you're not using the Management Console

    If you need to retrofit applications to run on a single server you can do so manually by editing the server and ISAPI INI files. The key items are:

    wc.ini:
    PATH=c:\temp\wc\
    TEMPLATE=WC_

    Your application INI file (project.ini):
    tempfilepath=d:\temp\wc\
    template=WC_

    These paths and temp directories must match. Start and stop the Web Server (or you can update the changes for wc.ini from the Administration Web page).

    Running the same Server as separate Applications

    In some cases you may need to run the same application on the same server. This means the same EXE file and same server names are used and a separate install is used to run against a different set of data.

    There are two issues that make this process a bit involved:

    In order to do this it's very important that you set the wwServer::lUseRegistryForStartupPath property to .F. Without this flag set Web Connection looks in the registry to find the server path and always starts out of the directory it finds there. If multiple applications are using this path they all point to the same directory which is not the desired result.

    By setting the flag to .F. Web Connection will not look in the registry and instead always look for configuration in the EXE's startup directory. This allows Web Connection to run the same exact application multiple times on the server.

    Prior to version 4.60 this could also be accomplished with the following code in YourAppServer::SetServerEnvironment:

    THIS.cAppStartPath = GetAppStartPath() && from wwUtils SET DEFAULT TO (THIS.cAppStartPath) THIS.cAppIniFile = THIS.cappstartpath + THIS.cAppName + ".ini" ... THIS.oConfig = CREATE("wcDemoConfig") THIS.oConfig.cFileName = THIS.cAppIniFile

    This basically overrides the default that wwServer.GetAppStartPath() retrieves and connects to the new INI file and path. You can also use code like the above to use custom logic to find your server's startup path and INI file.

    COM Operation

    If you're planning to do this with COM you have to do some additional work. The problem is that COM can only register a COM object in exactly one location pointing at exactly one EXE file. Hence you cannot register a COM object in more than one place.

    Separate EXE servers
    This means that you either have to build another totally separate EXE for each application even if they are using the same code base or come up with a startup path scheme for a single COM server object. If you go that route make sure that you rename your server class so the COM ProgId changes.

    If you need separate EXEs then you need to make sure to:


    The above ensures that the ClassId and ProgIds are different for each server. Note that you can build your second project using the same source files as the first one with the exception maybe of the main Server class PRG. So although you have a different EXE you can still utilize the same exact codebase.

    The main drawback of this approach is administration of two distinct servers.

    Single EXE for shared apps
    Another approach is to use a single COM server for multiple applications. Rather than build multiple EXEs you build a single EXE that routes to the proper directory. Inside of the EXE you then have multiple COM objects that have slightly different startup behavior but are otherwise the same.

    The following creates three separate servers in MyApp.exe:

    DEFINE CLASS MainServer as wwServer ... Main Web Connection Server implementation from your app ... This would be default code as usual ENDDEFINE *** Implement a second class that inherits from the first DEFINE CLASS MySecondaryServer as MainServer OLEPUBLIC *** Startup Path - retrieve from main INI file possibly cStartPath = "d:\webapps\MySecondApp\" FUNCTION SetServerEnvironment *** Base behavior DoDefault() *** Set a new startup path - Server will change to this directory THIS.cAppName = "My Second Server" THIS.cAppStartPath = ADDBS(THIS.cStartPath) ENDDEFINE *** Implement a third class that inherits from the first DEFINE CLASS MyThirdServer as MainServer OLEPUBLIC *** Startup Path - retrieve from main INI file possibly cStartPath = "d:\webapps\MyThirdApp\" FUNCTION SetServerEnvironment *** Base behavior DoDefault() *** Set a new startup path - Server will change to this directory THIS.cAppName = "My Second Server" THIS.cAppStartPath = ADDBS(THIS.cStartPath) ENDDEFINE

    Above you'd using some custom logic to figure out the StartupPath based on the class. The easiest and most configurable way likely would be to read configuration settings from the main server's INI file at startup to figure out the directories.

    Using this approach you can maintain a single COM EXE and effectively run them out of separate directories. All you need to do is specify the correct server in the wc.ini [Automation Servers] section.

    For the first app:
    Server1=MyApp.MainServer
    Server2=MyApp.MainServer

    For the second app:
    Server1=MyApp.MySecondServer
    Server2=MyApp.MySecondServer

    and so on. Each would live in their separate wc.ini file for the new application, but each would use the same EXE server.

    Realize that this approach means that all servers are indeed identical and are based on the same codebase, so this works only if the codebase is maintained as a single base.

    Access Denied Errors when loading COM servers


    When switching into COM operation I'm unable to get my server to come up. Instead I get an error message:

    Couldn't instantiate <serverProgId>. CoCreateInstance failed: Access Denied

    This error message implies that the COM server could not be started because the DCOM permissions are insufficient to launch the server. If you used DO BLD_<server> to build your server most of the DCOM configuration settings were actually made for you. However, under Windows 2000 some additional one time settings that cannot be automated may have to be made in the DCOMCNFG utility.

    In Windows NT 4.0 the IUSR_ and IWAM_ users were automatically available in the default security - in Windows 2000 I've seen no users at all in RC2, with the users there in RC1.

    Note that the calling security context is determined by the setting of the IIS application. Default mode is Medium security (Pooled) which is accessed through IWAM_<machinename>. Low (IIS Native) is accessed through IUSR_<machinename>. High security is administered through IWAM_<appid> - you can check the custom app ID by looking in the Security tab of the application.




    What is a script map?



    A script map is a map to an executable on the Web server, in the case of Web Connection wc.dll.
    You can set up a script map to wc.dll for a certain file extension (like .wwc for example)
    so that all .wwc files are mapped to wc.dll and thus to your WWWC application.

    So anytime somepage.wwc is called wc.dll is actually executed and the call is forwarded
    to your Web Connection application.

    In addition to being more readable and easier to remember scriptmaps also work
    in all directories they are scoped to so you can avoid problems with having to
    reference back wc.dll explicitly all the time.

    The idea is that with a script map you can use a more 'natural' naming mechanism with
    your dynamic Web pages.

    Instead of:

    http://localhost/wconnect/wc.dll?wwDemo~HelloWorld

    you can do:

    http://localhost/wconnect/helloworld.wwd

    or even:

    http://localhost/Helloworld.wwd

    where .wwd is mapped to wc.dll and .wwd is tied to the wwDemo process.

    To configure a script map manually in IIS you can use the IIS Admin Console,
    Home Directory, Configure and add the map there.

    To configure Web Connection for using your script map, you set up a map to the script
    extension in your Server's Process method in the large CASE statement. In the demo
    that'd be wcDemoMain.prg in the Process method. You'll see all the scriptmaps defined
    there and you can add the new one there.

    The new Project or New Process Wizards set this all up for you including creating the
    script map for you, but if you want to do this via code you can use either the
    wwAppWizard or wwWebServer classes.


    IntelliSense and Web Connection Objects


    It would be nice to get Intellisense on Web Connection objects wouldn't it? Unfortunately by default you don't get for the Intirnsic objects such as Request, Response, Server etc. because these objects are scoped PRIVATE higher up the hierarchy.

    There's a hack to work around this though: You can use #IF .F. block to add LOCAL declarations for these objects into your methods. Like this:

    FUNCTION WCDoSomething
    
    #IF .F. 
    LOCAL Request as wwRequest, Response as wwResponse
    #ENDIF
    
    *** Now Intellisense works (assuming wwRequest is loaded in memory)
    Response.Write(...)
    
    ENDFUNC

    You can take this a step further and use an Intellisense Script to create each new method with this code already embedded in it:

    LPARAMETER oFoxCode
    LOCAL lcCmdLine, lcClassName, lcFunctionName, lcOutput
    
    IF VARTYPE(go_FoxCodeLastClass) = "C"
       lcDefaultClass = go_FoxCodeLastClass
    ELSE
       lcDefaultClass = ""
    ENDIF
      
    lcClassName = INPUTBOX("Class Name","Class Definition",lcDefaultClass)
    lcFunctionName = INPUTBOX("Function","Class Definition")
    
    IF EMPTY(lcFunctionName) or EMPTY(lcClassName)
       RETURN lcCmdLine
    ENDIF
    
    *** Save the class used
    PUBLIC go_FoxCodeLastClass
    go_FoxCodeLastClass = lcClassName
    
    oFoxcode.valuetype = "V"
    
    TEXT TO lcOutput TEXTMERGE NOSHOW
    ************************************************************************
    * <<lcClassName>> :: <<lcFunctionName >>
    ****************************************
    ***  Function:
    ***    Assume:
    ************************************************************************
    FUNCTION  <<lcFunctionName>>()
    
    #IF .F. 
    LOCAL Request as wwRequest, Response as wwResponse
    #ENDIF
    ~
    
    ENDFUNC
    *  <<lcClassName>> :: <<lcFunctionName >>
    ENDTEXT
    
    
    RETURN lcOutput



    Creating a Scriptmap on IIS


    A scriptmap is an application mapping that maps a file extension in your Web Virtual directory at a specific handler application - in Web Connection's case a copy of wc.dll. Scriptmaps make it possible to have simpler URLS like this following:

    HelloWorld.wp?Id=Test

    and have it map to your application and the Helloworld method for example.

    Creating Script Maps in IIS

    Script Map Scope

    Scriptmaps can be created at the Root and effectively become global for the entire Web site, or at the Virtual directory level. It's preferred to install script maps at the directory level if possible. Installing at the directory level gives you the ability to use the same script map in multiple applications.

    Creating Script Maps from within Web Connection

    Web Connection will automatically create script maps for you when you install Web Connection and whenever you create a project with the
    New Project or Process Wizards. Scriptmap configuration is also provided in the Configure Your Server Wizard in the Web Connection Management Console. In addition, you can also run:

    DO CONSOLE WITH "SCRIPTMAP" DO CONSOLE WITH "SCRIPTMAP","UI"

    which lets you create scriptmap directly from within Web Connection. The UI dialog is also available from the Web Connection menu (started with do wcstart.prg). A full command line might look like this:

    DO CONSOLE WITH "SCRIPTMAP",".wc","c:\westwind\wconnect\wc.dll",.F.,; "IIS5","IIS://LOCALHOST/W3SVC/1/ROOT/wconnect"

    The last parameter is optional and if omitted creates the scriptmap at the root of the default IIS Web site (1). Using the syntax above allows you to specifically apply the scriptmap only to a particular virtual directory - wconnect in this case.

    Programmatic access is also available as part of the wwWebServer class.

    Mapping a Script Map to a Web Connection Process Class

    Script maps from IIS route to your Web Connection application. Once they arrive there the script map still needs to be routed to a specific Process class. For example, the Message Board extension is .wwt and it's routed to the wwThreads Process class in wwThreads.prg.

    This mapping occurs in the <yourapp>Main.prg file in the server's Process method:


    ************************************************************************ * wcDemoServer :: Process ************************* PROTECTED FUNCTION Process LOCAL lcParameter, lcExtension, lcPhysicalPath *** Retrieve first parameter lcParameter=UPPER(THIS.oRequest.Querystring(1)) *** Set up project types and call external processing programs: DO CASE CASE lcParameter == "WWTHREADS" DO wwThreads with THIS CASE lcParameter == "WWDEMO" DO WWDEMO WITH THIS ... OTHERWISE *** Check for Script Mapped files for: .WC, .WCS, .FXP lcPhysicalPath=THIS.oRequest.GetPhysicalPath() lcExtension = Upper(JustExt(lcPhysicalPath)) DO CASE CASE lcExtension == "WWT" DO wwThreads with THIS CASE lcExtension == "WWS" DO wwStore with THIS ... ENDCASE ... ENDFUNC

    Note that you can map both a parameter map and an extension. The parameter is for backwards compatibility and really shouldn't be needed. The script map extension is set up in the lower block where you map the extension and point it at your specific process class for your application.



    Removing multiple DCOMCNFG references for your COM server


    When creating COM based projects it's extremely important to be careful of how you generate your COM objects and how they get registered on the Web servers that they run on. Basically, COM is based on unique CLASSIDs that determine the entries in the registry for your server. When a project is built into an EXE or DLL file in Visual FoxPro the CLASSID is part of the project - create a new project and the CLASSIDS differ. This can cause problems if you register the server, then copy an update with a different classid to the Web server resulting in a server that doesn't respond. Even if you fully re-register your servers with /regserver and DCOMCNFG you may still have problems with multiple orphaned entries in the registry and DCOMCNFG. This is a COM issue and occurs with any COM server built in this fashion in any development environment. It's crucial you build your servers consistently before deploying. The appropriate steps are:

    Cleaning up the registry and DCOMCNFG

    If you do end up with multiple servers having been registered you can end up with multiple DCOMCNFG entries for the same server. The real server will always show as a Local Server and will have 4 or 5 (in Win2000) tabs of information available including the first page that contains the path to the server. All invalid references will show as Remote Servers. You have to clean up these entries in the registry and do so manually. Start by unregistering the server with <yourexe> /unregserver. Then go into RegEdit and find all reference to your ProgId and then clean up each of the subtrees that they are associated with by deleting them out of the registry. There will likely be a lot of references to be deleted (at least 4 per entry in DCOMCNFG). When you're done, go into DCOMCNFG and make sure the server listing is gone. Once that's done, recompile the project or re-register the server and make sure you always compile the same project for the final file that goes on the server. It's a hassle, but unfortunately that's how COM works.

    Framework Classes


    The Web Connection Framework classes are responsible for passing data between the Web Server and your application code. The base classes consist of the Server, Process, Request and Response objects. The wwServer and wwProcess objects that control application flow and mechanics with the wwRequest and wwResponse objects serving as message objects that handle the inputs and outputs of the application.

    wwServer

    This class deals with passing information between the Web server and your server application. It communicates with whatever server mechanism is in use (File based, COM, or ASP messaging) and passes that information on to your application code through its framework. The input is converted into the wwRequest object which your code can use to retrieve input from the Web client application retrieving data such as HTML form varrs, Server variables, Cookies, security information and so on. Based on the URL 'parameters' on the querystring, Web Connection routes the request to the appropriate Process class, which can be thought of as a 'sub-application'. wwServer does this through it's Process method which acts like a request router that reads the URL and figures out which sub app to send the request to.

    Every server application you build subclasses wwServer with a custom implementation that overrides several methods for configuration: SetServerEnvironment and SetServerProperties, which are used to configure the server for your specific environment. You also override the Process method which will include a CASE statement for the various sub-applications you choose to implement. You can use the New Project Wizard to set up a new Server Application.

    wwProcess

    The wwProcess class is your main entry point where Web specific code gets written. The server passes control to a subclass of the wwProcess object that you implement. In this class every method you create handles a specific Web request based on the URL 'parameters'. For example, wc.dll?wwDemo~TestPage loads the wwDemo object and calls the TestPage method. Each of the methods can take advantage of the Request object for input, and the Response object for output.

    Every sub-application you create must subclass the wwProcess and implement the individual handler methods. To create a new process class you can use the New Process Wizard which automates the process.

    wwRequest

    The wwRequest is responsible for providing your application with input that is specific to the currently running request. The request object provides information about HTML form variables (HTTP POST data really in various formats including POST, Multipart forms and XML posting) and Servervariables (which provide information about the current request in process. Info such as the browser used, the user's IP address, the port he's connecting, the user's authentication if logged in, HTTP Cookies and more). Form variables are accessed through the Form() method of the wwRequest class while the server variables are accessible through the ServerVariables() method. Many of these server variables are directly accessible with methods such as GetBrowser, GetServerName so you don't have to remember cryptic CGI variable names. Another important method is the QueryString() method which handles parsing and retrieving either postitional or named arguments of the URL querystring (everything following the ? on a Url which serves as 'parameters' for a Web request).

    This object is automatically created for you in the wwProcess class and is always available as THIS.oRequest or simply Request in any method of your wwProcess subclass.

    wwResponse

    The wwResponse object handles all output in Web Connection. At the lowest level the Response.Write() method sends output into the HTTP output stream that gets sent back to the Web server. Ontop of the basic Write() functionality wwResponse implements a number of high level methods like the ability to render cursors to HTML with single method calls, expand external HTML scripts and templates, handle automatic page creation with wrapper methods and so on. All of these methods internally call back into wwResponse.Write eventually to create the HTTP output.

    This object is automatically created for you in the wwProcess class and is always available as THIS.oResponse or simply Response in any method of your wwProcess subclass. When the Process class shuts down the output from the response object is returned back the wwServer class which in turn sends it back to the Web server for display.


    Summary

    Although this process sounds very complex with a lot of code running to get your request to the entry point of your code in the wwProcess class, all of this functionality is abstracted in the Web Connection framework, so you simply create a new method in your Process class to create new functionality. The Framework processing is also very efficient and occurs less than 1/100 of a second overhead on a 200mhz Pentium (original) machine, but it provides many additional features such as automatic error handling, request logging and transparent request operation.

    With Web Connection you can concentrate on writing your application code, not figuring out how to perform Web tasks.


    Class wwServer



    Communicating with the Web server and routing requests to your code.

    This class handles communication with the Web server. It can operate in several modes:

    Using file based messaging
    Messages are sent between the ISAPI extension and the app via files containing the server request data and HTML output.

    Using COM messaging
    The ISAPI extension instantiates a pool of COM objects of this class and then calls the ProcessHit method passing the request information as a parameter. The HTTP output is returned via the return value.

    As an ASP component
    The server can also act as an ASP component. The page simply retrieves the ASP context and then uses the request information available from the ASP objects to create and output results.

    Startup Sequence

    It's useful to understand how the startup order of events works in Web Connection. The entire operation starts with the server's Init firing. The server's Init should never be overridden by your code as that code is extremely important in setting up the server. Instead Init fires several startup events that you can hook into.

    Init starts by setting some base settings figuring out what mode the server's running in, and setting the startup path. This must happen prior to any other code running to ensure the server itself can get at further information needed. The server then calls SetServerEnvironment(), which is a method that should be subclassed by the user. Init the regains control and uses the settings (like the startup path and INI file) to read various settings required for proper server startup. The server uses the ReadConfiguration method to do so. Note that you can override these values in SetServerEnvironment() which is why you should only use these methods to override functionality.

    Once the settings have been read, the server is initialized and fires another method SetServerProperties(). This method allow you to set properties on the server since at that point the server is done with its internal settings. This method is also ideal to do things like load Class Libraries and Procedure Files and add additional paths that may be needed by your application.

    If you're running COM you can instantiate multiple servers simultaneously. The entire pool of configured objects in wc.ini are loaded at once and Init (along with the two Set methods) fires on each one. The server then also calls GetProcessID. This method is mainly used for COM, which uses it to retrieve a processId to allow the ISAPI extension to kill the server in case of a failure. It also serves to implement the cascading view of the servers. GetProcessId must be present or else the server may not load correctly.

    At this time the server or servers are ready to receive requests. Regardless of messaging mechanism the server is called on each hit via the ProcessHit() method. This method should not be overridden since it contains the core of the Web Connection server interface. Instead ProcessHit performs the request set up and then passes control to the Process() method. This method is the actual entry point where code fires on each hit. Process then acts as a routing mechanism that takes the request and routes it to a specific application which typically consists of a PRG or VCX that contains a class which handles the actual incoming request.

    Implementation

    When you create a subclass of wwServer you should implement the following methods:

    The code looks something like what's outlined below - this should change very little with a few adjustments for your server environment.

    **************************************************************
    ****          YOUR SERVER CLASS DEFINITION                 ***
    **************************************************************
    DEFINE CLASS wc3DemoServer AS wwServer OLEPUBLIC
    *************************************************************
    ***  Function: This is a subclass of the wwServer class
    ***            that is application specific. Each Web Connection
    ***            server you create *MUST* create a subclass of the
    ***            class and at least implement the Process and
    ***            SetServerEnvironment methods to  receive requests!
    *************************************************************
    
    *** Add any custom properties here
    *** These can act as 'global' vars 
    
    ************************************************************************
    * wcDemoServer :: SetServerEnvironment
    *************************************
    ***  Function: This method sets the server's environment in terms
    ***            of VFP Environment settings. This code executes
    ***            just prior to the Init Code of the base class. Any
    ***            SET or ON statements should be made here.
    ***
    ***            THIS.cAppStartPath returns your application's startup
    ***            path as stored in the registry. If you need to override
    ***            this path in code you should do so here. This value
    ***            is used to SET DEFAULT TO in the Init(). You can
    ***            set this value on the WC Status form's Startup Path.
    ***
    ***    Assume: VIRTUAL METHOD - must ALWAYS be implemented!!!
    ************************************************************************
    PROTECTED FUNCTION SetServerEnvironment
    
    *** Location of the startup INI file 
    THIS.cAppIniFile= addbs(THIS.cAppStartPath) + "wcmain.ini"
    
    *** This URL is executed when clicking on the Automation Server
    *** Form's Exit button. It forces operation through a browser!
    THIS.cCOMReleaseUrl="http://localhost/wconnect/wc.dll/maintain?release"
    
    #IF !DEBUGMODE
       *** Backup Error handler only - startup code and a few file access errors
       ***                             are the only things handled by this one
       ON ERROR DO ErrorHandler WITH ;
                  ERROR(),MESSAGE(),MESSAGE(1),SYS(16),LINENO() ;
                  IN WCMAIN.PRG
       SET RESOURCE OFF
    #ENDIF
    
    ENDFUNC
    * SetServerEnvironment
    
    
    ************************************************************************
    * wcDemoServer :: SetServerProperties
    *************************************
    ***  Function: This Method should be used to set any server properties
    ***            and any relative paths. At this time the server has
    ***            changed directories to its startup path, so it's safe
    ***            to set relative paths from here.
    ***
    ***            This code runs just prior to showing the server window.
    ************************************************************************
    PROTECTED FUNCTION SetServerProperties
    
    THIS.ReadConfiguration()   && Reads from THIS.cSetupIniFile
    
    *** Any settings you want to make to the server
    IF THIS.lShowServerForm
      THIS.oServerForm.Caption =This.cServerId + " - Web Connection " + WWVERSION 
    ENDIF  
    
    *** Add any data paths - SET DEFAULT has already occurred so this is safe!
    #IF ENTERPRISE
    	DO PATH WITH ".\WWTHREADS"
    	SET PROCEDURE TO wwtClasses ADDITIVE
    	SET PROCEDURE TO wwtList ADDITIVE
    #ENDIF
    
    DO PATH WITH ".\WWDEMO"
    DO PATH WITH ".\WW"
    
    *** Must add this here
    SET PROCEDURE TO wwDemo ADDITIVE
    SET PROCEDURE TO wwMaint ADDITIVE
    
    ENDFUNC
    * SetServerProperties
    
    
    
    #IF .F.
    ************************************************************************
    * wcDemoServer :: Process
    *************************
    ***  Function: This procedure's main purpose is to route incoming
    ***            requests to individual project PRGs/APPs.
    ***
    ***            The URL formatting used is as follows:
    ***            /wconnect/wc.dll?project~ClassMethod~Parm1~Parm2 etc.
    ***
    ***            The project 'parameter' is routed to the appropriate
    ***            program file which actually implements a Process class
    ***            to respond to requests. ClassMethod calls the method
    ***            in the wwProcess class implemented in project.prg
    ************************************************************************
    PROTECTED FUNCTION Process
    
    *** Retrieve first parameter
    lcParameter=UPPER(THIS.oRequest.Querystring(1))  
    
    *** Set up project types and call external processing programs:
    DO CASE
    	#IF ENTERPRISE 
    	   CASE lcParameter == "WWTHREADS"
    	     DO wwThreads with THIS
    	#ENDIF     
    
      CASE lcParameter == "WWDEMO"
         DO wwDemo with THIS
    
      *** HTTP Client Server for remote data, COM etc.
      CASE lcParameter = "WWHTTPDATA"
         DO wwHTTPData with THIS
         
      *** HTTP Client Demos
      CASE lcParameter == "HTTP"
          DO HTTP with THIS      
    
    *!*	  CASE lcParameter="MYCODE"
    *!*	     DO MyCode with THIS
    
       CASE lcParameter == "WESTWIND"
         DO WESTWIND with THIS
    
    *!*	  *** SYSTEM TASKS
    *!*	  CASE lcParameter == "WEBHITS"
    *!*	     DO WEBHITS WITH THIS
    *!*	     
    	  CASE lcParameter == "WWMAINT"
    	      DO wwMaint with  THIS
    
      OTHERWISE
         *** Check for Script Mapped files for: .WC, .WCS, .FXP
         lcPhysicalPath=THIS.oRequest.GetPhysicalPath()
         DO CASE
         CASE ATC(".WC",lcPhysicalPath) > 0 OR ATC(".FXP",lcPhysicalPath) > 0
            DO wwScriptMaps with THIS
    
         OTHERWISE
             *** Error - No handler available. Create custom 
    	     Response=CREATE([WWC_WWHTMLSTRING])
    	     Response.ErrorMsg("Unhandled Request",;
    	                       "The server is not setup to handle this type of Request: "+lcParameter)
    
    	     #IF WWC_SENDEMAIL_ONERROR 
    	         THIS.SendMail(WWC_MAILSERVER,WWC_ADMINISTRATOR_EMAIL, ;
    	           WWC_ADMINISTRATOR_EMAIL,;
    	           WWC_ADMINISTRATOR_EMAIL,"*** Base Class Abstract Methods", ;
    	           "Web Connection Error Message - Unhandled request",;
    	           "The request Query String is: " +THIS.oRequest.cQueryString + CR+;
    	           "              DLL or Script: " +THIS.oRequest.ServerVariables("Executable Path") + CR+;
    	           "                Server Name: " + THIS.oRequest.GetServerName() ) 
             #ENDIF
    
    	     IF THIS.lCOMObject
    	         *** Simply assign to output property
    	         THIS.cOutput=Response.GetOutput()
    	     ELSE
    	         *** FileBased - must output to file
    	         File2Var(THIS.oRequest.GetOutputFile(),Response.GetOutput())
    	     ENDIF
    	 ENDCASE
       
    ENDCASE
    
    RETURN
    #ENDIF
    
    ENDDEFINE
    

    Class Members

    MemberDescription
    Dispose The event is called when the server is released it's called explicitly but also from the destroy. If you have any references you've created that might be circular or otherwise need cleaning up it's recommended you override this method and clean up the code.
    OnInit This method is called at the very beginning of the server's initialization sequence. It essentially fires at the very beginning of server's Init() and should be used to set up any operational parameters that the server will need for initialization.
    OnLoad This method fires after the server's initialization is complete but before any requests have been processed. It's fired just before the server starts getting ready to process inbound requests.
    AddResource Allows adding an embeddable resource like an image, CSS or JS file or other file to be stored and then served as a WebResource url of the application.
    o.AddResource(lcKey,lcContent,lcContentType)
    GetProcessID
    Called by the ISAPI extension to finalize the initialization process.
    o.GetProcessID(lnInstance)
    Process The Process method is the main routing routine for incoming Web Connection requests. This method parses the base parameters/filenames and decides which Process class to call for request processing.
    o.Process()
    ProcessHit This is Web Connection's mainline entry routine that is entered by the server. Each of the server mechanisms calls this method first to activate Web Connection and pass forward the Web server request information.
    o.ProcessHit(lcRequestBuffer,llFile)
    ShowServerForm Loads the server form for display. The server form is a separate
    object that gets created by this method. Pass an optional
    parameter of .T. to release an already running form.
    o.ShowServerForm(llRelease)
    cAppIniFile This is the name of the INI file that the application will lookup to read configuration values from. This value is pre-set in the Initialization of the server, prior to accessing SetServerEnvironment. You can modify this value in SetServerEnvironment as needed. The value is then used by ReadConfiguration() to actually read the data from the INI file.
    cAppName The application's name. This value typically will be the server's class name, which is used in the INI file as the root reference. Note this should be more of an ID type value rather than a descriptive name, although either will work.
    cAppStartPath This is the application's startup path. This should point to the directory where the server was
    started in and where wcmain.ini (or your implementation thereof) should reside.
    cCOMReleaseURL In order to release a WWWC COM object from a visual form the request has to run over HTTP to release the server. When you click on the server's Exit button for example the code actually launches an HTTP session (via wwIPStuff) and then releases the server via this link.
    cErrorMsg An error message that is sent when an error occurs in the server's error method. This value is valid only when wwServer::lError = .T.
    cLogFile Determines the filename where requests are logged to when lLogToFile is set to .T. Default file is RequestLog.dbf.
    cOutput This property receives the HTTP output from your code when operating under COM. The wwProcess class handles assignment of the HTTP output to this property when the wwProcess object is released in its Destroy. For this reason, if you decide to write to this property it must occur outside of the process object.
    cRequestClass The class that Web Connection creates when to do request processing. Keep in mind that this may vary depending on whether you use ASP or plain Web Connection processing.
    lASPObject This flag determines whether the component is running under Active Server Pages. If so the server changes its behavior to use a different request class and sets up the appropriate Request and Response objects that map to the ASP objects.
    lComObject This flag determines whether the server is running under COM based messaging as opposed to file based processing.
    lDebugMode Determines whether the server is running in DebugMode or 'release mode'. When this flag is set to .T. error handling is disabled and errors stop in code. When .F. errors are handled and managed to various handlers that by default display error messages.
    lError Error flag set to .T. if a request fails. You can check cErrorMsg to see what error actually occurred.
    lLogToFile Determines whether every Web request is logged to a request log file. Data is logged to the
    file set in cLogFile.
    lShowRequestData Flag that can be passed forward to a wwProcess object to determine whether to show the current request data at the end of the current request. Note that this flag is not automatically respected in wwProcess - it's only used as a 'global' guideline.
    lUnattendedComMode When set causes the COM server to be run in VFP unattended mode (SYS(2335,0)) which prevents any user interface operation from firing. This will prevent accidental file open dialog boxes (which are not trappable) and generate errors for any modal UI operations (like an accidental MessageBox somewhere).
    lUseErrorMethodErrorHandling Flag that allows you to bypass TRY/CATCH error handling and instead implement your own Error() method on the Process class for error handling.
    lUseRegistryForStartupPath This property can be set to force Web Connection not to look in the registry for the startup path. This might be useful if you have multiple separate applications that use the same wwServer derived class names as the registry stores the wwServer classname as the registry key.
    nPageParseMode Determines how Web Control Framework Pages are parsed by Web Connection.
    nScriptMode These modes determine how the ASP style (WCS scripts) scripting files are compiled and executed. This flag affects how Response.ExpandTemplate() works and how generic WCS scripts fired through the wwScriptMaps Process class operate.
    nWebControlFrameworkCompileMode Determines how pages are run when the WebControlFramework operates. Runs either in compiled mode or in 'always' parse mode.

    Requirements

    Assembly: wwServer.prg

    wwServer::OnInit


    This method is called at the very beginning of the server's initialization sequence. It essentially fires at the very beginning of server's Init() and should be used to set up any operational parameters that the server will need for initialization.

    Use this method to make any environment settings (such as SET PATH statements and other SET commands) for the application, setting paths that might be needed at startup and to change the way that the configuration files are loaded.

    Note you should keep the code in OnInit minimal. You'll get another chance to configure your application after the server has loaded its configuration state in OnLoad().


    o.OnInit()                                          
    

    Remarks

    This is an abstract method without implementation in the base class.
    This class should always be implemented by the subclass of wwServer.

    Example

    *** Typical code that goes into this method
    PROTECTED FUNCTION OnInit
    
    *** Location of the startup INI file 
    THIS.cAppIniFile= addbs(THIS.cAppStartPath) + "wcmain.ini"
    
    *** This URL is executed when clicking on the Automation Server
    *** Form's Exit button. It forces operation through a browser!
    THIS.cCOMReleaseUrl="http://localhost/wconnect/wc.dll/maintain?release"
    
    #IF !DEBUGMODE
       *** Backup Error handler only - startup code and a few file access errors
       ***                             are the only things handled by this one
       ON ERROR DO ErrorHandler WITH ;
                  ERROR(),MESSAGE(),MESSAGE(1),SYS(16),LINENO() ;
                  IN WCMAIN.PRG
       SET RESOURCE OFF
    #ENDIF
    
    ENDFUNC
    * OnInit

    wwServer::OnLoad


    This method fires after the server's initialization is complete but before any requests have been processed. It's fired just before the server starts getting ready to process inbound requests.

    Use this method to setup your environment of the application. When this method is called the server has read all of its configuration settings so you can safely read values from the various configuration objects. For this reason this method, rather than OnInit() should be used to set any application specific settings.

    Use OnLoad() to set up the application environment including SET PATH, any system SET commands. Use OnLoad() to adjust the server's operational properties, to add properties or objects, load class libraries etc. Anything application specific should be managed here.


    o.OnLoad()                                             
    

    Remarks

    This method is an abstract method and should always be implemented in
    your subclass of wwServer.

    Example

    *** Typical code that goes into this method
    PROTECTED FUNCTION OnLoad
    
    *** Any settings you want to make to the server
    IF THIS.lShowServerForm
      THIS.oServerForm.Caption =This.cServerId + " - Web Connection " + WWVERSION 
    ENDIF  
    
    *** Add any data paths - SET DEFAULT has already occurred so this is safe!
    #IF ENTERPRISE
    	DO PATH WITH ".\WWTHREADS"
    	SET PROCEDURE TO wwtClasses ADDITIVE
    	SET PROCEDURE TO wwtList ADDITIVE
    #ENDIF
    
    DO PATH WITH ".\WWDEMO"
    DO PATH WITH ".\WW"
    
    *** Must add this here
    SET PROCEDURE TO wwDemo ADDITIVE
    SET PROCEDURE TO wwMaint ADDITIVE
    
    ENDFUNC
    * OnLoad
    

    wwServer::Dispose


    The event is called when the server is released it's called explicitly but also from the destroy. If you have any references you've created that might be circular or otherwise need cleaning up it's recommended you override this method and clean up the code.

    Make sure to call DODEFAULT() to call the base class code.

    o.Dispose()                                                       
    

    wwServer::GetProcessId



    Called by the ISAPI extension to finalize the initialization process.

    This method returns the current process's ID. It also doubles as a startup hook that is called when the server is created
    by the ISAPI DLL when running COM.

    It also handles cascading the server windows.




    o.GetProcessID(lnInstance)                                        
    

    Return Value

    numeric - ProcessId

    Parameters

    lnInstance
    An instance ID. Used for cascading windows.

    Remarks

    Only used in COM operation of the server. Not used in File based or ASP messaging modes.

    wwServer::AddResource


    Allows adding an embeddable resource like an image, CSS or JS file or other file to be stored and then served as a WebResource url of the application.

    To use this functionality you call Server.AddResource() to add images, .js, .css, .htm files etc. to the resource pool which is stored in Server.oResources. You can then retrieve these Resources via Server url by calling WebResources.wwd?Resource=Hello where wwd is your application's script map. WebResources is available on any Process class implementation.

    This functionality allows you to minimize external dependencies in your application and lets you 'embed' things like style sheets, images and JavaScript resources into your exe. The resources are then dynamically loaded through a Web Connection url.

    Note this approach causes extra hits on your Web Connection server and it's more expensive to serve requests this way than letting the Web Server manage the resources and caching. Returning reseources is efficient - they are cached internally and maximum caching is applied in the browser.

    o.AddResource(lcKey,lcContent,lcContentType)                      
    

    Parameters

    lcKey
    The Id by which the Resource can be retrieved. Should be unique and preferrably a single non-spaced string.

    lcContent
    The actual content of the resources in string format. The content can be binary such as an image, or text like a .js or .css file.

    lcContentType
    The content type for the resource: text/plain, image/gif,text/javascript,text/css etc.

    wwServer::Process


    The Process method is the main routing routine for incoming Web Connection requests. This method parses the base parameters/filenames and decides which Process class to call for request processing.

    This method MUST BE IMPLEMENTED by wwServer subclasses as the base method is generic.

    The bulk of an implementation typically will contain a CASE statement that routes each script map or positional parameter to the appropriate process class.

    o.Process()
    

    Return Value

    nothing

    Parameters

    none

    Remarks

    This method should always be overridden.

    wwServer::ProcessHit


    This is Web Connection's mainline entry routine that is entered by the server. Each of the server mechanisms calls this method first to activate Web Connection and pass forward the Web server request information.

    This method can only be called after the server has been constructed properly. The majority of the construction code occurs in the server's Init along with calls to the user customizable SetServerEnvironment and SetServerProperties methods.



    o.ProcessHit(lcRequestBuffer,llFile)                                                      
    

    Return Value

    This method returns a string of the result from the server's
    processing. This typically will be an HTML/XML or other HTTP
    string.

    Parameters

    lcRequestBuffer
    This parameter contains the all of the server's request information.
    Typically this parameter is a string, but it may also be a filename
    (if llFile is .T.) in which case the info is loaded from a file.
    In the future this parameter may receive an object with the request
    information.

    llFile
    If .T. the first parameter passed was a full filename which is read
    and converted to a string.

    Remarks

    When running an ASP Component this method can be called from an ASP page to run the Web Connection component:

    <% 
      Response.Buffer = True
      Set oServer = Server.CreateObject("wcASPDemo.wcDemoServer")
    
      oServer.ProcessHit()  ' writes its own output!
    
      ' Response.Write(oServer.cErrorMsg)  && for troubleshooting
    %>
    

    This response will be fully self contained based on the incoming request data with output sent directly into the ASP Response stream.

    wwServer::ShowServerForm


    Loads the server form for display. The server form is a separate
    object that gets created by this method. Pass an optional
    parameter of .T. to release an already running form.

    When the server starts it reads the ShowServerForm key from
    the startup ini file to determine whether to display the server
    form.



    o.ShowServerForm(llRelease)                                       
    

    Parameters

    llRelease
    If passed as .T. the form is released if visible. The actual object is unloaded.

    Remarks

    Ignored if running in COM DLL or ASP COM DLL mode, which cannot display the form. In filebased mode the form is always visible regardless of the setting.

    wwServer::cAppIniFile


    This is the name of the INI file that the application will lookup to read configuration values from. This value is pre-set in the Initialization of the server, prior to accessing SetServerEnvironment. You can modify this value in SetServerEnvironment as needed. The value is then used by ReadConfiguration() to actually read the data from the INI file.

    o.cAppIniFile                                                     
    

    wwServer::cAppName


    The application's name. This value typically will be the server's class name, which is used in the INI file as the root reference. Note this should be more of an ID type value rather than a descriptive name, although either will work.

    o.cAppName                                                        
    

    wwServer::cAppStartPath


    This is the application's startup path. This should point to the directory where the server was
    started in and where wcmain.ini (or your implementation thereof) should reside.

    This value is set by wwServer::GetAppBasePath which is gleamed from the registry at startup.
    This value can be overridden in wwServer::SetServerEnvironment although this is not recommended.


    o.cAppStartPath                                                   
    

    Remarks

    cAppStartpath is used internally in the wwServer class to locate the startup INI file.

    All path references should take cAppStartPath into account to make sure the server
    will run properly under all modes of operation. COM DLL servers cannot change path
    to the startup directory, so paths may not be relative to the current location but relative
    to THIS.cAppStartPath.

    wwServer::cCOMReleaseURL


    In order to release a WWWC COM object from a visual form the request has to run over HTTP to release the server. When you click on the server's Exit button for example the code actually launches an HTTP session (via wwIPStuff) and then releases the server via this link.

    Default: /wconnect/wc.dll/maintain?release

    o.cCOMReleaseURL                                                  
    

    wwServer::cErrorMsg


    An error message that is sent when an error occurs in the server's error method. This value is valid only when
    wwServer::lError = .T.

    o.cErrorMsg                                                       
    

    wwServer::cLogFile


    Determines the filename where requests are logged to when lLogToFile is set to .T. Default file is RequestLog.dbf.

    o.cLogFile                                                        
    

    wwServer::cOutput


    This property receives the HTTP output from your code when operating under COM. The wwProcess class handles assignment of the HTTP output to this property when the wwProcess object is released in its Destroy. For this reason, if you decide to write to this property it must occur outside of the process object.

    o.cOutput                                                         
    

    wwServer::cRequestClass


    The class that Web Connection creates when to do request processing. Keep in mind that this may vary depending on whether you use ASP or plain Web Connection processing.



    o.cRequestClass                                                   
    

    wwServer::lASPObject


    This flag determines whether the component is running under Active Server Pages. If so the server changes its behavior to use a different request class and sets up the appropriate Request and Response objects that map to the ASP objects.


    o.lASPObject                                                      
    

    wwServer::lShowRequestData


    Flag that can be passed forward to a wwProcess object to determine whether to show the current request data at the end of the current request. Note that this flag is not automatically respected in wwProcess - it's only used as a 'global' guideline.

    o.lShowRequestData                                                
    

    wwServer::lUseRegistryForStartupPath


    This property can be set to force Web Connection not to look in the registry for the startup path. This might be useful if you have multiple separate applications that use the same wwServer derived class names as the registry stores the wwServer classname as the registry key.

    The internal sequence to set cAppStartPath is:




    o.lUseRegistryForStartupPath                                      
    

    wwServer::lUseErrorMethodErrorHandling


    Flag that allows you to bypass TRY/CATCH error handling and instead implement your own Error() method on the Process class for error handling.

    This flag is primarily meant as a backwards compatibility feature for developers who've built extensive error handling into their existing pre-5.0 Web Connection applications. It's also meant to provide richer error information for error handlers. One side effect of TRY/CATCH error handling is that the FoxPro call stack is unwound so when the default OnError method is called in your code, all the code information (PRIVATE and LOCAL vars, ASTACKINFO() etc.) doesn't reflect the call stack of the time of the error. You do get this functionality in an error method.

    When lUseErrorMethodErrorHandling=.T., TRY\CATCH handling is not enabled even if the Server.lDebugMode flag is set to .F. Note this flag must be set prior to the call to Process() which means immediately after CREATEOBJECT(). The flag is used inside of the Process() call and before OnProcessProcess() is called.

    To implement a custom error methods we recommend using a #DEBUGMODE flag in WCONNECT_OVERRIDE.h and then writing code like this:

    *** In YourServer::OnInit FUNCTION OnInit ... lUseErrorMethodErrorHandling = .t. ENDFUNC *** In all your Process classes #IF !DEBUGMODE FUNCTION Error(lnerror,lcMethod,lnLine) *** Do whatever logging, email etc. you need to RETURN TO ROUTEREQUEST ENDFUNC #ENDIF

    Note the call to RETURN TO ROUTEREQUEST which is required to return execution back to the calling top level handler and complete the request. This is the way error handling worked in versions prior to Web Connection 5.0.

    To still take advantage of stock error handling you can do something similar to the following:

    #IF !DEBUGMODE FUNCTION Error(lnerror,lcMethod,lnLine) LOCAL loException as Exception loException = CREATEOBJECT("Exception") loException.LineNo = lnLine loException.LineContents = MESSAGE(1) loException.ErrorNo = lnError loException.Message = MESSAGE() loException.Procedure = SYS(16) *** Call stock error handling mechanism so we don't duplicate work *** Stock handler now has full access to current call stack this.OnError(loException) RETURN TO RouteRequest ENDFUNC #ENDIF

    which routes the callback to the OnError method which provides the default error handling in Web Connection that can be overridden. The RETURN TO then returns back in the mainline code.

    o.lUseErrorMethodErrorHandling                                   
    

    Remarks

    Note this setting is global! It applies to all process methods in your application and turns off the TRY/CATCH error management in wwServer::ProcessHit and wwProcess::RouteRequest and requires you to implement Error methods on the Process and Server classes to handle errors.

    Presumably this property will only be used for legacy applications.

    wwServer::lComObject


    This flag determines whether the server is running under COM based messaging as opposed to file based processing.

    o.lComObject                                                      
    

    wwServer::lDebugMode


    Determines whether the server is running in DebugMode or 'release mode'. When this flag is set to .T. error handling is disabled and errors stop in code. When .F. errors are handled and managed to various handlers that by default display error messages.

    This flag is always accessible inside of a Web Connection applications as Server.lDebugMode.

    This flag replaces the #define DEBUGMODE from previous versions of Web Connection.

    o.lDebugMode                                                      
    

    wwServer::lError


    Error flag set to .T. if a request fails. You can check cErrorMsg to see what error actually occurred.


    o.lError                                                          
    

    wwServer::lLogToFile


    Determines whether every Web request is logged to a request log file. Data is logged to the
    file set in cLogFile.

    This property is set when the server starts up from the startup ini
    file using the LogToFile setting in the [Main] section.

    o.lLogToFile                                                      
    

    wwServer::lUnattendedComMode


    When set causes the COM server to be run in VFP unattended mode (SYS(2335,0)) which prevents any user interface operation from firing. This will prevent accidental file open dialog boxes (which are not trappable) and generate errors for any modal UI operations (like an accidental MessageBox somewhere).

    In general it's a good idea to set this flag to true, but be absolutely sure that the server does not bring up any UI that might cause a problem. Note that many UI operations - like non-modal forms, WAIT WINDOWS actually work in this mode.

    This property only works in COM mode and has no effect in file mode.

    o.lUnattendedComMode                                              
    

    wwServer::nScriptMode


    These modes determine how the ASP style (WCS scripts) scripting files are compiled and executed. This flag affects how Response.ExpandTemplate() works and how generic WCS scripts fired through the wwScriptMaps Process class operate.

    The following settings can be applied to this flag:

    1. Dynamic Compilation
      In this mode script pages are compiled as needed if the WCS files and FXP file are out of sync. If you make a change to the WCS file the parser will detect the change and reparse and recompile the page automatically for you. This mode is the recommended mode while testing and debugging your script pages. It can also be used in deployed applications, but if you run multiple instances and make changes to script pages while servers are running there's no guarantee that all instances will see the changes - typically only the instance that catches the change first will see the new behavior. Only Web Connection server restart will ensure all instances are in sync again.
      Recommended for: Design time operation. Runtime environments where the pages are not changed frequently.

    2. Precompiled FXP files
      This mode runs only precompiled FXP pages. It's the most efficient mode and it doesn't check for files, but simply tries to execute the FXP file. If it fails no attempt is made to create the file.
      Recommended for: Runtime environments where best performance is required and files can be precompiled and FXP files can be deployed.

    3. Interpreted with ExecScript
      This mode is completely dynamic and reparses pages on every hit. The page is read from disk and reparsed and executed so this mode is the only one that guarantees that script changes are seen by multiple instances running simultaneously. This mechanism uses EXECSCRIPT() and so parses, compiles and executes a PRG file for every single page access which can be considerably slower than the other two mechanisms.
      Recommended for: environments where it's vital that pages can be changed at runtime without any restarts.

    This property is set when the server starts up from the startup ini file using the ScriptMode setting in the [Main] section.

    o.nScriptMode                                                     
    

    Remarks

    Starting with Web Connection 5.10 the settings here use the wwScripting class, which exhibits slightly different behavior than previous versions

    wwServer::nPageParseMode


    Determines how Web Control Framework Pages are parsed by Web Connection.

    1 - Parse and Run
    2 - Parse, Compile to FXP and Run
    3 - Run only

    Parse and Run
    Calls WebPageParser on every Web Control Framework page to parse the page. The page is then executed.

    This option should be used during development. Page rendering is slow, as the operation parses the page, loads the classes, executes it, then unloads all classes explicitly. This is required in order to replace the compiled FXP file.

    The framework makes no checks for updated files, so every page hit is parsed and executed. This option is not sufficient if you are running your deployed application as an EXE. For that you need to use Parse, Compile and Run.

    Parse, Compile and Run
    Same as Parse and Run except that the WebPageParser tries to explicitly compile the page after parsing. It is possible to use this option at runtime since it attempts to replace the existing FXP file.

    Note that this option is also slow - slower in fact than the Parse and Run because it also needs to compile.

    Compilation is also not guaranteed to work if multiple instances are running as FoxPro locks active FXP files that have classes loaded. So this option is primarily meant as a way to run in single instance mode and compile select pages on the fly.

    Run only
    This is the preferred mechanism for deployed applications. This option causes Web Connection to simply execute the FXP file or project embedded compiled file without any special checks.

    This option is considerably faster than the previous options as no parsing or compilation takes place and highly recommended for deployed applications

    Deployment options

    If you use Run Only you get fast and reliable operation because you are simply executing pre-compiled code, but with it you cannot change the page unless you recompile either an EXE file (for project embedded page PRG files) or the individual FXP files and re-deploy them on the server.

    The Parse, Deploy and Run option can be an option to dynamically recompile pages on site if you temporarily switch into single instance operation (from the ISAPI Admin page). You can execute the new pages and thereby dynamically recompile them.

    Another alternative is to locally compile all page classes and then put the application on Hold (ISAPI Status Page), copy the FXP files and then take the Hold off.

    I personally prefer embedding pages into a Project and update the entire EXE file on the server. It is simply more efficient to manage and test a single EXE file as opposed to keeping track of a large number of PRG/FXP files to upload to a server.


    o.nPageParseMode                                                  
    

    wwServer::nWebControlFrameworkCompileMode


    Determines how pages are run when the WebControlFramework operates. Runs either in compiled mode or in 'always' parse mode.

    o.nWebControlFrameworkCompileMode                                 
    

    Default Value

    1 - Run compiled pages only (executes the PRG file only and file must already exist)
    2 - Parses the page on the fly and then compiles and runs it

    Default Value: 2

    Server Support members of wwServer


    The methods in this group are typically not called directly by external applications, but they are available to customize operation of the server. These methods are internally used by the wwServer class and are listed here merely for completeness of the interface and special situations.

    wwServer::ASPCallRequest


    This method can be used to send output into a compound ASP page. Unlike calling ProcessHit to handle a full WC request in an ASP page, this method is used to embed partial HTTP output into a larger ASP document:



    o.ASPCallRequest(lcClass,lcMethod)                                
    

    Return Value

    Nothing. Output is sent to the ASP Response Object.

    Parameters

    These parameters map to the equivalent positional parameters on the querystring. Since CallASPRequest is independent of the querystring these values override URL parameters of the Request:


    lcClass
    The name of the process class to call. WebDemo, or wwDemo for example would be the PRG files and classes that house those process handlers.

    lcMethod
    The method to call in the process handler class. For example, HelloWorld or TestPage


    Example

    <% 
      Response.Buffer = True
      Set oServer = Server.CreateObject("wcASPDemo.wcDemoServer")
    %>
    
    This is some text in an ASP document with some ASP script <%= now %>...
    <p>
    
    Following is some text from a WC request:<p>
    <b><% oServer.ASPCallRequest "wwDemo","HelloASP"  %></b>
    <p>
    More plain Text from the ASP page.
    <P>
    Followed by another WC hit:<p>
    <b><% oServer.ASPCallRequest  "wwdemo","EndASP" %></b>
    
    
    

    wwServer::GetAppStartPath


    This method is used to determine the server's startup location. The
    startup location is used to manage data and code access for the
    server so the server can find the appropriate files. Behavior varies
    depending on how the server is running.

    cAppStartPath is Set in the server's Init and can be overridden by
    you in SetServerEnvironment. After SetServerEnvironment is called
    the server then adds the path to the current Fox PATH and in certain
    RUN modes changes directory to this path.

    COM EXE or Standalone EXE or VFP IDE operation
    Server does a CD (THIS.cAppStartPath) changing path to this directory.

    COM DLL
    Server only does a SET PATH TO (THIS.cAppStartPath). You should use
    the SetServerProperties method to explicitly add all paths the app
    will be accessing manually with code like this:

    DO PATH WITH THIS.cAppStartPath + "data"
    DO PATH WITH THIS.cAppStartPath + "DemoData"
    

    Location of the directory is determined in the following order:

    1. Out of the registry at the following key if it exists:

    HKLM\SOFTWARE\West Wind Technologies\Web Connection\Servers\<serverclass>

    If this key exists
    cAppStartPath will be set to this value.

    2. From Application.ServerName which returns the fully qualified
    path to the COM EXE or COM DLL. For standalone applications,
    SYS(16,0) is parsed. The function that performs this task is the generic
    GetAppStartPath in wwUtils.prg.

    To update the registry setting bring up the server window, click on
    Status and update the startup path. Then click on Save Settings to
    write the new value into the registry.


    o.GetAppStartPath()                                               
    

    Return Value

    The server's startup path which is the physical location where the EXE or DLL file or the current start PRG (interactive VFP) lives.

    Remarks

    cAppStartpath is used internally in the wwServer class to locate the startup INI file.

    All path references should take cAppStartPath into account to make sure the server
    will run properly under all modes of operation. COM DLL servers cannot change path
    to the startup directory, so paths may not be relative to the current location but relative
    to THIS.cAppStartPath.

    wwServer::LogRequest


    Logs a request into the log file. All log access should go through
    this method.

    o.LogRequest(lcParameter,lcRemoteAddress, lnSeconds, llError)     
    

    Return Value

    nothing

    Parameters

    lcParameter
    Request name/ID that identifies the URL.Typically this will be the query string.

    lcRemoteAddress
    IP Address of the client browser/application.

    lnSeconds
    Time it took to process this request in seconds.

    llError
    Error flag. If this parameter is .T. this entry is marked as an error which gets
    displayed in the error display log.

    wwServer::MTSSetAbort


    This method is a wrapper around the MTS SetAbort method, which commits a transaction.

    Please see the MTSSetComplete() topic for details on how to Set up for automatic MTS
    transaction support.

    o.MTSSetAbort()
    

    Return Value

    nothing

    wwServer::MTSSetComplete


    This method is a wrapper around the MTS SetComplete
    method, which commits a transaction. This method only
    does anything when the startup is set to use MTS transactions
    with a DLL server moved into MTS. It will not work when using
    out of process COM components or file based operation - in
    both cases the operation will simply be ignored.

    MTS operation is configured in Web Connection by compiling
    your server into a DLL and then moving that DLL into MTS.
    In your server's startup INI file such as wcmain.ini you
    can then specify UseMTS = 1 to indicate that you would like
    to use MTS transactions.

    When this is set up Web Connection will automatically wrap
    every Web Connection request in an MTS transaction. By
    default, if neither this method or MTSSetAbort is called all
    transactions are committed with MTSSetComplete.

    The global reference to the MTS object context that is used
    for this method is stored in the protected oMTSObjectContext property.
    When this is not set (ie. UseMTS not 1) it will be NULL and all requests
    to MTSSetComplete/MTSSetAbort are ignored.



    o.MTSSetComplete()                                                
    

    Remarks

    This method only applies to Web Connection's built in MTS
    transaction management which will occur on every request
    when enabled. This basically kicks in MTS's support for Just In
    Time Activation and transaction management.

    If you want to use transactions in a more granular fashion
    you can access the MTS component methods directly from your
    code (GetobjectContext(), SetComplete, SetAbort) and skip the
    UseMTS flag in the INI file. You can also isolate MTS operation to
    a specific subapplication by setting the ObjectContext in the Process
    object's Process method.

    wwServer::ReadConfiguration


    This method reads information out of the configuration INI file of your Web Connection application.
    The INI file typically has the same name as the project, so the sample INI file is called wcDemo.ini.

    The INI file consists of a Main section which sets the server's operational parameters and an optional
    section for each sub application.

    Reads the server's configuration data from the application
    INI file. The file contains the server's starup settings:

    [main]
    tempfilepath=c:\temp\
    template=wc_
    logtofile=1
    saverequestfiles=0
    showserverform=1
    showstatus=1
    usemts=0
    scriptmode=3
    timerinterval=200
    
    [wwdemo]
    datapath=d:\wwapps\wc3\wwDemo\
    htmlpagepath=d:\westwind\wconnect\
    

    In this example, wwDemo is a sub application with two configuration values. Configuration values in sub applications are very useful for server side configuration and typically handle things like paths and other values that may differ between development and online applications.

    Web Connection implements these settings via a special class, which you have to create and configure for each sub-application that requires configuration values. These values are read from the INI file on server startup and are then available for the duration that the server is running. You can access these in your application code like this:

    lcHTMLPath = THIS.oServer.oConfig.owwDemo.cHTMLPagePath

    In order for the above to work you need to create a custom Server config object, which adds individual objects for each sub application. The following code comes from the wcDemoMain.prg file that ships with Web Connection to configure the demo app and the wwDemo sub-application:

    DEFINE CLASS wcDemoConfig as wwServerConfig
    
    owwDemo = .NULL.
    owwMaint = .NULL.
    
    FUNCTION Init
    
    THIS.owwDemo = CREATE("wwDemoConfig")
    
    ENDFUNC
    
    ENDDEFINE
    
    DEFINE CLASS wwDemoConfig as wwConfig
    
    cHTMLPagePath = "d:\westwind\wconnect\"
    cDATAPath = "d:\wwapps\wcx\wwDemo\"
    
    ENDDEFINE
    

    To add any other subapplications simply create another object and add it to the wcDemoConfig class in the same fashion. The actual config object simply needs to have properties for each value that needs to be saved in the INI.

    Note that the value must start with a type prefix which is dropped when written to the INI file.

    To instantiate this class you need to then call it in your SetServerEnvironment startup routine:

    *** Override config with our own object which 
    *** has sub-configs for applications.
    *** wcDemoConfig is defined at the bottom of this PRG
    THIS.oConfig = CREATE("wcDemoConfig")
    THIS.oConfig.cFileName = THIS.cAppIniFile
    



    o.ReadConfiguration(lcIniFile)                                    
    

    Return Value

    nothing

    Parameters

    Optional - The INI file to read. This value defaults
    to the cAppIniFile property which is set at Init time
    and can be overridden in SetServerEnvironment.

    Remarks

    This method is called internally by the Init following
    the call to SetServerEnvironment.

    Uses the Settings object in Settings.prg

    wwServer::ReloadComServers


    This method can be used to reload COM Servers from within your Web Connection application code. The most common scneario for this feature is to refresh configuration file settings.

    This method calls the Web Connection Server Release URL which forces servers to shut down and restart and re-reread their startup configuration file settings.

    Requirements:

    This operation fires an asynchronous HTTP request against the ComReleaseUrl. The asynchronous call makes it possible to return a server response from a request that fires it.

    Note Username and Password are likely required and your application will have to provide this info somehow in order to allow access to this administrative URL. Some ways to do this is either to store the information somewhere and read it in at runtime, or prompt the user for it as part of the operation that triggers the restart.

    o.Reload(lcUsername,lcPassword,lcUrl)                       
    

    Parameters

    lcUsername
    Username required to access the Release Url.

    lcPassword
    Password required to access the Release Url.

    lcUrl
    Optional - the URL to the COM ReleaseUrl. If not provided Server.cComReleaseUrl is used - make sure this value is set correctly in YourApp.Ini or in Server.SetServerEnvironment.


    Example

    *** Example of Process method that updates the Server Config file
    FUNCTION UpdateConfig
    
    IF !Request.IsPostBack()
    	*** Page that displays Config editing
    	Response.ExandTemplate(Response.GetPhysicalPath())
        RETURN
    ENDIF
    
    *** Update Configuration Settings
    *** Do whatever you need to modify config settings.
    Request.FormVarsToObject(Server.oConfig.oMyApp,"txt")
    
    
    *** Save the Configuration settings back to the INI file
    Server.oConfig.Save()
    
    *** Reload COM Servers causing re-reading of INI settings
    IF Server.ReloadComServers("rstrahl",GetSystemPassword(),Server.cComReleaseUrl)
       this.StandardPage("Submitted servers for reloading.")
    ELSE
       this.StandardPage("Couldn't reload servers.")
    ENDIF   
    
    ENDFUNC
    
    

    wwServer::SaveConfiguration


    This method saves all information of the current server configuration object
    to an INI file. The main server settings as well as any sub-application settings
    are saved.

    For more details on the format and custom configuration of this configuration
    object please see
    wwServer::ReadConfiguration.

    This operation is called from the server's Status form when you click on
    Save All Settings.

    o.SaveConfiguration()                                             
    

    wwServer::SetLocale


    Provides a quick way to set a Locale format for a request. Locale information determines date and number/currency formats.

    This method sets the Locale to the browser's Locale provided via the Request.GetLocale()method.

    If this method is called the wwServer::oLocaleInfo is available which provides detailed info on the locale in use. It's based on the wwLocaleInfo class.

    You can call this method with an optional value of .T. to reset the Locale back to its default locale which is specifed in the wwServer::cDefaultLocale property.

    The Currency Symbol can be overridden via the wwServer::cLocaleCurrencySymbol. If this value is non-blank the specified currency symbol is used regardless of the Locale in use. This is useful if your site always accepts payments in a single currency, but still wants to display numbers in the appropriate number format to the browser.

    oServer.SetLocale(lcLocale)
    

    Return Value

    nothing

    Parameters

    llReset
    If .T. to forces the locale back to the default specified for the server in cDefaultLocale.
    If .F. or no parameter is passed the server switches to the locale of the user's browser.

    lcLocale
    Optional - The explicit numeric or string LocaleId to force the Locale to.

    Remarks

    Can be implemented at many levels:

    You can only switch to locales that are installed on your server. If you plan to do this I suggest you install the various language packs for various languages which can be installed from the Locale options in Control Panel. If a Locale is not found the defaullt Locale is used instead.

    Note it's your responsibility to reset the Locale. Once set the locale setting persists to the next request unless you properly unset the locale setting at the end of the page/method/request.

    Example

    *** The following example switches locale to 
    DEFINE CLASS wwYourServer
    
    cDefaultLocale = "de-de"
    
    ...
    
    * wwYourServer::Process
    FUNCTION Process
    
    *** Switch locale to browser setting
    this.SetLocale()   
    
    *** Retrieve first parameter
    lcParameter=UPPER(THIS.oRequest.Querystring(1))  
    
    
    *** Set up project types and call external processing programs:
    DO CASE
          CASE lcParameter == "WCDEMO"
             DO wcDemo with THIS
    
    	  ... more CASE statements etc.
    ENDCASE
    
    *** Switch back to default Locale
    this.SetLocale(.T.)
    
    ENDFUNC
    
    ...
    
    ENDDEFINE

    wwServer::SetLogging


    This method turns logging on or off.


    o.SetLogging(llLoging)                                            
    

    Return Value

    nothing.

    Parameters

    llLoging
    If .T. logging is turned on. If .F. logging is turned off.

    Remarks

    If setting logging on the lLogToFile property is turned on.

    wwServer::SetTimerInterval


    Used to set the timer interval of the server when running in file
    based operation.


    o.SetTimerInterval(lnInterval)                                    
    

    Parameters

    lnInterval
    The interval to set the timer to in milliseconds. Default: 200

    wwServer::cDefaultLocale


    The default Locale identifier for the Web site, such as en-US, de-DE, fr-CH for example.

    This value can be optionally used with automatic format conversion based on the browser language used.

    o.cDefaultLocale                                                  
    

    wwServer::cDLLFileName


    This property contains the full path the wc.dll file after the first hit fully qualified hit on the server.


    o.cDLLFileName
    

    wwServer::cTempFilePath


    Location of the temp file path for file based operation.

    o.cTempFilePath                                                   
    

    wwServer::lShowServerForm


    Determines whether the server form is visible or not.

    o.lShowServerForm                                                 
    

    wwServer::lShowStatus


    Determines whether the server displays request information on the main server form. This display causes a little bit of overhead so in unattended operations it's usually wasteful to have the display active.

    You can also remove the server form completely with the ShowServerForm() method and passing a parameter of .F.



    o.lShowStatus                                                     
    

    wwServer::lUseMTS


    Determines whether the server uses automatic MTS transactions for processing requests. Note: This works only if you build your servers into MTDLL COM objects and register the DLL with MTS.

    This flag has to be read from the startup ini file using the UseMTS key.

    o.lUseMTS                                                         
    

    wwServer::lUseRegistryForStartupPath


    This property is used to determine whether Web Connection looks in the registry to find its startup path. This method affects the behavior of the
    wwServer::GetAppStarPath() method.

    When true (the default) Web Connection looks up the registry key for this specific server in the registry and uses that path. The default behavior then changes path to this directory and runs from there.

    When false, Web Conneciton ignores this key and always uses the EXE's startup directory.

    The registry key is used to allow remote machines to launch against the configuration INI of an application on a remote machine. The idea being that you can run the EXE on a remote machine, but point at the startup path on the Web server.



    o.lUseRegistryForStartupPath                                      
    

    Remarks

    Set this key to .F. if you need to run the same EXE on a single machinefor different applications in different directories.

    Class wwProcess



    Handling requests and serving as the developer's entry point for Web requests. Most of your Web interface coding will occur at this class level.

    The wwProcess class is the heart of Web Connection from the developer's point of view. This class serves as your code entry point where custom logic occurs. Each request in your Web application maps to a method in a wwProcess subclass. To add new functionality you simply add a method to your implementation of the process class. The simplest implementation looks like this:

    ************************************************************* DEFINE CLASS YourProcess AS WWC_PROCESS ************************************************************* ********************************************************************* FUNCTION HelloWorld() ********************* THIS.StandardPage("Hello World from the " + THIS.Class + " process",; "If you got here, everything should be working fine. The time is: " + TIME()) ENDFUNC * EOF HelloWorld ********************************************************************* FUNCTION QuickData() ******************** IF !USED('tt_cust') USE (THIS.cAppStartPath + "tt_cust") ENDIF Response.HTMLHeader("Customer List") Response.Write("Filename: " + DBF() ) Response.ShowCursor() Response.HTMLFooter() ENDFUNC ENDDEFINE

    which can be accessed with a URL like this:

    Full Url Syntax

    wc.dll?YourProcess~HelloWorld

    Script Map syntax (where wc.dll is mapped to .YP)

    HelloWorld.yp

    Each additional method you add can be referenced in the same manner. Note that in the above StandardPage() is quick method that creates a fully self contained HTML page. For a more customized page you can use the wwResponse object (THIS.oResponse.Write() for example) to create programmatic output or expand template/script pages using the ExpandTemplate/ExpandScript methods of the wwResponse object.

    The easiest way to create a new process class is to use the New Process Wizard from the Web Connection Management Console, which hooks up new process entries to the mainline and creates a skeleton class that you can simply start adding methods to.

    Exposed generic objects

    wwProcess exposes several objects as PRIVATE variables for easier reference:

    These objects are available everywhere in your applications since they sit at the top of the call stack when your code gets called. This means these objects are in scope in your request methods, as well as further down in your business objects. They are also in scope in in Script and Template files where you can do things like the following:

    <%= Request.GetBrowser() %>


    Class Members

    MemberDescription
    OnError Occurs when an error occurs in the Web Connection Process handling and Server.lDebugMode is set to .T.
    OnProcessComplete This event is fired after the request processing has completed, but before the Process object is disposed and the response is sent back to the client.
    OnProcessInit The OnProcessInit method is a hook that can be used to initialize a process class. Here you can create custom objects, run additional configuration tasks that apply to every request that hits this process class.
    Authenticate Authenticates a user based on the Authentication method specified in the cAuthentionMode. Authentication modes supported are Basic and UserSecurity.
    o.Authenticate(lcUserName,lcErrorMessage)
    CaptchaImage Dual purpose function that is used to set a CAPTCHA image and then also serve the image to the client. CAPTCHA can be used to validate a request and ensure that a user enters the data as opposed to a robot or HTTP client to minimize SPAMming any form entries.
    o.CaptchaImage(lcText,lcFont,lnFontSize)
    ErrorMsg This method is a simple way to create a fully self contained HTML page with just a couple of lines of code. This is great for testing output or error messages that let the user know of certain problems with the application.
    o.ErrorMsg(lcHeader, lcBody, lvHeader, lnRefresh, lcRefreshUrl)
    GetAppSetting Returns a setting from the application's INI file.
    o.GetAppSetting(lcKey,lcSection)
    GetBaseUrlPath This method retrieves the base Url for the current request. This method sets the cBaseUrlPath property which is used in ResolveUrl() to fix up URLs.
    o.GetBaseUrlPath()
    GetWCIniSetting This method can be used to read values from the wc.ini file.
    o.GetWCIniSetting(lcKey,lcSection)
    GetWebResourceUrl Returns a URL to retrieve a Web Resource that is embedded in the application or served by the application dynamically. The URL can be used to embed the resource into the page.
    o.GetWebResourceUrl(lcResourceKey)
    InitSession This method creates an automatic session for a user visiting a site and keeps the user attached to this session while browsing around your site. Sessions are great to track people through your site for things like shopping carts that require you to keep info about the user handy.
    o.InitSession(lcSessionCookieName, lnTimeout)
    LogError Method that is used to log an error into the Web Connection Error Log. This method should be called from the OnError event of this class.
    o.LogError(loException)
    Process Use of this method is deprecated. You should use OnProcessInit instead.
    o.Process()
    ResolveUrl This method resolves a Url that starts with a generic ~/ prefix and replaces this with the base virtual path of the application.
    o.ResolveUrl()
    SendErrorEmail This method sends email to the assigned administrator. It's specialized in that the email message automatically contains status information about the currently running Web request to help pin down the problem easier.
    o.SendErrorEmail(lcSubject, lcMessage, lcRecipient, lcSender)
    StandardPage This method is identical in behavior to the ErrorMsg() method and the default implementation simply calls back to the ErrorMsg() method. Please see the documentation for this method. The main reason for both methods is to provide one error message and one status message page separately.
    o.StandardPage(lcHeader, lcBody, lvHeader, lnRefresh, lcRefreshUrl)
    WebResource Generic output method can be used to serve embedded resources like related image, css, javascript or other files through the application
    o.WebResource()
    cAuthenticatedUser The authenticated user after a call to Authenticate() if the request authenticated properly.
    cAuthenticationMode The method used to authenticate requests. Supported methods: Basic, UserSecurity.
    cAuthenticationUserMessage Message displayed on the Authentication dialog for the user when cAuthenticationMode is UserSecurity.
    cAuthenticationUserSecurityClass The class used for Authentication when the cAuthenticationMode is set to UserSecurity.
    cErrorTemplate This method allows overriding the default error display page using a template page called through ExpandTemplate. If set specifies a full physical disk path to a page that is used for ErrorMsg and StandardPage output.
    cMethodExecutionExclusions Allows exluding of class methods that are executed as methods on the process class. This allows executing of script pages even if a matching method exists on the wwProcess subclass.
    cSessionKey The name of the Session Cookie used for this application. This is the default Session value.
    cUrlBasePath The Web Application Virtual Path that marks the base path of this application. This path is used internally by ResolveUrl() to define the base path for a URL and the resolving of the ~ character in a path.
    lEnableSessionState Turns on Session Handling for this request.
    lShowRequestData Flag that when set causes the current request data - Form Variables, ServerVariables, Session Vars - to be displayed at the end of the current request.
    nPageScriptMode Determines the script mode used when a matching method is not found in the class. 1 - Classic (ExpandTemplate), 2 - Web Control Framework Page)
    oConfig The oConfig member maps to the server's configuration object for this project.
    oRequest Object reference to a ready to use wwRequest object, which is used for all inputs of your application. This object is preset by the Init method of this class. When Process or your custom methods get control the oRequest object is already set.
    oResponse Object reference to a ready to use wwResponse object, which is used for all HTTP output. This object is preset by the Init method of this class. When Process or your custom methods get control the oResponse object is already set.
    oServer Object reference to a ready to use wwServer object, which is passed down from the wwServer Process method which calls into your wwProcess subclass. This object is preset by the Init method of this class. When Process or your custom methods get control the oServer object is already set.
    oSession If you use the InitSession() method the oSession object will be loaded for you during the call to that method. Once loaded you can access the oSession object to retrieve and store session variables.
    oUserSecurity If a request Authenticated using cAuthenticationMode of UserSecurity you can access the user security object directly from here, with the appropriate user selected.

    Requirements

    Assembly: wwProcess.prg

    wwProcess::OnError


    Occurs when an error occurs in the Web Connection Process handling and Server.lDebugMode is set to .T.

    This method is the core error handler in Web Connection that gets fired when any error in executing code occurs. This event is fired from a TRY CATCH block in the main Process method of the class and forwards its exception object to this OnError method.

    The default error handler performs the following tasks:

    The default implementation of this method displays a descriptive error message to the user and calls LogError to log the error and fire administrative emails. If you override this method for custom error behavior make sure that you call wwProcess::LogError from your code.

    The default implementation looks like this and gives you an idea what error information is available for your custom OnError handlers:

    ************************************************************************ * wwProcess :: OnError **************************************** *** Function: Called when an error occurs and Server.lDebugMode is off. *** Assume: *** Pass: loException *** Return: .T. if you completely handled the error including output *** generation. Else return .F. ************************************************************************ FUNCTION OnError(loException as Exception) *** Shut down this request with an error page - SAFE MESSAGE (doesn't rely on any objects) THIS.ErrorMsg("Application Error",; "An application error occurred while processing the current page. We apologize for the inconvenience. " + ; "The error has been logged and forwarded to the site administrator and we are working on fixing this problem as soon as we can.<p>" + ; "<p align='center'><table cellpadding='5' border='0' background='whitesmoke' width='550' " +; "style='font-size:10pt;border-collapse:collapse;border-width:2px;border-style:solid;border-color:navy'>" + CRLF +; "<tr><td colspan='2' class='gridheader' align='left' style='font-weight:bold;color:cornsilk;background:navy'>Error Information:</th></tr>" + CRLF +; "<tr><td align='right' width='150'>Error Message:</td><td>"+ loException.Message + "</td></tr>" + CRLF +; "<tr><td align='right'>Error Number:</td><td> " + TRANSFORM(loException.ErrorNo) + "</td></tr>" + CRLF +; "<tr><td align='right'>Running Method:</td><td> " + loException.Procedure + "</td></tr>"+CRLF+; "<tr><td align='right'>Current Code:</td><td> "+ loException.LineContents + "</td></tr>"+CRLF+; "<tr><td align='right'>Current Code Line:</td><td> " + TRANSFORM( loException.LineNo) + "</td></tr>"+ crlf +; "<tr><td align='right'>Exception Handled by:</td><td>" + THIS.CLASS + ".OnError()</td></tr></table></p>") *** Log the Request IF TYPE("THIS.oServer")="O" AND !ISNULL(THIS.oServer) this.LogError(loException) ENDIF *** We've completely handled the error! RETURN .T. ENDFUNC * wwProcess :: OnError



    o.OnError(loException)                                           
    

    Remarks

    This method fires inside of a CATCH block so be very sure that this code is error free or you will fire an error that will hang the FoxPro instance.

    wwProcess::OnProcessInit


    The OnProcessInit method is a hook that can be used to initialize a process class. Here you can create custom objects, run additional configuration tasks that apply to every request that hits this process class.

    This method is called from the beginning of the Process() method call after the various PRIVATE objects (Response, Request, Server, Session, Config) have been assigned.

    This method should be used instead of overriding the Process() method in previous versions. Note that you cannot declare PRIVATE variables visible to Process methods later here as this method doesn't stay at the top of the call stack. If you need to declare PRIVATE variables visible to your Process method code, you still have to override Process. In this case we recommend you do this:

    FUNCTION Process *** Use Process() overrides only to declare PRIVATE vars *** otherwise use OnProcessInit() PRIVATE CustomVar CustomVar = CREATEOBJECT("MyCustom") DODEFAULT() ENDFUNC FUNCTION OnProcessInit() *** Any process configuration code THIS.InitSession("MyCookie") IF !THIS.Login("ANY") *** Stop processing RETURN .f. ENDIF *** Continue processing RETURN .T. ENDFUNC



    o.OnProcessInit()                                                
    

    wwProcess::OnProcessComplete


    This event is fired after the request processing has completed, but before the Process object is disposed and the response is sent back to the client.

    When this event fires all the intrinsic objects (Response, Request, Server etc.) are still available and you can potentially still modify the output sent back to the client by pushing data into the Response object.

    o.OnProcessComplete()                                            
    

    wwProcess::Authenticate


    Authenticates a user based on the Authentication method specified in the
    cAuthentionMode. Authentication modes supported are Basic and UserSecurity.

    This method provides a comprehensive authentication hook to a Web Connection request and check for authentication easily from within your code. It allows using Basic Auth or a UserSecurity class for authentication. This method is always accessible with Process.Authenticate() or alternately in Web Page code as THIS.Authenticate() (ie. on the wwWebPage class) which simply forwards to Process.Authenticate.

    If Authentication succeeds the Process.cAuthenticatedUser property is set which you can check for the username that is authenticated in your code.

    Basic Auth

    With Basic Auth all you need to provide is the authentication directive/user name. Call Authenticate with a parameter of ANY or WCINI or a username list (comma delimited) to authenticate for that specific user. Leave blank and authentication will always succeed - no authentication occurs. Basic Authentication works against Windows User Accounts and is managed by the Web Server itself.

    The request fires and if not authenticates pops up a Windows Authentication box. You put in your username a

    UserSecurity Auth

    This mechanism provides more control and uses a FoxPro class and a FoxPro table (by default) to authenticate users. You can optionally specify which class is used (must follow the wwUserSecurity interface or inherit from it) to authenticate, which allows overriding default behavior and operation of the class. The default mechanism looks up user info in a FoxPro table.

    When authentication succeeds you can check the cAuthenticatedUser property which returns the user's login name. You can also access the oUserSecurity property to get full access to the currently selected user (Process.oUserSecurity.oUser) but note that this requires a database lookup which otherwise is performed only when logging in.

    User Security Authentication stores authentication info in Session Variable which also means that Cookies must be enabled for this feature to work.

    o.Authenticate(lcUserName,lcErrorMessage)                        
    

    Parameters

    Note: The parameters are used only for Basic Authentication. The are ignored for UserSecurity authentication

    lcUserName
    Username with Basic Authentication
    The Username to Verify against in when cAuthenticationMode="Basic":

    Logout
    You can also pass a parameter of Logout with UserSecurity logins which forces the request to remove the authentication value stored in the session object.

    lcErrorMessage
    HTML Error message displayed when authentication fails as a string.


    Example

    *** User Security Authentication Web Control Page FUNCTION OnLoad() IF !Process.Authenticate() RETURN ENDIF this.lblMessage.Text = Process.cAuthenticatedUser + " " + ; Process.oUserSecurity.oUser.Fullname ENDFUNC *** User Security Authentication Process Class *** In the class header cAuthenticationMode = "UserSecurity" FUNCTION TestFunction IF !THIS.Authenticate() RETURN ENDIF this.StandardPage("You've Authenticated as " + this.cAuthenticatedUser) ENDFUNC *** Basic Authentication in a Process Class *** In the class header cAuthenticationMode = "Basic" FUNCTION TestFunction IF !THIS.Authenticate("WCINI") RETURN ENDIF this.StandardPage("You've Authenticated as " + this.cAuthenticatedUser) ENDFUNC

    wwProcess::CaptchaImage


    Dual purpose function that is used to set a CAPTCHA image and then also serve the image to the client. CAPTCHA can be used to validate a request and ensure that a user enters the data as opposed to a robot or HTTP client to minimize SPAMming any form entries.

    Please also check out the wwWebCaptcha Control which makes use of Captcha in Web Control Framework pages very easy.

    The first mode allows setting up the CAPTCHA with text and font and font size information. This sets up the CAPTCHA and stores the information in the active Session object. Note InitSession() must have been called! The session Variable used is "__Captcha".

    The second mode is responsible for actually serving the CAPTCHA to the client by using a special URL in yoru process class:


    where.wwd is the name of your scriptmap. This mode actually serves the image.

    The easiest way to see this work is in a Process method that handles both display and validation of the CAPTCHA:

    FUNCTION CaptchaTest lcResult = "" *** On a Postback validate CAPTCHA against form input IF Request.IsPostBack() lcCaptcha = Session.GetSessionVar("__CAPTCHA") IF LOWER(lcCaptcha) == LOWER(Request.Form("txtCaptcha")) lcResult = "You matched it!" ELSE lcResult = "Bing: Try again sucker" ENDIF ENDIF *** Generate a new CAPTHA - if you have a single page for display and postback *** make sure you generate the new CAPTCHA AFTER you've checked it! this.CaptchaImage(RIGHT(SYS(2015),5),"Verdana",30) *** Notice the imagelink: CaptchaImage.wwd TEXT TO lcHTML TEXTMERGE NOSHOW <form method="POST" action=""> Here it is: <img src='CaptchaImage.wwd'> <input name="txtCaptcha" > <input type="submit" name="btnSubmit" value="save"> </form> ENDTEXT this.StandardPage("Output",lcHtml) ENDFUNC

    You can also use the wwWebCaptcha Control which is easier to use than these low level functions, both in Web Control Pages (where everything is automatic) and in plain Process methods.

    o.CaptchaImage(lcText,lcFont,lnFontSize)                         
    

    Parameters

    lcText
    The text of the Captcha to display

    lcFont
    The font used for the CAPTCHA generated

    lnFontSize
    The font size of the CAPTCHA

    Remarks

    Requires that Session state is enabled for your Process class (this.InitSession()). It should be set globally because the CaptchaImage callback will requires the session to be available and it won't access any of your process method code. Set this.InitSession() in OnProcessInit().

    wwProcess::WebResource


    Generic output method can be used to serve embedded resources like related image, css, javascript or other files through the application

    This routine can be called with a URL like this:

    /wconnect/WebResource.wwd?Resource=WcPowerLogo

    where .wwd is the extension of your scriptmap. You can also access the resource like this:

    /wconnect/wc.dll?MyProcess~WebResource~&Resource=WcPowerLogo

    Resources can be of any type and can be added to the server at any time via the wwServer::AddResource(). This functionality is very useful for including related files of your application as part of the EXE rather than including the files externally in the Web directory. To use:

    Note that you can use wwProcess::GetWebResourceUrl to get a URL returned in the current application that can be used to retrieve a resource.

    o.WebResource()                                                  
    

    wwProcess::GetWebResourceUrl


    Returns a URL to retrieve a Web Resource that is embedded in the application or served by the application dynamically. The URL can be used to embed the resource into the page.

    WebResources are loaded with wwServer::AddResource and served with wwProcess::WebResource. This method can be used to get a relative URL returned that allows you to invoke a WebResource dynamically for embedding into the page.

    This method will be useful for control developers who want to dynamically embed resources into their applications to avoid having to ship external files with custom controls. An example of a control that uses this mechanism is the wwWebAjax control which uses a JavaScript file that is embedded in this way.

    o.GetWebResourceUrl(lcResourceKey)                               
    

    Parameters

    lcResourceKey
    The key used to identify the resource when the resource was added with wwServer.AddResource()

    wwProcess::ErrorMsg


    This method is a simple way to create a fully self contained HTML page with just a couple of lines of code. This is great for testing output or error messages that let the user know of certain problems with the application.

    This method also supports auto refresh of the Web page to go to another URL after a number seconds.

    This method is implemented identical as the StandardPage method of this class. There are two separate versions to allow for two kinds of status pages to be displayed so error can look different than successful response pages. To customize these methods simply reimplement the methods in your subclass of wwProcess.

    You can also override this method with a custom template by specifying a template file path in the cErrorTemplate property. When set the template is used instead of the hardcoded ErrorMsg method code.

    o.ErrorMsg(lcHeader, lcBody, lvHeader, lnRefresh, lcRefreshUrl)
    

    Return Value

    nothing

    Parameters

    lcHeader
    The header of the message. This is bolder at the top of the page and also reflects in the browser's title bar.

    lcBody
    The body of the message. This text can contain plain text or HTML.

    lvHeader
    Optional. The content type or HTTP Header object for this page. You can create a custom header with the wwHTTPHeader class or pass an HTTP content type.

    lnRefresh
    Optional - If you would like the page to refresh itself after a while to go to this or another URL you can specify an interval in seconds.

    lcRefreshUrl
    Specify the URL that you want to go to when you refresh the page automatically.

    Remarks

    Although this class is an output method, which typically should belong to the Response object (which has this same identical method BTW), this class is implemented here to allow you to easily subclass the method for custom display formatting, which you will most likely perform on this method. Since wwProcess is always subclassed and wwResponse almost never this was a logical compromise to provide an easier inheritance implementation.

    wwProcess::GetAppSetting


    Returns a setting from the application's INI file.


    o.GetAppSetting(lcKey,lcSection)                                 
    

    Parameters

    lcKey

    lcSection


    Remarks

    Note: this method is not recommended. You should use the wwServerConfig object instead and read properties of that object. This method is slow as it goes out to the INI file each time and can cause potential bottlenecks if multiple instances access the file simultaneously. The wwServerConfig object is in memory, fast and safe across instances.

    This method is useful if you already have a compiled server and you need to create a new setting and use it in a script page or other dynamic mechanism (like a Web Service).

    wwProcess::GetBaseUrlPath


    This method retrieves the base Url for the current request. This method sets the cBaseUrlPath property which is used in
    ResolveUrl() to fix up URLs.

    This method is called early on in INIT of the Process class.

    This method by default tries to look at:

    EVALUATE("this.oServer.oConfig.o" + this.Class + ".cVirtualPath")

    This looks for the process specific configuration class (created with the new project/process wizards) and retrieves its cVirtualPath property. If it exists it is used otherwise the BasePath will be blank.

    You can override this method to provide custom functionality. In fact, this method is not meant to be called by your code, but rather exists as a hook method to allow overriding the default behavior only.

    o.GetBaseUrlPath()                                                   
    

    wwProcess::GetWCIniSetting


    This method can be used to read values from the wc.ini file.

    Note that this method relies on the wcConfig key passed back from the wc.dll request to figure out the location of the INI file. This means this method is valid only during or after the first hit has occurred. Since this is a process method this is usually not a problem though.

    o.GetWCIniSetting(lcKey,lcSection)                               
    

    Return Value

    string or "" if not found

    Parameters

    lcKey
    The key to retrieve.

    lcSection
    The section in the wc.ini file to retrieve. Defaults to the wwcgi section which is the main section.

    wwProcess::InitSession


    This method creates an automatic session for a user visiting a site and keeps the user attached to this session while browsing around your site. Sessions are great to track people through your site for things like shopping carts that require you to keep info about the user handy.

    This method creates a session for the current process automatically. It uses the wwSession object to create a transparent session that automatically manages the cookie checks and assignments.

    To create an Auto Session is a two step process:

    1. Create the Session
      In your subclass of wwProcess in the Process method add the following code:
      FUNCTION Process
      THIS.InitSession("wwDemo")
      DODEFAULT()
      RETURN


    2. Use the Session in your code
      FUNCTION YourProcessMethod
      
      lcSessionVar = THIS.oSession.GetSessionVar("MyVar")
      IF EMPTY(lcSessionVar)
         *** Not set
         THIS.oSession.SetSessionVar("MyVar","Hello from a session. Created at: " + TIME())
         lcSessionVar = THIS.oSession.GetSessionVar("MyVar")
      ENDIF
      
      THIS.StandardPage("Session Demo","Session value: " + lcSessionVar)
      RETURN

    Typically you'll want to call InitSession in the Process method as I did above to initialize the Session for every hit the user makes on your application. InitSession() works well if you don't need to perform validation checks on the session. This method simply creates a session for a user if one doesn't exist already. Otherwise the same session is recovered based on the user's cookie. What this means is that InitSession offers you no hook to check whether a new session was created or whether you got an existing one, although the method does return the session ID to you. Since the Session ID is also the Cookie used you can rig this to do more sophisticated checks, but this is not what InitSession is intended for. InitSession() is not meant for checking and tracking login information. If you need to do this you'll have to create sessions manually and check the cookies for validity. See docs on the wwSession class.

    This method is responsible for initializing the oSession object property. The object allows attaching a Session to a particular user and attachment of any character variables to that user and session using the THIS.oSession.GetSessionVar() and THIS.oSession.SetSessionVar() methods. This method automatically retrieves a Cookie specified by the single parameter. The method also sets a cookie on the response request if a new cookie is created as long as an wwHTTPHeader Object, or wwHTML::ContentTypeHeader, HTMLHeader, ShowHTMLPage, ShowMemoPage are used.

    o.InitSession(lcSessionCookieName, lnTimeout)                    
    

    Return Value

    String - The current Session ID.

    Parameters

    lcSessionCookieName
    Optional - Name of the cookie to be created. Default is wwSession.

    lnTimeout
    The timeout on the session in seconds. This is how long the session sticks around for the user. Once this time is up and the user returns the session's state is released. Default: 1200.

    llPersist
    Creates a permanent cookie on the client that allows the session cookie to persist permanently on the client. Note that this may be useful for customer tracking on repeat visits. For example you can store the Session ID as part of a customer table and then retrieve the customer's ID/info based on that key. Note that only the Cookie persists - not the actual session (unless you set a really large timeout value) to allow you to retrieve the user ID and reuse it by setting this parameter to .T.

    Remarks

    Important!

    InitSession() requires one of Web Connection's standard HTTP header creation methods are called. The following methods fulfill this requirement:

    Make sure you check this if you have problems with sessions not working correctly!!! One of the above methods must be called for autosessions to work correctly when creating the initial cookie for the user. Once the cookie exists this is not a requirement any longer since the cookie is simply re-read.

    If you cannot call one of these methods - use the wwSession object manually. You can look at the code for InitSession() to get a solid idea how the code works.

    wwProcess::LogError


    Method that is used to log an error into the Web Connection Error Log. This method should be called from the OnError event of this class.

    o.LogError(loException)                                          
    

    Parameters

    loException
    An Exception object that contains error information about the last failure. Note that you can override this Exception object to provide custom messages.

    wwProcess::Process


    Use of this method is deprecated. You should use
    OnProcessInit instead.

    The Process method is the core processing method of the wwProcess class that initiates and completes request processing. It does all of the work.

    Use this method only if you need to declare PRIVATE variables that need to be visible to your lower level Process methods. Whenever you call this method make sure you call DoDefault() to handle core processing:

    FUNCTION Process PRIVATE MyCustom MyCustom = CREATEOBJECT("MyCustom") DODEFAULT() ENDFUNC


    o.Process()                                                      
    

    wwProcess::ResolveUrl


    This method resolves a Url that starts with a generic ~/ prefix and replaces this with the base virtual path of the application.

    This method is used extensively by the Web Control Page framework to allow placing images on forms and having them rendered without having to rely on specific relative paths. Instead you can use ~/ to have the application base path replaced into the path automatically.

    The virtual path is determined through the protected GetBaseUrl() method that is called during Init of the class.

    o.ResolveUrl()                                                   
    

    Remarks

    ResolveUrl relies on the cUrlBasePath property which is used to fix up paths. By default this value is set from the Process specific Configuration object defined in your Mainline PRG file. If you don't have a process level configuration object or the object is not hooked up or doesn't contain a cVirtualPath property the base path will end up blank and result in invalid Urls. To fix either assign cUrlBase in your Process startup code (OnProcessInit or Init or cUrlBasePath assignment) or make sure you create a configuration object and attach it to the server configuration object.

    wwProcess::SendErrorEmail


    This method sends email to the assigned administrator. It's specialized in that the email message automatically contains status information about the currently running Web request to help pin down the problem easier.

    The wwProcess::Error method for example, uses this method internally to send email to the administrator and adds code error information (Method, line, code, error message etc.).



    o.SendErrorEmail(lcSubject, lcMessage, lcRecipient, lcSender)
    

    Parameters

    lcSubject
    The header of the email. This header should have an easily identifiable text so you can flag and possibly sort this message into an error mail box for easy retrieval of problems on your server.

    lcMessage
    The body of the message. This body is appended with information about the request itself including the URL that ran the server that got hit, the client's IP address and browser.

    lcRecipient
    Optional - The email address of the person that is to receive the email. If not specified it defaults to the WWC_ADMINISTRATOR_EMAIL setting in wconnect.h.

    lcSender
    Optional - The email address of the person that's sending the message. If not specified it defaults to the WWC_ADMINISTRATOR_EMAIL setting in wconnect.h.


    Remarks

    This method relies on settings in wconnect.h for default values. In particular it looks at WWC_SENDEMAIL_ONERROR to determine whether email is sent at all, WWC_ADMINISTRATOR_EMAIL for whom to send mail to, and WWC_MAILSERVER for which mail server to use.

    This method uses wwIPStuff::SendMailAsync behind the scenes.

    wwProcess::StandardPage


    This method is identical in behavior to the
    ErrorMsg() method and the default implementation simply calls back to the ErrorMsg() method. Please see the documentation for this method. The main reason for both methods is to provide one error message and one status message page separately.

    o.StandardPage(lcHeader, lcBody, lvHeader, lnRefresh, lcRefreshUrl)
    

    Parameters

    lcHeader
    The header of the message. This is bolder at the top of the page and also reflects in the browser's title bar.

    lcBody
    The body of the message. This text can contain plain text or HTML.

    lvHeader
    Optional. The content type or HTTP Header object for this page. You can create a custom header with the wwHTTPHeader class or pass an HTTP content type.

    lnRefresh
    Optional - If you would like the page to refresh itself after a while to go to this or another URL you can specify an interval in seconds.

    lcRefreshUrl
    Specify the URL that you want to go to when you refresh the page automatically.

    wwProcess::cAuthenticationMode


    The method used to authenticate requests. Supported methods: Basic, UserSecurity.

    o.cAuthenticationMode
    

    wwProcess::cAuthenticatedUser


    The authenticated user after a call to Authenticate() if the request authenticated properly.

    o.cAuthenticatedUser                                             
    

    wwProcess::cAuthenticationUserSecurityClass


    The class used for Authentication when the
    cAuthenticationMode is set to UserSecurity.

    o.cAuthenticationUserSecurityClass                               
    

    wwProcess::cAuthenticationUserMessage


    Message displayed on the Authentication dialog for the user when cAuthenticationMode is UserSecurity.

    o.cAuthenticationUserMessage                                     
    

    wwProcess::cSessionKey


    The name of the Session Cookie used for this application. This is the default Session value.

    This value defaults to the WWWC_DEFAULT_SESSIONCOOKIE_NAME defined in wconnect.h. It's recommended that if you override this value you override it in the wwProcess class definition:

    DEFINE CLASS WebLogProcess as wwProcess cSessionKey = "WebLogCookie" ENDDEFINE


    o.cSessionKey                                                    
    

    wwProcess::cUrlBasePath


    The Web Application Virtual Path that marks the base path of this application. This path is used internally by ResolveUrl() to define the base path for a URL and the resolving of the ~ character in a path.

    The value is a virtual path. For example:

    This value can be explictly set in your Process Initialization code, although this is not recommended.

    By default this value is retrieved from the Server.oConfig.o<yourProcess>.cVirtualPath, which originates on the Process specific Configuration object defined in your <yourapplication>.Ini file.

    *** Process Config Section.

    The path is a LOGICAL path and should contain a trailing /.


    If available this value is read from the process specific Config object. In wwProcess the following code exists:

    IF TYPE("Server.oConfig.o" + THIS.Class +".cVirtualPath") = "C" THIS.cApplicationPath = EVALUATE( "Server.oConfig.o" + THIS.Class +".cVirtualPath") ENDIF



    o.cUrlBasePath
    

    wwProcess::cErrorTemplate


    This method allows overriding the default error display page using a template page called through ExpandTemplate. If set specifies a full physical disk path to a page that is used for ErrorMsg and StandardPage output.

    The template should include the following elements at a minimum:

    <html>
    	<head>
    	   <meta http-equiv="Content-Language" content="en-us">
    	   <title><%= pcHeader %></title>
    	   <link rel="stylesheet" type="text/css" href="westwind.css">  <!-- Optional -->
     	   <%= IIF(pnRefresh>0,[<META HTTP-EQUIV="Refresh" CONTENT="]+TRANS(pnRefresh)+ [; URL=]+pcRefreshUrl + [">],[wwSoap::cHttpProxy]) %>
    	</head>
    <body topmargin=0 leftmargin=0>
    <h1><%= pcHeader %></h1>
    <hr>
    <%= pcBody %>
    </body>

    The variables exposed are the same as the parameters to ErrorMsg except with a 'p' (for PRIVATE) instead of 'l' (LOCAL) first letter.

    pcHeader - header text
    pcBody - body text
    pcRefreshUrl - Url to redirect to if provided
    pnRefresh - Time in seconds to redirect if non zero

    o.cErrorTemplate                                                 
    

    wwProcess::cMethodExecutionExclusions


    Allows exluding of class methods that are executed as methods on the process class. This allows executing of script pages even if a matching method exists on the wwProcess subclass.

    This method is used to override methods like Login and Process to ensure these are not fired as methods, but CAN be run as script pages.

    Default Value:
    ",login,process,"



    o.cMethodExecutionExclusions                                     
    

    wwProcess::lEnableSessionState


    Turns on Session Handling for this request.

    This code eliminates the need to call this.InitSession explicitly and assigning it to the Session object instance. This eliminates the need to call InitSession() in the right place - with this property you can simply set the value anywhere and it just works eliminating the need to understand how the Session object works beyond getting and setting values.


    o.lEnableSessionState                                            
    

    wwProcess::lShowRequestData


    Flag that when set causes the current request data - Form Variables, ServerVariables, Session Vars - to be displayed at the end of the current request.

    Only applies if the content type of the request is text/html.

    o.lShowRequestData                                               
    

    Remarks

    Be careful of this functionality if you return non-HTML content as it will corrupt the output with the appended HTML in that case.

    The Request Data is simply appended to the end of the HTML content.

    You can use this property either at the Process method level or directly in any request before the Response object is released.

    Also check the wwServerConfig::lShowRequestData property which can be used globally to control display of this output.

    Example

    *** Example of conditionally enabling with a QueryString value 
    *** in a Process method override via a QueryString value
    FUNCTION Process
    
    #IF DEBUGMODE    && Only allow in debug mode
    IF !EMPTY(THIS.oRequest.QueryString("ShowRequestData"))
       IF !THIS.Login("WCINI")
          RETURN
       ENDIF
       THIS.lShowRequestData = .T.   
    ENDIF   
    #ENDIF
    
    DODEFAULT()
    
    RETURN .T.
    
    

    wwProcess::nPageScriptMode


    Determines the script mode used when a matching method is not found in the class. 1 - Classic (ExpandTemplate), 2 - Web Control Framework Page)

    The new default is 2.

    o.nPageScriptMode                                                
    

    wwProcess::oConfig


    The oConfig member maps to the server's configuration object for this project.

    You can access any custom configuration settings you've defined on the object through the following interfaces from within a wwProcess class:

    THIS.oConfig.cHtmlPagePath Config.HtmlPagePath

    oConfig and Config are mapped if a configuration class with the same name as the class exist (for the wwDemo class it would Server.oConfig.owwDemo). These are mapped to wwServerConfig instances that act as a master class. The class has to map to:

    Server.oConfig.o<ProcessName>

    For example, for the wwDemo class the hierarchy looks like this:

    Server.oConfig.owwDemo

    The class must be defined and hooked up to the wwServerConfig instance of the application. The new Process Wizard automatically creates these classes which are declared and hooked up at the bottom of your application's main PRG file (<myapp>Main.prg). Please check there to see how this works.

    Use the configuration class to hold any commonly configurable settings that get persisted into the application's INI file.

    o.oConfig                                                        
    

    wwProcess::oRequest


    Object reference to a ready to use
    wwRequest object, which is used for all inputs of your application. This object is preset by the Init method of this class. When Process or your custom methods get control the oRequest object is already set.

    The default Process method also exposes this object as a PRIVATE variable Request.

    o.oRequest                                                       
    

    wwProcess::oResponse


    Object reference to a ready to use
    wwResponse object, which is used for all HTTP output. This object is preset by the Init method of this class. When Process or your custom methods get control the oResponse object is already set.

    The default Process method also exposes this object as a PRIVATE variable Response.

    o.oResponse                                                      
    

    wwProcess::oServer


    Object reference to a ready to use
    wwServer object, which is passed down from the wwServer Process method which calls into your wwProcess subclass. This object is preset by the Init method of this class. When Process or your custom methods get control the oServer object is already set.

    The default Process method also exposes this object as a PRIVATE variable Server.

    o.oServer                                                        
    

    wwProcess::oSession


    If you use the
    InitSession() method the oSession object will be loaded for you during the call to that method. Once loaded you can access the oSession object to retrieve and store session variables.


    o.oSession                                                       
    

    wwProcess::oUserSecurity


    If a request Authenticated using cAuthenticationMode of UserSecurity you can access the user security object directly from here, with the appropriate user selected.


    FUNCTION OnLoad() IF !Process.Authenticate() RETURN ENDIF this.lblMessage.Text = this.cAuthenticatedUser + " " + ; this.oUserSecurity.oUser.Fullname) ENDFUNC



    o.oUserSecurity                                                  
    

    Class wwRequest


    The wwRequest class' purpose is to allow your programs to receive information from the web server and the HTML page that generated a Web request. It retrieves Form and QueryString variables, Cookies and ServerVariables - it basically acts as your input source for a request.

    Class Members

    MemberDescription
    aFormVars This method retrieves all the form variables that were submitted into a two dimensional array.
    o.aFormVars(@laVars,lcPrefix)
    Form Retrieves an HTML form variable posted to the Web server. Form variables are the primary interface for a Web client to communicate with a Web server and POST variables are the most common. Everytime a user on an HTML page fills out a form and submits it the values are POSTed to the server and can be retrieved with Request.Form("fieldname").
    o.form(lcVarname)
    FormVarsToObject Parses an object and pulls form variables from the request buffer by matching the property names to the request variables.
    o.FormVarsToObject(loObject,lcPrefix)
    GetApplicationPath Returns the physical OS path of the virtual directory of this Web Server application. In short it maps the virtual directory defined in IIS to a physical path. Can be used as a 'base url' for Web applications to base relative URls on.
    o.GetApplicationPath()
    GetAuthenticatedUser Returns the name of a user if he has been authenticated by the Web server. This variable gets set and stays set once a user has entered a valid username into the browser dialog box when prompted. The username is valid for a given Web server path and down and once set cannot be unset until the browser is shut down or another authentication request is made for the same user.
    o.getauthenticateduser(lnMode)
    GetBrowser Returns the client's browser display name.
    o.getbrowser()
    getclientcertificate Returns the contents of a client Certificate's Subject key. This information contains the info about the client. The following information is provided:
    o.getclientcertificate(lcSubKey)
    GetCookie Returns an HTTP Cookie that was previously set. HTTP cookies allow keeping state by keeping a persistent variable on the user's browser. Cookies are sent along in each HTTP request and appear as a server variable in the incoming request data.
    o.getcookie(lcCookie)
    GetCurrentUrl Returns the current URL.
    o.getcurrenturl(llHTTPS)
    GetExecutablePath
    Returns the logical, Web relative path to the DLL or script.
    o.getexecutablepath()
    GetExtraHeader Returns an Extra Header variable from the ALL_HTTP block.
    o.GetExtraHeader(lcHeader)
    GetFormMultiple This method retrieves multiselect HTML form variables from the CGI content file into an array. Multiselect variables can be returned when using scrolling HTML lists with the SELECT MULTIPE option or multiple radio buttons and checkboxes using the same variable name.
    o.getformmultiple(@taVars,tcVarname)
    GetIPAddress Returns the client's IP Address.
    o.getipaddress()
    GetLocale Returns the currently active language in the browser if available.
    o.GetLocale(@laLanguages)
    GetLogicalPath Returns the web server relative path of the current request. For example:
    o.getlogicalpath()
    GetMultipartFile Retrieves a file uploaded with HTTP file upload into a binary string.
    o.GetMultipartFile(lcKey, lcFileName)
    GetMultipartFormVar Retrieves a multipart form variable from the request buffer. Multipart form variables are submitted on the client side by specifiying an encoding type of "multipart/form-data".
    o.GetMultipartFormVar(lcKey)
    GetPhysicalPath Returns the physical path to a script mapped page or an executable DLL file. The physical path is a great tool for capturing the system specific path of script mapped pages, so you can capture the location of the page for further parsing.
    o.getphysicalpath()
    getpreviousurl Returns the URL of the page that this request was called from. If this page was typed into the browser window manually or accessed from a non-browser client this value will be blank.
    o.getpreviousurl()
    GetRawFormData Returns all of the form data in raw form.
    o.GetRawFormData()
    GetRelativeSecureLink This method allows you to easily create a secure link simply by specifying a relative link like any other link.
    o.GetRelativeSecureLink(lcLink,llNonSecure)
    GetRequestId Returns the unique Request ID for the currently executing request.
    o.GetRequestId()
    getservername Returns the server's domain or IP address. Note that this value returns only the server portion of the current URL.
    o.getservername()
    getserversoftware Returns the software that's running the Web server.
    o.getserversoftware()
    GetWCIniValue Retrieves a value from the Web Connection ISAPI/CGI configuration file. Most useful for retrieving the AdminUser value.
    o.getwcinivalue(lcKey, lcSection)
    InitializeRequest This method is responsible for setting up the Request object on each hit by passing in the POST data in some format and making it available to the specific Get methods. This method is fired on every Web request hit and deals with clearing out values from previous requests and then reassigning new values.
    o.initializerequest(lcPostData, lcTempPath)
    IsFormVar Checks to see if a form variable exists in the request POST buffer. This method returns .T. if the key exists even if the value is blank. It will only return .F. if the key doesn't exist at all. If you need to check for blank you can simply read the key with wwRequest::Form().
    o.IsFormVar(lcKey)
    islinksecure Checks to see if the user is coming in over the SSL port.
    o.IsLinkSecure(lcSecurePort)
    IsPostBack Determines whether the current request is running in POST mode.
    o.IsPostBack(lcVar)
    Params Returns a value by checking FormVars, QueryString, Session and ViewState (in that order) for a matching key and returning the value.
    o.Params(lcKey)
    QueryString This method returns the Query String and individual pieces of it. The method supports both numeric,positional parameters and named parameters. Positional parameters are great for grabbing request information:
    o.QueryString(lvKey)
    ServerVariables Retrieves a server variable from the request data. Server variables provide information about the current request including info about the client application (browser, IP Address), the current request (server name, querystring), authentication (username, port accessing this app) and status information (Cookies, type of request) etc.
    o.servervariables(lcKey)
    SetKey Sets a form or server variable to the specified value from the existing POST data block.
    o.setkey(lcKey, lcValue, lvReserved)
    cFormVars This property holds the raw POST buffer that the HTTP client submitted.
    cpathoverride Actual location of the Temporary path. This path is used to override any 'physical' paths to point to the network path instead.
    cServerVars This property holds the raw Server variables returned from the Web Connection DLL. This data is URLEncoded and was created by Web Connection on the fly. Every call to ServerVariables accesses this data directly to extract the appropriate server variable value.
    lusexmlformvars If .T. Request.Form retrieves variables from an XML document contained in the form buffer rather than regular post variables.
    lUtf8Encoding If .T. causes Request.Form() and Request.QueryString() to UTF-8 decode the returned values.
    nPostMode Request property that gets set by InitializeRequest and determines what kind of Form submission is occurring.
    oapi Internal wwAPI object. The object is not protected to allow persistent access to an API object through the Request.oAPI member.
    oxml Internal reference to a wwXML object. This object is not protected and can be treated as a 'global' reference to a wwXML object accessible through wwRequest.oXML.

    Requirements

    Assembly: wwRequest.prg

    How wwRequest works


    The wwRequest object is basically your input for a Web request. With it you can retrieve all the information that the Web Server makes available for each request.

    It allows access to:


    Here's how the access to the various items works:

    Form Variables:

    lcName = Request.Form("txtName")

    Query Strings:

    lcId = Request.QueryString("id")

    Server Variables:

    lcServerName = Request.ServerVariables("SERVER_NAME")

    Cookies:

    lcCookie = Request.GetCookie("WebStoreCookie")

    There are many methods that retrieve common ServerVariables with simpler names such as GetBrowser(), GetIpAddress(), GetExtraHeader() etc.

    The Request class is always available in Web Connection Process classes as this.oRequest (where this is the Process class) or more simply just as a PRIVATE variable called Request.

    The request object provides Form variables and Server variables. Where do they come from and what do they look like? Well, Web Connection lets you spy at the raw request data in the Server Status Form by capturing request data for a request. See the previous link for details on how to review both request inputs and outputs.

    How it works

    The request object is originally created when wwServer::Init fires. Based on the current messaging mechanism wwServer creates an oRequest and calls its by calling the wwRequest::InitializeRequest method. This method retrieves all the request information as a string and assigns it to the oRequest object splitting into cFormVars and cServerVars.

    As the request runs through the WWWC framework a new wwProcess object (your subclass thereof) is created and the oRequest member is set. During the Process method which kicks off the request processing oRequest is assigned to a PRIVATE Request variable which is then available to all Process code - your user code included.

    All your code has to do in a Process method is to access the Request object and its methods. So for example to check for an authenticated user you'd just call Request.GetAuthenticatedUser(). It's just there and ready and waiting for you.



    *** Core Methods required for subclassing


    The following methods are the core Request methods which retrieve data from the request data. These methods must be implemented by any custom access mechanism that uses a different physical mechanism to retrieve Request data. For example, both the wwRequest and wwASPRequest classes implement these methods differently.

    wwRequest is set up as an implementation class that provides behavior operation for Web Connection's native message mechanism which is based on wc.dll passing down a string of URLEncoded data. This is not the best class design, however due to performance issues this non-modular choice was made during WC 3.0's design. This choice provided 30% faster operation for request related operations, so I think you'll find that this was a worthwhile trade off <s>...

    In order to add different behaviors for the request class a full subclassing step is required with a number of methods needing full reimplementation. In the source code all of the methods that need to be reimplemented are grouped together at the bottom of the class. They are:

    The list is in order of importance - if you're implementing a new mechanism you can probably skip everything down from the FormXML entry - the ones below are rarely used, and never in the framework itself.

    All other methods of the class rely on these methods to retrieve their values, so if you get these methods working the rest will also work assuming the form variable names match Web Connections. If they don't then you will have to override all methods (as is the case in most of the wwASPRequest methods.


    wwRequest::aFormvars


    This method retrieves all the form variables that were submitted into a two dimensional array.



    o.aFormVars(@laVars,lcPrefix)
    

    Return Value

    Numeric - count of variables retrieved.

    Parameters

    @laVars
    An array that will receive the form variables and values.

    The array contains the name of the field and the value:
    1 - The name/key of the variable
    2 - the actual value as a string

    lcPrefix
    Optional - A prefix for a varname that is to be retrieved. For example to retrieve only variables that start with "txt". Note the prefix is case sensitive, so make sure.

    Remarks

    Note that certain form variables may not show up in the result due to the way HTML forms work. In particular Checkboxes, radios and buttons tend to not show up unless they are 'selected'.

    You can check the request data on the server status form to see what you're actually getting.

    Example

    FUNCTION EchoFormVars
    
    DIMENSION laVars[1,2]
    lnVars = Request.aFormVars(@laVars) 
    
    Response.HTMLHeader("Echo FormVars")
    
    FOR x = 1 to lnVars
    	Response.Write(laVars[x,1] + ": " + laVars[x,2] + "<br>")
    ENDFOR
    
    Response.HTMLFooter()

    wwRequest::Form


    Retrieves an HTML form variable posted to the Web server. Form variables are the primary interface for a Web client to communicate with a Web server and POST variables are the most common. Everytime a user on an HTML page fills out a form and submits it the values are POSTed to the server and can be retrieved with Request.Form("fieldname").

    This method handles POSTs in the following formats which map to the nPostMode property which gets automatically set based on the Content-Type header. If the header is missing but there is POST data UrlEncoded is used (just in case).

    For XML mode posting a single XML Elements are looked for.

    If no parameter is passed to Form() the entire POST buffer is returned.

    o.form(lcVarname)
    

    Return Value

    String value of the variable or ""

    Parameters

    lcVarname
    The name of the form variable to retrieve.

    Remarks

    To get multiple form variables like from a multi-select listbox or multiple checkboxes you need to use the
    GetFormMultiple method.

    Requires wwIPStuff.dll for large variables to be decoded.

    wwrequest::GetRawFormData


    Returns all of the form data in raw form.

    Use this method to retrieve the POSTed data in its entirety which is a common scenario if you are dealing XML or binary data posted from a thick client.

    You can also use this feature to 'save' form data from a request for logging or potenially reassigning or 'playing' back form data in another request.

    If the data is XML you can set the wwRequest::lUseXMLFormVars property to treat the XML data as your form variables - wwRequest::Form() will retrieve values from the XML elements transparently.

    Assigning Post Buffer Data
    Please note that the raw form data is also accessible via Request.cFormVars, but this string contains a leading & that is stripped by this method. If you ever need to assign a complete form variable buffer you can assign it like this:

    Request.cFormvars = "&" + lcMyPostBuffer


    o.GetRawFormData()                                               
    

    Return Value

    Raw post buffer as a string

    wwRequest::InitializeRequest


    This method is responsible for setting up the Request object on each hit by passing in the POST data in some format and making it available to the specific Get methods. This method is fired on every Web request hit and deals with clearing out values from previous requests and then reassigning new values.

    It deals with setting up the form data, configuring the querystring and setting up. Once this method complete the Request object is fully operational and can be accessed as normal.

    This method is called internally only, but it's the vital piece that sets up the request properly for operation.

    If for whatever reason to choose to implement your own Request implementation make sure that this method gets called on every request that needs to be processed as this method essentially sets up the Request object to return appropriate values for its interface.



    o.initializerequest(lcPostData, lcTempPath)
    

    Return Value

    nothing

    Parameters

    lcPostData
    The post data as a string by default with Web Connection's ISAPI extension. With ASP messaging this parameter will contain the ASP Request object. This data is used to set up the Request object to use the standard interface described through this class.

    lcTempPath
    This is required only for file based operation, which needs to know where the HTML output needs to be written to.

    Remarks

    This method is always automatically called from wwServer::ProcessHit().

    This method needs to be implemented for every new Request class that works of data different than the Web Connection ISAPI extension. The wwASPRequest class for example overrides this method.

    The content of the lcPostData generally will be in this format: EncodedServerVariables + POST_BOUNDARY + EncodedFormVariables.

    wwrequest::IsFormVar


    Checks to see if a form variable exists in the request POST buffer. This method returns .T. if the key exists even if the value is blank. It will only return .F. if the key doesn't exist at all. If you need to check for blank you can simply read the key with
    wwRequest::Form().


    o.IsFormVar(lcKey)                                               
    

    Parameters

    lcKey

    wwRequest::Querystring


    This method returns the Query String and individual pieces of it. The method supports both numeric,positional parameters and named parameters. Positional parameters are great for grabbing request information:

    wc.dll?wwDemo~ClientForm~West+Wind+Technologies~ID0001

    where each parameter can be accessed using Request.QueryString(1) through QueryString(4). Web Connection typically uses the first two positional parameters to identify the request that is being accessed so wwDemo is the class to call and ClientForm is the method inside of that class for example. Parameters 3 and 4 are application specific parameters.

    URLEncoded parameters are better for optional parameters and look like this:

    wc.dll?UserName=Rick+Strahl&UserId=0111&Address=400+Morton%0A%0Dhood+River,+OR

    Essentially spaces are converted to + signs, keys are separated by & and any control characters are converted to hex representations preceeded by a % sign.

    You can mix positional and URLEncoded parameters by adding a separating ~ between the posititional parms and the named ones:

    wc.dll?wwdemo~URLTest~&Username=Rick+Strahl&Company=West+Wind

    which allows you to use both GetCGIParameter(2), which returns URLTest or QueryString("Company") which returns West Wind.



    o.QueryString(lvKey)
    

    Return Value

    String value of the requested key.

    Parameters

    lvKey
    This parameter can either be a string or numeric value.


    Make sure when mixing positional and named parameter that you separate the two with a ~ and & as the link above does.

    wwrequest::Params


    Returns a value by checking FormVars, QueryString, Session and ViewState (in that order) for a matching key and returning the value.

    This function is useful if you store values in the QueryString or in Form Variables and you don't want to explicitly check the value in each of the available collections.

    Note though that this function is considerably slower than accessing the collections directly since all the collections are probed. The overhead occurs especially if keys don't exist in any of the collections.


    o.Params(lcKey)                                                  
    

    Parameters

    lcKey
    The key of the item to return.

    wwRequest::ServerVariables


    Retrieves a server variable from the request data. Server variables provide information about the current request including info about the client application (browser, IP Address), the current request (server name, querystring), authentication (username, port accessing this app) and status information (Cookies, type of request) etc.

    Most of the server variables that are returned are abstracted in method of this class, such as GetBrowser(), GetIPAddress(), GetAuthenticatedUsername() and so on.

    To see a list of available raw server variables you can capture the current request output by bringing up the Web Connection Status form and saving request data for review.

    The following shows typical contents of the ServerVariables available:

    DLLVersion=Web Connection 3.32 (32 servers)
    wcConfig=d:\westwind\wconnect\wc.INI
    REQUEST_METHOD=GET
    PATH_INFO=/wconnect/slowhit.wcs
    PATH_TRANSLATED=d:\westwind\wconnect\slowhit.wcs
    SCRIPT_NAME=/wconnect/slowhit.wcs
    PHYSICAL_PATH=d:\westwind\wconnect\slowhit.wcs
    SERVER_PROTOCOL=HTTP/1.1
    SERVER_SOFTWARE=Microsoft-IIS/4.0
    SERVER_NAME=localhost
    SERVER_PORT=80
    REMOTE_HOST=127.0.0.1
    REMOTE_ADDR=127.0.0.1
    AUTH_TYPE=Basic
    REMOTE_USER=rstrahl
    HTTP_AUTHORIZATION=Basic cnN0cmFobDpkc2ZhZG1z
    LOGON_USER=rstrahl
    HTTP_USER_AGENT=Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)
    HTTP_COOKIE=WWTHREADID=880U8OGY; ASPSESSIONIDGGGQQGCB=JNEHCPDAMLKOCNMEPLPNIBEH
    GMT_OFFSET=-36000
    ALL_HTTP=HTTP_ACCEPT:*/*HTTP_ACCEPT_LANGUAGE:en-usHTTP_CONNECTION:Keep-AliveHTTP_HOST:localhostHTTP_USER_AGENT:Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)HTTP_COOKIE:WWTHREADID=880U8OGY; ASPSESSIONIDGGGQQGCB=JNEHCPDAMLKOCNMEPLPNIBEHHTTP_AUTHORIZATION:Basic cnN0cmFobDpkc2ZhZG1zHTTP_ACCEPT_ENCODING:gzip, deflate


    Note: PHYSICAL_PATH, DLLVersion and wcConfig are specific to Web Connection.

    Note that Web Connection pulls specific keys out of the HTTP request buffer so not all HTTP variables are necessarily available. Web Connection pulls the most common ones that you are most likely to require for your applications. However, later versions of IIS or special extensions running on the Web Server might add additional HTTP variables to the Web Server. You can access these by telling wc.dll to specifically pull these values for each request by using the wc.ini [Extra Server Variables] section. For more info see the wc.ini topic.

    o.servervariables(lcKey)
    

    Return Value

    String value of the server variable or "" if it doesn't exist or is empty.

    Parameters

    lcKey
    Name of the server variable to return

    wwRequest::SetKey


    Sets a form or server variable to the specified value from the existing POST data block.

    This method is useful for operations where the program needs to make changes to existing POST variable provided by the client application. An example might be stripping out a password the user passed before redisplaying a DHTML form that contains this data. After the display operation is complete the original value can be reset.



    o.setkey(lcKey, lcValue, lvReserved)
    

    Parameters

    lcKey
    The form variable to set.

    lcValue
    The value to set the variable to

    lvReserved
    not used provided purely for compatibility

    Remarks

    This functionality may not be provided for non-Web Connection messaging types such as ASP messaging, since there's no way to change the POST buffer data in those tools.

    *** General purpose retrieval methods


    The following methods are general purpose methods that simply use the ServerVariables() method to retrieve values with more logical names than the CGI standard names. This also allows abstraction to interfaces where differences exist such as the ASP mechanism and the Web Connection request formatting.

    wwRequest::FormVarsToObject


    Parses an object and pulls form variables from the request buffer by matching the property names to the request variables.

    Optionally you can pass in a prefix for form variables to allow for potential naming problems for properties of multiple objects.

    Ideally you pass in existing object and Web Connection will populate the properties of the object from the form variables.

    If you don't pass an object a generic object is created for you with properties for each of the form variables of the HTML form and populated with String values (requires VFP8 or later).

    This method can be extremely useful for reducing form variable retrieval code from your Web applications especially if you map form fields directly to objects. Form variables must match the property names either directly or with an optional prefix (such as txt, but the prefix must consistent for all variables).


    o.FormVarsToObject(loObject,lcPrefix)                            
    

    Return Value

    nothing. Object passed in as parameter is filled with values.

    Parameters

    loObject
    An existing object to be imported to. Code walks the object structure (1 dimensionally) by looking for form vars.

    lcPrefix
    An optional prefix that can be used in form variables. This prefix is stripped when matching property values.


    Remarks

    Logical values are handled from checkboxes, but non-logical checkbox values will not be captured when not checked. Selection values for checkboxes must be On or True in order to be recognized as a True value for a logical. Multiselect lists or multiple selection radios will pull only the first selection into the target property.

    Object and array members are ignored and not filled or updated. Date values are imported with CTOD and CTOT. There may be formatting problems so these properties may require post processing and potential problems with SET STRICTDATE settings.

    You can work around any misparsing problems by performing additional post processing on the captured object data or handling any fields that might have been missed manually.

    This method may cause problems when capturing logical values into object proeprties that are not part of the form displayed. This comes from the HTML limitation of checkboxes and radios not returning an unchecked value. The method assumes that any logical value not found in the request data is false. This can cause any properties not on the form to be forced to a value of .F. even though the original value was .T. The workaround is to capture any non-participating logical value to tempoary vars and then restore them after the method call is completed.


    Example

    *** Using a cursor
    USE wws_Customer
    SCATTER NAME loCustomer MEMO BLANK
    
    Request.FormVarsToObject(loCustomer)
    
    
    *** Using a 'real' business object loCust = CREATE("cCustomer") loCust.New() *** Store form vars to oData properties Request.FormVarsToObject(loCust.oData) IF !loCust.Validate() THIS.Errormsg("Invalid Data") RETURN ENDIF loCust.Save()
    *** Without an object passed in loFormVars = Request.FormVarsToObject() *** Retrieve the txtName and txtCompany variables lcName = loFormVars.txtName lcCompany = loFormVars.txtCompany

    wwrequest::GetApplicationPath


    Returns the physical OS path of the virtual directory of this Web Server application. In short it maps the virtual directory defined in IIS to a physical path. Can be used as a 'base url' for Web applications to base relative URls on.

    This is an IIS5 and later feature only and it returns the value of the APPL_PHYSICAL_PATH server variable if available (only on IIS). If the variable is not available the Processes configuration cHtmlPagePath value is used for the base path.

    o.GetApplicationPath()                                           
    

    wwRequest::GetAuthenticatedUser


    Returns the name of a user if he has been authenticated by the Web server. This variable gets set and stays set once a user has entered a valid username into the browser dialog box when prompted. The username is valid for a given Web server path and down and once set cannot be unset until the browser is shut down or another authentication request is made for the same user.

    This method can check both Basic and Windows Auth values either seperately or combined.

    o.getauthenticateduser(lnMode)
    

    Return Value

    String of the logged in user name, or blank if not logged in.

    Parameters

    lnMode
    Optional - Determines what type of username to check for.

    0 - Basic Auth then Windows Auth
    1 - Basic Auth
    2 - Windows Auth

    Remarks

    In order for an authentication request to succeed on the server Basic Authentication must be enabled on the server.

    Note: Basic Authentication is a non-secure protocol that sits on top of HTTP. Passwords are passed as clear text (although encoded with a simple, easily breakable hash algorithm) and can be easily hijacked with a network sniffer. If you're worried about security make sure that your authentication request runs over SSL/HTTPS - when you do the entire request info is encrypted.

    Also, check out the wwProcess::Login method which abstracts the login and authentication process into a single easy to use method.

    Example

    lcUserName=Request.GetAuthenticatedUser()
       
    *** Did the user Authenticate
    IF EMPTY(lcUserName)
       *** Send Password Dialog - on success this request will be rerun
       *** AFter 3 failures an error message will be displayed.
       THIS.oResponse.Authenticate(Request.GetServername())
       RETURN .T.
    ENDIF   
    

    wwRequest::GetBrowser


    Returns the client's browser display name.

    The names tend to be verbose and do not lend themselves well for parsing and consistent values. For example IE 5 returns:

    Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)

    In order to do some useful browser detection you probably have to do a little more work. For example to check for IE you typically check for the MSIE string or the IE version MSIE 5.

    o.getbrowser()
    

    Return Value

    String of the user agent/browser name

    wwRequest::GetCookie


    Returns an HTTP Cookie that was previously set. HTTP cookies allow keeping state by keeping a persistent variable on the user's browser. Cookies are sent along in each HTTP request and appear as a server variable in the incoming request data.

    To set a cookie you need to use the wwHTTPHeader::AddCookie method to assign a cookie as part of the HTTP header for a returned request.

    Also see the How To section for Implementing HTTP Cookies.

    o.getcookie(lcCookie)
    

    Return Value

    String of the cookies contents.

    Parameters

    lcCookie
    The name of the cookie to retrieve


    Remarks

    Cookies are scoped to a particular virtual directory, so a cookie set in a different virtual than the one the current request is running in will not be visible.

    Although cookies follow the directory hierarchy down, a Cookie set in the root directory will also not be in scope in a virtual below the root, but will be in scope in a dircectory below the root that is not a virutal.

    Example

    *** Try to retrieve the cookie...
    lcId=Request.GetCookie("WWUSERID")
    
    *** Create Standard Header
    loHeader=CREATEOBJECT("wwHTTPHeader")
    loHeader.DefaultHeader()
    
    *** If not Found
    IF EMPTY(lcId)
       *** Create the cookie
       lcId=SYS(3)
       
       loHeader.AddCookie("WWUSERID",lcId,"/wconnect")
       
       *** To specify a permanent cookie supply NEVER or a specific expiration date
       *** loHeader.AddCookie("WWUSERID",lcId,"/","NEVER")
    ENDIF            
    
    *** Send Header and make sure to pass the Content Type (loHeader)
    Response.ContentTypeHeader(oHeader)
    
    *** more html
    Response.Write("<HTML>Hidi ho</HTML>")
    
    
    *** OR: use one of the following methods which take a header parameter: Response.HTMLHeader("Hidi ho",,,oHeader) Response.ExpandTemplate(THIS.cHTMLPagePath + "nocode.wc",oHeader) Response.ExpandScript(THIS.cHTMLPagePath + "nocode.wcs",oHeader)

    wwRequest::GetCurrenturl


    Returns the current URL.

    This method tries to reconstruct the current URL by looking all the component pieces available in the request information. Most of the time this is correct, but in some instances when scripmapped requests are involved this value may be incorrect.

    o.getcurrenturl(llHTTPS)
    

    Return Value

    String of the URL

    Parameters

    llHTTPS
    Optional - prefixes the request with HTTPS instead of HTTP. Note that by default HTTP:// is used regardless of whether the request runs HTTPS or not, since the Web server does not provide this information.

    wwrequest::GetExecutablePath



    Returns the logical, Web relative path to the DLL or script.

    http://www.west-wind.com/wconect/noceode.wc

    returns:

    /wconnect/nocde.wc

    The path is always in Web format with forward slashes and is relative to the Web server's root directory.


    o.getexecutablepath()
    

    Return Value

    string of the executable path

    Remarks

    Same as calling Request.ServerVariables("SCRIPT_PATH")

    wwRequest::GetExtraHeader


    Returns an Extra Header variable from the ALL_HTTP block.

    Extra Headers are custom headers posted by client applications to send special processing instructions to the server. For example, SOAP includes a SOAPMethodName extra header to specify the URI and name of the method to call.


    o.GetExtraHeader(lcHeader)                                       
    

    Return Value

    string of header or "" if not found

    Parameters

    lcHeader
    Name of the header to retrieve

    wwRequest::GetFormMultiple


    This method retrieves multiselect HTML form variables from the CGI content file into an array. Multiselect variables can be returned when using scrolling HTML lists with the SELECT MULTIPE option or multiple radio buttons and checkboxes using the same variable name.

    Multi-select values can come from Multi-select lists and drop downs or from radio buttons.

    o.getformmultiple(@taVars,tcVarname)
    

    Return Value

    Numeric - count variables retrieved into the array. The array will be filled.

    Parameters

    @taVars
    An array that will receive the form variables. MUST BE PASSED BY REFERENCE

    tcVarname
    The name of the form variable to retrieve.

    Example

    DIMENSION laVars[1]
    lnVars=Request.GetFormMultiple(@laVars,"LastName")
    
    FOR x=1 to lnVars
       Response.Write( laVars[1] + "<BR>")
    ENDFOR

    wwRequest::GetIpAddress


    Returns the client's IP Address.

    o.getipaddress()
    

    Return Value

    IP Address as a string.

    wwRequest::GetLogicalpath


    Returns the web server relative path of the current request. For example:

    http://www.west-wind.com/wconect/wc.dll?wwDemo~TestPage

    returns:

    /wconnect/wc.dll

    The path is always in Web format with forward slashes and is relative to the Web server's root directory.

    o.getlogicalpath()
    

    Return Value

    string

    wwRequest::GetLocale


    Returns the currently active language in the browser if available.

    The value is returned from the ACCEPT_LANGUAGES header and this method returns the first value in this list. The format of this value is in standard Locale-SubLocale format. Here are some examples:

    en-us
    en-gb
    de-de
    de-at
    de-ch



    o.GetLocale(@laLanguages)                                                  
    

    Return Value

    Language ID. If no language can be found en-us is returned.

    Parameters

    @laLanguages
    An optional array passed in by reference that receives all the available languages.

    Remarks

    This method return en-us if no language definition could be found. The optional array is filled with this value as well.

    wwRequest::GetMultipartFile


    Retrieves a file uploaded with HTTP file upload into a binary string.

    HTTP file uploads and multipart forms are sent up to server with multipart forms which are submitted in an HTML page as follows:


    <form ACTION="wc.dll?wwDemo~FileUpload" METHOD="POST" enctype="multipart/form-data">
         <font face="Verdana"><input TYPE="FILE" NAME="File">
          <br>
          <strong>File Description:</strong><br>
          <textarea rows="4" name="txtFileNotes" cols="43"></textarea><br>
          <input TYPE="submit" value="Upload File" name="btnSubmit"> </font></font></p>
    </form>



    o.GetMultipartFile(lcKey, lcFileName)                            
    

    Return Value

    String that contains binary image of the file. Use STRTOFILE() to save the file to disk.

    Parameters

    lcKey
    The name of the file to retrieve

    @lcFileName
    Optional - a string that will be filled with the file's name. must be passed by reference. Initial value is ignored.


    Example

    lcFileBuffer = Request.GetMultiPartFile("File",@lcFileName)
    lcNotes = Request.GetMultipartFormVar("txtFileNotes")
    lcFileName = SYS(2023)+"\"+lcFileName
    
    IF LEN(lcFileBuffer) > 5000000
       THIS.StandardPage("File upload refused",;
                         "Files over 500k are not allowed for this sample...<BR>"+;
                         "File: " + lcFilename)
       RETURN
    ENDIF
       
    *** Now dump the file to disk
    STRTOFILE(lcFilebuffer,lcFileName)
    
    THIS.StandardPage("Thank you for your file upload",;
                      "<b>File Uploaded:</b> " + lcFileName + ;
                      " (" + TRANSFORM(FileSize(lcFileName),"9,999,999") + " bytes)<p>"+ ;
                      "<b>Notes:</b><br>"+ CRLF + lcNotes )
    
    

    wwRequest::GetMultipartFormVar


    Retrieves a multipart form variable from the request buffer. Multipart form variables are submitted on the client side by specifiying an encoding type of "multipart/form-data".

    Multipart forms are inherently more efficient especially for large form submissions since the data is not encoded. Multi-part forms are also used for HTTP file uploads.

    To create a multipart form add the following enctype attribute to your form:

    <form ACTION="wc.dll?wwDemo~FileUpload" METHOD="POST" enctype="multipart/form-data">
         <font face="Verdana"><input TYPE="FILE" NAME="File">
          <br>
          <strong>File Description:</strong><br>
          <textarea rows="4" name="txtFileNotes" cols="43"></textarea><br>
          <input TYPE="submit" value="Upload File" name="btnSubmit"> </font></font></p>
    </form>



    o.GetMultipartFormVar(lcKey)                                     
    

    Return Value

    String of the value retrieved. A null string if the var doesn't exist.

    Parameters

    lcKey
    The multipart form variable to retrieve

    Example

    lcFileBuffer = Request.GetMultiPartFile("File",@lcFileName)
    lcNotes = Request.GetMultipartFormVar("txtFileNotes")
    lcFileName = SYS(2023)+"\"+lcFileName
    
    IF LEN(lcFileBuffer) > 5000000
       THIS.StandardPage("File upload refused",;
                         "Files over 500k are not allowed for this sample...<BR>"+;
                         "File: " + lcFilename)
       RETURN
    ENDIF
       
    *** Now dump the file to disk
    STRTOFILE(lcFilebuffer,lcFileName)
    
    THIS.StandardPage("Thank you for your file upload",;
                      "<b>File Uploaded:</b> " + lcFileName + ;
                      " (" + TRANSFORM(FileSize(lcFileName),"9,999,999") + " bytes)<p>"+ ;
                      "<b>Notes:</b><br>"+ CRLF + lcNotes )
    
    

    wwRequest::GetRequestId


    Returns the unique Request ID for the currently executing request.

    This ID is generated in the ISAPI extension and passed to your code via the REQUESTID server variable. This ID is logged in the Web Connection Log. It also gets logged in the ISAPI extension when an error occurs there, or when ISAPI Logging is enabled.

    o.GetRequestId()                                                 
    

    wwRequest::GetPhysicalPath


    Returns the physical path to a script mapped page or an executable DLL file. The physical path is a great tool for capturing the system specific path of script mapped pages, so you can capture the location of the page for further parsing.

    For example, Web Connection uses the Physical Path to capture scripts and uses the physical path to read in the original page, then parses it using the wwVFPScript script parser. Regardless of where the page was called from the physical path always returns the correct location for the file.

    For example:

    http://localhost/wconnect/scriptdemo.wcs

    returns:

    c:\inetpub\wwwroot\wconnect\scriptdemo.wcs



    o.getphysicalpath()
    

    Remarks

    If you're using any remote servers you have to be careful about the physical path. The physical path will always be returned with a Web server specific path. If you're running remotely via a DCOM object or remote filebased server, C:\inetpub\wwwroot\myapp is not a valid path to pull the script page from, so you need to fix up the path.

    You can do this by usign assiging a drive override which can use VFP's FORCEDRIVE() or a full path override by using FULLPATH to adjust the path for the full location.

    Physical path is a parsed value that guarantees to hold either the ISAPI extension's name or the name of the script that was executed. ISAPI does not provide this functionality natively and splits this between PATH_INFO and PATH_TRANSLATED and SCRIPT_NAME. Use GetPhysicalPath() to retrieve this value safely, but be aware that this variable does not exist in ASP or ISAPI directly.

    wwRequest::GetPreviousurl


    Returns the URL of the page that this request was called from. If this page was typed into the browser window manually or accessed from a non-browser client this value will be blank.


    o.getpreviousurl()
    

    wwRequest::GetRelativeSecureLink


    This method allows you to easily create a secure link simply by specifying a relative link like any other link.

    The problem is that switching between HTTP and HTTPS requires a fully qualified URL including protocol, domain name and full Web path, while most sites entirely link with relative links where the protocol and domain name are never used explicitly. It's often difficult to derive the full URL for a secure link especially if the site must be portable and the link cannot be hardcoded with a domain name required to do this.

    This method figures out the full current URL converts it to a fully quallified secure link and strips of the document name, then add your link or path at the end. So now to switch you can simply do:

    <a href="<%= Request.GetRelativeSecureLink([Somepage.wc]) %>">Secure Page</a>



    o.GetRelativeSecureLink(lcLink,llNonSecure)                      
    

    Return Value

    Fully qualified HTTP link based on the currently active URL

    Parameters

    lcLink
    A web server relative link. This path can include relative path information, but should never include a protocol and domainname. If you this method is not useful.

    llNonSecure
    Optionally you can specify to have the result return a plain HTTP protocol link. This is useful when switching out of secure mode back into the regular site. For example, after you're done after an order on secure site, you probably have a button for 'Shop some more' which returns to the main site in non-secure mode.


    Remarks

    This method will not work correctly if the current URL does not include a document name, such as home pages. To get around this always reference pages that use this method with an explicit page such as Default.wc.

    wwRequest::GetServerName


    Returns the server's domain or IP address. Note that this value returns only the server portion of the current URL.



    o.getservername()
    

    wwRequest::GetServerSoftware


    Returns the software that's running the Web server.

    o.getserversoftware()
    

    wwRequest::GetWCIniValue


    Retrieves a value from the Web Connection ISAPI/CGI configuration file. Most useful for retrieving the AdminUser value.

    o.getwcinivalue(lcKey, lcSection)
    

    Return Value

    String of the value retrieved or "" (null string) if the key doesn't exist.

    Parameters

    lcKey
    The key to retrieve

    lcSection
    Optional - the section to retrieve the value from. The default is the [Main] section.

    Remarks

    This method will not work with ASP messaging, since ASP doesn't use wc.ini or knows even where the file resides.

    wwRequest::IsLinkSecure


    Checks to see if the user is coming in over the SSL port.

    o.IsLinkSecure(lcSecurePort)
    

    Parameters

    lcSecurePort
    Optional - number of the port that is the dedicated SSL port. Defaults to the standard SLL port 448.

    Remarks

    This method looks at Request.ServerVariables("SERVER_PORT") to get the server port.

    wwRequest::IsPostBack


    Determines whether the current request is running in POST mode.

    This method is a shortcut for:

    Request.ServerVariables("REQUEST_METHOD") = "POST"


    o.IsPostBack(lcVar)                                                   
    

    Return Value

    .T. or .F.

    Parameters

    lcVar
    Optional - looks for a specific variable in the POST buffer.

    wwRequest::GetClientCertificate


    Returns the contents of a client Certificate's Subject key. This information contains the info about the client. The following information is provided:

    ClientCert Flags=1
    ClientCert Cookie=b0c24e793b9f11367c8ec916101b931e
    ClientCert Subject=
    L=Internet, O="VeriSign, Inc.", OU=VeriSign Class 1 CA - Individual Subscriber, OU="www.verisign.com/repository/CPS Incorp. by Ref.,LIAB.LTD(c)96",
    OU=Digital ID Class 1 - Microsoft Full Service,
    CN=Rick Strahl, E=rstrahl@west-wind.com

    You can retrieve the Flags and Cookie with the ServerVariables() method. This method only retrieves information in the Subject key.

    o.getclientcertificate(lcSubKey)
    

    Parameters

    lcSubKey
    Retrieves the key from the subject line. A couple of special keys have been provided:EMIAL, EMAIL retrieve those values. All other keys including the CN and E keys can be retrieved by specifying the keyname.


    Example

    Request.GetClientCertificate("CN")   && Retrieve Common Name

    wwrequest::lUtf8Encoding


    If .T. causes Request.Form() and Request.QueryString() to UTF-8 decode the returned values.

    Utf-8 encoding is off by default for pages, but if you switch the browser into UTF-8 mode or use AJAX and JavaScript Escape/EncodeUriComponent functions the values are automatically UTF-8 encoded. This option allows you to override the behavior and retrieve values in this format properly adjusted for the current character set.

    If you set the property you should do it early on in the request cycle. In Web Control Pages or controls you should set this switch in OnInit() so it occurs before the automatic Request -> Control mapping occurs. For example, the wwWebAjax control does this as JavaScript escape() functionality always encodes strings as UTF-8 in addition to Url encoding.

    o.lUtf8Encoding
    

    wwrequest::nPostMode


    Request property that gets set by InitializeRequest and determines what kind of Form submission is occurring.

    0 - Not a POST operation
    1 - UrlEncoded POST (standard HTML forms)
    2 - Multipart Forms
    3 - XML mode

    This property can be read after InitializeRequest is called.

    o.nPostMode                                                      
    

    wwRequest::cFormVars


    This property holds the raw POST buffer that the HTTP client submitted.

    The format of this buffer depends on the mechanism the client used to send the data to the server. Several modes are common (in order of occurrance):

    1. Standard HTTP POST buffer containing UrlEncoded key value pairs
    2. Multipart HTTP POST data which contains multiple raw data sections
    3. Posted XML from a client such as Microsoft's XMLHTTP which ships with the XMLDOM parser
    4. Non-standard raw data. If client and server both are under your control you can simply send raw data over the wire. The Web server simply passes the data forward. To do so, use wwIPStuff::AddPostkey("",lcData)

    If you want to retrieve raw data such as a raw XML post, you can do so using the wwRequest::FormXML method.




    o.cFormVars                                                      
    

    wwRequest::cPathOverride


    Actual location of the Temporary path. This path is used to override any 'physical' paths to point to the network path instead.

    wwRequest::cServerVars


    This property holds the raw Server variables returned from the Web Connection DLL. This data is URLEncoded and was created by Web Connection on the fly. Every call to ServerVariables accesses this data directly to extract the appropriate server variable value.


    GetConstants [TypeLibFile],[OutputFile],[Silent(.t. or .f.)]                                                  
    

    Default Value

    All parameters are optional. If no parms are passed you'll be prompted
    for an input type library file. The generated header file will be
    generically generated into your temp directory as HEADER.H and viewed
    in an editor window. From there you can cut or paste.

    If you pass parameters you can override the interactive behavior above.

    wwRequest::lUseXMLFormvars


    If .T. Request.Form retrieves variables from an XML document contained in the form buffer rather than regular post variables.

    This powerful feature allows you to transparently accept input from XML clients rather than standard HTML based Web pages. You can simply check for XML inputs as follows:

    lcXML = Request.FormXML()
    IF lcXML = "<?"
       Request.lXMLFormVars = .T.
       llXML = .T.
    ELSE
       llXML = .F.
    ENDIF
    
    *** Now continue reading form vars
    lcName = Request.Form("Name")
    

    This simple bit of logic allows you to simultaneously serve HTML and XML clients with an identical code base (although your output will probably have to XML formatted as well.

    wwRequest::oApi


    Internal wwAPI object. The object is not protected to allow persistent access to an API object through the Request.oAPI member.

    wwRequest::oXML


    Internal reference to a wwXML object. This object is not protected and can be treated as a 'global' reference to a wwXML object accessible through wwRequest.oXML.

    Class wwASPRequest



    This class is a subclass of the wwRequest object and implements request functionality for ASP requests.

    The interface to the class is identical to wwRequest with the following exceptions:

    (under construction - only basic operation works)

    Class Members

    Requirements


    Class wwPageResponse


    The wwPageResponse class provides the output object that Web Connection code writes output into. This class provides raw output via Response.Write() as well as header management. This class operates in memory and all user code interacts with this class in some way to generate its HTTP output for the client.

    The wwPageResponse object is new in Web Connection 5.0 and replaces the wwResponse set of classes that the user interacts with. This new class encapsulates both response output as well as HTTP headers in a fully cached manner so headers can be added to requests at any point in time.

    The class's Render() method is used to retrieve the entire HTTP output including headers for this request. wwPageResponse is not at this time the default Response object so you have to explicitly specify it in any wwProcess subclasses with:

    cResponseClass = "wwPageRepsonse"

    Backwards Compatibility
    For legacy purposes and compatibility with Web Connection 4.x and older there's a wwPageResponse40 class that implements all of the dropped wwResponse methods. You can reference this class easily in your wwProcess class.
    cResponseClass = "wwPageRepsonse40"
    This class should allow existing applictions to use the new Page class functionality without any major changes.



    Class Members

    MemberDescription
    AddCacheHeader Adds an HTTP header that provides for Maximum Caching Capabilities
    o.AddCacheHeader(lnExpirationSeconds)
    AddCookie Adds a cookie to the current Response.
    o.AddCookie(tcCookie,tcValue,tcPath,tcExpire)
    AddForceReload Adds maximum Cache Expiration headers to the current HTTP request. Essentially this forces the request to always be reloaded rather than cached.
    o.AddForceReload()
    AppendHeader Adds an HTTP Header to the current page.
    o.AppendHeader(lcHeaderKey,lcHeaderValue)
    BasicAuthentication This method creates a self contained HTTP request that asks the browser to
    present the login dialog into which the user must type the username and
    password. The password is then validated by the Web server typically
    against the NT (or Windows) user database. The exact validation varies by
    Web server - IIS uses the NT User database on the server.
    o.BasicAuthentication(tcRealm, tcErrorText)
    BinaryWrite Same as Write() but provided for ASP.NET compatibility.
    o.BinaryWrite(lcText)
    Clear Clears the content of the current Response output.
    o.Clear()
    DownloadFile Downloads a file directly from disk to the client using a FileOpen dialog.
    o.DownloadFile(lcFileName, lcContentType, lcFileDisplayName)
    End Ends the current response. Any further output send to the Response object is ignored.
    o.End()
    ExpandPage Executes a Web Control Framework page by providing a physical path to the page allowing for parsing, compiling and running the page.
    o.ExpandPage(lcPhysicalPath, lnPageParseMode)
    ExpandScript The ExpandScript method method is used to expand script pages that contain FoxPro expressions and Code blocks using Active Server like syntax. The method generates a fully self contained HTML page/HTTP output which includes an HTTP header.
    o.ExpandScript(tcPageName, tnMode, tvContentType, tlTemplateStr, llNoOutput)
    ExpandTemplate The ExpandTemplate method is used to expand template pages that contain FoxPro expressions using Active Server like syntax. The method generates a fully self contained HTML page/HTTP output which includes an HTTP header.
    o.ExpandTemplate(tcPageName, tcContentType, tlTemplateString, tlNoOutput)
    FastWrite Direct write to the output stream. This method directly writes into the response stream and bypasses any checks and flags. So it doesn't check for the ResponseEnded flag for example.
    o.FastWrite(lcText)
    HTMLFooter Creates a footer for a page. Creates </BODY></HTML> tags preceeded by
    option HTML passed as parm. Optionally pass text to display just prior to
    tags
    o.HTMLFooter(tcText,tlNoOutPut)
    Redirect HTTP Redirection allows you redirect the current request to another URL. A common scenario is for login operations where the user is trying to access functionality before he's logged in. When the request comes in you can redirect the user to the Login page first.
    o.Redirect(lcUrl)
    Render This method is called to return the output of the Response object. It combines the headers and the body together and returns the entire response.
    o.Render()
    TransmitFile This method allows you send large files to the client without having to use Response.Write() to load these files into a string in Visual FoxPro.
    o.TransmitFile(lcFileName, lcContentType)
    Write Raw HTTP output function. Writes directly to the Response stream/string.
    o.Write(lcText)
    ContentType Sets the content type for the current HTTP response. This value defaults to text/html if not set.
    Cookies Cookies collection that contains multiple cookies that are set for the current request. The cookies are written at the end when the headers are created and written.
    Encoding Sets the content encoding for the page for text content.
    Expires The number of minutes before the page is expired.
    GZipCompression If set to .T. automatically encodes the Response output to GZip compressed format. When set Web Connection automatically checks whether the client supports GZip compression and only compresses content if it does.
    Headers A collection of individual HTTP headers that are to be added to the request header. The Headers of the Response can be set at any time during the request as they are cached and written at the end of the request.
    RawHeaders This is the low level string that is used to hold any RAW headers you want to add to a request. This is an override to Headers.Add() to allow for any headers or content that doesn't follow the name/value syntax of headers.
    ResponseEnded Set to turn off output that follows called by the Write method. This flag is set by Response.End() so you usually should not have to set this explicitly. However, you can use it to check whether the Response object is active.
    Status Allows you to specify the Response Status Code. The status code is the status number and message that follows the HTTP protocol.

    Requirements

    Assembly: wwPageResponse.prg

    wwPageResponse :: BasicAuthentication


    This method creates a self contained HTTP request that asks the browser to
    present the login dialog into which the user must type the username and
    password. The password is then validated by the Web server typically
    against the NT (or Windows) user database. The exact validation varies by
    Web server - IIS uses the NT User database on the server.

    This mechanism works through Basic Authentication which is part of the HTTP
    protocol.


    o.BasicAuthentication(tcRealm, tcErrorText)
    

    Return Value

    nothing

    Parameters

    lcRealm
    Optional - The realm as its known in HTTP terms is the domain name or IP
    address of the server.

    lcErrorText
    The HTML text to display if an error occurs with login validation. IOW, if
    the user types in the wrong password this is what they'll see. This text is
    static.

    wwPageResponse::AddCacheHeader


    Adds an HTTP header that provides for Maximum Caching Capabilities

    o.AddCacheHeader(lnExpirationSeconds)
    

    Parameters

    lnExpirationSeconds

    wwPageResponse::AddCookie


    Adds a cookie to the current Response.

    o.AddCookie(tcCookie,tcValue,tcPath,tcExpire)
    

    Parameters

    tcCookie
    The name of the Cookie to set.

    tcValue
    The string value of the cookie

    tcPath
    Optional - The path to set it on. Default: /

    tcExpire
    Optional - When it expires. Specify can specify the Expiration either as:

    wwPageResponse::AddForceReload


    Adds maximum Cache Expiration headers to the current HTTP request. Essentially this forces the request to always be reloaded rather than cached.

    o.AddForceReload()
    

    wwPageResponse::AppendHeader


    Adds an HTTP Header to the current page.

    Essentially the same as Response.Headers.Add(), but this method is consistent with ASP.NET and shows up in Intellisense.

    o.AppendHeader(lcHeaderKey,lcHeaderValue)                   
    

    Parameters

    lcHeaderKey
    The header to add

    lcHeaderValue
    The value for the header

    Remarks

    More info on HTTP Headers

    Example

    *** Refresh the page in 2 seconds Response.AppendHeader("Refresh","2; url=/wconnect/weblog/default.blog") *** Add a custom header value Response.AppendHeader("Custom","MyCustomValue")

    wwPageResponse::BinaryWrite


    Same as Write() but provided for ASP.NET compatibility.

    o.BinaryWrite(lcText)
    

    Parameters

    lcText
    Any FoxPro string. This string can be text or binary data.

    wwPageResponse::Clear


    Clears the content of the current Response output.

    o.Clear()
    

    wwPageResponse::DownloadFile


    Downloads a file directly from disk to the client using a FileOpen dialog.

    This method is similar in behavior to TransmitFile but it adds additional headers to coerce the client to treat the file as a separate file so it isn't open in the browser as document but as a Save As dialog.

    o.DownloadFile(lcFileName, lcContentType, lcFileDisplayName)
    

    Parameters

    lcFileName
    The physical path to the filename to return to the client.

    Note: If you're using the Web Connection .NET Module the filename specified must be inside of the Web directory tree in order to be downloadable. This means any files from other locations first need to be moved or generated into a Web relative path. With the ISAPI handler the file can live anywhere.

    lcContentType
    Content type (text/html, application/msword etc.)

    lcFileDisplayName
    The name of the file that is displayed in the download dialog

    wwPageResponse::End


    Ends the current response. Any further output send to the Response object is ignored.


    o.End()
    

    wwPageResponse::ExpandPage


    Executes a Web Control Framework page by providing a physical path to the page allowing for parsing, compiling and running the page.

    This method can be used to call any Web Control Framework Page from a standard wwProcess method. This allows control in scenarios where a single set of pages serve multiple applications for example.

    *** Process Method FUNCTION DoHelloworld Response.ExpandPage("c:\sites\MyApp\Helloworld.wwf") ENDFUNC

    Note that you have to specify the physical path to the page and the page must exist. Web Connection parses out the path to the PRG file from the script page.

    Please note that as an alternative to ExpandPage() you can also call any already parsed PRG Page classes directly which has the same result:

    *** Process Method FUNCTION DoHelloworld DO HelloWorld_page.prg ENDFUNC

    This approach is more efficient in terms of execution, but it requires that the page is already pre-parsed and so will not detect changes to the markup unless manually compiled.


    o.ExpandPage(lcPhysicalPath, lnPageParseMode)                   
    

    Parameters

    lcPhysicalPath
    The physical path to the page. If not provided Request.GetPhysicalPath() is used.

    lnPageParseMode
    The mode in which the page is parsed.

    1 - Parse & Run
    2 - Parse & Compile & Run
    3 - Run only

    If not provided defaults to Server.nPageParseMode (also settable via the Status form).

    wwPageResponse::ExpandScript


    The ExpandScript method method is used to expand script pages that contain FoxPro expressions and Code blocks using Active Server like syntax. The method generates a fully self contained HTML page/HTTP output which includes an HTTP header.

    This class uses the wwScripting class to parse <%= %> expression blocks and <% %> codeblocks. ExpandScript() behavior can be called explicitly or is available through generic script processing in the wwScriptMaps process handler which can be mapped for any extension in your server's Process method.

    o.ExpandScript(tcPageName, tnMode, tvContentType, tlTemplateStr, llNoOutput)
    

    Return Value

    nothing

    Parameters

    tcPageName
    Optional - Name of the page from disk to expand. Optionally, this can be the actual string of the script to evaluate. If not provided Request.GetPhysicalPath() is used.

    tnMode
    Optional - Determines the compilation mode for scripts. Default Mode is 1. For more info see
    wwServer::nScriptMode.

    1. Dynamic Compilation
    2. Precompiled FXP files
    3. Interpreted with ExecScript

    tvContentType
    Optional - Either a wwHTTPHeader object or a content type string.



    Remarks

    The Server.lDebugMode flag determines the level of error information that is displayed. With DebugMode .T. the source error is displayed in the page and a PRG file is generated that you can examine in the same directory as the script file. With DebugMode .F. the PRG file is not generated and wwScripting will not be able to retrieve or display the location of the error only the error message.

    Example


    Response.ExpandScript(THIS.cHTMLPagePath + "scriptdemo.wcs") Response.ExpandScript(Request.GetPhysicalPath()) && Default


    Expression Examples:
    <%= DateTime() %>
    <%= lcVar %>
    <%= TQuery.FieldName %>
    <%= MyFunction() %>
    
    Code Block Examples:
    <%
        lcOutput = ""
        for x = 1 to 5
            lcOutput = lcOutput + TRANS(x) + "<br>"
        endfor
        Response.Write(lcOutput)
    %>
    <%
        SELECT Company from TT_CUST INTO CURSOR TQuery 
        SCAN
    %>
    Company: <%= Company %><br />
    <% ENDSCAN %>

    To exit a script issue RETURN as part of a script block:

    <% IF llCanceled 
          RETURN && Exit script
       ENDIF
    %>

    Script pages have access to a special wwScriptingHttpResponse object which provides the ability to write output into the HTTP stream using Response. methods. From within script code you can add headers, cookies and otherwise manipulate the Response.

    <%
    lcOutput = "New Value"
    Response.Write(lcOutput)
    %>
    
    <%
    Response.AppendHeader("Expires","-1")
    Response.AppendHeader("Refresh","1;url=http://www.west-wind.com/")
    Response.AddCookie("SomeCookie","Some Value")
    Response.AddCookie("MyCookie","My Cookie Value","/",Date() + 10)
    %>

    Overloads:

    o.ExpandScript(tcPageName, tnMode, tvContentType, tlTemplateStr, llNoOutput)

    wwPageResponse::ExpandTemplate


    The ExpandTemplate method is used to expand template pages that contain FoxPro expressions using Active Server like syntax. The method generates a fully self contained HTML page/HTTP output which includes an HTTP header.

    The template syntax allowed is simple and looks as follows:

    <%= Version() %>
    <%= Table.FieldName %>
    <%= MyUDF() %>
    <%= PrivateVar %>
    <%= Class.Property %>

    or

    <%
    Code Block (any valid procedural FoxPro code)
    %>

    Expressions are expanded prior to sending the document back to the client, so expressions are valid anywhere inside of the HTML document including in HTML field values and HREF links etc.


    o.ExpandTemplate(tcPageName, tcContentType, tlTemplateString, tlNoOutput)
    

    Parameters

    tcPageName
    The physical filename to merge.
    If tlTemplateString is .t. this value is the Text to merge rather than a filename.

    tcContentType
    Optional - The content type of the template output.
    text/html

    tlTemplateString
    Optional - If .t. specifies that the first parameter is the text of the template instead of a filename.
    .F.



    Remarks

    Expressions are not evaluated recursively - have an expression that returns another <%= %> expression this retrieved expression is not evaluated.

    wwPageResponse::FastWrite


    Direct write to the output stream. This method directly writes into the response stream and bypasses any checks and flags. So it doesn't check for the ResponseEnded flag for example.


    o.FastWrite(lcText)
    

    Parameters

    lcText
    The text to output

    wwPageResponse::Redirect


    HTTP Redirection allows you redirect the current request to another URL. A common scenario is for login operations where the user is trying to access functionality before he's logged in. When the request comes in you can redirect the user to the Login page first.

    Redirection is an HTTP feature that works through the HTTP header and requires that all existing output be discarded first.

    Redirection is also available through the wwHTTPHeader::Redirect method, which this method calls internally to generate the Redirect header.



    o.Redirect(lcUrl)
    

    Parameters

    lcUrl

    wwPageResponse::Render


    This method is called to return the output of the Response object. It combines the headers and the body together and returns the entire response.

    o.Render()
    

    wwPageResponse::TransmitFile


    This method allows you send large files to the client without having to use Response.Write() to load these files into a string in Visual FoxPro.

    This method can get around the 16 meg limitation of Visual FoxPro strings for request output in COM mode. TransmitFile is much more efficient at sending large files as it avoids loading both FoxPro and the ISAPI DLL with these large strings, but instead causes wc.dll to read output directly from file back to the client.

    You can send files directly:

    FUNCTION TransmitFile Response.TransmitFile("d:\sites\MySite\Images\sailbig.jpg","image/jpeg") RETURN

    Or you can generate output dynamically using a separate Response object that writes to file (or any other mechanism that writes a file for that matter) like wwResponseFile.

    FUNCTION HugeOutput LOCAL lcOutputFile, loResponse lcOutputFile = Server.oConfig.oWwDemo.cHtmlPagePath + ; "temp\" + SYS(2015) + ".htm" *** Create a separate response object loResponse = CREATEOBJECT("wwResponseFile",lcOutputFile) FOR x=1 TO 1000000 loResponse.Write("012345678901234567890" + "<br>") ENDFOR *** Release and close loResponse = .null. Response.TransmitFile(lcOutputFile,"text/html") *** Some housekeepingDeleteFiles(JustPath( lcOutputFile ) + "\_*.*",900) && timeout in 15 minutes ENDFUNC


    o.TransmitFile(lcFileName, lcContentType)
    

    Parameters

    lcFileName
    The physical File to return back to the client over HTTP

    Note: If you're using the Web Connection .NET Module the filename specified must be inside of the Web directory tree in order to be downloadable. This means any files from other locations first need to be moved or generated into a Web relative path. With the ISAPI handler the file can live anywhere.

    lcContentType
    The content type of the file you are sending back to the client. If omitted this.ContentType is used instead.

    wwPageResponse::Write


    Raw HTTP output function. Writes directly to the Response stream/string.

    This method is the lowest level method of the Response object and is used to write output directly into the stream.

    o.Write(lcText)
    

    Parameters

    lcText
    The text to output to the client.

    wwResponse::HTMLFooter


    Creates a footer for a page. Creates </BODY></HTML> tags preceeded by
    option HTML passed as parm. Optionally pass text to display just prior to
    tags



    o.HTMLFooter(tcText,tlNoOutPut)
    

    Return Value

    "" if sending to file and the output text if lNoOuput is .T.

    Parameters

    tcText
    Optional - HTML text to display just above the closing of the HTML
    document. This can be handy for using a predefined template that gets
    displayed on the bottom of every page. For example, the Web Connection Demo
    uses a #DEFINE to create a string of a page footer that displays the 'Back
    to Demo Page' and 'Show Code' links - these automatically get embedded onto
    every page.

    llNoOutPut
    When set to .T. output is not sent to file, instead returning the result as
    a string.

    wwPageResponse::ContentType


    Sets the content type for the current HTTP response. This value defaults to text/html if not set.

    Special values that can be set:

    NONE
    No header is created.

    FORCE RELOAD
    The header is set to try and force the page to always reload by removing any cache options.

    CACHE
    The header is set to try and optimize caching of the returned result.

    Default Value

    Initial value: text/html

    wwPageResponse::Encoding


    Sets the content encoding for the page for text content.

    By default no encoding is applied which means you are responsible for appropriately setting the headers and converting the encoding.

    Possible values for this property include:

    If no value is specified no encoding occurs and no headers are added. This leaves the browser to decide how to display the content unless you provide it.

    Encoding occurs as part of the Render() method at the end of the request. If you specify either UTF8 or UNICODE the wwPageResponse class encodes the output as it's retrieved from the cOutput property.

    You can specify this property to force output to be encoded into UTF8 and UNICODE which adds the appropriate Content-Type header and encodes the response into the appropriate format.

    This property causes the entire output to be encoded. Be careful with this feature if you have content that is already UTF-8 encoded in any way - for example if you have templates or WCF pages that are UTF-8 formatted. If these pages are already UTF-8 encoded you may end up double encoding. It's best to save all templates in OEM ANSI format.


    o.Encoding                                                  
    

    Remarks

    You can apply this globally to a process class in the OnProcessInit() method by doing:

    Response.Encoding = "UTF8"

    wwPageResponse::Cookies


    Cookies collection that contains multiple cookies that are set for the current request. The cookies are written at the end when the headers are created and written.

    The cookies collection consists of:

    1 - Cookie name
    2 - the full Cookie string

    To set a cookie you should use the AddCookie() method.

    Default Value

    Initial value: null

    wwPageResponse::Expires


    The number of minutes before the page is expired.

    -1 can be used to force the content to expire immediately.

    wwPageResponse::GZipCompression


    If set to .T. automatically encodes the Response output to GZip compressed format. When set Web Connection automatically checks whether the client supports GZip compression and only compresses content if it does.

    This routine adds the required Response header and compresses the output properly.

    This routine checks the Accept-Encoding header in an Assign method to the property, and then performs the actual header addition and compression as part of the Render() method.



    o.GZipCompression                                           
    

    Remarks

    Requires that Zlib1.dll is available.

    wwPageResponse::Headers


    A collection of individual HTTP headers that are to be added to the request header. The Headers of the Response can be set at any time during the request as they are cached and written at the end of the request.

    The collection contains a pair of name and value pairs that can be accessed like this:

    Response.Headers.Add("Expires","-1") Response.Headers.Add("Custom","CustomValue") Response.Headers.Clear() && Remove all headers

    You can also use the Response.AppendHeader() method which has the same functionality.


    Default Value

    Initial value: null

    Remarks

    More info on HTTP Headers

    wwPageResponse::RawHeaders


    This is the low level string that is used to hold any RAW headers you want to add to a request. This is an override to Headers.Add() to allow for any headers or content that doesn't follow the name/value syntax of headers.

    Each header should be followed by a CRLF!

    Web Connection build up headers by looping through all of the headers in the Headers collection and then simply appends this raw string to the header.

    wwPageResponse::ResponseEnded


    Set to turn off output that follows called by the Write method. This flag is set by Response.End() so you usually should not have to set this explicitly. However, you can use it to check whether the Response object is active.

    Default Value

    Initial value: .F.

    wwPageResponse::Status


    Allows you to specify the Response Status Code. The status code is the status number and message that follows the HTTP protocol.

    Examples:

    Response.Status = "200 OK" Response.Status = "401 Not Found" Response.Status = "500 Server Error"


    Default Value

    Initial value: 200 OK

    Old Components



    Class wwResponse


    Providing all HTTP output to your HTTP application. This class abstracts the output generation for the various messaging mechanisms (File, COM, ASP) and provides full control over all aspects of HTTP generation.

    The Response object is very flexible in that it is designed to handle multiple different output sources via special subclasses that implement only a few methods to handle the physical output generation logistics. Web Connection implements the following:

    wwResponse
         wwResponseFile
         wwResponseString
         wwASPResponse

    The lower classes implement only a select few low level methods (Write, FastSend etc.), while the core functionality is implemented at the wwResponse level. The Web Connection framework determines which of these subclasses of the wwResponse object to implement based on the request mode of the current request.

    Response Lifetime

    The Response object is created whenever a wwProcess object is created, which occurs on every hit. Web Connection calls into your custom process class and passes in the Request object as a startup parameter. The request object provides the context required to determine which mechanism (File, String, ASP) is in use and based on that wwProcess creates an instance of the wwResponseXXXX object.

    This newly created object becomes your main output mechanism and is passed to your code as a surrogate object of the wwProcess class as wwProcess::oResponse, or simply as a PRIVATE variable called Response (if you use the default processing of the Process method).

    Functionality

    The Response object is very powerful in the features it provides your application with. At the lowest level it provides the Write() method which is used for all output travelling back to the Web server. Write is the low level that all other methods in the wwResponse object call to get output sent to the server. These other methods simply create more complex HTTP output and eventually call Write() to submit the output into the HTTP output stream.

    Write and its slightly more efficient FastSend companion method are the low level functions, but wwResponse also provides a number of high level features:

    HTTP Headers

    Every response should have an HTTP header attached to it. By default the Response object creates a standard HTML HTTP header using a content type of text/html which is generated through Response.ContentTypeHeader(), which is implicitly called from many methods such as HTMLHeader, HTMLHeaderEx, ExpandTemplate and ExpandScript. To create headers manually use code like the following:
    lcXML = <somecode that generates XML)
    Response.ContentTypeHeader("text/xml")
    Response.Write( lcXML)
    

    Headers are used for custom HTTP behaviors such as content expiration, content description, cookie, security and much more. Custom headers require that you use the wwHTTPHeader object to create the header. You can then either output this header directly or pass it to one of the highlevel methods as a parameter. This example manually creates output:

    oHeader = CREATE('wwHTTPHeader")
    oHeader.DefaultHeader()
    oHeader.AddCookie("Name","Rick")
    oHeader.AddForceReload() && ExpireContent immediately
    
    Response.Write( oHeader.GetOutput() )  && Write the header into the Response stream
    Response.Write( ... content here... )

    If you use one of the high level methods you'd pass the oHeader object as a parameter:

    Response.HTMLHeader("Hello World","Hello World",,oHeader)
    
    SELECT * FROM TT_Cust into cursor TQuery
    Response.ShowCursor()
    

    For more info see the wwHTTPheader documentation.

    The llNoOutput parameter

    The Write method is implemented like this:

    wwResponse::Write(lcText,llNoOutput)

    The first parameter is the text to send. The second parameter is very important, but optional and allows you to specify whether the text is sent into the HTTP output stream or returned to you as a string! Most other wwResponse object methods also include this same parameter, which allows most methods to be used as string generators rather than outputting to the HTTP stream. This also allows sophisticated nesting of method calls which would otherwise not be possible - a compound method would collect multiple HTML strings into a single string before actually sending the output to the HTTP stream.

    You'll see the llNoOutput parameter in most of the wwResponse methods and the behavior is the same for all of them: If you pass it in as .T. the result is returned to you as a string and the input is not actually sent to the HTTP output source.

    SubClassing

    You'll undoubtably will want to subclass the wwResponse class. Note that you can also subclass the lower level methods, but it's not recommended that you do this since those are considered system classes.

    When you subclass wwResponse with your own class you need to make some additional configuration settings to make sure the wwResponseFile/String/ASP classes - which are the ultimately implemented classes that the framework uses - know to use your new subclass. To do this you have to make a change in WCONNECT.H. All the Web Connection classes are created using DEFINE's that identify each class. For the wwResponse class the line is:

    #DEFINE WWC_RESPONSE			wwResponse

    and you need to replace the wwResponse value with the name of your subclass.

    #DEFINE WWC_RESPONSE			wwCustomResponse

    Once you do this, make sure you recompile your project or all PRG/VCX files. Once you do the wwResponseFile/String/ASP classes will now use your subclass.



    Class Members

    MemberDescription
    authenticate This method is used to perform a request for Web server Basic Authentication to occur. When this method is called the browser login dialog box is popped up for the user to type in a username and password. The password is then validated by the Web server typically against the NT (or Windows) user database. The exact validation varies by Web server - IIS uses the NT User database on the server.
    o.authenticate(lcRealm, lcErrorMsg, llNoOutput)
    Clear Clears all current output from the HTTP output stream. The object remains valid, but output has to start over.
    o.Clear()
    contenttypeheader Adds a Content Type header to a request. Use this method to create a default header or one of the common defaults. For more complex header use the wwHTTPHeader object instead.
    o.ContenttypeHeader([loHTTPHeader | lcContentType], llNoOutput)
    DownloadFile Downloads a file to the client resulting in a File Download dialog rather than displaying the content inside of the browser.
    o.DownloadFile(lcFilename,lcContentType,lcFileDisplayName)
    ExpandScript The ExpandScript method method is used to expand script pages that contain FoxPro expressions and Code blocks using Active Server like syntax. The method generates a fully self contained HTML page/HTTP output which includes an HTTP header.
    o.ExpandScript(tcPageName, tnMode, tvContentType, tlTemplateStr, llNoOutput)
    expandtemplate The ExpandTemplate method is used to expand template pages that contain FoxPro expressions using Active Server like syntax. The method generates a fully self contained HTML page/HTTP output which includes an HTTP header.
    o.ExpandTemplate(lcPageName, lcContentType, llTemplateString, llNoOutput)
    FastWrite This method is very similar to the Write method, but is more efficient as it doesn't perform any error checking on the string and doesn't handle the lNoOutput parameter.
    o.FastWrite(lcText,llNotUsed)
    formbutton Creates a button HTML form element.
    o.formbutton(lcName, lcCaption, lcType, lnWidth, lcCustomTags, llNoOutput)
    formcheckbox Creates an HTML Form Checkbox.
    o.formcheckbox(lcName, llValue, lcText, lcCustomTags, llNoOutput)
    FormHeader Creates a <FORM...> tag. Note you're responsible for creating the closing tag at the end of the form.
    o.FormHeader(lcAction, lcMethod, lcTarget, lcExtraTags, llNoOutput)
    formhidden Creates a hidden HTML form field.
    o.formhidden(lcName, lcValue, llNoOutput)
    formradio Create a Radio Button HTML Form Field.
    o.formradio(lcName, lcValue, lcText, llSelected, lcCustomTags, llNoOutput)
    formtextarea Creates an HTML Form TextArea.
    o.formtextarea(lcName, lcValue, lnHeight, lnWidth,lcCustomTags, llNoOutput)
    formtextbox Creates an HTML TextBox element.
    o.formtextbox(lcName, lcValue, lnWidth, lnMaxWidth, lcCustomTags, llNoOutput)
    GetOutput This method retrieves the currently accumulated HTTP stream output as a string and clears the output stream with a call to the Clear() method.
    o.GetOutput(llNoClear)
    HRef This method creates a hyperlink string.
    o.href(lcLink,lcText,llNoOutput)
    htmlfooter Creates a footer for a page. Creates </BODY></HTML> tags preceeded by option HTML passed as parm. Optionally pass text to display just prior to tags
    o.htmlfooter(tcText,tlNoOutPut)
    HTMLHeader This method provides a high level HTML header generation routine. By default it creates the following:
    o.HTMLHeader(tcHeader,tcTitle,tcBackground,tcContentType,tlNoOutput)
    HTMLHeaderEx This method allows you to create custom HTMLHeader and HTTPHeader objects and pass it to this method for output creation.
    o.HTMLHeaderEx(lvHTMLHeader, lvHTTPHeader)
    IEChart This method allows embedding of the IE Chart ActiveX Control into your pages driven by data from the currently active cursor when the method is called. The data in the cursor to graph must contain at least two fields in the following format:
    o.iechart(cChartType,vWidth,cHeight,lNoOutput)
    nooutput Turns off all output until the wwResponse object is destroyed.
    o.NoOutput()
    Redirect HTTP Redirection allows you redirect the current request to another URL. A common scenario is for login operations where the user is trying to access functionality before he's logged in. When the request comes in you can redirect the user to the Login page first.
    o.redirect(lcUrl,llNoOutput)
    ShowCursor This method allows easy display of an entire table, simply by having a table or cursor selected and calling this method. You can optionally pass an array of headers as well as a title and the option to automatically sum all numeric fields.
    oHTML.ShowCursor(@aHeaders, cTitle, lSumNumbers, lNoOutput,cTableTags)
    standardpage Creates a standalone HTML page. The page is a fully self contained page that looks like this by default:
    o.StandardPage(cHeader, lcBody, lvHeader,lnRefresh,lcRefreshUrl,llNoOutput)
    TransmitFile This method allows you send large files to the client without having to use Response.Write() to load these files into a string in Visual FoxPro.
    o.TransmitFile(lcFilename,lvHeader)
    Write The Write and FastSend methods are used for all output that is sent to the HTTP output stream. They are the low level methods through which all output from the wwResponse object flows. All other methods of this object call into Write or FastSend to send their output into the HTTP stream. This centralized access makes this object flexible and able to serve different output mechanisms such as File
    o.Write(lcText,llNoOutput)
    WriteLn Writes output to the HTTP output stream with trailing carriage returns. This is identical to:
    o.WriteLn(lcText,llNoOutput)
    writememo Writes memo fields to output. Formats carriage returns to <p> and <br> as appropriate.
    o.writememo(lcText, llNoOutput)
    ContentType Sets the content type of the request for example: text/plain or text/xml. If this value is not set the default content type for a request is always text/html.
    cStyleSheet This property is used to force the HTMLHeader method to use the specified Cascading Style Sheet. Property asks for a URL that points at the CSS file.

    Requirements

    Assembly: wwResponse.prg, wwFileResponse.prg, wwStringResponse.prg

    *** Base Class Abstract Methods


    The following are abstract methods that any Response subclass should implement to write Response output to different output sources (ie. string, file, etc.).

    wwResponse::Write


    The Write and FastSend methods are used for all output that is sent to the HTTP output stream. They are the low level methods through which all output from the wwResponse object flows. All other methods of this object call into Write or FastSend to send their output into the HTTP stream. This centralized access makes this object flexible and able to serve different output mechanisms such as File or String or ASP Response object output by changing only a few methods.

    Write sends output as is without a trailing carriage return or other formatting.

    The optional lNoOutput parameter is used to avoid sending output to the HTTP stream, returning a string as a result of the method instead. This lNoOutput option is available on most other wwResponse methods and passed through to this method. This makes it possible to use wwResponse methods to generate output strings directly.

    o.Write(lcText,llNoOutput)
    

    Return Value

    "" or the input string if llNoOutput is .T.

    Parameters

    lcText
    Any kind of string based output that is to be placed in the HTTP output stream. This data can be a string or binary data.

    llNoOutput
    When set to .T. output is not sent to file, instead returning the result as a string.

    wwResponse::FastWrite


    This method is very similar to the
    Write method, but is more efficient as it doesn't perform any error checking on the string and doesn't handle the lNoOutput parameter.

    o.FastWrite(lcText,llNotUsed)
    

    Parameters

    lcText
    Any kind of string based output that is to be placed in the HTTP output stream. This data can be a string or binary data.

    llNotUsed
    A second parameter for the typical llNoOutput is provided for compatibility, but is ignored.

    wwResponse::GetOutput


    This method retrieves the currently accumulated HTTP stream output as a string and clears the output stream with a call to the
    Clear() method.

    This method is used internally to retrieve content from Response objects directly into string values that can be displayed. When using the wwResponseString object especially GetOutput is the primary mechanism for retrieving the HTTP output.


    o.GetOutput(llNoClear)
    

    Return Value

    String of the current HTTP Stream output.

    Parameters

    llNoClear
    When this parameter is passed as .T. the original HTTP stream is not cleared.

    wwResponse::Clear


    Clears all current output from the HTTP output stream. The object remains valid, but output has to start over.

    This method is used internally to clear the output buffers when errors occur - at that point output is reset and a full new HTTP response is written typically by wwProcess::ErrorMsg.

    o.Clear()
    

    *** General purpose methods


    The following methods are generic methods that write to the HTTP output stream. These methods use the base class abstract methods (mainly Send/FastSend) to send output to the HTTP stream and thus do not need to be implemented by output specifc subclasses (such as wwResponseFile, wwResponseString).

    wwResponse::Authenticate


    This method is used to perform a request for Web server Basic Authentication to occur. When this method is called the browser login dialog box is popped up for the user to type in a username and password. The password is then validated by the Web server typically against the NT (or Windows) user database. The exact validation varies by Web server - IIS uses the NT User database on the server.


    o.authenticate(lcRealm, lcErrorMsg, llNoOutput)
    

    Parameters

    lcRealm
    The domain name or IP address that is authenticated. This is basically the name of the domain that displays on the authentication dialog that the browser pops up.

    lcErrorMsg
    The error message HTML that you want to display if an error occurs.

    llNoOutput
    Standard NoOutput flag to return the result as a string.

    Remarks

    In order for an authentication request to succeed on the server Basic Authentication must be enabled on the server.

    Note: Basic Authentication is a non-secure protocol that sits on top of HTTP. Passwords are passed as clear text (although encoded with a simple, easily breakable hash algorithm) and can be easily hijacked with a network sniffer. If you're worried about security make sure that your authentication request runs over SSL/HTTPS - when you do the entire request info is encrypted.

    Also, check out the wwProcess::Login method which abstracts the login and authentication process into a single easy to use method.

    Example

    lcUserName=Request.GetAuthenticatedUser()
       
    *** Did the user Authenticate
    IF EMPTY(lcUserName)
       *** Send Password Dialog - on success this request will be rerun
       *** AFter 3 failures an error message will be displayed.
       THIS.oResponse.Authenticate(Request.GetServername())
       RETURN .T.
    ENDIF   
    

    wwResponse::ContentTypeHeader


    Adds a Content Type header to a request. Use this method to create a default header or one of the common defaults. For more complex header use the wwHTTPHeader object instead.

    What's a content type?

    Web requests require an HTTP header in order to let the browser know how to display the dynamically generated page. The header looks something like this:
    HTTP/1.0 200 OK
    Content-type: text/html
    
    <HTML>
      ...HTML content here.
    </HTML>
    

    In the above example the Content Type line plus a blank line is the actual text that makes up the Content type header.

    This method adds this ContentType header to a request. This method is also called internally by various other methods that manipulate the HTTP header. HTMLHeader() and any full page generation methods such as ExpandTemplate(), ExpandScript(), ErrorMsg() and StandardPage() pass forward their lvHeader parameter to this method.




    o.ContenttypeHeader([loHTTPHeader | lcContentType], llNoOutput)
    

    Parameters

    lcContentType
    For backward compatibility you can also call this method with a text parameterCGI requests require a Content Type header which usually looks like this in the generated document:

    Common types are:
    "text/html" - Default
    "text/xml" - XML
    "text/plain" - Text data
    "Force Reload" - Force browser to always reload page
    "Cache" - Maximum Cache Settings for this request
    "none" - No header is sent - your code has to set it up

    oHTTPHeader
    A preconfigured wwHTTPHeader object that was previously set up and configured.


    llNoOutput
    Optional - Output to string and not to HTTP stream when set to .t..


    Example

    **** Simple Content Type Assignment on XML data
    
    lcXML = oXML.CursorToXML()
    
    Response.ContentTypeHeader("text/xml")
    Response.Write(lcXML)
    RETURN
    
    **** HTTP Header object ****
    
    lcId=Request.GetCookie("WWUSERID")
    
    *** Create Standard Header - here to add an HTTP Cookie
    loHeader=CREATEOBJECT("wwHTTPHeader")
    loHeader.DefaultHeader()
    
    *** If not Found
    IF EMPTY(lcId)
       *** Create the cookie
       lcId=SYS(3)
       loHeader.AddCookie("WWUSERID",lcId,"/wconnect")
    ENDIF            
    
    *** Set the Content Type Header
    Response.ContentTypeHeader(loHeader)
    
    *** Alternately you can also pass the header to another method
    * Response.HTMLHeader("Cookie Test","Cookie Test Page",,loHeader)
    
    *** Followed by regular HTML text (any wwResponse methods valid)
    Response.Write("<HTML><BODY>")
    Response.Write("Cookie Value (wwUserId):  <b>" +lcId +"</b><p> ")
    Response.Write("</BODY></HTML>")
    
    RETURN

    wwResponse::ContentType


    Sets the content type of the request for example: text/plain or text/xml. If this value is not set the default content type for a request is always text/html.

    Note that you should always set a content type header explicitly since it's more efficient

    This property is here only for compatibility with ASP and it simply causes the ContentTypeHeader method to be called directly via Assign method.

    o.ContentType                                                   
    

    Remarks

    This property must be set before any other HTTP output is sent.

    wwResponse::DownloadFile


    Downloads a file to the client resulting in a File Download dialog rather than displaying the content inside of the browser.

    This method is a fully self contained Response action and creates the HTTP header and downloads the file to the client.

    o.DownloadFile(lcFilename,lcContentType,lcFileDisplayName)                        
    

    Parameters

    lcFilename
    Full path to the file to send to the client.

    lcContentType
    Content type of the file to send down. This will help identify the file on the client so it can be opened by hte correct viewer (ie. Word, Notepad or WinZip). Examples: text/txt, application-x-zip etc.

    lcFileDisplayName
    The name of the file that is to be displayed to the user. This should be a savable filename and include no path information.

    Remarks

    Make sure the file you are sending is not deleted as part of this request as the file is read by wc.dll. If you need to delete the file use
    wwUtils::DeleteFiles to do a delayed delete.

    wwResponse::ExpandTemplate


    The ExpandTemplate method is used to expand template pages that contain FoxPro expressions using Active Server like syntax. The method generates a fully self contained HTML page/HTTP output which includes an HTTP header.

    The template syntax allowed is simple and looks as follows:

    <%= Version() %>
    <%= Table.FieldName %>
    <%= MyUDF() %>
    <%= PrivateVar %>
    <%= Class.Property %>

    or

    <%
    Code Block (any valid procedural FoxPro code)
    %>

    Expressions are expanded prior to sending the document back to the client, so expressions are valid anywhere inside of the HTML document including in HTML field values and HREF links etc.


    o.ExpandTemplate(lcPageName, lcContentType, llTemplateString, llNoOutput)
    

    Return Value

    "" or HTML string if llNoOutput is .T.

    Parameters

    lcPageName
    Fully qualified filename of the template to expand from disk.
    Optionally, this may be a string containing the actual script if the llTemplateString parameter is set to .T.

    lvContentType
    Optional - Either an wwHTTPHeader object or content type string. See
    wwHTTPHeader class for info.

    llTemplateString
    Optional - If passed .T. it indicates that lcPageName was passed as the actual template string, rather than the filename.

    llNoOutput
    Optional - if .T. output is returned to string and no HTTP stream output is created

    Remarks

    Since embedded code blocks (ie. more than one line of code using <% %>) are dynamically evaluated through macro expansion you'll want to keep the use of functions to a minimum for speed reasons. If you use excessive functions in your pages I would highly recommend you build a custom Processing routine as a UDF instead of building the code into an HTML page. Custom code is much easier to debug and maintain and also runs a lot faster since no on the fly evaluation takes place.

    Example

    Response.ExpandTemplate(THIS.cHTMLPagePath + "node.wc")

    wwResponse::TransmitFile


    This method allows you send large files to the client without having to use Response.Write() to load these files into a string in Visual FoxPro.

    This method can get around the 16 meg limitation of Visual FoxPro strings for request output in COM mode. TransmitFile is much more efficient at sending large files as it avoids loading both FoxPro and the ISAPI DLL with these large strings, but instead causes wc.dll to read output directly from file back to the client.

    You can send files directly:

    FUNCTION TransmitFile Response.TransmitFile("d:\sailbig.jpg","image/jpeg") RETURN

    Or you can generate output dynamically using a separate Response object that writes to file (or any other mechanism that writes a file for that matter) like wwResponseFile.

    FUNCTION HugeOutput LOCAL lcOutputFile, loResponse lcOutputFile = Server.oConfig.oWwDemo.cHtmlPagePath + ; "temp\" + SYS(2015) + ".htm" *** Create a separate response object loResponse = CREATEOBJECT("wwResponseFile",lcOutputFile) FOR x=1 TO 1000000 loResponse.Write("012345678901234567890" + "<br>") ENDFOR *** Release and close loResponse = .null. Response.TransmitFile(lcOutputFile,"text/html") *** Some housekeepingDeleteFiles(JustPath( lcOutputFile ) + "\_*.*",900) && timeout in 15 minutes ENDFUNC



    o.TransmitFile(lcFilename,lvHeader)                             
    

    Parameters

    lcFilename

    lvHeader


    Remarks

    Since wc.dll handles the sending of the file the file access security requirements are important. wc.dll will revert to the underlying system account running the Web Connection application. This will typically be SYSTEM (in IIS5 and earlier) or whatever account you have set up in the IIS 6 or later Application Pool impersonation. Make sure you write/place the output file in a location where this account can read from it.

    This method makes it much more feasible to build a file management by authorization scheme like paid access management, as it removes almost completely the load from the Web Connection server instances and only has ISAPI streaming the data back which is very efficient.

    Make sure to NOT delete the file you are sending. If you need to delete the files after sending look into using wwUtils::DeleteFiles which allows you to do delayed cleanup.

    wwResponse::ExpandScript


    The ExpandScript method method is used to expand script pages that contain FoxPro expressions and Code blocks using Active Server like syntax. The method generates a fully self contained HTML page/HTTP output which includes an HTTP header.

    This class uses the wwScripting class to parse <%= %> expression blocks and <% %> codeblocks.

    Expression Examples:

    <%= DateTime() %>
    <%= lcVar %>
    <%= TQuery.FieldName %>
    <%= MyFunction() %>
    
    Code Block Examples:
    <%
        lcOutput = ""
        for x = 1 to 5
            lcOutput = lcOutput + TRANS(x) + "<br>"
        endfor
        Response.Write(lcOutput)
    %>
    <%
        SELECT Company from TT_CUST INTO CURSOR TQuery 
        SCAN
    %>
    Company: <%= Company %><br />
    <% ENDSCAN %>

    ExpandScript() behavior can be called explicitly or is available through generic script processing in the wwScriptMaps process handler which can be mapped for any extension in your server's Process method.

    o.ExpandScript(tcPageName, tnMode, tvContentType, tlTemplateStr, llNoOutput)
    

    Return Value

    nothing

    Parameters

    tcPageName
    Optional - Name of the page from disk to expand. Optionally, this can be the actual string of the script to evaluate. If not provided Request.GetPhysicalPath() is used.

    tnMode
    Optional - Determines the compilation mode for scripts. Default Mode is 1. For more info see
    wwServer::nScriptMode.

    1. Dynamic Compilation
    2. Precompiled FXP files
    3. Interpreted with ExecScript

    tvContentType
    Optional - Either a wwHTTPHeader object or a content type string.



    Example


    Response.ExpandScript(THIS.cHTMLPagePath + "scriptdemo.wcs") Response.ExpandScript(Request.GetPhysicalPath()) && Default


    Expression Examples:
    <%= DateTime() %>
    <%= lcVar %>
    <%= TQuery.FieldName %>
    <%= MyFunction() %>
    
    Code Block Examples:
    <%
        lcOutput = ""
        for x = 1 to 5
            lcOutput = lcOutput + TRANS(x) + "<br>"
        endfor
        Response.Write(lcOutput)
    %>
    <%
        SELECT Company from TT_CUST INTO CURSOR TQuery 
        SCAN
    %>
    Company: <%= Company %><br />
    <% ENDSCAN %>

    To exit a script issue RETURN as part of a script block:

    <% IF llCanceled 
          RETURN && Exit script
       ENDIF
    %>

    Script pages have access to a special wwScriptingHttpResponse object which provides the ability to write output into the HTTP stream using Response. methods. From within script code you can add headers, cookies and otherwise manipulate the Response.

    <%
    lcOutput = "New Value"
    Response.Write(lcOutput)
    %>
    
    <%
    Response.AppendHeader("Expires","-1")
    Response.AppendHeader("Refresh","1;url=http://www.west-wind.com/")
    Response.AddCookie("SomeCookie","Some Value")
    Response.AddCookie("MyCookie","My Cookie Value","/",Date() + 10)
    %>
    Overloads:
    o.ExpandScript(tcPageName, tnMode, tvContentType, tlTemplateStr, llNoOutput)

    wwResponse::HRef


    This method creates a hyperlink string.



    o.href(lcLink,lcText,llNoOutput)
    

    Return Value

    "" if sending to file and the output text if lNoOuput is .T.

    Parameters

    lcUrl
    The URL to link to.

    lcText
    Optional - The display text for the hyperlink. If not specified the URL is used.

    llNoOutput
    "" if sending to file and the output text if lNoOuput is .T.

    wwResponse::HTMLFooter


    Creates a footer for a page. Creates </BODY></HTML> tags preceeded by option HTML passed as parm. Optionally pass text to display just prior to tags



    o.htmlfooter(tcText,tlNoOutPut)
    

    Return Value

    "" if sending to file and the output text if lNoOuput is .T.

    Parameters

    tcText
    Optional - HTML text to display just above the closing of the HTML document. This can be handy for using a predefined template that gets displayed on the bottom of every page. For example, the Web Connection Demo uses a #DEFINE to create a string of a page footer that displays the 'Back to Demo Page' and 'Show Code' links - these automatically get embedded onto every page.

    llNoOutPut
    When set to .T. output is not sent to file, instead returning the result as a string.

    Example

    Response.HTMLHeader("Customer Demo Example","West Wind Customer Demo")
    
    Response.Write("Hello World")
    
    Response.HTMLFooter()

    wwResponse::HTMLHeader


    This method provides a high level HTML header generation routine. By default it creates the following:

    The method is configurable so that you can pass custom HTTP headers, leave out the HTML heading and a few other options.

    o.HTMLHeader(tcHeader,tcTitle,tcBackground,tcContentType,tlNoOutput)
    

    Return Value

    "" or string if tlNoOutput = .T.

    Parameters

    tcHeader
    The header of the document. This is the large text HTML generated at the top of the HTML document. Not generated if this value is passed in as blank or omitted.

    tcTitle
    Optional - The text for browser's title bar. If not passed the header is used.

    tcBackground
    Optional - Allows you to specify a background color or image.

    tvContentType
    Optional - By default this method uses a default HTTP header. You can pass in a custom HTTP header option (see
    ContentTypeHeader()) or an wwHTTPHeader object.

    tlNoOutput
    When set to .T. output is not sent to file, instead returning the result as a string.


    Example

    Response.HTMLHeader("Customer Demo Example","West Wind Customer Demo")
    
    Response.Write("Hello World")
    
    Response.HTMLFooter()

    wwResponse::cStyleSheet


    This property is used to force the HTMLHeader method to use the specified Cascading Style Sheet. Property asks for a URL that points at the CSS file.

    There are two ways you can use this method:

    It should be noted that currently only the HTMLHeader() method generates a stylesheet automatically. The HTMLHeaderEx method requries that you manually provide the Stylesheet. You can pull the default by querying Response.cStylesheet and pass it to the wwHTMLHeader::AddStyleSheet method.

    o.cStyleSheet                                                   
    

    Default Value

    Default: ""

    wwResponse::HTMLHeaderEx


    This method allows you to create custom HTMLHeader and HTTPHeader objects and pass it to this method for output creation.

    o.HTMLHeaderEx(lvHTMLHeader, lvHTTPHeader)                      
    

    Return Value

    "" or string if llNoOutput = .T.

    Parameters

    lvHTMLHeader
    An instance of the wwHTMLHeader class which contains output for the HTML document header including <HTML>, <BODY> and <HEAD> sections of the document. If left blank a default header is used. (<html><body>).
    Alternately you can pass a Title string which generates a basic default header. If not passed or passed blank a default header is also generated.

    lvHTTPHeader
    An instance of the wwHTTPHeader object, which contains output for the HTTP header. An HTTP header is required on every outgoing docment. If object is not passed a default text/html header is created.
    You can also pass in a default header string that contains just the content type (like text/xml for example) or a directive (like FORCE RELOAD).
    If not passed or passed empty a default header is generated.



    Example

    FUNCTION Headertest
    
    loHTMLHeader = CREATEOBJECT("wwHTMLHeader",Response)
    loHTTPHeader = CREATEOBJECT("wwHTTPHeader",Response)
    
    loHTTPHeader.DefaultHeader()
    loHTTPHeader.AddForceReload()
    loHTTPHeader.AddCookie("laston",TTOC(DATETIME()))
    
    loHTMLHeader.AddTitle("Header Test Page")
    loHTMLHeader.AddPageRefresh(Request.GetCurrentUrl(),5)
    loHTMLHeader.AddJavaScript([function test {] + CRLF + [alert("test"); } ] )
    loHTMLHeader.AddVBScript([function test ] + CRLF + [MsgBoxt("test") ] + CRLF + [end func])
    loHTMLHeader.AddStyleSheet("/westwind.css")
    loHTMLheader.cBodyTag = [<body bgcolor="#FFFFFF">]
    
    Response.HTMLHeaderEx(loHTMLHeader,loHTTPHeader)
    
    *** This is 
    Response.Write("<h1>Header Test Page</h1>")
    Response.Write("<hr>" + TIME() )
    Response.HTMLFooter()
    
    ENDFUNC
    

    wwResponse::IEChart


    This method allows embedding of the IE Chart ActiveX Control into your pages driven by data from the currently active cursor when the method is called. The data in the cursor to graph must contain at least two fields in the following format:





    FieldTypeFunction
    Field1CharacterThe label that is to be displayed with the data item. In a bar chart this means the column labels on a Pie chart it means the labels on the pie attached to each piece.
    Field 2-n NumericThe data item to graph.



    o.iechart(cChartType,vWidth,cHeight,lNoOutput)
    

    Return Value

    "" or the generated text string if lNoOutput is set to .T.

    Parameters

    cChartType
    Determines the type of chart that is displayed. Available chart types are:

    The default graph type if not passed is BAR.You may also pass a numeric string that corresponds to the IEChart object graph type explicitly. You can get the available types from the IEChart object documentation from MS's ActiveX Gallery site.

    nColumns
    Determines the number of columns that are graphed. By default only one column is graphed, but you make specify more than one for various graphs that support multiple datasets such as the stacked bar chart.

    vWidth
    Determines the width of the chart. You may pass a numeric value that contains a fixed pixel width or character string that specifies a percentage. The default is "100%".

    nHeight
    Determines the display height of the chart object in the HTML document in pixels. Default is 250.

    clabels
    You can specify space separated list of labels for the legend. If not specified no legend is displayed on the graph. If specified the number of space separated labels should match the number of colums specified in the nColums parameter.

    lNoOutput
    If .T. output will not be sent to file.

    Remarks

    Output generates an <OBJECT> tag which works only with ActiveX enabled browsers like MS Internet Explorer 3.0 or later.

    Example

    SELECT hour(time) as HOUR, COUNT(time) AS Hits, DAY(TIME) AS DAY;  
             FROM cgilog ;
       WHERE time > datetime() - 82800 ; 
       GROUP BY 1 ;
       INTO Cursor Tquery
    
    loHTML=CREATE("wwHTML","Chart.htm") 
    loHTML.HTMLHeader("IE Chart Test")
    loHTML.IEGraph("AREA",1,"100%",250)
    
    

    wwResponse::NoOutput


    Turns off all output until the wwResponse object is destroyed.

    This method is used internally to handle error handling so that when an error occurs and the code continues on through the framework no further HTML output is generated along the way.

    o.NoOutput()
    

    wwResponse::Redirect


    HTTP Redirection allows you redirect the current request to another URL. A common scenario is for login operations where the user is trying to access functionality before he's logged in. When the request comes in you can redirect the user to the Login page first.

    Redirection is an HTTP feature that works through the HTTP header and requires that all existing output be discarded first.

    Redirection is also available through the wwHTTPHeader::Redirect method, which this method calls internally to generate the Redirect header.



    o.redirect(lcUrl,llNoOutput)
    

    Return Value

    "" if sending to file and the output text if lNoOuput is .T.

    Parameters

    lcUrl
    The URL to redirect to.

    llNoOutput
    When set to .T. output is not sent to file, instead returning the result as a string.

    Remarks

    Redirection occurs without additional HTTP header directives. So if you generated a Cookie or Authentication request prior to the Redirection call those operations will not be submitted to the browser - it's not supported by the HTTP protocol.

    wwResponse::ShowCursor


    This method allows easy display of an entire table, simply by having a table or cursor selected and calling this method. You can optionally pass an array of headers as well as a title and the option to automatically sum all numeric fields.



    oHTML.ShowCursor(@aHeaders, cTitle, lSumNumbers, lNoOutput,cTableTags)
    

    Parameters

    @aHeaders
    An array that should contain as many text headers as they are columns in the table/cursor to display. If this parameter is not passed the field names are used as column headers.The header names may be followed by a colon followed by a number indicating the width of the header to override the field width which is used by default.

    Ctitle
    Title text to display above the headers.

    lSumNumbers
    Flag that allows automatic summing of all numeric fields in the table to display. The total is displayed at the bottom of the display below the appropriate numeric fields.

    LNoOutput
    When set to .T. output is not sent to file, instead returning the result as a string.

    cTableTags
    Allows adding additional table tags to the table display to control the appearance of the HTML table. For example, you could pass "WIDTH=100% BORDER=5" to force the table to be full size. The default value is "WIDTH=90%".


    Remarks

    By default an HTML table is used for output unless output size exceeds MAX_TABLE_CELLS (in WCONNECT.H) . When this number is exceeded ShowCursor reverts to a <pre> list display which can render much faster. The default size for MAX_TABLE_CELLS is 4000 cells.

    This is a simple high level method. If you want more control please use the wwShowCursor class.

    For better performance you should hand-code your tables with the Write() method. Hand coding can cut request times for large table requests by more than half.

    Example

    SELECT company, lname ;   
       FROM TT_Cust ;   
       INTO CURSOR Tquery
    
    *** Basic
    oHTML.ShowCursor()
    
    *** Using custom headers
    DIMENSION laHeader[2]
    laHeader[1]="Company"
    laHeader[2]="Last Name"
    
    oHTML.ShowCursor(@laHeaders,"Client List")

    wwResponse::StandardPage


    Creates a standalone HTML page. The page is a fully self contained page that looks like this by default:

    You can customize this look by subclassing this method. However, it's recommended that you use wwProcess::StandardPage to subclass since you can provide application specific subclassing at that level more easily.


    o.StandardPage(cHeader, lcBody, lvHeader,lnRefresh,lcRefreshUrl,llNoOutput)
    

    Return Value

    "" or HTML if llNoOutput is .T.

    Parameters

    lcHeader
    The header of the message. This is bolder at the top of the page and also reflects in the browser's title bar.

    lcBody
    The body of the message. This text can contain plain text or HTML.

    lvHeader
    Optional. The content type or HTTP Header object for this page. You can create a custom header with the wwHTTPHeader class or pass an HTTP content type.

    lnRefresh
    Optional - If you would like the page to refresh itself after a while to go to this or another URL you can specify an interval in seconds.

    lcRefreshUrl
    Optional - Specify the URL that you want to go to when you refresh the page automatically.

    llNoOutput
    Output is returned as string and not written to HTTP stream.

    Example

    Response.StandardPage("Welcome","Welcome to the West Wind Web Connection Demo.","FORCE RELOAD")

    wwResponse::WriteMemo


    Writes memo fields to output. Formats carriage returns to <p> and <br> as appropriate.

    o.writememo(lcText, llNoOutput)
    

    Return Value

    "" if sending to file and the output text if lNoOuput is .T.

    Parameters

    lcText
    The text to format. All carriage returns are stripped and replaced.

    llNoOutput
    When set to .T. output is not sent to file, instead returning the result as a string.

    wwResponse::WriteLn


    Writes output to the HTTP output stream with trailing carriage returns. This is identical to:

    Response.Write(tcText + CRLF)

    but is easier to type and read. Use this for convenience, but keep in mind there's a little overhead in WriteLn since it calls back onto the Write() method to actually dump to the HTTP stream.

    o.WriteLn(lcText,llNoOutput)
    

    Return Value

    "" or output string if llNoOutput is set.

    Parameters

    lcText
    The text to output

    llNoOutput
    If .T. the string is returned back and no output is written into the HTTP stream.

    *** Form Field Generation


    The following methods serve to insert HTML form fields into the HTTP output. Note that you still have to manage the <FORM> tag manually to wrap the individual fields.

    wwResponse::FormButton


    Creates a button HTML form element.

    o.formbutton(lcName, lcCaption, lcType, lnWidth, lcCustomTags, llNoOutput)
    

    Return Value

    "" or button text if llNoOutput = .t.

    Parameters

    lcName
    Name of the button field.

    lcCaption
    The caption of the button text.

    lcType
    Button type. Default: SUBMIT. Others include Button.

    lnWidth
    Width of the button.

    lcCustomTags
    Any custom HTML attributes.

    llNoOutput
    If .t. output is not sent to Response object but returned as a string

    wwResponse::FormHeader


    Creates a <FORM...> tag. Note you're responsible for creating the closing tag at the end of the form.

    o.FormHeader(lcAction, lcMethod, lcTarget, lcExtraTags, llNoOutput)
    

    Return Value

    "" or HTML if llNoOutput = .T.

    Parameters

    lcAction
    Form submission URL.

    lcMethod
    Method to submit the form. POST by default if not passed.

    lcTarget
    Optional - target frame/window to send output to.

    lcExtraTags
    HTML Attributes to add to FORM tag.

    llNoOutput
    Return HTML as string

    wwResponse::FormCheckbox


    Creates an HTML Form Checkbox.

    o.formcheckbox(lcName, llValue, lcText, lcCustomTags, llNoOutput)
    

    Parameters

    lcName
    Name of the Checkbox HTML field.

    llValue
    Value of the field (.T. or. .F.)

    lcText
    Text caption of the checkbox.

    lcCustomTags
    Any custom HTML attributes for the checkbox.

    llNoOutput
    If .T. return as string instead of going into Response stream.

    wwResponse::FormHidden


    Creates a hidden HTML form field.

    o.formhidden(lcName, lcValue, llNoOutput)
    

    Return Value

    "" or HTML if llNooutput is .T.

    Parameters

    lcName
    Field name to be created

    lcValue
    Preselected value of the field

    llNoOutput
    Return as string

    wwResponse::FormRadio


    Create a Radio Button HTML Form Field.

    o.formradio(lcName, lcValue, lcText, llSelected, lcCustomTags, llNoOutput)
    

    Return Value

    "" or HTML if llNoOutput = .T.

    Parameters

    lcName
    Name of the field to be created

    lcValue
    The value when it is selected.

    lcText
    The text of the radio button label.

    llSelected
    Selection flag - set .t. to select.

    lcCustomTags
    Custom HTML Attributes

    llNoOutput
    return as string

    wwResponse::FormTextArea


    Creates an HTML Form TextArea.

    o.formtextarea(lcName, lcValue, lnHeight, lnWidth,lcCustomTags, llNoOutput)
    

    Parameters

    lcName

    lcValue
    Value to set the text area to.

    lnHeight
    Height in Rows.

    lnWidth
    Width in columns

    lcCustomTags
    Custom HTML Attributes for this element.

    llNoOutput
    If .T. returns a string, otherwise goes into the Response stream.

    wwResponse::FormTextBox


    Creates an HTML TextBox element.

    o.formtextbox(lcName, lcValue, lnWidth, lnMaxWidth, lcCustomTags, llNoOutput)
    

    Return Value

    nothing or HTML if llNoOutput is .T.

    Parameters

    lcName
    Field name to be created

    lcValue
    Preselected value of the field

    lnWidth
    Display width of the field.

    lnMaxWidth
    Max characters to accept.

    lcCustomTags
    Any custom HTML attributes.

    llNoOutput
    Return as string

    Framework Support classes


    The framework support classes handle support to the core classes that the Web Connection framework provides. For the most part these are utility classes, that provide useful functionality ontop the base features, but several like the wwConfig and wwSession objects are actually tightly linked to the framework as support classes.

    Class wwHTTPHeader


    Customizing HTTP requests with special instructions and behaviors for browsers and other HTTP clients.

    This class deals with generating an HTTP header for Web Connection requests. Although you can generate headers manually via string output this class automates the process with standard headers and common methods to add common header features such as Authentication, Redirection, HTTP Cookies and page persistance.

    A basic HTTP header looks like this:

    HTTP/1.0 200 OK
    Content-type: text/html
    
    

    Note that headers must include a blank line following the header and before the actual content (lets say an HTML document) starts.

    This class is a utility class and primarily used internally by Web Connection to generate the HTTP headers for each request, but you can also access this class directly to create custom headers.

    The class basically works by outputting to a wwResponse object. You can either pass in an existing wwResponse object or you can have it create one of its own internally. If you use an existing object all output is directly routed into the existing Response object and this is the mechanism that Web Connection uses internally. If you don't pass one in, wwHTTPHeader creates a wwResponseString object and you have to call the wwHTTPHeader::GetOutput method to retrieve the content.


    Note:
    If you use the wwHTTPHeader object and pass in an existing wwResponse object, you will most likely have to call wwHTTPHeader::CompleteHeader() to force the header to complete itself. This adds the separating blank line into the output. If you let the wwHTTPHeader create its own internal object this is not necessary - wwHTTPHeader::GetOutput() automatically adds the blank line.

    *** Write out the header directly into the HTTP stream
    oHeader = CREATE("wwHTTPHeader",Response)
    oHeader.DefaultHeader()
    oHeader.AddForceReload()
    oHeader.CompleteHeader()
    
    *** Start our HTTP content now
    Response.WriteLn("<html>")
    ...
    Response.WriteLn("</html>")


    Example

    *** An example creating a custom header with HTTP cookies
    
    
    *** Try to retrieve the cookie...
    lcId=Request.GetCookie("WWUSERID")
    
    *** Create Standard Header
    loHeader=CREATEOBJECT("wwHTTPHeader")
    loHeader.DefaultHeader()
    
    *** If not Found
    IF EMPTY(lcId)
       *** Create the cookie
       lcId=SYS(3)
       
       loHeader.AddCookie("WWUSERID",lcId,"/wconnect")
       
       *** To specify a permanent cookie supply NEVER or a specific expiration date
       *** loHeader.AddCookie("WWUSERID",lcId,"/","NEVER")
    ENDIF            
    
    *** NOTE: We're passing the HTTPHeader header to HTMLHeader method
    
    *** Send Header and make sure to pass the Content Type (loHeader)
    Response.HTMLHeader("Cookie Test","Cookie Test",BACKIMG,loHeader)
    

    Class Members

    MemberDescription
    AddCacheHeader Adds a Cache header to the Request that causes the page to be cached on the client.
    o.AddCacheHeader(lnExpirationSeconds)
    AddCookie Adds an HTTP Cookie to the HTTP request header to be returned to the client.
    o.AddCookie(tcCookie, tcValue, tcPath, tcExpire)
    AddCustom AddCustom works much like AddHeader() in that it generates a custom HTTP header. Some HTTP headers don't conform to the Header/Value pair syntax so this method allows you to specify a complete HTTP header line rather than the key/value pair.
    o.AddCustom(lcCustomHeader)
    addforcereload Adds an immediate expiration to the page so the page will always be forced to reload and not be read from cache.
    o.addforcereload()
    addheader HTTP allows you to specify a number of custom headers for requests that can be interpreted by clients. This is one mechanism for the server to pass down data to the client. Typically the HTTP header is used for standard headers that are supported by browsers. For example, custom types in include things like Expires, Set-Cookie, Pragma and so on. You can also add your totally custom headers in
    o.addheader(lckey,lcValue)
    AddRequestId Adds the Web Connection Current Request ID if available.
    o.AddRequestId()
    authenticate This method creates a self contained HTTP request that asks the browser to present the login dialog into which the user must type the username and password. The password is then validated by the Web server typically against the NT (or Windows) user database. The exact validation varies by Web server - IIS uses the NT User database on the server.
    o.authenticate(lcRealm, lcErrorText)
    ClearHeader Clears all current output in the HTTP header.
    o.ClearHeader
    completeheader When you pass in a Response object when the wwHTTPHeader object is created this object generates the HTTP output every time when you call one of the methods in this class. Once the header is complete it's extremely important that the trailing blank line gets generated and CompleteHeader() is meant to do this.
    o.completeheader()
    defaultheader This method creates a default HTTP header for standard HTML requests. The header looks like this:
    o.defaultheader()
    FileDownloadHeader Returns a standard File Download header. This header causes a file download dialog to be displayed instead of returning the file as content to be displayed in the browser.
    o.FileDownloadHeader(lcFilename,lcContentType,lnFileSize)
    getoutput Retrieves the output of the header if no Response object was passed in. The data is generated into the internal Response object and GetOutput retrieves that output as a string.
    o.getoutput()
    redirect Redirects the current request to another URL. HTTP redirects are an HTTP construct to allow re-routing of requests mid-stream to another URL which is useful in situations where you're for example have multiple paths of operation and you need to go back to another page or request.
    o.redirect(tcUrl)
    SetContenttype Content types determine the type of data that is returned from a request for the benefit of the client application. Browsers rely on content types to figure out how to display incoming data. By default browsers expect to see HTML content which is
    o.setcontenttype(tcContentType)
    setprotocol Sets the protocol for the HTTP header.
    o.setprotocol(tcProtocol)
    cHTTPVersion HTTP Version for the HTTP header.

    Requirements


    wwHTTPHeader::AddCacheHeader


    Adds a Cache header to the Request that causes the page to be cached on the client.

    This method sets the following headers:

    this.AddHeader("Last-Modified",MimeDateTime( DATETIME(),.t.)) this.AddHeader("Expires",MimeDateTime( DATETIME() + lnExpirationSeconds,.t.)) THIS.AddHeader("Cache-Control","public,max-age=" + TRANSFORM(lnExpirationSeconds) )

    These headers cause requests to be cached for the duration specified either by the browser or Proxy.

    IIS 6 or later Kernel Cache

    If using IIS 6 or later, the these headers (Last-Modified and Expired) can trigger Kernel level HTTP.SYS caching of the request which is the most optimal way of caching for requests. Kernel level cache cause cached requests to be served directly from IIS without hitting the ISAPI extension or your FoxPro code again.

    Kernel Mode cache kicks in only fires if there is:


    Full rules for when Kernel Mode Cacing applies is show in this
    KB Article.

    Note that if Kernel Mode cache does not kick in because of a rule violation, you still get the benefit of browser and proxy caching if available for individual clients.

    o.AddCacheHeader(lnExpirationSeconds)                         
    

    Parameters

    lnExpirationSeconds
    The number of seconds to cache the request for.

    wwHTTPHeader::AddCookie


    Adds an HTTP Cookie to the HTTP request header to be returned to the client.

    HTTP cookies can be used to keep client state. A cookie is a piece of data that is stored on the browser to identify the user. You create the cookie as part of the HTTP header with this method and you can then read that cookie from the user by using wwRequest::GetCookie().

    Also see the How To section for Implementing HTTP Cookies.


    What does a Cookie look like?
    An HTTP Cookie is nothing more than an HTTP header fragment that is sent back from the server to the browser. A header containing a Cookie looks like this:

    HTTP/1.0 200 OK
    Content-type: text/html
    Set-Cookie: WWUSERID=43491556; path=/wconnect

    The Set-Cookie command instructs the browser to create the client side cookie. A Cookie can contain path information (in this case /wconnect) and expiration information which in this case is omitted. If the expiration is in the future the cookie is persisted to disk and can persist past a browser shut down. If like above, no expiration is provided the Cookie is considered session specific and goes away when the browser shuts down.


    Creating a permanent Cookie
    To create a permanent Cookie you have to specify a date in the future. The easiest way to to do this is with:

    oHeader.AddCookie("wwuserid",lcID,"/wconnect","NEVER")

    NEVER in this case is translated automatically into a date in the far future. You can also specify a specific date instead of never, but it must be a real date and it must follow GMT naming. For example:

    Sun, 27-Dec-2009 01:01:01 GMT

    Note:
    Dates in the far future beyond this date may cause problems on some browsers. This date works on all tested browsers.

    Cookie Paths
    Cookies are specific to the paths that they are created for. Above I set the cookie to be valid only in the /wconnect virtual directory and down. If you don't specify a path / or the root directory will be used by default. By using the root the Cookie is visible on the entire site.

    Why deal with specific paths? Some sites use lots of cookies and cookie strings are by spec limited to 256 character lengths although both IE and Netscape implement 1k strings or more. By partitioning Cookies into their virtuals you're avoiding overload of cookies and only retrieve the data you need in the appropriate location.



    o.AddCookie(tcCookie, tcValue, tcPath, tcExpire)
    

    Parameters

    tcCookie
    Name of the cookie to create

    tcValue
    The value to set it to.

    tcPath
    The virtual path that it applies to. By default this is the Web's root path, but realisticly you should scope it to the virtual directory that the request is running in. FOr example if you run the following url:

    /wconnect/wc.dll?wwDemo~TestPage

    where /wconnect is a virtual directory the cookie should be scoped to the /wconnect path.

    tcExpire
    Optional - Sets an expiration for the cookie. This must be a fully qualified string:
    For example: Sun, 27-Dec-2009 01:01:01 GMT

    This value can also be NEVER which generates the value above. Note: older browsers have problems with cookies far in the future - the above value works with most 3.x and later browsers.

    Example

    *** Try to retrieve the cookie...
    lcId=Request.GetCookie("WWUSERID")
    
    *** Create Standard Header
    loHeader=CREATEOBJECT("wwHTTPHeader")
    loHeader.DefaultHeader()
    
    *** If not Found
    IF EMPTY(lcId)
       *** Create the cookie
       lcId=SYS(3)
       
       loHeader.AddCookie("WWUSERID",lcId,"/wconnect")
       
       *** To specify a permanent cookie supply NEVER or a specific expiration date
       *** loHeader.AddCookie("WWUSERID",lcId,"/","NEVER")
    ENDIF            
    
    *** Send Header and make sure to pass the Content Type (loHeader)
    Response.ContentTypeHeader(oHeader)
    
    *** more html
    Response.Write("<HTML>Hidi ho</HTML>")
    
    
    *** OR: use one of the following methods which take a header parameter: Response.HTMLHeader("Hidi ho",,,oHeader) Response.ExpandTemplate(THIS.cHTMLPagePath + "nocode.wc",oHeader) Response.ExpandScript(THIS.cHTMLPagePath + "nocode.wcs",oHeader)

    wwHTTPHeader::AddCustom


    AddCustom works much like AddHeader() in that it generates a custom HTTP header. Some HTTP headers don't conform to the Header/Value pair syntax so this method allows you to specify a complete HTTP header line rather than the key/value pair.



    o.AddCustom(lcCustomHeader)                                   
    

    Parameters

    lcCustomHeader
    The full custom HTTP header presented as a line.

    wwHTTPHeader::AddForceReload


    Adds an immediate expiration to the page so the page will always be forced to reload and not be read from cache.

    This method basically sets an Expires: header that is in the past and forces the page to expire immediately. This results in the page always being reloaded and not being cached by proxies or browsers.

    Use this method whereever you hit identical URLs that generate different data or data that changes frequently.

    o.addforcereload()
    

    wwHTTPHeader::AddHeader


    HTTP allows you to specify a number of custom headers for requests that can be interpreted by clients. This is one mechanism for the server to pass down data to the client. Typically the HTTP header is used for standard headers that are supported by browsers. For example, custom types in include things like Expires, Set-Cookie, Pragma and so on. You can also add your totally custom headers in this same fashion. This won't do much

    Adds a custom HTTP header using a key/value string.



    o.addheader(lckey,lcValue)
    

    Parameters

    lckey
    The HTTP header to set. This can be a standard HTTP header or a custom header you make up of your own.

    lcValue
    The actual value to set the key to.

    wwHTTPHeader::AddRequestId


    Adds the Web Connection Current Request ID if available.

    This method is called automatically when GetOutput() is called, so typically this method call is not required.

    o.AddRequestId()                                              
    

    wwHTTPHeader::Authenticate


    This method creates a self contained HTTP request that asks the browser to present the login dialog into which the user must type the username and password. The password is then validated by the Web server typically against the NT (or Windows) user database. The exact validation varies by Web server - IIS uses the NT User database on the server.

    This mechanism works through Basic Authentication which is part of the HTTP protocol.



    o.authenticate(lcRealm, lcErrorText)
    

    Return Value

    nothing

    Parameters

    lcRealm
    Optional - The realm as its known in HTTP terms is the domain name or IP address of the server.

    lcErrorText
    The HTML text to display if an error occurs with login validation. IOW, if the user types in the wrong password this is what they'll see. This text is static.

    Remarks

    In order for an authentication request to succeed on the server Basic Authentication must be enabled on the server. By default this behavior is disabled - see your Web server security settings to enable this functionality.

    Note: Basic Authentication is a non-secure protocol that sits on top of HTTP. Passwords are passed as clear text (although encoded with a simple, easily breakable hash algorithm) and can be easily hijacked with a network sniffer. If you're worried about security make sure that your authentication request runs over SSL/HTTPS - when you do the entire request info is encrypted.

    Also, check out the wwRequest::GetAuthenticatedUser method which allows you to retrieve the authenticated username if the login attempt succeeded.

    wwHTTPHeader::ClearHeader


    Clears all current output in the HTTP header.

    o.ClearHeader                                                 
    

    wwHTTPHeader::CompleteHeader


    When you pass in a Response object when the wwHTTPHeader object is created this object generates the HTTP output every time when you call one of the methods in this class. Once the header is complete it's extremely important that the trailing blank line gets generated and CompleteHeader() is meant to do this.



    o.completeheader()
    

    wwHTTPHeader::DefaultHeader


    This method creates a default HTTP header for standard HTML requests. The header looks like this:

    HTTP/1.0 200 OK
    Content-type: text/html
    
    <html>
    ...
    </html>
    

    Note that every HTTP header must include a blank line after the header and before the actual content of the request starts (<html> in the example above) .



    o.defaultheader()
    

    wwHTTPHeader::FileDownloadHeader


    Returns a standard File Download header. This header causes a file download dialog to be displayed instead of returning the file as content to be displayed in the browser.

    Note this function only creates the header - it does not actually send the file.

    o.FileDownloadHeader(lcFilename,lcContentType,lnFileSize)     
    

    Return Value

    nothing

    Parameters

    lcFilename
    Filename to be send. The filename can be a full path name or a just a filename. It will be truncated to just the name of the file regardless.

    lcContentType
    The content type. ie. Text/Html, Text/txt etc. Optional but highly recommended for reliable downloads.

    lnFileSize
    Optional - the size of the file. Sent down to the client as Content-Length header so client can show progress information

    wwHTTPHeader::GetOutput


    Retrieves the output of the header if no Response object was passed in. The data is generated into the internal Response object and GetOutput retrieves that output as a string.


    o.getoutput()
    

    Return Value

    String - all the content generated from the header including the trailing blank header line.

    wwHTTPHeader::Redirect


    Redirects the current request to another URL. HTTP redirects are an HTTP construct to allow re-routing of requests mid-stream to another URL which is useful in situations where you're for example have multiple paths of operation and you need to go back to another page or request.

    This method clears all existing HTTP output before generating a custom HTTP header.

    You probably should use the wwResponse::Redirect() method instead of this method. wwResponse.Redirect calls this method.

    o.redirect(tcUrl)
    

    Parameters

    tcUrl


    Remarks

    This method cannot add any custom HTTP headers - they will simply be ignored. The Web browser ignores Cookies or any other custom HTTP header directions.

    wwHTTPHeader::SetContenttype


    Content types determine the type of data that is returned from a request for the benefit of the client application. Browsers rely on content types to figure out how to display incoming data. By default browsers expect to see HTML content which is text/html. But you can also return plain text text/plain for example. You can also return binary data such as a PDF document: application/pdf. Zip files, Word Docs, and most other common Windows file formats are represented and have their own content types.

    The browser uses this content type header to figure out what viewer or external application to launch to provide a useful interface to for the data downloaded. SetContentType sets the content type in the HTTP header on the server.



    o.setcontenttype(tcContentType)
    

    Return Value

    nothing

    Parameters

    tcContentType
    The content type to set the output to. Default: text/html

    wwHTTPHeader::SetProtocol


    Sets the protocol for the HTTP header.

    HTTP/1.0 200 OK
    Content-type: text/html
    
    

    The HTTP is the protocol and the 1.0 is the version as specified in the cHTTPVersion property.

    SetProtocol() is meant to be called if you need to create non-standard (ie. non-HTML) HTML headers that are built from scratch. Generally you will not pass any parameters to this method but rather use the default values which are based on the cHTTPVersion.

    If you do specify a parameter you should specify the entire protocol header line like:

    HTTP/0.9 200 OK



    o.setprotocol(tcProtocol)
    

    Parameters

    tcProtocol
    Optional - The full HTTP protocol and response line. HTTP/0.9 200 OK
    If you don't specify this parameter HTTP/1.0 200 OK will be generated. 1.0 is determined by the cHTTPVersion value, which you can override.

    wwHTTPHeader::cHTTPVersion


    HTTP Version for the HTTP header.

    Default Value

    Initial value: DEFAULT_HTTP_VERSION

    Class wwHTMLHeader


    The wwHTMLHeader class is used to customized the section between the <HTML> and <BODY> tags of the top of the document. As such it typically handles the following:

    Typically you'll use this class in combination with the
    wwResponse::HTMLHeaderEx method to serve the header. Using this object and the wwHTTPHeader object gives you full control over the top of an HTML without handcoding the entire string.

    The following is an example of how this class can be used:

    FUNCTION Headertest
    
    loHTMLHeader = CREATEOBJECT("wwHTMLHeader",Response)
    loHTTPHeader = CREATEOBJECT("wwHTTPHeader",Response)
    
    loHTTPHeader.DefaultHeader()
    loHTTPHeader.AddForceReload()
    loHTTPHeader.AddCookie("laston",TTOC(DATETIME()))
    
    loHTMLHeader.AddTitle("Header Test Page")
    loHTMLHeader.AddPageRefresh(Request.GetCurrentUrl(),5)
    loHTMLHeader.AddJavaScript([function test() {] + CRLF + [alert("test"); } ] )
    loHTMLHeader.AddVBScript([Sub test ] + CRLF + [MsgBoxt("test") ] + CRLF + [end Sub])
    loHTMLHeader.AddScriptLink("funclib.js","JavaScript")
    loHTMLHeader.AddStyleSheet("/westwind.css")
    loHTMLHeader.AddStyleSheetBlock("headertest {font:normal normal 10pt arial}")
    loHTMLheader.cBodyTag = [<body bgcolor="#FFFFFF">]
    
    Response.HTMLHeaderEx(loHTMLHeader,loHTTPHeader)
    
    *** This is 
    Response.Write("<h1>Header Test Page</h1>")
    Response.Write("<hr>" + TIME() )
    Response.HTMLFooter()
    
    ENDFUNC
    

    This generates the following HTTP document (including the HTTP header):
    HTTP/1.0 200 OK
    Content-type: text/html
    Expires: -1
    Set-Cookie: laston=07/28/2000 01:02:18 PM; path=/
    
    <html><head>
    <title>Header Test Page</title>
    <META HTTP-EQUIV="Refresh" CONTENT="5; URL=http://localhost/wconnect/headertest.wwd?">
    <LINK rel="stylesheet" type="text/css" href="/westwind.css">
    <script language="JavaScript">
    function test {
    alert("test"); } 
    </script>
    <script language="VBScript">
    function test 
    MsgBoxt("test") 
    end func
    </script>
    </head>
    <body bgcolor="#FFFFFF">
    <h1>Header Test Page</h1><hr>13:02:18
    <p></BODY>
    </HTML>
    

    The example above generates the output directly into the Response object and the passes it to HTMLHeaderEx, which build the HTTP and HTML headers for the docuemnt. However, you don't have to use this function. Another simple way to create a header is:

    Response.ContentTypeHeader()  && Create default HTTP Header
    
    oHeader = CREATE("wwHTMLHeader")
    oHeader.AddStyleSheet("westwind.ccs")
    oHeader.AddTitle("My Test Page")
    
    Response.Write( oHeader.GetOutput() )


    Class Members

    MemberDescription
    AddJavaScript Adds a block of JavaScript to the <head> section. This method can be called multiple times to add multiple blocks of script.
    o.AddJavaScript(lcScript)
    AddMetaTag Generic method that creates a <META> tag in the HTML header.
    o.AddMetaTag(lcName, lcValue)
    AddPageRefresh Adds a Meta Tag that automatically refreshes the current page after the specified amount of seconds. You can either refresh the current page (default) or go to another URL.
    o.AddPageRefresh(lcUrl, lnRefresh)
    AddScriptLink Adds a link to an external scripting file that is embedded into the header of the document.
    o.AddScriptLink(lcUrl, lcLanguage)
    AddStyleSheet Creates an external link to a Cascading Style Sheet via the <LINK> element.
    o.AddStyleSheet(lcUrl)
    AddStyleSheetBlock Adds a block of style tags into the header. Multiple calls to this method can be made for multiple style tags.
    o.AddStyleSheetBlock(lcStyleBlock)
    AddTitle Sets the document's title, which is displayed in the browser's title bar. Creates the <title> tag.
    o.AddTitle(lcTitle)
    AddVBScript Adds a block of VBScript to the <head> section. This method can be called multiple times to add multiple blocks of script.
    o.AddVBScript(lcScript)
    DefaultHeader Creates a default header including the title.
    o.DefaultHeader(lcTitle)
    GetOutput Combines the various header components and returns the output generated by the header object. This method must be called to generate the header.
    o.GetOutput()
    Init The INIT of the header class can optionally be used to pass in an optional Response object to generate output into. If no Response object is supplied the header creates its own.
    o.Init(loHTML)
    cBodyTag Allows you to customize the way the <BODY> tag is generated. The default is <BODY> - if you need additional tags change this property to your needs.
    cDocType Allows adding of a doc type header to the HTML document. This header is used by HTML parsers to determine the level of compatibility with HTML standards.
    cHTMLTag Creates the <HTML> tag of the document. Although this should never have to change this tag is the top most portion of the HTML document so if there's something you need to add there you can do it with this tag.

    Requirements


    wwHTMLHeader::AddJavaScript


    Adds a block of JavaScript to the <head> section. This method can be called multiple times to add multiple blocks of script.

    o.AddJavaScript(lcScript)
    

    Parameters

    lcScript
    Script block

    wwHTMLHeader::AddMetaTag


    Generic method that creates a <META> tag in the HTML header.

    Meta tags look like this in the <HEAD> section:

    <META Keywords="Web Conection,Foxpro">
    <META Content="West Wind Web Connection Homepage...">


    o.AddMetaTag(lcName, lcValue)
    

    Parameters

    lcName
    Name of the meta tag to create. For example: Keywords, Content etc.

    lcValue
    The value to set the meta tag to.

    wwHTMLHeader::AddPageRefresh


    Adds a Meta Tag that automatically refreshes the current page after the specified amount of seconds. You can either refresh the current page (default) or go to another URL.

    This method generates:

    <META HTTP-EQUIV="Refresh" CONTENT="5; URL=/headertest.wwd?">



    o.AddPageRefresh(lcUrl, lnRefresh)
    

    Parameters

    lcUrl
    The URL to redirect to. If not specified the currentUrl is refreshed

    lnRefresh
    The refresh interval in seconds. Default: 1 second


    Remarks

    This functionality is also provided by
    wwProcess::StandardPage and wwProcess::ErrorMsg using an optional parameter.

    wwHTMLHeader::AddScriptLink


    Adds a link to an external scripting file that is embedded into the header of the document.

    o.AddScriptLink(lcUrl, lcLanguage)                            
    

    Parameters

    lcUrl
    URL to load the script file from.

    lcLanguage
    Optional - Scripting language (VBScript, JavaScript) that the script block is in. Default is blank which defaults to the browser's default most likely javascript.

    wwHTMLHeader::AddStyleSheet


    Creates an external link to a Cascading Style Sheet via the <LINK> element.

    Generates the following:

    <LINK rel="stylesheet" type="text/css" href="/westwind.css">


    o.AddStyleSheet(lcUrl)
    

    Parameters

    lcUrl
    The URL where the stylesheet is located. Can be a full URL or relative to the current document.

    wwHTMLHeader::AddStyleSheetBlock


    Adds a block of style tags into the header. Multiple calls to this method can be made for multiple style tags.

    o.AddStyleSheetBlock(lcStyleBlock)                            
    

    Parameters

    lcStyleBlock
    A block of style sheet text. This can be either a full block of a Style section or and individual style tag description.

    wwHTMLHeader::AddTitle


    Sets the document's title, which is displayed in the browser's title bar. Creates the <title> tag.

    o.AddTitle(lcTitle)
    

    Parameters

    lcTitle
    Title for the document

    wwHTMLHeader::AddVBScript


    Adds a block of VBScript to the <head> section. This method can be called multiple times to add multiple blocks of script.

    o.AddVBScript(lcScript)
    

    Parameters

    lcScript
    Script block.

    wwHTMLHeader::DefaultHeader


    Creates a default header including the title.

    This method will generate a standard header and allow you to add to it. At this time this method only optionally adds the title so not much value is provided by it.

    o.DefaultHeader(lcTitle)
    

    Parameters

    lcTitle
    The title to be displayed in the header.

    wwHTMLHeader::GetOutput


    Combines the various header components and returns the output generated by the header object. This method must be called to generate the header.

    It causes the header to be generated from all of its components and return the result back either as a string or directly into a Response object if passed during the Init.

    o.GetOutput()
    

    wwHTMLHeader::Init


    The INIT of the header class can optionally be used to pass in an optional Response object to generate output into. If no Response object is supplied the header creates its own.



    o.Init(loHTML)
    

    Parameters

    loHTML
    A wwResponse object that output is written to directly.


    Example

    Function SomeProcessMethod
    
    Response.ContentTypeHeader()
    
    oHeader = CREATE("wwHTMLHeader",Response)
    oHeader.AddTitle("New Page")
    oHeader.AddStyleSheet("/westwind.css")
    oHeader.cBodyTag = [<body bgcolor="#FFFFFF">]   
    
    *** Commits the output into the Response object
    oHeader.GetOutput()
    
    *** or you can return a string:
    oHeader = CREATE("wwHTMLHeader")
    ...
    Response.Write( oHeader.GetOutput() ) 
    

    wwHTMLHeader::cBodyTag


    Allows you to customize the way the <BODY> tag is generated. The default is <BODY> - if you need additional tags change this property to your needs.

    Default Value

    Initial value: <body>

    wwHTMLHeader::cDocType


    Allows adding of a doc type header to the HTML document. This header is used by HTML parsers to determine the level of compatibility with HTML standards.




    o.cDocType
    

    Default Value

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

    wwHTMLHeader::cHTMLTag


    Creates the <HTML> tag of the document. Although this should never have to change this tag is the top most portion of the HTML document so if there's something you need to add there you can do it with this tag.

    o.cHTMLTag                                                    
    

    Class wwSession


    Tracking users through a site by keeping state information in a Session table.

    The wwSession class provides a state mechanism for tracking people through a 'visit' of a site. The most common example would be to track a customer through a shopping site. Since HTTP is stateless you have no way to track the user from request to request without using some client side mechanism for tracking the user. There are several ways to accomplish this: Using ID tags on every request to pass forth information on every URL and Form submission or by using HTTP cookies which are stored in the browser to keep track of the user. Sessions are a concept that extend use of an HTTP cookie and extend the concept through use of an object that can store more complex data in a database. The only information stored in the HTTP cookie is an ID number.

    wwSession uses a table to track user session information and can use either Fox tables or SQL server. Because of the table format Web Connection Sessions can work across machines.

    Class Members

    MemberDescription
    CreateTable This method creates a new session table if one doesn't exist yet or if you want to create a new one manually. This method is called automatically when a session is accessed.
    o.CreateTable()
    DeleteSession Physically removes a Session record from the session table. Note that this behavior is different than EndSession() which times out a session record.
    o.DeleteSession(lcId)
    EndSession Shuts down the current session for the user.
    o.EndSession(lcSessionId)
    GetField
    Retrieves a value from a field in the session table. This method mainly exists for your own custom fields that you might add to the session table for performance or query purposes. Since fields can be queried directly via SQL commands and VPF data commands they perform much better than Session variables.
    o.GetField(lcFieldName)
    GetSessionId Retrieves the user's current SessionId. This value is valid only after you've made a call to IsValidSession, NewSession or LocateSession.
    o.GetSessionId()
    GetSessionVar Returns a session variable from the Session object.
    o.GetSessionVar(lcVarName)
    GetUserId Returns the value from the UserID field of the current session. Note, a user ID is a manually assigned value, which typically is used to cross reference an application key such as a PK into a customer table. The Session ID is a random generated key while the user ID tends to be a user assigned value.
    o.GetUserId()
    IsValidSession
    This method is responsible for verifying a session id. It looks for the ID in the Session table and if found returns .T. other .F. It also loads the oData member with the content of the session record.
    o.IsValidSession(lcSessionId)
    LocateSession Low level method that tries to load a session and set the oData member. If found the oData member is set. If not found the oData member is set to a blank record. This is a low level method and it should usually not be called directly. Use IsValidSession instead which calls this method and then updates the timestamp and browser info.
    o.LocateSession(lcID)
    NewSession
    This method creates a new Session record for the current user. New sessions should be created when the session doesn't already exist. The following example demonstrates:
    o.NewSession(lcUserId)
    OpenTable Low level method used to open the session table.
    o.OpenTable()
    Reindex Packs and reindexes the session file. You can optionally specify a cut off date/time before which entries will be deleted to allow clearing out old session detail.
    o.Reindex(ltPurgeDateTime)
    SetField Sets the value of a custom field in the Session table.
    o.SetField(lcFieldName, lvValue)
    SetSessionID Overrides the current SessionID's value to a new value.
    o.SetSessionID(lcID)
    SetSessionVar Sets a Session variable to a specified value. All values must be passed in as strings.
    o.SetSessionVar(lcVarName, lcValue)
    SetUserId Note, a user ID is a manually assigned value, which typically is used to cross reference an application key such as a PK into a customer table. The Session ID is a random generated key while the user ID tends to be a user assigned value.
    o.SetUserId(lcUserId)
    TimeoutSessions Deletes all sessions that have timed out.
    o.TimeoutSessions(lnSeconds)
    cDataPath Name of the path where the table lives.
    cSessionID The Session ID of the currently accessed session.
    cTableName Name of the table. Note this should be just the table name. Use cDataPath for explicit pathing on the table.
    lDontSaveSession You can set this flag to cause an active Session not write out the session content when the object goes out of scope.
    lNoFileCheck Performance operation that skips the check for the file's existence before opening it when set to .T. Since sessions are accessed on every hit, if you know you have the table already in place there's no need to check for it.
    nSessionTimeout The timeout value in seconds for a user's session. If the session is idle for this amount of seconds the session is deleted.
    oRequest Optional: An instance of the wwRequest object. This object is used when creating a new session to retrieve the client's IP address and browser and store it in the IP and Browser fields respectively.

    Requirements


    How it works


    The wwSession class provides session management (or said another way - user tracking) to your Web Connection applications. The wwSession class is actually fairly low level and doesn't provide for management of the Cookie or ID, but rather providers merely the mechanism for storing and retrieving session information. Using wwSession in combination with wwProcess::InitSession or wwWebPage.lEnableSessionState makes the session usage automatic.

    wwProcess::InitSession is the easiest way

    The easiest way to utilize Sessions in Web Connection is by using the
    wwProcess::InitSession()method which combines the Cookie management and Session management in a single place. If you're using the Web Control Framework setting lEnableSessionState to .T. before you call the Session object - preferrably in the class definition - has the same effect.

    With one of these methods/property called the Session object becomes available as a Session variable on which you can call Session.GetSessionVar() or Session.SetSessionVar() to retrieve or set data that is persistent for a given user of your Web site.

    To use InitSession simply call it in the startup code of your Process class:

    FUNCTION OnProcessInit() Process.InitSession("wwDemo") Session = THIS.oSession DODEFAULT() RETURN

    or you can call Process.InitSession anywhere explicitly before accessing the Session object

    If you use Sessions globally in most or every request then setting up default Session handling in OnProcessInit() is preferrable because then the Session object is ALWAYS available.

    If you don't use them globally and you use Web Control Framework pages you can choose to enable Session state on a per page level by setting the lEnableSessionState property in the class definition:

    DEFINE CLASS MyWebPage : wwWebPage lEnableSessionState = .T. FUNCTION OnLoad() ... ENDFUNC ENDDEFINE

    Once initialized the Session object becomes available on the process object as a PRIVATE Session variable.

    FUNCTION YourProcessMethod lcSessionVar = Session.GetSessionVar("MyVar") IF EMPTY(lcSessionVar) *** Not set Session.SetSessionVar("MyVar","Hello from a session. Created at: " + TIME()) lcSessionVar = THIS.oSession.GetSessionVar("MyVar") ENDIF THIS.StandardPage("Session Demo","Session value: " + lcSessionVar) RETURN

    InitSession and simply using the Session object works fine as long as you use one of the standard HTTP header routines on the Response object. See the InitSession topic for more details.

    Manual Session management

    In order to make wwSession more flexible it is independent of how the Session ID is managed. This makes it possible for example to use URL provided IDs instead of cookies to manage the Session Id.

    With manual sessions you can determine when to assign users new sessions, such as after they have logged into some sort of logon page only. Otherwise new sessions may not be assigned. This allows you to create sessions based on application logic such as whether a user logged in.

    IsValidSession() is the method used to verify the existance of a session Id and this code tends to be central to an application or applet. For this reason, the Session checks and assignments tend to be made in code that runs in your subclass of the wwProcess::Process() method. NewSession() creates a new session for a user and you'll typically use this method following a call to IsValidSession() that failed. You can also branch off to user validating code first before then calling NewSession() to assign a new ID.

    With the use of HTTP Cookies or Ids passed as part of the URL you can track people through a site. This class provides a mechanism for storing and retrieving session specific values for a particular user by creating an entry in a table that has a timeout. Sessions use a single HTTP cookie which you still have to manage manually and pass through this session object. However, with the session class this process is very simple (see code below).

    Methods for creating, reading and timing out the session are provided. You can generically attach character variables to a session object, the values of which are stored in a free form memo field using the SetSessionVariable() method. The variables can be retrieved at any time with the GetSessionVariable() method. The data is stored in fragment XML format in the memo field.

    How it works

    The wwSession object is based on a table, which stores all the session information in its fields. The table is driven through a SessionId, which is typically driven through an HTTP cookie, but can be any mechanism you see fit to track a user.

    CREATE CURSOR WWSESSION ;
    (    SESSIONID   C (9),;
       USERID      C (15),;
       FIRSTON     T ,;
       LASTON      T ,;
       VARS        M ,;
       BROWSER     M ,;
       IP          M ,;
       HITS    I,;
       CDATA1      M ,;
       CDATA2      M )

    The table is created the first time you try to use any of the methods of the session object - you can use the Reindex() method to clean up and purge the session file. The file can be extended with your own custom fields and you can use SetField() and GetField() to retrieve these customfield values (as well as the full content of the ones listed above).

    The SessionId is the unique identifier for each session. Using the NewSession() method a new ID is generated and an entry is created in the table. The resulting SessionId should be used as the Cookie you pass back to the browser. On the next hit you can check for this Cookie and use it to check for a valid session using the IsValidSession() method.

    wwSession doesn't explicitly use the UserId field, but you can assign this field as a parameter of NewSession() and use it to cross reference a Session user with a user in an actual user table or security descriptor table. To optionally log the Browser and IP address you can assign a reference to a wwCGI object to the oCGI property. CDATA1 and CDATA2 are not used but can be used by you to store additional data without explicitly adding fields.

    The following example uses a wwSession object:

    * wwDemo :: SessionDemo FUNCTION SessionDemo LOCAL loSession, lcCookie, loHeader, lnCount *** Create an instance of the session loSession=CREATE("wwSession") loSession.nSessionTimeout= 300 && Seconds - 5 minutes *** Username from the HTML form (optional) lcName = Request.Form("txtName") *** Retrieve the HTTP Cookie lcCookie=Request.GetCookie("WWDEMOID") *** We'll need to create a custom HTTP header so we can potentially *** add the Cookie to it loHeader=CREATE("wwHTTPHeader") loHeader.DefaultHeader() *** Check if we have a valid Session from our Cookie IF !loSession.IsValidSession(lcCookie) *** Assign Request object to session so NewSession *** can read IP and Browser and store it in table loSession.oRequest = Request *** Nope - we have to create the session lcCookie=loSession.NewSession() *** And add a Cookie to the Request Header loHeader.AddCookie("WWDEMOID",lcCookie) ENDIF *** Save name and company in the Session IF !EMPTY(lcName) loSession.SetSessionVar("Name",lcName) ELSE lcName = loSession.GetSessionVar("Name") ENDIF *** Now let's deal with our counter demo code *** If counter doesn't exist VAL will return 0 which is OK here lnCount = VAL( loSession.GetSessionVar("wwDemoCounter") ) lnCount = lnCount + 1 *** Note we have to pass loHeader as a parameter since we might have *** added a cookie to it in this request. THIS.StandardPage("Session Demo",; IIF(!EMPTY(lcName),"Welcome " + lcName + "!","*** Base Class Abstract Methods") + "<p>" + CRLF + ; "<b>You've hit this page: " + TRANS(lnCount) + " times.<br>"+ CRLF +; "Your Session ID is: " + loSession.cSessionId + ".</b><p>"+ CRLF +; "The counter should keep increasing as you refresh this page. "+CRLF +; "To start a new session start up a new instance of your browser or shut down "+CRLF +; "the browser and restart it and then come back to this page. The counter will " + CRLF +; "start over at 0. This session will time out after 5 minutes of inactivity.<p>" +CRLF +; [<a href="wc.wc?wwDemo~ShowSession">Click here</a> to see the content of your session] ,; loHeader) *** Update the counter and write it back into the session loSession.SetSessionVar("wwDemoCounter",TRANSFORM(lnCount)) RETURN ENDFUNC

    Typically the Session Check code and new assignment (the IsValidSession() and NewSession() block) is required on every request, so this should probably live in the your subclassed version of wwProcess::Process() including the header generation. Moving the code there makes every request in the application check for the session rather than having to perform the check in all the request methods.

    This sort of code is usually not required. Note that you can specify a SessionKey, Timeout and permanent persistence in InitSession, so there really should be very few scenarios where manual session management is required.


    Using SQL Server for Sessions with the wwSessionSQL Class


    wwSession uses FoxPro tables to run the session management features. A subclass - wwSessionSQL - uses SQL Server tables underneath everything. To use the wwSessionSQL class set the oSQL member to a reference of a wwSQL object.

    oSQL = CREATE("wwSQL")
    oSQL.Connect("dsn=WestWind;uid=sa;pass=;)
    
    oSession = CREATE("wwSessionSQL")
    oSession.oSQL = oSQL
    

    From thereon in the session behaves identically to a session running against Fox tables.

    Alternately SQL Sessions can be established through the InitSession mechanism by telling Web Connection to use SQL Server tables for logging and Session support. See Configuring Web Connection for use with SQL Server for details on how to set up the tables and let Web Connection handle logging and session management into SQL Server tables.

    wwProcess::InitSession automatically loads the correct session object based on the WCONNECT.H constant WWC_USE_SQL_SYSTEMFILES.


    wwSession::CreateTable


    This method creates a new session table if one doesn't exist yet or if you want to create a new one manually. This method is called automatically when a session is accessed.

    The Session table is created with the following structure:

    CREATE TABLE ( THIS.cDataPath+THIS.cTableName ) FREE;
      (SESSIONID    C (9),;
       USERID      C (15),;
       FIRSTON     T ,;
       LASTON      T ,;
       VARS        M ,;
       BROWSER     M ,;
       IP          M ,;
       HITS		   I ,;
       CDATA1      M ,;
       CDATA2      M )
    

    You can add additional fields to this table for high frequency values that are set - direct access to fields is more efficient than session variables especially if you need to query information later on for statistical reports. Any fields you add are accessible with the SetField() and GetField() methods.



    o.CreateTable()
    

    Parameters

    None, but uses the cDataPath and cTableName to figure out where to create the file.

    wwSession::DeleteSession


    Physically removes a Session record from the session table. Note that this behavior is different than EndSession() which times out a session record.

    This method should be used sparingly.

    o.DeleteSession(lcId)                                             
    

    Return Value

    nothing

    Parameters

    lcId
    Optional - Session ID to kill.
    Default: current SessionID

    Remarks

    Moves the record pointer to the end of the Session file. Make sure your relocate your session with LocateSession() if necessary.

    wwSession::EndSession


    Shuts down the current session for the user.


    o.EndSession(lcSessionId)
    

    Return Value

    .T. or .F.

    Parameters

    lcSessionId
    The user's session ID to shut down.

    wwSession::GetField



    Retrieves a value from a field in the session table. This method mainly exists for your own custom fields that you might add to the session table for performance or query purposes. Since fields can be queried directly via SQL commands and VPF data commands they perform much better than Session variables.

    I suggest that for frequently used fields that require statistical queries later on you add a field instead of using a session var.


    o.GetField(lcFieldName)
    

    Return Value

    Value of the field.

    Parameters

    lcFieldName
    Name of the field to retrieve the value from

    Example

    lcValue = loSession.GetField("Hits")

    wwSession::GetSessionId


    Retrieves the user's current SessionId. This value is valid only after you've made a call to IsValidSession, NewSession or LocateSession.

    o.GetSessionId()
    

    wwSession::GetSessionVar


    Returns a session variable from the Session object.


    o.GetSessionVar(lcVarName)
    

    Return Value

    String value of the session variable or "" if it doesn't exist.

    Parameters

    lcVarName
    Name of the session variable to retrieve.

    wwSession::GetUserId


    Returns the value from the UserID field of the current session. Note, a user ID is a manually assigned value, which typically is used to cross reference an application key such as a PK into a customer table. The Session ID is a random generated key while the user ID tends to be a user assigned value.

    Set the user with SetUserId() after a call to NewSession().

    o.GetUserId()
    

    Return Value

    User ID string

    wwSession::IsValidSession



    This method is responsible for verifying a session id. It looks for the ID in the Session table and if found returns .T. other .F. It also loads the oData member with the content of the session record.

    The logic typically goes like this:

    lcID = Request.GetCookie("wwDemoCookie")
    
    loSession = CREATE("wwSession")
    
    loHeader = CREATE("wwHTTPHeader")
    loHeader.DefaultHeader()
    
    IF !loSession.IsValidSession(lcID)
       lcID = loSession.NewSession()
    
       *** Add the cookie to the HTTP header
       loHeader.AddCookie("wwDemoCookie",lcId)
    ENDIF
    
    
    *** Make sure the cookie gets written
    Response.ContentTypeHeader(loHeader)
    
    loSession.GetSessionVar("SomeValue")
    lcUserID = loSession.oData.UserID   && Static fields of the table through oData member
    
    ... more HTML here
    
    RETURN

    If IsValidSession() returns .F. you typically will want to create a new session and also add a Cookie (or other tracking mechanism) with the newly generated session id.

    IsValidSession calls the lower level LocateSession() and then sets the LastOn, Hits and SessionId fields of the oData member. You should not call LocateSession directly unless you don't need these update operations.

    o.IsValidSession(lcSessionId)
    

    Return Value

    .T. or .F.

    Parameters

    lcSessionId
    The Session ID to check out. Typically this value will come from an HTTP cookie or a URL based ID passed down from the URL that's consistent through out the application.

    wwSession::LocateSession


    Low level method that tries to load a session and set the oData member. If found the oData member is set. If not found the oData member is set to a blank record. This is a low level method and it should usually not be called directly. Use IsValidSession instead which calls this method and then updates the timestamp and browser info.

    o.LocateSession(lcID)                                            
    

    Parameters

    lcID

    wwSession::NewSession



    This method creates a new Session record for the current user. New sessions should be created when the session doesn't already exist. The following example demonstrates:

    lcID = Request.GetCookie("wwDemoCookie")
    
    loSession = CREATE("wwSession","wwDemoSession")
    
    loHeader = CREATE("wwHTTPHeader")
    loHeader.DefaultHeader()
    
    IF !loSession.IsValidSession(lcID)
       lcID = loSession.NewSession()
    
       *** Add the cookie to the HTTP header
       loHeader.AddCookie("wwDemoCookie",lcId)
    ENDIF
    
    
    *** Make sure the cookie gets written
    Response.ContentTypeHeader(loHeader)
    
    ... more HTML here
    
    RETURN

    NewSession() tends to be preceeded by a call to IsValidSession() to determine whether a new session needs to be created for the user. Obviously you have more control about this code such as first going to a login page and then assigning the new session after the user logged in successfully.



    o.NewSession(lcUserId)
    

    Return Value

    String of the new session ID

    Parameters

    lcUserId
    Optional - a user id to store in the new session.

    wwSession::OpenTable


    Low level method used to open the session table.

    o.OpenTable()
    

    Return Value

    .T. or .F.

    Parameters

    None but use cTableName and cDataPath

    wwSession::Reindex


    Packs and reindexes the session file. You can optionally specify a cut off date/time before which entries will be deleted to allow clearing out old session detail.


    o.Reindex(ltPurgeDateTime)
    

    Parameters

    ltPurgeDateTime
    Optional - This is the cut off time, before which entries from the session will be deleted.

    If not passed no entries are deleted, the file is just reindexed and packed.

    wwSession::SetField


    Sets the value of a custom field in the Session table.

    o.SetField(lcFieldName, lvValue)
    

    Parameters

    lcFieldName
    Name of the field to set. This name must match an actual field name.

    lvValue
    The value to set the field to.

    wwSession::SetSessionID


    Overrides the current SessionID's value to a new value.

    o.SetSessionID(lcID)                                             
    

    Return Value

    nothing

    Parameters

    lcID
    ID to set the current session entry to

    Remarks

    Use with care. When changing a session ID you are basically changing the way that the session is looked up. Session IDs correspond to the HTTP Cookie value, so you will have to override the cookie as well. Inside of a Web Connection request you can do the following:

          lcSessionID = "IDNew111111"
          Session.LocateSession(Session.cSessionID)
          Session.SetSessionId(lcSessionID)
          
          *** Force a new Cookie to be written with that value when page is built
          Response.cAutoSessionCookieName = Config.cCookieName
          Response.cAutoSessionCookie = loCustomer.oData.UserID
          Response.lAutoSessionCookiePersist = .T.
    

    wwSession::SetSessionVar


    Sets a Session variable to a specified value. All values must be passed in as strings.



    o.SetSessionVar(lcVarName, lcValue)
    

    Parameters

    lcVarName
    Name of the Session Variable to set

    lcValue
    The value to set the session variable to

    wwSession::SetUserId


    Note, a user ID is a manually assigned value, which typically is used to cross reference an application key such as a PK into a customer table. The Session ID is a random generated key while the user ID tends to be a user assigned value.

    Set the user with SetUserId() after a call to NewSession().

    o.SetUserId(lcUserId)
    

    Parameters

    lcUserId
    The User Id to assign to this session.

    wwSession::TimeoutSessions


    Deletes all sessions that have timed out.

    o.TimeoutSessions(lnSeconds)                                     
    

    Return Value

    nothing

    Parameters

    lnSeconds
    Seconds before which the sessions are timed out. If not specified uses nSessionTimeout.

    wwSession::cDataPath


    Name of the path where the table lives.

    Default: .\ (current path)

    Default Value

    Initial value: .\

    wwSession::cSessionID


    The Session ID of the currently accessed session.

    wwSession::cTableName


    Name of the table. Note this should be just the table name. Use cDataPath for explicit pathing on the table.

    Default Value

    Initial value: wwSession

    wwSession::lDontSaveSession


    You can set this flag to cause an active Session not write out the session content when the object goes out of scope.

    This method is provided for performance enhancement features on applications that use InitSession() which automatically read the session and then save it when the object goes out of scope. You can use this property to keep methods that don't actually use the session object from writing the session data back to the database when the session is not actually used during a process method call.

    o.lDontSaveSession                                               
    

    wwSession::lNoFileCheck


    Performance operation that skips the check for the file's existence before opening it when set to .T. Since sessions are accessed on every hit, if you know you have the table already in place there's no need to check for it.

    Default Value

    Initial value: .F.

    wwSession::nSessionTimeout


    The timeout value in seconds for a user's session. If the session is idle for this amount of seconds the session is deleted.

    Default: 1800 - 1/2 hour

    Default Value

    Initial value: 1800

    wwSession::oRequest


    Optional: An instance of the wwRequest object. This object is used when creating a new session to retrieve the client's IP address and browser and store it in the IP and Browser fields respectively.

    Note: This property is useful only when calling NewSession() and must be set prior to the call to NewSession.

    Default Value

    Initial value: .NULL.

    Class wwShowCursor


    Contained in wwShowCursor.prg

    The wwShowCursor class allows for easy display of table based data in HTML form. Its main functionality is to show a FoxPro table as HTML table output for a list display using a single method call. There are also methods for displaying a single record either as ASCII output (handy for email messages) or as an HTML table for display on an HTML page.



    Remarks

    IMPORTANT: By default the various Show methods use the currently active cursor WHEN THE OBJECT IS CREATED!!! So make sure you issue the object creation code following the cursor selection or SQL statement of the cursor you want to render. Alternately you can call BuildFieldListHeader() explicitly to 'reselect' the currently active cursor.

    Class Members

    MemberDescription
    BuildFieldListHeader This method allows you to pass a set of headers as an array to the object prior to calling one of the Show... methods.
    o.BuildFieldListHeader(lvHeader,llPrelist)
    EditRecord This method displays all fields of the table in editboxes for editing. The name of the HTML field matches the field name so you can retrieve any results with Request.Form("FieldName").
    o.EditRecord(lnDisplayMode,lcObjectName)
    EditTable The EditTable method can handle full table editing with a single method call.
    o.EditTable()
    GetOutput If you don't pass in an existing wwResponse object, this method will return the output of the generated HTML. If you don't pass in a wwresponse object, wwShowCursor creates one internally and GetOutput() returns the output as a string.
    o.GetOutput()
    SaveRecord Saves HTML form variables with the same names as the actual database fields into the currently open record of the database. This method is meant to be used in response to a EditRecord() created HTML form which contains all fields.
    o.SaveRecord()
    SetCursor Use this method to set a DBF or cursor other than the currently selected Alias
    o.SetCursor(lcDBF)
    ShowASCIIRecord Shows all the fields of the current record in ASCII format. This is useful for sending emails or other operations that can't use HTML as the presentation format.
    o.ShowASCIIRecord()
    ShowCursor Renders a cursor to HTML into the internal Response object. This method generates the list, but you have to use GetOutput() to retrieve a string.
    o.ShowCursor()
    ShowObject Displays an object's property values as an HTML table.
    o.ShowObject(loObject)
    ShowRecord Shows all the fields of the current record in an HTML table.
    o.ShowRecord()
    cAlternatingBGColor The background color used for alternating rows if lAlternateRows is set to .T.. The colors will alternate between this color and cTableBGColor.
    cBaseUrl Property that determines the base Url of the ShowCursor request.
    cCellPadding Cell Padding for the table.
    cCellSpacing Cell Spacing for the table.
    cExtraTableTags Any extra TABLE tags that are to be appended to the <TABLE> statement.
    cHeaderBGColor Background color for the table header. Use HTML color names or Hex color values.
    cHeaderColor The color of the header font.
    cHeaderFont The font used for the table header that shows the field labels.
    cKeyField The type of the key field used to identify records. Default is "N" for numeric.
    cKeyFieldType VFP Type for the key field used in EditTable(). Defaults to "N" numeric. Used only by EditTable and is required for any update operations.
    cPage_PageURL The base URL used for paging. This URL will be appended with additional values for page count and total recs.
    cRecordFieldList Field list filter used when displaying a single record with ShowRecord(). List should be a comma-delimited list of fields or field expressions that are to be displayed. This value becomes the field list value in a SQL Cursor SELECT statement.
    cTableBGColor Background for the HTML table.
    cTableBorder Border width of the table as a string value. Default = 1.
    cTableEditFieldList Allows you to provide a comma delimited list of fields that are to be displayed in edit view. If not passed all fields in the current cursor will be displayed.
    cTableFieldList This property determines the field list of fields to be displayed in ShowCursor() list display.
    cTableSortColumn Used if you provide a customized field list in cTableFieldList to determine the sortorder. Choose the column name of the table or a numeric value of the column if a complex field.
    cTableTitle Header for the table.
    cTableWidth Width of the table as a string. Use real pixel value or a % value.
    lAlternateRows Property that determines whether the ShowCursor display varies the color of odd and even rows for a ledger look.
    lCenterTable Centers the table if set.
    lShowAsTable Determines whether the table is displayed using a table or <pre> tags.
    lSortable Allows columns in the data list view (ShowCursor()) to be sorted by showing the column headers with Ascending and Descending links next to them.
    lSumNumerics If .T. all numeric fields are summed and totalled on the bottom of the table.
    nForceToPreList This number is the number of cells that are the limit of the size for the table displayed. If this number of cells is exceeded the table will automatically revert to a <pre> formatted list.
    nPage_ItemsPerPage If in paging mode this property determines how many items to display per page.
    nPage_ShowPage Use the nPage_ShowPage property to go to a specific page in the paging list. This parameter is optional and is retrieved from the URL containing a PAGE= value if not specified. If neither exists the first page is displayed.

    Requirements


    How it works


    The wwShowCursor class builds output on the fly from the currently selected Visual FoxPro workarea. The class takes an optional Response object as an input parameter to the Constructor (init()) so you can send output to the selected HTML output source in an optimized fashion. If no wwResponse object is passed an wwHTMLString object is created internally and the output is returned via this self-created object in which case you can use the GetOutput() method to retrieve the generated string output.

    The code to render a table as HTML looks like this:

    Response.HTMLHeader("ShowCursor Test")
    
    SELECT Company, CareOf as Full_Name ;
        FROM TT_CUST ;
        INTO CURSOR Tquery
    
    loShowCursor=CREATE("wwShowCursor",Response)
    loShowCursor.ShowCursor()
    Response.HTMLFooter()
    

    Using an internal Response object looks like this:

    Response.HTMLHeader("ShowCursor Test")

    SELECT Company, CareOf as Full_Name ;
    FROM TT_CUST ;
    INTO CURSOR Tquery

    loShowCursor=CREATE("wwShowCursor")
    loShowCursor.ShowCursor()

    Response.Write(loShowCursor.GetOutput())
    Response.HTMLFooter()

    The results of this output is identical although the first version is slightly faster as no secondary Response object is created.

    wwShowCursor can also display a single record either as an HTML table using the ShowRecord() method or as plain ASCII using using ShowASCIIRecord.

    To use the record functions you might do:

    SELE TT_CUST
    LOCATE FOR CustNo = lcCustId
    
    loShowCursor=CREATE("wwShowCursor")
    loShowCursor.ShowRecord()
    
    lcOutput=loShowCursor.GetOutput()
    

    Note that here no Response object is passed. In this case an Response object is created internally and the output can be returned with the GetOutput() method. You can SET FIELDS TO to limit fields to display or use a SELECT statement to select a single record with the appropriate fields.

    Field headers are displayed from the plain field names, except for ShowRecord/ShowASCIIRecord which use the DBC captions if available. In most cases tables used here are result sets from queries, so naming your fields with descriptive names using the AS clause is a good idea. When fields are displayed any underscores ("_") are converted to spaces in the field display.


    Paging wwShowCursor cursor displays (5 or n pages)


    The ShowCursor() method supports paged display via the cPage_ properties. By default these properties are disabled and paged display is off. Paged display allows viewing the results of a query in small pages rather than a whole result set. This is useful for large result sets that would take a long time to generate all at once.

    It's important to understand that this process is not fully automatic - some programming logic on the calling program code is required to implement paging - mainly keeping track of the page that is to be displayed. The idea is that you tell wwShowCursor how many items to display and which page to show. wwShowCursor will then take your result set and filter it accordingly and display only that data. In addition, it then tells you what the next and previous page numbers are so you can use them on the same request. Optionally, you can set up a base URL that will allow ShowCursor to display the Next and Previous buttons automatically the link of which point at the base URL plus an added page parameter (Page=10 for example).

    Paged display requires keeping track of the user who created the query and potentially the query conditions that created the cursor. You can use your own mechanism or use the wwSession object to assign the user some state information (see wwSession for more details on creating sessions).

    The following example uses a wwSession object to save the filter condition for a customer list query:

    FUNCTION PagedCustomerList
    ************************************************************************
    * wwDemo :: PagedCustomerList
    *********************************
    ***  Function: Demonstrates use of the wwSession object, Cookies the
    ***            wwShowCursor's Paging mode to display a query in paged 
    ***            format. The code below stores the Filter of the query
    ***            in a Session variable which is retrieved on each page
    ***            hit to re-run the query and jump to the appropriate
    ***            page.
    ************************************************************************
    LOCAL lcCookie, loSession, loHeader
    
    *** Create a Session Object
    THIS.InitSession()
    Session = THIS.oSession
    
    *** Now try retrieving the Query Parameters 
    *** Values on relevant if we're running from the form on the 'first hit'
    lcCompany=Request.Form("txtCompany")
    lcName=Request.Form("txtName")
    
    *** Check if we clicked the button of the form
    IF !EMPTY(Request.Form("btnSubmit"))
       *** Build a filter
       lcFilter=""
       IF !EMPTY(lcCompany)
          lcFilter="UPPER(Company) = '" + UPPER(lcCompany) + "' AND "
       ENDIF
       IF !EMPTY(lcName)
          lcFilter=lcFilter + "UPPER(Careof) = '" + UPPER(lcName) + "'  AND "
       ENDIF
       
       IF !EMPTY(lcFilter)
          lcFilter = lcFilter + "!DELETED()"
       ENDIF
       
       *** Store the filter in the Session Object
       Session.SetSessionVar("CustomerQueryFilter",lcFilter)
    ELSE
       *** No - paging through file
       *** Retrieve the filter from the Session Variable
       lcFilter=Session.GetSessionVar("CustomerQueryFilter")
    ENDIF
    
    IF !EMPTY(lcFilter)
      lcFilter= "WHERE " + lcFilter
    ELSE
      lcFilter = "WHERE .T."   && Force to disk
    ENDIF  
    
    *** Always re-run the query with the filter
    *** whether retrieved from session or new
    SELECT Company, CareOf, Phone ;
       FROM TT_CUST ;
       &lcFilter ;
       ORDER BY Company ;
       INTO CURSOR TQuery
    
    *** Create the HTML header for the page 
    ***  Note the custom HTTP header object containing the Cookie (loHeader)
    Response.HTMLHeader("Paged Customer List",,BACKIMG,loHeader)
    
    *** Create a ShowCursor Object and pass our HTML object
    loSC=CREATEOBJECT("wwShowCursor",Response)
    loSC.lAlternateRows = .T.
    
    *** Set the Paging parameters - 5 per page
    loSC.nPage_ItemsPerPage=5
    
    *** The URL to use for the Next/Prev buttons - This page - 
    *** SC will add parameter to the URL
    loSC.cPage_PageURL="PagedCustomerList.wwd?"  
    
    *** Now dump the HTML
    loSC.ShowCursor()
    
    Response.HTMLFooter(PAGEFOOT)
    
    ENDFUNC
    * PagedCustomerList
    

    A single Web Connection request handler handles this display by running a query, then re-calling this request for each of the Next/Previous links from the resulting HTML display. Note the use of the wwSession object to save the filter condition (lcFilter) between requests. The Session is established at the top. On repeat hits, the filter condition is retrieved from the Session objectand the query re-run each time. The result cursor is then passed loSC.ShowCursor() which in turn filters the result based on the nPage_ItemsPerPage and nPage_ShowPage properties set on wwShowCursor object.

    Paging and other Form Variables (<INPUT>)

    Note that if you are also hosting FORM variables in your HTML form, paging can cause you to loose the value of these form variables. A typical scenario is where you have a query condition input form variable at the top which you read and then based on this value populate the ShowCursor cursor with. The problem here is that when you click on a new Page link this causes the page to be submitted as a GET rather than a POST which means you loose any INPUT variables.

    To work around this you have to store any INPUT variables in a persistent matter. The easiest way to to do this is via Session variables. The code to do this for a single variable would look like this:

    THIS.InitSession()
    
    IF !Request.IsPostBack()
      lcValue = Session.GetSessionVar("MyInput")
    ELSE
      lcValue = Request.Form("MyInput")
      Session.SetSessionVar("MyInput",lcValue)
    ENDIF
    

    Paging and Sorting

    Paging and sorting are not supported on the same ShowCursor generated page. This really shouldn't be necessary anyway as you should display small chunks of data at a time which probably don't require sorting. Sorting will work on the first page, but will cause unpredictable results on other pages of the ShowCursor paging mechanism.

    wwShowCursor::BuildFieldListHeader


    This method allows you to pass a set of headers as an array to the object prior to calling one of the Show... methods.

    Sets the cHeaderString property.

    o.BuildFieldListHeader(lvHeader,llPrelist)
    

    Parameters

    lvHeader
    Either an array of strings that match each of the field names. or:
    A string that contains the entire header row.

    llPrelist
    If the header is on a <pre> formatted list it's formatted differently.


    Remarks

    It's recommended that you avoid this function if you can build a SQL statement with the appropriate fieldnames instead. For example:
    SELECT company as Customer_company, careof as Customer_name, phone as Phone_number ;
       INTO CURSOR TQuery
    
    will result in the same headers as in the example above without requiring the creation of an array and calling BuildFieldListHeader.

    Example

    SELECT company,careof,phone from tt_cust INTO CURSOR TQuery
    
    loSC = CREATEOBJECT("wwShowCursor")
    
    DIMENSION laHeaders[3]
    laHeaders[1] = "Customer Company"
    laHeaders[2] = "Customer Name"
    laHeaders[3] = "Phone Number"
    
    loSC.BuildFieldListHeader(@laHeaders)
    loSC.ShowCursor()
    
    Response.HTMLHeader("Header Demo")
    Response.Write(loSC.Getoutput())
    Response.HTMLFooter()
    

    wwShowCursor::EditRecord


    This method displays all fields of the table in editboxes for editing. The name of the HTML field matches the field name so you can retrieve any results with Request.Form("FieldName").

    This method creates only the table with the fieldlist inside of it, it does not generate the HTML Form and submit button - you can wrap those around with Response writes as follows:

    o.EditRecord(lnDisplayMode,lcObjectName)                   
    

    Return Value

    nothing - call GetOutput() to retrieve the generated HTML

    Parameters

    lnDisplayMode
    Optional - default 0
    0 - Generates with the data of the current record
    2 - Generates ASP style tags with fieldnames attached to an object name. Example: <%= oCust.Company %> where 'oCust' is the value passed as lcObjectName.

    lcObjectName
    Optional - The name of the object or cursor that is used for ASP style tag when lnDisplayMode = 2. The default is the name of the ALIAS() selected.

    Example

    * wwDemo::EditRecord
    Function EditRecord
    
    lcAction = Request.QueryString("Action")
    lcCustNo = PADL(ALLTRIM(Request.QueryString("CustNo")),8)
    
    IF !USED("tt_cust")
       USE TT_CUST IN 0
    ENDIF
    
    SELE TT_CUST
    
    IF EMPTY(lcCustNo)
       THIS.ErrorMsg("No Customer Number to edit",;
                     "Please enter a customer ID to edit")
       RETURN
    ENDIF
        
    LOCATE FOR CUSTNO = lcCustNo
    IF !FOUND()
       THIS.ErrorMsg("No Customer Number to edit","Please enter a customer ID to edit")
       RETURN
    ENDIF
    
    pcMessage = ""
    
    Response.HTMLHeader("Customer Editing")
    
    *** We're now on our record to edit or save
    DO CASE
      CASE lcAction = "Save"
        *** Save the data from the Request object
        loSC = CREATE("wwShowCursor")
        loSC.SaveRecord()
        pcMessage="Record Saved"
    ENDCASE
       
    Response.Write([<form action="wc.dll?wwDemo~EditRecord~&Custno=] + ALLTRIM(lcCustNo) + [&Action=Save" method="POST">] +CRLF)
    
    
    Response.Write([<input type="submit" name="btnSubmit" value="Save"><p>])
    
    *** Always display the data
    loSC = CREATE("wwShowCursor")
    loSC.cExtraTableTags = [style="font:normal normal 10pt Arial"]
    loSC.EditRecord()
    
    Response.Write( loSC.GetOutput() )
    
    Response.Write([</form>] + CRLF) 
    
    Response.HTMLFooter
    
    RETURN
    

    wwShowCursor::EditTable


    The EditTable method can handle full table editing with a single method call.

    Related Properties

    cBaseUrl
    The URL used to handle this request. URL should be complete to the point of additional querystring parameters to be added.
    Example: oSC.cBaseUrl = "wc.wc?wwDemo~TableEditor~&"

    cKeyField
    Name of the field to uniquely identify the record. Only a single field can be used here! Required for updates to work.

    cKeyType
    The FoxPro type of the key field either "N" or "C" for numeric and character respectively. Required for updates to work.

    cTableFieldList

    lAllowAdd
    Determines whether you can add new records. Note that records are added with blank data. Unless you have database rules to configure keyfields and default values all values will be displayed blank first (just like APPEND BLANK/BROWSE would).

    lAllowDelete
    Determines whether records can be deleted.


    o.EditTable()                                                 
    

    Return Value

    Nothing - output is written into oHTML member

    Parameters

    None

    Remarks

    Assumes Request object is in scope and supports Form and QueryString methods.

    Example

    Function TableEditor
    
    IF !USED("TT_CUST")
       USE TT_CUST  IN 0
    ENDIF
    
    SELE TT_CUST
    
    Response.HTMLHeader("Customer Browser")
    
    loSC = CREATE("wwShowCursor")
    
    loSC.cBaseUrl = "Tableeditor.wwd?"   && "TableEditor.wwd?"
    loSC.cKeyField = "custno"   && Required!!! Unique PK type value!
    loSC.cKeyType = "C"
    loSC.cTableFieldList = "company,careof,phone"  && List Fields
    loSC.cTableEditFieldList = "company,careof,phone,address,email"  && EditFields
    loSC.cTableSortColumn = "company"
    loSC.cExtraTableTags = [style="font:normal normal 10pt Arial"]
    loSC.lAlternateRows = .T.
    
    loSC.lAllowDelete = .F.
    loSC.lAllowAdd = .F.
    
    lcAction = UPPER(Request.Querystring("Action"))
    DO CASE
       *** Save operation needs custom handling
       CASE lcAction = "SAVENEW"
    	   *** If you do a full edit including key fields
    	   *** then you don't need this type of thing	
           APPEND BLANK
           REPLACE CUSTNO WITH SYS(2015)
           loSC.SaveRecord()
           Response.Redirect(loSC.cBaseUrl)
       OTHERWISE
           *** Default handling for everything else
           loSC.EditTable()
    ENDCASE
    
    *** Write the output
    Response.Write( loSC.GetOutput() )
    
    Response.HTMLFooter(PAGEFOOT)
    

    wwShowCursor::GetOutput


    If you don't pass in an existing wwResponse object, this method will return the output of the generated HTML. If you don't pass in a wwresponse object, wwShowCursor creates one internally and GetOutput() returns the output as a string.



    o.GetOutput()
    

    wwShowCursor::SaveRecord


    Saves HTML form variables with the same names as the actual database fields into the currently open record of the database. This method is meant to be used in response to a EditRecord() created HTML form which contains all fields.

    IMPORTANT
    If not all fields are represented on the form any non-represented fields will be replaced with empty vaules (null string, 0, False or empty dates and times). If you do have missing fields on the HTML form always limit your field choice with SET FIELDS.


    o.SaveRecord()                                                
    

    Example

    * wwDemo::EditRecord
    Function EditRecord
    
    lcAction = Request.QueryString("Action")
    lcCustNo = PADL(ALLTRIM(Request.QueryString("CustNo")),8)
    
    IF !USED("tt_cust")
       USE TT_CUST IN 0
    ENDIF
    
    SELE TT_CUST
    
    IF EMPTY(lcCustNo)
       THIS.ErrorMsg("No Customer Number to edit",;
                     "Please enter a customer ID to edit")
       RETURN
    ENDIF
        
    LOCATE FOR CUSTNO = lcCustNo
    IF !FOUND()
       THIS.ErrorMsg("No Customer Number to edit","Please enter a customer ID to edit")
       RETURN
    ENDIF
    
    pcMessage = ""
    
    Response.HTMLHeader("Customer Editing")
    
    *** We're now on our record to edit or save
    DO CASE
      CASE lcAction = "Save"
        *** Save the data from the Request object
        loSC = CREATE("wwShowCursor")
        loSC.SaveRecord()
        pcMessage="Record Saved"
    ENDCASE
       
    Response.Write([<form action="wc.dll?wwDemo~EditRecord~&Custno=] + ALLTRIM(lcCustNo) + [&Action=Save" method="POST">] +CRLF)
    
    
    Response.Write([<input type="submit" name="btnSubmit" value="Save"><p>])
    
    *** Always display the data
    loSC = CREATE("wwShowCursor")
    loSC.cExtraTableTags = [style="font:normal normal 10pt Arial"]
    loSC.EditRecord()
    
    Response.Write( loSC.GetOutput() )
    
    Response.Write([</form>] + CRLF) 
    
    Response.HTMLFooter
    
    RETURN
    

    wwShowCursor::SetCursor


    Use this method to set a DBF or cursor other than the currently selected Alias


    o.SetCursor(lcDBF)
    

    Return Value

    nothing

    Parameters

    lcDBF
    Name of the DBF file or Alias.

    wwShowCursor::ShowASCIIRecord


    Shows all the fields of the current record in ASCII format. This is useful for sending emails or other operations that can't use HTML as the presentation format.


    o.ShowASCIIRecord()
    

    wwShowCursor::ShowCursor


    Renders a cursor to HTML into the internal Response object. This method generates the list, but you have to use GetOutput() to retrieve a string.

    o.ShowCursor()
    

    Return Value

    nothing - output generated into an HTML object. Pass in an HTML object or use GetOutput() to retrieve the HTML result of the table.

    Remarks

    By default an HTML table is used for output unless number of cells exceeds the value in nForceToPreList which defaults to MAX_TABLE_CELLS (in WCONNECT.H) . When this number is exceeded ShowCursor reverts to a <pre> list display which can render much faster. The default size for MAX_TABLE_CELLS is 4000 cells.

    Example

    SELECT company, contact as Name, phone ;
       FROM wwDevRegistry ;
       INTO CURSOR TQuery
    
    loSC = CREATEOBJECT("wwShowCursor")
    loSC.lAlternateRows = .T.  && Alternate row colors
    loSC.ShowCursor()
    
    Response.HTMLHeader("Developer Listing")
    
    *** This writes grabs and writes the output from ShowCurso
    Response.Write( loSC.GetOutput() )
    
    Response.HTMLFooter()

    wwShowCursor::ShowObject


    Displays an object's property values as an HTML table.

    o.ShowObject(loObject)                                        
    

    Parameters

    loObject

    wwShowCursor::ShowRecord


    Shows all the fields of the current record in an HTML table.


    o.ShowRecord()
    

    wwShowCursor::cAlternatingBGColor


    The background color used for alternating rows if lAlternateRows is set to .T.. The colors will alternate between this color and cTableBGColor.

    o.cAlternatingBGColor                                         
    

    wwShowCursor::cBaseUrl


    Property that determines the base Url of the ShowCursor request.

    This property should be set to the current request in such a way that it can be added to with named QueryString parameters. For example:

    wc.dll?wwDemo~ShowCursor~&
    ShowCursor.wwd?
    ShowCursor.wwd?SomeParm=SomeValue&

    Note the trailing & which allows additional parameters to be added dynamically for operations such as paging and sorting.


    o.cBaseUrl                                                    
    

    wwShowCursor::cCellPadding


    Cell Padding for the table.

    Default Value

    Initial value: 3

    wwShowCursor::cCellSpacing


    Cell Spacing for the table.

    Default Value

    Initial value: 2

    wwShowCursor::cExtraTableTags


    Any extra TABLE tags that are to be appended to the <TABLE> statement.

    Tip:
    Use Class or Style tags to force the table to be formatted globally so you don't have to format each cell individually.


    wwShowCursor::cHeaderBGColor


    Background color for the table header. Use HTML color names or Hex color values.

    Default Value

    Initial value: #FFFFCC

    wwShowCursor::cHeaderColor


    The color of the header font.

    o.cHeaderColor                                                
    

    Default Value

    Initial Value: Black

    wwShowCursor::cHeaderFont


    The font used for the table header that shows the field labels.

    o.cHeaderFont                                                 
    

    wwShowCursor::cKeyField


    The type of the key field used to identify records. Default is "N" for numeric.

    o.cKeyField                                                   
    

    Remarks

    The key field used when changes are written to disk with the EditTable() or Save() methods.

    wwShowCursor::cKeyFieldType


    VFP Type for the key field used in EditTable(). Defaults to "N" numeric. Used only by EditTable and is required for any update operations.

    o.cKeyFieldType                                               
    

    Remarks

    The type of the key field. Use standard FOXTYPEs as returned by the TYPE() function.

    wwShowCursor::cPage_PageURL


    The base URL used for paging. This URL will be appended with additional values for page count and total recs.

    This URL should end in a ? (for plain URLs) or & (~& if using positional parameters in wc) for Querystring based URLs so that ShowCursor can add parameters on the URL after it.

    For example,

    PagedCustomerList.wwd?
    creates: PagedCustomerList.wwd?Page=4

    PagedCustomerList.wwd?CompanyFilter=a&
    creates: PagedCustomerList.wwd?CompanyFilter=a&Page=10

    wwShowCursor::cTableBGColor


    Background for the HTML table.

    Default Value

    Initial value: #EEEEEE

    wwShowCursor::cTableBorder


    Border width of the table as a string value. Default = 1.

    Default Value

    Initial value: 1

    wwShowCursor::cTableEditFieldList


    Allows you to provide a comma delimited list of fields that are to be displayed in edit view. If not passed all fields in the current cursor will be displayed.

    o.cTableEditFieldList                                         
    

    Remarks

    If the cKeyField value is set the keyfield is automatically added to the HTML form as a hidden form variable, so make sure you do not include the PK in the editfield list unless you really need to edit it.

    wwShowCursor::cTableFieldList


    This property determines the field list of fields to be displayed in ShowCursor() list display.

    o.cTableFieldList                                             
    

    Remarks

    This property re-runs a query with the limited field list internally essentially creating a filter. The field list must match actual fields in the table or cursor or else the ShowCursor() call may fail. Note that in general it'll be more efficient to prefilter the cursor in your own code than to use this prpoerty as another query is run into a cursor to reduce the field list.

    Applies only to ShowCursor() invokations and any methods such as EditTable that call ShowCursor internally. It does not apply to ShowRecord() or EditTable().

    wwShowCursor::cTableRecordFieldList


    Field list filter used when displaying a single record with ShowRecord(). List should be a comma-delimited list of fields or field expressions that are to be displayed. This value becomes the field list value in a SQL Cursor SELECT statement.

    o.cRecordFieldList                                            
    

    wwShowCursor::cTableSortColumn


    Used if you provide a customized field list in cTableFieldList to determine the sortorder. Choose the column name of the table or a numeric value of the column if a complex field.

    o.cTableSortColumn                                            
    

    wwShowCursor::cTableTitle


    Header for the table.

    wwShowCursor::cTableWidth


    Width of the table as a string. Use real pixel value or a % value.

    Default Value

    Initial value: 100%

    wwShowCursor::lAlternateRows


    Property that determines whether the ShowCursor display varies the color of odd and even rows for a ledger look.

    Use the cTableBGColor, and cAlternatingBGColor properties to customize the background color. Note for best results these should be light colors so that black text can be seen on them.

    o.lAlternateRows                                              
    

    wwShowCursor::lCenterTable


    Centers the table if set.

    Default Value

    Initial value: .T.

    wwShowCursor::lShowAsTable


    Determines whether the table is displayed using a table or <pre> tags.

    Note: that this property is overridden depending on the size of the table. If the table gets larger than

    Default Value

    Initial value: .T.

    wwShowCursor::lSortable


    Allows columns in the data list view (ShowCursor()) to be sorted by showing the column headers with Ascending and Descending links next to them.

    o.lSortable                                                   
    

    Remarks

    Requires that the
    cBaseUrl property is set.

    Works only on columns that are String, Numeric, Integer or Logical. Memos are not supported for sorting - if you have to sort on memo data use MLINE() or PADR() to force the memo into a regular string.

    Sorting currently works only with automatically generated headers from fieldnames. If you use custom headers it won't work.

    The Sort Order is selected based on the columns displayed, so it's important that there is no filter on the data. IOW, don't use FoxPro filters (SET FILTER TO) on a table when sorting. You can use cTableFieldList or your own SELECT statement to filter the data.

    Example

    ...
    SELECT * from tt_CUST into cursor TQuery
    
    loShowCursor = CREATEOBJECT("wwShowCursor")
    loShowCursor.cBaseUrl = "ShowCursor.wwd?"
    loShowCursor.lSortable = .T.
    
    loShowCursor.ShowCursor()
    
    Response.Write(loSC.Getoutput())
    
    ...

    wwShowCursor::lSumNumerics


    If .T. all numeric fields are summed and totalled on the bottom of the table.

    Default Value

    Initial value: .F.

    wwShowCursor::nForceToPreList


    This number is the number of cells that are the limit of the size for the table displayed. If this number of cells is exceeded the table will automatically revert to a <pre> formatted list.

    o.nForceToPreList                                             
    

    wwShowCursor::nPage_ItemsPerPage


    If in paging mode this property determines how many items to display per page.

    Default Value

    Initial value: 0

    Example

    loSC=CREATEOBJECT("wwShowCursor")
    loSC.lAlternateRows = .T.
    
    *** Set the Paging parameters - 5 per page
    loSC.nPage_ItemsPerPage=5
    
    *** The URL we post back to (usually the current page) 
    *** including trailing ? or &
    loSC.cPage_PageURL="PagedCustomerList.wwd?"  
    
    *** Now dump the HTML
    loSC.ShowCursor()
    
    *** Write output into HTTP stream
    Response.Write( loSC.GetOutput() )

    wwShowCursor::nPage_ShowPage


    Use the nPage_ShowPage property to go to a specific page in the paging list. This parameter is optional and is retrieved from the URL containing a PAGE= value if not specified. If neither exists the first page is displayed.


    o.nPage_ShowPage                                              
    

    Class wwDBFPopup


    The wwDBFPopup class allows for easy creation of data driven HTML popup form elements from a FoxPro table.

    How it works

    The wwDBFCursor class essentially takes an HTML object and then builds output on the fly from the currently selected Visual FoxPro workarea. The class takes an HTML object as an input parameter to the Constructor (init()) so you can send output to the selected output source in an optimized fashion. If no wwHTML object is passed an wwHTMLString object is created internally and the output is returned via this self-created object.

    Here's an example on how you can use this object to display a state popup from a table:

    *** Create a popup object and send output to the current Response object
    loPopup=CREATE("wwDBFPOPUP",Response)
    
    SELECT cData AS StateName, cData1 AS StateCode ;
       FROM sub_lookups ;
       WHERE TYPE="STATE" ;
       INTO CURSOR TList
    
    loPopup.cKeyValueExpression="TList.StateCode"
    loPopup.cDisplayExpression="TList.StateName"
    loPopup.cFormVarName="co_state_ID"
    loPopup.cAddFirstItem="<Select for US or Canada>"
    loPopup.cSelectedValue=Subscribers.Co_state_Id
    
    *** Build the Popup - output goes to HTML object
    loPopup.BuildList()
    
    *** If you don't pass HTML object a wwResponseString object is created 
    *** behind the scenes. To retrieve the value you'd use:
    * lcStatePopup=loPopup.GetOutput()


    Class Members

    MemberDescription
    BuildList The actual 'action' method that builds the table into the Response object. Uses the currently selected Alias().
    o.BuildList()
    GetOutput Returns the output from the BuildList call. This method only applies if an HTML object was not passed in.
    o.GetOutput()
    Reset Resets the object to its defaults.
    o.Reset()
    cAddFirstItem Allows you to specify an additional item that is inserted at the top of the list. This is useful for things like 'Please select one of the following items' prompts.
    cAddLastItem This property allows you to add an item to the end of the list. This is useful to provide non-data options such as 'All' or 'My Data' etc.
    cDisplayExpression The expression used to display in the listbox. This can be a field or any Fox expression as a string. The value is evaluated inside of a SCAN loop.
    cExtraSELECTTags Any extra tags you want to add to the <SELECT> tag.
    cFormVarName Name of the HTML form variable that is to receive the result from the selection. It's the NAME= tag in the SELECT tag.
    cKeyValueExpression The key value for each OPTION item. Set this value if you have a different value for each selection than the display value. For example, in an online store you might display the item description, but the actual value is going to be the Item ID/SKU.
    cSelectedDisplayValue The display value that is to be highlighted when the list is first displayed.
    cSelectedValue The key value that is to be highlighted when the list is first displayed.
    lMultiSelect Set to .T. if the list will be a multi-select list.
    nHeight Height of the list in rows. Default: 1

    Requirements


    wwDBFPopup::BuildList


    The actual 'action' method that builds the table into the Response object. Uses the currently selected Alias().


    o.BuildList()
    

    wwDBFPopup::GetOutput


    Returns the output from the BuildList call. This method only applies if an HTML object was not passed in.

    o.GetOutput()
    

    wwDBFPopup::Reset


    Resets the object to its defaults.

    o.Reset()
    

    wwDBFPopup::cAddFirstItem


    Allows you to specify an additional item that is inserted at the top of the list. This is useful for things like 'Please select one of the following items' prompts.

    Default Value

    Initial value: WWC_NULLSTRING

    Remarks

    You can add more than a single item here by using code as follows:

    loPopup.cAddFirstItem = [All Forums] + CRLF + ;
       [<option>Messages to you] + CRLF + ;
       [<option>------------------] + CRLF

    wwDBFPopup::cAddLastItem


    This property allows you to add an item to the end of the list. This is useful to provide non-data options such as 'All' or 'My Data' etc.

    o.cAddLastItem                                                  
    

    Default Value

    Initial Value: WWC_NULLSTRING

    Remarks

    You can add more than a single item here by using code as follows:
    loPopup.cAddLastItem = ;
       [<option>------------------] + CRLF +;
       [All Forums] + CRLF + ;
       [<option>Messages to you] + CRLF 
      

    wwDBFPopup::cDisplayExpression


    The expression used to display in the listbox. This can be a field or any Fox expression as a string. The value is evaluated inside of a SCAN loop.

    wwDBFPopup::cExtraSELECTTags


    Any extra tags you want to add to the <SELECT> tag.

    wwDBFPopup::cFormVarName


    Name of the HTML form variable that is to receive the result from the selection. It's the NAME= tag in the SELECT tag.

    wwDBFPopup::cKeyValueExpression


    The key value for each OPTION item. Set this value if you have a different value for each selection than the display value. For example, in an online store you might display the item description, but the actual value is going to be the Item ID/SKU.

    wwDBFPopup::cSelectedDisplayValue


    The display value that is to be highlighted when the list is first displayed.

    Default Value

    Initial value: WWC_NULLSTRING

    wwDBFPopup::cSelectedValue


    The key value that is to be highlighted when the list is first displayed.

    Default Value

    Initial value: WWC_NULLSTRING

    wwDBFPopup::lMultiSelect


    Set to .T. if the list will be a multi-select list.

    Default Value

    Initial value: .F.

    Remarks

    You can use Request.GetFormMultiple to retrieve multiple selection values.

    wwDBFPopup::nHeight


    Height of the list in rows. Default: 1

    Default Value

    Initial value: 1

    Control Architecture




    Class wwConfig


    Saving and restoring configuration information the easy way with a single method call and simple property assignments.

    This flexible object is used to persist configuration information as an XML file, an INI file or as a single XML value into the registry. This object basically persists the configuration object to a permanent location using the Save method, allowing transparent restoration of an object's state using the Load method. This object can be efficiently used as a configuation manager for any application that needs to store configuration values in a persistent format. The object itself can be loaded once at application startup and saved when the application shuts down with two simple method calls with the object available for the duration of the application using global object reference or as part of an application object.

    In its simple form you can define new values to be persisted simply by adding methods to a subclass of wwConfig:

    DEFINE CLASS CustConfig as wwConfig
    
    cFileName = "CustConfig.ini"
    cMode = "INI"
    
    *** Persist properties
    cDataPath = ".\data"
    cHTMLPath = ".\webpages"
    cAdminUser = "username"
    lSendEmailOnError = 1
    
    ENDDEFINE
    

    To save the settings from this object you'd simply call it's Save method:

    oConfig = CREATEOBJECT("CustConfig")
    
    oConfig.cDataPath = "\webapps\data\"
    oConfig.lSendEmailOnError = .F.
    
    oConfig.Save()

    This would generate an INI file CustConfig.ini like this:

    [config]
    DataPath=\webapps\data
    HTMLPath=.\webpages
    AdminUser=username
    SendEmailOnError=0

    To load values from the INI file you'd use:

    oConfig = CREATEOBJECT("CustConfig")
    oConfig.Load()
    
    ? oConfig.cDataPath
    IF oConfig.lSendEmailOnError
       SendEmail()
    ENDIF

    To add more values simply add another property to the object. Any values in the INI file that match the properties of the object (minus the type prefix character) are read into to properties with the Load() method and written out with the Save() method.

    wwConfig determines how to load and save values based on the setting of the cMode property, which can be set to a string value of INI, XML or REGISTRY.

    wwConfig can persist nested objects. All object properties attached to an instance of wwConfig will be persisted and restored along with the 'main' properties. The following illustrates how to create a nested object:

    DEFINE CLASS wcDemoConfig as wwConfig
    
    owwDemo = .NULL.
    owwMaint = .NULL.
    cCustomProperty = "Some Value"
    nCustomValue = 10
    
    FUNCTION Init
    
    *** Add nested objects - hierarchical persistance
    THIS.owwDemo = CREATE("wwDemoConfig")
    THIS.owwMaint = CREATE("wwDemoConfig")
    
    ENDFUNC
    
    ENDDEFINE
    
    *** Custom Config objects which can be created as subobjects
    *** of the config object.
    *** In the process class this can be references as:
    ***
    *** lcPath = THIS.oServer.oConfig.owwDemo.cDataPath
    ***
    *** You can add any number of custom config objects
    *** here and simply add them to the server!!!
    DEFINE CLASS wwDemoConfig as wwConfig
    
    cHTMLPagePath = "d:\westwind\wconnect\"
    cDATAPath = "d:\wwapps\wc3\wwDemo\"
    
    ENDDEFINE

    This generates:

    [config]
    CustomProperty=Some value
    CustomValue=10
    
    [wwdemo]
    HTMLPagepath=d:\westwind\wconnect\
    Datapath=d:\wwapps\wc3\wwdemo\
    
    [wwmaint]
    ;*** no properties

    Requirements

    All property names must start with a single character type identifier. The type identifier is removed when the property is persisted and reassembled when loaded back in. It's crucial that every property of this object has a prefix or else you'll see truncated property names in the persisted files/documents (it will still work though - just looks funny).

    Formats

    wwConfig can persist in two formats using XML or an INI file. Use the INI file method if you can't guarantee that the IE XML parser is available on the target machine.
    XML Format:
    <?xml version="1.0"?>
    <wcdemo>
    	<main>
    		<tempfilepath>c:\temp\</tempfilepath>
    		<template>WC_</template>
    		<logtofile>True</logtofile>
    		<saverequestfiles>False</saverequestfiles>
    		<showserverform>True</showserverform>
    		<showstatus>True</showstatus>
    		<usemts>False</usemts>
    		<scriptmode>3</scriptmode>
    		<timerinterval>175</timerinterval>
    		<wwdemo>
    			<datapath>d:\wwapps\wc3\wwDemo\</datapath>
    			<htmlpagepath>d:\westwind\wconnect\</htmlpagepath>
    		</wwdemo>
    		<wwmaint>
    			<datapath>d:\wwapps\wc3\wwDemo\</datapath>
    			<htmlpagepath>d:\westwind\wconnect\</htmlpagepath>
    		</wwmaint>
    	</main>
    </wcdemo>
    

    Ini Format:

    [main]
    tempfilepath=c:\temp\
    template=wc_
    logtofile=1
    saverequestfiles=0
    showserverform=1
    showstatus=1
    usemts=0
    scriptmode=3
    timerinterval=200
    
    [wwdemo]
    datapath=d:\wwapps\wc3\wwDemo\
    htmlpagepath=d:\westwind\wconnect\
    
    [wwmaint]
    datapath=d:\wwapps\wc3\wwDemo\
    htmlpagepath=d:\westwind\wconnect\
    

    Usage

    Using the class is very easy. Use the following steps:

    o=create("MyConfig")
    o.cFileName = "Config.xml"
    
    IF .F.
        o.cFileName = "Config.ini"
        o.cSubName = "Ini File Test"
        o.cMode = "INI"
        ? o.Save()
    ENDIF    
    IF .T.
        o.cFileName = "Config.ini"
        o.cMode = "INI"
        ? o.Load()
        ? o.cSubName
        ? o.oTest.cHTMLPagePath
    ENDIF
    IF .F.
    	o.cSubName = "Default Application - Not yet set"
    	o.nTimerInterval = 100
    	o.cTemplate="cfg_"
          o.cMode = "XML"
    	? o.Save() 
    	modi comm config.xml
    ENDIF
    IF .F.
        o.cMode = "XML"
        o.Load()
    ENDIF
    
    IF .F.
    	o.cSubName = "Test Application"
    	o.nTimerInterval = 100
          o.cMode = "REGISTRY"
    
       o.cRegPath = "Software\West Wind Technologies\TestConfig"
       o.cRegNode = "Parameters"
       ? o.Save()
    ENDIF
    IF .F.   
       o.cRegPath = "Software\West Wind Technologies\TestConfig"
       o.cRegNode = "Parameters"
       o.cMode="REGISTRY"
       
       ? o.Load()
    ENDIF
    

    Class Members

    MemberDescription
    Load Loads a persisted object from an XML file, INI file or the registry.
    o.Load()
    Save Persists the current Config object either to an INI file, XML file or the registry.
    o.Save()
    cFileName The filename to persist or read the configuration information from. Doesn't apply if you're using the Registry.
    cMode This property allows specifying the operational mode of the Config object.
    cRegNode The key name to used to persist a value in the registry. Note currently wwConfig persists the entire object as a single value in the registry. The value is written as an XML string.
    cRegPath The registry path name to used to persist a value in the registry.
    cSubName The subname for the persisted object. For XML this will be the class level element that all the fields live below. For INI files this will be the section header. The subname describes the object in question.

    Requirements


    wwConfig::Load


    Loads a persisted object from an XML file, INI file or the registry.

    Relies on the cMode ("INI","XML"*,"REGISTRY") property to determine how to load the object.

    Requires that the appropriate properties are set:

    INI, XML
    cFilename property is set.

    INI
    cSubName - section name (ie. [wwdemo]). Example: wwDemo

    REGISTRY
    cRegPath and cRegNode.

    o.Load()
    

    Return Value

    .T. or .F.

    wwConfig::Save


    Persists the current Config object either to an INI file, XML file or the registry.

    Depends on the cMode ("INI","XML"*,"REGISTRY") property to determine which to persist to.

    The appropriate properties must be set:

    INI, XML
    cFilename property is set.

    INI
    cSubName - section name (ie. [wwdemo]). Example: wwDemo

    REGISTRY
    cRegPath and cRegNode.

    o.Save()
    

    wwConfig::cFileName


    The filename to persist or read the configuration information from. Doesn't apply if you're using the Registry.

    wwConfig::cMode


    This property allows specifying the operational mode of the Config object.

    Supported modes are:


    If you set the mode you can always call Load() or Save() methods instead of having to call the individual methods (LoadIni, LoadRegistry etc.).

    cFileName and/or cRegNode/cRegPath must still be set prior to making the calls to Load/Save.


    o.cMode                                                           
    

    Default Value

    XML
    INI
    REGISTRY

    wwConfig::cRegNode


    The key name to used to persist a value in the registry. Note currently wwConfig persists the entire object as a single value in the registry. The value is written as an XML string.

    Default Value

    Initial value: Parameters

    wwConfig::cRegPath


    The registry path name to used to persist a value in the registry.

    Example:
    SOFTWARE\West Wind Technologies\Config

    Default Value

    Initial value: SOFTWARE\West Wind Technologies\Config

    wwConfig::cSubName


    The subname for the persisted object. For XML this will be the class level element that all the fields live below. For INI files this will be the section header. The subname describes the object in question.

    Default Value

    Initial value: config

    Class wwServerConfig


    This class is used to configure the Web Connection application and holds the main server's configuration settings which are read and written to the startup INI file.

    This class uses Save() and Load() methods of the wwConfig class to load the Server's INI file settings. You can use the Reload method to automatically cause COM Server instances to reload and re-read these settings.

    Relation
      wwConfig
        wwServerConfig

    Class Members

    MemberDescription
    cAdminEmail Admin Email address used for notifications and errors using the wwProcess::SendErrorEmail() method.
    cAdminMailServer Mail server used to send Admin email used for notifications and errors using the wwProcess::SendErrorEmail() method.
    cComReleaseUrl The URL used to release the Web Connection application server. By default: wc.dll?_maintain~release.
    cSQLConnectString SQL Connect String to use if you want to use SQL Logging and Session objects.
    cTempFilePath Temp file path location used for file based messaging. Set this even if you're running COM mode exclusively.
    cTemplate The file prefix used in file based messaging. The form polls for files of this extension in the temp path. Default: WC_\
    lAdminSendErrorEmail Flag to determine whether admin email is sent on errors.
    lLogToFile Flag to determine whether every request is logged to file.
    lSaveRequestFiles Flag that determines whether the last request information is saved to a file that can be reviewed.
    lShowRequestData Flag that is passed forward to the wwProcess object (by default, but can be overridden) to show the current request data at the end of an HTML page. Shows Form data and Server Variables.
    lShowServerForm Flag that determines whether the server form shows. Not available while running in the IDE - only works in compiled applications.
    lShowStatus Determines whether the status window shows each request. There's slight overhead in displaying this information, so if speed is of utmost importance turn this option off.
    nMemUsage Memory usage flag. This value is passed to VFP's SYS(3050) function to attempt to limit memory usage of the Web Connection application servers.
    nScriptMode The scripting mode setting
    nTimerInterval The frequency in milliseconds the timer fires for file based messaging.

    Requirements

    Assembly: wwServer.prg

    wwServerConfig::nTimerInterval


    The frequency in milliseconds the timer fires for file based messaging.

    Default Value

    Initial value: 200

    wwServerConfig::nScriptMode


    The scripting mode setting

    Default Value

    Initial value: 3

    3 - Interpreted using CodeBlock
    2 - Compiled FXP
    1 - VFP interpreted

    See the wwVFPScript class and wwResponse::ExpandScript for more info on these values.

    wwServerConfig::nMemUsage


    Memory usage flag. This value is passed to VFP's SYS(3050) function to attempt to limit memory usage of the Web Connection application servers.

    This behavior operation can be overridden as it's set in the exposed SetServerProperties method of the server's startup code like this:

    SYS(3050,2,THIS.oConfig.nMemUsage)

    Default Value

    Initial value: 8176

    wwServerConfig::lShowStatus


    Determines whether the status window shows each request. There's slight overhead in displaying this information, so if speed is of utmost importance turn this option off.

    Default Value

    Initial value: .T.

    wwServerConfig::lShowServerForm


    Flag that determines whether the server form shows. Not available while running in the IDE - only works in compiled applications.

    Default Value

    Initial value: .T.

    wwServerConfig::lShowRequestData


    Flag that is passed forward to the wwProcess object (by default, but can be overridden) to show the current request data at the end of an HTML page. Shows Form data and Server Variables.

    Default Value

    Initial value: .F.

    Remarks

    Should be used for debugging purposes only. Make sure you unset this flag when you're done debugging!

    Note be very careful with this feature if you're generating binary content (such as images) or other non-HTML output like XML with this flag set. Since the data is appended to the end of the document content other than HTML will likely become invalid and not display or load correctly. For these situation you can use wwProcess::lShowRequestData selectively on specific requests to show only those requests you are actively debugging.

    wwServerConfig::lSaveRequestFiles


    Flag that determines whether the last request information is saved to a file that can be reviewed.

    Default Value

    Initial value: .F.

    wwServerConfig::lLogToFile


    Flag to determine whether every request is logged to file.

    Default Value

    Initial value: .T.

    wwServerConfig::lAdminSendErrorEmail


    Flag to determine whether admin email is sent on errors.

    Default Value

    Initial value: .F.

    wwServerConfig::cTemplate


    The file prefix used in file based messaging. The form polls for files of this extension in the temp path. Default: WC_\

    Default Value

    Initial value: wc_

    wwServerConfig::cTempFilePath


    Temp file path location used for file based messaging. Set this even if you're running COM mode exclusively.

    Default Value

    Initial value: SYS(2023)+"\"

    wwServerConfig::cSQLConnectString


    SQL Connect String to use if you want to use SQL Logging and Session objects.

    Remarks

    Setting this value will force SQL operation.

    Make sure you run the CONSOLE's Create SQL Log and Session tables options to create the appropriate databases and add the appropriate connection string to your application.

    wwServerConfig::cComReleaseUrl


    The URL used to release the Web Connection application server. By default: wc.dll?_maintain~release.

    Default Value

    Initial value: http://localhost/wc.wc?_maintain~Release

    wwServerConfig::cAdminMailServer


    Mail server used to send Admin email used for notifications and errors using the wwProcess::SendErrorEmail() method.

    wwServerConfig::cAdminEmail


    Admin Email address used for notifications and errors using the wwProcess::SendErrorEmail() method.

    Class wwCache


    The wwCache class provides a simple caching mechanisms for strings. Caching allows you to generate output once, store it to the cache and then attempt to retrieve it from the cache if it already exists the next time the same content is required.

    Cached items are accessible by key and have an expiration, which allows for timing the availability of cached content to a timeout period that content is valid.

    Caching is powerful for storing previously generated output - complete HTML repsonses, or HTML fragments of a page. XML results or fragments of results. You can even cache binary content like the result of a PDF output generation for example. Anything that can be represented as a string can be cached.

    Caching can provide huge performance gains for your applications by avoiding regenerating output and not requiring you to go back to the database to re-run complex queries. Instead results are returned from a single indexed key of the cache cursor or table.


    Relation
      wwCache

    Remarks

    The cache is implemented as a cursor, so the contents of the cache is not permanently stored on disk, but stored in temporary files.

    Category list items are case sensitive

    Class Members

    MemberDescription
    AddItem This method adds an item to the cache.
    o.AddItem(lcKey as String, lcValue as String, lnTimeoutSeconds as Integer)
    Expire Removes all expired entries from the cache data file.
    o.Expire()
    GetItem Retrieves an item by key from the cache.
    o.GetItem(lcKey as String)
    IsCached Determines whether an item is cached or not.
    o.IsCached(lcKey as String)
    Open Opens the cache cursor. Either creates it or just uses it if already open.
    o.Open()
    Reindex Packs and reindexes the file specified in the cFixedFileName. This method works only if this property is set and only if an exclusive lock can be set on the table.
    o.Reindex()
    Remove This method removes an entry from the Cache.
    o.Remove(lcKey)
    cCacheCursor The name of the cursor used for caching.
    cFixedFileName This property allows you to specify a fixed filename for the cache. including the .DBF extension.
    nDefaultCacheTimeout The default timeout for the cache in seconds.

    Requirements

    Assembly: wwCache.prg

    How wwCache works


    The wwCache object provides a very simple caching mechanism to hang on to string based output that was previously generated. You can deposit this output in the cache to avoid re-generating that same content again. Cache items have a key and an expiration and you can easily check for existance of cache content and retrieve it if it exists.

    In Web Connection the cache object is instantiated on the the Server object and is always available as:

    Server.oCache

    in process requests.

    A typical usage scenario is a static list in an application. For example, in our Web store the category list rarely changes so the list can be generated once and then be cached and reused from the cache. The code to do this might be as simple as this:

    FUNCTION Categorylist LOCAL lcOutput, oLookups *** See if the output is cached lcOutput = Server.oCache.GetItem("wwstore_categorylist") IF !ISNULL(lcOutput) RETURN lcOutput ENDIF oLookups = CREATE([WWS_CLASS_LOOKUPS]) #IF WWSTORE_USE_SQL_TABLES oLookups.SetSQLObject(Server.owwStoreSQL) #ENDIF oLookups.GetCategories("_TLookups",,.T.) *** Build a simple string outptu from the categories *** HTML includes specific HTML formatting for hover *** buttons and underline adding to the look and feel lcOutput = "" SCAN lcoutput = lcOutput + ; [<tr><td width=100% align="right" class="menulink">] +; [<a href="itemlist.wws?Category=] + UrlEncode(TRIM(cData1)) + ; [" class="menulink">]+TRIM(cData1) + [</a></td></tr>]+ CRLF + ; [<tr><td height="1"><img src="space.gif" width="100%" height="1"></td></tr>] + CRLF ENDSCAN *** Add the generated HTML to the Cache Server.oCache.AddItem("wwstore_categorylist",lcOutput) USE IN _TLOOKUPS RETURN lcOutput

    In this scenario the cache content avoids a trip to the database and creation of the HTML.

    Using a Fixed Table for Caching

    By default wwCache uses a Cursor for caching which means that each individual instance of FoxPro has its own copy of the Cache. This means that there may be cache duplication if you are running multiple instances. Using a cursor is a good choice as it avoids issues of memo bloat and index corruption of tables as the cache tables get recreated on every startup.

    In most situations cursors work fine - caching works best in high volume scenarios and not for one of requests that might get run a couple of times, so re-running requests is usually not an issue. However, if you have situations where the cache content is required to be in sync and if you have long running requests that create cache content it might make more sense to have shared Cache state between instances.

    To do this you can set the wwCache class cFixedFileName property to the name of a Table stored on disk. the table will be automatically created. The best way to accomplish this is to create a custom subclass of the wwCache class:

    DEFINE Class MyCache of wwCache cFixedFilename = "__wwCache" ENDDEFINE

    and then tell Web Connection to use your custom class in WCONNECT_OVERRIDE.H:

    #UNDEFINE WWC_CACHE #DEFINE WWC_CACHE MyCache

    At this point you can continue to use Server.oCache and get your persistent file based cache.

    Multiple Cache Entries for the same page

    In many situations you may have to cache multiple pages based on the querystring parameters passed. For example, you may have an RSS feed that has no parameters, and more specific feeds that return data for a specific subset and each of these should be cached.

    You can omit the lcKey parameter on all of the Cache methods to automatically create a key that is specific to the URL entered, so that you can in effect cache multiple version of a page. So for example, on the Message Board RSS feed you can cache the default cache feed, and the cache feed for just the Web Connection Forum, and the feed for the Internet Protocols etc. etc.

    Table based caching

    The wwCache class by default uses Cursors, which means that each instance of Web Connection uses a separate cache cursor. However, you can specify to use a fixed table by setting the cFixedFileName property which results in a sharable table that can be used by all instances simultaneously. The advantage is that you have a common cache pool and all are seeing exactly the same results. For very busy sites it's recommended that you do use a shared table to minimize cache incoherency (mismatched between caches in different instances).

    Note that if you use a table based cache file you will need to pack and reindex the file occasionally as the file can quickly get large! Make sure you do this occasionally to avoid memo bloat and index corruption and keep the cache size manageable and performing fast.

    When and How to use Caching


    Caching is a great technology feature, but you want to be sure to understand what it implies. Caching is used to improve performance of repeated requests that are essentially semi-static - they get created once and then then don't need to be refreshed dynamically for some period of time.

    Typical examples of cachable content include things like RSS feed, Product category pages in an E-Store, News pages, Home Pages in dynamic Web sites etc.

    Caching is most efficient for busy sites where many hits can be saved by using Caching.

    You should be careful to cache content for too long of a period. By doing so you making it more difficult to refresh content once it is loaded into the cache. The longer the cache expiration period the more concern there is about stale content being served. That said you should realize that on a busy site short cache times can reap enormous benefits. If you have a site that is getting 10 hits a second to a specific page and you cache this page for a mere 1 minute you are serving this page 600 times for actually generating the text for it once. Increasing the cache expiration will not improve the performance of this operation (unless it is extremely lengthy) by much. Keeping cache expiration as short as possible should be considered.

    Post Cache Replacement

    The wwCache class returns results as strings, which means you also have the possibility of updating the Cache content once it's been retrieved from the cache. If you have a page that is 90% cacheable but contains a small block of updatable content (ie. a shopping cart summary in an E-Store) you can leave a placeholder in the cached content and perform a STRTRAN() on that data for quick post-cache substitution. This is a very powerful feature!

    II6 and later HTTP Caching

    If you're using IIS 6 you can also take advantage of Web Connection's support for Kernel Caching. Kernel Caching allows a page to be cached by IIS so it never actually hits your Web Connection backend while cached. To do this you can use the
    wwHttpHeader::AddCacheHeader() method to apply maximum caching for the request:

    LOCAL loHttpHeader as wwHttpHeader loHttpHeader = CREATEOBJECT("wwHttpHeader") loHttpHeader.SetProtocol() loHttpHeader.Setcontenttype("text/xml") loHttpHeader.AddCacheHeader(300) && 5 minutes Response.Write( loHttpHeader.GetOutput() ) Response.Write( STRCONV(lcXML,9) )

    If you use this option be sure that your caching can be full page caching as your Web Connection request will not get hit again.

    wwCache::AddItem


    This method adds an item to the cache.


    o.AddItem(lcKey as String, lcValue as String, lnTimeoutSeconds as Integer)
    

    Parameters

    lcKey as String
    Key of the item to add.

    If this value is omitted inside of Web Connection the key becomes the Script Name + QueryString which should uniquely identify this entry.

    lcValue as String
    String value of the item to add.

    lnTimeoutSeconds as Integer
    Optional - The timeout in seconds for the item. if not specified the default cache timeout is used.

    wwCache::Expire


    Removes all expired entries from the cache data file.

    o.Expire()
    

    wwCache::GetItem


    Retrieves an item by key from the cache.




    o.GetItem(lcKey as String)
    

    Return Value

    returns the Cache content or NULL if the key doesn't exist

    Parameters

    lcKey as String
    The key of the cached value to retrieve.

    wwCache::IsCached


    Determines whether an item is cached or not.


    o.IsCached(lcKey as String)                                                       
    

    Return Value

    .t. or .f.

    Parameters

    lcKey
    The key to check for existance.

    wwCache::Open


    Opens the cache cursor. Either creates it or just uses it if already open.

    The wwCache class uses a cursor, so each instance of Web Connection uses the same

    o.Open()
    

    wwCache::Reindex


    Packs and reindexes the file specified in the cFixedFileName. This method works only if this property is set and only if an exclusive lock can be set on the table.

    o.Reindex()                                                        
    

    wwCache::Remove


    This method removes an entry from the Cache.

    o.Remove(lcKey)                                                    
    

    Parameters

    lcKey
    The key to remove

    wwCache::cCacheCursor


    The name of the cursor used for caching.

    Default Value

    Initial value: __wwCache

    wwCache::cFixedFileName


    This property allows you to specify a fixed filename for the cache. including the .DBF extension.

    When set, the class will use a fixed file to store cache settings which allows multiple instances to share a single cache file.

    Remember that if you set this property you will use a FoxPro table that must be packed and Reindexed from time to time to avoid size overloading and index corruption. You can use the Reindex method of this class to pack and reindex the file.

    o.cFixedFileName                                                   
    

    wwCache::nDefaultCacheTimeout


    The default timeout for the cache in seconds.

    o.nCacheTimeout
    

    Default Value

    Initial value: 3600

    Class wwUserSecurity


    The wwUserSecurity class provides a base interface for implementing FoxPro table and Windows Auth user authentication. The class includes user retrieval and non-visual management features like adding deleting etc.

    Custom
      wwUserSecurity

    Class Members

    MemberDescription
    Authenticate Tries to authenticate a user. If the user logon is successful the user record is set to the selected user. If it fails the method returns .F. and returns a blank User record.
    o.Authenticate(lcUsername, lcPassword)
    AuthenticateNt Authenticates username and password against Windows System accounts.
    o.AuthenticateNt(lcUsername,lcPassword)
    Close Closes the user file.
    o.Close()
    CreateTable Creates the User table if it doesn't exist. Called by the Open method.
    o.CreateTable(lcFileName)
    DeleteUser Deletes the currently selected user.
    o.DeleteUser()
    GetUser Retrieves a user into the oUser member based on a PK or Username and Password lookup.
    o.GetUser(lcPK, lcPassword)
    GetUserByUsername Like the GetUser() method but retrieves a user by username rather than by PK.
    o.GetUserByUsername(lcUsername)
    NewUser Creates a new user record and stores it in the oUser member data. You need to fill the data and then call SaveUser() to commit the data to disk.
    o.NewUser()
    Open Opens the user file and/or selects it into cAlias. If the table is already open this method only selects the Alias.
    o.Open(lcFileName, llReOpen, llSilent)
    Reindex Reindexes and compacts the user table.
    o.Reindex()
    SaveUser Saves the currently active user to file. Saves the oUser member to the database.
    o.SaveUser()
    calias Alias of the user file.
    cdomain Domain name when using NT Authentication for request.
    cerrormsg Holds error messages when lError = .T. or when any methods return False.
    cfilename Filename for the user file.
    lcasesensitive Determines wheter usernames and passwords are case sensitive. The default is .F.
    lerror Error Flag. True when an error occurs during any operation. Set but not required as most methods return True or False.
    ndefaultaccounttimeout The default value when the account should time out in days. Leave this value at 0 to force the account to never timeout.
    nminpasswordlength Minimum length of the password.
    oUser The actual member that holds user data. Filled by the GetUser and NewUser methods.

    Requirements

    Assembly: wwUserSecurity.prg

    wwUserSecurity::Authenticate


    Tries to authenticate a user. If the user logon is successful the user record is set to the selected user. If it fails the method returns .F. and returns a blank User record.

    By default authentication occurs against a UserSecurity table specified in cFilename and cAlias. lCaseSensitive determines whether the username/password have to be case sensitive.



    o.Authenticate(lcUsername, lcPassword)
    

    Parameters

    lcUsername
    The username to authenticate

    lcPassword
    The password to validate

    wwUserSecurity::AuthenticateNt


    Authenticates username and password against Windows System accounts.


    o.AuthenticateNt(lcUsername,lcPassword)
    

    Parameters

    lcUsername
    Windows Username

    lcPassword
    Windows Password

    wwUserSecurity::Close


    Closes the user file.



    o.Close()
    

    wwUserSecurity::CreateTable


    Creates the User table if it doesn't exist. Called by the Open method.

    o.CreateTable(lcFileName)
    

    Parameters

    lcFileName

    wwUserSecurity::DeleteUser


    Deletes the currently selected user.

    o.DeleteUser()
    

    wwUserSecurity::GetUser


    Retrieves a user into the oUser member based on a PK or Username and Password lookup.

    GetUser always sets the oUser member even on failure in which case the value is set to an empty object.


    o.GetUser(lcPK, lcPassword)
    

    Return Value

    .T. or .F. sets oUser member.

    Parameters

    lcPK
    A string PK for the user to retrieve

    If 2 parameters are passed this parameter represents a Username

    lcPassword
    The password for the user to retrieve if 2 parameters are passed and the first parameter is a username. If username and password are passed behavior of this method is similar to Authenticate

    wwUserSecurity::GetUserByUsername


    Like the
    GetUser() method but retrieves a user by username rather than by PK.

    o.GetUserByUsername(lcUsername)
    

    Parameters

    lcUsername
    The Username to retrieve.

    wwUserSecurity::NewUser


    Creates a new user record and stores it in the oUser member data. You need to fill the data and then call SaveUser() to commit the data to disk.

    Note only works with Table based security.

    o.NewUser()
    

    wwUserSecurity::Open


    Opens the user file and/or selects it into cAlias. If the table is already open this method only selects the Alias.

    o.Open(lcFileName, llReOpen, llSilent)
    

    Return Value

    .T. or .F.

    Parameters

    lcFileName
    The dbf filename to open.

    llReOpen
    Forces the file to re-opened even if it is already open. Used to force file into a specific work area.

    llSilent
    If .T. a FileOpen dialog is not displayed if the file cannot be found.

    wwUserSecurity::Reindex


    Reindexes and compacts the user table.

    o.Reindex()
    

    wwUserSecurity::SaveUser


    Saves the currently active user to file. Saves the oUser member to the database.

    o.SaveUser()
    

    Return Value

    .T. or .F.

    wwUserSecurity::calias


    Alias of the user file.

    Default Value

    Initial value: usersecurity

    wwUserSecurity::cdomain


    Domain name when using NT Authentication for request.

    Default Value

    Initial value: .

    wwUserSecurity::cerrormsg


    Holds error messages when lError = .T. or when any methods return False.

    wwUserSecurity::cfilename


    Filename for the user file.

    Default Value

    Initial value: usersecurity

    wwUserSecurity::lcasesensitive


    Determines wheter usernames and passwords are case sensitive. The default is .F.

    Default Value

    Initial value: .F.

    wwUserSecurity::lerror


    Error Flag. True when an error occurs during any operation. Set but not required as most methods return True or False.

    Default Value

    Initial value: .F.

    wwUserSecurity::ndefaultaccounttimeout


    The default value when the account should time out in days. Leave this value at 0 to force the account to never timeout.

    Default Value

    Initial value: 30

    wwUserSecurity::nminpasswordlength


    Minimum length of the password.

    Default Value

    Initial value: 4

    wwUserSecurity::oUser


    The actual member that holds user data. Filled by the GetUser and NewUser methods.

    Pk
    C (10)
    The unique ID for the user. Shouldn't be manually set usually automatically created by NewUser() with SYS(2015)

    Username
    C (15)
    The User Id for the user used in password validation

    Password
    C (15)
    The password for validating the user.

    Fullname
    C (40)
    The full name.

    Mappedid
    C (15)

    Email
    M (4)
    The email address for the user.

    Notes
    M (4)

    Properties
    M (4)

    Log
    M (4)

    Admin
    L (1)

    Created
    T (8)

    Laston
    T (8)

    Logoncount
    I (4)

    Active
    L (1)

    Expireson
    D (8)




    o.oUser                                                     
    

    Web Control Framework (New)


    The Web Connection WebControl Framework is a rich, object oriented Web Development framework similar to ASP.NET that lives ontop of the core Web Connection engine. Although there are similiarities to ASP.NET and you can use Visual Studio 2005/Visual Web Developer to create your HTML Script pages, it is based 100% on FoxPro code.

    The framework uses a control based architecture that lets you access Web content through controls with properties rather than dealing with low level HTML elements directly (although that is still completely possible). In additon an event based model makes it much easier to write isolated code for specific actions instead of monolithic methods that need to handle lots of different operations. A Web Page button click can be mapped to a FoxPro method in a Page class for example.

    The framework supports visual editing support in Visual Studio .NET 2005/Visual Web Developer 2005 including using property editors. An included script parser can use script pages created in VS.NET and turn them into a FoxPro class that can be executed in Visual Foxpro. Your code then implements another class that provides the base for this generated class and lets you create you FoxPro business application logic.

    The Page Pipeline has the following advantages over traditional Web Connection Applications:



    How the Web Control Framework works


    The Web Connection WebControl Framework is a powerful abstraction layer for creating Web based content. It brings a control based architecture to Web development which removes to a large degree the low level requirements for accessing Web content, and instead allows interacting with more familiar user interface objects such as textboxes, labels, listboxes and so on.

    The architecture is fairly large under the covers, but it follows a relatively simple programming paradigm that's based on Inheritance, Containership, Delegation and Eventing.

    In conversational terms the idea of the WebControl framework is this:

    Although a very simple view of the Page framework it describes accurately the process involved. The key to understanding the framework is:


    Page Pipeline for automatic Page Processing

    The Page Arcititecture provides the basic inheritance and containership model. All Controls derive from a common wwWebControl class which provides a lot of the core behavior for controls. All other classes are derived from this class. Controls can either be simple controls or containers. Containers have the ability to contain child controls. The parent controls send event messages to the child controls to perform processing.

    Each control is a self contained unit and responsible for its own state (with a few exceptions like Viewstate which is stored on the form). Events fire on the page which is the highest level container. Page then delegates the events down to each control in turn. The key method where most controls do their core processing is the Render() method that is responsible for generating HTML output for the control.

    Render() is but one of the 'events' that are fired in specific order for each control that exists on a Page. Each control supports additional events that can either be manually fired - or more commonly - get fired from a Page object and its Run() method. The Run() method fires a sequence of events against all the controls contained on it.

    The Page Pipeline has the following advantages over traditional Web Connection Applications:


    Control Architecture


    A WebControl application is based on the concept of WebControls. Each control in this framework is a self contained unit that knows how to display itself and process its own event messages and act upon them, updating its control state.

    wwWebControl is the base for all other controls

    Every object in the framework - including the top level wwWebPage container - is based on the wwWebControl class, which provides much of the core processing and behavior for most controls.

    The framework is based on a hierarchy of controls. The basis of all controls is the WebControl base class which provides the core functionality. All other controls are based on the wwWebControl class, wwWebTextBox, wwWebCheckbox, wwWebDropDownList etc. all inherit a common set of functionality and behaviors. In addition there are a set of list classes - wwWebDropDownList, wwWebListBox, wwWebRadioButtonList - which inherit from wwWebListControl that provides sub-item functionality.

    wwWebControl implements many common properties. There are display properties like Width, Height, Style, CSSClass. There are databinding related properties like ControlSource and ControlSourceFormat, there are raw HTML properties like PreHtml and PostHtml. The control also implements a lot of stock behavior such as automatically managing ViewState and provides methods that can provide easy implementation of reading POST data and performing databinding. It's important to understand that every control can choose to completely override these behaviors, or simply pick and choose which features to use and which one to leave alone.

    The important thing to understand about controls is that they are a self contained unit and the control and the control alone is responsible for creating output and managing its internal state. Controls are self-contained and designed to work outside of the Page Framework so you can use the controls in standard Web Connection Process code for rendering output. In this scenario, you can simply set a few properties and call the Render() method to generate the output.

    Render() is the key method for most controls that's responsible for generating the final output for each control and for most of the controls in the framework this method does 90% of the work. However, there are many other methods and events that all work together if you use the controls inside of the Page Pipeline.

    Containership is key

    Each control implements its own functionality, but by themselves the controls are fairly limited. While you can set properties and call the Render() method on all controls, the real power of the controls isn't realized until they are 'activated' via events that fire through the Page Pipeline.

    The Page Pipeline works through containership. Everytime a control is added the control is added to the parent's ChildControls collection and the added control gets a reference to the ParentControl. Through this model the parent control can reference all children and fire events on it. And the childcontrol always has access to its immediate parent control as well as the Page object, which provides many services like ViewState and ClientScript registration features for example.

    The following figure shows the control containership model.

    Note that the wwWebPage contains most of the other controls. wwWebForm is a special 'control' that provides the Postback form control and its end tag. Most controls are simple controls like TextBox, ListBox, but there are also a couple of container controls like wwWebDataGrid which can contain Column controls, and the Panel control which can contain any number of controls.

    The containership is managed through FoxPro code which looks like this:

    DEFINE CLASS HelloWorld_Page_WCSX AS HelloWorld_Page Id = [HelloWorld_Page] *** Control Definitions form1 = null txtCompany = null btnSubmit = null btnChangeColor = null lblMessage = null FUNCTION Init(loPage) DODEFAULT(loPage) __lcHtml = [] __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <html>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <head>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <title>Hello World Demo</title>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <link href="westwind.css" rel="stylesheet" type="text/css" />]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ </head>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <body>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [] _1LZ06ZN6P = CREATEOBJECT("wwWebLiteral",THIS) _1LZ06ZN6P.Text = __lcHtml THIS.AddControl(_1LZ06ZN6P) THIS.form1 = CREATEOBJECT("wwWebform",THIS) THIS.form1.Id = "form1" THIS.AddControl(THIS.form1) __lcHtml = [] __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <div>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <h1>Hello World Demo</h1>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <p>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <a href="default.htm">Demo Home</a></p>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <br />]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ Please enter a Customer Name:<br />]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [] _1LZ06ZN6R = CREATEOBJECT("wwWebLiteral",THIS) _1LZ06ZN6R.Text = __lcHtml THIS.AddControl(_1LZ06ZN6R) THIS.txtCompany = CREATEOBJECT("wwwebtextbox",THIS) THIS.txtCompany.Id = "txtCompany" THIS.txtCompany.Width = [252px] THIS.AddControl(THIS.txtCompany) __lcHtml = [] __lcHtml = __lcHtml + [&nbsp;]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [] _1LZ06ZN6T = CREATEOBJECT("wwWebLiteral",THIS) _1LZ06ZN6T.Text = __lcHtml THIS.AddControl(_1LZ06ZN6T) THIS.btnSubmit = CREATEOBJECT("wwwebbutton",THIS) THIS.btnSubmit.Id = "btnSubmit" THIS.btnSubmit.Text = [Go] THIS.btnSubmit.Width = [80] THIS.btnSubmit.HookupEvent("Click",THIS,"btnSubmit_Click") THIS.AddControl(THIS.btnSubmit) __lcHtml = [] __lcHtml = __lcHtml + [&nbsp;] _1LZ06ZN6W = CREATEOBJECT("wwWebLiteral",THIS) _1LZ06ZN6W.Text = __lcHtml THIS.AddControl(_1LZ06ZN6W) THIS.btnChangeColor = CREATEOBJECT("wwwebbutton",THIS) THIS.btnChangeColor.Id = "btnChangeColor" THIS.btnChangeColor.Text = [Change Color] THIS.btnChangeColor.HookupEvent("Click",THIS,"btnChangeColor_Click") THIS.AddControl(THIS.btnChangeColor) __lcHtml = [] __lcHtml = __lcHtml + [&nbsp;]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <br />]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <br />]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [] _1LZ06ZN6Z = CREATEOBJECT("wwWebLiteral",THIS) _1LZ06ZN6Z.Text = __lcHtml THIS.AddControl(_1LZ06ZN6Z) THIS.lblMessage = CREATEOBJECT("wwweblabel",THIS) THIS.lblMessage.Id = "lblMessage" THIS.lblMessage.Attributes.Add("size","20") THIS.AddControl(THIS.lblMessage) __lcHtml = [] __lcHtml = __lcHtml + [</div>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [] _1LZ06ZN71 = CREATEOBJECT("wwWebLiteral",THIS) _1LZ06ZN71.Text = __lcHtml THIS.AddControl(_1LZ06ZN71) _1LZ06ZN72 = CREATEOBJECT("wwWebForm",THIS) _1LZ06ZN72.RenderType = 2 THIS.AddControl(_1LZ06ZN72) __lcHtml = [] __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ </body>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ </html>] _1LZ06ZN73 = CREATEOBJECT("wwWebLiteral",THIS) _1LZ06ZN73.Text = __lcHtml THIS.AddControl(_1LZ06ZN73) ENDFUNC ENDDEFINE

    Everything on a WebPage - including literal text - is an object. The above class was autogenerated from a script page that uses declarative syntax to describe this same page. The page looks like this:

    <%@ Page Language="C#" ID="HelloWorld_Page" GeneratedSourceFile="controldemo\Helloworld_page.prg" %> <%@ Register Assembly="WebConnectionWebControls" Namespace="Westwind.WebConnection.WebControls" TagPrefix="ww" %> <html> <head> <title>Hello World Demo</title> <link href="westwind.css" rel="stylesheet" type="text/css" /> </head> <body> <form id="form1" runat="server"> <div> <h1>Hello World Demo</h1> <p> <a href="default.htm">Demo Home</a></p> <br /> Please enter a Customer Name:<br /> <ww:wwWebTextBox ID="txtCompany" runat="server" Width="252px"></ww:wwWebTextBox>  <ww:wwWebButton ID="btnSubmit" runat="server" Text="Go" Width="80" Click="btnSubmit_Click" />  <ww:wwWebButton ID="btnChangeColor" runat="server" Text="Change Color" Click="btnChangeColor_Click" />  <br /> <br /> <ww:wwWebLabel ID="lblMessage" runat="server" size="20"></ww:wwWebLabel> </div> </form> </body> </html>

    The advantage of the markup format is that it can also be edited in an ASP.NET compatible environment like Visual Studio 2005:

    Web Connection includes a WebPageParser class that is integrated to automatically convert the markup code to the VFP code shown above.

    In this example the page is fairly one dimensional as there are no other containers on the form. You could however have a contained panel control that in turn contains other controls. Once the controls are available as a class however, it's becomes very easy to reference them directly and assign properties to them.

    Master of Ceremonies: wwWebPage

    The master container is the wwWebPage object. The Page object is the start of all operations that sits of the top of the hierarchy. It in turn contains other controls, which then in turn can contain other controls. Typically a WebPage contains controls like Labels, TextBoxes, Checkboxes etc. But it can also contain container objects such as Panels that can in turn contain other controls. This is an important aspect of the framework as it allows you to build composite controls that contain a lot of functionality. In addition, container controls make it real easy to show and hide blocks of controls.

    The Page object is the master event object in that is starts the event sequence at the top level of the containership hierarchy. It's Run() method fires off an event sequence that activates each of the controls on the form by delegating the events to each of the child controls.

    Every event fired on the Page also delegates this same event to its child controls. So when OnLoad fires on the Page it also pushes this event down to the textboxes and checkboxes and labels on the form, which may or may not have code in these event implementations. If there's a container control that receives an event from the Page it will then delegate the event down to its child events in turn.

    This simple mechanism makes it possible to build a sophisticated framework of controls that all respond to a common set of events in a consistent order. As a developer building a top level application, this happens all behind the scenes. But as a control developer you have the ability to hook into the events and override or modify existing functionality, which provides incredibly powerful access to the page processing.



    Page Event Sequence


    The page event sequence is crucial to understanding how the WebControl Framework works. A request runs through the entire pipeline started by the wwWebPage::Run method which kicks off the whole process of events. The Run method fires the events shown in the figure below on the Page object, which in turn delegates these events to any ChildControls of the Page. Any container object in turn delegates the events into their ChildControls in turn.

    Any of these events can be intercepted either by subclassing the control or Web Page or by using BindEvent() to attach to the event method. The most common implementation is in your custom Page implementations where you create your application level code.

    The following figure shows the high level event sequence:

    Although the image above shows the wwWebForm event sequence, this sequence really applies to all controls as Parent controls fire these same events on each one of their ChildControls.

    1. OnInit()
      Fired on Initialization of the control. The control should make no assumptions about other controls or the Page at this time. The firing order is controls fire first, the container last.This method is a good place to add new control via code or modify startup properties of the currently executing control.

    2. OnLoadViewState()
      Called immediately after Init is complete and loads up control state from ViewState. ViewState is a special state mechanism that is written into a hidden form variable and posted back when the page is returned and it allows tracking of values that don't POST back. For example, it can help track things like button captions and colors etc. Viewstate comes in two flavors: Implicit and Explicit. Implicit is used by the various controls to persist themselves as needed. For example, the DataGrid always persists the current PageIndex so it can be picked up when the page returns. Textboxes persist their text IF a control is disabled or invisible. Explicit Viewstate lets you persist specific values by either using the Control.PersistProperty() method or by using the ViewState collection to add values to ViewState manually. ViewState loads up controls state before POST data is applied - IOW, POST data overrides any values set in Viewstate.

    3. OnLoadControlState()
      Called right after ViewState aquisition. This method loads control values into their DefaultProperty value from POST data. This means TextBox Text, CheckBox Checked, List SelectedValue values are read from the POST buffer and assigned providing your application with controls value access to Form data without writing any Request code.

    4. OnLoad()
      Fired after the control has completely loaded. At this time the state of the control should be stable and the state of the control set from ViewState or POST buffer. OnLoad is the key method for doing 'business work' on a control. Page.OnLoad() is the method that is used to do most of your non-event page processing and all of your processing in read-only pages.

    5. FireEvents()
      Once OnLoad has fired FireEvents() is called which causes any events fired from the client to be routed to the appropriate event handler. So, if a button click event is configured the Page's OnLoad() method fires first, followed by the Event method configured for the button. Events are wired via BINDEVENT in VFP and bind to event method configured on the base WebControl method.

    6. OnPreRender()
      OnPreRender() is a last chance hook that is used after supposedly all processing is complete on the page, but just before the page starts rendering. This method is useful to check flags to see whether some task was completed else where and if not take final action to configure the output. It's also a good hook to create State Machine like formatting of control properties.

      For example, you may have a form with a grid on it, but there are many controls that determine how that grid renders. So you might hook a ShowGrid method from the PreRender() method that is responsible for setting all the grid properties and Databinding the grid at the last minute with all the settings that were previously made. Usually this approach is much better than applying specific Databinding in events or OnLoad as it often results in multiple databinding calls.

    7. Render()
      Render() is responsible for generating the final output for the control. For most controls this method does most of the work and pulls together all the pieces to render the final output - usually HTML. Render() manages many aspects of the control including dealing with visibility (basically not rendering if invisible), DisplayError display and managing PreHtml and PostHtml. Control designers should look carefully at implementations of the base controls to see what is required.

    8. OnSaveViewState()
      The last step on the pipeline is to save any Viewstate that has been set by the page or simply is forwarded from the previous page hit. Each control again manages this on its own and the Form takes all the control's viewstate values and combines them into a single value that gets wrapped up and written into a hidden form variable in the HTML document.

      Note: Unlike ASP.NET Web Connection's use of Viewstate is very lightweight as it doesn't persist things like list data content. Rather it persists only crucuial state values and values like that of disabled controls which is required. You can persist those kinds of values if you choose but you have to do it explicitly.

    9. OnError()
      Every page also gets an OnError event fired in the event of an error. You can choose to handle the error on the page level or let it bubble back up to the wwPageProcess class to handle.

    10. Dispose()
      This method isn't really an event but it's fired off the Destroy. Dispose() is very crucial to the framework in that it makes sure that all child controls and dependencies are destroyed correctly. Control developers need to make 100% sure that Dispose() releases all references to other controls explicitly! This includes references to parent controls and any child controls. Dispose should always call the Dispose() method of any child controls before performing its own cleanup code.



    Script Page Support


    The control architecture provides the base functionality for the framework and it's entirely responsible for rendering a page. A page is essentially rendered by dropping controls that each render their own portion of the page. The process is based on containership and an event pipeline that causes operations to fire on each of the controls of the page.

    You can also drive the controls through script code that looks and feels a lot like ASP.NET code. In fact you can use many ASP.NET native controls directly with the framework. Basically any ASP.NET control that has a direct match in the Web Connection framework can be used although you have to realize that the functionality is not necessarily one to one.

    The script engine works through code generation and works by parsing the script page and generating Web Connection Page Controls from the HTML document containing the script and control markup. The WebPageParser class performs this job and can do this job on the fly in development mode. Once generated the code become completely independent of the original script page as the code is pure PRG code that is executed inside of Visual FoxPro.

    Script Page Rules


    Script Pages follow some rather strict rules in order for the Web Connection Page parser to be able to parse the pages. Although the Web Control framework closely follows the ASP.NET syntax, this doesn't mean that all ASP.NET controls are supported, nor that all properties or syntax options are available.

    The following rules must be observed:



    From Script To Code


    The Web Control Framework is designed to work with HTML Script pages and uses a format that can be used in Visual Studio or any other .NET capable environment. The idea is that you can use a rich editing environment such as VS.NET 2005/2008 and create your script pages.

    A Web Connection script page is defined as follows:

    <%@ Page Language="C#" ID="Test_Page" GeneratedSourceFile="~\webcontrols\Test_Page.prg" %> <%@ Register Assembly="WebConnectionWebControls" Namespace="Westwind.WebConnection.WebControls" TagPrefix="ww" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <link href="westwind.css" rel="stylesheet" type="text/css" /> </head> <body style="margin-top:0px;margin-left:0px"> <form id="form1" runat="server"> <ww:wwWebErrorDisplay runat="server" id="ErrorDisplay" Text=' rick="test"' /> </form> </body> </html>

    It's vital that a few key elements are placed into the page:

    Note:
    In order to render the Web Connection controls in VS.NET's visual designer you need to add the wwWebControls.dll into your Bin directory and at the following below the page directive:
    <%@ Register Assembly="wwWebControls" Namespace="Westwind.Web.Connection.WebControls" TagPrefix="ww" %>

    Script to Code

    Web Connection converts the script page you generate in your editor into a Visual FoxPro 8.0 or later PRG file by reading the generatedSourceFile, id and inheritedClass attributes. The Page Compiler is available in WebPageCompiler.prg:

    DO WebPageParser with "d:\westwind\wconnect\webControls\FirstForm.wcsx"

    which creates the following source code file:

    #INCLUDE WCONNECT.H *** Small Stub Code to execute the generated page PRIVATE __WEBPAGE __WEBPAGE = CREATEOBJECT("FirstForm_Page_WCSX") #IF DEBUGMODE __WEBPAGE.Run() RELEASE __WEBPAGE #ELSE TRY __WEBPAGE.Run() FINALLY *** MUST MAKE ABSOLUTELY SURE WE RELEASE RELEASE __WEBPAGE ENDTRY #ENDIF RETURN ************************************************************** DEFINE CLASS FirstForm_Page as WebPage *************************************** *** Your Implementation Page Class - put your code here *** This class acts as base class to the generated page below ************************************************************** FUNCTION OnLoad() ENDFUNC ENDDEFINE *# --- BEGIN GENERATED CODE BOUNDARY --- #* ******************************************************* *** Generated by PageParser.prg *** on: 08/29/2005 01:13:55 AM *** *** Do not modify manually - class will be overwritten ******************************************************* DEFINE CLASS FirstForm_Page_WCSX AS FirstForm_Page *** Control Definitions frmFirstForm = null txtHello = null btnSubmit = null FUNCTION Init(loPage) DODEFAULT(loPage) __lcHtml = [] __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + [<html>] _1LO02N2KV = CREATEOBJECT("WebLiteralControl",THIS) _1LO02N2KV.Text = __lcHtml THIS.AddControl(_1LO02N2KV) __lcHtml = [] __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <title>Untitled Page</title>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [</head>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [<body>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ ] _1LO02N2KW = CREATEOBJECT("WebLiteralControl",THIS) _1LO02N2KW.Text = __lcHtml THIS.AddControl(_1LO02N2KW) THIS.frmFirstForm = CREATEOBJECT("WebForm",THIS) THIS.frmFirstForm.Id = "frmFirstForm" THIS.AddControl(THIS.frmFirstForm) __lcHtml = [] __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ Here's some Html Text]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ ] _1LO02N2L5 = CREATEOBJECT("WebLiteralControl",THIS) _1LO02N2L5.Text = __lcHtml THIS.AddControl(_1LO02N2L5) THIS.txtHello = CREATEOBJECT("webtextbox",THIS) THIS.txtHello.Id = "txtHello" THIS.txtHello.value = [Hello] THIS.AddControl(THIS.txtHello) __lcHtml = [] __lcHtml = __lcHtml + [ ]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ ] _1LO02N2L6 = CREATEOBJECT("WebLiteralControl",THIS) _1LO02N2L6.Text = __lcHtml THIS.AddControl(_1LO02N2L6) THIS.btnSubmit = CREATEOBJECT("webbutton",THIS) THIS.btnSubmit.Id = "btnSubmit" THIS.AddControl(THIS.btnSubmit) __lcHtml = [] __lcHtml = __lcHtml + [ ]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ ] _1LO02N2L7 = CREATEOBJECT("WebLiteralControl",THIS) _1LO02N2L7.Text = __lcHtml THIS.AddControl(_1LO02N2L7) _1LO02N2L8 = CREATEOBJECT("WebForm",THIS) _1LO02N2L8.RenderType = 2 THIS.AddControl(_1LO02N2L8) __lcHtml = [] __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + [</body>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [</html>] _1LO02N2L9 = CREATEOBJECT("WebLiteralControl",THIS) _1LO02N2L9.Text = __lcHtml THIS.AddControl(_1LO02N2L9) ENDFUNC ENDDEFINE *# --- END GENERATED CODE BOUNDARY --- #*

    There are three parts to this source file:

    Note the class hierarchy: Your code implements the base class and the generated class inherits from this base class. The stub above implements the generated class, calls the run method and output is generated into the Response object.

    In Web Connection if you wanted to run this form now you could simple create a Process class and DO

    FUNCTION Process_DoSomeThing DO FirstForm_page ENDFUNC

    And that's it!

    Easy as that is there's actually an easier and more convenient way to do this.

    How scripts get fired from a wwProcess class

    Web Connection 5.0 integrates Web Control processing right into the core framework. wwProcess works by default looking for a matching method to execute based on the page name. In Version 5.0 the framework has been extended so if a method cannot be found it will try to execute the physical page that it maps to on disk. The default execution mode is to process the page as Web Control page.

    There's a flag on the wwProcess class - nPageScriptMode that determines this operation and the default is 2 which is to execute the page as Web Control Page. You can also use a value of 1 which is the 'legacy' behavior that uses ExpandTemplate instead.

    This means that you can use the same wwProcess based class as your base handler for both method based implementations AND use Web Control Pages. This means you can use a single configuration point for

    ************************************************************************ *PROCEDURE wwPageDemo **************************** *** Function: Processes incoming Web Requests for wwPageDemo *** requests. This function is called from the wwServer *** process. *** Pass: loServer - wwServer object reference ************************************************************************* LPARAMETER loServer LOCAL loProcess #INCLUDE WCONNECT.H LOCAL loProcess loProcess=CREATE("wwPageDemo",loServer) loProcess.lShowRequestData = loServer.lShowRequestData IF VARTYPE(loProcess)#"O" *** All we can do is return... WAIT WINDOW NOWAIT "Unable to create Process object..." RETURN .F. ENDIF *** Call the Process Method that handles the request loProcess.Process() RETURN ************************************************************* DEFINE CLASS wwPageDemo AS wwWebPageProcess ************************************************************* nPageScriptMode = 2 && Web Control Pages ********************************************************************* FUNCTION HelloWorld() ************************ THIS.StandardPage("Hello World from wwPageDemo process",; "If you got here, everything should be working fine.<p>" + ; "Time: <b>" + TIME()+ "</b>") ENDFUNC * EOF wwPageDemo::HelloWorld *** Recommend you override the following methods: *** ErrorMsg *** StandardPage *** Error ENDDEFINE

    In fact web Connection automatically maps this process to the WCSX extension within your Web application.


    Note:
    If you use some alternate extension you need to make sure you create a scriptmap and add the custom extension to the <youapp>Main.prg 's Process method.

    Once this is in place you can now instantiate your demo simply by going to the name of the page.

    http://localhost/wconnect/webcontrols/firstForm.wcsx

    Make sure that you have at least compiled this page once.

    Compiling and Debug Modes

    As you may have surmised from the above, you need to compile your pages. Without compilation there's nothing to run, so you must compile.

    The wwWebPageProcess class can run in one of two modes:

    Generally you'll want to work in Debug mode in the development environment, just be aware that the Server.nScriptMode flag has no effect once DebugMode is turned off.

    Very Important Project Note:
    Because pages are loaded dynamically by Web Connection, you have to make sure you add pages to your compiled project manually. If you don't the VFP project manager will not include your generated classes and your custom code. Either add them manually or add a dummy method that is never called to your project that executes each of the pages.


    How DataBinding works


    DataBinding is a crucial piece in the productivity for the Web Control Framework. As we are usually dealing with data driven applications getting data to display in pages is the most important thing.

    The Control framework supports two modes of DataBinding using ControlSource DataBinding for single value controls like TextBoxes, CheckBoxes as well as Listbased DataBinding for list controls like the DataGrid, DropDowns and ListBoxes.


    DataBinding Methods and Properties


    As we're using Visual FoxPro we're pretty much always interested in database access. Web Connection makes consuming data in the Web Controls framework fairly easy. There are two kinds of databinding mechanisms supported by the framework.

    ControlSource Binding (Single Control Binding)

    Control Source binding works similar to the way it works in Visual FoxPro. You assign a ControlSource which is an expression that is used for binding and unbinding a value from a control to an expression. For two-way controls (TextBox, CheckBox etc.) the expression must be able to read and write data. For one way controls Controlsources can be any valid expression.

    The following properties and methods apply:

    DataSource Binding (List Binding)

    DataSource binding applies to list controls like the wwWebListBox, wwWebDropDownList, wwWebRadioButtonList and wwWebDataGrid. It involves setting the DataSource property to a VFP cursor and on some controls providing addition properties for display and value assignments.



    Control Source DataBinding


    ControlSource Databinding allows you to bind all the controls on a form that have a ControlSource set to a property of an object, a variable or database field. Essentially it works just like ControlSource binding in a Desktop form, except that that binding has to be done explicitly by calling the DataBind() and UnbindData() methods of the Page object or each control.

    The most common scenario looks like this:

    1. Create your controls and set their ControlSource
    2. On the first page hit (!this.IsPostback) you DataBind() the control sources in OnLoad()
    3. Subsequent hits are not re-bound, except for non-PostBack fields
    4. You save on a button click or other action
    5. In the save operation you reload the original data, then call Page.UnbindData() to unload the ControlSources

    If we put this into code imagine a simple page that binds to a customer object from a table. For clarity, I'll use a cursor with SCATTER NAME commands here to create objects to bind to, but realize that you should probably use a business object and bind to its properties.

    Assume that in this page there are a number of controls that have their ControlSource set this.oCustomer.Company, this.oCustomer.Name etc. for binding.

    DEFINE CLASS Customer_Page as wwWebPage oCustomer = null nPk = 0 ************************************************************************ * CustomerPage :: Onload **************************************** *** Function: Onload method of the form fires on EVERY hit to the *** page. Note that you don't want to bind every time, *** only on the first hit ************************************************************************ FUNCTION Onload() *** Something to select the customer we'll bind to this.nPk = VAL(Request.QueryString("ID")) *** Only bind on the first page hit *** On Postbacks we want to preserve the values entered IF !this.IsPostBack SELECT * from wwDevRegistry WHERE PK = this.nPK ; INTO CURSOR TQuery IF _TALLY < 1 this.ErrorDisplay.Text = "Invalid Customer" RETURN ENDIF SCATTER NAME this.oCustomer MEMO THIS.DataBind() && Bind the control to this.oCustomer fields ENDIF *** Any non-postback controls like labels, images etc. need to be manually rebound this.lblMessage.DataBind() ENDFUNC ************************************************************************ * CustomerPage :: btnSave_Click **************************************** *** Function: Method that saves the customer entry. We have to reload *** the data first so that we have a base object (or record) *** that the page can unbind to. ************************************************************************ FUNCTION btnSave_Click() *** Must reload the data first so we have something to bind back to IF !USED("wwDevRegistry") USE wwDevRegistry IN 0 ENDIF SELE wwDevRegistry LOCATE FOR Pk = this.nPk IF !FOUND() THIS.ErrorDisplay.ShowError("Invalid Record...") RETURN ENDIF SCATTER NAME this.oCustomer MEMO this.UnbindData() *** Check for binding errors and display on Error control IF this.BindingErrors.Count > 0 this.ErrorDisplay.Text = this.BindingErrors.ToHtml() RETURN ENDIF ENDFUNC ENDDEFINE

    The key thing to understand about ControlSource Binding is that you need to typically bind only once in the lifetime of the form on the Initial page hit which is an HTTP GET. The page is accessed only and displayed. You don't want to rebind the page on subsequent hits because binding overwrites the values the user might have entered into the form.

    To avoid rebinding on every hit you'll want to check Page.IsPostBack which determines whether the page is in a GET or POST operation - POST operations occur only when you're clicking a button or take another action.

    When you want to save the data, you need to make sure that you have something to bind back to. IOW, you have to create the object again preferrably with the original data you used to load the form. This object is then used to receive the values that the user has entered on the HTML form when you call UnbindData().

    In almost any scenario you'll want to check for binding errors after UnbindData() is called by checking the BindingErrors collection. BindingErrors occur if a value is unbound and cannot be stored back into the underlying field. This can include things like invalid number or date formats or because of validation rules you've set on the control (like IsRequired). The BindingErrors Collection allows you to easily get a list of errors and display them on a form in a rich environment. The BindingErrors collection works together with the controls on the form and the Page to provide a rich error display to the user.




    How Controls are parsed


    Controls are parsed as part of the Web page parsing and turned into Visual FoxPro objects that are child objects of the Page. The Parser parses objects by reading their tag name and translating this name into a Visual FoxPro based class match.

    By default the naming works like this: If you have a control on a form like this:

    <ww:wwWebTextBox runat="server" id="txtName" Width="90" onclick="MoreDetail();"/>

    Web Connection looks for a control named wwWebTextBox which must be available at the time the page is parsed. The class is instantiated at parse time, and then properties and events are mapped to the control using PEMSTATUS() to try and match up properties and events. Alternately Web Connection also looks for ASP.NET controls and automatically tries to adjust the name. So:

    <asp:TextBox ... />

    is turned into wwWebTextBox and essentially exhibits the same behavior as the code above.

    Properties of the controls are parsed using PEMSTATUS() to match properties and events. If there's a matching property or event it is assigned directly. Any other tags that aren't recognized are parsed into the Attributes collection of the control and rendered as is. So for example, in the tag above, the client side onclick= attribute doesn't match any properties or events on the FoxPro control, so it's added to the Attributes collection, which then renders the onclick directly into the INPUT tag:

    <input type="text" name="txtName" style="width:90px" onclick="MoreDetail();" />

    If you create controls of your own, the process will work exactly the same way and you can simply reference the controls by their name, prefixed with ww:, as long as you make sure that the controls can be instantiated when the parser is run. If a class cannot be found or there's an error you'll get an error message generated into the page like this:

    [Unsupported Control: wwwebunknowncontrol]

    where the control name is embedded into the brackets.

    Note that the RUNAT attribute is required in order for controls to be recognized as controls. An ID is also required in order for controls to generate accessible names in the page.

    Containership

    Controls can be laid out hiearchically, so you can have controls that contain other controls. Examples are Panel or User Control as well as indirect containers like Lists, DataGrids, Repeaters which can contain list items, columns or item templates respectable.

    Container controls have the IsContainerControl property set to true and must implement the AddControl method that accepts contained objects of specific types. The default implementation of AddControl accepts any control and simply adds it to the container as content which is how Panels and User Controls work. Other controls like the DataGrid, Lists and Repeaters expect very specific types of objects only and skip everything else.




    Understanding Code Generation and Compilation


    WebControl Script Pages require parsing and compilation into FoxPro code. A WCSX style script page is parsed into a Visual FoxPro PRG file that can then run independently of the page on disk.

    Pages can be compiled in two ways:

    Runtime Compilation Note:
    If you are running projects as compiled EXE\APP files and you have included the Web Control Framework page into the project, nPageParseModes 1 and 2 will have no effect on the running application and not update changes to the pages unless you stop and restart the EXE/APP. Web Connection will re-generate the PRG files, but your application uses the compiled version in the project in this case, so no changes are applied. If you want to run EXE/APP files during development make sure you exclude the Web Control Framework Pages and Controls from the project. Our recommendation is that if you are debugging and making frequent changes run the PRG files rather than APPs or EXEs.

    Using WebPageParser Class to parse scripts into PRG Files

    The WebPageParser class handles parsing of script pages into Visual FoxPro classes. The simplest way to compile a file is the plain PRG file syntax:

    *** Compile a page DO webpageParser WITH "d:\westwind\wconnect\webcontrols\testpage.wcsx" *** Compile a user control DO webpageParser WITH "d:\westwind\wconnect\webcontrols\testpage.ascx",2

    This is an interactive format that shows the output generated in a code window immediately after compilation. You can turn off the display by passing a third parameter of .t. for silent operation. The above syntax also support batch compilation if you pass wildcard characters:

    DO webpageParser WITH "d:\westwind\wconnect\webcontrols\*.wcsx"

    How it works

    WebPageParaser looks at the source file specified and automatically reads the GeneratedSourceFile attribute of the wwWebPage tag to write the output to the specified file. If the file does not exist it's created with the generated class, plus a stub class for your code at the top. After the first generation this stub class is never touched again unless you delete the file (Note if you change the Id of the generated class in the script page you will have to adjust the stub code and your class name manually).

    Make sure all Control Classes are loaded!
    The Parser instantiates all Control classes on a page in order to read member information on these classes. This means that all control classes must be in the SET PROCEDURE/SET CLASSLIB stack along with any dependencies that might get loaded via Initialization code. So make sure that before you run the compiler you run your application or do whatever is necessary to load all PRG and VCX based classes into memory.

    The WebPageParser is a class with a number of options which you can customize and utilize for your own compilation tasks. You can look at the source to see the gory details - it' a pretty hairy piece of code. But there are three core methods you can work with from highest level to lowest level.

    To get an idea how the parser works here is the code that runs when you run DO WEBPAGEPARSER:

    #INCLUDE wconnect.h DO WCONNECT && CLEAR oParser = CREATEOBJECT("WebPageParser") oParser.Parsemode = 1 && 1 Page 2 Control oParser.CompileOutputFile = .F. oParser.ParseToFile(lcPhysicalPage) MODIFY COMMAND (oParser.GeneratedSourceFile) nowait ? oParser.GeneratedSourceFile ? oParser.cErrorMsg

    Runtime Auto-Compilation using Server.nPageParseMode

    You can also automatically compile pages using the Server.nPageParseMode property and setting it to 1 (Parse and Run) or 2 (Parse, Compile and Run). When set, Web Connection loads script pages, parses them, compiles them, dynamically loads the script page, executes it and unloads the classes from memory. This process which is not very efficient makes it possible to make changes to script pages without stopping and restarting the Web Connection application. This means you can add new controls, modify properties etc. and see those changes immediately.

    nPageParseModes include: 1 - Parse and Run, 2 - Parse Compile and Run, 3 - Run only. The parse options are primarily meant for development. Option 3 the recommended mechanism for deployed applications due to FoxPro's inability to consistently unload loaded classes from memory across instances.

    The PageParsemode is configurable in <yourApplication>.ini in the PageParseMode key and via the Web Connection Status form.

    Note that it is possible to compile pages at runtime with compiled applications, both using automatic compilation (Modes 1 and 2) or by using WebPageParser. But although it seems tempting to allow dynamic compilation and it works with a single instance of Web Connection, in a multi-instance scenario only the current instance will be able to see the changes to the compiled code. Therefore if you recompile at runtime make sure you unload and reload all Web Connection servers.

    Compilation of user controls

    User Controls are special constructs that act much like a WebPage but are treated like a control that can be dragged onto a form. They allow designing of reusable visual components. As mentioned above User Controls are treated by Web Connection like pure programmatic controls and thus they load and always stay loaded even in DebugMode. They do not unload like Pages. Thus any change to a UserControl requires a start and stop of the Web Connection server.

    User Controls can either be compiled individually like Page using WebPageParser (pass 2 as the second parameter), or they can auto-compile as part of a page that references them. So if Page1.wcsx references UserControl1.ascx, Page1 will trigger a compile of UserControl1.

    Deploying compiled Files

    The page compiler creates standalone PRG files that consists of two classes for each page or control: A generated class and an implementation that you use to attach code to for mainline and event processing. The file generated is a standalone PRG and it is dynamically invoked by the Web Connection framework, which means it's not automatically sucked into a Project file.
    This means you have to either:

    The latter approach is probably the preferable one, but it requires some diligence in making sure you catch all the generated files. The easiest way to force files into your project including future projects is to add a method to one of your existig already loaded PRG files (or create a new one that gets added). In it add a dummy method that loads any of the PRG files by executing them. This method is actually never called but it will force the VFP project manager to import the files.

    * MyApp_Loader.prg *** Add any user and custom controls SET PROCEDURE TO webcontrols\CustomUserControl.prg ADDITIVE RETURN *** Pull in 'evaluated' project files FUNCTION DUMMY DO GuestBook_page.prg DO DataGrid_Page.Prg ENDFUNC

    There's also an utility on the Web Connection | Tools Menu for Drag and Drop PRG and VCX references into a PRG file.

    Deploying Web Control Framework Pages


    It's important to understand how Web Connection works in regards to page compilation and project file inclusion.

    When a WebControl Framework page is compiled and run in the FoxPro IDE it is loaded dynamically from a PRG file. What this means is that the page loads and runs fine in the development environment, but there's no reference for the project manager to pull the generated file or files into a project automatically.

    This means you will have to deal with runtime scenarios carefully. You can do one of two things:

    Add Pages to your Project manually

    There are a number of ways that you can get your files into project. The easiest is to simply use the project manager and add the files as needed or drag and drop the files from Explorer directly into the project manager.

    You'll want to copy all the generated PRG files. By convention I like to name any Pages with a _Page and controls with a _Control postfix so it's easy to find the files that need to be manually managed.

    One problem with this approach is that if you create a new project the files will have to be added again. A better way is to add a method to a new or existing program file and in it include the PRG files as needed:

    FUNCTION WEBLOG_LOADPAGES SET PROCEDURE TO "WEBLOG\WEBLOGFOOTER_CONTROL.PRG" ADDITIVE SET PROCEDURE TO "WEBLOG\WEBLOGHEADER_CONTROL.PRG" ADDITIVE SET PROCEDURE TO "WEBLOG\WEBLOGPOSTS_PAGE.PRG" ADDITIVE SET PROCEDURE TO "WEBLOG\WEBLOGRSSPAGE_PAGE.PRG" ADDITIVE SET PROCEDURE TO "WEBLOG\WEBLOGSIDEBAR_CONTROL.PRG" ADDITIVE SET PROCEDURE TO "WEBLOG\ADMINDEFAULT_PAGE.PRG" ADDITIVE SET PROCEDURE TO "WEBLOG\NEWENTRY_PAGE.PRG" ADDITIVE SET PROCEDURE TO "WEBLOG\SHOWENTRY_PAGE.PRG" ADDITIVE SET PROCEDURE TO "WEBLOG\WEBLOG_ROUTINES.PRG" ADDITIVE SET PROCEDURE TO "WEBLOG\WEBLOGDEFAULT_PAGE.PRG" ADDITIVE ENDFUNC

    This ensures that the files always get included and compiled. To help with generating this list of files there's a helper form in the TOOLS directory and on the Web Connection Menu.

    You can then take the output from this window and paste it into your 'loader' routine.

    Running with FXP files

    You can also skip adding the files to the project altogether and simply run the FXP directly. You can copy all the files to the server and run them directly.

    Some people like this approach if there are many changes to the application so you can just update one or two files, but remember that even though these FXP files are small they do get loaded by the Web Conenction server once they've executed and they are locked once they do. So you still have to shut down the server to update them and you forego some of Web Connection's administrative features.

    Script File Deployment:
    It's important to understand that even though pages compile into the project or an FXP file that are fully self-contained, the script pages (WCSX or custom script mapped pages) are still required on disk in order to allow Web Connection to figure out which PRG file to execute for a given page.

    EXE/APP Execution Note:
    Once the files are inside of your project and you execute the EXE/APP file, Web Connection can no longer dynamically load and unload the Page classes. This means any changes made to Pages are not reflected until you recompile. For development purposes either exclude the files from your project or better yet execute the main PRG file instead (ie. DO <MyApp>Main.prg) to retain full dynamic compilation.


    How Scripts get fired from wwProcess


    Web Connection 5.0 integrates Web Control processing right into the core framework. wwProcess works by default looking for a matching method to execute based on the page name. In Version 5.0 the framework has been extended so if a method cannot be found it will try to execute the physical page that it maps to on disk. The default execution mode is to process the page as Web Control page

    There's a flag on the wwProcess class - nPageScriptMode that determines this operation and the default is 2 which is to execute the page as Web Control Page. You can also use a value of 1 which is the 'legacy' behavior that uses ExpandTemplate instead.

    The default extension: WCSX Pages

    Web Connection integrates the Web Control Framework into the standard Web Connection wwProcess class processing that is used to route requests. By default Web Connection and any new project created through the Wizards is already configured to process any page that ends in WCSX which is routed to the generic wwWebPageHandler class. This class is merely a subclass of wwProcess. This class is meant to be generic and not to be modified.

    This means that WCSX pages run out of the box, but you can't configure the process class and create common entry points for configuring each request. This may or may not be a problem.

    Creating custom Web Control Page extensions

    However, Web Connection can hook Web Control pages to any wwProcess class and by default this functionality is enabled via nPageScriptMode = 2. If a method is not found, Web Connection will try to find the physical page and try to process it using the Web Control Page processing.

    This means that you can use the same wwProcess based class as your base handler for both method based implementations AND use Web Control Pages. The advantage of this approach is that you can customize the startup code and have a common hook point for your wwProcess class using OnProcessInit or Process and you can add properties and methods to this class that will be visible via the Process reference in the script code.

    The whole Process class looks like this:

    ************************************************************************ *PROCEDURE wwPageDemo **************************** *** Function: Processes incoming Web Requests for wwPageDemo *** requests. This function is called from the wwServer *** process. *** Pass: loServer - wwServer object reference ************************************************************************* LPARAMETER loServer LOCAL loProcess #INCLUDE WCONNECT.H LOCAL loProcess loProcess=CREATE("wwPageDemo",loServer) loProcess.lShowRequestData = loServer.lShowRequestData IF VARTYPE(loProcess)#"O" *** All we can do is return... WAIT WINDOW NOWAIT "Unable to create Process object..." RETURN .F. ENDIF *** Call the Process Method that handles the request loProcess.Process() RETURN ************************************************************* DEFINE CLASS wwPageDemo AS WWC_PROCESS ************************************************************* nPageScriptMode = 2 && Web Control Pages *** Fires for both methods and Web Control pages. FUNCTION OnProcessInit THIS.oResponse.cStyleSheet = this.ResolveUrl("~/webcontrols/westwind.css") ENDFUNC ********************************************************************* FUNCTION HelloWorld() ************************ THIS.StandardPage("Hello World from wwPageDemo process",; "If you got here, everything should be working fine.<p>" + ; "Time: <b>" + TIME()+ "</b>") ENDFUNC * EOF wwPageDemo::HelloWorld *** Recommend you override the following methods: *** ErrorMsg *** StandardPage *** Error ENDDEFINE

    For example, let's say you have a Web Store application, that has an extension of WWS. By default it manages requests mapped to methods. But if a page called SomeNonMethodPage.wws is called it's then routed directly to the Page processing framework.

    To configure this setup you'll need to configure Visual Studio to make sure that your extension is recognized:

    Thiese are outlined in Configuring VS.NET 2005.

    Once this is in place you can now instantiate your demo simply by going to the name of the page.

    http://localhost/wconnect/webcontrols/firstForm.wws



    Web Control Pages and Debug Modes in wwProcess


    As you may have surmised previous topics, in order to run a Web Control Page the page must be compiled. Without compilation a Web Control Page is just text and it won't do anything.

    The wwProcess class can run Web Control Pages in two ways:

    Generally you'll want to work in Debug mode in the development environment, just be aware that the Server.nScriptMode flag has no effect once DebugMode is turned off.

    Very Important Project Note:
    Because pages are loaded dynamically by Web Connection, you have to make sure you add pages to your compiled project manually. If you don't the VFP project manager will not include your generated classes and your custom code. Either add them manually or add a dummy method that is never called to your project that executes each of the pages.


    Creating Custom Web Controls


    Web Connection's Web Control framework is fully extensible so you can create new controls quite easily. On a high level the process works like this:

    The last two steps involve working with Visual Studio and .NET. The process for these tasks is pretty basic however, so even if you're not familiar with .NET you should be able to do this fairly easily by using the existing controls contained in the shipped WebConnectionWebControls project as a template.

    Creating the WebControl in FoxPro code

    This is the main step for control creation. The FoxPro class determines the runtime behavior of the control. To create a new control you create a class that inherits from WebControl or from any other of the existing WebControl implementations like wwWebTextBox, wwWebDataGrid etc..

    Let's create a simple control called wwWebTimeLabel. This control displays the current time or the number of elapsed seconds since the page was loaded as a custom label. This control is essentially a slightly fancy label that uses a JavaScript script timer to redraw its content at a given interval.

    Note:
    To demonstrate the whole process I'm going to inherit this control from wwWebControl, but it would actually be slightly easier to inherit this control from wwWebLabel. Using wwWebControl as a base class means you have to implement all functionality on your own and that's the idea of this walkthrough.

    Start by creating a new PRG file for your custom class or classes. I created MyCustomControls.prg. You can store it anywhere, but I suggest you store it in a seperate directory where you keep all your Web Connection customizations. Here's the class implementation:

    SET PROCEDURE TO WebControl SET PROCEDURE TO WebControls SET PROCEDURE TO wwCollections ************************************************************* DEFINE CLASS wwWebTimeLabel AS wwWebControl ************************************************************* #IF .F. *:Help Documentation *:Topic: Class wwWebTimerDisplay *:Description: Simple label control that display some text plus an increasing time value of either a full date string or elapsed seconds since the page was loaded *:ENDHELP #ENDIF *** Custom Properties Text = "Time: " *** Determines how frequently the time value is updated in milliseconds UpdateInterval = 1000 *** Time Display Mode: Time: Show Time - Seconds: Seconds since page loaded TimeDisplayMode = "Time" ************************************************************************ * wwWebTimeLabel :: OnPreRender **************************************** *** Function: *** Assume: *** Pass: *** Return: ************************************************************************ FUNCTION OnPreRender() LOCAL lcScript IF this.TimeDisplayMode = "Time" *** Generate the JavaScript that updates the Text display TEXT TO lcScript TEXTMERGE NOSHOW function RefreshDate(loCtl) { loCtl.innerHTML = "< <this.Text> >" + new Date().toLocaleString(); } ENDTEXT this.Page.ClientScript.Add("wwWebTimerDisplay_Time",lcScript) *** Set up the timer to update this script every second this.Page.StartupScript.Add("wwWebTimerDisplay",[window.setInterval("RefreshDate(document.getElementById('] + ; this.UniqueID + ['))",] + TRANSFORM(this.UpdateInterval) + [);]) ELSE *** Generate the JavaScript that updates the Text display TEXT TO lcScript TEXTMERGE NOSHOW var TimeLoaded = new Date(); function RefreshDateSeconds(loCtl) { loCtl.innerHTML = "< <this.Text> > " + ((new Date().getTime() - TimeLoaded.getTime()) / 1000).toFixed() + " seconds"; } ENDTEXT this.Page.ClientScript.Add("wwWebTimerDisplay_Seconds",lcScript) *** Set up the timer to update this script every second this.Page.StartupScript.Add("wwWebTimerDisplaySeconds",[window.setInterval("RefreshDateSeconds(document.getElementById('] + ; this.UniqueID + ['))",] + TRANSFORM(this.UpdateInterval) + [);]) ENDIF *** Not really necessary since there are no child controls *** For any Container control this is essential!!! DoDefault() ENDFUNC * wwWebTimerDisplay :: OnPreRender ************************************************************************ * wwWebTimeLabel :: Render **************************************** *** Function: *** Assume: *** Pass: *** Return: ************************************************************************ FUNCTION Render() LOCAL lcOutput IF this.Visible = .F. RETURN ENDIF *** Get basic tags attributes like ID width etc lcBaseTags = this.WriteBaseTags() *** write an empty <span> tag into the document - the JavaScript *** will update the tag on load lcOutput = "<span " + lcBaseTags + "></span>" RETURN this.PreHtml + lcOutput + this.PostHtml ENDFUNC * wwWebTimerDisplay :: render ENDDEFINE *EOC wwWebTimerDisplay

    The control implementation is very simple. The core functionality of any control is the Render() method which is responsible for rendering the final output of the control. This control is pretty simple and only renders an empty <SPAN> tag on the page.

    Notice the call the WriteBaseTags() which writes out common control settings including the ID, colors, size, styles and a number of other tags. This is useful so that your control can automatically inherit all of these things without having to write out colors, sizes etc. individually. WriteBaseTags() is the most highlevel of these methods in the wwWebControl class. More low level versions can write out portions of all of this data. Check the various WriteXXX() methods in the wwWebControl class documentation.

    Most of the useful stuff that happens is in JavaScript which is dynamically added to the page in the code shown in OnPreRender(). Basically there's a script method added that updates the label text for the two supported display modes. One more displays the current date time every second. The other displays the elapsed seconds since the page was loaded. To get the timer started the timer needs to be set off once the page has completed loading which is done with the this.Page.StartupScript collection to which a call to window.setInterval is added. setInterval is essentially a timer that fires every second (or whatever you specify in the Interval property).

    Note that I added two properties to the control:

    *** Determines how frequently the time value is updated in milliseconds UpdateInterval = 1000 *** Time Display Mode: Time: Show Time - Seconds: Seconds since page loaded TimeDisplayMode = "Time"

    These new properties are accessible in the ASPX page markup as attributes.

    Assuming your control implementation works the control is now ready to be embedded into a page. The first thing you need to make sure of is that Web Connection can find your class so make sure you add MyCustomControls.prg into your Server's OnLoad code:

    SET PROCEDURE TO MyCustomControls ADDITIVE

    Note if you forget this WebPageParser will not be able to compile the page that contains the control properly.

    Now you're ready to stick the control into your test page. You can add the control to a page like this:

    <ww:wwWebTimeLabel runat="server" id="lblElapsed" Text="Elapsed Time: " Interval="2000" TimeDisplayMode="Seconds" />

    Notice that I can simply reference the new properties here and these properties will get assigned the values specified in this markup. Run the page and you should see a label popping up after the initial interval and the label should then update every two seconds.

    What about Visual Studio?

    That was easy! But if you go into Visual Studio and designmode you'll notice that the control is not showing up. Instead you get an ugly indescript grey box with a nasty error message. Intellisense on the control is also not available so you get not easy typing support for your custom properties.

    This all makes sense. The control runs just fine, because the FoxPro code for the control is all there. We've created a new FoxPro control, but VS.NET has no idea this control exists. So in order to get VS.NET to display the control, we'll have to create a .NET Custom Control. This requires us to write some .NET code, but this code is very straight forward. Basically what we need to do in .NET is:

    That sounds like a lot of work, but it's actually quite easy to do, especially if you use the Web Connection Web Controls project as a template. Pick a control that is close to yours and use that class as a base template.

    Note that here I chose to create a new control that inherits from wwWebControl, which is the lowest level of subclassing. If you choose you can also subclass from stock ASP.NET controls or from the Web Connection .NET controls.

    Ok, let's create a design time control for the wwWebTimeLabel control. First thing we need to do is create a C# new Class Library project in Visual Studio and add it to a solution. It's easiest to add this to an existing Web Connection Web project - I'm using the WebLog sample here.

    Your project should now look something like this:

    (the WebConnectionWebControls project is not required for this demo, but I recommend you load it into your project anyway so you can subclass the controls from there easily).

    Add references to System.Web and System.Drawing
    In order to create an ASP.NET control we'll need to add references to System.Web and System.Drawing which are required for controls to load and render. To do this:

    Go to the references node in your Control project


    Your project should now look like this:


    Create your class
    Next remove Class1.cs from the project and add a new class called wwWebTimeLabel.cs:

    Change the basic code in the class to look something like this:

    using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.Drawing.Design; namespace Westwind.WebConnection.MyWebControls { public class wwWebTimeLabel : WebControl { } }

    The namespace is really up to you - it will show up in the @Register element definition on the page that hosts one or more of these controls. The other changes basically add some namespaces that you're likely to be using in your code.

    Adding Properties

    The next step is to add the custom properties to your control. For the wwWebTimerLabel control we need to add two new properties to the control:

    DisplayMode is basically a multiple value selector - in FoxPro just a string. But in .NET we can use an Enumerator for this. So at the bottom of the page before the last } add an Enum definition:

    namespace Westwind.WebConnection.MyWebControls { public class wwWebTimeLabel : WebControl { } public enum DisplayModes { Time, Seconds } }

    Next let's add the properties:

    public class wwWebTimeLabel : WebControl { [Description("The text for the label displayed before the time value."),DefaultValue("Time: "), Category("Timer")] public string Text { get { return _Text; } set { _Text = value; } } private string _Text = "Time: "; [Description("Determines how often the time value refreshes"),DefaultValue(1000), Category("Timer")] public int Interval { get { return _Interval; } set { _Interval = value; } } private int _Interval = 1000; [Description("The message displayed in front of the time value"), DefaultValue(DisplayModes.Time), Category("Timer")] public DisplayModes DisplayMode { get { return _DisplayMode; } set { _DisplayMode = value; } } private DisplayModes _DisplayMode = DisplayModes.Time; }

    Note that in this control I have to implement the Text property because a plain WebControl class doesn't have a Text property. However, if I had inherited from Label, Text would already exist and I could simply implement a constructor that sets the Text default value to the same value as our FoxPro default value.

    Note the [] Attributes that determine some of the designer behaviors for the control. Description is text that gets displayed as help text in the designer. Default Value is important too - this value should match the private default value and should also match the default value of your control. When the default is set, VS.NET doesn't insert the text into the HTML markup which means the default is used - so it's important that your control uses consistent default values in both FoxPro and here in the .NET designer control.

    Implementing the Rendering
    The final thing left to do is create the Render() method so we have something the designer can display in designmode. Now again - this would have been a lot easier if I had inherited from the ASP.NET Label in which case all I would have to do is set the text property and call the base.Render() method which would then appropriately render the text. But to demonstrate let's do this from scratch so you can see what is involved in generating your output manually.

    The Render() method is the key method that is used to render output in ASPX pages and in the designer. It uses an HtmlWriter object that is passed in that you can write to with the Write() method. You can simply fire strings into the writer and this output gets rendered.

    Here's a simple implementation:

    protected override void Render(HtmlTextWriter writer) { string TimeValue = DateTime.Now.ToString(); if (this.DisplayMode == DisplayModes.Time) TimeValue = "2 seconds"; writer.Write("<span id='" + this.ID + "' class='" + this.CssClass + "' style='color:" + this.ForeColor.ToString() + ";background:" + this.BackColor.ToString() + ";'" + ">" + this.Text + " " + TimeValue + "</span>"); }

    Notice that based on the property settings I'll render a slightly different display. The VS.NET designer refreshes and calls the Render method of this control for every change made to the properties of the control so the changes show up in the designer immediately! This makes it possible to build some fairly sophisticated control displays in the designer.

    The code above is very simple though: It merely renders a <span> tag and adds a couple of style settings that are likely going to be set.

    If you have problems compiling your class you can use the complete source at the end of this topic.

    Compile this code. Once compiled you have now created a DLL assembly that can be loaded into the toolbox of VS.NET. To add it you can simply go into the toolbox, right click add tab. Then once the tab exists right click on the content and Add Controls.

    But you might want to hold of on that step. We'll want to make changes to these controls, and there's actually an easier way to get the controls loaded in the designer.

    Adding a reference to the Control Library in your Web Project
    You can add a project reference to your Web Project. To do this:

    Now, go into the visual designer of a Web Page. You may have to right click and click Refresh to reload the page and its environment. Open the toolbox and you should now see an automatic tab for your MyWebControls project:

    Go ahead and drop the control on to your Web Page. You should see the control rendering with the output we've generated. Try changing the Text and the DisplayMode and notice how the control is rendering in the designer. Nothing fancy but it works!

    If you look at the Property Sheet you'll find that our custom controls are there and they can be modified as expected including a dropdown for the DisplayModes:

    Cool, n'est pas?

    Refining the Control for the Designer
    There are couple of things you can do make this control work a little nicer in the designer. For one, if you look at the properties displayed in the property sheet, there are a number of properties that Web Connection doesn't respect or render and it's a good idea to hide these values.

    To do this you can go into the control and override the properties setting the [Browsable(false)] attribute to force the designer to not show these properties in the designer.

    A common set of default properties to ignore are:

    #region *** Overriden hidden properties [Browsable(false)] public override string SkinID { get { return base.SkinID; } set { base.SkinID = value; } } [Browsable(false)] public override bool EnableTheming { get { return _EnableTheming; } set { _EnableTheming = value; } } private bool _EnableTheming = false; [Browsable(false)] public override Color BorderColor { get { return base.BorderColor; } set { base.BorderColor = value; } } [Browsable(false)] public override short TabIndex { get { return base.TabIndex; } set { base.TabIndex = value; } } [Browsable(false)] public override BorderStyle BorderStyle { get { return base.BorderStyle; } set { base.BorderStyle = value; } } [Browsable(false)] public override Unit BorderWidth { get { return base.BorderWidth; } set { base.BorderWidth = value; } } #endregion

    Compile - now if you go into the designer and look at the property sheet it'll look a lot leaner and more appropriate for your control:

    Finally, it's also a good idea to set some default attributes on the class itself so that class can show a custom icon (or at least a more appropriate icon then the generic control icon) and allow a default insertion signature.

    [ToolboxBitmap(typeof(Label)), DefaultProperty("Text"), ToolboxData("<{0}:wwWebTimeLabel runat='server'/>")] public class wwWebTimeLabel : WebControl

    Now, if you load the control into the toolbox ('real' loading not with the project refernce though) you will see the customized icon.

    Designer Controls Summary
    And voila, there you have it. Your first user control. The process to create this is not exactly trivial. Especially if you are not familiar with .NET. But you can use the existing controls in the WebConnectionWebControls project as a guideline. There are lots of different scenarios covered for the control logic.

    I also want to remind you that you should try to reuse functionality as much as possible especially in the designer control. In the designerControl the easiest thing for this sample would have been to subclass from wwWebLabel and simply set the Text property to the value we want, the call base.Render(); to let the label handle the actual rendering. The code for this would have simply been:

    protected override void Render(HtmlTextWriter writer) { this.Text = this.Text + " " + TimeValue; base.Render(writer); }

    The base label handles the proper display and class attributes etc. so your code doesn't have to. Use existing Web Connection controls to subclass from or even ASP.NET controls if the display is appropriate in the designer. Often times the main thing is getting the property values - the display is really an esoteric thing - you want something to display but it doesn't necessarily have to match the actual display exactly. A good example is the wwWebHtmlEditor control - you can't display the actual editor in the designer, so a rough placeholder is displayed instead. It's nice to have accurate visual display in the designer, but it's not that crucial. If anything make sure that page placement (height, width, colors, styles, class are Ok) but beyond that it's up to you to decide how much you want to implement.

    Complete Source Code for the wwWebTimerLabel Control

    using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.Drawing.Design; namespace Westwind.WebConnection.MyWebControls { [ToolboxBitmap(typeof(Label)), DefaultProperty("Text"), ToolboxData("<{0}:wwWebTimeLabel runat='server'/>")] public class wwWebTimeLabel : WebControl { [Description("The text for the label displayed before the time value."), DefaultValue("Time: "), Category("Timer")] public string Text { get { return _Text; } set { _Text = value; } } private string _Text = "Time: "; [Description("Determines how often the time value refreshes"), DefaultValue(1000), Category("Timer")] public int Interval { get { return _Interval; } set { _Interval = value; } } private int _Interval = 1000; [Description("The message displayed in front of the time value"), DefaultValue(DisplayModes.Time), Category("Timer")] public DisplayModes DisplayMode { get { return _DisplayMode; } set { _DisplayMode = value; } } private DisplayModes _DisplayMode = DisplayModes.Time; protected override void Render(HtmlTextWriter writer) { this.Text = this.Text + " " + TimeValue; base.Render(writer); string TimeValue = DateTime.Now.ToString("d"); if (this.DisplayMode == DisplayModes.Time) TimeValue = "2 seconds"; writer.Write("<span id='" + this.ID + "' class='" + this.CssClass + "' style='color:" + this.ForeColor.ToString() + ";background:" + this.BackColor.ToString() + ";'" + ">" + this.Text + " " + TimeValue + "</span>"); } #region *** Overriden hidden properties [Browsable(false)] public override string SkinID { get { return base.SkinID; } set { base.SkinID = value; } } [Browsable(false)] public override bool EnableTheming { get { return _EnableTheming; } set { _EnableTheming = value; } } private bool _EnableTheming = false; [Browsable(false)] public override Color BorderColor { get { return base.BorderColor; } set { base.BorderColor = value; } } [Browsable(false)] public override short TabIndex { get { return base.TabIndex; } set { base.TabIndex = value; } } [Browsable(false)] public override BorderStyle BorderStyle { get { return base.BorderStyle; } set { base.BorderStyle = value; } } [Browsable(false)] public override Unit BorderWidth { get { return base.BorderWidth; } set { base.BorderWidth = value; } } #endregion } public enum DisplayModes { Time, Seconds } }


    Getting Intellisense to work in the Web Control Framework


    The Web Control Framework is fairly big and there are a lot of properties and methods to keep track of. Using it means making liberal use of the Help File until you remember common members that you frequently access.

    Web Connection provides through it autogenerated Page header basic support for Intellisense in most situations. Generated pages include the following definition:

    #INCLUDE WCONNECT.H *** Small Stub Code to execute the generated page PRIVATE __WEBPAGE __WEBPAGE = CREATEOBJECT("Test_Page_WCSX") __WEBPAGE.Run() RELEASE __WEBPAGE RETURN ************************************************************************ DEFINE CLASS Test_Page as WWC_WEBPAGE OF WWC_WEBPAGE_FILE ************************************************************************ #IF .F. *** This line provides Intellisense: Ensure your path includes this page's location LOCAL this as Test_Page_WCSX of test_page.prg #ENDIF FUNCTION OnLoad() this. && Gets intellisense on the Page class LOCAL loGrid as wwWebDataGrid loGrid = this.dgCustomers loGrid. && Gets Intellisense on a control ENDFUNC ENDDEFINE

    The above works to provide Intellisense in your code as long as the following criteria are met:


    This means WebControl.prg (the base class), any possible subclasses you have created and the actual page class must be in in the FoxPro path.

    If you're using the Web Connection Add-in and Show FoxPro Code, the Add-in will automatically add paths in the VFP IDE launched to the classes directory and the path of the page that is displayed which means you should get Intellisense automatically.

    Web Control Framework Walk Through


    This walk-through is a detailed guide to getting up and running with the Web Connection Web Control Framework from setting up a new project, configuring Visual Studio .NET 2005 as a script editor (optional - you can use any text editor) to creating a Helloworld sample, followed by a more involved data access sample for the Developer Registry that is similar to the
    Business Object Walk Through.

    This walk through also provides some information of how things work behind the scenes so it provides both a practical step by step guide as well as as some practical background about how the framework works.

    The samples here are kept visually plain in order to minimize the HTML markup displayed for individual forms and highlight the developer features discussed in each topic.

    HTML Page Templates Note:
    You can find the page templates for these demos in the \html\ControlDemo_Templates folder of the Web Connection installation. This way you don't have to type in all the HTML manually, but rather you can cut and paste each of the portions you're working with as well as seeing the final output in a working file.

    First Step: Create a new project

    Creating a new project


    The first step in this walk through is to set up a new Web Connection project. To do so let's start with the Management Console and the
    New Project Wizard. If you're unfamiliar with Web Connection you might want to quickly review how the NewProcess Wizard works at the link above. Here's the short version.

    Let's create a project with the following characteristics:

    Set up a Project called ControlDemo (which will have ControlDemoMain.prg) as it's startup and a process named ControlProcess. We won't actually use this process class in this demo, but it's a good idea to set up a new project anyway in case you decide later you want to override the default page handling behavior.

    Next create a virtual directory. Notice the checkbox for Web Control Support. When checked this copies some additional files into your Web Directory. A couple of files to configure Visual Studio specifically. Copy a separate copy of wc.dll into this directory so this app is free standing.

    Finally configure a scriptmap for the new Process class. In this case I'll choose .DP (for DevProcess), which will be the extension used for the demos. Note that you can also use the generic .WCSX extension, but using a custom script map allows you to associate a specific Process class with your project, so you can override Application wide settings. Using .WCSX is easier, but using a custom extension gives you more control.

    Starting up

    Once the project completes building you should see a browser window pop up. In Visual FoxPro you will see a project window with a new project created. Note, if you are running the shareware version no project is created since there's no source code to create a project with. You can still run this walk through though simply by using the PRG files.

    Start up your DevDemo server with the following code from the VFP command window:

    CLEAR ALL CLOSE ALL DO DevDemoMain.prg

    A server window should pop up at this time waiting for a request to start up.

    Switch over into the browser's window and either use the page it automatically loaded for you or type:

    http://localhost/devDemo/

    into the browser's Address bar. You should now be able to click the first and second link and get a very short Hello World message that confirms that the server is up and running and ready to take requests. Make sure that both links work before moving on.

    Configure the Application

    We need to make a couple of small configuration changes to your new Web Connection Server Application.

    First create a DevDemo directory below the Web Connection root, which we'll use to store our Page classes that contain the CodeBehind for the visual pages we'll create.

    Next let's make one small change to the generated main program file to allow the application to find our data files. I'm going to use the wwDevRegistry sample data in the wwDevRegistry directory underneath the Web Connection root directory.

    To find the data we'll need to SET the FoxPro path. To this we'll add the path to the server's start up code in the OnLoad() method:

    Now the server is ready for creating our samples.

    Next Step: Setting up Visual Studio 2005

    Setting up Visual Studio 2005


    In the first step we created a new project and configured a Web Virtual directory, and configured it for operation with Web Connection. When you use Visual Studio for the first time with a Web Connection project you'll need to do a one time setup to configure extensions and the toolbox. Here's what you need to do.

    Visual Web Developer Note:
    The free Visual Web Developer from Microsoft is a stripped down ASP.NET specific version of VS.NET and provides all the core features described here. However, it does not support Add-ins, so the features of the Web Connection Source and Browse View menu functionality is not available.

    I'm going to use Visual Studio 2005 for my samples here since it provides visual editing support for the Web Controls that we'll be using. You can use any text editor to create the text though including Notepad. Some editors like FrontPage and Dreamweaver are also .NET aware and can use custom controls.

    Visual Studio 2003 will also work, but only in text mode. Visual Studio 2003 does not work with pages without CodeBehind (it insists on a C# or VB.NET codebehind files which of course doesn't do).

    Opening the project in VS.NET 2005

    To start Visual Studio and open your directory follow these steps:

    1. Start Visual Studio
    2. File | Open | Web Site
    3. Select File System and point at the directory of the Virtual created (d:\westwind\ControlDemo)
    4. Click Open

    Your Solution Explorer should look something like this once you'd done this:

    Notice that the bin directory contains a WebConnectionWebControls.dll file. This file contains the Web Connection control definitions that map the FoxPro controls implemented in the Web Connection Framework. Although Web Connection works with some of the basic ASP.NET controls, it's best to use the custom controls as they map more closely to the features supported by Web Connection.

    Let's create a new Web Page. Click on the project (the path) and right click select Add New Item. Make sure that Visual C# is selected as the target language to show the Web Connection Page Templates. If you installed a Web Connection project you can select a new Web Connection Page as shown below:

    .

    In this case I'm creating a new file called Helloworld.dp. Remember .dp is the scriptmap we configured in the new project wizard and we're using a page with this extension. You can also use a .wcsx extension to get the generic version, while .dp is bound to our specific Process class. In general use the more specific version because it allows you to customize the behavior later on.

    Using the Web Connection Template is best as it automatically adds some default controls to the page (wwWebPage specifically). We'll look at what the page generated looks like in a second.

    Configuring the ASP.NET Editor to work with your Extension

    Now VS shows you the Web Connection Page in the editor, but you'll notice that it doesn't have any syntax highlighting - it's merely black and white text. That's because VS doesn't recognize .DP or .WCSX as extensions for Web Forms by default. So we need to tell it to recognize it as a Web Form. To do this:

    This one time setup will give you editor support so the form can be viewed as WebForm. Close the form and reopen it and you should see the designer options now.

    In addition Visual Studio can automatically recognize any custom controls and provide intellisense if the control is registered in the project's Web.Config file. Web Connection installs a Web.Config file that provides this functionality with a Web Control project, but if you do this manually add the following to your Web.config file (or create one if it doesn't exist). Put the file in the Virtual directory root of your project.

    <?xml version="1.0"?> <configuration> <!-- xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0" --> <system.web> <compilation defaultLanguage="c#" debug="true"> <buildProviders> <add extension=".wcsx" type="System.Web.Compilation.PageBuildProvider" /> <add extension=".dp" type="System.Web.Compilation.PageBuildProvider" /> </buildProviders> </compilation> </system.web> <appSettings> <add key="FoxProjectBasePath" value="c:\wwapps\wc3\"/> <add key="WebProjectBasePath" value="c:\westwind\wconnect\weblog\" /> <add key="WebProjectVirtual" value="http://localhost/wconnect/weblog/" /> </appSettings> </configuration>

    In addition to the extension mapping notice the FoxProjectBasePath setting in the file. This setting determines where the Web Connection Add-in looks for source code PRG files to find and load Codebehind files. If you move your project to a different directory make sure you remember to set this path or the Add-in won't find the source files to edit.

    The WebProjectBasePath and WebProjectVirtual are used by the View in Browser behavior and determines where Web Connection looks for the source files. Basically the base path maps the project root and Web Connection looks for files in project relative paths. The Virtual is used as a base path and the project relative path is appended.

    The new project Wizard creates this file automatically mapping wcsx and your custom scriptmap as well as the base paths to the current Web Connection directory when you ran the Wizard. The Virtual is pointed at localhost by default.

    Adding the WestWindWebConnectionControls to the Toolbox

    The new project contains a WebConnectionWeb
    Controls.dll file which contains the controls support by Web Connection and their visual representation. In order to use these controls they need to be added to the VS.NET Toolbox. You can do so by:

    The wwWebConnectionWebControls.dll file lives in the wconnect\VisualStudio\WebConnectionWebControls\debug\bin directory and you can pick it from there.

    Once added, your toolbox should now look something like this:

    Here's another optional step, if you want to poke around with the .NET control implentations that Web Connection provides you can also add the Web Control project directly to the current project:

    Your project should now look like this:

    You can now easily browse the .NET source for the Web Connection controls. These controls are used only for rendering the control in the designer and providing the property interface that captures the property values in the designer. If you have a Web Connection page open you'll also notice that there's a new tab in the Toolbox that shows all the controls. What's nice about this list is, if you make a change - add a control, remove a control it's automatically reflected in this list, while the statically added control list doesn't change unless you manually override it. As I said this is an optional task, but great if you like poke around or plan on modifying controls.

    Alrighty then. With the controls on the toolbox we're ready to start getting some work done.

    Next: Setting up your first Web Control Page

    Setting up your first WebControl page


    Actually we were already creating a new page in the last step. It's called HelloWorld.dp. Let's open that page in the Visual Studio Editor. If you switch to Design view you'll see a blank page.

    At the top of the page type - Hello World Demo and press enter. Highlight the text and use the Styles dropdown to set the text Heading 1. Add some more text like a link back to the Demo Home Page, select the text and use the Hyperlink icon on the toolbar create the link. The whole enchilada should now look something like this:

    Let's first switch to Html Source View. You should now see something similar to this:

    <!-- * Set the name of your class in the ID property * Point the GeneratedSourceFile at a PRG file in your FoxPro project directory * NOTE: the path is *relative* to your executing directory (ie: CURDIR() + GeneratedSourceFile) * Remove this block of comment text -->> <%@ Page Language="C#" ID="Helloworld_Page" GeneratedSourceFile="*** PATH\Helloworld_Page.prg" %> <%@ Register Assembly="WebConnectionWebControls" Namespace="Westwind.WebConnection.WebControls" TagPrefix="ww" %> <html> <head> <title>Hello World</title> <link href="westwind.css" rel="stylesheet" type="text/css" /> </head> <body style="margin-top:0px;"> <form id="form1" runat="server"> <h1>Hello World Demo</h1> <br /> <a href="/">Demo Home</a> <ww:wwWebErrorDisplay runat="server" id="ErrorDisplay" /> </form> </body> </html>

    Notice the comment at the top of the page which directs you to change the ID and GeneratedSourceFile path, which is the path to the PRG file that acts as the CodeBehind file for this page.

    Change the @Page tag to:

    <%@ Page Language="C#" ID="Helloworld_Page" GeneratedSourceFile="devDemo\Helloworld_Page.prg" %>

    This says use a Helloworld_Page class stored in devdemo\HelloWorld_page.prg as the class that handles this page's logic. When this page is now run for the first time, Web Connection will create the Helloworld_Page.prg file and generate a class from the page. If you should forget to add the path for the class, the first page request will fail because the *** Path is an invalid path specifier and so you'll get an error.

    The @Page tag is is used by Web Connection as the master container for a page. The @Page tag is required for Visual Studio to render the page. The @Register tag is used by Visual Studio to load the Web Connection controls assembly so that the designer can properly display the Web Connection controls with its properties. Both tags are stripped from the final generated output.

    The @Page tag REQUIRES that you set two properties:

    ID
    The ID tag is used to specify the classname for your generated class.The first time you 'run' this page a class is created - 2 actually - that execute the page as FoxPro code. The ID specifies the class you write code for. The ID + _WCSX is a generated class that contains the control definitions parsed from this HTML page. We'll see what this looks like in a minute.

    GeneratedSourceFile
    This is the source code location where the class is generated. This file contains two classes one with your custom user code and the generated control code for the page. The page is regenerated everytime you 'run' the page in development mode. Your custom code is kept separate and is not touched by the code updates.

    The path specified should be relative to where your FoxPro application is running! So above I use devDemo\HelloWorld.prg which goes into the DevDemo directory beneath my Web Connection Install directory.

    You can also specify a ~ to specify a path relative to the virtual root directory - ie. the Web path of your project. This allows you to store your PRG/FXP files in the same path as the WCSX or ScriptMapped pages. I don't recommend this but the option is there. For example to create the page in the same directory as the main page:

    GeneratedSourceFile="~\MyPage.prg"
    GeneratedSourceFile="~\SubDir\MyPage.prg"

    When choosing this option keep in mind that the page runtime implications. At runtime the page will execute as an FXP and you will need to explicitly copy the FXP file to the server and unload the server to replace file. For more information see the section on Understanding Code Generation and Compilation of dynamic pages.

    Next: Adding controls to the page

    Adding controls to the page


    Now that the page is set up we're ready to add some controls and look at what the FoxPro code looks like. Let's use the VS Toolbox to add a few controls to the form from the West Wind Control toolbox section.

    Let's drop a wwWebTextBox, a wwWebButton - side by side and a wwWebLabel below the button. Name the controls txtName, btnSubmit and lblMessage respectively. You should end up with something that looks like this:

    Script code should now look like this in Html Source View:

    <%@ Page Language="C#" ID="Helloworld_Page" runat="server" GeneratedSourceFile="devDemo/Helloworld_Page.prg" %> <%@ Register Assembly="WebConnectionWebControls" Namespace="Westwind.WebConnection.WebControls" TagPrefix="ww" %> <html> <head> <title>Hello World</title> <link href="westwind.css" rel="stylesheet" type="text/css" /> </head> <body style="margin-top:0px;"> <h1>Hello World Demo</h1> <form id="form1" runat="server"> <br /> <a href="/">Demo Home</a> <br /> <br /> Please enter your Name:<br /> <ww:wwWebTextBox ID="txtName" runat="server" Width="262px"></ww:wwWebTextBox> <ww:wwWebButton ID="btnSubmit" runat="server" Text="Say Hello" Width="88px" /><br /> <br /> <ww:wwWebLabel ID="lblMessage" runat="server"></ww:wwWebLabel> </form> </body> </html>

    Note that there are controls embedded into this page. So the page contains the txtName and btnSubmit controls as well as the Form and some plain HTML controls like the <a href> tag.

    You can also use the equivalent ASP.NET controls (asp:TextBox, Label, Button etc.). Any ASP.NET control that can be mapped with a wwWeb prefix can render including any custom controls you create. So if you want to handle the asp:ContentPlaceHolder control you could create a class called wwWebContentPlaceHolder that can implement the functionality.

    Now let's see if we can run this form before doing anything fancy. Switch back to Visual FoxPro and startup our application that we created earlier.

    DO DevDemoMain.prg

    Next bring up your browser and go to:

    http://localhost/controldemo/HelloWorld.dp

    You should see the page rendered in the browser.

    If things are not working:
    If you get an error follow the directions in the error message - most likely at this point the error would have to do with bad formatting in the document or missing a properly formatted wwWebPage tag. Make sure that the tag exists and has the required attributes set.

    Behind the scenes Web Connection picked up the script page, parsed it into a PRG file and then executed the page rendering the HTML result for you. If you look at:

    <WebConnectionPath>\devDemo\HelloWorld_page.prg

    you should find the following generated code for the source code for the generated class.

    Alternately you can use the Web Connection Add-in in Visual Studio by either right clicking or clicking on the Tools menu:

    The Web Connection specific menu includes:

    Add-ins and Visual Web Developer Note:
    The Add-in only works in Visual Studio 2005. Visual Web Developer is not supported as VWD does not support user Add-ins.

    Here's the source code generated:

    #INCLUDE WCONNECT.H *** Small Stub Code to execute the generated page PRIVATE __WEBPAGE __WEBPAGE = CREATEOBJECT("Helloworld_Page_WCSX") __WEBPAGE.Run() RELEASE __WEBPAGE RETURN ************************************************************** DEFINE CLASS Helloworld_Page as WWC_WEBPAGE *************************************** *** Your Implementation Page Class - put your code here *** This class acts as base class to the generated page below ************************************************************** FUNCTION OnLoad() ENDFUNC ENDDEFINE *# --- BEGIN GENERATED CODE BOUNDARY --- #* ******************************************************* *** Generated by PageParser.prg *** on: 02/09/2006 11:30:14 PM *** *** Do not modify manually - class will be overwritten ******************************************************* DEFINE CLASS Helloworld_Page_WCSX AS Helloworld_Page Id = [Helloworld_Page] *** Control Definitions form1 = null txtName = null btnSubmit = null lblMessage = null FUNCTION Initialize(loPage) DODEFAULT(loPage) __lcHtml = [] __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + [<html>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [<head>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <title>Hello World</title>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <link href="westwind.css" rel="stylesheet" type="text/css" />]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [</head>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [<body style="margin-top:0px;">]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <h1>Hello World Demo</h1>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [] _1QA1EDKY8 = CREATEOBJECT("wwWebLiteral",THIS,"_1QA1EDKY8") _1QA1EDKY8.Text = __lcHtml THIS.AddControl(_1QA1EDKY8) THIS.form1 = CREATEOBJECT("wwWebform",THIS,"form1") THIS.AddControl(THIS.form1) __lcHtml = [] __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <br />]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <a href="/">Demo Home</a>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <br />]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <br />]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ Please enter your Name:<br />]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [] _1QA1EDKYC = CREATEOBJECT("wwWebLiteral",THIS,"_1QA1EDKYC") _1QA1EDKYC.Text = __lcHtml THIS.AddControl(_1QA1EDKYC) THIS.txtName = CREATEOBJECT("wwwebtextbox",THIS,"txtName") THIS.txtName.Width = [262px] THIS.AddControl(THIS.txtName) __lcHtml = [] __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + [] _1QA1EDKYE = CREATEOBJECT("wwWebLiteral",THIS,"_1QA1EDKYE") _1QA1EDKYE.Text = __lcHtml THIS.AddControl(_1QA1EDKYE) THIS.btnSubmit = CREATEOBJECT("wwwebbutton",THIS,"btnSubmit") THIS.btnSubmit.Text = [Say Hello] THIS.btnSubmit.Width = [88px] THIS.AddControl(THIS.btnSubmit) __lcHtml = [] __lcHtml = __lcHtml + [<br />]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [ <br />]+CHR(13)+CHR(10) __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + [] _1QA1EDKYG = CREATEOBJECT("wwWebLiteral",THIS,"_1QA1EDKYG") _1QA1EDKYG.Text = __lcHtml THIS.AddControl(_1QA1EDKYG) THIS.lblMessage = CREATEOBJECT("wwweblabel",THIS,"lblMessage") THIS.AddControl(THIS.lblMessage) __lcHtml = [] __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + [] _1QA1EDKYI = CREATEOBJECT("wwWebLiteral",THIS,"_1QA1EDKYI") _1QA1EDKYI.Text = __lcHtml THIS.AddControl(_1QA1EDKYI) _1QA1EDKYJ = CREATEOBJECT("wwWebForm",THIS) _1QA1EDKYJ.RenderType = 2 THIS.AddControl(_1QA1EDKYJ) __lcHtml = [] __lcHtml = __lcHtml + []+CHR(13)+CHR(10) __lcHtml = __lcHtml + [</body>]+CHR(13)+CHR(10) __lcHtml = __lcHtml + [</html>] _1QA1EDKYK = CREATEOBJECT("wwWebLiteral",THIS,"_1QA1EDKYK") _1QA1EDKYK.Text = __lcHtml THIS.AddControl(_1QA1EDKYK) ENDFUNC ENDDEFINE *# --- END GENERATED CODE BOUNDARY --- #*

    As you can see the Web Connection parser turns the HTML Script page into a class - a couple of classes to be exact. At the bottom is a purely generated class that contains control definitions that map the content of the page to control object definitions. This part is re-generated every time the page is executed in development mode.

    The top part of the file consists of a small loader that instantiates the generated class and another class that acts as a base class for the generated class. This class is the custom class where you can write any custom code. This class is also generated only once - after that it is not touched again so any changes you make to the base class is never overwritten.

    The nPageParseMode Flag determines compilation mode

    The generation happens automatically based on the wwServer::nPageParseMode flag. In development mode this flag is set to 1 or 2 which causes the page to be parsed generated, loaded, executed, then explicitly unloaded. This process is pretty slow, but it allows you to make changes to the class without having to restart the Web Connection server. In deployed applications the nPageParseMode flag can be set to 3 which simply executes the pages which is considerably faster. This setting can be set on the Web Connection Status form of the server as well as in the <yourapp>.ini file.

    Want to manually generate the page?
    The parsing and code generation is performed by the WebPageParser class, which you can call directly from the VFP Command Window:

    DO WebPageParser with "c:\westwind\controldemo\HelloWorld.dp"

    This performs the exact same actions as the page parser in the wwProcess class does and as such it picks up the generated source file name and class name from the source code file. The class itself provides more low level functionality so you can tell it where and what to generate exactly.

    It's important that you set up your environment in such a way that any controls used and any other dependencies are loaded in the environment. During page parsing the WebPageParser instantiates all controls loaded on a form in order to get property and method information.

    Next: Writing the Hello World Code

    Hello World with Web Controls


    Up to this point we've added controls to the page, but we haven't updated the page nor written any code. That's about to change. Let's take the page we created in the last topic and lets add logic to do the obligatory Hello World code.

    Let's start by handling the Click event of the button. To do this go to Design View and select the button then enter "btnSubmit_Click" into the Click property of the property sheet in the Web Connection Events section.

    Alternately you can make the change in code:

    <ww:wwWebButton ID="btnSubmit" runat="server" Text="Go" Width="80" Click="btnSubmit_Click" />

    Notice the Click="btnSubmit_Click". What we're telling Web Connection to do here is to route the Button's Click event to the btnSubmit_Click method of the CodeBehind Page class.

    Now switch to VFP and open the Helloworld_Page.prg file. Change the Helloworld_Page class by adding the following method to it:

    FUNCTION btnSubmit_Click() this.lblMessage.Text = "Hello " + this.txtName.Text + ". Time is: " + TRANSFORM(DATETIME()) ENDFUNC

    Now go ahead and run the form. You should now see the message in the label control underneath the Textbox with the caption changing with the name of the input field and the changing Time value.

    The button click fires an event into your FoxPro class and the method specified in the Click property. The method is called and you can write any FoxPro code in this method you see fit.

    The beauty here is that you are talking to an object with properties and for the most part the details of the HTML generation are hidden from you. The framework does the right thing rendering the control into the appropriate HTML.

    On closer inspection you also see another nice feature of the page framework: Notice that the textbox automatically maintained its input value on the button click. If you're new to Web Development this may not seem like a big deal, but if you've done Web Development with most non ASP.NET like tools you had to manually repopulate the values into textboxes by explicitly binding them - typically with <%= Expression %> sytnax. It's automatic with the Web Control framework which automatically maps the POST data back into the appropriate control or controls.

    Saving non-POSTback values

    The above is nice, but it works with a value that's posted back. What if we want to change a property that doesn't get posted back? For example add another textbox and button to the form as txtColor and btnColor like this:

    Enter a color: <ww:wwWebTextBox ID="txtColor" runat="server" Width="165px">Orange</ww:wwWebTextBox> <ww:wwWebButton ID="btnColor" runat="server" Text="Change Color" Width="114px" Click="btnColor_Click"/>

    The form should now look like this in the designer:

    Then hook up this code in the FoxPro class:

    FUNCTION btnColor_Click() this.btnColor.ForeColor = this.txtColor.Text this.btnColor.Style = "font-weight:bold" ENDFUNC

    Run the page again and refresh. You'll notice clicking the button indeed changes the color and font weight of the button as expected. So here is another example of an assignment to a page control.

    But you may also notice that there is a potential problem with the demo now if you click back and forth between the Say Hello and Cange Color buttons. If you click on Say Hello after changing the color of the button you loose that color. If you click on the color button after the Say Hello button you lose the label text.

    Now this behavior may actually what you want. A value set on a label may or may not be permanent in a Web application, same with a color. The behavior works as it does because a Web page is stateless and any non-POST values are not restored automatically. POST values are textboxes, checkboxes, list values, radio buttons etc. all of which POST back to the server when the page is submitted. The values are sent and Web Connection can restore the state.

    However, properties like captions, colors, attributes, styles etc. are not posted back and so can't be automatically restored. The next time the page loads these values revert back to their default values. For the Label it means blank. For the button it means no color and non-bold.

    This is normal for Web applications and probably appropriate for most scenarios. However if you do want to persist the value of a propery you can do this by adding the following to the OnLoad() of the Page:

    FUNCTION OnLoad() this.btnColor.PreserveProperty("ForeColor") this.lblMessage.PreserveProperty("Text") ENDFUNC

    Now run the demo again and you'll see that both the text and the color are indeed preserved. Note that the style of the button is not preserved so the button turns Bold on a click, but loses the bold when you click Say Hello again. That's because we didn't persist the style. If you wanted to keep the style as well:

    this.btnColor.PreserveProperty("Style")

    and voila the boldness gets persisted as well.

    This is a powerful concept as it allows you to persist data between postbacks. PreserveProperty works with Controls and the Page itself too. So you can store properties of the Page object. Note that this Method works only with simple types - objects are not supported.

    What's happening behind the scenes is that these properties get written into ViewState. ViewState is an encoded string of all the preserved state of all the controls. This encoded string is persisted into the WebPage and posted back for each request. The control framework knows how encode and decode the ViewState and repopulate properties whenever a page is posted back. Web Connection picks up the Viewstate and reassigns it to the controls when they load the next time, which effectively persists these non-conventional values.

    So why is this not automatic? It sure would be nice if you could set a property and it would automatically preserve. The problem is two fold. First you may not want to persist values all the time. For example, you may be setting the lblMessage to some rather large value and if you persist it in ViewState that value keeps moving back and forth. You may not need that. So ViewState size is one issue. Web Connection only persists a minimal amount of settings in Viewstate. The other issue is figuring out what's changed. VFP doesn't provide a good mechanism at runtime to see whether a property still contains its default value, so there's really no good way to know what to persist. You'd have to manually parse control properties which would be extremely slow. The other alternative is to persist certain properties only, but even that is a very expensive proposition in processing time. In the end, we decided in the interest of performance it's best to not do any default persistence except for vital properties (for example, CurrentPageIndex on the WebDataGrid). It's easy enough to enable this functionality in initialization code.

    So any preservation of properties is declarative, but it's really easy to do with PreserveProperty() calls.

    What happens on Errors?

    What if you made an error in your code? If it worked go back and put some garbage into the above click method and run the request again. Here's what you should see:

    The error stops right in your FoxPro code. Cool - you can fix the problem, go back to the command window start right back up. The code stops when the Server.lDebugMode flag is set to .T. which is on by default. You can change this flag in the application's startup INI file (ControlDemo.ini - DebugMode flag) or on the Web Connection Status form. If lDebugMode = .F. the error displays in the browser like this instead:

    This default error page can be overridden by subclassing your Process class and overriding the OnError method. Alternately you can use BindEvent to route the OnError method to your own handler and override the logic there. To see the default logic look at wwWebPageProcess::OnError() in wwProcess.prg.

    Ok, you've now seen the control basics of the Web Connection Web Control framework. It's time to move on to some more interesting examples that involve data.

    Stock .WCSX or custom .DP extensions?

    Note in the previous examples I've been using the .DP extension for my pages which is a custom scriptmap we created for our process class. Note that if you were to rename the Helloworld.dp page to Helloworld.wcsx it would work exactly the same way.

    .DP is a map to a custom Process Class, which is useful if you need to override Process class options or provide processing that applies to every single request (every .DP page for example). When I created these demos originally that support was not there yet, so the following demos still use the stock .WCSX extension. You can use either that or the .DP extension. In your own applications I recommend you always use a custom scriptmap to allow for maximum flexibility even if you don't need it when you start out - chances are you will need it later.

    So, as we move forward think of mentally replacing .WCSX extension with your custom extension!

    Next: Creating a customer list page with a wwWebDataGrid

    Creating a developer list page with a wwWebDataGrid


    So now let's do something a little bit more useful. Let's use the data from the Developer Registry Sample walk through and present it as a list. Then we'll drill into the data, display it for editing and saving for a simple application.

    Let's create a new page and call it DeveloperList.wcsx. The process is going to be very much the same as the last sample. To review:

    Up this point everything's pretty much just like before. So let's have some fun here...

    Go over and drag and drop a wwWebDataGrid underneath the Message Label. Your page should now look like this:

    Actually I jumped ahead a little bit here - I set a number of properties on the wwWebDataGrid. Specifically I applied various styles from westwind.css, set AutoGenerateColumns to true and set the data source to TDevelopers. The DataSource is a cursor that we'll read from.

    Just to get an idea from those setting the wwWebDataGrid ASPX tag looks like this:

    <ww:wwWebDataGrid ID="gdDevelopers" runat="server" AutoGenerateColumns="True" CssClass="blackborder" DataSource="TDevelopers" PageSize="10" AlternatingItemCssClass="gridalternate" HeaderCssClass="gridheader" PagerCssClass="gridpager" PagerTextColor="White"> </ww:wwWebDataGrid>

    So now there's one thing left to do - trigger the code to give us this TDevelopers datasource to bind to. We want to hook up the Click event to the btnSubmit_Click method on the FoxPro class. To do this we'll add a click event to the btnSubmit button:

    <ww:wwWebButton ID="btnSubmit" runat="server" Text="Go" Width="80" Click="btnSubmit_Click" />

    Note for VS.NET Event Hookup:
    Events that fire back to FoxPro must be hooked up in the source view and not through the VS.NET IDE. This is because VS.NET adds code to the page and often uses different event names (turning click into OnClick etc.). It's better to explicitly enter the event handler here.

    Once the Click event is hooked up head back to VFP to hook up the code to this event.

    Back in FoxPro

    Go back to FoxPro. You know the drill right? Open DeveloperList_page.prg... Oh wait! No page, huh? We haven't generated yet. To generate the page we can simply run it with DebugMode on (which triggers the page compiler), or we can use the page compiler directly. Let's do that so you can see how it works by running the WebPageParser.prg class from VFP. Type:

    DO WebPageParser with "<yourWebPath>\controldemo\DeveloperList.wcsx"

    This should now parse the page and show you a new source file for DeveloperList_page.prg. At the top of the listing there should be a mostly an empty class called DeveloperList_page. This class is where we're going to hook up our code, with the generated class below it.

    I'm going to use the wwDevRegistry business object to do the data access and feed me this cursor. I'll create a btnSubmit_Click event handler method and implement the code to get the data and then databind it to the DataGrid. Sounds all very complicated - but it's not. Here's what the class looks like after all of this:

    ************************************************************** DEFINE CLASS DeveloperList_Page as WWC_WEBPAGE ********************************************** oDeveloper = null FUNCTION OnLoad() *** Create instance of business object this.oDeveloper = NEWOBJECT("wwDevRegistry","wwDevRegistry.vcx") ENDFUNC FUNCTION btnSubmit_Click() *** Retrieve a list of developers which returns a few fields into TDevelopers lnCount = this.oDeveloper.DeveloperListQuery(this.txtCompany.Text) IF lnCount = 0 THIS.SetError("No records returned.") RETURN ENDIF *** Force auto-column creation this.gdDevelopers.DataBind() ENDFUNC *** Quick Error Display function FUNCTION SetError(lcMessage) this.lblMessage.Text = lcMessage this.lblMessage.ForeColor = "red" ENDFUNC ENDDEFINE

    Go ahead and run the page now and you should see a result like this:

    It works! In the screen shot I passed no parameters so I get a list back of all customers. Because I set the PageSize to 10 I only see the first 10 items. If you put a filter into the Company field you'll see that that works as well, returning only a couple of items.

    But, there's a problem with this DataGrid display if you're displaying a long list that requires multiple pages, isn't there? If you click on one of the Page buttons, the DataGrid disappears, rather than going to the specified page. Can you see why?

    It's because we're updating the datagrid on the button click, but when we change pages we're not clicking on the button and the DataGrid never gets databound. Remember we're still dealing with a stateless Web application, so there's no physical grid anywhere - we're drawing the grid by way of the Databinding we apply, and we're only doing it in the button click event - it doesn't happen on all code paths. That's not what we want - we want the grid to ALWAYS display.

    The best way to do this is to do the grid rendering after all our page code has run. First OnLoad() fires then any events fire, then OnPreRender() fires and finally the page is rendered. We can implement the OnPreRender() method to write code that occurs just before rendering and that's why it's a good hook point to put our grid formatting and Databinding.

    So let's change the code to this:

    ************************************************************** DEFINE CLASS DeveloperList_Page as WWC_WEBPAGE *************************************** *** Your Implementation Page Class - put your code here *** This class acts as base class to the generated page below ************************************************************** oDeveloper = null FUNCTION OnLoad() this.oDeveloper = NEWOBJECT("wwDevRegistry","wwDevRegistry.vcx") ENDFUNC FUNCTION btnSubmit_Click(loCtl) *** Force the first page to be displayed on a new query *** If we don't do this the Page index stays at the previous selection this.gdDevelopers.CurrentPageIndex = 1 ENDFUNC FUNCTION OnPreRender() lnCount = this.oDeveloper.DeveloperListQuery(this.txtCompany.Text) IF lnCount = 0 THIS.SetError("No records returned.") RETURN ENDIF *** Force auto-column creation this.gdDevelopers.DataBind() ENDFUNC FUNCTION SetError(lcMessage) this.lblMessage.Text = lcMessage this.lblMessage.ForeColor = "red" ENDFUNC ENDDEFINE

    Notice that I merely removed the code from the Click event and moved it to OnPreRender(). Instead I put in a setting the resets the page of the grid back to page 1, because as we run a new query we want to make sure we see page 1 not the last selected page. By moving the code into OnPreRender() we're now firing the grid setup on EVERY page hit and our event handling merely sets a few properties to determine just how the grid is to render.

    Now if you run the form, the data list will properly handle the paging and stay displayed on the page. You don't need to do anything else to get the paging to work. Another side benefit of this feature is that the page immediately displays the list of customers on entry, without forcing a click on thte Go button.

    A common Page Pattern

    Using a Web Page framework this is a common pattern: In order to maintain state you need to draw controls that show over multiple POSTbacks from a central method that always fires. Typically this will be OnPreRender(), but it can also be OnLoad() if you don't modify the control in events. Any code you fire in an event will only be called when you indeed cause that event to happen. In most cases you'll want to use events to set properties that are used later on in the final display rendering code - in the example above it turns out we don't need the Click event handler to do anything - we could have left it out - because our state for the grid routine comes from the txtCompany TextBox.

    We'll see more examples of this as we move forward, but it's an important concept to get familiar with now. It's a typical state machine concept that's used, where your specific page code deals with setting properties and values on controls or objects, with a central state manager/display routine that is then responsible for handling how the page controls display. It's a lot more like Windows Desktop development than Web development, but modified to support the stateless metaphor where everything needs to always redraw.

    Customizing the Grid display

    At this point we have a grid with the fields we specified but it doesn't look all that great. For one thing we have fields in there that we may not need (Pk) and we may want to change the layout of the various columns. So, let's start by isolating our display routine for the grid into a separate method called ShowGrid. Then I'll go in and remove the PK column and add some basic formatting to a couple of the columns. Here's what the code looks like now:

    FUNCTION btnSubmit_Click(loCtl) *** Force the first page to be displayed on a new query this.gdDevelopers.CurrentPageIndex = 1 ENDFUNC FUNCTION OnPreRender() THIS.ShowDataGrid() ENDFUNC FUNCTION ShowDataGrid lnCount = this.oDeveloper.DeveloperListQuery(this.txtCompany.Text) IF lnCount = 0 THIS.SetError("No records returned.") RETURN ENDIF *** Auto-gen columns from cursor fields this.gdDevelopers.AutogenerateColumns = .T. *** Force auto-column creation this.gdDevelopers.DataBind() *** Auto-generated Column names are lower case this.gdDevelopers.Columns.Remove("pk") *** Change the alignment of the State column LOCAL loCol as wwWebDataGridColumn loCol = this.gdDevelopers.Columns.Item("state") loCol.HeaderAttributeString = [style='text-align:center'] loCol.ItemAttributeString = [style='text-align:center;color:darkred;font-weight:bold'] *** Assign a format string loCol.Format = "@!" *** Or you can use an expression - *** expression must accept 1 parameter and return a string * loCol.Format = "=Upper" *** Give Company field some breathing room loCol = this.gdDevelopers.Columns.Item("company") loCol.ItemAttributeString = [style="width:250px"] ENDFUNC

    All the action is now in ShowGrid, which calls DataBind() to auto-generate the columns. Once DataBind has been called you can then access the auto-created columns and work with them. The first thing you can do with auto-generated columns is drop those you don't need as I'm doing with the PK here. Then I take the state column and apply style formatting via the Header and ItemAttributeString properties. These properties take any value that gets added onto the TD tag for the header and item display cell specifically. Each cell for that column gets these tags. Using CSS style tags gives you the most flexibility and I recommend that you use them liberally for this task.

    Format Expressions
    Note also the Format Expression - you can assign any VFP format expression to the grid column. Alternately you can use a function or method expression if you proceed the expression with an = sign. When you use the = sign Web Connection will run the specified method and pass the raw data value as parameter. The function must return a string.

    Our display now looks like this:

    Notice the PK field is missing, the State field shows in red and is centered and the Name field has a little breathing space.

    Manipulating Field Expressions and Adding New Columns

    In the example above I added columns automatically because the AutoGenerateColumns flag is set to .T. But you can also create a grid entirely with code (or script markup as I'll show later). DataGrid columns are bound to field values via their Expression property.

    Let's start by adding an Edit column to the datagrid. Here's how to do that in the ShowGrid method:

    loCol = CREATEOBJECT("wwWebDataGridColumn") loCol.HeaderText = "Action" loCol.Expression = ['<a href="EditDeveloper.wcsx?id=' + TRANSFORM(pk) + '">Edit</a>'] loCol.HeaderAttributeString = [align='center'] loCol.ItemAttributeString = [align='center'] this.gdDevelopers.Columns.Add("edit",loCol)

    Notice the Expression field which conains an expression that is evaluated as each row is scanned in the datagrid. Make sure you embed an expression for any values like the PK, not the actual value - IOW, don't do this:

    loCol.Expression = ['<a href="EditDeveloper.wcsx?id=] + TRANSFORM(pk) + [">Edit</a>']

    This line of code results in all values being set with the same exact Pk because this in effect hardcodes the PK into the expression. If this seems difficult to see the difference try it out and play around with the expression string.

    Cool! What you're seeing here is that we have A LOT of control over this new column and how it displays. We can do this with existing columns as well as new ones.

    Let's add one more thing to our sample here to segue into the next step. Let's create a hyperlink on the Company to allows to view a developer entry. To do so add this code on the bottom of ShowGrid():

    *** Add a hyperlink to drill down into company loCol = this.gdDevelopers.Columns.Item("company") loCol.Expression = ['<a href="ShowDeveloper.wcsx?id=' + TRANSFORM(pk) + '">' +TRIM(Company) + '</a>']

    Our grid now looks like this:

    Adding Sorting

    A final excercise for you with might be to add sorting to the grid. Let's say we want to allow sorting by Company and by Name.

    *** Add a hyperlink to drill down into company loCol = this.gdDevelopers.Columns.Item("company") loCol.Id="company" loCol.Expression = ['<a href="ShowDeveloper.wcsx?id=' + TRANSFORM(pk) + '">' +TRIM(Company) + '</a>'] loCol.Sortable = .t. loCol.SortExpression = "upper(company)" *** Make the Name column sortable loCol = this.gdDevelopers.Columns.Item("name") loCol.Sortable = .t.

    Sorting makes the column headers clickable as links and shows a * next to the sorted column to indicate that it's the sort key.

    The final result of sorting applied looks like this:

    Note on Sorting and Expressions:
    Sorting is based on SortExpressions that are applied by running another query against the cursor specified in the datasource. It does a SELECT * and adds the sort expression, then uses ORDER BY on it. This has two issues: There's some additional overhead as a new cursor is created. It also means that the cursor used for expressions changes. As a result it's highly recommended that your column field expressions are not using Cursor.Field syntax, but just the Field name for any references to data source fields.

    Grid Color Formatting
    You'll notice that this grid looks reasonably nice as is without having to do any special formatting. The formatting is controlled via the Westwind.css stylesheet that is attached to this page. Westwind.css contains many commonly used styles like GridHeader, GridNormal, GridAlternate etc. that are used as defaults in the grid. You can change the way this CSS stylesheet looks or simply copy those styles to another stylesheet.

    You can also use your own styles of course - nothing is hard coded, but the defaults use the styles provided in westwind.css. A few things are crucial in these styles, specifically the link color style used for A tags inside of headers, which wouldn't show properly without special style attributes.

    For example the following tag is set up for the gridheader tag to ensure that links show with a compatible color:

    .gridheader A, gridheader A:visited
    { color: gold; }
    

    Without this formatting links would be lost with some low res color contrast.

    In general we recommend that you provide the stock styles provided in westwind.css - it's a solid staring point for common styles and you can build ontop of that list with application specific styles as needed.

    Using VS.NET for adding columns declaratively

    In the example here I started with Auto-generated columns because we actually wanted to display most of the columns of the returned cursor. Here it was easier to autogenerate and then modify the existing columns with the two or three property changes needed to be made.

    You can also create columns declaritively inside of VS.NET. You can use the Columns collection in the designer. Click on the Columns collection and start adding columns declaratively:

    Using this in the VS.NET IDE gives you a reasonably close approximation as to what the data grid will look like when rendered. Realize that it's not exact, as the .NET code rendering is only minmal and abstract and basically duplicates what the Fox control does behind the scenes.

    Note that you don't need VS.NET to create this user interface. You can use any text editor and create the markup manually which looks like this:

    <ww:wwWebDataGrid ID="gdDevelopers" runat="server" AutoGenerateColumns="false" CssClass="blackborder" DataSource="TDevelopers" PageSize="10" AlternatingItemCssClass="gridalternate" HeaderCssClass="gridheader" PagerCssClass="gridpager" PagerTextColor="White" Width="500"> < Columns > <ww:wwWebDataGridColumn ID="WwWebDataGridColumn1" runat="server" Expression="Name" FieldType="C" HeaderText="Developer Name" /> <ww:wwWebDataGridColumn ID="WwWebDataGridColumn2" runat="server" Expression="Company" FieldType="C" HeaderText="Company" Sortable="True" SortExpression="upper(name)" /> <ww:wwWebDataGridColumn ID="WwWebDataGridColumn3" runat="server" Expression="state" FieldType="C" HeaderText="State" Sortable="True" HeaderAttributeString="align='center'" ItemAttributeString="align='center'" SortExpression="" /> < /Columns > </ww:wwWebDataGrid>

    Note that you'll want to turn AutoGenerateColumns to false if you use custom columns.

    Onwards

    Phew... long topic, but we covered a lot of options here. The next step is to allow drilling into the Developers and display the data with DataBinding.

    Displaying the Developer Data

    Displaying Developer Data


    As the next step let's create a small form for just displaying developer data. I'll keep this very, very simple first and then add a few enhancements to this base form to demonstrate some of the nice features that make it easy to provide rich content without any code easily and intuitively.

    This page is accessed from the Developer List by clicking on the Company name hyperlink.

    Here's what our first version of the form should look like in the browser:

    To start let's create this form and call it ShowDeveloper.wcsx with a Codebehind class created as ShowDeveloper in ShowDeveloper_Page.prg. Here's what this page should look like:

    <%@ Page Language="C#" ID="Showdeveloper_page" GeneratedSourceFile="controldemo\ShowDeveloper_Page.prg" %> <%@ Register Assembly="WebConnectionWebControls" Namespace="Westwind.WebConnection.WebControls" TagPrefix="ww" %> <html> <head> <title>Developer Display for <%= this.Page.oDeveloper.oData.Company %></title> <link href="westwind.css" rel="stylesheet" type="text/css" /> </head> <body> <form id="form1" runat="server"> <div> <h1>Developer Information for <%= this.Page.oDeveloper.oData.Company %></h1> </div> <br /> <a href="default.htm">Demo Home</a> | <a href="DeveloperList.wcsx">Developer List</a><br /> <br /> <br /> <br /> <table class="blackborder" width="500" cellpadding="6" > <tr> <td valign="top" align="right" class="blockheader" style="font-weight:bold" > Company: </td> <td valign="top"> <ww:wwWebLabel ID="lblCompany" runat='server' ControlSource="this.Page.oDeveloper.oData.Company" style="font-weight:bold"></ww:wwWebLabel> </td> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold"> Name: </td> <td valign="top"> <ww:wwWebLabel ID="lblName" runat='server' ControlSource="this.Page.oDeveloper.oData.Name"></ww:wwWebLabel> </td> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold; height: 28px;"> Address: </td> <td valign="top" style="height: 28px"> <ww:wwWebLabel ID="lblAddress" runat='server' ControlSource="DisplayMemo(this.Page.oDeveloper.oData.address)"></ww:wwWebLabel> </td> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold"> Country: </td> <td valign="top"> <ww:wwWebLabel ID="lblCountry" runat='server' ControlSource="this.Page.oDeveloper.oData.Country"></ww:wwWebLabel> </td> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold"> Phone: </td> <td valign="top"> <ww:wwWebLabel ID="lblPhoneNumber" runat='server' ControlSource="this.Page.oDeveloper.oData.Phone"></ww:wwWebLabel> </td> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold; height: 28px;"> Email: </td> <td valign="top" style="height: 28px"> <ww:wwWebLabel ID="lblEmail" runat="server" ControlSource="this.Page.oDeveloper.oData.Email" /> </td> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold"> Web Site: </td> <td valign="top"> <ww:wwWebLabel ID="lblWebSite" runat="server" ControlSource="this.Page.oDeveloper.oData.WebSite" /> </tr> <tr> <td width="131" valign="top" class="blockheader" align="right" style="font-weight: bold"> Services offered:</td> <td valign="top" width="453"> <ww:wwWebCheckBox ID="chkDevelopment" runat='server' ControlSource="this.Page.oDeveloper.oData.Dev" Text="Development" /> <ww:wwWebCheckBox ID="chkTraining" runat='server' ControlSource="this.Page.oDeveloper.oData.Training" Text="Training" /> <ww:wwWebCheckBox ID="chkSupport" runat='server' ControlSource="this.Page.oDeveloper.oData.Support" Text="Support" /> <hr> <%= DisplayMemo(this.Page.oDeveloper.oData.Services) %> </td> </tr> </table> </form> </body> </html>

    Note that this page basically consists of a number of wwWebLabel controls that are databound via their ControlSource properties. There are also three Checkbox controls which are used to display the logical values for which support options a developer supports.

    Notice that the ControlSource property points at this.Page.oDeveloper.oData, which is the wwDevRegistry wwbusiness object that we've used so far. ControlSource values work the same as standard VFP form ControlSource properties so you can bind to fields, variables or as in this case properties of a business object.

    From a logic perspective all we have to do on this page is load up the business object and then DataBind the form controls and we're done. The custom code behind for this page is very simple: It merely loads the business object based on the queryString ID value that was passed into the form. There's very little code:

    ************************************************************** DEFINE CLASS Showdeveloper_page as WWC_WEBPAGE *************************************** *** Your Implementation Page Class - put your code here *** This class acts as base class to the generated page below ************************************************************** oDeveloper = null FUNCTION OnLoad() this.oDeveloper = NEWOBJECT("wwDevRegistry","wwDevRegistry.vcx") lnId = VAL(Request.QueryString("Id")) IF lnId = 0 OR !this.oDeveloper.Load(lnId) Process.ErrorMsg("Invalid Developer",; "Please make sure you select a valid developer to display.",,; 5,"developerlist.wcsx") RETURN ENDIF THIS.DataBind() ENDFUNC ENDDEFINE

    Note that here I decided to display an error page that's external rather than displaying an error message in the same page. That's because if an invalid developer id was chosen there's really nothing you want to display on this 'display' page, so there's no need to continue on this page. Instead I use the standard Web Connection Process.ErrorMsg() page to display an error that automatically redirects back to the Developer Listing page.

    So as you can see we've written almost no code to make this page. Everything happens in the markup by assigning the ControlSource which automatically binds the controls.

    What about Binding Errors

    What happens if you have an error in a binding expression? This is likely a frequent scenario, but if you have an error in a binding expression you will see the Text property of the control changed to *! Binding Error. This usually happens if you have an invalid expression or mistype a property name.

    Accessing Controls with Code

    Remember that the controls are accessible to you via code so if you rather assign a value from the code behind you can do that as well. For example, the Web Site link is a Web site, so wouldn't it be nice to change the display to actually display a link to click on.

    FUNCTION OnLoad() this.oDeveloper = NEWOBJECT("wwDevRegistry","wwDevRegistry.vcx") lnId = VAL(Request.QueryString("Id")) IF lnId = 0 OR !this.oDeveloper.Load(lnId) ... RETURN ENDIF THIS.DataBind() *** Manipulate lblWebSite control directly via Code this.lblWebSite.Text = [<a href="] + this.oDeveloper.oData.WebSite + ; [" target="__top">] + this.oDeveloper.oData.WebSite + [</a>] ENDFUNC

    Using Better Controls

    The above works just fine and is a great way to go if you have complex rules that need to fire for content in a control. But in the example above there's actually an easier way. Go ahead and remove the above line from the code. Let's flip back to WCSX page in the HTML editor and let's go and replace the wwWebLabel for lblWebSite with the following wwWebHyperLink control tag:

    <ww:wwWebHyperLink ID="hypWebSite" runat="server" ControlSource="this.Page.oDeveloper.oData.WebSite" />

    The HyperLink control automatically creates the hyper link and is smart enough to match Text and NavigateUrl properties based on the values provided. For databinding you can provide both LinkControlSource and a plain ControlSource for the text. If the NavigateUrl/LinkControlSource is missing it assumes the link is to be displayed for both.

    Along the same lines let's use an wwWebEmailLink control for the email address. This control is nice as it encodes the email address so it can't easily be harvested. We'll take two shots at this. First let's replace the lblEmail control with the following:

    <ww:wwWebMailLink ID="lblEmail" runat="server" EmailControlSource="this.Page.oDeveloper.oData.Email" />

    Go ahead and try this out. And voila you'll end up with an email link. The link will be minimally encoded with entities. Not much we can do if the email address has to display on the Web Page. But now change the code and add some static text like this:

    <ww:wwWebMailLink ID="lblEmail" runat="server" EmailControlSource="this.Page.oDeveloper.oData.Email" Text="Click here for email" />

    and now the email address is not exposed in plain text anymore. You don't have to use static text either - you can bind the text using the ControlSource property. The following actually makes the most sense:

    <ww:wwWebMailLink ID="lblEmail" runat="server" EmailControlSource="this.Page.oDeveloper.oData.Email" ControlSource="this.Page.oDeveloper.oData.Name" />

    which displays the name and has a javascript link to the email link popup.

    Ok, one more control. The wwDevRegistry also contains a Logo Url which if set points to a logo url for the company to display an image in the page. The easiest way to display this image is by using an wwWebImage control in the page at the top. Add the following control just above the <Table> tag:

    <ww:wwWebImage runat="server" ID="imgLogo" ControlSource="this.Page.oDeveloper.oData.Logo" />

    The wwWebImage is really nice because you can databind it and it automatically detects empty image links and doesn't render if the image is empty, so you won't get broken links.

    Here's what our form now looks like with all of these changes:

    Cool. Now you can see how you can quickly display information using controls and databind the content in these controls. The next step is to actually be able to edit this information.

    Next: Editing Developer Data


    Editing Developer Data


    In the last sample we displayed data. Now let's allow editing of this data.

    Create a new page and call it EditDeveloper.wcsx add a wwWebPage control and assign the class names as shown below:

    <%@ Page Language="C#" ID="EditDeveloper_page" GeneratedSourceFile="controlDemo\EditDeveloper_page.prg" %> <%@ Register Assembly="WebConnectionWebControls" Namespace="Westwind.WebConnection.WebControls" TagPrefix="ww" %> <html > <head runat="server"> <title>Developer Editor</title> <link href="westwind.css" rel="stylesheet" type="text/css" /> </head> <body> <form id="form1" runat="server"> </form> </body> </html>

    So now, we already did the base layout in the last topic, so let's simply copy the content from between the <FORM> tag from the ShowDeveloper.wcsx page and paste it into this page. The go in and do a Search and Replace for wwWebLabel to wwWebTextBox. And voila right of the top we've adjusted most of the controls and are ready for editing. A bit more work will be required for the Email and Web Link fields, but for now this is a good start.

    Go ahead and run the form, incomplete as it from the browser:

    http://localhost/controldemo/editdeveloper.wcsx?id=3

    Looks a bit haphazard, the textboxes are empty and there are some script errors on the page, but it's not too bad for a start. Let's fix a couple visual things. Drop a westwind.css stylesheet onto the page, and change the width of the all the textboxes to 350. You can do this by selecting all the controls in the VS.NET editor and applying the width.

    There are a couple of more things that you should fix here:

    The errors we're seeing on the running page are because we haven't hooked up any code yet to load the actual developer business object. So let's create our form's OnLoad method. This code should be identical to what we were doing in ShowDeveloper_page to start with:

    ************************************************************** DEFINE CLASS EditDeveloper_page as WWC_WEBPAGE *************************************** *** Your Implementation Page Class - put your code here *** This class acts as base class to the generated page below ************************************************************** oDeveloper = null oLookups = null FUNCTION OnLoad() this.oDeveloper = NEWOBJECT("wwDevRegistry","wwDevRegistry.vcx") lnId = VAL(Request.QueryString("Id")) IF lnId = 0 OR !this.oDeveloper.Load(lnId) Process.ErrorMsg("Invalid Developer",; "Please make sure you select a valid developer to display.",,; 5,"developerlist.wcsx") RETURN ENDIF this.oLookups = NEWOBJECT("wwLookups","wwDevRegistry") lnResult = this.oLookups.GetCountries() this.lstCountry.DataSource = "TCountries" this.lstCountry.DataTextField = "Country" IF !this.IsPostBack THIS.DataBind() ENDIF *** Must always databind the image since it doesn't post back this.imgLogo.DataBind() ENDFUNC ENDDEFINE

    The only difference here is that I changed the DataBind to only fire when we are not posting back. This makes sure that we don't reload the data after we've made changes, but before the data has saved, such as when errors occur during validation. Note that I manually bind the Image however - the image doesn't post its value back, so we want to rebind this value on every hit. Again note the image control will automatically not render if the ImageUrl isn't set.

    So we now have a functioning page that displays the developer information in a mostly editable environment. The page looks like this now:

    Not bad considering we've basically picked up most of this from the previous page. We need to do more work though. We'll need to fix the links and make them editable as well as the Services description on the bottom. So let's start with the links. The links should merely convert to textboxes as well. So let's do that:

    <ww:wwWebTextBox ID="txtEmail" runat="server" ControlSource="this.Page.oDeveloper.oData.Email" Width="350" /> <ww:wwWebTextBox ID="txtWebSite" runat="server" ControlSource="this.Page.oDeveloper.oData.WebSite" Width="350" ondblclick="window.open(this.value,'DevSite');" style="color:blue" />

    For the Services area below we can use the following expression:

    <ww:wwWebTextBox runat="server" ID="txtServices ControlSource="this.Page.oDeveloper.oData.Services" TextMode="MultiLine" width="400" Height="350" />


    Adding a Country Lookup

    Let's make one more change to show you how easy it is to create a drop down list and have it databind. Let's change the Country field to a dropdown list based on the wwLookups::GetCountries() business object method. This method returns a cursor of country names we can databind into a dropdown list.

    Let's start by replacing the txtCountry field with a lstCountry field in the markup like this:

    <ww:wwWebDropDownList ID="lstCountry" runat="server" Width="350px" ControlSource="this.Page.oDeveloper.oData.Country" > </ww:wwWebDropDownList>

    This tells the control to bind to the Country field of our business object. The binding here is for the selected value of the control. In order to bind the list of countries we need to use a little code in the code behind page in the OnLoad() of the Page class:

    this.oLookups = NEWOBJECT("wwLookups","wwDevRegistry") lnResult = this.oLookups.GetCountries() this.lstCountry.DataSource = "TCountries" this.lstCountry.DataTextField = "Country"

    In this case I'm only binding the text field, but I can also bind a DataValueField if necesary. And that's all it takes.

    So now our form looks like this:

    Since we're going to handle errors, we might as well one more thing. Select all the TextBoxes and set the IsRequired Property to true to force the fields to be populated. This will give us Binding errors we can display on the fields if they are blank.

    Saving the Data

    Time to start saving the data. So let's add a button to the bottom of the form first:

    <ww:wwWebButton ID="btnSubmit" runat="server" AccessKey="s" Text="Save Developer Info" Click="btnSubmit_Click" />

    In addition let's also add an wwWebErrorDisplay control onto the page so we can display errors in a meaningful way. While you can use a simple label for error display the wwWebErrorDisplayControl does a much nicer job of it. Go back into VS.NET and drag and drop the control onto the form above the image control:

    <ww:wwWebErrorDisplay ID="ErrorDisplay" runat="server" CssClass="ErrorDisplay" ErrorImage="images/warning.gif" InfoImage="images/info.gif" Text="" UserMessage="Please correct the following:" />

    If you're using VS.NET your layout now should look like this:

    Saving User Changes

    Ok, let's run the page to make sure everything works so far. The page should now run well in display mode. If you end up clicking the button on the bottom of the form you'll get an error because we haven't implemented the button click event yet. So let's implement it. Here's the code we're going to use:

    ************************************************************************ * editDeveloper_Page :: btnSubmit_Click **************************************** *** Function: Save the Developer Information by unbinding *** and validating the input. ************************************************************************ FUNCTION btnSubmit_Click() *** Unbind the data back into the control source for this ID this.UnbindData() IF !this.oDeveloper.Validate() this.AddValidationErrorsToBindingErrors(this.oDeveloper.oValidationErrors) ENDIF IF THIS.BindingErrors.Count > 0 this.ErrorDisplay.Text = this.BindingErrors.ToHtml() RETURN ENDIF *** If we get here there are no errors IF !this.oDeveloper.Save() this.ErrorDisplay.Text = this.oDeveloper.cErrorMsg RETURN ENDIF this.ErrorDisplay.ShowMessage("Developer Entry Saved") ENDFUNC * editDeveloper_Page :: btnSubmit_Click

    That's all we need to deal with doing basic error checking and saving the data! Very little code here. The first thing that happens is the data from the form is unbound back into their control sources, which moves all the field values back to the business object oData fields.

    The UnbindData() method does the work of looking at the control sources and retrieving the data from each control. If an error occurs during the data conversion, UnbindData() stuffs the errors in the BindingErrors collection. You can retrieve the binding errors directly or you can use the ToString() or ToHtml() methods which return string and HTML presentations of the errors.

    Next we call the Validate method of the business object. Validate is basically checking business rules before saving the data. Internally the business object updates an oValidationErrors collection with business rule violations. The code looks something like this.

    *** wwDevRegistry::Validate() LOCAL loDev loDev = THIS.oData this.cErrorMsg = "" this.oValidationerrors.Clear() IF EMPTY(loDev.Company) this.AddValidationerror("A company name is required.","txtCompany") ENDIF IF EMPTY(loDev.Name) this.AddValidationError("A contact name is required.","txtName") ENDIF IF LEN(loDev.Services) < 200 this.AddValidationError("The service description is too short. At least 200 characters are required.","txtServices") ENDIF IF this.oValidationErrors.Count > 0 THIS.SetError(this.oValidationErrors.ToString()) RETURN .F. ENDIF RETURN .T.

    Note that this uses a wwBusiness object, and this is really an implementation detail. You can do your validation any way you choose. One nice thing about the wwBusiness oValidationErrors collection is that it can be automatically added to the BindingErrors collection of the WebPage which combines both. The following code does this:

    IF !this.oDeveloper.Validate() this.AddValidationErrorsToBindingErrors(this.oDeveloper.oValidationErrors) ENDIF

    If an error occurs you get a rich error display in the ErrorDisplay control, which provides a nice display for our error information with this code:

    IF THIS.BindingErrors.Count > 0 this.ErrorDisplay.Text = this.BindingErrors.ToHtml() RETURN ENDIF

    To check this out go into the form and leave a couple of field blank or leave just a few characters of text in the Services field. You should get a page with rich error info that looks like this:

    The first error in the figure is a Binding Error - the IsRequired flag was triggered upon saving. The second is a business rule violation error. Both are displaying in the same error display.

    Cool isn't it? Notice that you can click on the links in the error and it will highlight the field and put the cursor there. Also notice the mouseover effect (in IE at least) for the error icons and the ability to specify where you want the error icons placed. BTW, the icon itself is configured with the Page's ErrorIconUrl property - it defaults to the application root's images directory and the warning.gif file.

    If you 'fix' the problems (or simply reset the page), then make a more minor change, you can see what the save operation looks like. Notice it calls the ShowMessage() method of the ErrorDisplay object, which simply displays a message with an info icon. ShowMessage is meant for plain informational messages and provides a diffferent display mode:

    this.ErrorDisplay.ShowMessage("Developer Entry Saved")

    Note that most aspects of this are configurable. The display of the error box is CSS driven, and the messages and images are configurable via control properties as well. This is automatic error handling is very easy to use and requires very little code. Error handling in classic Web Connection applications was often omitted or used really simple pages because it was such a pain to build. Now error handling is so easy that it practically is a no-brainer.

    Next: Adding a new Developer

    Adding a new Developer


    In the last step we allowed editing existing developers. Now let's add the ability to add new developers. This is pretty simple as you are going to use the same user interface.

    The key difference in this process is that we don't have a new Id for the developer until we save. This presents an interesting problem the way the app works currently as it won't go passed a missing Id. So what needs to happen is we need to allow entries through when there's no Id and create a new or empty developer record we can fill data into. We can then go ahead enter data and save the entry, at which point a real ID will be assigned.

    To make this work we need to keep track of this new ID. One way to do this is to store in ViewState, which is essentially page specific state. In addition to checking for an Id on the querystring we'll also check for it in ViewState. If in neither we find an ID we assume it's a new developer. Here's the code that goes into the Page OnLoad for the EditDeveloper_Page.prg file:

    FUNCTION OnLoad() this.oDeveloper = NEWOBJECT("wwDevRegistry","wwDevRegistry.vcx") *** Check for the ID in QueryString and ViewState (new) this.nId = VAL(Request.QueryString("Id")) IF this.nID = 0 *** Viewstate returns .null. if not present this.nId = VAL( this.ViewState.Item("Id") ) IF ISNULL(this.nId) this.nID = 0 ENDIF ENDIF *** If it's empty create a new developer (not saved yet) IF THIS.nId # 0 *** Load existing developer IF !this.oDeveloper.Load(this.nId) Process.ErrorMsg("Invalid Developer",; "Please make sure you select a valid developer to display.",,; 5,"developerlist.wcsx") RETURN ENDIF ELSE this.oDeveloper.New() ENDIF ... more code omitted ENDFUNC

    Notice that I changed the lnId variable to a property of the form - we'll need to look at it when we save our entry in the end. This code checks for the Id in both Querystring and ViewState if it doesn't find anything creates a new, empty Developer record which is not yet saved.

    You should now be able to run the form and see it display with all empty fields if you go to:

    http://localhost/controlDemo/EditDeveloper.wcsx

    That's half of it. The other half is actually saving the data and the question is how do you know how you need to save the data for a new record or an existing record? In this case it doesn't really matter, because the business object will figure it out on its own. But if you need to detect the new record you simply check for THIS.nID = 0. So the button click now looks like this:

    FUNCTION btnSubmit_Click() *** Unbind the data back into the control source for this ID this.UnbindData() IF !this.oDeveloper.Validate() this.AddValidationErrorsToBindingErrors(; this.oDeveloper.oValidationErrors) ENDIF *** Demonstrate manually adding a binding error IF EMPTY(this.txtState.SelectedValue) this.AddBindingError("Please select a state","lstState") ENDIF *** If we have binding errors now we need to display them *** and not save the developer entry yet IF THIS.BindingErrors.Count > 0 this.ErrorDisplay.Text = this.BindingErrors.ToHtml() RETURN ENDIF *** Approve here so it shows up in the list THIS.oDeveloper.oData.Approved = .T. *** If we get here there are no errors IF !this.oDeveloper.Save() this.ErrorDisplay.Text = this.oDeveloper.cErrorMsg RETURN ENDIF IF this.nID = 0 *** Add the new id into ViewState this.ViewState.Add("Id",this.oDeveloper.oData.Pk) *** Rebind image now that the value is set this.imgLogo.DataBind() ENDIF this.ErrorDisplay.ShowMessage("Developer Entry Saved") ENDFUNC * editDeveloper_Page :: btnSubmit_Click

    Functionally there's only one small block of code that has changed in this method to support adding a new record:

    IF this.nID = 0 *** Add the new id into ViewState this.ViewState.Add("Id",this.oDeveloper.oData.Pk) ENDIF

    This code makes sure that the new Id gets written into Viewstate, so that after we save and we come badk to redisplay the page, the the OnLoad can now find the new ID for this new developer in the ViewState and load up the data properly.

    A few words about ViewState

    Viewstate is very handy to persist values for a specific page. You might have used Session state or hidden form fields for this stuff before, but now you can simply stick any values you need to see on a postback into the ViewState and have access to them on the next hit. Note that ViewState values are limited to values that convert easily into strings and back with the TRANSFORM() function. All values are returned as strings.


    And there you have it. We've done the whole cycle - view data, drill into it to view and edit it, and then finally add to it.

    Next: A few more additions

    A few additions and the full EditDeveloper code


    In the last step we completed the editing and adding sequence. In this final step let's add a few more minor details and then summarize and show the final code here.

    The first thing you might have noticed is that I glossed over a few more minor feature enhancements I made. I added a State dropdown to the form and databound it to a cursor to display the states. Here's the WCSX control declaration:

    <ww:wwWebDropDownList ID="txtState" runat="server" Width="350px" ControlSource="this.Page.oDeveloper.oData.State" >

    The data for this list comes from the wwd_lookups table and is retrieved via the wwLookups business object, which returns a cursor of two fields: State and StateCode. Unlike the Country drop down used earlier here we want to bind both the StateCode as the value we save and start with and the State name which is displayed in the list:

    lnResult = this.oLookups.GetStates() this.txtState.FirstItemText = "*** Please enter a State" this.txtState.DataSource = "TStates" this.txtState.DataTextField = "State" this.txtState.DataValueField = "StateCode"

    Notice that I also added a *** Please enter a State item to the listbox's bound data which has an empty value. The Country drop down doesn't need this as it defaults to United States on a new entry, but the state value should be indeterminate until you make a selection of a specific state.

    Manually adding BindingErrors

    The databinding and unbinding is automatically handled by the code we already have, however, the error checking for it is not. The business object doesn't handle this (actually it should, but for arguments sake let's say it doesn't <g>). So we need to manage this one piece of validation ourselves in the code.

    What we want to do is check for the empty Selected value and then add an error to the BindingErrors of the page which is as simple as this in the btnSubmit_Click method after UnbindData():

    *** Demonstrate manually adding a binding error IF EMPTY(this.txtState.SelectedValue) this.AddBindingError("Please select a State","txtState") ENDIF

    This demonstrates that you can easily add your own binding errors that participate in the rest of the form based validation routines. You probably have to do more of this if you don't use wwBusiness objects or your own objects that implement some way to easily assign validation errors to the BindingErrors automatically.

    If you use another business object framework you should be able to create subclass of the wwWebPage class that provides the ability to automatically add the binding errors similar to this code shown earlier:

    IF !this.oDeveloper.Validate() this.AddValidationErrorsToBindingErrors(; this.oDeveloper.oValidationErrors) ENDIF

    which makes business object rule violations a snap to display!

    Complete Script and PRG code for the EditDeveloper Form

    I've made a lot of changes in small snippets here, so I thought I'd finish up this walkthough by pasting the full WCSX source and the full source of the custom class I created. There are a few minor enhancements over what I previously showed, but it's pretty close to the walkthrough code.

    Here's the complete source code for the VFP CodeBehind class:

    ************************************************************** DEFINE CLASS EditDeveloper_page as WWC_WEBPAGE *************************************** *** Your Implementation Page Class - put your code here *** This class acts as base class to the generated page below ************************************************************** oDeveloper = null oLookups = null nId = 0 ************************************************************************ * EditDeveloper_Page :: OnLoad **************************************** *** Function: Handles initial display of the developer *** and list binding ************************************************************************ FUNCTION OnLoad() this.oDeveloper = NEWOBJECT("wwDevRegistry","wwDevRegistry.vcx") *** Check for the ID in QueryString and ViewState (new) this.nId = VAL(Request.QueryString("Id")) IF this.nID = 0 *** Viewstate returns .null. if not present this.nId = VAL( this.ViewState.Item("Id") ) IF ISNULL(this.nId) this.nID = 0 ENDIF ENDIF *** If it's empty create a new developer (not saved yet) IF THIS.nId # 0 *** Load existing developer IF !this.oDeveloper.Load(this.nId) Process.ErrorMsg("Invalid Developer",; "Please make sure you select a valid developer to display.",,; 5,"developerlist.wcsx") RETURN ENDIF ELSE this.oDeveloper.New() ENDIF *** Populate the Country Lookups this.oLookups = NEWOBJECT("wwLookups","wwDevRegistry") lnResult = this.oLookups.GetCountries() *** Data bind the TCountries result cursor to the listbox this.txtCountry.DataSource = "TCountries" this.txtCountry.DataTextField = "Country" lnResult = this.oLookups.GetStates() this.txtState.FirstItemText = "*** Please enter a State" this.txtState.DataSource = "TStates" this.txtState.DataTextField = "State" this.txtState.DataValueField = "StateCode" *** Bind the input controls only on the first hit IF !this.IsPostBack *** Bind all the single field controls THIS.DataBind() ELSE *** Must always databind the image since it doesn't postback this.imgLogo.DataBind() ENDIF ENDFUNC * EditDeveloper_Page :: OnLoad ************************************************************************ * editDeveloper_Page :: btnSubmit_Click **************************************** *** Function: Save the Developer Information by unbinding *** and validating the input. *** Assume: *** Pass: *** Return: ************************************************************************ FUNCTION btnSubmit_Click() *** Unbind the data back into the control source for this ID this.UnbindData() IF !this.oDeveloper.Validate() this.AddValidationErrorsToBindingErrors(; this.oDeveloper.oValidationErrors) ENDIF *** Demonstrate manually adding a binding error IF EMPTY(this.txtState.SelectedValue) this.AddBindingError("Please select a State","txtState") ENDIF *** If we have binding errors now we need to display them *** and not save the developer entry yet IF THIS.BindingErrors.Count > 0 this.ErrorDisplay.Text = this.BindingErrors.ToHtml() RETURN ENDIF *** Approve here so it shows up in the list THIS.oDeveloper.oData.Approved = .T. *** If we get here there are no errors IF !this.oDeveloper.Save() this.ErrorDisplay.Text = this.oDeveloper.cErrorMsg RETURN ENDIF IF this.nID = 0 *** Add the new id into ViewState this.ViewState.Add("Id",this.oDeveloper.oData.Pk) *** Rebind image now that the value is set this.imgLogo.DataBind() ENDIF this.ErrorDisplay.ShowMessage("Developer Entry Saved") ENDFUNC * editDeveloper_Page :: btnSubmit_Click ENDDEFINE


    <%@ Page Language="C#" GeneratedSourceFile="controlDemo\EditDeveloper_page.prg" ID="EditDeveloper_page" ErrorIconUrl="images/warning.gif" %> <%@ Register Assembly="WebConnectionWebControls" Namespace="Westwind.WebConnection.WebControls" TagPrefix="ww" %> <html> <head runat="server"> <title>Developer Editor</title> <link href="westwind.css" rel="stylesheet" type="text/css" /> </head> <body> <form id="form1" runat="server"> <div> <h1> Developer Information for <%= this.Page.oDeveloper.oData.Company %> </h1> </div> <br /> <a href="default.htm">Demo Home</a> | <a href="DeveloperList.wcsx">Developer List</a> | <a href="EditDeveloper.wcsx">Add Developer</a> | <a href="<%= Request.GetCurrentUrl() %>"> Reset Page</a><br /> <br /> <ww:wwWebErrorDisplay ID="ErrorDisplay" runat="server" CssClass="ErrorDisplay" ErrorImage="images/warning.gif" InfoImage="images/info.gif" UserMessage="Please correct the following:" Center="False" Width="500px" /> <br /> <ww:wwWebImage runat="server" ID="imgLogo" ControlSource="this.Page.oDeveloper.oData.Logo" /> <table class="blackborder" width="500" cellpadding="6"> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold"> Company: </td> <td valign="top" style="width: 453px"> <ww:wwWebTextBox ID="txtCompany" runat='server' ControlSource="this.Page.oDeveloper.oData.Company" Style="font-weight: bold" Width="350px" IsRequired="True"></ww:wwWebTextBox> </td> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold"> Name: </td> <td valign="top" style="width: 453px"> <ww:wwWebTextBox ID="txtName" runat='server' ControlSource="this.Page.oDeveloper.oData.Name" Width="350px" IsRequired="True"></ww:wwWebTextBox> </td> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold; height: 28px;"> Address: </td> <td valign="top" style="height: 28px; width: 453px;"> <ww:wwWebTextBox ID="txtAddress" runat="server" TextMode="MultiLine" ControlSource="this.Page.oDeveloper.oData.address" Width="350px" IsRequired="True"></ww:wwWebTextBox>  </td> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold; height: 28px;"> City:</td> <td valign="top" style="height: 28px; width: 453px;"> <ww:wwWebTextBox ID="City" runat="server" ControlSource="this.Page.oDeveloper.oData.City" Width="350px" IsRequired="True"></ww:wwWebTextBox> </td> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold"> State:</td> <td valign="top" style="width: 453px"> <ww:wwWebDropDownList ID="txtState" runat="server" Width="350px" ControlSource="this.Page.oDeveloper.oData.State"> </ww:wwWebDropDownList></td> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold"> Country: </td> <td valign="top" style="width: 453px"> <ww:wwWebDropDownList ID="txtCountry" runat="server" Width="350px" ControlSource="this.Page.oDeveloper.oData.Country"> </ww:wwWebDropDownList></td> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold"> Phone: </td> <td valign="top" style="width: 453px"> <ww:wwWebTextBox ID="txtPhoneNumber" runat='server' ControlSource="this.Page.oDeveloper.oData.Phone" Width="350px" IsRequired="True"></ww:wwWebTextBox> </td> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold; height: 28px;"> Email: </td> <td valign="top" style="height: 28px; width: 453px;"> <ww:wwWebTextBox ID="txtEmail" runat="server" ControlSource="this.Page.oDeveloper.oData.Email" Width="350" IsRequired="True" /> </td> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold"> Web Site: </td> <td valign="top" style="width: 453px"> <ww:wwWebTextBox ID="txtWebSite" runat="server" ControlSource="this.Page.oDeveloper.oData.WebSite" Width="350" ondblclick="window.open(this.value,'DevSite')" Style="color: blue" IsRequired="False" /> </tr> <tr> <td valign="top" align="right" class="blockheader" style="font-weight: bold"> Logo Url: </td> <td valign="top" style="width: 453px"> <ww:wwWebTextBox ID="txtLogo" runat="server" ControlSource="this.Page.oDeveloper.oData.Logo" Width="350" ondblclick="window.open(this.value,'DevSite')" Style="color: blue" IsRequired="False" /> </tr> <tr> <td width="131" valign="top" class="blockheader" align="right" style="font-weight: bold"> Services offered:</td> <td valign="top" style="width: 453px"> <ww:wwWebCheckBox ID="chkDevelopment" runat='server' ControlSource="this.Page.oDeveloper.oData.Dev" Text="Development" /> <ww:wwWebCheckBox ID="chkTraining" runat='server' ControlSource="this.Page.oDeveloper.oData.Training" Text="Training" /> <ww:wwWebCheckBox ID="chkSupport" runat='server' ControlSource="this.Page.oDeveloper.oData.Support" Text="Support" /> <hr> <ww:wwWebTextBox runat="server" ID="txtServices" ControlSource="this.Page.oDeveloper.oData.Services" TextMode="MultiLine" Width="400" Height="150" ErrorMessageLocation="2" /> </td> </tr> <tr> <td align="right" class="blockheader" style="font-weight: bold" valign="top" width="131"> </td> <td style="width: 453px" valign="top"> <ww:wwWebButton ID="btnSubmit" runat="server" AccessKey="s" Text="Save Developer Info" Click="btnSubmit_Click" /></td> </tr> </table> <br />   </form> </body> </html>




    Visual Studio Configuration and Operation


    In order to utilize Web Connection Web Control Framework pages in Visual Studio 2005 or Visual Web Developer there are a few configuration settings and files that need to be copied.

    This topic tree describes the requirements in some detail, both for manual configuration and auto-configuration through the Web Connection Wizards.

    Note that Web Connection requires Visual Studio 2005 or the free Visual Web Developer 2005 in order to utilize the visual environment. Visual Studio 2003 is not supported using the custom controls provided due to major limitations in display of controls and no support for custom extensions in the editor.

    Configuring Visual Studio .NET


    Web Connection will automatically configure itself for operation with Visual Studio if you use the Web Connection installation and create new projects with the new project Wizard. The wizards will ensure that the default templates and the Add-in get installed properly as well as configuring your Web.Config file for you.

    To run this, please use the following code from your Web Connection install directory:

    DO CONSOLE WITH "VSNETCONFIG"

    or use the Web Connection Menu and Configure Visual Studio.

    This utility configures all of the configuration settings required. However, depending on your Visual Studio configuration it's possible that some settings may not be set up completely. The most common problem is that the Web Connection controls fail to register properly on the Visual Studio Toolbox and you may have to add them manually as described below.

    Manual Visual Studio Configuration

    This topic describes the complete process for manual configuration as well as a couple of the steps that cannot be automated. The following are covered:

    Copy WebConnectionWebControls.dll into the bin directory


    occurs by default when you create a new project
    In order for Visual Studio to be able to see Web Connection specific controls you need to copy the wwWebControls.dll file into the BIN directory of your Web application. This is a file that won't be used by your application, but Visual Studio needs in order to display the Web Connection controls on the toolbox and in the designer with all the custom Web Connection control properties.

    Add WebConnectWebControls.dll to the VS.NET Toolbox


    one time manual configuration
    In order to use the Web Connection Controls and to visually drop them onto Web forms you'll want to register the controls. To do this in Visual Studio:

    Enable support for WCSX or custom Extensions


    Each extension must be manually configured once in Visual Studio so Visual Studio recognizes these extensions as Web Forms.

    Custom ScriptMap Extensions:
    You can use any scriptmap extension for your script classes. Web Control framework pages can automatically be accessed if a page does not exist in your wwProcess subclass. For simplicity I'm going to use WCSX here, which is the default scriptmapped engine supported by Web Connection, but you can map any process class to a scriptmap and then use this same process for that script extension.

    1. In VS.NET 2005 under Tools | Options | Text Editor Extensions add WCSX (or your Process Class' scriptmap - .dp shown here) as Web Form Editor

      Extended Options:
      Make sure that when you go to Tools | Options you always check the Show all settings checkbox. Some options like the extension mapping become available only after enabling this flag. This is especially true for Visual Web Developer.

    2. Add a Web.config to the root of your Web directory
      Then add the following to have VS.NET recognize your specific application extensions:

      <configuration> <system.web> <compilation defaultLanguage="c#" debug="true"> <buildProviders> <add extension=".dp" type="System.Web.Compilation.PageBuildProvider" /> <add extension=".blog" type="System.Web.Compilation.PageBuildProvider" /> <add extension=".wcsx" type="System.Web.Compilation.PageBuildProvider" /> </buildProviders> </compilation> </system.web> </configuration>

      This adds the necessary namespace recognition and ensures that syntax color highlighting and intellisense work properly against ASP.NET controls.

      Note that the New Project and Process Wizards in Web Connection automatically add WCSX and the custom scriptmap configured for your process into the provided web.config file so this step is optional. If you decide later to add additional script maps or change an existing map in your application these maps have to be manually configured by both adding the editor association and the web.config BuildProvider setting.

    Adding Web Connection Page and User Control Templates


    Web Connection also ships with a set of templates that are available for C# Web Pages and templates. These templates provide a blank page layout for you with the wwWebPage control already placed in the page so you don't have to drop it manually.

    You can copy these templates in an automated way by doing:

    DO CONSOLE WITH "VSNET2005CONFIG"

    inside of your Web Connection install directory or by running the Configure VS.NET 2005 option from the Web Connection Menu.

    These files are installed for you automatically when you create a new Web Connection project, but if you need to manually copy the files you can move:

    <wconnect_InstallDir>\VisualStudio\Web Connection Page.zip
    <wconnect_InstallDir>\VisualStudio\Web Connection User Control.zip

    to:

    <My Documnents Dir>\Visual Studio 2005\Templates\ItemTemplates\Visual Web Developer

    Once these templates have been moved they will show up on the Add New Item option in Visual Studio. Important: Make sure you are using C# as your language.

    Adding the Web Connection FoxPro Source View Add-In


    The Web Connection Source View Add-in attaches to the Tools menu in Visual Studio and allows you to directly jump to the FoxPro CodeBehind behind page from within Visual Studio if you are editing a Web Connection Web Control page. The page must contain the <%@Register> tag for the Westwind.WebConnection.Controls library in order for the addin to be available.

    Visual Web Developer Note:
    This feature does not work with Visual Web Develop as it doesn't support Add-ins as Visual Studio does. With VWD you have to manually open the codebehind file from Visual FoxPro.

    As the Page Templates CONSOLE and Web Conection menu can automatically install the Add-in for you with:

    DO CONSOLE WITH "VSNET2005CONFIG"

    To manually install the Add-in, you can copy the WebConnectionAddin.Addin file from the <wconnectInstall>\VisualStudio directory into:

    <My Documents>\Visual Studio 2005\Addins

    Open the file and change the path in the <Assembly></Assembly> key to the path of the Addin dll. which is:

    <wconnectInstall>\VisualStudio\VsNet2005Addin\WebConnectionAddin.dll

    If you're using a locale other than US, copy the en-us directory to your locale specifier (ie. de-DE, de-AU, fr-CA etc.)

    Configure the FoxPro Base Path for the Add-in


    In order for the Add-in to find the FoxPro source file it needs to know the base path of the FoxPro application, so it can properly find the file and open it. The Source File contains a relative path to the current FoxPro IDE instance and so this path needs to be stored somewhere. The AppSettings section in the web.config file should have a section like this:

    <appSettings> <add key="FoxProjectBasePath" value="c:\wwapps\wc3\"/> <add key="WebProjectBasePath" value="c:\westwind\wconnect\weblog\" /> <add key="WebProjectVirtual" value="http://localhost/wconnect/weblog/" /> <add key="IdeOnLoadPrg" value="" /> <add key="WebBrowser" value="" /> <add key="WebBrowserAlternate" value="" /> <add key="FoxProEditor" value=""/> <add key="FoxProEditorAlternate" value="C:\Program Files\TextPad 5\textpad.exe"/> </appSettings>

    The Add-in reads the FoxProBasePath key out of the web.config file and then uses this path plus the project relativve path of the page to open the appropriate FoxPro source file.

    Along the same lines the WebProjectBasePath entries are used to find the Web directory and then is used to launch the browser in this directory. This is the directory and virtual name of the directory where your Web files (.wcsx or your scriptmap) live.

    The paths are automatically set when you create a new project with the new project Wizard. Make sure that you adjust these values if you move your project or otherwise change paths.

    For more detail on Add-in configuration and the values you can set in the configuration see the Visual Studio Add-in Configuration topic.

    A complete Web.config file


    For completeness sake, here's a complete Web.config for a Web Connection project. You can use this for reference.

    <?xml version="1.0"?> <configuration> <system.web> <compilation defaultLanguage="c#" debug="true"> <buildProviders> <!-- Add any script map extensions for your Web Connection pages here. Required by VS --> <add extension=".blog" type="System.Web.Compilation.PageBuildProvider"/> <add extension=".wcsx" type="System.Web.Compilation.PageBuildProvider"/> </buildProviders> </compilation> </system.web> <appSettings> <!-- Your FoxPro project path where you're working from. Acts as relative base path for PRG files --> <add key="FoxProjectBasePath" value="d:\wwapps\wc3\"/> <!-- The path of your Web files for this project. Acts as relative base path for Web templates. --> <add key="WebProjectBasePath" value="c:\westwind\wconnect\weblog\" /> <!-- The Web Server path to get to your base Web directory for the application. Used for preview your pages --> <add key="WebProjectVirtual" value="http://localhost/wconnect/weblog/" /> <!-- The default browser used. Blank IE Automation, otherwise specify browser path --> <add key="WebBrowser" value="" /> <add key="WbBrowserAlternate" value="c:\program files\firefox\firefox.exe" /> <!-- Specify an alternate non-VFP editor. Blank uses VFP Editor in IDE --> <add key="FoxProEditor" value="" /> <!-- Alternate FoxPro editor used with Show FoxPro Code. --> <add key="FoxProEditorAlternate" value="c:\programs\EditPadPro6\EditPadpro.exe" /> <!-- Optional PRG file launched when VFP IDE starts. Use for optional environment config --> <add key="IdeOnLoadPrg" value="" /> </appSettings> </configuration>

    The key elements are:




    Visual Studio Add-in Configuration


    The Web Connection Visual Studio Add-in provides a number of useful features that makes editing and previewing Web Control Framework pages easier.

    Visual Studio only support
    The Web Connection Add-in only works in full versions of Visual Studio due to limitations by Microsoft in all of the Express versions. It won't work in Visual Web Developer.

    The add-in is available on any Web Connection Control markup pages (both in HTML markup or in the visual designer) and pops up via right click on the Context menu:

    The last four items on this context menu make up the add-in operation:

    Add-in Location

    Visual Studio Add-ins can be added with a .addin file in the <documents>\Visual Studio 200x\Addins folder. The Add-in file contains XML meta data that points at the name, icon and physical filename of the Add-in. The Web Connection Add-in's physical location is located in:

    <WebConnectionInstall>\Visual Studio\WebConnectionAddin\

    where you can find the DLL and resource dlls. The add-in is updated automatically with a Web Connection update if you copy the full installation ontop of an existing version.

    Add-in specific Web.Config Configuration Settings

    The Add-in uses several configuration settings to configure its operation using the generic AppSettings section in web.config.

    Most importantly the Add-in needs to know the location of your Web path (ie. your virtual path and your physical file path) to your markup pages as well as the base FoxPro path where source code files are stored. Most of the other keys are optional and are defaulted.

    Here's what a configuration section looks like:

    <configuration> <appSettings> <add key="FoxProjectBasePath" value="c:\wwapps\wc3\" /> <add key="WebProjectBasePath" value="c:\westwind\wconnect\" /> <add key="WebProjectVirtual" value="http://rasnote/wconnect/" /> <add key="IdeOnLoadPrg" value="OnLoadIde.prg"/> <add key="WebBrowser" value="" /> <add key="WebBrowserAlternate" value ="c:\programs\firefox\firefox.exe"/> <add key="FoxProEditor" value="" /> <add key="FoxProEditorAlternate" value="C:\Programs\EditPadPro6\editpadpro.exe" /> </appSettings> <configuration>

    Here's what each of the keys mean:

    Only the first three keys are required to be configured - all the remainders are optional. If you don't specify WebBrowserAlternate or FoxProEditorAlternate the alternate menu options are not shown.



    The .NET Controls in WebConnectionWebControls.Dll


    In order for Visual Studio and Visual Web Developer to display the Web Connection controls with all of the custom properties that are specific to the framework you'll end up using the WebConnectionWebControls.dll in your project and drop it onto the toolbar.

    Web Connection ships with the source code of these .NET controls, which are mere shells that implement the appropriate properties and render the interface into the designer to match. These controls are not used at runtime - they are merely a design time feature of the Web Connection framework in order to provide Toolbox and Intellisense support.

    The source code and project file for the WebConnectionWebControls.dll can be found in:

    <wconnectInstallDir>\VisualStudio\WebConnectionWebControls

    Customizing or overriding the Controls

    Since you get the source code you can review of how these controls are set up and how they render themselves. One of the really cool aspects of using Visual Studio for control editing is that you can effectively create a custom renderer and even full designers (if you're willing to dig into some .NET code). The base control implementations consist mainly of C# Property implementations, plus Rendering for a few of the controls like the DataGrid, and Repeater etc.

    You can extend these controls, or subclass them to create custom controls that map to your own Web Connection Controls you might create. Of course you can also create new controls completely from scratch. To subclass we recommend that you create a new .NET Class project and then subclass either any stock .NET controls or one of the Web Connection controls ( you can use the source code provided as a reference ). We'll be updating this DLL occasionally with updates and you'll want to make sure that our changes don't override yours.

    Adding the project to your Web Project in VS.NET

    While working in Visual Studio I like to work with the source code for the Web Controls available. This allows making changes to the controls quickly for one. You can simply recompile the control project to immediately update the controls. You also get the most current version of the controls in your project - by adding the project to your Web project and then Adding a Reference to the Control project you can use a single copy of the DLL in all of your projects that's always up to date and stays up to date.

    Another bonus is that control based controls also drops onto the Toolbox automatically without requiring the tedious steps to register a DLL on the Toolbox and update it everytime you add a control. If you add a new control - the new control immediately shows up on the toolbox.

    If you plan on building custom controls I highly recommend this approach.

    To configure your project with the Web Connection Web Controls project:

    And you're off. You can now make changes to the Web Control project and then right click on the control project and Build to recompile that project. Make sure you build only the Web Control project as the Web Site compilation will likely fail because there will be FoxPro code in pages that the ASP.NET compiler cannot compile. We don't care about ASP.NET compilation after all - the code will be compiled by the Web Connection Server.



    Creating custom Page and Control Templates


    Web Connection ships with a custom Visual Studio template that is used to create new Web Conection Web Control pages. This page contains everything to set up the page so you can simply add your FoxPro class name and start adding control to the main page.

    The default generated page templated looks like this:

    <!-- * Set the name of your class in the ID property * Set the GeneratedSourceFile at a PRG file in your FoxPro project directory * NOTE: the path is relative to your executing directory (CURDIR()) * Remove this block of comment text -->> <%@ Page Language="C#" ID="TEstPage_Page" GeneratedSourceFile="*** Path\TEstPage_Page.prg" %> <%@ Register Assembly="WebConnectionWebControls" Namespace="Westwind.WebConnection.WebControls" TagPrefix="ww" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <link href="westwind.css" rel="stylesheet" type="text/css" /> </head> <body style="margin-top:0px;margin-left:0px"> <form id="form1" runat="server"> <ww:wwWebErrorDisplay runat="server" id="ErrorDisplay" /> </form> </body> </html>

    How Page Templates work in Visual Studio

    Page templates in Visual Studio are stored in your personal template folder and they are contained in a ZIP file that contains the template, a configuration file and an icon. The template zip files live in:

    c:\Users\<yourAccount>\Documents\Visual Studio 2005\Templates\ItemTemplates\Visual Web Developer

    The easiest way to extend the template is to copy the Web Connection Page.Zip file and copy it to a new file say My Web Connection Page.zip.

    Start by opening the default.wcsx page and customizing the template the way you'd like to. Maybe you want to add another or different style sheet, add an typical company logo or maybe even a common Web Control at the top and bottom of the page. Do whatever you think you need in your template for a default layout. Save the file.

    Next open the MyTemplate.vsTemplate file which opens as an XML document in Visual Studio. Make a few changes like this to change the name and description and maybe the default file name for the template.

    <VSTemplate Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005" Type="Item"> <TemplateData> <DefaultName>Default.wcsx</DefaultName> <Name>My custom Web Connection Page</Name> <Description>My Own West Wind Web Connection Page</Description> <ProjectType>Web</ProjectType> <ProjectSubType>CSharp</ProjectSubType> <SortOrder>10</SortOrder> <Icon>__TemplateIcon.ico</Icon> </TemplateData> <TemplateContent> <References /> <ProjectItem TargetFileName="$fileinputname$.$fileinputextension$" ReplaceParameters="true" >MyDefault.wcsx</ProjectItem> </TemplateContent> </VSTemplate>

    Save the file to disk. Now take those two modified files and copy them back into the My Web Connection Page.zip file and save the Zip file.

    The changes made are immediately available in Visual Studion - no need to even restart. If you open the Add New Item dialog in Visual Studio you should now see the My Web Connection Template in the selection list:

    It's easy to create custom templates - in fact it's so easy it might be useful to create a new template just for a specific project that loads all the project specifc stuff into a page.

    If you create custom templates for yourself be sure you back them up so you don't loose them as you move across machines.

    Common Tasks and Questions


    The following section is a short set of knowledgebase like topics that address a few common scenarios and questions.

    Security in Web Control Framework applications


    Security in Web Control Framework applications works the same as it does in classic Web Connection applications. The core Security mechanism is implemented through the wwUserSecurity class or by using Basic Authentication. Both can be very easily accessed through the
    Process.Authenticate method.

    Security is connected to the relevant wwProcess class and although the wwWebPage class has an Authenticate method, that method simply forwards its processing to the wwProcess class. The Authenticate() method is a page level Authentication method which is used to intercept the page processing and forces authentication if not yet authenticated by presenting a login page.

    The form is self-contained and keeps the current URL alive. Once successful, the page posts back to the original URL which now authenticates and continues to display.

    The wwProcess class handles all of this functionality through a few configuration properties and the Authenticate method. The key property values that are customizable are:

    *** Basic UserSecurity cAuthenticationMode = "UserSecurity" *** Class used for UserSecurity style authentication cAuthenticationUserSecurityClass = "wwUserSecurity" *** A user object for the authenticated user oUserSecurity = null *** The name of the user that was authenticated cAuthenticatedUser = ""

    The AuthenticationMode can be either Basic or UserSecurity. If Basic Windows User accounts are used through Basic Authentication provided by the Web Server. If UserSecurity is used the wwUserSecurty class is used using FoxPro based tables for the authentication store. You can specify exactly which class is used so typically you end up overriding the wwUserSecurity class and using a custom table. For example the Web Log does this:

    *********************************************************************** DEFINE CLASS WebLogUserSecurity AS wwUserSecurity OF wwUserSecurity.prg *********************************************************************** *** Stock Properties cAlias = "WebLogUserSecurity" cFilename = "Weblog\data\WeblogUserSecurity" ENDDEFINE *EOC WebLogUserSecurity

    and then assigns

    cAuthenticationUserSecurityClass = "WebLogUserSecurity"

    Once you've decided how to authentication you can very easily do it with code like the following inside of a Web Control Page itself:

    FUNCTION OnLoad() IF !Process.Authenticate() RETURN ENDIF this.lblMessage.Text = Process.cAuthenticatedUser + " " + ; Process.oUserSecurity.oUser.Fullname ENDFUNC

    Notice that the Process class assigns the oUserSecurity member (when using UserSecurity in cAuthenticationMode), which contains some detail about the user based on the values stored in the UserSecurity table. This object is always available after authentication with values blank if authentication fails.

    If you're using Basic Authentication the code changes a little bit in that you need to specify which user, group or mechanism to validate:

    FUNCTION OnLoad() IF !Process.Authenticate("WCINI") RETURN ENDIF this.lblMessage.Text = Process.cAuthenticatedUser ENDFUNC

    Note the parameter passed to Authenticate(), which can be "ANY", "WCINI" or a user or group "ricks" or a list of users/groups "ricks,markuse". Also with Basic Authentication (Process.cAuthenticationMode="Basic") the oUserSecurity object is not available so the only information about the user that you get is the username in the Process.cAuthenticatedUser property.

    Using the wwWebLogin Control

    The Page framework also includes a wwWebLogin control that can be dropped onto a page and you can use the control as a page level access validator. When not logged in the control displays as login display:

    and you can enter username and password into it. The control has a LoggedIn property you can query from the page:

    IF !THIS.Login.LoggedIn this.panelAdminContent.Visible =.F. ENDIF

    Based on the on the LoggedIn flag or the IsAdmin flag you can show or hide content as needed based on the users level of authentication.

    When you're logged in the control displays as a small box with the username displayed inside of it:

    Of course you may not want to display the logged in control at all in which case you can simply hide the control altogether by setting it's visible flag to false:

    Of course you may not want to display the logged in control at all in which case you can simply hide the control altogether by setting it's visible flag to false:

    FUNCTION OnLoad() IF !THIS.Authenticate() RETURN ENDIF this.Login.Visible = .F. ... do whatever you need to ENDFUNC

    The control can also be used via code directly. For example, in the WebLog sample there’s a generic Authentication routine which looks like this:

    ************************************************************************ * WebLog_Routines :: IsAdminLogin **************************************** *** Function: Generic Admin Security routine that can be generically *** called from the admin pages to validate users and force *** a login. ************************************************************************ FUNCTION IsAdminLogin() loLogin = CREATEOBJECT("wwWebLogin") loLogin.UserSecurityClass = "WebLogUserSecurity" IF loLogin.Login() AND loLogin.IsAdmin RETURN .T. ENDIF Process.ErrorMsg("Access Denied",; "<blockquote>The Administrative features require that you log in first. " +; "Please return to main page of the Web Log form first and log on from there.<p></blockquote><hr>",,; 3,Process.ResolveUrl("~/Default.blog#WebLogin") ) *** Shut down Web Control Framework Response Object Response.End() RETURN .F. ENDFUNC

    The WebLogin class’s Login method manages all aspects of the login process from authentication to the retrieving and setting Session variables. Again, some of the behavior of the control delegates out to the Process class.

    Here based on the result of the login we display an error message in case the login fails.

    All of these classes work together and behind the scenes the core functionality is based on the wwProcess class settings that drive the Session management and authentication lookups. This provides a much more flexible mechanism over WWWC 4.x for displaying login information in a variety of ways and scenarios.

    Firing Custom Server Events from the Client


    The Web Control framework handles events in two ways:

    Firing a Button from the Client

    Button clicks is completely automatic and built into both HTML and Web Connection natively. There's nothing special about triggering clicks - they just automatically fire events on the server. If you want to trigger a button to post back to the server the easiest is simply to call the specific button's 'click' method in JavaScript code:

    <script type="text/javascript"> function ScriptCode() { document.getElementById('btnSubmit').click(); } </script>

    This fires the click of the button and simply submits it. Easy.

    Firing a custom Event from the client

    But buttons are usually not the only target of explicitly triggered events from the client. A more common scenario is that you click an item in a datagrid and want to have it fire an event on the server that doesn't navigate to a new page and instead handles the operation on the current page.

    Firing events from the client is straight forward. Let's take a TextBox and attach to the onblur event (focus lost) and fire it into the page as a POSTBACK.

    <ww:wwWebTextBox runat="server" id="txtName" onblur="__doPostBack('Page','ontxtName_LostFocus')" /> <ww:wwWebLabel runat="server" id="lblNameEcho"></ww:wwWebLabel>

    This says when onblur fires, submit the form and fire the OnTxtName_LostFocus event on the Page object. 'Page' in this case is a fixed name (you can use 'Page' or 'this'), but you can specify the unique ID for any control on the page and point at any method of the control.

    Next we need to implement the event:

    DEFINE CLASS Absolute_Page as WWC_WEBPAGE FUNCTION OnLoad() this.RegisterPostbackScriptCode() ENDFUNC FUNCTION OnTxtName_LostFocus() this.lblNameEcho.Text = this.txtName.Text + ". " + TIME() ENDFUNC ENDDEFINE

    Note the call to RegisterPostbackScriptCode() - this ensures that the __doPostback script handler is available on the page and is required when you manually add events on the client.

    A more complex Scenario: Firing DataGrid Item events

    Let's look at a more complex example. You have a grid display and have a Delete button on the grid. When you click Delete you want to post back to the current form delete the selected record and then redisplay the grid without the deleted record.

    Let's look at the example:

    <ww:wwWebDataGrid ID="gdCustomers" runat="server"> <Columns> <ww:wwWebDataGridColumn runat="server" ID="Company" Expression="Company" > </ww:wwWebDataGridColumn> <ww:wwWebDataGridColumn runat="server" ID="WwWebDataGridColumn1" Expression="this.Page.DeleteExpression()" HeaderText="Action" > </ww:wwWebDataGridColumn> </Columns> </ww:wwWebDataGrid>

    The second column is a Delete link. I'm using an Expression that points back to the page to create the output for this column which is going to be a hyperlink that calls the javascript:__doPostBack() function. I'm using an expression because there will be embedded expressions in the string and it's easier to write in VFP than in the VS.NET editor.

    FUNCTION DeleteExpression() RETURN [<a href="javascript:__doPostBack('Page','DeleteCompany','] + TRANSFORM(pk) + [');" >Delete</a>] ENDFUNC FUNCTION DeleteCompany lcResult = Request.Form("__EventParameter") *** Delete Code would go here this.Errordisplay.ShowMessage("you've selected " + lcResult ) ENDFUNC

    I created two methods in the Page class. The first generates the Expression to display and as you can see it creates the __doPostBack() function call for each link in the grid. It says:

    The actual HTML generated inside of the grid column looks like this:

    <a href="javascript:__doPostBack('Page','DeleteCompany','32');" >Delete</a>

    Note the special name 'Page' in this example. You can use Page or This to specify that you want to fire an event on the Page object. For any other control use its UniqueID value to identify the control (ie. this.txtButton.UniqueID).

    When clicked this code now goes out and fires the Page.DeleteCompany method in the page. The mthod then can use Request.Form("__EventParameter") to retrieve the parameter that the client sent. So at this point you'd be ready to delete the record with a PK value of 32. The code above merely echos back the value passed.

    Note parameters are always passed and retrieved as strings.

    Using custom wwProcess classes and ScriptMaps


    The samples in the Web Connection 5.0 documentation show using the default generic WCSX extension for creating Web Control Framework applications. While this works just fine, if you use WCSX extension you are actually giving up a bit of configurability, because WCSX is mapped to a generic process class that you can’t explicitly override.

    The preferred approach is to use a custom process class and a custom scriptmap like the .Blog extension for the WebLog for example. By doing so you get the ability to create custom logic into the wwProcess class, letting you override things like the stock error handling in OnError(), overriding OnProcessInit() for global processing you want to have happen on every request and setting properties on the Process object that customize operation such as default error page and so on.

    If you create a new project with Web Connection this is automatically built into the new project – the scriptmap is configured and pointed at the new Process class. In Web Connection 5.0 any Process request that is made that cannot be matched to a method, goes to disk (actually to a cache cursor) and sees if there is a Web Control Framework page to process and if so processes it. IOW, the Web Control Framework is now built into any Process class you create.

    There’s one kink though: Any extension you create must be configured in Visual Studio or Visual Web Developer so that it understands the extension and treats it as a Web Form. Here’s how:

    1. In VS.NET 2005 under Tools | Options | Text Editor Extensions add WCSX or your custom scriptmap extension (like .Blog) as Web Form Editor
      Extended Options:
      Make sure that when you go to Tools | Options you always check the Show all settings checkbox. Some options like the extension mapping become available only after enabling this flag. This is especially true for Visual Web Developer.

    2. Add a Web.config to the root of your Web directory
      Then add the following to have VS.NET recognize your specific application extension:
      <configuration> <system.web> <compilation defaultLanguage="c#" debug="true"> <buildProviders> <add extension=".blog" type="System.Web.Compilation.PageBuildProvider" /> <add extension=".wcsx" type="System.Web.Compilation.PageBuildProvider" /> </buildProviders> </compilation> </system.web> </configuration>

    Web Connection will automatically create #2 when you create a new project or process with the Wizards as it creates a web.config file automatically. But the Editor configuration is a required manual step for each scriptmap you define.

    Already have a project using WCSX Extensions?

    If you started out creating a new project with the WCSX extension don’t fret. You can easily use a custom class as well. Here’s what you do:

    And that’s it. At that point you can modify the MyProcess class and provide customized behaviors just like a custom scriptmap. Just be aware that now all WCSX requests route through this handler in this project.

    When starting out from scratch it’s best to use a custom scriptmap from the start.



    FAQ Posts from the Web Connection WebLog


    The following are a series of Web Log Entries that might be of interest



    Web Control Reference


    The Web Control framework comes with a number of stock controls and this section is a reference to these controls.

    Because there are so many controls this list is broken up into Core Controls which are the base class controls and some elemental controls like WebPage and WebUserControl. And then there are the following subgroups:

    Control Inheritance

    This guide shows properties, events and methodsthat each control implements. Some of the controls have


    Core Controls

    The following is a list core controls that drive the base functionality of the Web Control framework. wwWebControl is the base control for all other controls, while wwWebPage is the entry point for Page processing that manages postback and viewstate management as well as event processing. The wwWebUserControl is a visual control class that allows creation of composite classes by users and embed them into other pages.

    ClassDescription
      wwWebControl The wwWebControl class is the base controls for all other controls in the Web Connection WebControl framework. It provides the core functionality for most controls.
      wwWebPage The wwWebPage class is the top level container for a WebControl page. This container acts as the entry point for event processing and provides the HTML Document for page generation.
      wwWebUserControl The wwWebUserControl class is the base class that is used for visual User Controls that can be visually designed and then be dragged and dropped onto page canvas for visual resusability. User Controls are essentially mini forms and behave very similar to forms and provide a container for other visual controls.


    Class wwWebControl


    The wwWebControl class is the base controls for all other controls in the Web Connection WebControl framework. It provides the core functionality for most controls.

    If you are a control developer you will want to take a close look at the control class to see how to override the base functionality of the Control Class.


    Custom
      wwWebControl

    Class Members

    MemberDescription
    AddAttribute Adds an attribute to the Attributes collection with the optional ability to append to the existing attribute value if one exists by using the llAppend parameter.
    o.wwWebControl.AddAttribute(lcKey,lcValue,llAppend)
    AddControl This method is used to add any child controls to this control.
    o.AddControl(loCtl)
    AddScriptAttribute Adds JavaScript code to an attribute based script event like onclick. This method allows appending script so that scripts are added cumulatively.
    o.wwWebControl.AddScriptAttribute(lcScriptingEvent,lcScript,llAppend)
    AddStyleTag Adds or changes a style tag on the control's Style property. If you're chaning style properties in code it's recommended you use this method rather than directly changing the Style property manually.
    o.AddStyleTag(lcStyle,lcValue)
    BindControlSource Internal method used to bind a ControlSource to the ControlProperty of the control.
    o.BindControlSource()
    DataBind Performs databinding on a specific control. The base implementation is an abstract method so you need to provide an implementation for each control you create.
    o.DataBind()
    Dispose Key method for clearing state of a control.
    o.Dispose()
    FindControl Returns a control in the control's immediate child collection by name.
    o.FindControl(lcControlId,llRecursive)
    FireEvents Generic WebControl method that is used retrieve the Event Target information and tries to find and fire the event specified.
    o.FireEvents(lcEventTarget)
    GetCachedOutput Internal method that retrieves the cached output by checking the cache. Returns "" if no cache hit is made.
    o.GetCachedOutput()
    HookupEvent Attaches an event an event handler to a control event.
    o.HookupEvent(lcEvent,loHandler,lcMethod)
    LoadViewState Internal implementation method that loads up control state from ViewState. Occurs prior to Post data processing.
    o.LoadViewState()
    OnInit First event fired in the Page Pipeline. Called from the Init() of the control. This event always fires and you can use it to override any initiliazation behavior.
    o.OnInit()
    OnLoad OnLoad fires once a control has been fully initialized. This means it has initialized and had its control state loaded either from ViewState or POST data.
    o.OnLoad()
    OnLoadControlState OnLoadControlState fires when the control tries to retrieve it's state in a POST back. The default implementation doesn't do anything - each control must implement its own functionality.
    o.OnLoadControlState()
    OnLoadViewState OnLoadViewState is called after Init and prior to reading POST data. This method retrieves any Viewstate data stored for this control and assigns it. The default implementation provides for most scenarios.
    o.OnLoadViewState()
    OnPreRender Fires just before the Render() method and after Events have fired, so you can make any last minute changes to the page state before rendereing.
    o.OnPreRender()
    OnSaveViewState Called after Render() has completed. This method by default handles picking up and encoding the ViewState.
    o.OnSaveViewState()
    PreserveProperty PreserveProperty is used to save property values between PostPacks using ViewState.
    o.PreserveProperty(lcProperty)
    Process Starts the Page Event Pipeline that starts firing the Event Sequence from OnInit through Dispose.
    o.Process()
    Render The core method for each Web Control class that is responsible for creating the final HTML output for the control.
    o.Render()
    RenderControlError This method is responsible for rendering Error icons/messages if an error occurs during unbinding.
    o.RenderControlError()
    ResolveUrl Fixes up Urls based on an Application Relative path including support for ~ path syntax that injects the Application relative path into the URL.
    o.ResolveUrl(lcUrl)
    SaveViewState Internal method that manages encoding ViewState for each control.
    o.SaveViewState()
    UnbindControlSource Unbinds a Control Source from a Web Control back into the underlying Field. Works only for simple
    binding.
    o.UnbindControlSource()
    UnbindData Unbind data retrieves data from a Control and binds it back into its ControlSource.
    o.UnbindData()
    WriteBaseTags Writes out the basic tags used on a control.
    o.WriteBaseTags(llSkipId)
    WriteCachedOutput Takes output and writes it into the cache. The framework takes care of naming the cachekey or uses the CacheKey you specified explicitly.
    o.WriteCachedOutput(lcOutput)
    WriteEnabledTags Writes the Enabled and ReadOnly tags if they are set.
    o.WriteEnabledTags()
    WriteEncodedAttributeString Writes an attribute value string that is properly encoded.
    o.WriteEncodedAttributeString(lcValue)
    WriteIdTags Writes only the Name and ID tags.
    o.WriteIdTags()
    WriteStyleAndClassTags Writes out the Style and Class tags for the control.
    o.WriteStyleAndClassTags(llNoWidthAndHeight)
    WriteUnitValue Writes a measurement value as a string. If a numeric value is passed it's turned into a string with a px postfix for pixel size. String values are passed through and all others are transformed into a string.
    o.WriteUnitValue(vUnit)
    AllowedMarkupChildren A comma delimited list of element names that are allowed as child elements of this control and can be used without specifying runat="server" or the full wwWeb<ControlClass> name.
    Attributes A collection of Attributes that are to be rendered on the control. Any attributed added is rendered into the HTML of the control.
    AutoPostBack Determines whether controls automatically post back
    BackColor The Background Color for the control.
    BindingErrorMessage An error message that is to be displayed when a bind back error occurs on the control.
    CacheDuration If this value is > 0 the output of this control is cacheable. Value is in seconds.
    CacheKey The optional name of the Cache key used if caching the output from the control.
    CanContainLiterals Determines whether a control is a container control that can contain literal content.
    ChildControls Child Controls of the current control.
    Context A reference to the Web Connection Process class. Provided for ASP.NET conceptual compatibility.
    ControlIndex Internally used index used to create a unique control ID when rendering list containers like the Repeater.
    ControlSource A ControlSource expression that is evaluated during databinding and assigned to the control's ControSourceProperty.
    ControlSourceFormat FoxPro Format String used when a control source is bound to data. Also support a format method.
    ControlSourceMode Determines how the ControlSource is applied for inbound and outbound binding.
    ControlSourceProperty The control's property that the ControlSource is binding to.
    ControlSourceType Optional property that allows explicit specification of the type of the control source.
    ControlSourceUnbindExpression This optional property allows you to explicitly specify a separate ControlSource expression for unbinding back into the underlying data source. Use this if you need to bind to methods rather than properties or to read and write into separate underlying properties.
    CssClass A CSS class name applied to this control.
    DefaultEvent The Default Event that is fired for a given control if no event argument can be found.
    DefaultProperty Default property for a control that has embedded content. For example, a TextBox's content is mapped to the .Text property.
    Enabled Determines whether a control is enabled.
    EnableViewState Determines whether ViewState is enabled for this control.
    ErrorMessageLocation Determines the location of Error Messages for controls when using control unbinding.
    ForeColor ForeColor for this control if it applies.
    Height The height of the control. This value is either numeric or string with string preferrable.
    ID The ID/Name of the control.
    IsContainerControl Determines whether this control allows contained controls.
    IsInputControl Determines whether a control is used as an INPUT control and requires a NAME tag to be rendered.
    IsPostBack Determines whether the current request is in a PostBack. Generally this property should be checked only on the form.
    lDisposeCalled Internal flag that determines whether Dispose has been called on this control. Used to avoid duplicate Dispose calls.
    lEndResponse Flag that is set to tell that the Response output has completed. No further rendering needs to take place.
    OverrideNamingContainer This property can be set on a container control to force it to NOT generate an additional naming container for its UniqueId property. This can avoid contained control names like panelContent_txtName and instead generate the field as plain old txtName.
    Page Reference to the top level page object.
    ParentControl The immediate parent of the current control.
    PostHtml Html Text as a string the follows the control.
    PreHtml Html text as a string that preceeds the control.
    PreservedProperties A collection of properties of the control that are persisted into ViewState.
    ReadOnly Determines if a control is read only.
    RequiresFormRef Internal property that determines whether the control
    Style A CSS Style string that is applied to the control.
    Text The 'value' of the control. This is the primary value used for display and assignment.
    ToolTip Sets the pop up tooltip for the control as set by title= on most controls
    UniqueID A Unique ID for the control which is embedded into the page this value includes container and index information. Any retrieval of form variables should be done using UniqueID values.
    UserFieldName The display fieldname for this control - optionallyused for error messages
    ValidationExpression Validation Expression that validates the value that as been unbound by the Unbind() method by Control Source Binding.
    ViewState ViewState is a collection of state values that are specific to a given control. Each control has its own ViewState and there's a 'global' viewstate on the Page object.
    Visible Determines if a control is visible. Invisible controls are not rendered at all!
    Width Width for the control. This value can be a numeric or string value - string is preferred. This value is mirrored by the Protected cWidth property which can be more efficient.

    Requirements


    Web Control Basics


    A Web Control is an object that has properties which are then rendered into HTML by the framework. The advantage of a control is that your code can simply set properties and you can have a single optimized HTML generation routine that turns the control property settings into HTML consistently.

    If you create controls in script tags, any control properties that are found in the script markup for a control are mapped to the property. So if you have a textbox with:

    <ww:wwWebTextBox runat="server" id="txtName" Text="Rick" Width="200px" onchange="CheckValidity()"/>

    Web Connection parses the script and assigns Id, Text, Width to the matching properties of the control. It gets turned into:

    loCtl = CreateObject("wwWebTextBox") loCtl.ID = "txtName" loCtl.Text = "Rick" loCtl.Width = "200px" *** Any non-property/method attributes are added to the Attributes loCtl.Attributes.Add("onchange","CheckValidity")

    In addition to properties any control also works with custom attributes that are assigned either in script page content or via code. In the TextBox example onchange is not a property or method on the control so it is mapped to a custom attribute and added to the Attributes collection which is rendered as is into the HTML. Anything that doesn't map to a Property or Method is turned into an attribute and simply added. At render time the attribute is simply rendered as is.

    The Attributes collection is also available programmatically from within your code, and you can use it to force new attribute tags onto controls that are not already handled by the control framework.

    Web Control Events


    Web Controls also support events. In script code events are mapped by using an attribute for the event name and then assigning a handler method on the form to it.

    <ww:wwWebButton runat="server" id="btnSubmit" Text="Click me" Click="btnSubmit_Click" />

    The above maps the button's Click event to the btnSubmit_Click method on the WebPage which should look like this:

    FUNCTION btnSubmit_Click() *** Do something this.btnSubmit.Text = "Clicked" ENDFUNC

    In code this maps to the following using the HookupEvent method of a control:

    THIS.btnSubmit = CREATEOBJECT("wwwebbutton",THIS) THIS.btnSubmit.Id = "btnSubmit" THIS.btnSubmit.Attributes.Add("AccessKey","S") THIS.btnSubmit.HookupEvent("Click",THIS,"btnSubmit_Click") THIS.btnSubmit.Text = [ Save Topic ] THIS.btnSubmit.Width = [131px] THIS.AddControl(THIS.btnSubmit)

    If you hook up an event in the script page or via code and don't have a corresponding handler method defined in your class, you will get a runtime error immediately when the page runs as a reminder that you have to implement the method. The error occurs inside of the Web Connection framework in the HookupEvent method when debugging is on, otherwise it bubbles to the page's OnError handler or if not handled there to the Process.OnError method.

    Event Firing from the Client

    Note that you can map any function in this manner. But keep in mind that it's the control's responsibility to fire these events and make them happen. Some events like Button Clicks are automatic, but most other events like an OnChange event or PageChanged event needs to be explicitly triggered by the client code.

    Events can be initiated on the client via the PostBack script handler. This script handler is a small bit of JavaScript that initiates a POSTBack, and passes back events to the server. The server parses the event parameters and fires events based on that.

    It's usually the responsibility of the server control to:

    Example: DataGrid Paging Events
    This is easiest to explain with an example. The wwWebDataGrid includes a PageChanged event. In order to fire this event when you click on one of the paging buttons you'll see code like this fired from the button:

    javascript:__doPostBack('gdEntries','PageIndexChanged','2')

    The control actually generates these script calls into the HTML:

    <tr class="gridpager" ><td colspan="2" align="right" class="gridpager" > Pages: <b>1</b> <a href="javascript:__doPostBack('gdEntries','PageIndexChanged','2')" style="color:white">2</a> <a href="javascript:__doPostBack('gdEntries','PageIndexChanged','3')" style="color:white">3</a> </td></tr></table>

    These events specify to call the PageIndexChanged event on the gdEntries control with a parameter of 2 or 3 or whatever. This actually triggers an event on the server.

    The script call above includes the __doPostBack() script which looks like this:

    <input type="hidden" id="__EVENTTARGET" name="__EVENTTARGET" value="" /> <input type="hidden" id="__EVENTARGUMENT" name="__EVENTARGUMENT" value="" /> <input type="hidden" id="__EVENTPARAMETER" name="__EVENTPARAMETER" value="" /> <script type="text/javascript"> var theForm = document.forms['form1']; if (!theForm) { theForm = document.form1; } function __doPostBack(eventTarget, eventMethod,eventParameter) { if (!theForm.onsubmit || (theForm.onsubmit() != false)) { theForm.__EVENTTARGET.value = eventTarget; if (eventMethod) theForm.__EVENTARGUMENT.value = eventMethod; if (eventParameter) theForm.__EVENTPARAMETER.value = eventParameter; theForm.submit(); } } </script>

    So how does all of this get generated? The Web Control needs to manage this on its own. So the wwWebDataGrid does the following.

    In OnPreRender() it checks to see what the PageSize is. If it's greater than 0 it needs to add the post back script and it also need to trigger keeping track of the current page selected in ViewState:

    * wwWebDataGrid::OnPreRender() FUNCTION OnPreRender() IF THIS.PageSize > 0 AND !ISNULL(this.Page) *** Must register stock code for Page changes this.Page.RegisterPostbackScriptcode() *** We want to automatically save the CurrentPage Index this.PreserveProperty("CurrentPageIndex") ENDIF IF !EMPTY(this.SortColumn) this.Page.RegisterPostbackScriptcode() *** Always persist the sort column if it's set this.PreserveProperty("SortColumn") ENDIF IF !EMPTY(this.SortOrder) this.PreserveProperty("SortOrder") ENDIF ENDFUNC

    This ensures that the Postback script gets injected into the form. Note that if the script exists already it's not added more than once.

    To render the script calls that trigger the PageIndexChanged event in each of the pager links is the responsibility of the control which manages this as part of the rendering process:

    PROTECTED FUNCTION PagerPostBackLink(lnPage,lcText) IF EMPTY(lcText) lcText = TRANSFORM(lnPage) ENDIF RETURN [<a href="javascript:__doPostBack('] + this.UniqueId + [','PageIndexChanged','] + ; TRANSFORM(lnPage) +[')" style="color:] + this.PagerTextColor + [">] + lcText + [</a>]

    The logic to decide which buttons to display is actually a bit length to show here, but just know that it loops through all the pages that are to be displayed and calls this method to actually inject the link into the page.

    Firing the event

    Once the user clicks on one of the pagers the event is officially fired. The event values are encoded into hidden form variables which are sent to the server. The are:

    These parameters are then used by the WebPage's FireEvents method to determine which control and which event is to be fired if any. The WebPage then calls the control's method that maps the event specified. So if the event is click on btnSubmit the btnSubmit.Click method is fired.

    btnSubmit.Click() however is not going to have any code in it because it's on the base control. So rather you have attached an event handler to this 'event' using btnSubmit.HookupEvent() which is automatically generated for you when you use the script syntax described earlier:

    <ww:wwWebButton ... Click="btnSubmit_Click" />

    HookupEvent routes the event to your specified method - in this case the WebPage control's btnSubmit_Click method, where you can then handle the event.

    Events are a modular way to code!

    The event handling mechanism is very powerful and at the core of the Web Connection WebControl Framework. Compared to classic Web Connection applications it allows isolation of distinct blocks of code into logical sippets much in the way you write code in desktop applications. This makes for more modular and much easier management of code in complex pages. Rather than one monumental function that has lots of CASE statements to figure out where to go you have event methods that logically map to operations from a Web page.

    Web Control Inheritance and Containership


    All control inherit from wwWebControl and so rely on this base class for most of their internal functionality. Because most controls rely on base behavior of the wwWebControl class it's important to remember that if you are overriding any methods or events of the controls you should always call DoDefault() to make sure the base behavior gets called.

    The Importance of DoDefault()

    The base behavior is especially important for container controls that need to fire events into their child controls. Containership events are fired from the parent control into the child control. So for example OnInit fires on the Page object, which in turn calls the OnInit in each of the child controls. If you override the OnInit() metod on the Page and forget to call DoDefault() the child controls won't fire teir OnInit events. This may or may not be a problem depending on whether the child controls have logic in these events. As you might expect this can lead to some really hard to debug bugs in your code.

    As a hard and fast rule: Always call DoDefault() when overriding any control methods either on Custom controls or User Controls or Page objects.

    There are two exceptions to this rule:

    Both of these AUTOMATICALLY call their child control events. The reason for this inconsistency is that these two methods are the most commonly implememented in your applications and this way you won't have to remember the DoDefault().


    wwWebControl.AddAttribute


    Adds an attribute to the Attributes collection with the optional ability to append to the existing attribute value if one exists by using the llAppend parameter.

    o.wwWebControl.AddAttribute(lcKey,lcValue,llAppend)                         
    

    Parameters

    lcKey
    The attribute name to add

    lcValue
    The value to set it to

    llAppend
    if .T. appends the value to the existing attribute's value if it exists.

    wwWebControl.AddScriptAttribute


    Adds JavaScript code to an attribute based script event like onclick. This method allows appending script so that scripts are added cumulatively.

    o.wwWebControl.AddScriptAttribute(lcScriptingEvent,lcScript,llAppend)       
    

    Parameters

    lcScriptingEvent
    The event attribute to assign the script to (ie. onclick, onchange etc.)

    lcScript
    The JavaScript code to use

    llAppend
    Whether the script is replaced (.f. default) or appended (.t.)

    wwWebControl::AddControl


    This method is used to add any child controls to this control.

    AddControl provides the basic containership mechanism by setting the ParentControl property of the child control so it can reference back up the class hierarchy.

    o.AddControl(loCtl)
    

    Parameters

    loCtl
    Child control to be added.

    wwWebControl::AddStyleTag


    Adds or changes a style tag on the control's Style property. If you're chaning style properties in code it's recommended you use this method rather than directly changing the Style property manually.

    This method checks for existing style tags and updates them or adds the style.

    Example:

    o.Style = "color:green;height:20px;width:400px;border-color:black;border-width:30px;" o.AddStyleTag("font-weight","bold") o.AddStyleTag("color","cornsilk") o.AddStyleTag("width","240px")

    Result:
    color:cornsilk;height:20px;width:240px;border-color:black;border-width:30px;font-weight:bold;



    o.AddStyleTag(lcStyle,lcValue)                                
    

    Parameters

    lcStyle
    The style to set for example, background or border-width

    lcValue
    the value to set it to: green or 2

    wwWebControl::BindControlSource


    Internal method used to bind a ControlSource to the ControlProperty of the control.


    o.BindControlSource()
    

    wwWebControl::DataBind


    Performs databinding on a specific control. The base implementation is an abstract method so you need to provide an implementation for each control you create.

    Databinding comes in two flavors:

    If you're new to DataBinding please check out the databinding section of the documentation as there are a few important considerations you need to think about.

    o.DataBind()
    

    wwWebControl::Dispose


    Key method for clearing state of a control.

    It is crucial that every control calls Dispose() when shutting down, especially container controls. If Dispose() is not called it's very likely that there will be hung references with containers.

    For details on implementing a custom Dispose() handler please see the implementation of wwWebControl::Dispose(), which demonstrates complete cleanup. In most cases it's sufficient to simple DoDefault() and use the default behavior, but you may have to override for any custom collections or other reference objects you define.

    o.Dispose()
    

    Remarks

    Any control that contains reference to any other control reference objects (collections of objects especially) should implement a custom Dispose() handler to clean up these objects and child objects. Often you may have to explicitly loop through the child objects.

    overrides dispose should call DoDefault(). If a control has child controls that need some sort of explicit cleanup you should implement a custom Dispose and loop through the child controls yourself. The default implementation loops through all controls and childcontrols and calls Dispose() on each.


    Example

    *** ListControl::Dispose()
    FUNCTION Dispose()
    
    *** Don't do if we were already here
    IF THIS.lDISPOSECALLED 
       RETURN
    ENDIF
    
    *** Clear custom collections
    THIS.Items=null
    This.SelectedValues = null
    
    *** Let the default handler do its thing
    DODEFAULT()
    ENDFUNC

    wwWebControl::FindControl


    Returns a control in the control's immediate child collection by name.

    o.FindControl(lcControlId,llRecursive)
    

    Parameters

    lcControlId
    The Name of the control. Note this control must be an immediate child of the control you are referencing. The control must have been added via AddControl.

    llRecursive
    If set recursively drills into child containers to find a control. Default is to only search the current container (.F.).

    wwWebControl::FireEvents


    Generic WebControl method that is used retrieve the Event Target information and tries to find and fire the event specified.

    This method is relevant only for a container control and generally fired only at that top level WebPage. This Event does not delegate into child controls.

    o.FireEvents(lcEventTarget)
    

    Parameters

    lcEventTarget
    Optional EventTarget which is the name of a control.

    wwWebControl::GetCachedOutput


    Internal method that retrieves the cached output by checking the cache. Returns "" if no cache hit is made.

    This method

    o.GetCachedOutput()
    

    wwWebControl::HookupEvent


    Attaches an event an event handler to a control event.

    For example, you use this method to wire an a button's Click event to your form's handler method that handles this event.


    o.HookupEvent(lcEvent,loHandler,lcMethod)
    

    Parameters

    lcEvent

    loHandler

    lcMethod


    Example

    *** Hookup button click event to btnDelete_Click on Page Class THIS.btnDelete = CREATEOBJECT("wwwebbutton",THIS) THIS.btnDelete.Id = "btnDelete" THIS.btnDelete.HookupEvent("Click",THIS,"btnDelete_Click") THIS.btnDelete.Text = [Delete] THIS.AddControl(THIS.btnDelete)

    wwWebControl::LoadViewState


    Internal implementation method that loads up control state from ViewState. Occurs prior to Post data processing.

    This method is responsible for de-parsing the controls viewstate and assigning properties. Generally you don't need to override this behavior.



    o.LoadViewState()
    

    wwWebControl::OnInit


    First event fired in the Page Pipeline. Called from the Init() of the control. This event always fires and you can use it to override any initiliazation behavior.

    o.OnInit()
    

    Remarks

    Always call DODEFAULT() if you override this method.

    When this event fires the page and other controls may not be initialized yet. Use this method only for internal initialization.

    wwWebControl::OnLoad


    OnLoad fires once a control has been fully initialized. This means it has initialized and had its control state loaded either from ViewState or POST data.

    OnLoad() fires before any events are fired on the control.

    OnLoad() is the Event method that does NOT require DODEFAULT() to be called as the page framework automatically calls the child containers for you. This is inconsitent but done primarily to make the OnLoad() code easier to work with and consistent with ASP.NET.

    o.OnLoad()
    

    wwWebControl::OnLoadControlState


    OnLoadControlState fires when the control tries to retrieve it's state in a POST back. The default implementation doesn't do anything - each control must implement its own functionality.

    This method is called after OnLoadViewState().

    Be sure to call DoDefault() especially on container controls!

    o.OnLoadControlState()
    

    wwWebControl::OnLoadViewState


    OnLoadViewState is called after Init and prior to reading POST data. This method retrieves any Viewstate data stored for this control and assigns it. The default implementation provides for most scenarios.

    ViewState is page based state that is encoded and sent back to the client on each request. It is encoded (base64) and reassigned when the page is POSTed back to the server. You can use the Control's ViewState object to assign viewstate or you can call PreserveProperty() which saves a specific property and automatically restores it once.

    Any value stored in ViewState is automatically persisted until you explicitly clear it.

    o.OnLoadViewState()
    

    wwWebControl::OnPreRender


    Fires just before the Render() method and after Events have fired, so you can make any last minute changes to the page state before rendereing.

    OnPreRender() is very useful for code that need to run for every path through the application and is particular useful for state based control assignments in complex pages. Often it can be used to do the final control assignments that are collected in the OnLoad or Event methods.



    o.OnPreRender()
    

    Remarks

    Don't forget to DODEFAULT(). It is vital on this method!

    wwWebControl::OnSaveViewState


    Called after Render() has completed. This method by default handles picking up and encoding the ViewState.

    The final viewstate is collected by the Page class and encoded and written onto the form as a hidden form variable.


    o.OnSaveViewState()
    

    wwWebControl::PreserveProperty


    PreserveProperty is used to save property values between PostPacks using ViewState.

    This method lets you easily persist a control's property value in ViewState automatically. Note that the value persists only on the immediate PostBack so if you always want to persist a property you should put the call to PreserveProperty into the OnLoad() or other event that ALWAYS fires.


    o.PreserveProperty(lcProperty)
    

    Parameters

    lcProperty


    Example

    *** Tell WWWC to track properties in viewstate this.btnShowPanel.PreserveProperty("text") this.btnShowPanel.PreserveProperty("forecolor") this.panContainer.PreserveProperty("visible") this.ErrorDisplay.PreserveProperty("text") this.radWebTool.PreserveProperty("visible") this.lstWebTool.PreserveProperty("visible")

    wwWebControl::Process


    Starts the Page Event Pipeline that starts firing the Event Sequence from OnInit through Dispose.

    This method should be called only on the the top level container to tell it to start processing page events. Typically this method is only called on the Page object.


    o.Process()
    

    wwWebControl::Render


    The core method for each Web Control class that is responsible for creating the final HTML output for the control.

    The Render method should check for several things:




    o.Render()
    

    wwWebControl::RenderControlError


    This method is responsible for rendering Error icons/messages if an error occurs during unbinding.

    o.RenderControlError()
    

    wwWebControl::ResolveUrl


    Fixes up Urls based on an Application Relative path including support for ~ path syntax that injects the Application relative path into the URL.

    When parsing the path, the ~ value is replaced with the Application's relative virtual path (ie. /wconnect/) which is appended to the Url provided replacing the ~. The application's relative path is retrieved from the app

    For example:

    Assume Web Connection is installed in /wconnect/ of the Web site and I want to reference an image in /wconnect/images/ while running the application out of /wconnect/weblog/.

    To reference the image you can use:

    ~images/SomePic.gif

    ~ expands to the application's base path of /wconnect/ which is injected.

    ResolveUrl is used on all URL based properties in Web Conntrols. So the HyperLink control, the WebImage control all use this functionality to provide consistent pathing in your apps.

    this.picImage.ImageUrl = "~images/wconnect.gif"


    o.ResolveUrl(lcUrl)
    

    Parameters

    lcUrl


    Remarks

    ResolveUrl relies on the
    wwProcess::cUrlBasePath property which is used to fix up paths. By default this value is set from the Process specific Configuration object defined in your Mainline PRG file and the related INI file key it uses. If you don't have a process level configuration object or the object is not hooked up or doesn't contain a cVirtualPath property the base path will end up blank and result in invalid Urls. To fix either assign cUrlBase in your Process startup code (OnProcessInit or Init or cUrlBasePath assignment) or make sure you create a configuration object and attach it to the server configuration object.

    wwWebControl::SaveViewState


    Internal method that manages encoding ViewState for each control.

    Generally you don't need to override this method.


    o.SaveViewState()
    

    wwWebControl::UnbindControlSource


    Unbinds a Control Source from a Web Control back into the underlying Field. Works only for simple
    binding.

    This is the internal implemenation method


    o.UnbindControlSource()
    

    wwWebControl::UnbindData


    Unbind data retrieves data from a Control and binds it back into its ControlSource.

    This powerful method makes short work of retrieving data from controls back into their data stores. DataBind() works hierarchically so you can call it on the WebPage and it will fire for all controls on the page.

    ControlSource binding is available at the control level.

    o.UnbindData()
    

    wwWebControl::WriteBaseTags


    Writes out the basic tags used on a control.

    This method writes out most of the common tags specific to this control. This is the highlevel method that does most of the stock control tag output. More specific versions only write a few specific controls.

    This method writes:



    o.WriteBaseTags(llSkipId)
    

    Parameters

    llSkipId
    If .T. doesn't write ID and NAME attributes.

    wwWebControl::WriteCachedOutput


    Takes output and writes it into the cache. The framework takes care of naming the cachekey or uses the CacheKey you specified explicitly.


    o.WriteCachedOutput(lcOutput)
    

    Parameters

    lcOutput
    The output to write to the cache

    wwWebControl::WriteEnabledTags


    Writes the Enabled and ReadOnly tags if they are set.

    o.WriteEnabledTags()
    

    wwWebControl::WriteEncodedAttributeString


    Writes an attribute value string that is properly encoded.

    The result value ensures that values like Text=" "Some Value" " don't occur but rather are translated into Text=" "Some Value" ". Other encodings may be added at a later point

    o.WriteEncodedAttributeString(lcValue)                        
    

    Parameters

    lcValue

    wwWebControl::WriteIdTags


    Writes only the Name and ID tags.

    o.WriteIdTags()
    

    wwWebControl::WriteStyleAndClassTags


    Writes out the Style and Class tags for the control.

    The Style tag is written by using the existing value, and then parsing into it the height and width and color information. If these tags exist already in your Style tag they are not overwritten.


    o.WriteStyleAndClassTags(llNoWidthAndHeight)
    

    Parameters

    llNoWidthAndHeight
    Skip positional attributes

    wwWebControl::WriteUnitValue


    Writes a measurement value as a string. If a numeric value is passed it's turned into a string with a px postfix for pixel size. String values are passed through and all others are transformed into a string.

    o.WriteUnitValue(vUnit)                                       
    

    Return Value

    numeric value as a properly formatted string

    Parameters

    vUnit
    A numeric or string size or location indicator

    wwWebControl::Attributes


    A collection of Attributes that are to be rendered on the control. Any attributed added is rendered into the HTML of the control.

    For example, to add a custom client side JavaScript script handler you can use:

    this.Attributes.Add("OnClick","ClientClick(this.value);")

    You can use this for any attribute you might need to add.

    Default Value

    Initial value: null

    wwWebControl::AllowedMarkupChildren


    A comma delimited list of element names that are allowed as child elements of this control and can be used without specifying runat="server" or the full wwWeb<ControlClass> name.

    This property is used for controls to notify the parser of what 'unconventional' element names to expect when parsing child control content.

    Examples of controls that use this feature are the list control (for <ListItems> and <asp:ListItem> both of which can be specified without runat="server") and <ItemTemplate> on a Repeater. For example the Repeater control defines this list:

    AllowedMarkupChildren = "itemtemplate,headertemplate,footertemplate"

    So that a Repeater can be defined like this:

    <ww:wwWebRepeater runat="server" id="repList"> <HeaderTemplate> Header Text<hr /> <HeaderTemplate> <ItemTemplate> Company: <%= Company %><br> CareOf: <%= CareOf %> <p /> <ItemTemplate> <FooterTemplate> <hr /> </FooterTemplate> </ww:wwWebRepeater>

    Notice that HeaderTemplate and ItemTemplate do not have a runat="server" attribute nor use the wwWebHeaderTemplate prefix.

    This functionality is provided for control developers and this functionality is required for any control that is to be contained in the parent container with special naming syntax. Each item that is found (ie. ItemTemplate) is added to the container with AddControl.

    o.AllowedMarkupChildren                                       
    

    wwWebControl::AutoPostBack


    Determines whether controls automatically post back

    Note this property is not used by all controls. Some controls that support it:



    Default Value

    Initial value: .F.

    wwWebControl::BackColor


    The Background Color for the control.

    wwWebControl::BindingErrorMessage


    An error message that is to be displayed when a bind back error occurs on the control.

    This property can be set for binding and validation errors and usually is set as part of ControlSource and ValidationExpression handlers that are called when binding and unbinding values. By assigning a message to this property you are overriding the default message binding error message.

    wwWebControl::CacheDuration


    If this value is > 0 the output of this control is cacheable. Value is in seconds.

    Cached content is stored using the wwCache object in Server.oCache.

    Be very careful with Cached content. Once cached you can not easily uncache controls because the cache is read very early in the page cycle. The only way to clear items is to effectively purge the item manually (for example: Server.oCache.RemoveItem(this.MyControl.CacheKey)).

    Note that caching will capture whatever value the control has at render time. This can have some interesting side effects for some controls. For example, a listbox the content will be the list's items as well as any possible selections, so any selections or non-selections will always be restored on the next hit. Caching works best for content controls that like labels, containers, or other complex objects that might render repeated values that stay the same for the duration specified.

    Currently the Cache implementation is Render time specific only, so all code up to Render() executes. Only when Render fires is the cache checked. This behavior may change in the future.

    Default Value

    Initial value: 0

    wwWebControl::CacheKey


    The optional name of the Cache key used if caching the output from the control.

    Optional - use only if you need to share the content with multiple controls on multiple forms otherwise the default will be the UniqueKey of the control.

    By default Web Connection will generate a unique cache key based on the URL and UniqueId of the control.

    Use the this property only if you need to share a cached control in multiple places of your application. In that scenario all of the controls will share the same cache instance.

    wwWebControl::CanContainLiterals


    Determines whether a control is a container control that can contain literal content.

    Some controls like Panel can while others like the WebDataGrid cannot. Internally used.

    Default Value

    Initial value: .T.

    wwWebControl::ChildControls


    Child Controls of the current control.

    Child Controls are vital to the Web Connection model as everything is based on containership. Parent Controls fire the events they receive into the ChildControls. The parser renders Parents and children hierarchically and objects are disposed hierarchically.

    ChildControls and the Parent properties provide for the containership model in the WebControl Framework.

    Default Value

    Initial value: null

    wwWebControl::Context


    A reference to the Web Connection Process class. Provided for ASP.NET conceptual compatibility.

    Default Value

    Initial value: null

    wwWebControl::ControlIndex


    Internally used index used to create a unique control ID when rendering list containers like the Repeater.

    This value must be assigned explicitly by the parent rendering the child controls since it holds the master iterator.

    Default Value

    Initial value: 0

    wwWebControl::ControlSource


    A ControlSource expression that is evaluated during databinding and assigned to the control's ControSourceProperty.

    The Expression() must be valid for the context of the control. If you need to reference properties of the Page object you need to explicitly reference it. Some examples of valid values:

    "this.Page.nPk"
    "this.Page.oEntry.oData.Company" && business object
    "Request.IsPostBack"
    "Server.oConfig.oWebLog.cHtmlPagePath"

    Note that you can't use PRIVATE and LOCAL variables defined elsewhere in the class since the call stack doesn't work downwards but on a sibling level. This means any values that you want to bind to either need to be PUBLIC (bad idea) or bound to a property on the Page or Control object.

    wwWebControl::ControlSourceFormat


    FoxPro Format String used when a control source is bound to data. Also support a format method.

    You can use standard FoxPro Format expressions like "@!" for uppercase, or "@YL" for a long date value. You can also use InputMasks like "999,999.99" for numbers. Stock FoxPro behavior.

    In addition you can also use dynamic code as Format methods which must take a single value as input and return a string result as output. The function or method must be prefixed with an = sign.

    For exampe to format a data you might use: "=TimeToC" which is wwUtils UDF function. You can also use methods in the current page: "=this.Page.FormatDate". Note that you cannot specify parenthesis here - the control will automatically format the expression and call this function/method with the evaluated ControlSource expression as a parameter.

    Note that this feature has tremendous potential for allowing you to dynamically assign content to controls. The same behavior is also used for DataGrids and essentially allows you to format each grid cell dynamically.

    Unbinding Note:
    If you're using format strings for input fields, you may run into problems saving the data back on the server. If you unbind and use a format that cannot be converted back into the original format (say a string that's formatted: "(-999.99)" ) the unbinding will fail. So be mindful of the two-way conversion that needs to take place.

    wwWebControl::ControlSourceMode


    Determines how the ControlSource is applied for inbound and outbound binding.


    o.ControlSourceMode                                           
    

    Default Value

    TwoWay

    wwWebControl::ControlSourceProperty


    The control's property that the ControlSource is binding to.

    For TextBox the ControlSource Property is Text. For a CheckBox it's Checked. For a ListBox it's SelectedValue.

    You can use this value to bind the ControlSource to any property of the control. Web Connection will try to do the appropriate type conversion for you.

    Default Value

    Initial value: Text

    wwWebControl::ControlSourceType


    Optional property that allows explicit specification of the type of the control source.

    If omitted Web Connection uses TYPE() on the control source to determine the type, but this may not always work if a value is null or set to a different type than the data saved. This property allows explicit overriding.

    o.ControlSourceType                                           
    

    wwWebControl::ControlSourceUnbindExpression


    This optional property allows you to explicitly specify a separate ControlSource expression for unbinding back into the underlying data source. Use this if you need to bind to methods rather than properties or to read and write into separate underlying properties.

    The value is assumed to be a property name by default.

    To use an expression prefix the expression with an =. To get the captured input value already converted to its proper type use {0} as a placeholder in the expression:

    UnBind to a simple property:
    ControlSourceUnbindExpression="MyPropertyName"

    UnBind an expression to a method:
    ControlSourceUnbindExpression="=this.Page.oBusiness.SetValue('MyProperty',{0})"



    o.wwWebControl.ControlSourceUnbindExpression                                
    

    wwWebControl::CssClass


    A CSS class name applied to this control.

    wwWebControl::DefaultEvent


    The Default Event that is fired for a given control if no event argument can be found.

    wwWebControl::DefaultProperty


    Default property for a control that has embedded content. For example, a TextBox's content is mapped to the .Text property.

    <ww:wwWebTextBox runat="server" id="txtName">Rick</ww:/wwWebTextBox>

    Rick is mapped to the Text property which is the DefaultProperty for the TextBox.

    Default Value

    Initial value: Text

    wwWebControl::Enabled


    Determines whether a control is enabled.

    PostBack controls that are disabled are automatically persisted into ViewState so their values are preserved. Similar behavior to ReadOnly.

    Default Value

    Initial value: .T.

    wwWebControl::EnableViewState


    Determines whether ViewState is enabled for this control.

    Generally you won't want to turn off Viewstate on controls. Unlike ASP.NET, Web Connection makes very sparing use of viewstate and does not persist list content into viewstate.

    Default Value

    Initial value: .T.

    wwWebControl::ErrorMessageLocation


    Determines the location of Error Messages for controls when using control unbinding.

    Supported values are:

    0 - None
    1 - Icon to the right
    2 - Icon and Text below
    3 - Error Text only below


    Default Value

    Initial value: 1

    wwWebControl::ForeColor


    ForeColor for this control if it applies.

    wwWebControl::Height


    The height of the control. This value is either numeric or string with string preferrable.

    Default Value

    Initial value: 0

    wwWebControl::ID


    The ID/Name of the control.

    Note this value is used for collection access only. The Web page generated ID will always be the UniqueId.

    wwWebControl::IsContainerControl


    Determines whether this control allows contained controls.

    Typical container controls are Panel, UserControl. Some not so common ones are DataGrid (contains columns), Repeater (contains Item Template) etc.

    This is an internal optimization flag, that allows shortcutting control events right at the top.


    Default Value

    Initial value: .F.

    wwWebControl::IsInputControl


    Determines whether a control is used as an INPUT control and requires a NAME tag to be rendered.

    o.IsInputControl                                              
    

    wwWebControl::IsPostBack


    Determines whether the current request is in a PostBack. Generally this property should be checked only on the form.

    Default Value

    Initial value: .f.

    wwWebControl::lDisposeCalled


    Internal flag that determines whether Dispose has been called on this control. Used to avoid duplicate Dispose calls.

    Any custom implementation of Dispose() should set this flag or have it set by the call to DoDefault().

    Default Value

    Initial value: .f.

    wwWebControl::lEndResponse


    Flag that is set to tell that the Response output has completed. No further rendering needs to take place.

    Default Value

    Initial value: .F.

    wwWebControl::Page


    Reference to the top level page object.

    This reference is used quite a bit internally so it's quite vital. The Page reference is passed in to the Init() of the control.

    Controls should check for the existance of the Page object before using it to ensure it is available. Some controls can be used outside of the page framework and if they can you want to make sure they don't have to use the Page object.

    Default Value

    Initial value: null

    wwWebControl::ParentControl


    The immediate parent of the current control.

    The ParentControl property is set in the AddControl() method.

    Default Value

    Initial value: null

    wwWebControl::OverrideNamingContainer


    This property can be set on a container control to force it to NOT generate an additional naming container for its UniqueId property. This can avoid contained control names like panelContent_txtName and instead generate the field as plain old txtName.

    This property can be applied to any container control, which always generate a new naming container by default. Simply set the property to true on the control to force the container to

    o.OverrideNamingContainer                                     
    

    wwWebControl::PostHtml


    Html Text as a string the follows the control.

    wwWebControl::PreHtml


    Html text as a string that preceeds the control.

    wwWebControl::PreservedProperties


    A collection of properties of the control that are persisted into ViewState.

    By specifying a propertyname you are telling Web Connection to save the property value into viewstate and retrieve it again on the next Postback. This is a very powerful feature that allows keeping state for properties that don't normally post back. For example, if you want to track a button caption or the color of a label.

    Note that PreserveProperty must be reset on every hit, so a call to PreserveProperty only affects the immediate request. If you want to more permanently track properties add them at the beginning of the Onload() override of your form.



    Default Value

    Initial value: null

    Example

    *** Tell WWWC to track properties in viewstate this.btnShowPanel.PreserveProperty("text") this.btnShowPanel.PreserveProperty("forecolor") this.panContainer.PreserveProperty("visible") this.ErrorDisplay.PreserveProperty("text") this.radWebTool.PreserveProperty("visible") this.lstWebTool.PreserveProperty("visible")

    wwWebControl::ReadOnly


    Determines if a control is read only.

    The stock Postback controls (TextBox, ChkBox, Lists etc.) controls are automatically persisted into ViewState if the ReadOnly property is .T.

    Default Value

    Initial value: .F.

    wwWebControl::RequiresFormRef


    Internal property that determines whether the control
    requires a permanent form reference or whether it can
    be a local variable

    Default Value

    Initial value: .T.

    wwWebControl::Style


    A CSS Style string that is applied to the control.

    wwWebControl::Text


    The 'value' of the control. This is the primary value used for display and assignment.

    Note that Value mirrors the Text property, but it's more efficient to use Text.

    wwWebControl::ToolTip


    Sets the pop up tooltip for the control as set by title= on most controls

    o.ToolTip                                                     
    

    wwWebControl::UniqueID


    A Unique ID for the control which is embedded into the page this value includes container and index information. Any retrieval of form variables should be done using UniqueID values.

    wwWebControl::UserFieldName


    The display fieldname for this control - optionallyused for error messages

    wwWebControl::ValidationExpression


    Validation Expression that validates the value that as been unbound by the Unbind() method by Control Source Binding.

    You can use any FoxPro expression that evaluates to .T. or .F. The expression executes in the scope of the control so THIS references the control.

    .T. indicates that validation is successful. .F. indicates that the validation failed. If failed the message in BindingErrorMessage is set with a generic error message or if the message is already set that value is displayed. This means you can pass the control and set the binding message explicitly.

    Examples:
    The simplest thing to do is to use a standard expression like this:

    !EMPTY(this.Text)

    This will display a generic error message (Input is invalid). Alternately you can point at some user defined code of your own to perform the validation. Typically you'll want to pass the control as a reference. For example:

    this.Page.ValidateName(THIS)

    You'd then implement a method on your Web Page clas that does something like this:

    FUNCTION ValidateName(loControl) IF loControl.Text != "Some Value" loControl.BindingErrorMessage = "Some Value must be entered." RETURN .F. ENDIF RETURN .T.

    To take this one step further you might create a more generic validation handler for the Page:

    THIS.Page.ControlValidation(this)

    Then implement a method for this functionality on your Web Page:

    FUNCTION ControlValidation(loControl) DO CASE CASE loControl.Id = "txtValue" IF loControl.Text $ "Value1,Value2,Value3" *** To assign a customer validation error message assign here: loControl.BindingErrorMessage = "Make sure the value is correct." RETURN .F. ENDIF CASE loControl.Id = "txtValue2" ... ENDCASE RETURN .T.

    Note that you can create a custom validation error message by assigning the BindingErrorMessage to the control. This gives you full control for the message displayed in the summary and next to the control.


    o.ValidationExpression                                        
    

    wwWebControl::ViewState


    ViewState is a collection of state values that are specific to a given control. Each control has its own ViewState and there's a 'global' viewstate on the Page object.

    ViewState is page based state that is encoded and sent back to the client on each request. It is encoded (base64) and reassigned when the page is POSTed back to the server. You can use the Control's ViewState object to assign viewstate or you can call PreserveProperty() which saves a specific property and automatically restores it once.

    Any value stored in ViewState is automatically persisted until you explicitly clear it.

    To save:

    Page.ViewState.Add("FilterString",lcFilter)

    To retrieve:

    lcValue = Page.ViewState.Item("FilterString") IF ISNULL(lcValue) lcValue = "" ENDIF

    Default Value

    Initial value: null

    wwWebControl::Visible


    Determines if a control is visible. Invisible controls are not rendered at all!

    Default Value

    Initial value: .T.

    wwWebControl::Width


    Width for the control. This value can be a numeric or string value - string is preferred. This value is mirrored by the Protected cWidth property which can be more efficient.

    Default Value

    Initial value: 0

    Class wwWebPage


    The wwWebPage class is the top level container for a WebControl page. This container acts as the entry point for event processing and provides the HTML Document for page generation.



    Custom
      
    wwWebControl
        wwWebPage

    Class Members

    MemberDescription
    AddBindingError Adds a binding error to the page's BindingErrors collection.
    o.AddBindingError(lcMessage,lvControl)
    AddValidationErrorsToBindingErrors Allows you to a wwBusiness ValidationErrors collection and automatically add these errors to the BindingErrors Collection.
    o.AddValidationErrorsToBindingErrors(loValidationErrors)
    Authenticate Allows you to check whether the current request is authenticated and if not prompt for authentication.
    o.Authenticate(lcValidUserName,lcErrorMessage)
    GetPostbackEventParameter Returns the PostBack event parameter that might have been set by a postback event from the client.
    o.GetPostbackEventParameter()
    GetPostbackEventReference Returns a client side Postback Event link (__doPostBack() call) that can be used to fire a server side event from client code.
    o.GetPostbackEventReference(lcControlId,lcEvent,lcParameter,llIsLink)
    OnError Page level Error manager method. Return .T. to indicate that you have handled the error. Return .F. to indicate the error should bubble up to the Process class.
    o.OnError(loException)
    RegisterClientScriptBlock Adds a client script block to the page. The block is added at the top of the page and typically used for adding functions.
    o.wwWebPage.RegisterClientScriptBlock(lcID,lcScriptCode)
    RegisterClientScriptInclude Inserts a link to an external script file into the page. Generates <script src="scriptpage.js"></script> into the page.
    o.wwWebPage.RegisterClientScriptInclude(lcID,lcScriptLink,lcLocation)
    RegisterCssInclude Inserts a stylesheet link into the page.
    o.RegisterCssInclude(lcId,lcHref)
    RegisterCursor Adds a cursor to the list of open cursors that should be automatically closed when the page Dispose() fires.
    o.RegisterCursor(lcCursorName)
    RegisterPostbackScriptCode This method is used to register the stock PostBackScript block in the page. This method should be called by any control that requires an Autopostback operation to be initiated.
    o.RegisterPostbackScriptCode()
    RegisterStartupScript Embeds a script into that page that is added at the bottom of the page and allows executing 'startup' code.
    o.wwWebPage.RegisterStartupScript(lcId,lcScript)
    Render Renders the Page by rendering the page and all of its contained components.
    o.Render()
    Run This is a standalone routine that causes the page to process the Event pipeline and write output into the Response object. This method handles setup and cleanup plus error handling for the page.
    o.Run()
    SetFocus Sets the focus to the control that is passed if possible.
    o.SetFocus(loControl)
    BindingErrors Contains errors after UnbindData calls of the form.
    ClientScript The ClientScript collection holds all client scripts, script includes (<script> links) and code loaded CSS links. This property generally should not be manually accessed, but rather is manipulated through RegisterClientScriptBlock, RegisterClientScriptInclude and RegisterCssInclude.
    EnableSessionState Determines wheter sessions are used or not on this page. The default is off.
    ErrorIconUrl The URL to an image file that is used to display the error icon.
    FormName The name of the name of the form.
    Header The Page.Header controls is a simple container control that references the <head> tag of a page.
    HiddenFormVars Name Value Collection that allows adding of HiddenForm variables to the page.
    IsWebPage Marker Interface Property that lets you check for the control being a WebPage object.
    StartupScript A collection of Script blocks that are rendered at the bottom of the Form. These scripts are automatically run when the page loads.
    StopEventProcessing Property that allows you to specify that no further page events should fire, but that rendering should still proceed.
    SurpressHttpHeader By default a standard Http Header is created, but you can set this property and write a standard Web Connection HTTP header on your own.

    Requirements


    The <ww:wwWebPage> Directive


    This directive is the most vital directive in the page scripting framework as is the root element and required for the parser and runtime to figure out which class and program to execute.

    Note that <ww:wwWebPage> is required in any parsed script page and must contain the runat, ID and GeneratedSourceFile attributes! All three of these attributes are required!

    These 3 attributes are special attributes. Otherwise you can use any attributes that map to the properties of the wwWebPage class.

    Where and how to use this tag

    The tag must sit at the top of the document and wrap all of your page content. Only content inside the tags will get generated. The only thing that should proceed the <ww:wwWebPage> directive is ASP.NET page directives required for VS.NET to properly render the document in the Web Form editor. These are not required for Web Connection, but for VS.NET/ASP.NET.

    <%@ Page Language="C#" %> <%@ Register Assembly="WebConnectionWebControls" Namespace="Westwind.WebConnection.WebControls" TagPrefix="ww" %> <ww:wwWebPage runat="server" ID="CustomerList_Page" GeneratedSourceFile="controldemo\CustomerList_page.prg" > <html> <head> <title>Customer List Demo</title> <link href="westwind.css" rel="stylesheet" type="text/css" /> </head> <body> <form id="form1" runat="server"> <div> <ww:wwWebTextBox runat="server" id="txtName">Text to Display</ww:wwWebTextBox> <br> <ww:wwWebButton runat="server" id="btnSubmit">Go</ww:wwWebButton> ... rest of the page here </div> </form> </body> </html> </ww:wwWebPage>

    Note that the <ww:wwWebPage> tag wraps all of the content of the document.

    Required Attributes

    The following attributes are required and must be present on the wwWebPage tag.

    ID
    This will become the name of your class that handles these requests. The first time you 'run' this page a class is created - 2 actually - that execute the page as FoxPro code. The ID specifies the class you write code for. The ID + _WCSX is a generated class that contains the control definitions parsed from this HTML page. We'll see what this looks like in a minute.

    GeneratedSourceFile
    As you might have guessed this is the source code location where the class is generated. This file contains two classes one with your custom code and the generated code for the page. The page is regenerated everytime you 'run' the page in development mode. Your custom code is kept separate and is not touched by the code updates.

    The path specified should be relative to where your FoxPro application is running! So I use ControlDemo\Customerlist.wcsx which goes into the ControlDemo directory beneath my Web Connection directory. You can also specify a ~ to specify a path relative to the physical location of the file (ie. the Web Directory). I don't recommend this but the option is there.

    runat="server"
    All controls require this directive, which the parser uses to find controls. If you don't put this directive on a control the parser won't find it and it's skipped over. Get used to it - you'll need it if you manually edit and create controls in text.

    wwWebPage::AddBindingError


    Adds a binding error to the page's BindingErrors collection.

    This method is called internally for any unbinding errors that occur, but you can also manually add errors to this collection. For example, say you're doing a post check for an 'empty' value on a list selection where the first item is *** Please select one. In this case you might do:

    *** Unbind all controls on the form this.UnbindData() IF this.lstStates.selectedValue = "***" THIS.AddBindingError("You have to select a state","lstStates") ENDIF IF this.BindingErrors.Count > 0 this.ErrorDisplay.Text = this.BindingErrors.ToString() RETURN ENDIF



    o.AddBindingError(lcMessage,lvControl)
    

    Parameters

    lcMessage
    The error message to display.

    lvControl
    An instance of a wwWebControl or a control's ID. Passing the control is more efficient - passing a string requires finding the control by parsing the control tree.

    This value is optional. If you specify the value the Error display can highlight the control when clicked and set focus there from the error summary display.

    wwWebPage::RegisterCursor


    Adds a cursor to the list of open cursors that should be automatically closed when the page Dispose() fires.

    When using cursors it's common to run a query early in the page cycle and then databind the cursor say to a DataGrid. Since databinding occurs during the Render() phase there's really no clean way to close the cursor in a linear fashion. The only option you have is to explicitly implement a Dispose() method and close the cursor in this method.

    RegisterCursor() automates this process by allowing you to procedurally add a cursor as soon as it's been loaded. Internally the page keeps track of the specified cursors and then closes them all in the .Dispose() method of the page ensuring that the cursors are closed.

    oCustomer = CREATEOBJECT("busCustomer") lnCount = oCustomer.GetCustomerList("TEntries") *** Register cursor to be closed THIS.Page.RegisterCursor("TEntries") *** Do whatever you need to do with the cursor this.lstCustomers.DataSource = "TEntries" this.lstCustomer.DataBind()


    o.RegisterCursor(lcCursorName)                                        
    

    Parameters

    lcCursorName
    The Alias of an open cursor that should be closed when the page is done.

    wwWebPage::AddValidationErrorsToBindingErrors


    Allows you to a wwBusiness ValidationErrors collection and automatically add these errors to the BindingErrors Collection.

    Makes it real easy to display error information from your business objects.

    IF !this.oCustomer.Validate() THIS.AddValidationErrorsToBindingErrors(this.oCustomer.oValidationErrors) ENDIF IF this.BindingErrors.Count > 0 this.ErrorDisplay.Text = this.BindingErrors.ToString() RETURN ENDIF


    o.AddValidationErrorsToBindingErrors(loValidationErrors)
    

    Parameters

    loValidationErrors

    wwWebPage::Authenticate


    Allows you to check whether the current request is authenticated and if not prompt for authentication.

    This mechanism is fully self contained - it will:

    This mechanism simply defers to wwProcess.Authenticate.. The authentication mechanism used (Basic or wwUserSecurity) depends on the wwProcess.cAuthenticationMode property configured on your process class. The default is Basic (ie. Windows Authentication).

    This simple method allows you to query the user's authentication in the OnLoad() of the page:

    IF !THIS.Authenticate("ANY") && Basic Auth RETURN && not validated - login dialog ENDIF *** Authenticated - move on

    Typically you'd call this code from the OnLoad of the form right at the beginning of page processing.



    o.Authenticate(lcValidUserName,lcErrorMessage)
    

    Return Value

    .T. or .F.

    Parameters

    lcValidUserName
    A username or WCINI or ANY

    lcErrorMessage
    Error message to display if validation fails.

    wwWebPage::OnError


    Page level Error manager method. Return .T. to indicate that you have handled the error. Return .F. to indicate the error should bubble up to the Process class.

    o.OnError(loException)
    

    Return Value

    T. to say you've handled the error. .F. to let the default handlers at the process class kick in.

    Parameters

    loException
    An exception object that contains error information

    wwWebPage.SetFocus


    Sets the focus to the control that is passed if possible.

    *** Page level OnLoad event FUNCTION OnLoad() this.SetFocus(this.txtName) ENDFUNC


    o.SetFocus(loControl)                                             
    

    Parameters

    loControl
    The control that is to be focused.


    Remarks

    This routine adds JavaScript to the page to focus a control.

    wwWebPage::RegisterPostbackScriptCode


    This method is used to register the stock PostBackScript block in the page. This method should be called by any control that requires an Autopostback operation to be initiated.

    o.RegisterPostbackScriptCode()
    

    wwWebPage.RegisterClientScriptInclude


    Inserts a link to an external script file into the page. Generates <script src="scriptpage.js"></script> into the page.

    Scripts can be injected into the page at various locations to allow more control over scripts and provide for better integration with manually added scripts. The three locations - headertop, header and script - correspond roughly to library, support/plug-in library, and application level locations.

    Injected script tags are of the following form:

    <script src="/virtual/script.js" type="text/javascript" />


    o.wwWebPage.RegisterClientScriptInclude(lcID,lcScriptLink,lcLocation)                  
    

    Parameters

    lcID
    An id for this script resource - generally this id should be a shared id so the script file only loads once.

    lcScriptLink
    A full URL to the script resource. You may use ~ syntax to access relative Urls.

    lcLocation
    Optional location where the script is injected. Possible values are:
    headertop - at the top of the <head> tag
    header - at the bottom of the <head> tag
    script or not passed - after the <form> tag

    Scripts are injected at these locations based on priority. The default behavior is script

    wwWebPage::RegisterCssInclude


    Inserts a stylesheet link into the page.

    Note the link is generated just below the <form> tag of the page not in the header so this script will load after anything loaded in the header. This functionality should be primarily used by controls that wish to add required scripts or links to embedded script resources.

    o.RegisterCssInclude(lcId,lcHref)                                
    

    Parameters

    lcId
    A unique Id for the script. Use a shared Id if you want this script to be shared by many controls.

    lcHref
    The URL to the stylesheet - can use ~/ syntax.

    wwWebPage.RegisterClientScriptBlock


    Adds a client script block to the page. The block is added at the top of the page and typically used for adding functions.

    o.wwWebPage.RegisterClientScriptBlock(lcID,lcScriptCode)                    
    

    Parameters

    lcID
    Id for this script. Use a shared ID if this script block needs to be accessed potentially by multiple controls. Use a Unique id (ie. UniqueID + "_myscript") if the script needs to vary for multiple controls.

    lcScriptCode
    The script code to embed into the page

    wwWebPage.RegisterStartupScript


    Embeds a script into that page that is added at the bottom of the page and allows executing 'startup' code.

    This method is useful for executing clientscript code that needs to run when the page first loads. For example - popping up notifications, re-navigating the page, setting up timers etc.

    Note: This code is simply located at the bottom of the page just inside the </form> tag. There's no guarantee that all items on the page like images have loaded but the DOM will be able to see all controls that have been loaded on the page.

    o.wwWebPage.RegisterStartupScript(lcId,lcScript)                            
    

    Parameters

    lcId
    The unique ID of the startup script

    lcScript
    The script code to embed

    wwWebPage::GetPostbackEventReference


    Returns a client side Postback Event link (__doPostBack() call) that can be used to fire a server side event from client code.

    A common way that this function would be called is from a Grid Column Expression. Here's an example:

    <ww:wwWebDataGridColumn runat="server" ID="WwWebDataGridColumn1" Expression="HREF( this.Page.GetPostbackEventReference('Page','DeleteCompany',Trans(pk),.t.) ,[Delete])" HeaderText="Action" >

    This code uses the HREF() expression to generate a hyperlink and the link itself is set by the GetbackEventReference method. The output from this function generates something like this:

    __doPostBack('Page','DeleteCompany','10');

    Actually the output in this case is used for a HyperLink so the final parameter llIsLink is passed as .T. which prefixes the javascript: text to the link so the actual output is:

    javascript:__doPostBack('Page','CustomerDeleted','10');

    The latter is required only if you embed the link into an HREF expression. If you call this code from elsewhere (say in a JavaScript function) the javascript prefix is not required.

    Remember if you manually use __doPostback() from script code, you have to ensure that this.Page.RegisterPostbackScriptCode was called somewhere in your code to ensure the postback script is included in the page source.

    Other ways to use this method

    You will frequently want to assign this method dynamically for navigation expressions. For example you might do the following in a Repeater:

    <ww:wwWebImageButton runat="server" ID="btnDelete" Click="btnDelete_Click" ImageUrl="~/images/remove.gif" UrlControlSource="this.Page.GetPostbackEventReference('Page','btnDelete_Click',TRANS(pk),.T.)" />

    You can then handle this event as follows inside of the page:

    FUNCTION btnDelete_Click() *** Retrieve the event parameter (same as: Request.Form("__EventParameter") ) lcId = this.GetPostBackEventParameter() *** Do something with the value this.oEntry.Delete( VAL(lcId) ) ENDFUNC


    o.GetPostbackEventReference(lcControlId,lcEvent,lcParameter,llIsLink)     
    

    Parameters

    lcControlId
    The UniqueID of the control that is to be accessed.

    As a special case you can acces the Page object with:


    this allows you to directly fire events on the Page object which essentially allows you to fire custom events.

    lcEvent
    The event on that control to fire. Note this is a Control's event not the event that you have mapped onto the Page object (ie. Click rather than btnSubmit_Click)

    lcParameter
    An optional parameter that is to be passed to the server event. The parameter's value can be retrieved with:

    Page.GetPostBackEventParameter()

    llIsLink
    If .T. generates the javascript: prefix that is required for hyperlink click operations. Generates:

    javascript:__doPostBack('Page','CustomerDeleted','10');


    Remarks

    Note: Response is returned with string parameters set to single quotes (ie. 'value') - this may affect how you embed this value.

    wwWebPage::GetPostbackEventParameter


    Returns the PostBack event parameter that might have been set by a postback event from the client.

    This operation is a shortcut for:

    lcString = Request.Form("__EventParameter")

    Postback parameters are created as part of postback script created with GetPostBackEventReference - one of its parameters is a state parameter that can be posted back, typically some sort of ID. GetPostBackEventParameter retrieves this parameter on a postback.

    The following event handler on a page illustrates:

    FUNCTION DeleteCustomer() lcId = this.GetPostBackEventParameter() IF !THIS.oCustomer.Delete(VAL(lcId)) this.ErrorDisplay.ShowError("Couldn't delete customer") return ENDIF *** Just redisplay the list ENDFUNC

    This code is called from client events generated say in a grid or repeater which looks like this:

    <a href="javascript:__doPostBack('Page','DeleteCustomer','270878') ">Delete</a>

    which is generated from this markup code:

    <ww:wwWebDataGridColumn ID="colAction" runat="server" Expression="Href(this.Page.GetPostBackEventReference('Page','DeleteCustomer',Trans(pk),.t.),'Delete')" style="text-align: center;" HeaderText="Action" />

    Note the embedded GetPostBackEventReference and the Trans(Pk) which is the parameter embedded in server side generation code.

    o.GetPostbackEventParameter()                                    
    

    wwWebPage::Render


    Renders the Page by rendering the page and all of its contained components.

    o.Render()
    

    wwWebPage::Run


    This is a standalone routine that causes the page to process the Event pipeline and write output into the Response object. This method handles setup and cleanup plus error handling for the page.

    This method is very highlevel and acts merely as a controller wrapper around the wwWebControl::Process method. The Run method basically does following:


    This method is typically called for you as part of the Page class PRG file which contains a stub that does the following:

    #INCLUDE WCONNECT.H *** Small Stub Code to execute the generated page PRIVATE __WEBPAGE __WEBPAGE = CREATEOBJECT("PunchIn_Page_WCSX") __WEBPAGE.Run() RELEASE __WEBPAGE RETURN

    so you can just DO <YourPage>.prg to 'execute the actual page.

    o.Run()
    

    wwWebPage::BindingErrors


    Contains errors after UnbindData calls of the form.

    The BindingErrors collection

    Default Value

    Initial value: null

    wwWebPage::ClientScript


    The ClientScript collection holds all client scripts, script includes (<script> links) and code loaded CSS links. This property generally should not be manually accessed, but rather is manipulated through RegisterClientScriptBlock, RegisterClientScriptInclude and RegisterCssInclude.

    ClientScript blocks are injected after <form> tag and so render towards the top of the page. Script includes render depending on their location specified either in the top or bottom of the header or below the <form> tag.

    You can loop through the collection of scripts, but note that some items may have location prefixes:

    These are used internally to figure out how to render each item.

    Note that StartupScripts are stored in their own StartupScript collection.

    Default Value

    Initial value: null

    wwWebPage::EnableSessionState


    Determines wheter sessions are used or not on this page. The default is off.

    Note that Session state is much less crucial in WebControl applications since you have page state in the form of ViewState.

    Default Value

    Initial value: .f.

    wwWebPage::ErrorIconUrl


    The URL to an image file that is used to display the error icon.

    Default Value

    Initial value: ~images/warning.gif

    wwWebPage::FormName


    The name of the name of the form.

    wwWebPage::Header


    The Page.Header controls is a simple container control that references the <head> tag of a page.

    The main purpose of this control is to allow adding of additional header content programmatically, especially for control level code.

    The wwWebPageHeader control which renders the header also adds two overloaded methods that you may find useful:

    The header is used internally extensively to add scripts to the page.

    Default Value

    Initial value: null

    wwWebPage::HiddenFormVars


    Name Value Collection that allows adding of HiddenForm variables to the page.

    this.Page.HiddenFormVars.Add("__TABSELECTION" + this.UniqueId)



    o.HiddenFormVars                                                 
    

    wwWebPage::IsWebPage


    Marker Interface Property that lets you check for the control being a WebPage object.

    Default Value

    Initial value: .T.

    wwWebPage::StopEventProcessing


    Property that allows you to specify that no further page events should fire, but that rendering should still proceed.

    Similar in behavior to Response.End() except that this property will still render the page, where Response.End() doesn't generate any further output.

    This method is useful to short circuit page processing for things like error display. For example, you might have a page where you check for a specific query string value. if the value is missing you might want to display an error so you update ErrorDisplay.ShowError() to display the error on the error control. However, you might not want to continue processing OnLoad() and OnPreRender() events which have code to load data and other code that is not applicable or would fail without the query string value. Setting the StopEventProcessing=.T. allows you to simply return and effectively jump straight to the page's Render() method which will now render the error control and otherwise empty data (or whatever other settings you apply to the page before returning).

    o.StopEventProcessing                                              
    

    wwWebPage::StartupScript


    A collection of Script blocks that are rendered at the bottom of the Form. These scripts are automatically run when the page loads.

    Scripts are added to the Page in the order they are added to the object.

    lcScript = [window.setTimeout("window.location='newpage.wcsx';"),5000)] this.Page.StartupScript.Add("PageReload",lcScript)

    wwWebPage::SurpressHttpHeader


    By default a standard Http Header is created, but you can set this property and write a standard Web Connection HTTP header on your own.

    Default Value

    Initial value: .f.

    Class wwWebUserControl


    The wwWebUserControl class is the base class that is used for visual User Controls that can be visually designed and then be dragged and dropped onto page canvas for visual resusability. User Controls are essentially mini forms and behave very similar to forms and provide a container for other visual controls.

    This class by itself doesn't do anything - it's a shell implementation that inherits and derives all of its functionality from the wwWebControl class. The class acts as a marker interface to the Page Parser to identify externally loaded User Controls and provides the container needed to parse the User Control contents.

    Functionally user controls act just like wwWebPage objects with the difference that they don't contain a form. User Controls provide visual subclassing by allowing you to create User Controls that contain other controls, providing a visual mechanism for building Composite classes.

    This is a very powerful reusability mechanism that lets you design page components, like page headers, footers, menus and side bars that are reused on other pages. User Controls are full class controls so you can expose properties on the control class and set these properties via control markup or code.

    Custom
      
    wwWebControl
        wwWebUserControl

    o.wwWebPage.
    

    Class Members

    Requirements


    Using a User Control


    Once you have created a user control (preferrably with an .ASCX extension so Visual Studio can render it properly) you can use it on any page. Using of UserControls requires a couple of not quite intuitive steps because you need to provide some additional information about the control that is not automatically set when the control is dropped on the page.

    The easiest way to get the control onto the page is to drag the created user control onto the design surface in Visual Studio. When you do this you will see the following at the top of the page:

    <%@ Register Src="controls/PageHeader.ascx" TagName="PageHeader" TagPrefix="uc1" %>

    and this in the location where you dropped the control (invalid code):

    <uc1:PageHeader ID="PageHeader1" runat="server" />

    If you were to try and run the page as it is now you will get an error stating that the PageHeader control cannot be found:

    Parsing of the page default.tt failed.
    Couldn't create control: PageHeader1 [pageheader_WCSX].
    Error: Class definition PAGEHEADER_WCSX is not found.

    The reason for this is that Web Connection has no idea where the control lives at this point, so in order for the control to work you need to provide a couple of additional property settings on the control:

    <uc1:PageHeader ID="PageHeader" runat="server" ScriptFile="controls/PageHeader.ascx" SourceFile="TimeTrakker\PageHeader_Control.prg" />

    If you run again now Web Connection can find the control's markup file (ScriptFile - a relative path from the base site) and the PRG file (SourceFile - a relative path from the current FoxPro IDE path).

    At this point the control exists on the page and you can access is as this.Page.PageHeader and through it access its properties and values.

    User Controls don't unload!

    Note that User Controls get loaded like other controls using SET PROCEDURE TO and then loaded as a class. The class is never released (unlike pages which are) and so User Controls, like built in controls stay permanently loaded in the running application.

    This means:

    If you change the markup of a user control you have to stop the application, clear memory and then restart it. IOW, while Page markup can generally be changed and show immediately, User Control markup does not until your unload and restart.



    Creating a User Control


    The WebControl Framework supports visually designed User Controls. User Controls are similar to full pages in behavior but are essentially a page fragment that can be embedded into another page or another control making it possible to build highly reusable components visually and embed them into many pages. The idea is that a UserControl provides the ability to visually design a page fragment and embed it into another page or control.

    There are two steps to using User Controls

    The process is straight forward, but User Controls have a few special rules that must be observed when they are embedded into pages. Specifically you need to specify the classname, source file and script location explicitly when embedding the control into a parent page. User Controls also act like 'real' user controls which means they load as a library and so cannot be unloaded. This means changes to the user control require stopping and restarting of the application.

    Creating a User Control

    Creating a user control is very similar to creating a Page. You bascially drop controls onto a page and hook up event logic to various operations and events. To create a control you can use the Visual Studio New Web Connection Control template from the Add New Item shortcut menu in the VS.NET project.

    Once the control template has been created set the ControlClass and GeneratedSourceFile properties to give it classname and the locatino of the PRG file where the class is created. The following creates a small custom Login form and demonstrates a simple layout for a user control:

    <%@ Control Language="C#" ClassName="LoginControl" %> <%@ Register Assembly="WebConnectionWebControls" Namespace="Westwind.WebConnection.WebControls" TagPrefix="ww" %> <ww:wwWebUserControl ID="TestControl" runat="server" ControlClass="LoginControl" GeneratedSourceFile="WebControls\LoginControl_control.prg" /> <div style="background:silver;width:300;border-width:2px"> <ww:wwWebLabel ID="lblLoginName" runat="server">Login Nam"e:</ww:wwWebLabel><br /> <ww:wwWebTextBox ID="txtLoginName" runat="server" Height="22px" Width="196px"></ww:wwWebTextBox><br /> <ww:wwWebLabel ID="WwWebLabel1" runat="server">Password:</ww:wwWebLabel><br /> <ww:wwWebTextBox ID="txtLoginPassword" runat="server" Height="22px" Width="196px"></ww:wwWebTextBox> <ww:wwWebButton ID="btnSecurityLogin" runat="server" Text="Login" Width="80" Click="btnSecurityLogin_Click" /> <hr /> <ww:wwWebLabel runat='server' ID="lblMessage" ></ww:wwWebLabel> <br /> </div>

    The <%@ %> tags are required for VS.NET rendering only and are ignored by the parser. Just make sure that the WebConnectionControls are referenced so you can use any Web Connection controls with your user control. As with pages you need to tell Web Connection what the name of the generated class will be (ControlClass) and where the class is to be generated as a PRG file (GenerateSourceFile). The path specified is relative to the current directory.

    Controls can be visually designed just like pages, so you can drag and drop controls from the Toolbox onto the page and set properties with the Property Sheet or you can just use the HTML markup editor and Intellisense.

    Generating the Control Class as a PRG File
    Once you've laid out your control you will need to generate it. There are two ways to do this - just like with pages:

    The former involves running WebPageParser.prg:

    DO WebPageParser with "c:\westwind\wconnect\webControls\LoginControl.ascx",2

    You point the parser at the physical path of the ASCX control file and pass 2 as the second parameter which tells the parser to parse control as opposed to a page. This command causes the control to be parsed and the ControlClass specified to be generated in the path relative path specified GeneratedSourceFile. Remember that GeneratedSourceFile is a RELATIVE path to the current directory! You'll want to ensure you always generate from the same location.

    The other alternative is easier - you automatically parse the control when the page executes. But before we can do this, we need to drop a control on a Page first.

    Referencing the User Control and the Page

    User Controls act very much like small self contained pages in that you can use the same controls inside the user control and handle the same events like OnInit, OnLoad, OnPreRender and even events from controls defined in the control.

    If you need to reference the control or the control's properties from within one of the controls you need to use THIS.ParentControl (ie. the control your working in, one level back to the user control) to reference it. For example, assume you have SubTitle property on the user control and you want to display this value as an expression you have to use code like this:

    <%= this.ParentControl.SubTitle %>

    The same applies if you were to use a control source that binds to a user control property:

    <ww:wwWebTextBox runat='"Server" id="txtTitle" ControlSource="this.ParentControl.SubTitle" />

    User Controls and Custom Properties

    User controls are classes and so you can add custom properties in your code behind for the control. So imagine for a minute that you have a couple of properties attached to your CodeBehind control:

    #INCLUDE WCONNECT.H SET PROCEDURE TO logincontrol_control.prg ADDITIVE RETURN ************************************************************** DEFINE CLASS LoginControl as WWC_WEBUSERCONTROL *************************************** *** Your Implementation Control Class - put your code here *** This class acts as base class to the generated page below ************************************************************** Username = "" Password = "" FUNCTION OnLoad() DODEFAULT() ENDFUNC FUNCTION OnPreRender() IF !THIS.IsPostBack() this.txtUserName.Text = this.Username this.txtPassword.Text = this.Password ENDIF ENDFUNC FUNCTION btnSecurityLogin_Click() this.lblMessage.Text = "Logged in" ENDFUNC ENDDEFINE *# --- BEGIN GENERATED CODE BOUNDARY --- #* ******************************************************* *** Generated by WebPageParser.prg *** on: 07/28/2007 09:19:43 PM *** *** Do not modify manually - class will be overwritten ******************************************************* DEFINE CLASS LoginControl_WCSX AS LoginControl Id = [TestControl] *** ... generated class code omitted

    These properties can then automatically be assigned in the controls' markup:

    <uc1:LoginControl ID="LoginForm" runat="server" ControlClass="LoginControl" SourceFile="webcontrols\LoginControl_Control.prg" ScriptFile="./LoginControl.ascx" Username="Rick" Password="SuperSecret" />

    If you want Intellisense to work for these custom controls you can add some .NET code into the page to define those properties. Using C# and Server markup you can add the following before the wwWebUserControl definition in the page:

    script runat="server"> public string Username { get { return _Username; } set { _Username = value; } } string _Username = ""; public string Password { get { return _Password; } set { _Password = value; } } string _Password = ""; </script>

    This is purely optional and only required for Intellisense to work on the control when dropped onto a page but it's a nice touch if you use the control in multiple places.


    < %= % > and < % if Expression % > Syntax


    The Web Control framework supports embedding FoxPro expressions into pages. This mechanism actually substitutes special controls into the page that handle expression evaluation.

    Web Connection only supports two kinds of special embedded script tag formats:

    IMPORTANT NOTE: Expression String Delimiters
    HTML requires that expressions get embedded into the page with quotes and Web Connection will use double quotes for any embedded expressions it generates. This means your expression syntax should not use double quotes (") as string delimiter and instead use either square brackets([ ]) or single quotes (' ').

    For example don't use: <% if(Tquery.Status = "Completed" %> but use <% if TQuery.Status = [Completed] %>.

    <%= Expression %> Evaluation

    The <%= %> syntax is the most efficient way to embed a dynamic value into the script page. This syntax actually maps to a wwWebEvalControl which handles displaying the expression and if there's an error displaying the error.

    Typical examples:

    <%= this.Page.oBusiness.oData.Title %> <%= this.Page.SomeStringReturningMethod() %> <%= TCursor.SomeField %>

    These expressions can be embedded into page in place of any literal content. You can also use them inside of template controls like the wwWebRepeater Item Template for example.

    Note that you cannot use <%= %> expressions inside of quoted attributes of controls, so the following does not work:

    <ww:wwWebTextBox runat="server" id="txtName" Text="<%= TCursor.Name %>" />

    Assignment to controls at runtime can be made either directly via code (this.txtName.Text = TCursor.Name from OnLoad or other event code) or by databinding via ControlSource:

    <ww:wwWebTextBox runat="server" id="txtName" ControlSource="TCursor.Name" %> />

    and you can then call either the Page's DataBind() method or the control's DataBind() (this.txtName.DataBind() ) method via code.

    <% if Expression %> <% endif %>

    Full FoxPro script processing like Script Templates is not supported in the Web Control Framework, so the <% %> script syntax is not supported. This is a design decision to discourage the use of embedding excessive code into pages, which should no longer be necessary with the new powerful control and code behind architecture.

    However, there's one common usage pattern that is supported: Conditional display of content by using a conditional <% if %> block. The need to conditionally display a block of content is so frequent that an exception is built into the Web Connection framework to simplify displaying conditional content more easily using the <% if Expression %> <% endif %> syntax. Here's a short example:

    Some text in my script page here

    <% if this.Page.oBusiness.oData.LastAccess > Date() - 5 %>
    Common content: <ww:wwWebLabel runat="server" ControlSource="this.Page.oBusiness.oData.Content" />
    <hr>
    <% endif %>

    Some more text and controls here

    The Web Control framework actually turns the conditional <% if %> expression into a wwWebPanel control with an invisible border. If the expression is false, the control is not rendered. The above actually is turned into:

    <ww:wwWebPanel runat="server" VisibleExpression="this.Page.oBusiness.oData.LastAccess">
    Common content: <ww:wwWebLabel runat="server" ControlSource="this.Page.oBusiness.oData.Content" />
    <hr>
    </ww:wwWebPanel>

    Note that you can also use the wwWebPanel directly if you choose.

    Also note that <% else %> is not supported. If you need an else condition you will need to explicitly define another < % if %> block with the negated expression in it. For example:

    <% if this.Page.oBusiness.oData.LastAccess > Date() - 5 %>
    Common content: <ww:wwWebLabel runat="server" ControlSource="this.Page.oBusiness.oData.Content" />
    <hr>
    <% endif %>
    <% !if this.Page.oBusiness.oData.LastAccess > Date() - 5 %>
    Display something else...
    <hr>
    <% endif %>



    Base Controls


    These are the core Web Control Framework controls that you will be using most of the time for data entry in combination with the
    List Controls.

    ClassDescription
      wwWebTextBox The TextBox control provides basic text input functionality for single line, multiline and password type text input.
      wwWebPanel The wwWebPanel control is a container content control that can contain other literal content and other controls. You can also nest panels.
      wwWebLiteralControl Similar to the Label control but has no markup properties. This control is the most efficient control to inject content into a page as it takes the text and inserts it into the control as is.
      wwWebLabel A label control that has text that is to be displayed.
      wwWebImage A programmable image control that embeds an HTML <img> tag into the page.
      wwWebHiddenField The hidden field control is just a specialized text control that creates an invisible field in the HTML document.
      wwWebErrorDisplay The wwWebErrorDisplay control can be used to display error messages to the user. The control is closely tied to the page's BindingErrors collection and allows for easy display of these errors.
      wwWebCheckBox The wwWebCheckbox class provides functionality for a standard Web checkbox control.


    Class wwWebTextBox


    The TextBox control provides basic text input functionality for single line, multiline and password type text input.

    Custom
      
    wwWebControl
        wwWebTextBox

    Class Members

    MemberDescription
    Change Change event that is fired if the value of the control is changed. Only fired when AutoPostBack=true.
    ControlSource The ControlSource binds a value expression to the Text Property by default.
    ControlSourceFormat FoxPro Format String used when a control source is bound to data. Also support a format method.
    IsRequired Determines whether the text box can be left empty. If empty an error is created in the BindindErrors collection
    LabelText Text for a label before the textbox.
    Text The Text property is the key value property for the TextBox. It's the default ControlSource Property as well.
    TextMode Input mode for the TextBox control

    Requirements


    wwWebTextBox::Change


    Change event that is fired if the value of the control is changed. Only fired when AutoPostBack=true.

    <ww:wwWebTextBox runat="server" id="txtName" AutoPostBack="true" Change="OnTxtNameChange" />

    To handle the event just implement the method on the page object:

    DEFINE CLASS Absolute_Page as WWC_WEBPAGE FUNCTION OnLoad() ENDFUNC FUNCTION OnTxtNameChange() this.lblNameEcho.Text = this.txtName.Text + ". " + TIME() ENDFUNC ENDDEFINE


    o.wwWebTextBox.Change                                                       
    

    wwWebTextBox::ControlSource


    The ControlSource binds a value expression to the Text Property by default.

    Expressions can be any valid Visual FoxPro expression that is in Scope including variables, class properties or even functions and class methods if you're only doing inbound binding.

    o.ControlSource                                               
    

    wwWebTextBox::ControlSourceFormat


    FoxPro Format String used when a control source is bound to data. Also support a format method.

    You can use standard FoxFormat expressions like "@!" for uppercase, or "@YL" for a long date value. You can also use InputMasks like "999,999.99" for numbers. Stock Fox behavior.

    In addition you can also use Format methods which must take a single value as input and return a string result as output. The function or method must be prefixed with an = sign.

    For exampe to format a data you might use: "=TimeToC" which is wwUtils UDF function. You can also use methods in the current page: "=this.Page.FormatDate()".

    Note that this feature has tremendous potential for allowing you to dynamically assign content to controls. The same behavior is also used for DataGrids and essentially allows you to format each grid cell dynamically.

    Unbinding Note:
    If you're using format strings for input fields, you may run into problems saving the data back on the server. If you unbind and use a format that cannot be converted back into the original format (say a string that's formatted: "(-999.99)" ) the unbinding will fail. So be mindful of the two-way conversion that needs to take place


    o.ControlSourceFormat                                         
    

    wwWebTextBox::IsRequired


    Determines whether the text box can be left empty. If empty an error is created in the BindindErrors collection

    Default Value

    Initial value: .F.

    wwWebTextBox::LabelText


    Text for a label before the textbox.

    Note the text is placed as plain text - no table formatting.

    wwWebTextBox::Text


    The Text property is the key value property for the TextBox. It's the default ControlSource Property as well.

    o.Text                                                        
    

    wwWebTextBox::TextMode


    Input mode for the TextBox control

    The following modes are supported:


    Default Value

    Initial value: singleline

    Class wwWebPanel


    The wwWebPanel control is a container content control that can contain other literal content and other controls. You can also nest panels.


    This control renders as a DIV tag and by default the DIV tag renders as an invisible container that shows no borders or background. The content however is visible based on the Visible or VisibleExpression properties. If you want the container to be visible you can set the BackColor and various Border properties.

    Panels are great tools for creating content that can be hidden easily and dynamically both from client and server code. You can set the Visible property to .F. all child content is also not rendered. You can also dynamically force the panel to display or hide using the VisibleExpression which can be set in the designer.

    Example:

    <ww:wwWebPanel ID="Panel1" runat="server" Height="194px" Width="321px" CssClass="gridalternate" BorderColor="black" BorderWidth="2px" BackColor="lightsteelblue" style="padding-left:10px;margin-left:1px"> <br /> Enter your name:<br /> <ww:wwWebTextBox ID="txtName" runat="server" Width="263px">Rick</ww:wwWebTextBox><br /> <br /> Enter your company:<br /> <ww:wwWebTextBox ID="txtCompany" runat="server" Width="263px">West Wind</ww:wwWebTextBox><br /> <br /> Enter your Address:<br /> <ww:wwWebTextBox ID="txtAddress" runat="server" Height="94px" TextMode="MultiLine" Width="263px">32 Kaiea Place</ww:wwWebTextBox> <br /> <ww:wwWebButton ID="btnShowData" runat="server" Click="btnShowData_Click" Width="135px" Text="Show Data" /><br /> </ww:wwWebPanel>


    Class Members

    MemberDescription
    BackImageUrl A Url to an image that is used as a background image
    BorderWidth Determines the width of the border.
    BorderWidth Determines the width of the border for the panel area.
    HorizontalAlign Left Center Right Justified
    RenderAsDivTag Determines whether the control renders as a DIV tag. If .F. the Panel doesn't render an HTML container and only renders the inner items.
    Scrollbars Determines if and how the panel displays scrollbars.
    Visible Determines whether the Panel is displayed or hidden.
    VisibleExpression An expression that is evaluated at Render time to determine whether the control is rendered.
    Wrap Determines whether white space wraps.

    Requirements


    wwWebPanel::BackImageUrl


    A Url to an image that is used as a background image

    wwWebPanel::BorderWidth


    Determines the width of the border.

    wwWebPanel::BorderWidth


    Determines the width of the border for the panel area.

    o.BorderWidth                                                   
    

    wwWebPanel::HorizontalAlign


    Left Center Right Justified

    wwWebPanel::RenderAsDivTag


    Determines whether the control renders as a DIV tag. If .F. the Panel doesn't render an HTML container and only renders the inner items.

    o.RenderAsDivTag                                                
    

    wwWebPanel::Scrollbars


    Determines if and how the panel displays scrollbars.

    Options include:


    wwWebPanel::Visible


    Determines whether the Panel is displayed or hidden.

    If Visible is set to .F. it overrides any expression check from the VisibleExpression property.

    o.Visible                                                       
    

    wwWebPanel::VisibleExpression


    An expression that is evaluated at Render time to determine whether the control is rendered.

    If the Visible property is set to .F. this expression has no effect. If this Expression is empty it is ignored. Otherwise if the expression evaluates to .F. the control is not rendered. If it is .T. the control is rendered.

    wwWebPanel::Wrap


    Determines whether white space wraps.

    Default Value

    Initial value: .T.

    Class wwWebLiteralControl


    Similar to the Label control but has no markup properties. This control is the most efficient control to inject content into a page as it takes the text and inserts it into the control as is.

    Text only Rendering
    Unlike other controls this control will ONLY render its text property content. It will not render styles, id's or anything else - just the raw text. This control is also optimized and does not fire any of the standard events. Visibility also has no effect on it. It just renders static text.

    This is the most basic control that the framework provides. The framework itself uses this control to render all static page text (ie. text that is not inside of controls) so every page in fact uses this control extensively.

    Use the literal control for all dynamic content that needs to embed raw HTML/Text into the page without any special formatting. Use the Label control if you need more programmatic control over the content once it's rendered such as changing the style, formatting or other display attributes.

    Custom
      
    wwWebControl
        wwWebLiteralControl

    Class Members

    MemberDescription
    Text The literal text that is placed into the page output.

    Requirements


    Text


    The literal text that is placed into the page output.

    o.Text()
    

    Class wwWebLabel


    A label control that has text that is to be displayed.

    Not much custom functionality here - most of the functionality derives from the base wwWebControl.

    Custom
      
    wwWebControl
        wwWebLabel

    Class Members

    MemberDescription
    ControlSource The ControlSource binds a value expression to the Text Property by default.
    ControlSourceFormat FoxPro Format String used when a control source is bound to data. Also support a format method.
    Text The text of the label that is displayed.

    Requirements


    wwWebLabel::ControlSource


    The ControlSource binds a value expression to the Text Property by default.

    Binding is one way only on the label control - Unbinding is not supported.

    Expressions can be any valid Visual FoxPro expression that is in Scope including variables, class properties or even functions and class methods if you're only doing inbound binding.

    o.ControlSource                                               
    

    wwWebLabel::ControlSourceFormat


    FoxPro Format String used when a control source is bound to data. Also support a format method.

    You can use standard FoxFormat expressions like "@!" for uppercase, or "@YL" for a long date value. You can also use InputMasks like "999,999.99" for numbers. Stock Fox behavior.

    In addition you can also use Format methods which must take a single value as input and return a string result as output. The function or method must be prefixed with an = sign.

    For exampe to format a data you might use: "=TimeToC" which is wwUtils UDF function. You can also use methods in the current page: "=this.Page.FormatDate()".

    Note that this feature has tremendous potential for allowing you to dynamically assign content to controls. The same behavior is also used for DataGrids and essentially allows you to format each grid cell dynamically.

    Unbinding Note:
    If you're using format strings for input fields, you may run into problems saving the data back on the server. If you unbind and use a format that cannot be converted back into the original format (say a string that's formatted: "(-999.99)" ) the unbinding will fail. So be mindful of the two-way conversion that needs to take place


    o.ControlSourceFormat                                         
    

    wwWebLabel::Text


    The text of the label that is displayed.

    o.Text                                                          
    

    BindingErrors


    Web Connection supports the concept of binding errors when using the Databind() and Unbind() methods of the Page class. These methods cause a set of BindingErrors to be set on the page's BindingErrors collection.

    Two classes control binding errors:

    You'll be interacting mostly with the BindingError collection as a whole most likely as in code as the following:

    FUNCTION btnSubmitForm_Click() this.UnbindData() IF this.BindingErrors.Count > 0 *** Display on the Error Display control this.ErrorDisplay.ShowError(this.BindingErrors.ToHtml(),"Please correct the following") RETURN ENDIF *** Validate Business Rules IF !THIS.oDeveloper.Validate() this.AddValidationErrorsToBindingErrors(THIS.oDeveloper.oValidationErrors) this.ErrorDisplay.ShowError(this.BindingErrors.ToHtml(),"Please correct the following") RETURN ENDIF *** Finally save IF !THIS.oDeveloper.Save() this.ErrorDisplay.ShowError("Save Error: " + THIS.oDeveloper.cErrorMsg ELSE this.ErrorDisplay.ShowMessage( "Entry Saved" ) ENDIF ENDFUNC

    OnBindingErrorLink Client Function


    When using DataBinding you often use the BindingErrorsCollection.ToHtml method to bind errors to controls and to provide a link list to the error controls in the ErrorDisplay control.

    When using the BindingErrorsCollection.ToHtml() method to generate binding errors and the list of links that can be clicked to activate any of the controls there canbe a problem with controls that are current not visible on the page. A typical scenario is when Tab pages are used and the control(s) with an error are on a page that is currently hidden.

    The client code that gets generated as part of .ToHtml() method includes a check for a special OnBindingErrorLink(element) function, that - if it exists in the page - is executed on any error link click. You can use this function to intercept clicks and based on the control passed in optionally activate the Tab page or otherwise make the control visible.

    You can return true from the function to indicate that you've handled the click completely and no further processing should occur. Otherwise the default behavior is still executed which will activate the link.

    You can check individual controls:

    function OnBindingErrorLink(ctl) { if (ctl.id == "txtTest" || ctl.id="txtName" || ctl.id=="txtAddress") ActivateTab("divPostBack"); }

    Or you can use more generic code to capture entire groups of controls. The following example checks to see if a particular if the control lives on a TabPage (by checking for a specific ID name like TabPage_1) and then activates that tab before letting the default behavior activate the control:

    function OnBindingErrorLink(errorCtl) { var control = errorCtl; // *** Find the parent 'tab' page id while(control.parentNode) { control = control.parentNode; if (!control) break; if (control.id.substr(0,7) == "TabPage") { ActivateTab("Tabs",control.id); return; } } }

    This is obviously more work than automatic activation but it gives you the needed control to activate and make visible the controls in question optionally.

    If the handler is missing no action is taken. This is a purely optional feature.

    Class BindingError


    Class that holds information about a particular binding error that occurred. Only the message and a display name of the field that is in error is held - no reference to the actual control.

    Generally you won't use this class directly only as part of the BindingErrorsCollection.

    Class Members

    MemberDescription
    Message The display message for the error. This message is generated as part of the ControlUnbind event and either shows the message you've set on the control or a generic message that fieldname can't be blank or invalid value for fieldname.
    ObjectName Optional object name that is used instead of the generic field name. By default fieldnames are generated by stripping off txt or chk or lst or rad. You can override the field name explicitly here by providing an Objectname.

    Requirements


    BindingError::Message


    The display message for the error. This message is generated as part of the ControlUnbind event and either shows the message you've set on the control or a generic message that fieldname can't be blank or invalid value for fieldname.

    o.Message                                                     
    

    BindingError::ObjectName


    Optional object name that is used instead of the generic field name. By default fieldnames are generated by stripping off txt or chk or lst or rad. You can override the field name explicitly here by providing an Objectname.

    o.ObjectName                                                  
    

    Class BindingErrorCollection


    Holds a collection of binding errors that are created when
    Unbind Data fails.

    Class Members

    MemberDescription
    ToHtml Returns an HTML result string for the active binding errors in the collection. This method generates the basic message as well as javascript links to activate and highlight each of the controls when clicked from the selection list that is generated.
    o.ToHtml()
    ToString Returns a CR/LF delimited string of binding error messages
    o.ToString()

    Requirements

    Assembly: webcontrol.prg

    BindingError::ToHtml


    Returns an HTML result string for the active binding errors in the collection. This method generates the basic message as well as javascript links to activate and highlight each of the controls when clicked from the selection list that is generated.

    Typically the result from a BindingErrorCollection is stored on Page.BindingErrors and is used to bind to an ErrorDisplay control. The following code might be used in a click button to save data from a form:

    this.UnbindData() IF this.BindingErrors.Count > 0 *** Display on the Error Display control this.ErrorDisplay.ShowError(this.BindingErrors.ToHtml(),"Please correct the following") RETURN ENDIF *** Validate Business Rules IF !THIS.oDeveloper.Validate() this.AddValidationErrorsToBindingErrors(THIS.oDeveloper.oValidationErrors) this.ErrorDisplay.ShowError(this.BindingErrors.ToHtml(),"Please correct the following") RETURN ENDIF *** Finally save IF !THIS.oDeveloper.Save() this.ErrorDisplay.ShowError("Save Error: " + THIS.oDeveloper.cErrorMsg ELSE this.ErrorDisplay.ShowMessage( "Entry Saved" ) ENDIF

    wwWebTabControl Note:
    If you're using the wwWebTab control and you have controls with errors that are hidden, the default control activation code will not cause the pages to be activated and so clicking the error link will not show the control. You can implement a global JavaScript
    OnBindingErrorLink(element) function to intercept error link clicks and optionally activate the page explicitly.




    o.ToHtml()
    

    BindingError::ToString


    Returns a CR/LF delimited string of binding error messages



    o.ToString()
    

    Class wwWebImage


    A programmable image control that embeds an HTML <img> tag into the page.

    Class Members

    MemberDescription
    AlternateText The alternate text that is displayed when hovering the image or spoken in accessibility.
    BorderWidth The width of the link border around the image. Defaults to 0.
    ImageAlign Determines the image alignment in the context of the content element that hosts it.
    ImageUrl The Url of the image that is displayed.

    Requirements


    wwWebImage::ImageUrl


    The Url of the image that is displayed.

    o.ImageUrl                                                      
    

    wwWebImage::BorderWidth


    The width of the link border around the image. Defaults to 0.

    Note you should set any other style attributes using the style property.

    o.BorderWidth                                                   
    

    wwWebImage::ImageAlign


    Determines the image alignment in the context of the content element that hosts it.

    Default is blank which is inline, but you can use left and right to force the image to flow with the content in the left and right margins respectively.

    o.ImageAlign                                                    
    

    wwWebImage::AlternateText


    The alternate text that is displayed when hovering the image or spoken in accessibility.

    o.AlternateText                                                 
    

    Class wwWebHiddenField


    The hidden field control is just a specialized text control that creates an invisible field in the HTML document.

    <ww:wwWebHiddenField runat="server" id="txtHidden" Text="hidden text" />

    All field output is driven through the Text property. Please note that you should not set a Value property in the declarative code.

    Class Members

    MemberDescription
    Text The text property contains that value of the hidden field.

    Requirements

    Assembly: webcontrols.prg

    wwWebHiddenField.Text


    The text property contains that value of the hidden field.

    o.wwWebHiddenField.Text                                                     
    

    Class wwWebErrorDisplay


    The wwWebErrorDisplay control can be used to display error messages to the user. The control is closely tied to the page's BindingErrors collection and allows for easy display of these errors.

    Custom
      
    wwWebControl
        wwWebErrorDisplay

    Class Members

    MemberDescription
    ShowError Displays an error message based on an Html string passed as input. The message is displayed in error format with the ErrorIcon if provided.
    o.ShowError(lcMessage,llIsText)
    ShowMessage This method is used to display informational messages on a page. The messages are displayed with the informational icon displayed if provided.
    o.ShowMessage(lcMessage)
    CellPadding The cell padding for the table that is rendered by this control.
    Center Determines whether the control is centered in its HTML container.
    CssClass The CssClass used for the table display.
    ErrorImage Image used as error icon which is the primary display icon. The icon is optional.
    InfoImage Image used as icon when calling ShowMessage.
    RenderMode Determines the type of text you assign which can be either Text or HTML
    Text The error message text to display.
    Width The width of the table that is displayed containing the error message.

    Requirements


    wwWebErrorDisplay::ShowError


    Displays an error message based on an Html string passed as input. The message is displayed in error format with the ErrorIcon if provided.

    o.ShowError(lcMessage,llIsText)
    

    Parameters

    lcMessage
    The text of the message to display.

    lcUserMessage
    A user message that is displayed above the message

    wwWebErrorDisplay::ShowMessage


    This method is used to display informational messages on a page. The messages are displayed with the informational icon displayed if provided.

    o.ShowMessage(lcMessage)
    

    Parameters

    lcMessage
    The text of the message to display. Treated as raw HTML.

    lcUserMessage
    A user message that is displayed above the message

    wwWebErrorDisplay::CellPadding


    The cell padding for the table that is rendered by this control.

    Default Value

    Initial value: 10

    wwWebErrorDisplay::Center


    Determines whether the control is centered in its HTML container.

    Default Value

    Initial value: .t.

    wwWebErrorDisplay::CssClass


    The CssClass used for the table display.

    Recommended default CSS class:
    ErrorDisplay

    o.CssClass                                               
    

    wwWebErrorDisplay::ErrorImage


    Image used as error icon which is the primary display icon. The icon is optional.

    wwWebErrorDisplay::InfoImage


    Image used as icon when calling
    ShowMessage.

    wwWebErrorDisplay::RenderMode


    Determines the type of text you assign which can be either Text or HTML

    Default Value

    Initial value: Html

    wwWebErrorDisplay::Text


    The error message text to display.

    o.Text                                                   
    

    wwWebErrorDisplay::Width


    The width of the table that is displayed containing the error message.

    Default Value

    Initial value: 400

    Class wwWebCheckBox


    The wwWebCheckbox class provides functionality for a standard Web checkbox control.

    It supports is AutoPostback and Click() event firing on AutoPostback operation.

    Custom
      
    wwWebControl
        wwWebCheckBox

    Class Members

    MemberDescription
    Click Click event for the CheckBox when the value is clicked and AutoPostBack is true.
    Checked Determines whether the checkbox is checked or not
    Width Stock Properties

    Requirements


    wwWebCheckBox::Click


    Click event for the CheckBox when the value is clicked and AutoPostBack is true.

    o.Click()
    

    wwWebCheckBox::Checked


    Determines whether the checkbox is checked or not

    Default Value

    Initial value: .f.

    wwWebCheckBox::Width


    Stock Properties

    Default Value

    Initial value: 10

    Button Controls


    These are commonly used button controls in the Web Control Framework.

    ClassDescription
      wwWebButton The wwWebButton class is the typical 'action' item for a WebPage. Clicking a button usually submits the form and causes a Click event to be fired on the form.
      wwWebHyperLink This object is used to create hyperlinks to navigate to other pages. It does not perform a postback.
      wwWebLinkButton The wwWebLinkButton is similar to a wwWebButton, but displays as a link rather than a button. The Button posts back to the current page and can trigger an event on the current page.
      wwWebImageButton The wwWebImageButton is a control for displaying both pure image links or link and text links. The button can post back to the same page or navigate to a new URL if a NavigateUrl/UrlControlSource is provided.
      wwWebMailLink The wwWebMailLink control embeds an email link into the page that attempts to thwart Spam bots from picking up the email address. It does this by splitting the email address and referring to a JavaScript function to actually pop up the mailto link.


    Class wwWebButton


    The wwWebButton class is the typical 'action' item for a WebPage. Clicking a button usually submits the form and causes a Click event to be fired on the form.

    Note that this interface is pretty sparse, but remember you have access to the the Attributes collection. In markup you can simply add any attributes needed and they will get added.

    So:

    <ww:wwWebButton ... AccessKey="S"/>

    sets the AccessKey attribute as you would expect. From code you can do:

    this.btnSubmit.Attributes.Add("AccessKey","S")


    Custom
      
    wwWebControl
        wwWebButton

    Class Members

    MemberDescription
    Click Occurs when a user clicks the button.
    OnClientClick Code that is translated into the onclick client event.
    Text The text to display on the button.
    UseSubmitBehavior Determines whether a sumbit or a plain HTML Button is created when the button is rendered.

    Requirements


    wwWebButton::Click


    Occurs when a user clicks the button.

    The click event should be routed to a method on the form. The event is designated in script code like so:

    <ww:wwWebButton runat="server' id="btnSubmit" value="Save" click="btnSubmit_Click" />

    You then need to implement the btnSubmit_Click method on your WebPage class:

    FUNCTION btnSubmit_Click() *** Do something in response to click ENDFUNC


    o.Click()
    

    wwWebButton::OnClientClick


    Code that is translated into the onclick client event.

    Note:
    If you use OnClientClick() on a submit button the button will still submit the form unless you return false from your JavaScript handler.

    Note that when UseSubmitBehavior is false you cannot fire 'click' events for this button since the button officially is not being submitted unless you manually hook up a PostBackEventReference.

    o.OnClientClick                                                
    

    wwWebButton::Text


    The text to display on the button.

    o.Text                                                         
    

    wwWebButton::UseSubmitBehavior


    Determines whether a sumbit or a plain HTML Button is created when the button is rendered.

    When .T. a type="Submit" is generated and the button posts back to the server whenever clicked. When .F. type="Button" is generated and the button doesn't post back to the server automatically.

    The default is .T. and that's the most common use, however if you use buttons for firing JavaScript code on the client you'll want to use SubmitBehavior .F. and then hook up the appropriate client behavior to the onclick or onclientclick.

    One common use of this feature is to create buttons that can be used for JavaScript operations or for 'manual' postbacks. For example a manual postback might cause a server method to be called as part of the postback:

    <ww:wwWebButton runat="server" ID="btnClient" UseSubmitBehavior="false" onclick="__doPostBack('page','PageMethod');"/>


    Class wwWebHyperLink


    This object is used to create hyperlinks to navigate to other pages. It does not perform a postback.

    If you you want to have a link button that posts back to the current page look at wwWebLinkButton.


    Custom
      wwWebControl
        wwWebHyperLink

    Class Members

    MemberDescription
    ControlSource The ControlSource binds a value expression to the Text Property by default. This binds the HyperLink's display label.
    ImageUrl Optional Image Url that causes an image to be displayed instead of text.
    NavigateUrl The Url to Navigate to
    Target Target Frame for the link
    Text The text that is displayed for the hyperlink.
    UrlControlSource Controlsource that applies a databinding expression against the NavigateUrl.

    Requirements


    wwWebHyperLink::ControlSource


    The ControlSource binds a value expression to the Text Property by default. This binds the HyperLink's display label.

    Binding is one way only. Unbind() is not supported.

    Expressions can be any valid Visual FoxPro expression that is in Scope including variables, class properties or even functions and class methods if you're only doing inbound binding.

    o.ControlSource                                               
    

    wwWebHyperLink::ImageUrl


    Optional Image Url that causes an image to be displayed instead of text.

    Note if you need both text and image data or anything more complex the wwWebImage control offers more control.

    wwWebHyperLink::NavigateUrl


    The Url to Navigate to

    wwWebHyperLink::Target


    Target Frame for the link

    wwWebTextBox::Text


    The text that is displayed for the hyperlink.

    Note if you need both text and image data or anything more complex than either text or image you can use the Text
    property and embed HTML into it.

    o.Text                                                        
    

    wwWebHyperLink::UrlControlSource


    Controlsource that applies a databinding expression against the NavigateUrl.

    This property is especially useful for binding page level post back events that cause a postback to a page event. The following example is used inside of a Repeater to delete an item and the PK is passed as an event 'parameter':

    <ww:wwWebHyperLink runat="server" ID="btnDelete" Click="btnDelete_Click" Text="Delete" CssClass="hoverbutton" UrlControlSource="this.Page.GetPostbackEventReference('Page','btnDelete_Click',TRANS(pk),.T.)" />

    To handle this event the event is fired and it can pick up the __EventParameter POST variable for state:

    FUNCTION btnDelete_Click() lcId = Request.Form("__EventParameter") IF !this.oEntry.Delete( VAL(lcId) ) this.ErrorDisplay.ShowError("Couldn't delete entry:<br>" + this.oEntry.cErrormsg RETURN ENDIF this.ErrorDisplay.ShowMessage( "Entry deleted." ) ENDFUNC * btnDelete_Click

    Note that you need to call DataBind() on this control (or its host data control like a Repeater/DataGrid) in order for this expression to be applied.

    Class wwWebImageButton


    The wwWebImageButton is a control for displaying both pure image links or link and text links. The button can post back to the same page or navigate to a new URL if a NavigateUrl/UrlControlSource is provided.

    As such this control is a hybrid between a wwWebHyperLink, wwWebLinkButton and wwWebImage. Basically you should be able to use this control for any links that require images. The control renders as a hyperlink with the image as its content.

    The control renders as:

    <a href="punchin.tt" id="lnkPunchIn" class="hoverbutton"> <img src="images/Punchin.gif" id="lnkPunchIn_image" > Punch In</a> </div>

    The main control is the div tag and it receives the ID and all base tags. You can use CSS to style the HREF and IMG tags internally.

    Control sources of this control map to:

    For example to cause a custom postback operation to occur (in a Repeater or DataGrid for example):

    <ww:wwWebImageButton runat="server" ID="btnDelete" Click="btnDelete_Click" ImageUrl="~/images/remove.gif" CssClass="hoverbutton" UrlControlSource="this.Page.GetPostbackEventReference('Page','btnDelete_Click',TRANS(pk),.T.)" />



    Custom
      
    wwWebControl
        wwWebImageButton

    Class Members

    MemberDescription
    Click Occurs when you click on the image link. Note this event does NOT fire if you have specified a NavigateUrl in which case the image doesn't post back and just navigates to the new URL.
    BorderWidth The borderwidth of the image. The value is a string and defaults to "0".
    ControlSource ControlSource that applies a databinding expression against the ImageUrl.
    DisabledImageUrl Image displayed when the control is disabled (enabled=.F.). If not set the ImageUrl is used.
    ImageUrl The Url to the Image to display.
    NavigateUrl The Url that is navigated to when you click the link.
    OnClientClick Allows attaching of a client side JavaScript click handler that fires when the link is clicked. Use return false; to force the click to prevent navigation:
    Target The target frame that the response will be rendered into.
    Text Optional text property for the Image button. If specified displays to the right of the image.
    UrlControlSource Controlsource that applies a databinding expression against the NavigateUrl.

    Requirements


    wwWebImageButton::Click


    Occurs when you click on the image link. Note this event does NOT fire if you have specified a NavigateUrl in which case the image doesn't post back and just navigates to the new URL.

    o.Click()                                                      
    

    wwWebImageButton.ControlSource


    ControlSource that applies a databinding expression against the ImageUrl.

    o.wwWebButton.ControlSource                                                 
    

    wwWebImageButton::BorderWidth


    The borderwidth of the image. The value is a string and defaults to "0".

    o.BorderWidth                                                  
    

    Default Value

    "0"

    wwWebImageButton::DisabledImageUrl


    Image displayed when the control is disabled (enabled=.F.). If not set the ImageUrl is used.

    o.DisabledImageUrl                                                     
    

    wwWebImageButton::ImageUrl


    The Url to the Image to display.

    o.ImageUrl                                                     
    

    wwWebImageButton::NavigateUrl


    The Url that is navigated to when you click the link.

    o.NavigateUrl                                                  
    

    wwWebImageButton::OnClientClick


    Allows attaching of a client side JavaScript click handler that fires when the link is clicked. Use return false; to force the click to prevent navigation:

    <ww:wwWebImageButton ID="lnkPunchIn" runat="server" ImageUrl="images/Punchin.gif" NavigateUrl="punchin.tt" Text="Punch In" CssClass="hoverbutton" OnClientClick="alert('hello'); return false;" />


    o.OnClientClick                                                
    

    wwWebImageButton::Target


    The target frame that the response will be rendered into.

    o.Target                                                  
    

    wwWebImageButton::Text


    Optional text property for the Image button. If specified displays to the right of the image.

    o.Text                                                         
    

    wwWebImageButton::UrlControlSource


    Controlsource that applies a databinding expression against the NavigateUrl.

    This property is especially useful for binding page level post back events that cause a postback to a page event. The following example is used inside of a Repeater to delete an item and the PK is passed as an event 'parameter':

    <ww:wwWebImageButton runat="server" ID="btnDelete" Click="btnDelete_Click" ImageUrl="~/images/remove.gif" CssClass="hoverbutton" UrlControlSource="this.Page.GetPostbackEventReference('Page','btnDelete_Click',TRANS(pk),.T.)" />

    To handle this event the event is fired and it can pick up the __EventParameter POST variable for state:

    FUNCTION btnDelete_Click() lcId = Request.Form("__EventParameter") IF !this.oEntry.Delete( VAL(lcId) ) this.ErrorDisplay.ShowError("Couldn't delete entry:<br>" + this.oEntry.cErrormsg RETURN ENDIF this.ErrorDisplay.ShowMessage( "Entry deleted." ) ENDFUNC * btnDelete_Click

    Note that you need to call DataBind() on this control (or its host data control like a Repeater/DataGrid) in order for this expression to be applied.

    o.UrlControlSource                                             
    

    Class wwWebLinkButton


    The wwWebLinkButton is similar to a wwWebButton, but displays as a link rather than a button. The Button posts back to the current page and can trigger an event on the current page.


    Custom
      
    wwWebControl
        wwWebLinkButton

    Class Members

    MemberDescription
    Click Occurs when the user clicks the link.
    OnClientClick Optional client click JavaScript handler code.
    Text The text for the link to display.

    Requirements


    wwWebLinkButton::Click


    Occurs when the user clicks the link.

    The click event should be routed to a method on the form. The event is designated in script code like so:

    <ww:wwWebButton runat="server' id="btnSubmit" value="Save" click="btnSubmit_Click" />

    You then need to implement the btnSubmit_Click method on your WebPage class:

    FUNCTION btnSubmit_Click() *** Do something in response to click ENDFUNC


    o.Click()                                                      
    

    wwWebLinkButton::OnClientClick


    Optional client click JavaScript handler code.

    Use return false; in the handler to prevent the link from navigating.

    o.OnClientClick                                            
    

    wwWebLinkButton::Text


    The text for the link to display.

    o.Text                                                     
    

    Class wwWebMailLink


    The wwWebMailLink control embeds an email link into the page that attempts to thwart Spam bots from picking up the email address. It does this by splitting the email address and referring to a JavaScript function to actually pop up the mailto link.

    WebControl
      wwWeb

    Class Members

    MemberDescription
    EmailControlSource Control Source that binds the Email address. This property is used in combination with the plain ControlSource which binds the Text property.
    ImageUrl An optional Image Url. If an image is specified the Text property is ignored.
    Message Optional text that is displayed inside of the email message as preseeded text.
    Subject Optional subject for the email message popped up.

    Requirements

    Assembly: webcontrols.prg

    wwWebMailLink::EmailControlSource


    Control Source that binds the Email address. This property is used in combination with the plain ControlSource which binds the Text property.

    wwWebMailLink::ImageUrl


    An optional Image Url. If an image is specified the Text property is ignored.

    wwWebMailLink::Message


    Optional text that is displayed inside of the email message as preseeded text.

    wwWebMailLink::Subject


    Optional subject for the email message popped up.

    List Controls


    List controls are controls that typically databind list style content by scanning over cursors and populating its data with the content of the cursor.

    ClassDescription
      wwWebDataGrid The wwWebDataGrid provides a cursor based, read-only 'grid' control for displaying data in columnar format.
      wwWebDropDownList The wwWebDropDownlist creates a standard Web dropdown list.
      wwWebListBox The wwWebListBox creates a standard Web ListBox.
      wwWebListControl The wwWebListControl class provides the base behavior for the ListBox and DropDownList controls. It implements nearly all of the features of both controls.
      wwWebRadioButtonList Provides a list of radio buttons that act as a single control value. Making a selection on this control causes the SelectedValue property to be set.
      wwWebRepeater The WebRepeater class provides a template driven repeatable display bound to a datasource. The repeater supports Cursors, Object Arrays and Object Collections for databinding.


    Class wwWebDataGrid


    The wwWebDataGrid provides a cursor based, read-only 'grid' control for displaying data in columnar format.

    The DataGrid can display data either based on the current database structure by auto-generating columns from the data source, or you can create custom column layouts to describe the layout. Databinding can be done via powerful FoxPro expressions that are rendered for each column and can be executed dynamically to produce sophisticated layouts and rendering.

    For more details on how the DataGrid works check the wwWebDataGrid How To's section.

    wwWebControl
      wwWebDataGrid

    Example

    Script Code:

    <ww:wwWebDataGrid ID="gdCustomers" runat="server" PageSize="10"> <Columns> <ww:wwWebDataGridColumn runat="server" id="colCompany" Expression="Company" HeaderText="Company" /> <ww:wwWebDataGridColumn runat="server" id="colCareOf" Expression="CareOf" HeaderText="Name" /> </Columns> </ww:wwWebDataGrid>

    Fox Code:

    THIS.gdCustomers = CREATEOBJECT("wwwebdatagrid",THIS) THIS.gdCustomers.Id = "gdCustomers" THIS.gdCustomers.PageSize = 10 THIS.AddControl(THIS.gdCustomers) THIS.colCompany = CREATEOBJECT("wwwebdatagridcolumn",THIS) THIS.colCompany.Id = "colCompany" THIS.colCompany.Expression = [Company] THIS.colCompany.HeaderText = [Company] THIS.colCompany.UniqueId = [gdCustomers_colCompany] THIS.gdCustomers.AddControl(THIS.colCompany) THIS.colCareOf = CREATEOBJECT("wwwebdatagridcolumn",THIS) THIS.colCareOf.Id = "colCareOf" THIS.colCareOf.Expression = [CareOf] THIS.colCareOf.HeaderText = [Name] THIS.colCareOf.UniqueId = [gdCustomers_colCareOf] THIS.gdCustomers.AddControl(THIS.colCareOf)

    Class Members

    MemberDescription
    PageIndexChanged Default handler always fires and sets the page index.
    RowRender Fires just before an item row is rendered in the DataGrid. You can optionally return the full content of the row as a string.
    SortOrderChanged Fires when the SortOrder header is clicked and the page order is changed.
    AddControl Overridden AddControl method that allows adding Column objects to the DataGrid.
    o.AddControl(loCtl)
    DataBind Databinds the DataGrid. Unlike some other list controls you should always call DataBind() to ensure that internal settings get updated properly for binding. This includes page counts, sort orders etc.
    o.DataBind()
    RemoveColumn Removes a column properly from the DataGrid.
    o.RemoveColumn(lcColumnId)
    ActiveColumn The active grid column object instance during databinding. This property can be used to get access to the individual column that is current active for databinding.
    ActiveColumnAttributeString The content of the column's ItemAttributeString that is used to render the current column. This value can be safely overridden in any databinding (ControlSource) or formatting expression to affect only the active column.
    AlternatingItemCssClass The CSS Class used on Alternating Rows.
    AutoGenerateColumns Automatically generates all columns if set to .t.
    CellPadding The CellPadding for the HTML table.
    Columns Collection of wwWebGridColumn items that contain the behaviors for each to the Columns that are displayed.
    CurrentPageIndex Holds the value for the current Page that is displayed if paging is on. You can also preset this value to set the grid to a specific page. If the page index exceeds the number of pages the last page is selected.
    DataKeyField When specified causes each row to be rendered with an id expression that includes the evaluated expression. This can be useful for client side script code to pass state information to the server for data that needs updating.
    DataSource The name of a cursor or table that we are binding to.
    HeaderCssClass The CSS Class used for the List Header.
    ItemCssClass The CSS Class used for displaying normal rows. By default no Class is applied so the default table class/styles are used.
    PageCount Number of total pages for datasource based on the PageCount. This property gets set after DataBind() has been called and is set only if PageSize is greater than 0.
    PagerColumnAttributes Any custom attributes you might need to set on the page column.
    PagerCssClass The CSS Class used in the Pager row of the table.
    PagerText The text displayed before the actual page display. Defaults to Pages:.
    PageSize Size of the Page to display. If non-zero causes the DataGrid to add a Pager band to the bottom of the list that allows the user to select a different page.
    RowAttributeString A complete attribute string that can be applied to the <TR> tag of the data items.
    RowContent Can be assigned to inside of the RowRender event to completely override rendering of an individual row. If set the full row including the <tr></tr> tags must be rendered in the text assigned to this property. This value is cleared before every call to the RowRender event.
    SortColumn Internal property used to keep track which column name is currently sorted. The value is the name of the column.
    SortOrder Returns the current direction of the Sorting that might be active. Either empty or DESC.

    Requirements


    wwWebDataGrid How To's


    This section shows a few topics that demonstrate DataGrid specific tasks:



    Adding Columns to a DataGrid


    The wwWebDataGrid can render columns either automatically - rendering basically all the columns in a cursor/table that is hooked up via DataSource, or allow you to create columns individually by adding Column objects to the DataGrid. The latter can be done visually in the Visual Studio editor, via script tags, or via code.

    AutoGenerateColumns

    AutoGenerateColumns can be used to force the DataGrid to render all columns of the data source automatically. In this configuration you have very little control as Web Connection uses default formatting for everything.

    This mode sets up columns in the call to DataBind. At that time the Column objects are created and added to the DataGrid.

    One trick with AutoGenerateColumns is to call DataBind(), then remove or adjust column the auto generated columns. To do this call DataBind, then use things like this:

    this.gdGrid.DataSource = "TQuery" this.gdGrid.DataBind() *** Now adjust columns this.gdGrid.Columns.Remove("FieldName") loCol = this.gdGrid.Columns.Item("FieldName2") loCol.HeaderText = "New heading" loCol.Expression = "Upper(Company)"

    Manual Column Configuration

    In most cases you'll want to explicitly configure columns, either visually or in code. The best way to do this is inside of the Visual Studio Editor. To do so select the DataGrid in design view, the right click Properties and select the Columns property in the Property Grid:

    then you can edit the columns in the Column Collection Editor:

    Each column has a number of custom properties that determine how data is bound to the column and how the column displays. The most important properties are:

    Expression
    The binding expression. This expression is evaluated for every row that is displayed in the DataGrid. This can be a field name, a property of an object such as a business object (ie. this.Page.oMyBusiness.oData.Company) or a UDF() (upper(Company)) or a method of an object.

    Format
    The FoxPro format expression that is used to format the field display. Can also be a UDF, function or method by prefixing with an = sign (ie. =upper or =this.Page.FormatUpdate).

    ItemAttributeString
    This is a catchall HTML tag string that can be attached to each item. For example, you can use align="center" or style="width:200px" or class="HighLightColumn".

    There are other properties of course, but these are likely the ones you'll be using most frequently.

    The column object is very flexible - between the Expression and format properties you have the capability to very finely tune the output values displayed in the grid as well as providing full formatting control over the columns as they are rendered.

    One very useful trick is to use Expression values that call a method on the form and then use the form method to do sophisticated formatting of the value and the cell that it displays in by manipulating the column object on the fly.

    The following example demonstrates how to display a DateTime Value and highlight every value displayed within the last year. The expression would be:

    Expression="this.Page.FormatUpdated( Updated )"

    Note this.Page (this refers to the DataGrid). The code to handle this then manipulates the column for every access:

    FUNCTION FormatUpdated(ltUpdated) LOCAL loCol *** If HighLight Checkbox is set - highligh recent dates IF THIS.chkHighLight.Checked *** Grab the column by the ID loCol = this.gdDeveloperList.Columns.Item("wwWebDataGridColumn3") *** And change the columnns style IF ltUpdated > TTOD( DATETIME() ) - 365 loCol.ItemAttributeString=" style='background:red;color:cornsilk;font-weight:bold;' " ELSE *** Normal colors - simply clear the style - note if Style was set on the *** the column you'd have to set this value the set style loCol.ItemAttributeString = " style='' " ENDIF ENDIF *** Now format the Time into a string with wwUtils TimeToC RETURN TimeToC( ltUpdated) ENDIF ENDFUNC

    Note that this code retrieves the column, and changes its style. Make sure if you change column properties that you set them on every pass! A change made here will affect all subsequent renderings.

    This is very powerful, but more importantly relatively easy to do. It takes very little code to perform this sort of formatting in your FoxPro code.

    Columns in Code

    Columns can also be added in code. In fact, even if you create columns in the visual editor or script code they are eventually translated into code by the Page Parser. You can of course write that code yourself. Here's an example of a DataGrid setup in WCSX script:

    <ww:wwWebDataGrid ID="gdDeveloperList" runat="server" CssClass="blackborder" PageSize="10" Width="640"> <Columns> <ww:wwWebDataGridColumn ID="wwWebDataGridColumn1" runat="server" Expression="Href('EditData_Cursor.wcsx?id=' + TRANS(pk),Company )" HeaderText="Company" Sortable="True" SortExpression="upper(Company)" ItemAttributeString="style='width:250;'"> </ww:wwWebDataGridColumn> <ww:wwWebDataGridColumn ID="wwWebDataGridColumn2" runat="server" Expression="Name" HeaderText="Developer Name" Sortable="True" SortExpression="upper(name)" ItemAttributeString="style='width:200;'"> </ww:wwWebDataGridColumn> <ww:wwWebDataGridColumn ID="wwWebDataGridColumn3" runat="server" Expression="Updated" Format="=this.Page.FormatUpdated" HeaderText="Last Update" HeaderAttributeString="align='center'"> </ww:wwWebDataGridColumn> </Columns> </ww:wwWebDataGrid>

    and here is the generated code for that script code:

    THIS.gdDeveloperList = CREATEOBJECT("wwwebdatagrid",THIS,"gdDeveloperList") THIS.gdDeveloperList.CssClass = [blackborder] THIS.gdDeveloperList.PageSize = 10 THIS.gdDeveloperList.Width = [640] THIS.AddControl(THIS.gdDeveloperList) THIS.wwWebDataGridColumn1 = CREATEOBJECT("wwwebdatagridcolumn",THIS,"wwWebDataGridColumn1") THIS.wwWebDataGridColumn1.Expression = [Href('EditData_Cursor.wcsx?id=' + TRANS(pk),Company )] THIS.wwWebDataGridColumn1.HeaderText = [Company] THIS.wwWebDataGridColumn1.Sortable = .T. THIS.wwWebDataGridColumn1.SortExpression = [upper(Company)] THIS.wwWebDataGridColumn1.ItemAttributeString = [style='width:250;'] THIS.wwWebDataGridColumn1.UniqueId = [gdDeveloperList_wwWebDataGridColumn1] THIS.gdDeveloperList.AddControl(THIS.wwWebDataGridColumn1) THIS.wwWebDataGridColumn2 = CREATEOBJECT("wwwebdatagridcolumn",THIS,"wwWebDataGridColumn2") THIS.wwWebDataGridColumn2.Expression = [Name] THIS.wwWebDataGridColumn2.HeaderText = [Developer Name] THIS.wwWebDataGridColumn2.Sortable = .T. THIS.wwWebDataGridColumn2.SortExpression = [upper(name)] THIS.wwWebDataGridColumn2.ItemAttributeString = [style='width:200;'] THIS.wwWebDataGridColumn2.UniqueId = [gdDeveloperList_wwWebDataGridColumn2] THIS.gdDeveloperList.AddControl(THIS.wwWebDataGridColumn2) THIS.wwWebDataGridColumn3 = CREATEOBJECT("wwwebdatagridcolumn",THIS,"wwWebDataGridColumn3") THIS.wwWebDataGridColumn3.Expression = [Updated] THIS.wwWebDataGridColumn3.Format = [=this.Page.FormatUpdated] THIS.wwWebDataGridColumn3.HeaderText = [Last Update] THIS.wwWebDataGridColumn3.HeaderAttributeString = [align='center'] THIS.wwWebDataGridColumn3.UniqueId = [gdDeveloperList_wwWebDataGridColumn3] THIS.gdDeveloperList.AddControl(THIS.wwWebDataGridColumn3)

    As you can see there's a 1 to 1 mapping between the script code and the FoxPro code and you can use the FoxPro code directly inside of a page to add a datagrid dynamically or add additional columns to a grid.

    How to create column and cell specific formatting


    The DataGrid allows you to create custom formatting for each column and with a little bit of extra work each individual cell of the grid. The key to this functionality is the innocuous Format property which allows you to format text.

    Column based Formatting

    Applying column based formatting is as easy as applying either a FoxPro format expression. For example, you can use:

    @! - all upper case string
    @YL – display a spelled out date format

    You can also apply format expressions which are functions that can be called. Functions must follow some simple rules:

    1. Take a single parameter of the value
    2. Return string output as a response

    You can call format expressions like this:

    =UPPER
    =Proper
    =MyUdf
    =this.Page.FormatField

    Each of these functions must take a single input parameter and return a string. A format expression applied in either of these ways formats a column as a whole.

    The most common scenario is probably the last where you call a method of the form to perform your custom formatting. For example:

    FUNCTION FormatField(ldDate) RETURN TimeToC(ldDate) + " PST"

    Cell based formatting

    You can also create cell based formatting that displays custom formats based on values of a cell. In order to do this you need to create custom expressions. For example, consider a Date column where you want to display all dates after a certain date in a different color than all other dates. To do this you might create a column like this:

    <ww:wwWebDataGridColumn ID="colUpdated" runat="server" Expression="this.Page.EnteredExpression(Entered)" HeaderText="Last Update" ItemAttributeString="style='width:130px;background:teal;'"> </ww:wwWebDataGridColumn>

    You can then implement an EnteredExpression() method on the Page that handles the updating of the value:

    FUNCTION EnteredExpression(ldValue) *** Highlight filtered entries IF ldValue > {01/01/2007} this.dgDevelopers.ActiveColumnAttributeString="style='background:red;'" ENDIF RETURN timetoc(ldValue)

    The grid's ActiveColumnAttributeString allows you to apply any attributes to the table column. Here I apply a custom style, but you can use a CSS class or any other attribute you see fit - basically whatever you specify gets added to the column definition.

    You can also get access to the underlying column object (or any other column):

    FUNCTION EnteredExpression(ldValue) loCol = THIS.gdDevelopers.ActiveColumn loCustCol = this.gdDevelopers.Columns.Item("colCompany") IF ldValue > {01/01/2004} loCol.ItemAttributeString="style='background:red;'" loCustCol.ItemAttributeString = "style=font-weight: bold;" ELSE *** IMPORTANT - changing properties on the column *** changes it for all rows following! loCol.ItemAttributeString="style='background:teal;'" loCustCol.ItemAttributeString = "" ENDIF RETURN timetoc(ldValue)

    Generally it's preferrable to make visual changes using ActiveItemAttributeString since the change is applied only to the current column with any other columns not overridden falling back to the default rendering. If you modify the column explicitly you have to make sure you reset the column to its default rendering manually (ie. duplicate whatever attributes you have applied in the markup or column definition).




    wwWebDataGrid::PageIndexChanged


    Default handler always fires and sets the page index.
    Note prior to this event (OnLoad) the page index is
    is not current


    o.PageIndexChanged()
    

    wwWebDataGrid::SortOrderChanged


    Fires when the SortOrder header is clicked and the page order is changed.

    You can t

    o.SortOrderChanged()
    

    wwWebDataGrid::RowRender


    Fires just before an item row is rendered in the DataGrid. You can optionally return the full content of the row as a string.

    The event is fired before any output for the row is generated and if you return a string value that value is assumed to be the full output of the row.

    You can also modify the columns of the grid in order to change the behavior. For example, assume for a second that you have a set of values that you want to group with different colors by a date range. Each data gets a different color. You can implement this with the following code. Assume this table has an entered field and the cursor is sorted by date and databound:

    <ww:wwWebDataGrid ID="gdLinksByDay" runat="server" CssClass="BlackBorder" width="95%" PageSize="25" AlternatingItemCssClass="" RowRender="gdLinksByDay_RowRender"> <Columns> <ww:wwWebDataGridColumn ID="colEntered" runat="server" Expression="Entered" HeaderText="Entered" HeaderAttributeString="align='center'" FieldType="D" ItemAttributeString="style='font-size:8pt'"> </ww:wwWebDataGridColumn> ... More Columns </Columns> </ww:wwWebDataGrid>

    The key is the RowRender event which points at this method:

    DEFINE CLASS MyPage as wwWebPage *** Internal Tracking Values for next group lAlternate = .F. dLastDate = {^2000/01/01} FUNCTION OnLoad() ENDFUNC FUNCTION gdLinksByDay_RowRender() *** Grab the current DataItem from cursor *** DataGrid DataSource Cursor will be in scope here lvValue = TQuery.Entered *** Check last date - if changed flip the alternate *** switch which tells us to change background color IF this.dLastDate # lvValue this.lAlternate = !this.lAlternate ENDIF IF THIS.lAlternate lcAttributes = "class='gridalternate'" ELSE lcAttributes = "" ENDIF *** Apply the CSS Class to each column FOR lnX = 1 TO this.gdLinksByDay.Columns.Count loCol = this.gdLinksByDay.Columns.Item(lnX) loCol.ItemAttributeString = lcAttributes ENDFOR this.dLastDate = lvValue ENDFUNC * ReferingLinks_Page :: EnteredFieldExpression ENDDEFINE


    This code basically checks for a changed date in the cursor and based on that value changes the CSS Class of each of the columns in the grid to display in a common color. So all the dates of the same kind display in the same color.

    o.RowRender()                                                
    

    Class wwWebDataGridColumn


    A Web Data Grid Column provides attributes for a child column inside of a Web Data Grid.

    DataGrid columns are added to a DataGrid via AddControl which looks like this:

    Script Code:

    <ww:wwWebDataGrid ID="gdCustomers" runat="server"> <Columns> <ww:wwWebDataGridColumn runat="server" id="colCompany" Expression="Company" HeaderText="Company" /> <ww:wwWebDataGridColumn runat="server" id="colCareOf" Expression="CareOf" HeaderText="Name" /> </Columns> </ww:wwWebDataGrid>

    Fox Code:

    THIS.gdCustomers = CREATEOBJECT("wwwebdatagrid",THIS) THIS.gdCustomers.Id = "gdCustomers" THIS.AddControl(THIS.gdCustomers) THIS.colCompany = CREATEOBJECT("wwwebdatagridcolumn",THIS) THIS.colCompany.Id = "colCompany" THIS.colCompany.Expression = [Company] THIS.colCompany.HeaderText = [Company] THIS.colCompany.UniqueId = [gdCustomers_colCompany] THIS.gdCustomers.AddControl(THIS.colCompany) THIS.colCareOf = CREATEOBJECT("wwwebdatagridcolumn",THIS) THIS.colCareOf.Id = "colCareOf" THIS.colCareOf.Expression = [CareOf] THIS.colCareOf.HeaderText = [Name] THIS.colCareOf.UniqueId = [gdCustomers_colCareOf] THIS.gdCustomers.AddControl(THIS.colCareOf)


    Class Members

    MemberDescription
    CssClass CSS class that is applied to each cell in this column. Applied to the individidual TD tag in the table.
    Expression The Expression used to evaluate the column display value.
    FieldType Explicit declaration of the FieldType for the resulting expression.
    Format A FoxPro Format expression applied to the value that is displayed.
    HeaderAttributeString Any additional attributes you might want to apply to the header each item in a column. This property basically allows you to override any cell behaviors.
    HeaderText Header text for the column that is displayed in the header row.
    ItemAttributeString Any additional attributes you might want to apply to each item in a column. This property basically allows you to override any cell behaviors.
    Sortable Determines whether a column is sortable. If it is sortable a SortExpression is applied to the column.
    SortExpression An optional SortExpression that is applied to a column if it is the currently sorted column.
    Style Style that is applied to each cell in this column. Applied to the individidual TD tag in the table.

    Requirements


    wwWebDataGridColumn::CssClass


    CSS class that is applied to each cell in this column. Applied to the individidual TD tag in the table.

    o.CssClass                                             
    

    wwWebDataGridColumn::Expression


    The Expression used to evaluate the column display value.

    This value can be any FoxPro expression including a Cursor/Table Fieldname, variable, object property, UDF() or class method call.

    Examples:

    <Columns> <ww:wwWebDataGridColumn ID="wwWebDataGridColumn1" runat="server" Expression="Company" /> <ww:wwWebDataGridColumn ID="wwWebDataGridColumn2" runat="server" Expression="FirstName + ' ' + Lastname" HeaderText="Name" /> <ww:wwWebDataGridColumn ID="colEntered" runat="server" Expression="this.Page.EnteredColumnExpression(Entered)" FieldType="C" HeaderText="Entered" /> </Columns>


    o.Expression                                           
    

    wwWebDataGridColumn::FieldType


    Explicit declaration of the FieldType for the resulting expression.

    This value is optional for most expressions, as Web Connection figures out the type at runtime. However, it's important to provide type information for complex expressions calling methods/function since TYPE() cannot return type information on those.

    If TYPE() returns 'U', Web Connection defaults to string, which may or may not work, but is the most common scenario.

    o.FieldType                                            
    

    wwWebDataGridColumn::Format


    A FoxPro Format expression applied to the value that is displayed.

    This can be a FoxPro Format expression like @! or @YL or a function call.

    You can also use functions or methods for formatting by prefacing the name of a function/method with an = sign. However, the function MUST accept at least a single parameter (the value of the Expression) and return a string result.

    Examples:


    Note that you specify only the the = and function/method name not the () or parameter list. Web Connection will pass the specified function a single parameter which is expression value.

    Note that if you use expressions it's often easier to simply create an Expression instead and simply pass the expression.

    o.Format                                               
    

    wwWebDataGridColumn::HeaderAttributeString


    Any additional attributes you might want to apply to the header each item in a column. This property basically allows you to override any cell behaviors.

    Example:
    HeaderAttributeString="align='right' style='color:red;font-weight:bold'"

    Note the use of single quotes that are required for VS.NET rendering for anything inside quotes.

    o.HeaderAttributeString                                
    

    wwWebDataGridColumn::HeaderText


    Header text for the column that is displayed in the header row.

    The header text can be specified as a static string or - if prefixed with an = - an expression. For example, if you wanted to display the current date time in the header you'd use:

    loCol.HeaderText = "=DATETIME()"



    o.HeaderText                                           
    

    Default Value

    static header string or a FoxPro expression if prefixed with an = sign.

    wwWebDataGridColumn::ItemAttributeString


    Any additional attributes you might want to apply to each item in a column. This property basically allows you to override any cell behaviors.

    Example:
    ItemAttributeString="align='right' style='color:red;font-weight:bold'"

    Note the use of single quotes that are required for VS.NET rendering for anything inside quotes.

    o.ItemAttributeString                                  
    

    wwWebDataGridColumn::Sortable


    Determines whether a column is sortable. If it is sortable a SortExpression is applied to the column.

    o.Sortable                                             
    

    wwWebDataGridColumn::SortExpression


    An optional SortExpression that is applied to a column if it is the currently sorted column.



    o.SortExpression                                       
    

    Remarks

    Sorting in the datagrid requires that FoxPro cursors are not filtered views that can be generated by some SELECT statements. To ensure output of a query to a table use the NOFILTER clause on the SELECT statement.

    wwWebDataGridColumn::Style


    Style that is applied to each cell in this column. Applied to the individidual TD tag in the table.

    o.Style                                                
    

    Client Event Properties


    Due to the way Visual Studio renders content in the designer any custom tags defined in on child controls cause the main control to fail to render.

    In order to facilitate a few common client scenarios, the data grid column also publishes a few Web Browser Events that are mapped to the column explicitly. While you can always use ItemAttributeString or a custom expression to add additional attributes to the colum cell definition it's often easier to do so with explicitly.

    The following JavaScript events are surfaced as properties:



    wwWebDataGrid::AddControl


    Overridden AddControl method that allows adding Column objects to the DataGrid.

    Script Code:

    <ww:wwWebDataGrid ID="gdCustomers" runat="server"> <Columns> <ww:wwWebDataGridColumn runat="server" id="colCompany" Expression="Company" HeaderText="Company" /> <ww:wwWebDataGridColumn runat="server" id="colCareOf" Expression="CareOf" HeaderText="Name" /> </Columns> </ww:wwWebDataGrid>

    Fox Code:

    THIS.gdCustomers = CREATEOBJECT("wwwebdatagrid",THIS) THIS.gdCustomers.Id = "gdCustomers" THIS.AddControl(THIS.gdCustomers) THIS.colCompany = CREATEOBJECT("wwwebdatagridcolumn",THIS) THIS.colCompany.Id = "colCompany" THIS.colCompany.Expression = [Company] THIS.colCompany.HeaderText = [Company] THIS.colCompany.UniqueId = [gdCustomers_colCompany] THIS.gdCustomers.AddControl(THIS.colCompany) THIS.colCareOf = CREATEOBJECT("wwwebdatagridcolumn",THIS) THIS.colCareOf.Id = "colCareOf" THIS.colCareOf.Expression = [CareOf] THIS.colCareOf.HeaderText = [Name] THIS.colCareOf.UniqueId = [gdCustomers_colCareOf] THIS.gdCustomers.AddControl(THIS.colCareOf)


    o.AddControl(loCtl)
    

    Parameters

    loCtl

    wwWebDataGrid::RemoveColumn


    Removes a column properly from the DataGrid.

    Note that you should call this method instead of: grid.Columns.Remove() in order to properly hande cleanup.

    o.RemoveColumn(lcColumnId)                                   
    

    Parameters

    lcColumnId
    The column to remove by name

    wwWebDataGrid::DataBind


    Databinds the DataGrid. Unlike some other list controls you should always call DataBind() to ensure that internal settings get updated properly for binding. This includes page counts, sort orders etc.

    o.DataBind()
    

    wwWebDataGrid::AlternatingItemCssClass


    The CSS Class used on Alternating Rows.

    Default Value

    Initial value: gridalternate

    wwWebDataGrid::ActiveColumn


    The active grid column object instance during databinding. This property can be used to get access to the individual column that is current active for databinding.

    This property is merely a short cut for code like this:

    loCol = this.gdData.Column.Item("activeColumn")

    Note that if you change any properties on this column you are changing the core column definition and the column will render with your changes for any remaining data rows.

    If you need to modify only display attributes or styles and do so for individual rows, you may use ActiveColumnAttributeString which is restored for each column rendering.

    o.ActiveColumn                                               
    

    wwWebDataGrid::ActiveColumnAttributeString


    The content of the column's ItemAttributeString that is used to render the current column. This value can be safely overridden in any databinding (ControlSource) or formatting expression to affect only the active column.

    Note that this is the preferred mechanism for changing rendering of individual columns conditionally. Say if you want to highlight specifc dates in a data column you might code like the following:

    FUNCTION EnteredColumnExpression(ltEntered) IF ltEntered >= {^2007/01/01} this.dgCustomers.ActiveColumnAttributeString = "style='background: green;'" ENDIF RETURN ShortDate(ltEntered,2) ENDFUNC * EnteredColumnExpression

    Unlike modifying the ActiveColumn directly - which affects all remaining rows rendered with this column definition - changing the ActiveColumnAttributeString is applied only to the current column, leaving the original column ItemAttributestring intact.

    o.ActiveColumnAttributeString                                
    

    wwWebDataGrid::AutoGenerateColumns


    Automatically generates all columns if set to .t.

    Default Value

    Initial value: .F.

    wwWebDataGrid::CellPadding


    The CellPadding for the HTML table.

    Default Value

    Initial value: 3

    wwWebDataGrid::Columns


    Collection of
    wwWebGridColumn items that contain the behaviors for each to the Columns that are displayed.

    Columns are added using the AddControl method of the DataGrid.

    wwWebDataGrid::CurrentPageIndex


    Holds the value for the current Page that is displayed if paging is on. You can also preset this value to set the grid to a specific page. If the page index exceeds the number of pages the last page is selected.

    Default Value

    Initial value: -1;

    wwWebDataGrid::DataSource


    The name of a cursor or table that we are binding to.

    wwWebDataGrid::DataKeyField


    When specified causes each row to be rendered with an id expression that includes the evaluated expression. This can be useful for client side script code to pass state information to the server for data that needs updating.

    The id is generated as follows:

    <DataGridId>_<EvaluatedExpression>

    For example, assuming a table has an PK field you might have:

    <ww:wwWebDataGrid ID="gdDeveloperList" runat="server" DataKeyField="Pk">

    Which renders each row as:

    <tr id="gdDeveloperList_474" valign="top" >

    You can then use client code from a cell to determine the value via JavaScript:

    <ww:wwWebDataGridColumn ID="wwWebDataGridColumn1" runat="server" Expression="Href([BasicDataBinding.wcsx?id=] + TRANS(pk),Company,[ target='_ShowDev' ] )" HeaderText="Company" ItemAttributeString="style='width:250;'" onclick="alert( 'Row Pk: ' + this.parentNode.id.split('_' )[1]);"


    o.DataKeyField                                               
    

    wwWebDataGrid::HeaderCssClass


    The CSS Class used for the List Header.

    Default Value

    Initial value: gridheader

    wwWebDataGrid::ItemCssClass


    The CSS Class used for displaying normal rows. By default no Class is applied so the default table class/styles are used.

    wwWebDataGrid::PageCount


    Number of total pages for datasource based on the PageCount. This property gets set after DataBind() has been called and is set only if PageSize is greater than 0.

    Typically you can look at this value in the PageIndexChanged event. This property is mostly used internally, but is exposed externally for you to use if needed.

    Default Value

    Initial value: 0

    wwWebDataGrid::PagerColumnAttributes


    Any custom attributes you might need to set on the page column.

    wwWebDataGrid::PagerCssClass


    The CSS Class used in the Pager row of the table.

    Note that the main pager's css class is .gridpager. This class is configurable by this property.

    There are additional non-configurable styles that exist for the selected pager page, and the individual pager pages which are the a tags:

    These styles cannot be specified via properties, but they can be modified via customized CSS styles either in the current page or in the main stylesheet.

    Default Value

    Initial value: gridpager

    wwWebDataGrid::PagerText


    The text displayed before the actual page display. Defaults to Pages:.

    o.PagerText                                                  
    

    wwWebDataGrid::PageSize


    Size of the Page to display. If non-zero causes the DataGrid to add a Pager band to the bottom of the list that allows the user to select a different page.

    Default Value

    Initial value: 0

    wwWebDataGrid::RowContent


    Can be assigned to inside of the
    RowRender event to completely override rendering of an individual row. If set the full row including the <tr></tr> tags must be rendered in the text assigned to this property. This value is cleared before every call to the RowRender event.

    If this property is set with a string value all other rendering for this row does not occur.

    o.RowContent                                                 
    

    wwWebDataGrid::RowAttributeString


    A complete attribute string that can be applied to the <TR> tag of the data items.

    This method is very useful if you override the RowRender event, to allow custom coloring of rows for example by assigning custom CSS classes or background colors.

    FUNCTION gdLinksByDay_RowRender *** Grab the current DataItem from cursor *** DataGrid DataSource Cursor will be in scope here lvValue = Entered *** Check last date - if changed flip the alternate *** switch which tells us to change background color IF this.dLastDate # lvValue this.lAlternate = !this.lAlternate ENDIF *** Create the ItemAttributeString IF THIS.lAlternate lcAttributes = "class='gridalternate'" ELSE lcAttributes = "class=''" ENDIF this.gdLinksByDay.RowAttributeString = lcAttributes this.dLastDate = lvValue ENDFUNC


    o.RowAttributeString                                         
    

    wwWebDataGrid::SortColumn


    Internal property used to keep track which column name is currently sorted. The value is the name of the column.

    Remarks

    Sorting in the datagrid requires that FoxPro cursors are not filtered views that can be generated by some SELECT statements. To ensure output of a query to a table use the NOFILTER clause on the SQL statement.

    wwWebDataGrid::SortOrder


    Returns the current direction of the Sorting that might be active. Either empty or DESC.

    Remarks

    Sorting in the datagrid requires that FoxPro cursors are not filtered views that can be generated by some SELECT statements. To ensure output of a query to a table use the NOFILTER clause on the SQL statement.

    Class wwWebDropDownList


    The wwWebDropDownlist creates a standard Web dropdown list.

    The list supports both DataSource binding for it's list content and ControlSource binding for the SelectedValue property by default.

    Please refer to the wwWebListControl class reference for control behavior. The only difference between the base functionality is pre-configuration for a single line (DropDown) display.

    Custom
      wwWebControl
        wwWebListControl
          wwWebDropDownList

    Class Members

    Requirements


    Class wwWebListBox


    The wwWebListBox creates a standard Web ListBox.

    The list supports both DataSource binding for it's list content and ControlSource binding for the SelectedValue property by default.

    Please refer to the wwWebListControl class reference for control behavior.

    Custom
      wwWebControl
        wwWebListControl
          wwWebListBox

    Class Members

    Requirements


    Class wwWebListControl


    The wwWebListControl class provides the base behavior for the ListBox and DropDownList controls. It implements nearly all of the features of both controls.

    The list supports both DataSource binding for it's list content and ControlSource binding for the SelectedValue property by default.



    Custom
      
    wwWebControl
        wwWebListControl

    Class Members

    MemberDescription
    Change Fires if AutoPostBack is set to .T. and you select a new item in the list.
    AddControl Allows adding of wwWebListItem controls.
    o.AddControl(loCtl)
    AddItem Adds an item to the list control manually.
    o.AddItem(lcText,lcValue,llSelected)
    ClearItems Clears all the items manually added to the control. Removes all items and clears the DataSource.
    o.ClearItems()
    DataBind DataBind on a list control performs ControlSource binding only. List based binding occurs at Render() time.
    o.DataBind()
    DataSource DataSource used to bind the list items to. The datasource must be a FoxPro cursor to table.
    DataTextField The field used for displaying the text of the list item.
    DataValueField The cursor/table field used for the value of the list item. If not provided the DataTextField value is used for the value.
    FirstItemText The FirstItemText property allows adding an item to the list at the very beginning.
    FirstItemValue The FirstItemValue property allows adding an item to the list at the very beginning and set its value.
    Items Collection of simple items which is are simply name value pairs of Text and Value.
    LastItemText Adds a last item and sets its text.
    LastItemValue Adds a last item and sets its value.
    ListMode Determines how this list is displayed either in DropDown or List mode.
    SelectedValue The text value of the currently selected item in the list control.
    SelectedValues A collection of selected values when the control is set to multiselect mode.
    SelectionMode Selection mode for listboxes and dropdowns which is either Single or Multiple.

    Requirements


    Using List Binding with Code


    List controls support databinding both as List binding and single DataSource binding as the following example demonstrates. The code below binds the list to a query result cursor called TStates and binds the SelectedValue to a business object property called State:

    *** Use bus object to retrieve State List this.oLookups = NEWOBJECT("wwLookups","wwDevRegistry") *** Retrieve TStates Cursor lnResult = this.oLookups.GetStates() this.txtState.FirstItemText = "*** Please enter a State" this.txtState.DataSource = "TStates" this.txtState.DataTextField = "State" this.txtState.DataValueField = "StateCode" *** Bind the ControlSource *** Effectively sets the SelectedValue from the bus obj state this.txtState.ControlSource = "this.Page.oEntry.oData.State" *** Bind the input controls only on the first hit IF !this.IsPostBack *** Bind all the single field controls THIS.DataBind() ENDIF

    The DataSource binding in this example binds the Listbox with the content to display in the dropdown for list binding and binds the SelectedValue to the State field in the business object.

    Note that you'll want to always bind the list, but you'll only want to bind the

    Using Control Source binding for the List


    Control Source binding works like any other control like the TextBox, CheckBox or Label. In ControlSource binding the SelectedValue property is bound to a data source. So you can bind to any expression that can be two-way bound which typically will be a database field of an open table or cursor, or a property of a business object or really any object whatsoever.

    this.txtState.ControlSource = this.Page.oDeveloper.oData.StateCode


    Class wwWebListItem


    ListItem object used to add items to a List Control. Used primarily as an internal object to read listitems from script pages.

    Script Code:

    <ww:wwWebDropDownList ID="lstColors" runat="server" Width="200"> <asp:ListItem>Reddish</asp:ListItem> <asp:ListItem Value="Blue">Blueish</asp:ListItem> <asp:ListItem Value="Green">Greenish</asp:ListItem> </ww:wwWebDropDownList>

    Fox Code:

    THIS.lstColors = CREATEOBJECT("wwwebdropdownlist",THIS) THIS.lstColors.Id = "lstColors" THIS.lstColors.Width = [200] THIS.AddControl(THIS.lstColors) _1N0116E4D = CREATEOBJECT("wwWeblistitem",THIS) _1N0116E4D.Id = "_1N0116E4D" _1N0116E4D.Value = [Blue] _1N0116E4D.Text = [Blueish] _1N0116E4D.UniqueId = [lstColors__1N0116E4D] THIS.lstColors.AddControl(_1N0116E4D) _1N0116E4F = CREATEOBJECT("wwWeblistitem",THIS) _1N0116E4F.Id = "_1N0116E4F" _1N0116E4F.Value = [Green] _1N0116E4F.Text = [Greenish] _1N0116E4F.UniqueId = [lstColors__1N0116E4F] THIS.lstColors.AddControl(_1N0116E4F)

    Class Members

    MemberDescription
    Selected Determines whether the item is selected.
    Text The text of the list item.
    Value The value of the List Item. If not set the item uses Text.

    Requirements


    wwWebListItem::Selected


    Determines whether the item is selected.

    o.Selected                                                   
    

    wwWebListItem::Text


    The text of the list item.

    o.Text                                                       
    

    wwWebListItem::Value


    The value of the List Item. If not set the item uses Text.

    o.Value                                                      
    

    wwWebListControl::Change


    Fires if AutoPostBack is set to .T. and you select a new item in the list.

    A typical setup looks like this in the Page (using a wwWebListbox concrete instance):

    <ww:wwWebListBox runat="server" id="lstStates" AutoPostBack="True" Change="lstStates_Change" />

    On the server you then implement:

    FUNCTION lstStates_Change() this.lblSelected.Text = this.lstStates.SelectedIndex ENDFUNC


    o.Change()
    

    wwWebListControl::AddControl


    Allows adding of wwWebListItem controls.

    Used by the script manager to add ListItems from Script Pages.

    o.AddControl(loCtl)
    

    Parameters

    loCtl
    A wwWebListItem object.

    wwWebListControl::AddItem


    Adds an item to the list control manually.

    If the control has a Cursor DataSource set the Added items are appended at the end.

    o.AddItem(lcText,lcValue,llSelected)                      
    

    Parameters

    lcText
    The text of the item to display

    lcValue
    The value of the item to display. If not provided will be the same as the text.

    llSelected
    Determines if the item is selected.

    wwWebListControl::ClearItems


    Clears all the items manually added to the control. Removes all items and clears the DataSource.

    o.ClearItems()                                            
    

    wwWebListControl::DataBind


    DataBind on a list control performs ControlSource binding only. List based binding occurs at Render() time.

    o.DataBind()
    

    wwWebListControl::DataSource


    DataSource used to bind the list items to. The datasource must be a FoxPro cursor to table.

    wwWebListControl::DataTextField


    The field used for displaying the text of the list item.

    wwWebListControl::DataValueField


    The cursor/table field used for the value of the list item. If not provided the DataTextField value is used for the value.

    wwWebListControl::FirstItemText


    The FirstItemText property allows adding an item to the list at the very beginning.

    This is very useful for common list scenarios where you need to have things like:

    *** Please select a State

    as a first item.

    wwWebListControl::FirstItemValue


    The FirstItemValue property allows adding an item to the list at the very beginning and set its value.

    This is very useful for common list scenarios where you need to have things like:

    *** Please select a State

    as a first item.

    wwWebListControl::Items


    Collection of simple items which is are simply name value pairs of Text and Value.


    Default Value

    Initial value: null

    wwWebListControl::LastItemText


    Adds a last item and sets its text.

    wwWebListControl::LastItemValue


    Adds a last item and sets its value.

    wwWebListControl::ListMode


    Determines how this list is displayed either in DropDown or List mode.

    Integer values are used:
    1 - DropDown
    2 - List

    Default Value

    Initial value: 1

    wwWebListControl::SelectedValue


    The text value of the currently selected item in the list control.



    wwWebListControl::SelectedValues


    A collection of selected values when the control is set to multiselect mode.

    You can check for the number of selected values like this:

    IF this.lstChoices.Count > 0 FOR lnX = 1 to this.lstChoices.Count lcOutput = lcOutput + this.lstChoices.SelectedValues.Item(lnX) + "<hr>" ENDFOR this.lblMessage.Text = lcOutput ENDIF

    Note that SelectedValues is used only if the control is in MultiSelect mode. Otherwise use SelectedValue to retrieve selection status. SelectedValue is always set even on multiple selections in which case the first value is the SelectedValue.

    Default Value

    Initial value: null

    wwWebListControl::SelectionMode


    Selection mode for listboxes and dropdowns which is either Single or Multiple.

    Possible values:


    Default Value

    Initial value: Single

    Class wwWebRadioButtonList


    Provides a list of radio buttons that act as a single control value. Making a selection on this control causes the SelectedValue property to be set.

    <ww:wwWebRadioButtonList ID="Colors" runat="server" BackColor="#FFFF80" Width="558px"> <asp:ListItem value="Red" Enabled="False">Red Item</asp:ListItem> <asp:ListItem Value="Green">Green Entry</asp:ListItem> <asp:ListItem Value="Brown">Brown Entry</asp:ListItem> </ww:wwWebRadioButtonList>



    Custom
      
    wwWebControl
        wwWebListControl
          wwWebRadioButtonList

    Class Members

    MemberDescription
    SelectedValue Holds the selected value of this control.
    o.SelectedValue
    RepeatDirection Determines the direction of the radio buttons - Vertical or Horizontal .

    Requirements

    Assembly: webcontrols.prg

    How the wwWebRadioButtonList works


    The wwWebRadioButtonList control is a list control that contains a list of radio buttons. Note though the child radio buttons are not RadioButton objects but rather wwWebListItems.

    Retrieving SelectedValue
    The control returns its selected value via the SelectedValue property. This property is set during PostBack and automatically reflects the last selections.

    To set the SelectedValue you can either assign to the SelectedValue property or set the Selected property of the List item either in the markup/designer or via code.

    List DataBinding
    The control supports both manual adding of controls or data bound binding.

    To manually bind (which is also what happens with markup):

    loCtl = this.RadioButtonList loItem = CREATE("wwWebListItem") loItem.Text = "Red Item" loItem.Value = "Red" loItem.Selected = .T. loCtl.Items.Add(loItem.Value, loItem)

    Alternately you can bind to a data source:

    loCtl = this.RadioButtonList Select color, colorName from Colors into TColors loCtl.DataSource = "TColors" loCtl.DataTextField = "ColorName" loCtl.DataValueField = "Color"


    wwWebRadioButtonList::SelectedValue


    Holds the selected value of this control.

    The value is set on Postback for reading.

    If you need to set the selected value for the control use SelectedValue only with databound results. For manual markup or manually added properties you should set the selected property on the individual item.

    o.SelectedValue                                       
    

    wwWebRadioButtonList::RepeatDirection


    Determines the direction of the radio buttons - Vertical or Horizontal .

    Vertical buttons are rendered as a table - horizontal buttons are rendered as elements one after the other using standard flow.

    Default Value

    Initial value: vertical

    Class wwWebRepeater


    The WebRepeater class provides a template driven repeatable display bound to a datasource. The repeater supports Cursors, Object Arrays and Object Collections for databinding.

    Repeaters work by using templates for displaying content. It has an ItemTemplate, Alternating Item template footer and header templates which can in turn contain other controls, expressions and literal text.

    <ww:wwWebRepeater runat="server" id="repItems" ItemChanged="repItems_ItemChanged" DataSource="tt_cust"> <HeaderTemplate> <h1>Customer List</h1> </HeaderTemplate> <ItemTemplate> Company: <ww:wwWebLabel runat="server" Text="hello" ControlSource="tt_cust.company"></ww:wwWebLabel> Name: <%= tt_cust.Careof %> <hr> </ItemTemplate> <FooterTemplate> <small>generated at: <%= DateTime() %></small> </FooterTemplate> </ww:wwWebRepeater>

    Note:
    In order for the ControlSource binding to work you have to call DataBind() on the repeater either explicitly or through the Page's DataBind() method. <%= Expression %> binding always works regardless of whether DataBind() was called or not.

    Custom
      
    wwWebControl
        wwWebRepeater

    Class Members

    MemberDescription
    ItemChanged Event that fires just after a new item has been loaded. You can hook this event to affect processing that occurs just before the data item is rendered.
    ItemDataBind Event that is fired when an each item is databound in the ItemTemplate.
    ItemPreRender Called just before a data item is rendered. This event occurs after control state - if any - has been restored
    ActiveItemTemplate The currently active Item template instance while iterating over items. You can use this object in ItemDataBind() to grab the current template object if necessary.
    DataSource The DataSource that is bound to this control. Can be a Cursor, Array of objects or a collection of Objects.
    DataSourceType 1 - Cursor/Table
    2 - One Dimensional Object Array
    3 - Collection of Objects
    IsAlternatingItem Flag that determines if during rendering an alternating item is active.
    MaxItems Maximum number of items to display in this list. 0 means display all.
    NoDataHtml Text string/html to display if there's no data to display, ie. the datasource contains no data.

    Requirements


    How the wwWebRepeater works


    The wwWebRepeater control provides a template driven data display mechanism. You basically create a template that is repeated for each item in the datasource assigned to the Repeater. The content of the template(s) can contain HTML or other Web Controls to provide dynamic data to display.

    There are two templates that can be used to render the repeating content: ItemTemplate and AlternatingItemTemplate and there are HeaderTemplate and FooterTemplate that can be used to render respective header and footer values.

    Template content can contain both static HTML markup or controls. The content of the template is rendered for each iteration of the DataSource control.

    Here's an example of a template:

    <ww:wwWebRepeater ID="repLatestItems" runat="server"> <HeaderTemplate> <div class="gridheader"> Web Log Entries </div> </HeaderTemplate> <ItemTemplate> <b><ww:wwWebHyperLink ID="hypTitle" runat="server" ControlSource="TEntries2.Title" UrlControlSource="'NewEntry.blog?id=' + TRANS(pk)"></ww:wwWebHyperLink></b><br /> <small><i> <ww:wwWebLabel ID="lblEntered" runat='server' ControlSource="TEntries2.Entered" ControlSourceFormat="=TimeToC"> </ww:wwWebLabel></i></small><br /> <% if TEntries2.Active %> <b>Active</b> <% endif %> <small><%= TextAbstract(StripHtml(TEntries2.body),150) %></small> <br /> <p /> </ItemTemplate> <AlternatingItemTemplate> <div class="gridalternate"> <b><ww:wwWebHyperLink ID="hypTitle" runat="server" ControlSource="TEntries2.Title" UrlControlSource="'NewEntry.blog?id=' + TRANS(pk)"></ww:wwWebHyperLink></b><br /> <small><i> <ww:wwWebLabel ID="lblEntered" runat='server' ControlSource="TEntries2.Entered" ControlSourceFormat="=TimeToC"> </ww:wwWebLabel></i></small><br /> <% if TEntries2.Active %> <b>Active</b> <% endif %> <small><%= TextAbstract(StripHtml(TEntries2.body),150) %></small> <br /> <p /> </div> </AlternatingItemTemplate> </ww:wwWebRepeater>

    The repeater works with a set of template controls. It supports HeaderTemplate, ItemTemplate, AlternatingItemTemplate and FooterTemplate. These templates can contain other literal content or additional Web Connection controls. When the control is rendered Web Connection renders each of the templates, binding each of the templates on every item that is currently active. The headers and footers bind once - at the beginning and end respectively - and the ItemTemplates render for each data item the Repeater is bound to.

    Notice that there's static HTML, a HyperLink control, some Web Connection Web Controls and as an evaluated expression (<%= %>). You can even use an IF conditional expression. All of them work in a repeater template.

    In this example the Repeater is bound to a cursor - TEntries2 and the template is referencing this cursor and its fields.

    SELECT TOP 20 Title,Body,Entered,pk FROM Blog_Entries ; WHERE Active INTO CURSOR TEntries2 ; ORDER BY Entered DESC * this.repLatestItems.DataSourceType = 1 && This is the default this.repLatestItems.DataSource = "TEntries2" this.repLatestItems.DataBind() && Hooks up template binding

    You can bind directly by using expressions or by using a control and assigning a ControlSource. Web Connection will DataBind each of the controls as they are loaded.

    You can also bind to arrays and collections as long as they contain objects for the data members. In that scenario instead of the cursor name you get a DataItem object that you can reference for the currently active item.

    Here's a contrived example using a collection of objects:

    SELECT * FROM tt_cust INTO CURSOR TQuery this.oCol = CREATEOBJECT("Collection") SCAN SCATTER NAME oData MEMO this.oCol.Add( oData ) ENDSCAN this.repCustData.DataSource = "this.Page.oCol" this.repCustData.DataSourceType = 3 this.DataBind()

    The template might look like this:

    <ww:wwWebRepeater ID="repCustData" runat="server"> <ItemTemplate> <%= DataItem.CareOf %> <%= DataItem.Company %> <hr /> </ItemTemplate> </ww:wwWebRepeater>

    Nesting

    You can also nest another wwWebRepeater inside of a master repeater. Simply define another wwWebRepeater control in the master template to make this happen which can be useful for master detail displays. One tricky thing about this feature is that you will need to somehow feed the child repeater's data source with updated data.

    You can use the ItemChanged event to create code that goes out and requeries a cursor to retrieve detail items that the repeater's DataSource is set to. Alternately you can also embed an <%= %> expression into the page and call a page method:

    <%= this.Page.GetChildCursor() %>

    In the ItemChanged event handler or the GetChildCursor method defined you'd then run a SELECT to feed the datasource. Note you may have to save and reset the current alias so as to not interfere with the master SCAN loop.

    wwWebRepeater Templates


    The repeater works by using templates that are applied at various stages in the rendering of the control. You can use each of these templates and bind content to them to cause output to be displayed.

    The ItemTemplate is the key template and the only one that is required. The item template is applied for each databound item with each of the templates being explicitly databound so that embedded controls gain access to the current data item (either a cursor's row or an object in the case of an array or collection object).

    The following templates are available:

    <ww:wwWebRepeater ID="repLatestItems" runat="server"> <HeaderTemplate> <div class="gridheader"> Web Log Entries </div> </HeaderTemplate> <ItemTemplate> <b><ww:wwWebHyperLink ID="hypTitle" runat="server" ControlSource="TEntries2.Title" UrlControlSource="'NewEntry.blog?id=' + TRANS(pk)"></ww:wwWebHyperLink></b><br /> <small><i> <ww:wwWebLabel ID="lblEntered" runat='server' ControlSource="TEntries2.Entered" ControlSourceFormat="=TimeToC"> </ww:wwWebLabel></i></small><br /> <% if TEntries2.Active %> <b>Active</b> <% endif %> <small><%= TextAbstract(StripHtml(TEntries2.body),150) %></small> <br /> <p /> </ItemTemplate> <AlternatingItemTemplate> <div class="gridalternate"> <b><ww:wwWebHyperLink ID="hypTitle" runat="server" ControlSource="TEntries2.Title" UrlControlSource="'NewEntry.blog?id=' + TRANS(pk)"></ww:wwWebHyperLink></b><br /> <small><i> <ww:wwWebLabel ID="lblEntered" runat='server' ControlSource="TEntries2.Entered" ControlSourceFormat="=TimeToC"> </ww:wwWebLabel></i></small><br /> <% if TEntries2.Active %> <b>Active</b> <% endif %> <small><%= TextAbstract(StripHtml(TEntries2.body),150) %></small> <br /> <p /> </div> </AlternatingItemTemplate> </ww:wwWebRepeater>



    wwWebRepeater::DataSource


    The DataSource that is bound to this control. Can be a Cursor, Array of objects or a collection of Objects.

    The ItemTemplate sees each DataItem as a DataItem object variable when Array or Cursor data is used.

    <%= DataItem.Company %>

    Cursors can simply use the Cursor.Fieldname or just FieldName syntax:

    <%= TQuery.Company %> <%= Company %>

    wwWebRepeater::DataSourceType


    1 - Cursor/Table
    2 - One Dimensional Object Array
    3 - Collection of Objects

    The ItemTemplate sees each DataItem as a DataItem object when Array or Cursor data is used.

    Default Value

    Initial value: 1

    wwWebRepeater::IsAlternatingItem


    Flag that determines if during rendering an alternating item is active.

    Allows you to create quick switches in the ItemTemplate without having to have a separate AlternatingItemTemplate.

    <ItemTemplate> <div class="<%= iif(THIS.Repeater1.IsAlternatingItem,'gridalternate','gridnormal') %>"> <%= company %><br> <%= Lastname %> - <%= DateEntered %> </div> </ItemTemplate>



    o.IsAlternatingItem                                          
    

    wwWebRepeater::MaxItems


    Maximum number of items to display in this list. 0 means display all.

    Default Value

    Initial value: 0

    wwWebRepeater::NoDataHtml


    Text string/html to display if there's no data to display, ie. the datasource contains no data.

    wwWebRepeater::ItemChanged


    Event that fires just after a new item has been loaded. You can hook this event to affect processing that occurs just before the data item is rendered.

    You can also use this event to set the datasource for nested data items. For example, it's possible to nest another Repeater inside of the repeater to display child items. When you do the child repeater needs a mechanism to load the child data and this event can serve this task.

    If you want similiar behavior but more control over exactly where in the template processing code fires you can also use an embedded expression like this:

    ww:wwWebRepeater runat="server" id="repItems" DataSource="tt_cust"> <ItemTemplate> Start<ww:wwWebLabel runat="server" Text="hello" ControlSource="tt_cust.company"></ww:wwWebLabel> <br /> --- <br /> <%= this.Page.repItems_Changed()" %> <ww:wwWebRepeater runat="server" id="repChildren" DataSource="timebill"> <ItemTemplate> <ww:wwWebLabel ID="WwWebLabel1" runat="server" ControlSource="Titems.enterd"></ww:wwWebLabel> </ItemTemplate> </ww:wwWebRepeater> <hr /> </ItemTemplate> </ww:wwWebRepeater>

    Note that this method has access to a DataItem object when dealing with an Object Array Data source. The DataItem will reflect the current active object.

    o.ItemChanged()                                              
    

    wwWebRepeater::ItemDataBind


    Event that is fired when an each item is databound in the ItemTemplate.

    This method actually fires after general databinding (if any) for the template has been performed and gives the opportunity to override the value with a custom value.

    Here's a contrived example that binds the DateTime to a label in the ItemTemplate:

    FUNCTION GuestList_ItemDataBind() *** Current Template and lblName Item in the template loTemplate = this.GuestList.ItemTemplate *** Now get the control loCtl = loTemplate.FindControl("lblDate") *** Do whatever you need to with it loCtl.Text = TRANS(DateTime()) *** OR Databind it if you didn't data bind the list *oCtl.DataBind() ENDFUNC



    o.ItemDataBind()                                             
    

    Remarks

    This event is ALWAYS fired even if you don't call DataBind() on the repeater! If you need to check whether DataBinding is active or not, you can check wwWebRepeater.lDataBindCalled (ie. GuestList.lDataBindCalled in the example above).

    This method has access to a DataItem object when dealing with an Object Array Data source. The DataItem will reflect the current active object.

    wwWebRepeater.ItemPreRender


    Called just before a data item is rendered. This event occurs after control state - if any - has been restored

    o.wwWebRepeater.ItemPreRender()                                             
    

    wwWebRepeater::ActiveItemTemplate


    The currently active Item template instance while iterating over items. You can use this object in ItemDataBind() to grab the current template object if necessary.

    o.ActiveItemTemplate                                         
    

    Extended Controls


    Extended controls are a few special purpose controls that are included with the Control Framework. These are probably not frequently used but can come in handy in many situations.

    ClassDescription
      wwWebFileUpload Displays a File Upload control on the form, which allows selection of a file from the local file system and uploading it to the Web Server.
      wwWebHtmlEditor Provides a very basic rich HTML Edit control for Internet Explorer only. For any other browser it displays a text box instead.
      wwWebLogin The wwWebLogin control provides both a visual login control as well as the ability to authenticate users directly a