| Project moved under Tynamo! Please visit us at http://tynamo.org/tapestry-conversations+guide |
Conversations for Tapestry5!
Have you ever wished to have a scope longer than a request but shorter than session?
Have you ever spent hours optimizing your pages just to avoid the use of sessions?
Have you ever wondered why you have to elaborately describe your page flows in a lengthy XML format, when all you want to do is to make your objects survive through redirect-after-post pattern?
Don't you just wish that your web framework would automatically clean up the session-persisted objects once the user is done using them?
Don't you just hate it when you are browsing for that absolutely cheapest hotel at your favorite hotel search site and you start a second search on a new tab, then come back to the first tab only to see the results from the second search being displayed when you click on the next page link?
If you answered yes to any of the questions above, tapestry-conversations is for you ![]()
Trails takes the view that a conversation should happen on a single page (obviously interacting with the same page either with redirect-after-post or ajax). I say Trails, but this module is available for Tapestry 5 with no dependencies to other Trails modules. While it's not as a generic as conversations in JBoss Seam or Spring Web Flow, the conversation-within-page convention makes it far simpler to automatically manage the conversation state and discard the conversation when the page is not used anymore. The thinking is that many short-lived, on-going conversations with a tight memory (session) management should generally be more useful in a web application than complex wizard-type flows. A conversation still needs to be opened manually in the code (typically in your onActivate) because you want (you really do, trust me) the conversation to behave deterministically. If you want your page to support multiple tabs (i.e. have multiple conversations using the same page open at the same time), you need some identifier (typically in the url) to keep the conversations separate. Also if your conversation started automatically, you might easily loose the previous conversation and get a new conversational context without being able to notify the user.
In Trails conversations, the conversation identifier is stored either as a cookie or as part of the context. Storing it as part of the context makes it possible to have multiple conversations at the same on separate tabs. Cookies are nice because they are transparent and don't change the url, but if the page url doesn't differ in any other way, it's not possible to have more than one conversation open (per user and per page).
To use the feature, you need to add the following dependency to your pom.xml:
For those who enjoy the thrill of living on the bleeding edge
, automatically deployed, unstable snapshots are hosted at Codehaus' CI repository (http://ci.repository.codehaus.org/org/trailsframework/tapestry-conversations/) and manually deployed, sometimes more stable snapshots at snapshot repository (http://snapshots.repository.codehaus.org). If you are going to use SNAPSHOTs, I suggest using the CI ones - the manually deployed ones will always lag behind.
Then, on the page you need to add a org.trailsframework.conversations.services.ConversationManager:
Depending on which conversation option you use (cookie / context parameter) you have two patterns to choose from. For a page supporting a single conversation at a time you can do:
Then you create the conversation in onActivate:
Note using @Persist("session") makes it clear there can be only conversation at a time. All other persisted properties can be decorated as follows:
Or, with pages using conversation scope, it's generally easier to use meta annotation @Meta("tapestry.persistence-strategy=conversation") so you don't have to explicitly specify strategy in @Persist annotation. This works well for components with persisted properties as well.
If your page should support multiple conversations, the usage pattern is as follows:
Notice how we force a redirect in onActivate() (the one without parameters) if the page is not initialized (and the conversation hasn't been started). In onActivate that accepts the conversationId as a parameter, we have to check if it's valid conversation an if not, we again force a redirect. Note that we are not persisting conversationId in this case, but simply relying on passing it along using the activation context. If you choose to keep conversationId as a context parameter, the implementation assumes the conversation id is the last parameter of your activation context. Certainly there are other alternatives for creating and using a conversational scope, but the important thing is to keep in mind that you need to manage the conversation identifier to get a hold of the conversation and that when you create a new conversation, a user should be able to return to it even if he refreshes the browser.
Okay, now that you know the principle and options you have for creating conversations, let's focus on how you might want to use conversations in real world. At times it may be useful to start with an empty conversational context, but in practice a much common use case is that you want to initialize a page with some context and then start a conversation in that context, or perhaps create and modify new objects relevant in that context. Let's say you have a Class (Course) that you want to register for. The registration involves filling out several fields and includes semantic validation so you can enter some values, submit them partially and advance to next section (for example you can take the same Course at different times, so you first pick the time you want, etc.) The system accepts Course as the initial context, then creates a Registration object and starts a conversation, letting you modify the same Registration object while making multiple round-trips to the server and back. You can also have several registrations available on different tabs so you can make the schedule work for you. The code could be something like this:
Idle conversations are ended automatically when new conversations are started. There's also an optional ConversationModerator component that can be used to end a conversation when user navigates away from the page, to check if a conversation has been idle for too long or to warn the user that the conversation is about to become idle. Any request to the same page (render, component event) will reset the idle timer by default. If you do not want that to happen you can pass in "keepalive=false" as a URL parameter.
Using ConversationModerator component
ConversationModerator is an automatic, invisible ajax component. To use it on a page, add:
<t:conversation.moderator/>
There are several parameters that you can set that change the behavior of the component, the following table should explain them:
name |
description |
default value |
|---|---|---|
idlecheck |
the initial delay after which component checks if the current conversation has become idle. If idlecheck is set, the timing for subsequent checks depends on the keepalive value and the conversation's maxIdleSeconds value |
15 |
warnbefore |
the initial delay after which component checks if the current conversation has become idle |
15 |
warnbeforehandler |
a string identifier for the Javascript function to be called if warnbefore is set. Of form [object.property.]function. If the identifier specifies context (e.g object.property) this is set to that context. If not set, but warnbefore is set, displays an alert box |
null |
endedhandler |
Similar to warnbeforehandler, but called after the conversation has becomed idle. |
null |
keepalive |
Set to make idlecheck to reset the idle counter. Useful when you want to make sure a conversation doesn't become idle until user closes the browser |
false |
In essence, a ConversationModerator allows you to maintain the memory resources more tightly on the server with small increase in network traffic. You might wonder why you couldn't just use browser's onUnload event to end conversations. Obviously that won't work if users just leave the pages open in their browsers (as people frequently do), but more importantly it will not work either when users refresh the page or when a user invokes a (non-ajax) action on the page, causing redirect-after-post. The few requests the component makes for more conservative server session management should be well worth the increased network.
