14.6: Managing Control Flow
- Page ID
- 39659
\( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \)
\( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)
\( \newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\)
( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\)
\( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\)
\( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\)
\( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\)
\( \newcommand{\Span}{\mathrm{span}}\)
\( \newcommand{\id}{\mathrm{id}}\)
\( \newcommand{\Span}{\mathrm{span}}\)
\( \newcommand{\kernel}{\mathrm{null}\,}\)
\( \newcommand{\range}{\mathrm{range}\,}\)
\( \newcommand{\RealPart}{\mathrm{Re}}\)
\( \newcommand{\ImaginaryPart}{\mathrm{Im}}\)
\( \newcommand{\Argument}{\mathrm{Arg}}\)
\( \newcommand{\norm}[1]{\| #1 \|}\)
\( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\)
\( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\AA}{\unicode[.8,0]{x212B}}\)
\( \newcommand{\vectorA}[1]{\vec{#1}} % arrow\)
\( \newcommand{\vectorAt}[1]{\vec{\text{#1}}} % arrow\)
\( \newcommand{\vectorB}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \)
\( \newcommand{\vectorC}[1]{\textbf{#1}} \)
\( \newcommand{\vectorD}[1]{\overrightarrow{#1}} \)
\( \newcommand{\vectorDt}[1]{\overrightarrow{\text{#1}}} \)
\( \newcommand{\vectE}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash{\mathbf {#1}}}} \)
\( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \)
\( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)
\(\newcommand{\avec}{\mathbf a}\) \(\newcommand{\bvec}{\mathbf b}\) \(\newcommand{\cvec}{\mathbf c}\) \(\newcommand{\dvec}{\mathbf d}\) \(\newcommand{\dtil}{\widetilde{\mathbf d}}\) \(\newcommand{\evec}{\mathbf e}\) \(\newcommand{\fvec}{\mathbf f}\) \(\newcommand{\nvec}{\mathbf n}\) \(\newcommand{\pvec}{\mathbf p}\) \(\newcommand{\qvec}{\mathbf q}\) \(\newcommand{\svec}{\mathbf s}\) \(\newcommand{\tvec}{\mathbf t}\) \(\newcommand{\uvec}{\mathbf u}\) \(\newcommand{\vvec}{\mathbf v}\) \(\newcommand{\wvec}{\mathbf w}\) \(\newcommand{\xvec}{\mathbf x}\) \(\newcommand{\yvec}{\mathbf y}\) \(\newcommand{\zvec}{\mathbf z}\) \(\newcommand{\rvec}{\mathbf r}\) \(\newcommand{\mvec}{\mathbf m}\) \(\newcommand{\zerovec}{\mathbf 0}\) \(\newcommand{\onevec}{\mathbf 1}\) \(\newcommand{\real}{\mathbb R}\) \(\newcommand{\twovec}[2]{\left[\begin{array}{r}#1 \\ #2 \end{array}\right]}\) \(\newcommand{\ctwovec}[2]{\left[\begin{array}{c}#1 \\ #2 \end{array}\right]}\) \(\newcommand{\threevec}[3]{\left[\begin{array}{r}#1 \\ #2 \\ #3 \end{array}\right]}\) \(\newcommand{\cthreevec}[3]{\left[\begin{array}{c}#1 \\ #2 \\ #3 \end{array}\right]}\) \(\newcommand{\fourvec}[4]{\left[\begin{array}{r}#1 \\ #2 \\ #3 \\ #4 \end{array}\right]}\) \(\newcommand{\cfourvec}[4]{\left[\begin{array}{c}#1 \\ #2 \\ #3 \\ #4 \end{array}\right]}\) \(\newcommand{\fivevec}[5]{\left[\begin{array}{r}#1 \\ #2 \\ #3 \\ #4 \\ #5 \\ \end{array}\right]}\) \(\newcommand{\cfivevec}[5]{\left[\begin{array}{c}#1 \\ #2 \\ #3 \\ #4 \\ #5 \\ \end{array}\right]}\) \(\newcommand{\mattwo}[4]{\left[\begin{array}{rr}#1 \amp #2 \\ #3 \amp #4 \\ \end{array}\right]}\) \(\newcommand{\laspan}[1]{\text{Span}\{#1\}}\) \(\newcommand{\bcal}{\cal B}\) \(\newcommand{\ccal}{\cal C}\) \(\newcommand{\scal}{\cal S}\) \(\newcommand{\wcal}{\cal W}\) \(\newcommand{\ecal}{\cal E}\) \(\newcommand{\coords}[2]{\left\{#1\right\}_{#2}}\) \(\newcommand{\gray}[1]{\color{gray}{#1}}\) \(\newcommand{\lgray}[1]{\color{lightgray}{#1}}\) \(\newcommand{\rank}{\operatorname{rank}}\) \(\newcommand{\row}{\text{Row}}\) \(\newcommand{\col}{\text{Col}}\) \(\renewcommand{\row}{\text{Row}}\) \(\newcommand{\nul}{\text{Nul}}\) \(\newcommand{\var}{\text{Var}}\) \(\newcommand{\corr}{\text{corr}}\) \(\newcommand{\len}[1]{\left|#1\right|}\) \(\newcommand{\bbar}{\overline{\bvec}}\) \(\newcommand{\bhat}{\widehat{\bvec}}\) \(\newcommand{\bperp}{\bvec^\perp}\) \(\newcommand{\xhat}{\widehat{\xvec}}\) \(\newcommand{\vhat}{\widehat{\vvec}}\) \(\newcommand{\uhat}{\widehat{\uvec}}\) \(\newcommand{\what}{\widehat{\wvec}}\) \(\newcommand{\Sighat}{\widehat{\Sigma}}\) \(\newcommand{\lt}{<}\) \(\newcommand{\gt}{>}\) \(\newcommand{\amp}{&}\) \(\definecolor{fillinmathshade}{gray}{0.9}\)Seaside makes it particularly easy to design web applications with non-trivial control flow. There are basically two mechanisms that you can use:
- A component can call another component by sending
caller call: callee
. The caller is temporarily replaced by the callee, until the callee returns control by sendinganswer:
. The caller is usuallyself
, but could also be any other currently visible component. - A workflow can be defined as a task. This is a special kind of component that subclasses
WATask
(instead ofWAComponent
). Instead of definingrenderContentOn:
, it defines no content of its own, but rather defines ago
method that sends a series ofcall:
messages to activate various subcomponents in turn.
Call and answer
Call and answer are used to realize simple dialogues.
There is a trivial example of call:
and answer:
in the rendering demo of Figure 14.4.2. The component SeasideEditCallDemo
displays a text field and an edit
link. The callback for the edit link calls a new instance of SeasideEditAnswerDemo
initialized to the value of the text field. The callback also updates this text field to the result which is sent as an answer.
(We underline the call:
and answer:
sends to draw attention to them.)
SeasideEditCallDemo >> renderContentOn: html html span class: 'field'; with: self text. html space. html anchor callback: [self text: (self call: (SeasideEditAnswerDemo new text: self text))]; with: 'edit'
What is particularly elegant is that the code makes absolutely no reference to the new web page that must be created. At run-time, a new page is created in which the SeasideEditCallDemo
component is replaced by a SeasideEditAnswerDemo
component; the parent component and the other peer components are untouched.
It is important to keep in mind that call:
and answer:
should never be used while rendering. They may safely be sent from within a callback, or from within the go
method of a task.
The SeasideEditAnswerDemo
component is also remarkably simple. It just renders a form with a text field. The submit button is bound to a callback that will answer the final value of the text field.
SeasideEditAnswerDemo >> renderContentOn: html html form: [ html textInput on: #text of: self. html submitButton callback: [ self answer: self text ]; text: 'ok'. ]
That’s it.
Seaside takes care of the control flow and the correct rendering of all the components. Interestingly, the Back button of the browser will also work just fine (though side effects are not rolled back unless we take additional steps).
Convenience methods
Since certain call–answer dialogues are very common, Seaside provides some convenience methods to save you the trouble of writing components like SeasideEditAnswerDemo
. The generated dialogues are shown in Figure \(\PageIndex{1}\). We can see these convenience methods being used within SeasideDialogDemo>>renderContentOn:
The message request:
performs a call to a component that will let you edit a text field. The component answers the edited string. An optional label and default value may also be specified.
SeasideDialogDemo >> renderContentOn: html html anchor callback: [ self request: 'edit this' label: 'done' default: 'some text' ]; with: 'self request:'. ...
The message inform:
calls a component that simply displays the argument message and waits for the user to click Ok
. The called component just returns self
.
... html space. html anchor callback: [ self inform: 'yes!' ]; with: 'self inform:'. ...
The message confirm:
asks a question and waits for the user to select either Yes
or No
. The component answers a boolean, which can be used to perform further actions.
... html space. html anchor callback: [ (self confirm: 'Are you happy?') ifTrue: [ self inform: ':-)' ] ifFalse: [ self inform: ':-(' ] ]; with: 'self confirm:'.
A few further convenience methods, such as chooseFrom:caption:
, are defined in the convenience
protocol of WAComponent
.
Tasks
A task is a component that subclasses WATask
. It does not render anything itself, but simply calls other components in a control flow defined by implementing the method go
.
WAConvenienceTest
is a simple example of a task defined in the package Seaside-Tests-Functional
. To see its effect, just point your browser to http://localhost:8080/tests/functional
, select WAFlowConvenienceFunctionalTest
and click Restart
.
WAFlowConvenienceFunctionalTest >> go [ self chooseCheese. self confirmCheese ] whileFalse. self informCheese
This task calls in turn three components. The first, generated by the convenience method chooseFrom: caption:
, is a WAChoiceDialog
that asks the user to choose a cheese.
WAFlowConvenienceFunctionalTest >> chooseCheese cheese := self chooseFrom: #('Greyerzer' 'Tilsiter' 'Sbrinz') caption: 'What''s your favorite Cheese?'. cheese isNil ifTrue: [ self chooseCheese ]
The second is a WAYesOrNoDialog
to confirm the choice (generated by the convenience method confirm:
).
WAFlowConvenienceFunctionalTest >> confirmCheese ^self confirm: 'Is ', cheese, ' your favorite cheese?'
Finally a WAFormDialog
is called (via the convenience method inform:
).
WAFlowConvenienceFunctionalTest >> informCheese self inform: 'Your favorite cheese is ', cheese, '.'
The generated dialogues are shown in Figure \(\PageIndex{2}\).
Transactions
We saw in Section 14.3 that Seaside can keep track of the correspondence between the state of components and individual web pages by having components register their state for backtracking: all that a component need do is implement the method states
to answer an array of all the objects whose state must be tracked.
Sometimes, however, we do not want to backtrack state: instead we want to prevent the user from accidentally undoing effects that should be permanent. This is often referred to as ”the shopping cart problem”. Once you have checked out your shopping cart and paid for the items you have purchased, it should not be possible to go Back with the browser and add more items to the shopping cart!
Seaside allows you to enforce restriction this by defining a task within which certain actions are grouped together as transactions. You can backtrack within a transaction, but once a transaction is complete, you can no longer go back to it. The corresponding pages are invalidated, and any attempt to go back to them will cause Seaside to generate a warning and redirect the user to the most recent valid page.
The Seaside Sushi Store is sample application that illustrates many of the features of Seaside, including transactions. This application is bundled with your installation of Seaside, so you can try it out by pointing your browser at http://localhost:8080/seaside/examples/store
. (If you cannot find it in your image, there is a version of the sushi store available on SqueakSource from http://www.squeaksource.com/SeasideExamples/.)
The sushi store supports the following workflow:
- Visit the store.
- Browse or search for sushi.
- Add sushi to your shopping cart.
- Checkout.
- Verify your order.
- Enter shipping address.
- Verify shipping address.
- Enter payment information.
- Your fish is on its way!
If you toggle the halos, you will see that the top-level component of the sushi store is an instance of WAStore
. It does nothing but render the title bar, and then it renders task, an instance of WAStoreTask
.
WAStore >> renderContentOn: html "... render the title bar ..." html div id: 'body'; with: task
WAStoreTask
captures this workflow sequence. At a couple of points it is critical that the user not be able to go back and change the submitted information.
Purchase some sushi and then use the Back button to try to put more sushi into your cart.
You will get the message That page has expired.
Seaside lets the programmer say that a certain part of a workflow acts like a transaction: once the transaction is complete, the user cannot go back and undo it. This is done by sending isolate:
to a task with the transactional block as its argument. We can see this in the sushi store workflow as follows:
WAStoreTask >> go | shipping billing creditCard | cart := WAStoreCart new. self isolate: [[ self fillCart. self confirmContentsOfCart ] whileFalse ]. self isolate: [ shipping := self getShippingAddress. billing := (self useAsBillingAddress: shipping) ifFalse: [ self getBillingAddress ] ifTrue: [ shipping ]. creditCard := self getPaymentInfo. self shipTo: shipping billTo: billing payWith: creditCard ]. self displayConfirmation.
Here we see quite clearly that there are two transactions. The first fills the cart and closes the shopping phase. (The helper methods such as fillCart
take care of instantiating and calling the right subcomponents.) Once you have confirmed the contents of the cart you cannot go back without starting a new session. The second transaction completes the shipping and payment data. You can navigate back and forth within the second transaction until you confirm payment. However, once both transactions are complete, any attempt to navigate back will fail.
Transactions may also be nested. A simple demonstration of this is found in the class WANestedTransaction
. The first isolate:
takes as argument a block that contains another, nested isolate:
WANestedTransaction >> go self inform: 'Before parent txn'. self isolate: [self inform: 'Inside parent txn'. self isolate: [self inform: 'Inside child txn']. self inform: 'Outside child txn']. self inform: 'Outside parent txn'
Go to http://localhost:8080/tests/functionals
, select WATransactionTest
and click on Restart
. Try to navigate back and forth within the parent and child transaction by clicking the Back button and then clicking Ok
. Note that as soon as a transaction is complete, you can no longer go back inside the transaction without generating an error upon clicking Ok
.