Page History Navigation in stateful applications...
Moderator: General Moderators
Page History Navigation in stateful applications...
I've been grappling with this problem for years -- proper behavoir of the browser back/forward buttons in heavily stateful web applications.
When I started my project years ago, not knowing too much about web applications, I built in a "back button detector" based on HTTP_REFERER to send users to an "error" page if they used it. Horrible, I know. I am hoping to finally remedy that situation, but I'm unsure how to proceed.
1. First I'm not entirely sure what the user expects that back button to do. In most e-commerce type sites, back!=undo, e.g. it doesn't remove an item from your shopping cart.
2. My application is not quite a regular "shopping cart" system, though it does share some similarities. Its a site for ballroom dances, targetting the collegiate competitive dance crowd, where a given individual often has multiple dance partners at a competition. Here's the nominal navigation flow:
A. User arrives at the site, enters his/her identification information. (Not a login)
A.1. System checks if this information is close to anything already on-file. If there is a match it presents the match, and asks the user if s/he is any of the matches (user confirms via an autogenerated password at previous registration time.)
B. User arrives a "partners.php", this show all current registrations and offers a form for adding a new partner (as well as dropping events, etc). The user enteres information for the new partner and submits.
B.1 System checks if the partner is a close match for an existing entry (common problem would be "Jen" versus "Jenny" for firstname when entered by different people, etc). User either selects an existing match or instructs it to create a new record.
C. Returns to partner's for visual review of the changes and/or adding additional partners.
While users have complained about my "back button prohibition" I'm not sure where they are trying to use it. I suspect in most cases its to return to the "Match pages" A.1 and B.1 to change their selection after deciding that maybe they chose incorrectly and want to choose a different option. They might be trying to "go back" to edit a new partner as well, instead of selecting the "edit partner" option next to their current registrations.
In all cases, the operations have greatly affected the state of the system, both in terms of $_SESSION and database contents. (Earlier versions of the system that required an explicit "checkout" to finalize registrations resulting in countless unintended abandoned registrations -- thus the per page style database commits as the process proceeds). Because the session data is changed and because the database is changed, returning to a given page later, will not create the original page, etc.
I've been thinking about possible solutions. Here are two possible approaches (not exclusive of each other).
1. Nonce's (Not More than Once): assign a strictly increasing identifier on each page, embed it in the forms, and compare to the session nonce on the submitted page. Can be used to detect stale/new submissions/resubmissions. Can be used as either a more robust back-button checker (some browsing environment/proxies don't set HTTP_REFERER, etc properly) or to branch execution base on old/new content.
2. Full "Session History" -- GoF style undo/redo using State/Memento/Command design patterns. Extremely non-trivial for a complete inplementation in the case of some cascading deletes in the DB.
Has anyone found solutions to this problem that they like?
When I started my project years ago, not knowing too much about web applications, I built in a "back button detector" based on HTTP_REFERER to send users to an "error" page if they used it. Horrible, I know. I am hoping to finally remedy that situation, but I'm unsure how to proceed.
1. First I'm not entirely sure what the user expects that back button to do. In most e-commerce type sites, back!=undo, e.g. it doesn't remove an item from your shopping cart.
2. My application is not quite a regular "shopping cart" system, though it does share some similarities. Its a site for ballroom dances, targetting the collegiate competitive dance crowd, where a given individual often has multiple dance partners at a competition. Here's the nominal navigation flow:
A. User arrives at the site, enters his/her identification information. (Not a login)
A.1. System checks if this information is close to anything already on-file. If there is a match it presents the match, and asks the user if s/he is any of the matches (user confirms via an autogenerated password at previous registration time.)
B. User arrives a "partners.php", this show all current registrations and offers a form for adding a new partner (as well as dropping events, etc). The user enteres information for the new partner and submits.
B.1 System checks if the partner is a close match for an existing entry (common problem would be "Jen" versus "Jenny" for firstname when entered by different people, etc). User either selects an existing match or instructs it to create a new record.
C. Returns to partner's for visual review of the changes and/or adding additional partners.
While users have complained about my "back button prohibition" I'm not sure where they are trying to use it. I suspect in most cases its to return to the "Match pages" A.1 and B.1 to change their selection after deciding that maybe they chose incorrectly and want to choose a different option. They might be trying to "go back" to edit a new partner as well, instead of selecting the "edit partner" option next to their current registrations.
In all cases, the operations have greatly affected the state of the system, both in terms of $_SESSION and database contents. (Earlier versions of the system that required an explicit "checkout" to finalize registrations resulting in countless unintended abandoned registrations -- thus the per page style database commits as the process proceeds). Because the session data is changed and because the database is changed, returning to a given page later, will not create the original page, etc.
I've been thinking about possible solutions. Here are two possible approaches (not exclusive of each other).
1. Nonce's (Not More than Once): assign a strictly increasing identifier on each page, embed it in the forms, and compare to the session nonce on the submitted page. Can be used to detect stale/new submissions/resubmissions. Can be used as either a more robust back-button checker (some browsing environment/proxies don't set HTTP_REFERER, etc properly) or to branch execution base on old/new content.
2. Full "Session History" -- GoF style undo/redo using State/Memento/Command design patterns. Extremely non-trivial for a complete inplementation in the case of some cascading deletes in the DB.
Has anyone found solutions to this problem that they like?
How's this:
(1) issue a redirect to the next page after processing each submission
The redirect stops form resubmissions if refresh is clicked (you can filter these out in other ways but this is probably simplest). Refresh-submissions as well as genuine "secondary submissions" complicate the application controller logic.
(2) send no-cache headers for each page in the sequence
No-cache headers avoid stale, browser-cached pages when the back button is clicked.
(3) keep a tally of form submissions
With a record of form submissions, you can for example detect if they arrived at B by submitting forms A or C, or by using the back button. You should be able to apply whatever logic you need to interpret requests into domain manipulations - possibly there will be stuff to roll back if they've been clicking the back button. Or...
(4) UnitOfWork
They could of course back-button right out of the wizard altogether so a UnitOfWork might be the answer. This could store changes as they move back and forward through the page sequence. Page views would display the current state of the UnitofWork (updated with each submission/back button click according to your application controller logic). Finally, the UnitOfWork commits to persistent storage only if/when they click an "I'm done now" link.
(1) issue a redirect to the next page after processing each submission
The redirect stops form resubmissions if refresh is clicked (you can filter these out in other ways but this is probably simplest). Refresh-submissions as well as genuine "secondary submissions" complicate the application controller logic.
(2) send no-cache headers for each page in the sequence
No-cache headers avoid stale, browser-cached pages when the back button is clicked.
(3) keep a tally of form submissions
With a record of form submissions, you can for example detect if they arrived at B by submitting forms A or C, or by using the back button. You should be able to apply whatever logic you need to interpret requests into domain manipulations - possibly there will be stuff to roll back if they've been clicking the back button. Or...
(4) UnitOfWork
They could of course back-button right out of the wizard altogether so a UnitOfWork might be the answer. This could store changes as they move back and forward through the page sequence. Page views would display the current state of the UnitofWork (updated with each submission/back button click according to your application controller logic). Finally, the UnitOfWork commits to persistent storage only if/when they click an "I'm done now" link.
I already get that protection. Each visible page submits to an "interstitual" page. The interstitual pages handle logic and then redirect to a new page (either back to the previous for error fixing or forward). The institual pages kidna form the C of MVC, but was designed long before I knew about them (so its not a paticularly good implementation of it...)McGruff wrote:How's this:
(1) issue a redirect to the next page after processing each submission
The redirect stops form resubmissions if refresh is clicked (you can filter these out in other ways but this is probably simplest). Refresh-submissions as well as genuine "secondary submissions" complicate the application controller logic.
Hmm, I thought no-cache headers didn't stop cacheing for back button, state-history type needs (or at least many browsers didn't respect it for that). But I'll give it a go.(2) send no-cache headers for each page in the sequence
No-cache headers avoid stale, browser-cached pages when the back button is clicked.
OK, this sounds a little bit like my "nonces" and a "light" version of the session history. I think its what I'll try to implement this weekend.(3) keep a tally of form submissions
With a record of form submissions, you can for example detect if they arrived at B by submitting forms A or C, or by using the back button. You should be able to apply whatever logic you need to interpret requests into domain manipulations - possibly there will be stuff to roll back if they've been clicking the back button. Or...
This is probably my preferred solution (but also the hardest to implement); however I know from past experience that my users won't click the "I'm done" reliably so I can't do it.(4) UnitOfWork
They could of course back-button right out of the wizard altogether so a UnitOfWork might be the answer. This could store changes as they move back and forward through the page sequence. Page views would display the curent state of the UnitofWork (updated with each submission/back button click according to your application controller logic). Finally, the UnitOfWork commits to persistent storage only if/when they click an "I'm done now" link.
Thanks for the comments.
I did a quick check with form sequences in Firefox 1.0, & IE6 - one with and one without caching. As expected, back-buttoning didn't update pages without the no-cache headers, but both browsers seemed to respect these if present. Not an exhaustive test though so you could be right that this can't always be relied upon.nielsene wrote:Hmm, I thought no-cache headers didn't stop cacheing for back button, state-history type needs (or at least many browsers didn't respect it for that). But I'll give it a go.
Could you force them to do what you want by removing all links from the page except "submit" and "I'm done"? The only way out of the wizard would be to back-button back to where they came from or to click the "I'm done" link.nielsene wrote:This is probably my preferred solution (but also the hardest to implement); however I know from past experience that my users won't click the "I'm done" reliably so I can't do it.
Nope, they close the browser... Sigh... The application-programmer in me wants to say "stupid users",but the UI-side can understand them.... Partially because the site is only registration, not payment -- therefore people aren't expecting to have to "check-out" or any such formal commit process.McGruff wrote:Could you force them to do what you want by removing all links from the page except "submit" and "I'm done"? The only way out of the wizard would be to back-button back to where they came from or to click the "I'm done" link.nielsene wrote:This is probably my preferred solution (but also the hardest to implement); however I know from past experience that my users won't click the "I'm done" reliably so I can't do it.
Would be very interested to hear about these, although I can't promise I'll know enough to help solve any dilemmas.nielsene wrote:Hopefully I'll be reposting some of my Unit testing dilemmas in the near future, franticly working towards a code roll out.....)
I'm utterly test-infected these days so much so that I don't seem to write code any more: instead I write tests and code just seems to happen incidentally.
- John Cartwright
- Site Admin
- Posts: 11470
- Joined: Tue Dec 23, 2003 2:10 am
- Location: Toronto
- Contact:
I've written a flowcontroller class that allows you to build up a stack with tasks.. Source and some tests
It has methods:
flowcontroller -> constructor allows you to set the startpage
checkTask() -> test if the user is where is should be (if not, redirect him)
previousTask() -> go back, unwind the stack
nextTask($task) -> go to next task
From this stack you could also build "breadcrumbs" so the user can see where he has been before and decide to go back.. Meaby it becomes clearer if you try it yourself: http://timvw.madoka.be/phpkist/tests/fl ... .page1.php
It has methods:
flowcontroller -> constructor allows you to set the startpage
checkTask() -> test if the user is where is should be (if not, redirect him)
previousTask() -> go back, unwind the stack
nextTask($task) -> go to next task
From this stack you could also build "breadcrumbs" so the user can see where he has been before and decide to go back.. Meaby it becomes clearer if you try it yourself: http://timvw.madoka.be/phpkist/tests/fl ... .page1.php
I tried playing with your demo a little, but it seems to lose history as you go through at times (at series of links greater than 3).timvw wrote:I've written a flowcontroller class that allows you to build up a stack with tasks.. Source and some tests
It has methods:
flowcontroller -> constructor allows you to set the startpage
checkTask() -> test if the user is where is should be (if not, redirect him)
previousTask() -> go back, unwind the stack
nextTask($task) -> go to next task
From this stack you could also build "breadcrumbs" so the user can see where he has been before and decide to go back.. Meaby it becomes clearer if you try it yourself: http://timvw.madoka.be/phpkist/tests/fl ... .page1.php
Plus a history by itself is insufficient. You basically need to ensure that all page transitions are idempotent/reversible. This almost entails storing a snapshot of the complete database on each page transition along with the values of any GPC's involved in that transition.
Or you could approach this from the point of very long running transactions -- rollback/commit/savepoints -- as this would save you from having to explicitly snapshot the DB. However, its extremely bad to keep a transaction open across a page load, so that's out....
Well, that was intendednielsene wrote: I tried playing with your demo a little, but it seems to lose history as you go through at times (at series of links greater than 3).
I've been thinking about the idea of using transactions, but as you say it's a PITA to have transactions that last longer than one script/run.nielsene wrote: Plus a history by itself is insufficient. You basically need to ensure that all page transitions are idempotent/reversible. This almost entails storing a snapshot of the complete database on each page transition along with the values of any GPC's involved in that transition.
Another option is keeping an audit table (table, row, column, oldvalue, newvalue) that tracks all changes to the database. But that isn't really workable if you have cascading delete/update either...
A complete copy of the database seems impractical.. Meaby that a solution with views is more realistic. Every time a user performs a task, view with the current state of the database are generated...
I'm not a big fan of the "meta-audit" table that a lot of peopole use, exactly as you describe. I greatly prefer the model advocated by Date, Darwen, and Lorentzos in Temporal Data and the Relational Model. About 10% of my tables have been converted to their "full temporal" design, as best as can be realized within the limitations of SQL based DB's. If I finish the temporalization (massive under taking), then I would basically have a CVS-like ability to roll forward/back to arbitrary points in time, without requiring script-spanning transactions.timvw wrote:Another option is keeping an audit table (table, row, column, oldvalue, newvalue) that tracks all changes to the database. But that isn't really workable if you have cascading delete/update either...
A complete copy of the database seems impractical.. Meaby that a solution with views is more realistic. Every time a user performs a task, view with the current state of the database are generated...
They gist of their design is extending each attribute of each relation to have a "_since" timestamp, auguented by a series of history tables -- one for each key, attribute pair -- from the base relations with a interval stamped valid time. (Their intervals are closed pairs of timestamps, basically.) They then discuss a huge amount of other required infrastructure support required, but it makes a remarkably powerful/consistent/logical system. The decomposition of historic and current data, they in part refer to as the 6th Normal Form.
**** NOTE: you might need to look at the l33t speak filter -- it didn't allow me to post something underlined, the square bracket "u" square bracket was getting expanded to "you".