Display multiple pages in a page frame

On a web site for editing the user profile, a user can enter address, billing information, and so forth, on various pages. All of these pages are organized in a page frame. Each page is accessible through a tab. The current tab is highlighted with a different color.

For the simplest possible design you create a separate AFP document for each page of the pageframe. Each document has got a copy of all tabs. The one of the current page is highlighted. Each tab has got a link to the appropriate page document, except for the current one:

<a href="page3.afp">Billing information</a>

The biggest disadvantage of this solution is that you must duplicate many elements across many files, for instance, the page tabs. When you add a new page or change a caption you have to go through all documents and repeat the modification in each of it.

Complexity quickly increases if you can hide individual pages. For instance, a new customer doesn't need a "Purchase History" tab on the profile page. In this case, you have to replicate the conditions among all involved documents. Moreover, in all potentially unavailable pages you have to check if calling the page is allowed. Even though there's no direct link to a page, nobody can stop a user from simply calling a page by entering its address in the browser.

A frequently used solution is to create a single AFP document pageframe.afp for the entire pageframe. A parameter page defines which page is actually displayed. To show page 3 of the pageframe, you would use code like this:

<a href="pageframe.afp?page=3">Billing information</a>

In each tab you use the same address as the link and just swap the page number accordingly. Clicking on a tab reloads the current document and automatically displays a different page. For mainly non-interactive content this is a viable solution. Of course, you need to verify that a user can currently call a specifc page. It's just as easy to enter a URL with a parameter into the browser's address textbox.

Where you run into troubles with this design is when a page contains actions that cause the current page to be displayed again. When defining an HTML form you must specify an action attribute in the form tag. This can be the same or a different AFP document. In either case you must pass the current page number, as well. Otherwise, you won't be able to display the document with the correct page once the processing is done:

<form action="pageframe.afp?page=3&action=submit" method="post">

If the page posts back to itself, the page might have to deal with multiple, different HTML forms. In addition to checking whether REQUEST_METHOD contains POST, you have to figure out which form caused the page to be called. The more pages of a pageframe you combine into the same physical document, the more likely it is that you encounter more than one HTML form.

This approach gets complicated asa the number of forms and links increases. Each link must contain all parameters that are required to display the page properly. Especially with generic components that are incorporated into documents using *!<[INCLUDE: ]> it's difficult to pass all values at all times.

One approach to this problem are page specific data, more precisely, page call specific data. This kind of data is transferred only from one page to another. In contrast to sessions, a user can open multiple branches of page calls. For instance, a user could open a page in a new browser instance. This doesn't change the session ID. However, the user can now follow the program flow in two directions simultaneously. There are two possibilities to transfer data from one page to another.

When managing page specific data on the server-side, you create a unique ID on every request. Session.CreateSessionID()provides an easy way to generate such an ID. Each time a user requests a page, you end up with a different ID. At the end of an AFP document, for instance in the Event_PageAfter event, you use this ID to store all values. AFP provides are two mechanism for storing data.

For one, you can store values as session data. Convert all values into a single string and store this string using Session.SetSessionData(). The first parameter contains all data you need to persist, the second parameter contains the unique ID of the page call. You can create this data string in similar fashion as AFP manages session variables. For instance, all page specific variables could start with "P" for page. Assuming the unique ID has been stored to lcPageCallID, you can use the following code to put all variables starting with "P" into a single string:

Local lcVariables, lnSelect

lnSelect = Select()

Create Cursor ___AFPVariables (cVariables M)

Append Blank

Save to memo cVariables all like P*

lcVariables = ___AFPVariables.cVariables

USE in ___AFPVariables

Select (m.lnSelect)

Session.SetSessionData(m.lcVariables,m.lcPageCallID)

To load these variables you insert the following code at the beginning of the AFP page or the Event_PageBefore event:

Local lcVariables, lnSelect

lcVariables = APP.GetSessionData(m.lcPageCallID)

If not Empty(m.lcVariables)

lnSelect = Select()

   Create Cursor ___AFPVariables (cVariables M)

   Insert into ___AFPVariables values (m.lcVariables)

   Release All like P*

   Restore From Memo cVariables Additive

   USE in ___AFPVariables

   Select (m.lnSelect)

Endif

Alternatively, you can store page specific data in object properties. Create an object at the beginning of a page that has properties for all required information. When using the VFP 8 version such an object could base on the EMPTY class to which you add properties using the ADDPROPERTY() function. You could define your own class with all properties in the .code file, as well. To save these values, use the Session.SetObject() function, to load them back use the Session.GetObject() function.

In both cases you have to pass the request ID on to the next page. On approach is a hidden form field, the other one is using a query string parameter. At first glance there doesn't seem to be a difference between this and the second approach that used a page query string parameter. However, this method allows you to pass as many page specific values as you need. More importantly, you don't need to know each and every parameter in each and every link on the page. No matter what page you use, all you need to pass on is the ID of the page call (or requests.)

The evident disadvantage of server-side solutions is their resource consumption. Each request of a page creates a new file in the session directory. The clean-up thread goes through the entire directory every three minutes and validates each single file. At the end of the session it is automatically deleted.

Of course, this isn't an AFP specific problem, rather an issue with all server products for web applications. Microsoft, for instance, implemented the so-called ViewState in ASP.NET. Similar to the server-side approach they put all request specific values into a string. Instead of storing this string on the server it's added to the form as a hidden input element:

<input type="hidden" name="__VIEWSTATE" value="…">

Hence, whenever the form is posted back, this state is transmitted as part of the response. It's no big deal to implement this in AFP, as well. You need to add the hidden input element to each HTML form on a page. To simplify this daunting task we recommend handling this in the Event_PageAfter event. Create a string with all page-specific values just like you did with server-side storage. Additionally, you have to encode the string with Server.HTMLEncode() or STRCONV(). To pass the string on as part of each form, you can use the following code (assuming that lcViewState contains the line with the element definition):

LOCAL lcBody

lcBody = Response.Body()

lcBody = STRTRAN( "</form>", m.lcViewState+"</form>", 1, -1, 1 )

Response.Clear()

Response.BinaryWrite( m.lcBody )

This replaces extends all occurrences of </form> with the extended version. Client-side storage has disadvantages, as well. Data is only passed with POST requests using forms. You have to replace simple links with HTML forms or JaveScript, if the requested document requires access to certain page specific values.

Storing data on the client is always a security risk as everything has to be protected against tampering. Either you encrypt the string with sufficiently secure algorithm and sufficiently strong password, or you validate data for plausibility before using them. It depends on your application, its usage pattern and your security demands which solution is most suitable for you.