Web Connection Web Control Framework Pages and Page Code Reuse
July 09, 2007 •
A number of questions have recently come up regarding dynamic execution of Web Control Framework Pages in the Web Connection framework. In this post I’ll review briefly how Web Control Pages work and then dig into some ways of how you can access these pages dynamically from regular Process class method code as opposed to only via direct script access and on a new feature that provides all of the functionality that direct script access provides via a simple Response.ExpandPage method.
A quick review of Web Control Framework Pages
The Web Control Framework is Web Connection’s ASP.NET like framework that provides a rich page model for creating page based applications. A Page in this context is basically a fully self contained HTML layout that is made up of static HTML text and any number (or none) of controls that provide page features and represented in Visual FoxPro as an object that contains other objects – controls and literal controls specifically.
Page classes can be manually generated using pure code, but more commonly visual markup is used. The visual layout uses ASP.NET style control syntax so you can edit Web Control Framework pages in Visual Studio and get full design time and property sheet support including the ability to the drag and drop controls and get Intellisense support for their functionality.
Markup pages look like this:
<%@ Page Language="C#" %>
<%@ Register Assembly="WebConnectionWebControls"
Namespace="Westwind.WebConnection.WebControls"
TagPrefix="ww" %>
<ww:wwWebPage ID="HelloWorld_Page" runat="server"
GeneratedSourceFile="webcontrols\helloworld_Page.prg" >
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Hello World Test</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" />
Enter your name: <ww:wwWebTextBox id="txtName" runat="server" />
<ww:wwWebButton id="btnSubmit" runat="server" Text="Say it"
Click="btnSubmit_Click" />
<hr />
<ww:wwWebLabel id="lblMessage" runat="server" />
</form>
</body>
</html>
</ww:wwWebPage>
and they can be designed in the Visual Studio WYSIWYG designer where you can drag and drop Web Connection controls, use the property sheet to assign properties etc.
Web Connection then supports generating code from the markup page, translating the HTML markup and control definitions into a FoxPro PRG file that contains the following:
- A small stub loader for the Page class so you can just DO MyPage.prg
- A base class that is used to attach your user code
- A generated class that inherits from the base class
The whole thing looks like this:
#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
FUNCTION btnSubmit_Click()
this.lblMessage.Text = "Hello " + this.txtName.Text
ENDFUNC
ENDDEFINE
*# --- BEGIN GENERATED CODE BOUNDARY --- #*
*******************************************************
*** Generated by WebPageParser.prg
*** on: 07/09/2007 03:14:07 PM
***
*** Do not modify manually - class will be overwritten
*******************************************************
DEFINE CLASS HelloWorld_Page_WCSX AS HelloWorld_Page
Id = [HelloWorld_Page]
*** Control Definitions
form1 = null
ErrorDisplay = null
txtName = null
btnSubmit = null
lblMessage = null
FUNCTION Initialize(loPage)
LOCAL __lcHtml
DODEFAULT(loPage)
__lcHtml = []
__lcHtml = __lcHtml + []+ CRLF +;
[<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">]+ CRLF +;
[<html xmlns="http://www.w3.org/1999/xhtml">]+ CRLF +;
[<head>]+ CRLF +;
[ <title></title>]+ CRLF +;
[ <link href="westwind.css" rel="stylesheet" type="text/css" />]+ CRLF +;
[</head>]+ CRLF +;
[<body style="margin-top:0px;margin-left:0px">]+ CRLF +;
[]
CTL0003 = CREATEOBJECT("wwWebLiteral",THIS,"CTL0003")
CTL0003.Text = __lcHtml
THIS.AddControl(CTL0003)
THIS.form1 = CREATEOBJECT("wwWebform",THIS,"form1")
THIS.AddControl(THIS.form1)
__lcHtml = []
__lcHtml = __lcHtml + []+ CRLF +;
[]+ CRLF +;
[]
CTL0005 = CREATEOBJECT("wwWebLiteral",THIS,"CTL0005")
CTL0005.Text = __lcHtml
THIS.AddControl(CTL0005)
THIS.ErrorDisplay = CREATEOBJECT("wwweberrordisplay",THIS,"ErrorDisplay")
THIS.AddControl(THIS.ErrorDisplay)
__lcHtml = []
__lcHtml = __lcHtml + []+ CRLF +;
[]+ CRLF +;
[ Enter your name: ]
CTL0007 = CREATEOBJECT("wwWebLiteral",THIS,"CTL0007")
CTL0007.Text = __lcHtml
THIS.AddControl(CTL0007)
THIS.txtName = CREATEOBJECT("wwwebtextbox",THIS,"txtName")
THIS.AddControl(THIS.txtName)
__lcHtml = []
__lcHtml = __lcHtml + []+ CRLF +;
[]
CTL0009 = CREATEOBJECT("wwWebLiteral",THIS,"CTL0009")
CTL0009.Text = __lcHtml
THIS.AddControl(CTL0009)
THIS.btnSubmit = CREATEOBJECT("wwwebbutton",THIS,"btnSubmit")
THIS.btnSubmit.Text = [Say it]
THIS.btnSubmit.HookupEvent("Click",THIS,"btnSubmit_Click")
THIS.AddControl(THIS.btnSubmit)
__lcHtml = []
__lcHtml = __lcHtml + []+ CRLF +;
[ <hr />]+ CRLF +;
[]
CTL0011 = CREATEOBJECT("wwWebLiteral",THIS,"CTL0011")
CTL0011.Text = __lcHtml
THIS.AddControl(CTL0011)
THIS.lblMessage = CREATEOBJECT("wwweblabel",THIS,"lblMessage")
THIS.AddControl(THIS.lblMessage)
__lcHtml = []
__lcHtml = __lcHtml + []+ CRLF +;
[]
CTL0013 = CREATEOBJECT("wwWebLiteral",THIS,"CTL0013")
CTL0013.Text = __lcHtml
THIS.AddControl(CTL0013)
CTL0014 = CREATEOBJECT("wwWebForm",THIS)
CTL0014.RenderType = 2
THIS.AddControl(CTL0014)
__lcHtml = []
__lcHtml = __lcHtml + []+ CRLF +;
[</body>]+ CRLF +;
[</html>]
CTL0015 = CREATEOBJECT("wwWebLiteral",THIS,"CTL0015")
CTL0015.Text = __lcHtml
THIS.AddControl(CTL0015)
ENDFUNC
ENDDEFINE
*# --- END GENERATED CODE BOUNDARY --- #*
The stub code at the top is basically a loader that knows how to instantiate the page and run it through the Page pipeline. The page pipeline fires a set of standard (like OnInit, OnLoad, OnPreRender, Dispose etc.) and user activated events (like button clicks, or selections in lists). The class relies on the various Web Connection intrinsic objects such as Request, Response, Server, Session, Config being available so it must be run in the context of a wwProcess request.
The first class is a base class that can be used to attach any user code to the page. This includes startup or custom rendering code, event handlers for clicks and selection changes, databinding expressions and any amount of custom methods and properties that you’d like to add to the class. Anything that is application specific can be put here.
The code to above the generated Code boundary ONE TIME GENERATED. Meaning if the PRG file does not exist and you run the page (when page parsing is enabled) the PRG file is created and all three components are generated. Once the page exists any changes made to the page markup cause only the bottom section of the page that contains the generated code to be updated. Any code above the boundary is never touched although the code file is updated every time a markup change is made.
Page Inheritance
Note that the generated class inherits from the base class above.Inheritance is used to allow keeping the changes to user code separate from the markup changes. By using a separate class we can isolate the code that you write from the code that is generated.
Because the class that you write code on is the base class you can also change the base class for your user code class to something other than the default wwWebPage class. This means you can use a custom page class that is application specific or maybe specific to your framework.
But even more interesting is that you can also create a base class that is shared by multiple markup pages. So you could create HelloWorld_Base as wwWebPage and then create several different HelloWorld.wscx variations that create empty classes that simply inherit from Helloworld_base.
You could start with this class in a separate PRG file:
**************************************************************
DEFINE CLASS HelloWorld_Base 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
FUNCTION btnSubmit_Click()
this.lblMessage.Text = "Hello " + this.txtName.Text
ENDFUNC
ENDDEFINE
Then for the actual WCSX code behind page you’d use:
#INCLUDE WCONNECT.H
SET PROCEDURE TO Helloworld_base.prg
*** Small Stub Code to execute the generated page
PRIVATE __WEBPAGE
__WEBPAGE = CREATEOBJECT("HelloWorld_Page_WCSX")
__WEBPAGE.Run()
RELEASE __WEBPAGE
RETURN
**************************************************************
DEFINE CLASS HelloWorld_Page1 as HelloWorld_base
***********************************************
ENDDEFINE
Using this approach you could now have several different versions of the Helloworld page that inherits the same functionality:
**************************************************************
DEFINE CLASS HelloWorld_Page2 as HelloWorld_base
***********************************************
ENDDEFINE
As long as the code in the base class can count on any objects referenced to exist this sort of inheritance allows you reuse functionality easily across pages.
Running Web Control Pages
By default the Web Connection framework takes care of running Web Control Framework pages. It does so through a fairly complex mechanism in wwProcess::RouteRequest that checks the mode that the framework is in and based on that decides whether to parse and precompile WCF pages. It looks for the page on disk parses out the generated PRG file and then eventually executes this PRG that runs the page. There’s some caching and optimization that deals with deciding when to compile when to just run and so on.
This functionality is the default and it works in 99% of scenarios. It’s based around the concept of a physical file on disk and a matched PRG file that acts as a backing file. The default behavior always looks at the physical file first to figure out the location of the PRG file to execute.
There have been a number of questions of how to run a Web Control page through a custom path or reuse the same page from multiple virtual directories (common in scenarios where the same code and markup is used for different associations/groups/companies just with different data).
Web Control pages are classes and they are self contained. Once you’ve generated a PRG file and class from a markup page, the PRG file is fully self-contained and can execute inside of the Web Connection Process environment. The PRG doesn’t need a markup file and it contains everything it needs to ‘run’ the page effectively on its own. All the default parsing and ‘routing’ code is not required once the PRG exists and your code knows how to find it.
A simple DO HelloWorld_Page.prg is all that it takes to execute the page!
This means you can fairly easily create custom routing schemes to Web Control Pages using standard Web Connection Process class methods. For the uninitiated: the wwProcess class is the base handler that responds to a specific type Web Connection request – for example for a specific extension like .wdd. Process classes have various levels of default request routing. The first and highest priority route maps a page name (ie. Helloworld.wwd – page name is HelloWorld) to method of the same name if it exists. This is the most basic and most efficient map that is known as classic wwProcess method handling. If no method can be found script execution is used instead and Web Connection tries to find a script file on disk and execute it as a Web Page. If still no match is found the request fails.
So the important concept is that you can use plain wwProcess method to fire a Web Control page. Assuming Helloworld_Page.prg exists you can call this PRG file from any Process method simply with code like this:
*** Process Method
FUNCTION DoHelloworld
DO Helloworld_Page.prg
ENDFUNC
That’s it.
Because this is plain process method code and it doesn’t look at the markup file at all there’s no association of the page with the underlying markup file. IOW, you can execute this page now from anywhere in response to any request you choose!
But understand that when you do this you need to ensure that the markup was already parsed into a PRG file. Calling the page in this fashion is not dynamic meaning it calls the PRG file and that PRG file will not be updated if there are changes to the markup unless you explicitly compile (with WebPageParser.prg) or by running the page directly through it’s physical path script mapping.
A new addition: wwPageResponse.ExpandPage()
To address this scenario a little better and for pure consistency with the other scripting mechanisms build into Web Connection I decided to add a new ExpandPage() method to the wwPageResponse class, which abstracts the full page parsing, compilation and execution cycle that is natively build into the wwProcess class.
With ExpandPage() you can now simply point at the physical path of a script page and Web Connection will then execute the page directly from a Process method. So here the code becomes:
*** Process Method
FUNCTION DoHelloworld
*** Point at the original script file
lcPath = this.WebScriptPath
Response.ExpandPage( lcPath + "Hellowold.dp")
ENDFUNC
This code differs from calling the PRG directly in that it will parse and compile the page first just as if it was executed by direct URL access. There’s another optional parameter that specifies the Page Parsing mode which defaults the value set by wwProcess.nPageParseMode (1 – parse & run 2 – parse & compile & run 3 – run only) and which are also exposed in the Status Forms Page Parse Mode dropdown.
ExpandPage() pretty much gives you full control over the process. One note: It’s specific to wwPageResponse only and not by wwResponse/String/File since it relies on a host of features in the new Response class.
So with this functionality in place it should now be possible to use one set of pages and codebehind classes to service multiple applications/virtual directories with a single set of pages. This is definitely a specialty scenario but this seems to come up pretty consistently.
It should also make it easier yet to integrate WebControl functionality into existing applications and I’m hoping the easier it is to hook this functionality into existing applications the easier it will be for people to use this stuff!
ExpandPage will be released with the next update of Web Connection (5.30 or so).
RandyP
July 15, 2007