avi at beta4.com
Sat Feb 8 15:34:40 CET 2003
There were some recent questions about how state management works in
Seaside 2.2, At some point I hope to have a tutorial on that; for now,
all I have is an outline of one. I thought the outline might be useful in
itself, however, so I'm forwarding it to the list (this was originally an
email to Stef Ducasse).
Ok... so there are actually two topics I want to cover. One is the
management of state while backtracking, the other is embedding components.
The first topic would be a discussed in a short tutorial called "Retracing
Your Steps". In broad strokes, this would be:
1. demonstrate the problem (show how the current WACounter doesn't behave
properly with the back button)
2. demonstrate the implementation that fixes this (using StateHolder)
3. discuss when it's necessary or useful to use StateHolder and when it's
4. talk about the need for and implementation of a way of limiting the
ability to backtrack
In more detail:
1. In the earlier tutorial, we looked at the WACounter component, which
maintains a simple integer count. Clicking on the ++ or -- links
increments and decrements the count. What happens when you use the back
button? Try this:
- start a new counter (http://localhost:9090/seaside/counter)
- click on ++ until the counter shows 6
- hit the back button to where the counter shows 2
- hit the -- link
You probably expect it to show 1 (2--), but it shows 5 instead. Why?
We've been using the same instance of this component the
whole time, and chronologically, the last value of the counter was 6.
We're asking it to decrement, and it does.
This may not seem like a huge problem in the case of the counter, but
inconsistency is a Bad Thing. The page was showing 2, and you asked it to
do something with (you thought) 2, but it applied the action to 6 instead.
If, say, the page was showing a product instead of a number, and the link
was "buy" instead of "--", you could have real problems.
2. Seaside solves this by introducing a class for holding state that
needs to be backtrack-aware. A StateHolder is just like a ValueHolder,
but it detects use of the back button, and adjusts its state accordingly.
To be more specific: whenever a link is clicked on a page, all of the
StateHolders for the current session take on the value that they had when
that particular page was produced.
We can get a StateHolder by asking the current session for one
(WASession>>stateHolder). So, to modify WACounter to be consistent under
backtracking, we must do two thinhgs - first, use the "abstract variable"
refactoring to replace all references to 'count' with sends to accessor
methods, so, for example,
count := count + 1
self count: self count + 1
(this is something the RB can do for us).
Then, we must make the 'count' instance variable hold a StateHolder
count := session stateHolder.
self count: 0.
Finally, we must implement the accessors:
^ count contents
count contents: aNumber
If we perform the experiment from before, when we click the -- when
showing 2, we should now (correctly) see 1.
3. Although you can use a StateHolder anywhere you like in your code, it
should only be necessary to use it in very specific cases. First of all,
you should never use it in your domain model code - it would be very odd
for customer data, for example, to disappear just because the user hit the
back button. This is *not* meant to be a global state rewind mechanism.
Instead, it is meant to be used for *UI* state. Examples of UI state are:
- the current record being shown on a page
- the currently selected page in a batch of search results
- the currently selected pane of a tab panel
- which nodes on a tree widget are expanded
and so on. This is all state that is non-crucial to the domain, but that
the user expects to be consistent with what they see on the page, when
they click a link.
There are essentially three kinds of UI state you might find in a
component. These are
- immutable state, that is set on initialization of a component and never
changes. For example, a form that edits a customer's info will likely be
instantiated with the customer it is to edit, and this value never
- transient state, that is reset with every request. For example, the
current values of form fields (which will be modelled by inst vars in
the component) are changed every time the form is submitted.
- persistent but mutable state, that doesn't fall into either of the
previous two categories. It is only this last kind for which StateHolders
4. It is great to be able to provide the abilitiy to safely backtrack, but
sometimes you want to prevent your users from backtracking. For example,
consider a typical online store, where after filling a shopping cart,
users are led through a checkout process. You may well want to be able to
backtrack during that process (mistyped the shipping address), but once
the credit card has been charged, you don't want the user to be able to
backtrack change the contents of the shopping cart!
Seaside provides the WASession>>isolate: method to control backtracking.
#isolate: is passed a block, and the block is immediately evaluated. When
the block ends, any pages that were created within it immediately expire.
If the user backtracks to them and clicks on any link or submits any form,
they will be notified that that page has expired and be redirected to a
non-expired page. #isolate: calls can also be nested.
More information about the Seaside