Qwicap Prompt Pattern

Chris W. Johnson
Information Technology Services
The University of Texas at Austin
17 March, 2007

Qwicap attempts to impose as few requirements as possible on client code, but there is one situation where it must impose a design pattern: Sending a web page to the user, and processing the form data set sent back from that page. It usually goes something like this:

while (true) {
	
    try {
    	
        Q.prompt(ThisWebPage);    //  "Q" is the current Qwicap instance.
        
Retrieve and validate the input sent from the page, using the Qwicap "get..." methods (and "rejectInput" if the built-in validation features aren't sufficient for your purposes).
If you decide a different page should be shown, branch to your method that shows that page. That method must also implement this prompt pattern.
When the work of this page is complete, use a "break" statement, or equivalent logic, to exit the enclosing "while" loop.
    } catch (QwicapAbandonmentException e) {
        e.rethrowIfThisPageWasAbandoned();
    }
}

An implementation of the prompt pattern is shown in the Qwicap Introduction.

There is room for variation within this pattern, but its essentials must be retained in order for Qwicap to work correctly. Those essentials are: (1) Prompting, retrieving/validating/rejecting input, and branching to subsidiary web pages, must occur within a "try" block that catches the "QwicapAbandonmentException" and invokes that exception's "rethrowIfThisPageWasAbandoned" method. (2) That "try" block must be placed within a loop that does not exit until you have no further need for the web page it handles. (3) Handle only one web page within each such loop.

If you want to get started with Qwicap as quickly as possible, that's all you need to know; use that pattern and your web application will work pretty much as you'd expect. If you want to understand the purpose of the pattern, continue reading.

Abandonment Issues

Conventional applications enjoy the simplicity of rigidly controlling their user's movement within them. (Whether users enjoy this is another matter.) Web applications don't have this luxury; they can define a user's options as rigidly as they wish, but the user can always abandon the current page by using their browser's "back" button and then ambush the application with inputs from past pages. Web applications may choose to detect and ignore such input, which can simplify matters, but this approach has been known to annoy users who are accustomed to their "back" buttons. Alternately, web applications can be designed to keep no state by embedding all of the information they need in every form they generate, and then starting their work more-or-less from scratch with every hit, but that creates its own set of problems.

Qwicap attempts to provide the application developer with the simplicity of being unaware of such inputs, and the convenience of maintaining rich state information, while letting users enjoy their "back" buttons. It can't satisfy all of the desires of either developer or user, but it achieves a useful middle-ground: Web application developers get the luxury of having the effects of the "back" button handled transparently to their code, in exchange for implementing the "prompt" pattern. Users get to hit their "back" buttons, in exchange for only being able to go back to the most recent version of pages that are still in-play (in other words, pages whose implementations are still in the call chain; inputs from other pages are discarded).

An Example

Below is a very contrived fragment of an example application that prompts for various inputs, and allows the user to navigate among pages within the application. Four pages are provided. The first three, A, B and C, each prompt for a mathematical constant (they don't do anything with the input; it's merely to prevent the pages from seeming completely pointless). The remaining page, Z, displays the correct value of those constants. All of the pages allow the user to go to page Z, but pages A, B and C are arranged in an ordered progression: C can only be reached from B, and B can only be reached from A.

The pages A, B and C are each composed of two forms: one allows the user to enter a mathematical constant, and the other provides the navigation buttons, e.g. "Goto Page B", "Go Back".

(This example assumes a reference to the current Qwicap instance is present in an instance variable named "Q".)

Example Implementation Example Page
A00 public void methodA() {
A01     MutableMarkup DocA = Q.getDocument("a.html").getMutable();
A02     
A03     while (true) {
A04         try {
A05             Q.prompt(DocA);
A06             if (Q.has("gotoB"))
A07                 methodB();
A08             else if (Q.has("gotoZ"))
A09                 methodZ();
A10             else if (Q.has("quit"))
A11                 break;
A12             else {
A13                 getDouble("pi", 3.13, 3.15, false, 0.0);
A14             
A15                 ....  //  Do something useful with the input
A16             }
A17         } catch (QwicapAbandonmentException e) {
A18             e.rethrowIfThisPageWasAbandoned();
A19         }
A20     }
A21     
A22     Q.goodbye(Q.getDocument("goodbye.html").getMutable());
A23 }

Page A

B00 public void methodB() {
B01     MutableMarkup DocB = Q.getDocument("b.html").getMutable();
B02     
B03     while (true) {
B04         try {
B05             Q.prompt(DocB);
B06             if (Q.has("gotoC"))
B07                 methodC();
B08             else if (Q.has("gotoZ"))
B09                 methodZ();
B10             else if (Q.has("goback"))
B11                 return;
B12             else {
B13                 getDouble("e", 2.70, 2.72, false, 0.0);
B14             
B15                 ....  //  Do something useful with the input
B16             }
B17         } catch (QwicapAbandonmentException e) {
B18             e.rethrowIfThisPageWasAbandoned();
B19         }
B20     }
B21 }

Page B

C00 public void methodC() {
C01     MutableMarkup DocC = Q.getDocument("c.html").getMutable();
C02     
C03     while (true) {
C04         try {
C05             Q.prompt(DocC);
C06             if (Q.has("goback"))
C07                 return;
C08             else if (Q.has("gotoZ"))
C09                 methodZ();
C10             else {
C10                 Q.getDouble("phi", 1.60, 1.62, false, 0.0);
C11                 
C12                 ....  //  Do something useful with the input
C13             }
C14         } catch (QwicapAbandonmentException e) {
C15             e.rethrowIfThisPageWasAbandoned();
C16         }
C17     }
C18 }

Page C

Z00 public void methodZ() {
Z01     MutableMarkup DocZ = Q.getDocument("z.html").getMutable();
Z02     
Z03     while (true) {
Z04         try {
Z05             Q.prompt(DocZ);
Z06             if (Q.has("goback"))
Z07                 return;
Z08         } catch (QwicapAbandonmentException e) {
Z09             e.rethrowIfThisPageWasAbandoned();
Z10         }
Z11     }
Z12 }

Page Z

π = 3.1415....
e = 2.7182....
Φ = 1.6180....

The Call Chain

Consider the case where a user goes from page A, to B, to C. When page C is displayed (at line C05), the call chain would look like this:

  1. methodC:C05
  2. methodB:B07
  3. methodA:A07

Because the methods supporting pages A and B are still in the call chain (this was previously referred to as being "in-play"), they remain valid targets for user interaction. In other words, if the user hits their browser's "back" button and returns to either page, then interacts with any of the forms on one of those pages, Qwicap will accept the form input as valid, and will automatically return the program's point of execution to the method that is supposed to handle that input. The QwicapAbandonmentException and the Prompt pattern provide the mechanism for accomplishing this.

To illustrate the workings of that process using the call chain above, suppose the user has clicked their browser's "back" button twice, and is now looking at page A ("a.html"). Suppose then that the user enters a value for π and clicks on page A's "Submit π" button. The application is still at line C05, where a form data set that supplies a value for π is neither expected, nor wanted. So, Qwicap must:

  1. Identify the page-of-origin1 ("a.html") of the form data set.
  2. Determine whether the method that managed the page (the method that passed the page to Qwicap.prompt) is still present in the call chain.
  3. If the method that managed the page is still present in the call chain, Qwicap must force your program to resume execution of that method, so that that method can process the form data set. Conversely, if the method is not in the call chain anymore, Qwicap must discard the unwanted form data set, and retransmit the current page ("c.html") to the user.

In this case, Qwicap would find that (1) it can identify the page-of-origin ("a.html"), and (2) the method that manages the page (methodA) is still present in the call chain. Therefore, Qwicap sets out to accomplish objective no. 3: Forcing your program to resume execution of that method (methodA). It begins that process by throwing a QwicapPageAbandonedException, one of several subclasses of QwicapAbandonmentException.

The QwicapPageAbandonedException causes the prompt call at line C05 to terminate, and execution jumps to the catch on line C14. That exception handler calls QwicapAbandonmentException's method rethrowIfThisPageWasAbandoned. That rethrow method examines the current call chain and determines that the program is executing methodC, which is not where it wants your application to be. So, the method re-throws the exception which causes methodC to exit and execution to resume in method B at the QwicapAbandonmentException handler on line B17.

The exception handler at B17 then calls the rethrowIfThisPageWasAbandoned method. That method again determines that execution hasn't returned to the method that manages page A, and therefore re-throws the QwicapAbandonmentException for the second time. That causes methodB to exit and execution resumes in method A at its QwicapAbandonmentException handler on line A18.

The exception handler at A18 then calls the rethrowIfThisPageWasAbandoned method. That method determines that execution has, at long last, returned to the method that does manage page A, and therefore it does nothing. That causes the exception handler to fall-through to the bottom of the enclosing loop (line A20), and for the loop to bring us back to the prompt invocation on line A05. Qwicap recognizes it already has a fresh form data set from this page, and therefore the prompt method does nothing. That leaves methodA exactly where it needs to be to begin processing the form data set that was original received by the prompt method invocation in methodC.

Fresh and Stale Pages

[Obviously, this documentation isn't finished.]

1. Note that Qwicap does not identify pages by comparing file names, e.g. "a.html", so even if several methods in the current call chain had loaded their pages from the same file, Qwicap would correctly distinguish between them. It would confuse Qwicap, however, if methods in the call chain shared the same instance of a MutableMarkup object, i.e. the same mutable, in-memory copy of a web page. So don't do that.
Valid XHTML 1.0! Valid CSS!