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:
*** Action required: Check and ensure your Web Service doesn't require explict type returns. If it does add: this.oSOAP.lIncludeTypes = .t. to Init() of the service to get old behavior
*** Action required: If you set the lUseErrorMethodErrorHandling flag in your code you will have to adjust this flag and move the code to your server's OnInit method or as a property override setting in the class definition of your wwServer subclass.
*** ACTION REQUIRED:
To take advantage of this feature make sure that ZLib1.dll gets deployed on the server with your application.
*** ACTION REQUIRED:
Make sure you update both wwIPStuff.dll and wwIPStuff.vcx in your applications. There was a change in the DLL to provide this feature. The new code will not run with the old DLL, but old code will work with the new DLL.
Behavior change: The new behavior is the default and it might cause some problems with some existing scripts especially if you manipulated headers. wwVFPScript is no longer loaded by default. To switch back to the old behavior set the WWC_SCRIPT_PROCESS setting to wwVfpScript and WWC_LOAD_WWVFPSCRIPT .T. in wconnect.h/wconnect_override.h.
wwWebAjax Control added
There's new AJAX control that is part of the Web Control Framework that provides very easy AJAX integration into existing applications. The control supports hover window popups from URL content, very easy URL callback support and a simplified Page Method Callback Mechanism that makes it super easy to callback on the current server page and return values using a simple RPC style interface.
wwUtils.File2Var reads and writes in shared mode
File2Var now always reads in shared mode meaning that files can be accessed at the same time. This might improve performance a little with template reads and the like. For writing there's an optional flag that allows shared writes as well although you should be very careful with this option and only use it in special situations. Unlike STRTOFILE and FILETOSTR(), File2Var doesn't cause a FoxPro error but instead returns a blank or .T. and .F. respectively.
wwRequest::lUtf8Encoding
Added a flag that allows retrieving all request data in UTF8 format. When set both QueryString and Form variable retrievals are UTF-8 decoded into the current character set (STRCONV(,11)). This is required for any client page that posts data in UTF 8 format - such as any request data encoded using JavaScript encode or encdeUriComponent functions.
This change greatly simplifies COM configuration and allows Web Connection in most cases to run in COM mode without any additional configuration beyond COM registration with /regserver. The change also mimizes and simplifies the security impact and gives Administrators control over the security environment that the Web Connection application runs under.
The new operation is implemented by default but a new Impersonation switch in wc.ini allows switching back to the old style of Impersonation operation.
For current pricing for both developer and runtime licenses please check our product pricing page.
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.
The topics in this tree describe key changes in the framework that will affect existing applications.
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)
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.
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
The typical flow of a request now is:
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
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.
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.
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
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:
and then re-run the demo or menu option.CLEAR ALL CLOSE ALL SET CLASSLIB TO
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
However, you can stil run any Web Connection application simply by executing the main PRG file (ie. DO webDemoMain.prg).
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.
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
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
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.
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.
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.).
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.
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:
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.
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.
The message board provides the following features:
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 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.
http://www.west-wind.com/wwDevRegistry/
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.
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.
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.
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.
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!
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.
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.
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.
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.
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.
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.
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.
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:
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
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.
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.
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.
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.
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.
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.
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.
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.
If you do this manually when you start up next time follow one of these steps:
Note
If you have VFP configured to start up in a specific directory in your VFP Tools|Options settings the shortcut will not do the trick. In that case follow the manual instructions for startup. You can verify your startup path by typingCDinto the command window - it should display the Web Connection Install path.
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.
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.
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.
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,
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.
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.
This section describes several possible problem scenarios.
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.
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.

Usually this latter setting is not required, especially if you are installing into the server's Web application tree (ie. \inetpub\wwwroot) because this tree has automatically enabled the Anonymous Web account. If you install outside of this tree however you're responsible for setting the permissions.
The Web Connection Setup, New Project, New Process and Configuration Wizards all automatically apply these settings for IIS.
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:
Without this setting Apache returns invalid script map path information which the above class fixes.DEFINE CLASS wcDemoServer AS WWC_SERVER OLEPUBLIC cRequestClass = "wwApacheRequest" ...
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.
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
Creating a virtual directory
If the directory exists below the Web root, you may not be able to create the virtual through the Wizard as the MMC sees a naming conflict with the existing directory. In this case simply select the directory from the Web site's directory listing and right click, then click Properties. Go down to the Application Settings setting and click on the Create button to create the virtual directory. As above check the Execute Permission to Scripts and Executables if you have wc.dll in this directory, otherwise set it to Scripts.

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.
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
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.
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:

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:
Next you go to or create your virtual directory for your your application and select the West Wind Web Connection Pool.
Make sure that 'Verify that file exists' is not checked!

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.
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.
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.
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.
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.

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

The extension needs to be explicitly enabled:

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:
Script maps can be configured in the service manager as follows:


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.
Here's a quick review of the issues involved:
%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:
The flag is set in the Application Pool Settings Manager with the Advanced Options:

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 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.
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.
The Wizards will handle the following:
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"
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.
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
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.
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
Check with your Apache Administrator for more information on setting up Web Server authentication for requests.
*** Load the Web Connection class libraries
DO WCONNECT
*** Load the server - wcDemoServer defined below
goWCServer=CREATE("wcDemoServer")
*** If running a CGI Web server uncomment the following line
goWCServer.oRequest = CreateObject("wwShellCGI")
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:
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.
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.
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>
The VS Web Server can also be run without Visual Studio actually running optionally.
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.
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.
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.
To work around this make sure you set the AdminAccount key in web.config to blank ("") so no authentication is applied.
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>
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>
public class AppConfiguration : wwAppConfiguration
| Member | Description | |
|---|---|---|
![]() |
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 |
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
public string AdminPage
public bool AutoStartServers
public ComServerLoadingModes ComServerLoadingMode
public string ComServerProgId
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
public bool LogDetail
public string MessageDisplayFooter
File Com
public MessagingMechanisms MessagingMechanism
0 - means no checks are performed. Note that ASP.NET settings may override
public int PostBufferLimit
public int ServerCount
public string TempFilePrefix
public string TempPath
public int Timeout
public string UpdateFile
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
| Member | Description | |
|---|---|---|
![]() |
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 |
public string Busy
Exception is also the 'generic' failure message that is displayed if an internal error occurs in the module processing.
public string Exception
public string InvalidRequestId
public string Maintenance
public string NoOutput
public string OleError
public string PostBufferSize
public string Timeout
public string TranmitFileFailure
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:
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:
Summary:
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:
YourApplication.exe
YourApplication.ini
wwIPStuff.dll
zlib.dll
wwImaging.dll (required only if you use the imaging function in wwAPI)msvcr71.dll (for Apache only. should be copied into the System directory if it doesn't exist)
Console.exe
.\SCRIPTS
.\TEMPLATES
.\TOOLS\DComPersmissions.exe (can go into the main 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!
For application files again copy all files that are related to your project including the executable and data files. In addition copy the following:
This directory needs to be accessible to the SYSTEM account (or whatever account your Web Server is running under) with full access.
wc.dll?_maintain~UpdateExe
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.
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.
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.
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.
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 ...
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:
In IIS to create a virtual directory:
Once you've done this you should see a dialog like this:

To set up the scriptmap:

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:
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!
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.
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.
o=CREATE("WebDemo.WebDemoServer")
? o.ProcessHit("query_string=wwMaint~FastHit")
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.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.
To do this you use the DCOMCNFG utility (part of Component Services in Windows XP/2003 Server and later):

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.
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.
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.
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.
Non Default Configurations 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 ServersFind 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.
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.
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.
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:
etc.DO CONSOLE WITH "DCOM","webDemo.WebDemoServer","IUSR_RASNOTEBOOK" DO CONSOLE WITH "DCOM","webDemo.WebDemoServer","Administrators"
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!
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).
[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.
A recommended starting point for instances is 2 instances per processor. If you have light load operations
If your DCOMCNFG settings get totally screwed up due multiple ProgId entries, you have two things you can do: Create a new project with a new project and class name. Or you can clean up the registry. To clean up the registry search for the ClassName in the registry and wipe out all trees that contain this name. For badly corrupted servers this can be as much as 50 entries that need to be cleaned out. When you wipe out subtrees you want to wipe out the subtree for the AppId and ClassId entries. Be very careful - know what you're doing, and if you don't ask for help.
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.

To trouble shoot a file based installation please check out the Troubleshooting a Filebase Server Installation topic.
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.
The following need to be moved to the server:
YourApplication.exe
YourApplication.ini
wwIPStuff.dll
zlib.dll
wwImaging.dll (required only if you use the imaging function in wwAPI)msvcr71.dll (for Apache only. should be copied into the System directory if it doesn't exist)
Console.exe
.\SCRIPTS
.\TEMPLATES
.\TOOLS\DComPersmissions.exe (can also go into the main directory)
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.
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.
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.
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 nameRunning 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=2Servers 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.
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.
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.
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
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!
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 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) 1 |
|
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 |
|
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
|
|
ServerLoading |
Used only in the Automation Server section this option determines how requests cycle through the loaded servers. 0 -Load Based |
|
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 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. |
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.
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:
[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.
Note that you can hide the first parameter with something like this:
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.
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.
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.
#DEFINE DEBUGMODE .T.
#DEFINE SERVER_IN_DESKTOP .F.
#DEFINE WWXML_USE_VFP_XMLTOCURSOR .F.
#DEFINE WWWC_FILTER_UNSAFECOMMANDS .F.
#DEFINE DEFAULT_HTTP_VERSION "1.0"
#DEFINE DEFAULT_CONTENTTYPE_HEADER ;
"HTTP/1.0 200 OK" + CR + ;
"Content-type: text/html" + CR#DEFINE MAX_STRINGSIZE 10000
#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
#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.
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.
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 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.
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 |
|
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').
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:
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.
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.
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.
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!
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.
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
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
<?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
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.
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:
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.
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.
The figure above demonstrates that you can set breakpoints and step through live Web requests in the VFP debugger.
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.
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.
The DebugMode flag can be set in the following places:
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.
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.
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.
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.
Both mechanisms log a unique RequestId which identifies each request.
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.
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.
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.
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.
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.
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.
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.
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.
o=CREATEOBJECT("test.testserver")
o.ProcessHit("query_string=wwmaint~Fasthit")Make sure you have your pathing properly corrected to adjust for access across the network using UNC pathing. For example, if your data sits on the local C: drive build your data access so it points at: \\YourServer\CROOT\DATA\MyFile rather than accessing the drive letter (unless you can guarantee drive maps - drive maps are significantly faster than UNC names).
o=CREATEOBJECTEX("test.testserver","REMOTESERVER")
? oProcess.GetProcessID(0)
? o.ProcessHit("query_string=wwmaint~Fasthit")This should work assuming your current user account has the DCOM rights described above to access the COM object on the remote server. If there is a problem note the problem: Access denied errors most likely are caused by invalid DCOMCNFG settings. CoCreateInstance errors or other instantiation problems are caused by the server not being registered correctly or a mismatch between the client and server class ids. Make sure the server versions are identical and re-register the server in that case.
[Automation Servers] Server1=wwDemoOle.wcDemoServer Server2=wwDemoOle.wcDemoServer,RemoteServer Server3=wcRemote.wcRemoteServer,OtherRemoteServer
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:
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.
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.

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.
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.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.
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.
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.
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 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:
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.
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.
*** 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.
************************************************************************ * 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.
*** 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.
Using HTML forms then is a mutli-step process:
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.
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.

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.
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.
********************************************************************* 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.
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.
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).
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.
You can find the sample templates and PRG file in the html\devregistry directory of your installation.
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:
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.
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:
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:
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())
RETURNThe 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.
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.
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:
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.
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.
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.
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:
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:
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.
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 NOWAITBoth 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)
#ENDIFActually 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...
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:
************************************************************************
* 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.
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.
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!
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
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.*** 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().
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.
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 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.
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.
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.
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.
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.
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.
You will always create a subclass of this class and override at least three methods: OnInit(), OnLoad() and Process(). The first two are for configuration of the server, while Process serves as an entry point for your code on each request - Process() gets called after the Web server has created a wwRequest object. The New Project Wizard sets up the basic skeleton server class, which you can customize with your own configuration settings.
The class provides a simple interface for file or string based HTML creation. The methods range from the low-level Write() method to the ShowCursor() method that displays an entire cursor with one line of code to the immensely powerful ExpandTemplate() and ExpandScript() which can display HTML containing embedded FoxPro expressions or even entire CodeBlocks (courtesy of Randy Pearson's CodeBlck) right inside of the HTML text. The Response class provides over 30 methods and allows access to the HTTP header for very fine control over output. Support for HTTP Cookies, Authentication, Keep-Alive and Page Caching is all built in. You'll access this object in your code as Response.
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
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.
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.
#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:
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.
/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
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.
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:
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:
* 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).
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
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.
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.
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
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.
Web Connection supports three scripting mechanisms of which the first is a special case:
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
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>
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:
For additional examples of how to use scripting see the scripting samples on the Web Connection Demo page.
<% 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>
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:
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 |
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" )
ENDFUNCThe 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:
[wwDemo] datapath=wwDemo\ htmlpagepath=d:\inetpub\wwwroot\wconnect\
Once you've set this path you can access the template like this:
Response.ExpandTemplate( Server.oConfig.owwDemo.cHTMLPagePath + "CallTemplate.wc" )
Now if you end up moving the project, you simply change the setting in the INI file rather than changing code.
Response.ExpandTemplate( Request.GetPhysicalPath() )
GetPhysicalPath returns a mapped disk path to the scriptpage that may or may not exist based on the URL. IIS provides this info. If we called:
http://localhost/wconnect/CallTemplate.wwd
It would return:
d:\inetpub\wwwroot\wconnect\CallTemplate.wwd
Using this mechanism provides consistency in your applications, because if you do this you're always mapping a Process method to the template/script page, which makes it easy to correlate where the template is fired from.
Note
Be careful with this however if you create pages that transfer control to other Process methods. GetPhysicalPath will be accurate only for the actual request method that maps to the URL, not any transferred method calls. So if CallMethod() call THIS.CallAnotherMethod() to perform tasks, and then it calls into GetPhysicalPath() it'll still return the path to CallMethod.wwd not CallAnotherMethod.wwd. If you do this a lot you'll have to use the Config pathing or use explicit filename overrides.
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:
<%
Response.Buffer = True
Set oServer = Server.CreateObject("ASPWebDemo.WebDemoServer")
oServer.ProcessHit() ' writes its own output!
' Response.Write(oServer.cErrorMsg) && for troubleshooting
%>
A URL to this page looks very much like a standard Web Connection request except that you use wc.asp instead of wc.dll:
/wconnect/wc.asp?webprocess~HelloWorld
<%
Response.Buffer = True
Set oServer = Server.CreateObject("ASPWebDemo.WebDemoServer")
%>
This is some text in an ASP document with some ASP script <%= now %>...
<p>
Following is some text called 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>
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=100Note 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:
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.
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:
- scripts
The Create process and project options require the following subdirectories:
- templates
- scripts
The Setup option requires the following subdirectories:
- scripts
- html
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
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.
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:
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.
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.
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:
You can recompile the project at any time with one of the following:
DO BLD_WebDemo
BUILD EXE WebDemo FROM WebDemo RECOMPILE
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.
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.
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):
o=CREATE("WebDemo.WebDemoServer")
? o.ProcessHit("query_string=wwMaint~FastHit")
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.
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.
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.
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
ENDDEFINEWhen 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).
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.
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.
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.
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:
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.
Once the creation has completed you need to configure Web Connection properly to use the newly created SQL tables.
#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.
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.
#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:
#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.
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:
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.
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.
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.
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.
lcPort =THIS.oRequest.ServerVariables("SERVER_PORT")
IF lcPort # "443"
THIS.oResponse.Redirect( THIS.oRequest.GetRelativeSecureLink(THIS.oRequest.GetCurrentUrl()) )
RETURN
ENDIF
Basic Authentication works against Windows User accounts so users must exist as Windows Accounts in order to be used for 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().
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.
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.
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
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

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.
************************************************************************ * 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.
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.
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:
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
The first step is to understand that Web Connection consists of two components, which both expose administrative functions:
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.
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.
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.
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.
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
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.
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!
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
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.
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.
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.
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.
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.
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)
ENDFUNCInstead 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])
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()
RETURNNote 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()
RETURNThe 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.
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.cHTTPHeadersWhat 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).
ContentTypeHeader
HTMLHeader
ExpandTemplate
ExpandScript
Example:
Response.ContentTypeHeader() && Writes HTTP Header
Response.Write("<html>")
...
Response.HTMLHeader("Hello World")
Response.Write("<hr>")
...loHeader = CREATE("wwHTTPHeader")
loHeader.DefaultHeader()
loHeader.AddCookie("wwhome",SYS(2015))
*** To apply the header
Response.Write(loHeader.GetOutput())
Response.Write("<html>")
...
You can also pass an HTTP header object directly to one of the Response methods above that normally create an header automatically. Instead of sending the header output directly into the HTTP stream, pass the loHeader object as a parameter:
Response.ContentTypeHeader(loHeader)
Response.Write("<html>")or
Response.ExpandTemplate("somepage.wc",loHeader)
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.
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.
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.
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
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)
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.
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.
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.
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.
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).
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.
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 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.
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.
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.
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
HelloWorld.wp?Id=Test
and have it map to your application and the Helloworld method for example.

Make sure that the 'Check that file exists' checkbox is unchecked!
Make sure that you have the 'Check that File exists' checkbox unchecked unless you absolutely want require that every request has a page associated with it! You also need to set this flag if you want to use NTLM Directory and File Security with pages.Windows XP Bug: OK button disabled
If your OK button is disabled in XP, ensure that you have a . before the extension name (ie. .wp). If the OK button still doesn't show click on the file's textbox which forces the filename to expand.
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.
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.
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.
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.
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.
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.
With Web Connection you can concentrate on writing your application code, not figuring out how to perform Web tasks.
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.
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.
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
| Member | Description | |
|---|---|---|
![]() |
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. |
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().
*** 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
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.
*** 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
Make sure to call DODEFAULT() to call the base class code.
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)
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)
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.
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()
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)
llFile
If .T. the first parameter passed was a full filename which is read
and converted to a string.
<%
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.
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)
o.cAppIniFile
o.cAppName
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
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.
Default: /wconnect/wc.dll/maintain?release
o.cCOMReleaseURL
o.cErrorMsg
o.cLogFile
o.cOutput
o.cRequestClass
o.lASPObject
o.lShowRequestData
The internal sequence to set cAppStartPath is:
o.lUseRegistryForStartupPath
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
Presumably this property will only be used for legacy applications.
o.lComObject
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
o.lError
This property is set when the server starts up from the startup ini
file using the LogToFile setting in the [Main] section.
o.lLogToFile
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
The following settings can be applied to this flag:
This property is set when the server starts up from the startup ini file using the ScriptMode setting in the [Main] section.
o.nScriptMode
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
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
o.nWebControlFrameworkCompileMode
Default Value: 2
o.ASPCallRequest(lcClass,lcMethod)
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
<%
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>
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()
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.
o.LogRequest(lcParameter,lcRemoteAddress, lnSeconds, llError)
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.
Please see the MTSSetComplete() topic for details on how to Set up for automatic MTS
transaction support.
o.MTSSetAbort()
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()
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.
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)
Uses the Settings object in Settings.prg
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)
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 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
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()
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)
lcLocale
Optional - The explicit numeric or string LocaleId to force the Locale to.
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.
*** 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
o.SetLogging(llLoging)
o.SetTimerInterval(lnInterval)
This value can be optionally used with automatic format conversion based on the browser language used.
o.cDefaultLocale
o.cDLLFileName
o.cTempFilePath
o.lShowServerForm
You can also remove the server form completely with the ShowServerForm() method and passing a parameter of .F.
o.lShowStatus
This flag has to be read from the startup ini file using the UseMTS key.
o.lUseMTS
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
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.
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() %>
| Member | Description | |
|---|---|---|
![]() |
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. |
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
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
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.
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.
The request fires and if not authenticates pops up a Windows Authentication box. You put in your username a
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)
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.
*** 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
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:
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)
lcFont
The font used for the CAPTCHA generated
lnFontSize
The font size of the CAPTCHA
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()
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)
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)
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.
o.GetAppSetting(lcKey,lcSection)
lcSection
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).
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()
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)
lcSection
The section in the wc.ini file to retrieve. Defaults to the wwcgi section which is the main section.
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:
FUNCTION Process
THIS.InitSession("wwDemo")
DODEFAULT()
RETURNFUNCTION 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)
RETURNTypically 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)
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.
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.
o.LogError(loException)
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()
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()
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)
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.
This method uses wwIPStuff::SendMailAsync behind the scenes.
o.StandardPage(lcHeader, lcBody, lvHeader, lnRefresh, lcRefreshUrl)
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.
o.cAuthenticationMode
o.cAuthenticatedUser
o.cAuthenticationUserSecurityClass
o.cAuthenticationUserMessage
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
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
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
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
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
Only applies if the content type of the request is text/html.
o.lShowRequestData
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 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.
The new default is 2.
o.nPageScriptMode
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
The default Process method also exposes this object as a PRIVATE variable Request.
o.oRequest
The default Process method also exposes this object as a PRIVATE variable Response.
o.oResponse
The default Process method also exposes this object as a PRIVATE variable Server.
o.oServer
o.oSession
FUNCTION OnLoad() IF !Process.Authenticate() RETURN ENDIF this.lblMessage.Text = this.cAuthenticatedUser + " " + ; this.oUserSecurity.oUser.Fullname) ENDFUNC
o.oUserSecurity
| Member | Description | |
|---|---|---|
![]() |
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. |
It allows access to:
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.
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.
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.
o.aFormVars(@laVars,lcPrefix)
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.
You can check the request data on the server status form to see what you're actually getting.
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()
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)
Requires wwIPStuff.dll for large variables to be decoded.
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()
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)
lcTempPath
This is required only for file based operation, which needs to know where the HTML output needs to be written to.
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.
o.IsFormVar(lcKey)
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)
http://localhost/wc.wc?wwDemo~TestPage~&Company=West+Wind&Name=Rick
where Company and Name would be key values to retrieve.
Make sure when mixing positional and named parameter that you separate the two with a ~ and & as the link above does.
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)
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)
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)
lcValue
The value to set the variable to
lvReserved
not used provided purely for compatibility
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)
lcPrefix
An optional prefix that can be used in form variables. This prefix is stripped when matching property values.
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.
*** 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
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()
This method can check both Basic and Windows Auth values either seperately or combined.
o.getauthenticateduser(lnMode)
0 - Basic Auth then Windows Auth
1 - Basic Auth
2 - Windows Auth
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.
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
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()
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)
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.
*** 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)
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)
http://www.west-wind.com/wconect/noceode.wc
/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()
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)
Multi-select values can come from Multi-select lists and drop downs or from radio buttons.
o.getformmultiple(@taVars,tcVarname)
tcVarname
The name of the form variable to retrieve.
DIMENSION laVars[1] lnVars=Request.GetFormMultiple(@laVars,"LastName") FOR x=1 to lnVars Response.Write( laVars[1] + "<BR>") ENDFOR
o.getipaddress()
http://www.west-wind.com/wconect/wc.dll?wwDemo~TestPage
/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()
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)
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)
@lcFileName
Optional - a string that will be filled with the file's name. must be passed by reference. Initial value is ignored.
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 )
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)
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 )
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()
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()
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.
o.getpreviousurl()
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)
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.
o.getservername()
o.getserversoftware()
o.getwcinivalue(lcKey, lcSection)
lcSection
Optional - the section to retrieve the value from. The default is the [Main] section.
o.IsLinkSecure(lcSecurePort)
This method is a shortcut for:
Request.ServerVariables("REQUEST_METHOD") = "POST"
o.IsPostBack(lcVar)
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)
Request.GetClientCertificate("CN") && Retrieve Common Name
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
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
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):
If you want to retrieve raw data such as a raw XML post, you can do so using the wwRequest::FormXML method.
o.cFormVars
GetConstants [TypeLibFile],[OutputFile],[Silent(.t. or .f.)]
If you pass parameters you can override the interactive behavior above.
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.
The interface to the class is identical to wwRequest with the following exceptions:
(under construction - only basic operation works)
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"
This class should allow existing applictions to use the new Page class functionality without any major changes.cResponseClass = "wwPageRepsonse40"
| Member | Description | |
|---|---|---|
![]() |
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. |
This mechanism works through Basic Authentication which is part of the HTTP
protocol.
o.BasicAuthentication(tcRealm, tcErrorText)
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.
o.AddCacheHeader(lnExpirationSeconds)
o.AddCookie(tcCookie,tcValue,tcPath,tcExpire)
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:
o.AddForceReload()
Essentially the same as Response.Headers.Add(), but this method is consistent with ASP.NET and shows up in Intellisense.
o.AppendHeader(lcHeaderKey,lcHeaderValue)
lcHeaderValue
The value for the header
*** Refresh the page in 2 seconds Response.AppendHeader("Refresh","2; url=/wconnect/weblog/default.blog") *** Add a custom header value Response.AppendHeader("Custom","MyCustomValue")
o.BinaryWrite(lcText)
o.Clear()
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)
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
o.End()
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)
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).
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)
tnMode
Optional - Determines the compilation mode for scripts. Default Mode is 1. For more info see wwServer::nScriptMode.
tvContentType
Optional - Either a wwHTTPHeader object or a content type string.
Response.ExpandScript(THIS.cHTMLPagePath + "scriptdemo.wcs") Response.ExpandScript(Request.GetPhysicalPath()) && Default
<%= 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:
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)
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.
o.FastWrite(lcText)
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)
o.Render()
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)
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.
This method is the lowest level method of the Response object and is used to write output directly into the stream.
o.Write(lcText)
o.HTMLFooter(tcText,tlNoOutPut)
llNoOutPut
When set to .T. output is not sent to file, instead returning the result as
a string.
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.
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
Response.Encoding = "UTF8"
The cookies collection consists of:
1 - Cookie name
2 - the full Cookie string
To set a cookie you should use the AddCookie() method.
-1 can be used to force the content to expire immediately.
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
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.
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.
Examples:
Response.Status = "200 OK" Response.Status = "401 Not Found" Response.Status = "500 Server Error"
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
wwASPResponseThe 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.
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).
Write and its slightly more efficient FastSend companion method are the low level functions, but wwResponse also provides a number of high level features:
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.
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.
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.
| Member | Description | |
|---|---|---|
![]() |
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. |
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)
llNoOutput
When set to .T. output is not sent to file, instead returning the result as a string.
o.FastWrite(lcText,llNotUsed)
llNotUsed
A second parameter for the typical llNoOutput is provided for compatibility, but is ignored.
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)
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()
o.authenticate(lcRealm, lcErrorMsg, llNoOutput)
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.
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.
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
HTTP/1.0 200 OK Content-type: text/html <HTML> ...HTML content here. </HTML>
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)
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..
**** 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
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
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)
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.
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)
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
Response.ExpandTemplate(THIS.cHTMLPagePath + "node.wc")
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)
lvHeader
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.
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)
tnMode
Optional - Determines the compilation mode for scripts. Default Mode is 1. For more info see wwServer::nScriptMode.
tvContentType
Optional - Either a wwHTTPHeader object or a content type string.
Response.ExpandScript(THIS.cHTMLPagePath + "scriptdemo.wcs") Response.ExpandScript(Request.GetPhysicalPath()) && Default
<%= 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.href(lcLink,lcText,llNoOutput)
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.
o.htmlfooter(tcText,tlNoOutPut)
llNoOutPut
When set to .T. output is not sent to file, instead returning the result as a string.
Response.HTMLHeader("Customer Demo Example","West Wind Customer Demo")
Response.Write("Hello World")
Response.HTMLFooter()
o.HTMLHeader(tcHeader,tcTitle,tcBackground,tcContentType,tlNoOutput)
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.
Response.HTMLHeader("Customer Demo Example","West Wind Customer Demo")
Response.Write("Hello World")
Response.HTMLFooter()
There are two ways you can use this method:
Response = THIS.oResponse Response.cStyleSheet = "westwind.css"
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
o.HTMLHeaderEx(lvHTMLHeader, lvHTTPHeader)
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.
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
| Field | Type | Function |
| Field1 | Character | The 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 | Numeric | The data item to graph. |
o.iechart(cChartType,vWidth,cHeight,lNoOutput)
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.
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)
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()
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)
llNoOutput
When set to .T. output is not sent to file, instead returning the result as a string.
oHTML.ShowCursor(@aHeaders, cTitle, lSumNumbers, lNoOutput,cTableTags)
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%".
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.
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")
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)
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.
Response.StandardPage("Welcome","Welcome to the West Wind Web Connection Demo.","FORCE RELOAD")
o.writememo(lcText, llNoOutput)
llNoOutput
When set to .T. output is not sent to file, instead returning the result as a string.
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)
llNoOutput
If .T. the string is returned back and no output is written into the HTTP stream.
o.formbutton(lcName, lcCaption, lcType, lnWidth, lcCustomTags, llNoOutput)
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
o.FormHeader(lcAction, lcMethod, lcTarget, lcExtraTags, llNoOutput)
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
o.formcheckbox(lcName, llValue, lcText, lcCustomTags, llNoOutput)
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.
o.formhidden(lcName, lcValue, llNoOutput)
lcValue
Preselected value of the field
llNoOutput
Return as string
o.formradio(lcName, lcValue, lcText, llSelected, lcCustomTags, llNoOutput)
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
o.formtextarea(lcName, lcValue, lnHeight, lnWidth,lcCustomTags, llNoOutput)
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.
o.formtextbox(lcName, lcValue, lnWidth, lnMaxWidth, lcCustomTags, llNoOutput)
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
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>")
*** 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)
| Member | Description | |
|---|---|---|
![]() |
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. |
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.
Kernel Mode cache kicks in only fires if there is:
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)
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)
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.
*** 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)
o.AddCustom(lcCustomHeader)
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()
Adds a custom HTTP header using a key/value string.
o.addheader(lckey,lcValue)
lcValue
The actual value to set the key to.
This method is called automatically when GetOutput() is called, so typically this method call is not required.
o.AddRequestId()
This mechanism works through Basic Authentication which is part of the HTTP protocol.
o.authenticate(lcRealm, lcErrorText)
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.
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.
o.ClearHeader
o.completeheader()
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()
Note this function only creates the header - it does not actually send the file.
o.FileDownloadHeader(lcFilename,lcContentType,lnFileSize)
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
o.getoutput()
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)
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)
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)
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
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() )| Member | Description | |
|---|---|---|
![]() |
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. |
o.AddJavaScript(lcScript)
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)
lcValue
The value to set the meta tag to.
This method generates:
<META HTTP-EQUIV="Refresh" CONTENT="5; URL=/headertest.wwd?">
o.AddPageRefresh(lcUrl, lnRefresh)
lnRefresh
The refresh interval in seconds. Default: 1 second
o.AddScriptLink(lcUrl, lcLanguage)
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.
Generates the following:
<LINK rel="stylesheet" type="text/css" href="/westwind.css">
o.AddStyleSheet(lcUrl)
o.AddStyleSheetBlock(lcStyleBlock)
o.AddTitle(lcTitle)
o.AddVBScript(lcScript)
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)
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()
o.Init(loHTML)
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() )
o.cDocType
o.cHTMLTag
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.
| Member | Description | |
|---|---|---|
![]() |
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. |
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.
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.
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.
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.
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()
This method should be used sparingly.
o.DeleteSession(lcId)
o.EndSession(lcSessionId)
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)
lcValue = loSession.GetField("Hits")
o.GetSessionId()
o.GetSessionVar(lcVarName)
Set the user with SetUserId() after a call to NewSession().
o.GetUserId()
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
RETURNIf 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)
o.LocateSession(lcID)
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
RETURNNewSession() 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)
o.OpenTable()
o.Reindex(ltPurgeDateTime)
If not passed no entries are deleted, the file is just reindexed and packed.
o.SetField(lcFieldName, lvValue)
lvValue
The value to set the field to.
o.SetSessionID(lcID)
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.
o.SetSessionVar(lcVarName, lcValue)
lcValue
The value to set the session variable to
Set the user with SetUserId() after a call to NewSession().
o.SetUserId(lcUserId)
o.TimeoutSessions(lnSeconds)
Default: .\ (current path)
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
Default: 1800 - 1/2 hour
Note: This property is useful only when calling NewSession() and must be set prior to the call to NewSession.
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.
| Member | Description | |
|---|---|---|
![]() |
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. |
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.
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.
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
Sets the cHeaderString property.
o.BuildFieldListHeader(lvHeader,llPrelist)
llPrelist
If the header is on a <pre> formatted list it's formatted differently.
SELECT company as Customer_company, careof as Customer_name, phone as Phone_number ; INTO CURSOR TQuerywill result in the same headers as in the example above without requiring the creation of an array and calling BuildFieldListHeader.
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()
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)
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.
* 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
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()
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)
o.GetOutput()
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()
* 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
o.SetCursor(lcDBF)
o.ShowASCIIRecord()
o.ShowCursor()
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()
o.ShowObject(loObject)
o.ShowRecord()
o.cAlternatingBGColor
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
Tip:
Use Class or Style tags to force the table to be formatted globally so you don't have to format each cell individually.
o.cHeaderColor
o.cHeaderFont
o.cKeyField
o.cKeyFieldType
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
o.cTableEditFieldList
o.cTableFieldList
Applies only to ShowCursor() invokations and any methods such as EditTable that call ShowCursor internally. It does not apply to ShowRecord() or EditTable().
o.cRecordFieldList
o.cTableSortColumn
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
Note: that this property is overridden depending on the size of the table. If the table gets larger than
o.lSortable
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.
...
SELECT * from tt_CUST into cursor TQuery
loShowCursor = CREATEOBJECT("wwShowCursor")
loShowCursor.cBaseUrl = "ShowCursor.wwd?"
loShowCursor.lSortable = .T.
loShowCursor.ShowCursor()
Response.Write(loSC.Getoutput())
...
o.nForceToPreList
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() )
o.nPage_ShowPage
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()| Member | Description | |
|---|---|---|
![]() |
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 |
o.BuildList()
o.GetOutput()
o.Reset()
loPopup.cAddFirstItem = [All Forums] + CRLF + ; [<option>Messages to you] + CRLF + ; [<option>------------------] + CRLF
o.cAddLastItem
loPopup.cAddLastItem = ; [<option>------------------] + CRLF +; [All Forums] + CRLF + ; [<option>Messages to you] + CRLF
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()
ENDIFTo 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\"
ENDDEFINEThis generates:
[config] CustomProperty=Some value CustomValue=10 [wwdemo] HTMLPagepath=d:\westwind\wconnect\ Datapath=d:\wwapps\wc3\wwdemo\ [wwmaint] ;*** no properties
<?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\
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
| Member | Description | |
|---|---|---|
![]() |
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. |
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()
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()
Supported modes are:
cFileName and/or cRegNode/cRegPath must still be set prior to making the calls to Load/Save.
o.cMode
Example:
SOFTWARE\West Wind Technologies\Config
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
| Member | Description | |
|---|---|---|
![]() |
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. |
3 - Interpreted using CodeBlock
2 - Compiled FXP
1 - VFP interpreted
See the wwVFPScript class and wwResponse::ExpandScript for more info on these values.
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)
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.
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.
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
Category list items are case sensitive
| Member | Description | |
|---|---|---|
![]() |
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. |
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.
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.
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.
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.
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.
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.
o.AddItem(lcKey as String, lcValue as String, lnTimeoutSeconds as Integer)
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.
o.Expire()
o.GetItem(lcKey as String)
o.IsCached(lcKey as String)
The wwCache class uses a cursor, so each instance of Web Connection uses the same
o.Open()
o.Reindex()
o.Remove(lcKey)
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
o.nCacheTimeout
| Member | Description | |
|---|---|---|
![]() |
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. |
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)
lcPassword
The password to validate
o.AuthenticateNt(lcUsername,lcPassword)
lcPassword
Windows Password
o.Close()
o.CreateTable(lcFileName)
o.DeleteUser()
GetUser always sets the oUser member even on failure in which case the value is set to an empty object.
o.GetUser(lcPK, lcPassword)
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
o.GetUserByUsername(lcUsername)
Note only works with Table based security.
o.NewUser()
o.Open(lcFileName, llReOpen, llSilent)
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.
o.Reindex()
o.SaveUser()
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
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:
<ww:wwWebButton runat="server" id="btnSubmit" Text="Go" Click="btnSubmit_Click"/>
All you need to do to handle this event then is implement a FoxPro based btnSubmit_Click method on the Page class of the server where all your implementation code is written:
FUNCTION btnSubmit_Click(loCtl) this.lblMessage.Text = "Hello " + this.txtName.Text + ; ". Time is: " + TRANS(DateTime()) ENDFUNC
It's hard to understand the utility of this mechanism and the affect it has on manageability readability of your code until you have a chance to use it and see how little code you end up writing in your event methods to manage the Web Interface.
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:
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:
<ww:wwWebButton runat="server" id="btnSubmit" Text="Go" Click="btnSubmit_Click"/>
All you need to do to handle this event then is implement a FoxPro based btnSubmit_Click method on the Page class of the server where all your implementation code is written:
FUNCTION btnSubmit_Click(loCtl) this.lblMessage.Text = "Hello " + this.txtName.Text + ; ". Time is: " + TRANS(DateTime()) ENDFUNC
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.
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 + [ ]+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 + [ ] _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 + [ ]+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.
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.
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.
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.
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.
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.
The following rules must be observed:
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:
<asp:TextBox runat="Server"/> <ww:WebTextBox runat="Server"/>
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" %>
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.
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.
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.
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.
The following properties and methods apply:
ControlSource Expressions must be in scope in the context of the control. This means that to reference other controls on the form you need to first reference the form and then the other controls or objects on that form.
A typical ControlSource reference to a business object property might look like this
loCtl.ControlSource = "this.Page.oDeveloper.oData.Name"
ControlSource="oGuest.Entered" ControlSourceFormat="=this.Page.ConvertTimeString"
where ConvertTimeString is a method on the form. Any UDF that takes a single parameter and returns a string can be used. Note: This same scheme can also used in DataGrids for column binding.
This BindingErrors collection can be used to dispaly error indicators next to controls, and display an HTML error message. In conjunction with the wwWebErrorDisplay object it can also provide a rich display of error information. You can also add custom binding errors and there's even a method to store wwBusiness oValidationErrors directly into the BindingErrors collection for fully automated error handling.
ControlSourceFormat
If a ControlSource is set you can also set the format for this ControlSource. Formats are used only when displaying the value. Format can be either a standard FoxPro format expression or if preceeded by an = sign a UDFor function that takes a single input parameter of the value and returns a string. For example you can use:
ControlSource="oGuest.Entered" ControlSourceFormat="=this.Page.ConvertTimeString"
where ConvertTimeString is a method on the form. Any UDF that takes a single parameter and returns a string can be used. Note: This same scheme can also used in DataGrids for column binding.
The most common scenario looks like this:
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.

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.
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.
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.
*** 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"
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
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.
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.
* 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.
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:
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.
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.
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 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.
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
The wwProcess class can run Web Control Pages in two ways:
The script mode of 3 is the default so generally you don't have to worry about that so that the only effective flag is the DebugMode flag. The Debug Mode flag can now be set by:
This mode allows you change pages and have Web Connection immediately regenerate and execute the page without stopping the server. Web Connection behind the scenes manages loading and unloading the page class.
Note that if you have any hanging references on the page Web Connection won't be able to unload the page and will then no longer be able to execute the updated code. It will continue to run the code that was previously loaded. Make sure you don't have hanging references! If you do, you'll have to stop the Web Connection Server, release all objects, then start it back up.
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.
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.
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.
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

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.
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 } }
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:
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.
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
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.
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:
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.
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:
*** Add any data paths - SET DEFAULT has already occurred so this is safe! DO PATH WITH THIS.cAppStartPath + "wwDevRegistry\" DO PATH WITH THIS.cAppStartPath + "devDemo\" *** Add any SET CLASSLIB or SET PROCEDURE code here SET CLASSLIB TO wwDevRegistry ADDITIVE
Now the server is ready for creating our samples.
Next Step: Setting up Visual Studio 2005
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).
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.

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.
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
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
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.
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.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"
Next: Writing the 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.
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.

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.
.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
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.
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.
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.
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.
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:

*** 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.
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.
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.
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
<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
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
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" />
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.
<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:

************************************************************************ * 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
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.
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
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.
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!
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>
This topic tree describes the requirements in some detail, both for manual configuration and auto-configuration through the Web Connection Wizards.
Configuring Visual Studio .NET
Visual Studio Add-in Configuration
The .NET Controls in WebConnectionWebControls.Dll
Creating custom Page and Control Templates
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.
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.
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.

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.
<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:
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:
<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.
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.
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
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.
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.
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>
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.
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.

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.
<script type="text/javascript"> function ScriptCode() { document.getElementById('btnSubmit').click(); } </script>
This fires the click of the button and simply submits it. Easy.
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.
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.
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:
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.
<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.
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.
Firing Server Events from Client Code in Web Connection 5.0
Mixing Client and Server Events in Web Connection 5.0
Business Object vs. Direct DataBase Access
Creating custom Web Connection Web Controls
Creating custom Page Templates for Visual Studio
Web Connection and Visual Studio FAQ
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:
| Class | Description |
|---|---|
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. |
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
| Member | Description | |
|---|---|---|
![]() |
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. |
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.
<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.
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.
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.
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().
o.wwWebControl.AddAttribute(lcKey,lcValue,llAppend)
lcValue
The value to set it to
llAppend
if .T. appends the value to the existing attribute's value if it exists.
o.wwWebControl.AddScriptAttribute(lcScriptingEvent,lcScript,llAppend)
lcScript
The JavaScript code to use
llAppend
Whether the script is replaced (.f. default) or appended (.t.)
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)
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)
lcValue
the value to set it to: green or 2
o.BindControlSource()
Databinding comes in two flavors:
Controlsource binding occurs either by individually calling the control's DataBind() event, or by calling the WebPage's DataBind event which calls into each control's DataBind event in turn. ControlSources can also be unbound - which is the reverse process of taking the control's value and putting it back into the underlying field or property. You use the Unbind method on the control or the WebPage for this task.
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()
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()
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.
*** 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
o.FindControl(lcControlId,llRecursive)
llRecursive
If set recursively drills into child containers to find a control. Default is to only search the current container (.F.).
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)
This method
o.GetCachedOutput()
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)
loHandler
lcMethod
*** 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)
This method is responsible for de-parsing the controls viewstate and assigning properties. Generally you don't need to override this behavior.
o.LoadViewState()
o.OnInit()
When this event fires the page and other controls may not be initialized yet. Use this method only for internal initialization.
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()
This method is called after OnLoadViewState().
Be sure to call DoDefault() especially on container controls!
o.OnLoadControlState()
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()
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()
The final viewstate is collected by the Page class and encoded and written onto the form as a hidden form variable.
o.OnSaveViewState()
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)
*** 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")
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()
The Render method should check for several things:
o.Render()
o.RenderControlError()
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)
Generally you don't need to override this method.
o.SaveViewState()
This is the internal implemenation method
o.UnbindControlSource()
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()
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)
o.WriteCachedOutput(lcOutput)
o.WriteEnabledTags()
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)
o.WriteIdTags()
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)
o.WriteUnitValue(vUnit)
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.
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
Note this property is not used by all controls. Some controls that support it:
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.
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.
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.
Some controls like Panel can while others like the WebDataGrid cannot. Internally used.
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.
This value must be assigned explicitly by the parent rendering the child controls since it holds the master iterator.
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.
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.
o.ControlSourceMode
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.
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
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
<ww:wwWebTextBox runat="server" id="txtName">Rick</ww:/wwWebTextBox>
Rick is mapped to the Text property which is the DefaultProperty for the TextBox.
PostBack controls that are disabled are automatically persisted into ViewState so their values are preserved. Similar behavior to ReadOnly.
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.
Supported values are:
0 - None
1 - Icon to the right
2 - Icon and Text below
3 - Error Text only below
Note this value is used for collection access only. The Web page generated ID will always be the UniqueId.
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.
o.IsInputControl
Any custom implementation of Dispose() should set this flag or have it set by the call to DoDefault().
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.
The ParentControl property is set in the AddControl() method.
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
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.
*** 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")
The stock Postback controls (TextBox, ChkBox, Lists etc.) controls are automatically persisted into ViewState if the ReadOnly property is .T.
Note that Value mirrors the Text property, but it's more efficient to use Text.
o.ToolTip
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
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
Custom
wwWebControl
wwWebPage
| Member | Description | |
|---|---|---|
![]() |
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. |
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.
<%@ 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.
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.
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)
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.
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)
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)
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)
lcErrorMessage
Error message to display if validation fails.
o.OnError(loException)
*** Page level OnLoad event FUNCTION OnLoad() this.SetFocus(this.txtName) ENDFUNC
o.SetFocus(loControl)
o.RegisterPostbackScriptCode()
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)
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
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)
lcHref
The URL to the stylesheet - can use ~/ syntax.
o.wwWebPage.RegisterClientScriptBlock(lcID,lcScriptCode)
lcScriptCode
The script code to embed into the page
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)
lcScript
The script code to embed
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.
<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)
As a special case you can acces the Page object with:
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');
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()
o.Render()
This method is very highlevel and acts merely as a controller wrapper around the wwWebControl::Process method. The Run method basically does 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()
The BindingErrors collection
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.
Note that Session state is much less crucial in WebControl applications since you have page state in the form of ViewState.
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.
this.Page.HiddenFormVars.Add("__TABSELECTION" + this.UniqueId)
o.HiddenFormVars
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
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)
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.
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.
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.
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.
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.
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" />
#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.
Web Connection only supports two kinds of special embedded script tag formats:
For example don't use:
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 (' '). <% if(Tquery.Status = "Completed" %> but use <% if TQuery.Status = [Completed] %>.
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.
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:
<% if this.Page.oBusiness.oData.LastAccess > Date() - 5 %> Some more text and controls hereSome text in my script page here
Common content: <ww:wwWebLabel runat="server" ControlSource="this.Page.oBusiness.oData.Content" />
<hr>
<% endif %>
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 %>
| Class | Description |
|---|---|
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. |
| Member | Description | |
|---|---|---|
![]() |
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 |
<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
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
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
Note the text is placed as plain text - no table formatting.
o.Text
The following modes are supported:
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>
| Member | Description | |
|---|---|---|
![]() |
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. |
o.BorderWidth
o.RenderAsDivTag
Options include:
If Visible is set to .F. it overrides any expression check from the VisibleExpression property.
o.Visible
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.
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
| Member | Description | |
|---|---|---|
![]() |
Text | The literal text that is placed into the page output. |
o.Text()
Not much custom functionality here - most of the functionality derives from the base wwWebControl.
Custom
wwWebControl
wwWebLabel
| Member | Description | |
|---|---|---|
![]() |
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. |
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
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
o.Text
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
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.
Generally you won't use this class directly only as part of the BindingErrorsCollection.
| Member | Description | |
|---|---|---|
![]() |
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. |
o.Message
o.ObjectName
| Member | Description | |
|---|---|---|
![]() |
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() |
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
o.ToHtml()
o.ToString()
| Member | Description | |
|---|---|---|
![]() |
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. |
o.ImageUrl
Note you should set any other style attributes using the style property.
o.BorderWidth
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
o.AlternateText
<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.
| Member | Description | |
|---|---|---|
![]() |
Text | The text property contains that value of the hidden field. |
o.wwWebHiddenField.Text
| Member | Description | |
|---|---|---|
![]() |
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. |
o.ShowError(lcMessage,llIsText)
lcUserMessage
A user message that is displayed above the message
o.ShowMessage(lcMessage)
lcUserMessage
A user message that is displayed above the message
Recommended default CSS class:
ErrorDisplay
o.CssClass
o.Text
It supports is AutoPostback and Click() event firing on AutoPostback operation.
Custom
wwWebControl
wwWebCheckBox
| Member | Description | |
|---|---|---|
![]() |
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 |
| Class | Description |
|---|---|
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. |
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")
| Member | Description | |
|---|---|---|
![]() |
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. |
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
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
o.Text
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');"/>
If you you want to have a link button that posts back to the current page look at wwWebLinkButton.
Custom
wwWebControl
wwWebHyperLink
| Member | Description | |
|---|---|---|
![]() |
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. |
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
Note if you need both text and image data or anything more complex the wwWebImage control offers more control.
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
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.
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
| Member | Description | |
|---|---|---|
![]() |
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. |
o.wwWebButton.ControlSource
o.BorderWidth
o.DisabledImageUrl
o.ImageUrl
o.NavigateUrl
<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
o.Target
o.Text
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
| Member | Description | |
|---|---|---|
![]() |
Click | Occurs when the user clicks the link. |
![]() |
OnClientClick | Optional client click JavaScript handler code. |
![]() |
Text | The text for the link to display. |
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
Use return false; in the handler to prevent the link from navigating.
o.OnClientClick
o.Text
| Member | Description | |
|---|---|---|
![]() |
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. |
| Class | Description |
|---|---|
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. |
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
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)
| Member | Description | |
|---|---|---|
![]() |
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. |
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)"

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.
<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.
@! - 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:
=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"
<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).
You can t
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.
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)
| Member | Description | |
|---|---|---|
![]() |
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. |
o.CssClass
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
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
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 if you use expressions it's often easier to simply create an Expression instead and simply pass the expression.
o.Format
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
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
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
o.Sortable
o.SortExpression
o.Style
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:
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)
Note that you should call this method instead of: grid.Columns.Remove() in order to properly hande cleanup.
o.RemoveColumn(lcColumnId)
o.DataBind()
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
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
Columns are added using the AddControl method of the DataGrid.
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
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.
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.
o.PagerText
If this property is set with a string value all other rendering for this row does not occur.
o.RowContent
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
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
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
The list supports both DataSource binding for it's list content and ControlSource binding for the SelectedValue property by default.
Custom
wwWebControl
wwWebListControl
| Member | Description | |
|---|---|---|
![]() |
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. |
*** 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
this.txtState.ControlSource = this.Page.oDeveloper.oData.StateCode
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)
| Member | Description | |
|---|---|---|
![]() |
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. |
o.Selected
o.Text
o.Value
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
Used by the script manager to add ListItems from Script Pages.
o.AddControl(loCtl)
If the control has a Cursor DataSource set the Added items are appended at the end.
o.AddItem(lcText,lcValue,llSelected)
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.
o.ClearItems()
o.DataBind()
This is very useful for common list scenarios where you need to have things like:
*** Please select a State
as a first item.
This is very useful for common list scenarios where you need to have things like:
*** Please select a State
as a first item.
Integer values are used:
1 - DropDown
2 - List
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.
Possible values:
<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
| Member | Description | |
|---|---|---|
![]() |
SelectedValue | Holds the selected value of this control. o.SelectedValue |
![]() |
RepeatDirection | Determines the direction of the radio buttons - Vertical or Horizontal . |
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"
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
Vertical buttons are rendered as a table - horizontal buttons are rendered as elements one after the other using standard flow.
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
| Member | Description | |
|---|---|---|
![]() |
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. |
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>
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.
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>
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 %>
The ItemTemplate sees each DataItem as a DataItem object when Array or Cursor data is used.
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
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.
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
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.ActiveItemTemplate
| Class | Description |
|---|---|
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 |