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.