Conversational Pitfalls
Seam conversations have certain rules that you need to be aware of when using them. This article came about because for the last couple of years, the same questions have been asked on the Seam forums regarding conversations. It is also a couple of issues that cropped up while I was working on the Seam vs. Spring Web Flow articles. Some of the problems are uncannily similar with similar solutions, so parts of this series may be of interest to non-Seam users. Additionally, it seems like a lot of this stuff will also apply to the conversational pieces of JSR 299 – Contexts and Dependency Injection which will be a part of JEE 6.
Conversational Fundamentals
Let’s start with a fairly descriptive and steady paced description of what conversations are and what they are not. Some of these behaviors are probably more figurative that literal. This mainly applies to Seam, but is also similar to CDI.
Conversations in Seam are like buckets that hold conversational data. A user session has a list of conversation contexts each identified by a conversation Id. A conversation is either long running or temporary and even when you are not in a long running conversation, you are using a temporary conversational context or bucket. The only difference between a long running conversation and a temporary conversation is the boolean longRunning
flag on the conversation instance. When you start a conversation, all that happens is the longRunning
flag on the conversation is set to true. When you end the conversation, the longRunning
flag is cleared. There is no destruction of conversational objects and no clearing out of the conversation. Abandoned long running conversations sit in the conversation list until they time out. Note that in JCDI (JSR 299), the long running flag has been renamed to a isTransient
flag on the conversation.
Components that are scoped to the conversation are still held in the conversation context regardless of whether it is long running or temporary. The only difference with a long running conversation is that the id for the conversation is propagated from one page to the next. When the id is propagated, that same conversation ‘bucket’ is used by the next page and the objects that were in the previous page are accessible in the new page. In each request, Seam determines if the conversation is long running, and if so, it passes the conversation id to the next page as a parameter. Obviously, this propagation only takes place with faces requests, or GET requests that have special handling like manual conversation propagation or you are using the Seam JSF controls that provide GET requests with automatic conversation propagation.
When rendering a new page, at some point Seam will need to access the conversational context. It does this by looking to see if the request contained a conversation id and if so, looking up the existing conversation context instance for that id. Otherwise, it obtains a new conversation instance with a new conversation Id. When there is no long running conversation, no id is propagated so each page obtains a new conversation instance and Id. When we start a long running conversation Seam will automatically propagate the conversation id from one page to the next for us which is how we write applications with multiple pages using the same conversation.
When you end a conversation, the longRunning
flag is set to false and Seam no longer propagates the id from one page to the next. However, when you end a conversation on one page, Seam will still propagate the conversation to the next page one last time. This allows us to carry things like faces messages and any other data over the conversation boundary. This next page gets to use the same conversation instance that just finished outside of the conversation boundary. It has a number of benefits as well as some important consequences that can cause many problems that are tricky to track down.
If you end your conversation and set the before-redirect
attribute to true the conversation is ended before the redirect so the conversation id is not propagated and the new page starts with a fresh conversation context. The problem with this is that since there is no propagation of the conversation, faces messages (and any other information you wanted to carry to the new page) is not propagated either. This is to be expected since you explicitly specified that you wanted to end the conversation before the new page.
There are also times when Seam will propagate the conversation from one page to the next when it is not long running. If a redirect is taking place, then Seam promotes the conversation to long running temporarily so the conversation id parameter is passed to the redirect. The conversation is then demoted again to a temporary conversation. This allows items such as faces messages to propagate so you can show messages even after a redirect.
One useful way of seeing which conversation you are using is to add Conversation = #{conversation.id}
to your template so you can see what conversation id the page is using. This way you can see whether or not the conversation has propagated and whether you are using the same value from the previous page. If the conversation id on two pages is the same, they are using the same conversation instance and sharing data.
Hopefully, this section has provided enough of an overview into Seam conversations, cleared some of the misconceptions up and will make understanding some of the conversational pitfalls easier.
Dirty Data
When you end a conversation, the conversation instance contains multiple pieces of data. Let’s take the scenario of editing a widget, and clicking save or cancel and being taken back to the widget view page. Our Seam page uses a factory method to generate the #{widget}
instance from our WidgetHome
bean. We may edit the instance, and post back the changes, and then decide to cancel our changes. At this point, our widget in the conversation is dirty, it has changes made to it that we have cancelled. If we do nothing and just go back to the view page, the conversation will be re-used since we propagate that ended conversation id over one last time. The #{widget}
references in the view page will refer to our existing dirty instance in the re-used conversation. Since the variable exists in the conversation already, Seam won’t call the factory method to get a fresh instance. As a result our view page shows the dirty instance with all our modifications.
This problem can be solved in a couple of different ways, each with its own problems. The simplest is to use before-redirect=true
when ending the conversation. This way our view page won’t use the dirty data from the ended conversation and it starts with a fresh conversation. This may be a problem though if you are passing the widgetId parameter from the previous page. Pages.xml lets you easily add parameters to redirects, but pageflows don’t. It also means that you won’t be able to pass any other data over such as a message to say you have cancelled changes. Another similar, but more forceful, way of doing this is just to have a non-faces ‘cancel’ link on your edit page that takes you straight to the edit page with a GET request and passes in the widget Id. In this scenario, the conversation is abandoned since we used a link and it will not propagate the conversation or the associated dirty data over with it, nor can you pass any cancellation messages over.
The other method is to let the view page re-use the conversation, but when the user cancels the changes, actually refresh the entity that has been changed. You create a cancel method on the entity home bean which is called when the user cancels their changes and it will refresh the entity so it is no longer dirty. This has the benefit of causing minimal disruption, but if you have a long chain of objects that ended up dirty, making sure you refresh them all can be tricky.
Stale Data
The stale data problem is similar to the dirty data problem except the problem occurs within the same conversation and without ending it. If we have some data in a conversation that depends on other values in the conversation, when those values change, we have to make sure we update our data, otherwise it will become stale. For example a paginated list of sales orders from on an EntityQuery means that we need to get a new set of results when we click next or previous. However, if the results of the query are outjected from the query bean and referenced as #{orderList}
in the JSF page, then we will see the stale data problem. The first time we enter the page, the variable #{orderList}
doesn’t exist, so Seam calls the factory for #{orderList}
which returns the list of the first 10 results. Our datatable shows the list of results using the variable orderlist
.
<h:dataTable value="#{orderList}"> <!-- Columns Here --> </h:dataTable>
The user clicks next in the paginator which does a postback, increases the firstResult
value and causes a refresh on the result list referenced by the entity query to show the next 10 results. The page is rendered but the displayed results are exactly the same. We are still looking at the first 10 rows. Why? Because the orderList
context variable is used by the view to obtain the list of orders. When the page is rendered the second time, JSF looks for the value of #{orderList}
and finds it already exists in the conversational context even though it contains the first 10 results, and not the second 10. Seam has no reason to call the factory method to obtain the latest values. In essence, we have two sets of results, the original set which is referenced by the orderList
seam variable, and the current set which is the correct list of results and is held by the query bean.
We have two ways of fixing this problem. The first is to change the value our data table is referencing so it is always referencing the result set on the query bean directly.
<h:dataTable value="#{orderQuery.resultList}"> <!-- Columns Here --> </h:dataTable>
With this code, there is no El expression used that can become stale. When the page is re-rendered the table will get the results from the order query directly each time ensuring that the most up to date values are used. This is the easiest solution but it cannot be used if you are using the DataModel
annotation since Seam requires you to use the outjected data model variable. In such cases refer to the second solution which is to invalidate the orderList
variable when we refresh the search results. This is fairly easy, but requires us to override the refresh
method in the entity query. When we refresh the list of orders, we simply remove the #{orderList}
context variable. i.e.
@Override public void refresh() { super.refresh(); Contexts.removeFromAllContexts("orderList"); }
This means that when the user causes a refresh of the query data, the variables referencing that data will be ejected from all contexts and the next time that value is requested in a JSF page, the appropriate Seam factory will be called to get the most up to date result set.
Starting a conversation
Even starting a conversation can be a problem. In most cases, we utilize the join="true"
attribute to gloss over the fact that we are going into a page that requires a conversation with or without a conversation already started. When we hit our target page, we know that we will be in a conversation, either a newly started one, or an existing conversation. We may not care which except that not considering our conversation start and end points has side effects. Using the join attribute is not a substitue for thinking about conversation demarcation.
First is the issue of old data where a new page may re-use data from the last page because they share the conversation. For example, we are viewing a widget and the widget instance is loaded into the temporary conversation. 10 minutes later, we click edit which takes us to the widget edit page and starts a long running conversation. The edit page needs an instance of a widget which it already has from the view page, but this instance of a widget is 10 minutes old and has been edited by another user since you loaded the view page and therefore, you need to do a refresh. This problem arose without us even starting a long running conversation in the view page. The answer here is to ensure that you are always starting with a fresh conversation instance when you go in to the edit page. The easiest way to do that is to use propagation="none"
on the link to edit the widget, or just use a plain old GET link.
If a page is meant to start a conversation and also start a pageflow then you cannot enter that page with an existing conversation. If you do, the page will join the existing conversation and the pageflow will not start. Pageflows can only be started with a fresh conversation. Note that initially a page may be simple enough that you don’t need a pageflow and you could ignore conversation demarcation, but if you later start a pageflow on that page, you could end up with all sorts of headaches as invoking that page from different places may or may not start the pageflow depending on whether the page you were on previously had a long running conversation.
Menus
Always consider your menus and their links and what pages they will be used in. They could be called from pages where there may or may not be a long running conversation. Consider whether that conversation is propagated or not. Typically (and luckily) most links in menus are to top level items and thus conversation propagation is never really required which means you can just never propagate it. Propagation is mostly task centric and therefore is more likely to appear in the main page itself rather than a general application wide menu.
One other problem I found was with rich menu items which only allow an action on the menu item. Typically to end or demarcate a conversation, you would need an s:link
or s:button
since only these Seam specific controls were are aware of the concept of conversation propagation. Since our menu is shared across all pages if we were in a pageflow, we could end up with Illegal Navigation issues since we don’t account for global menu navigation in our pageflows. Alternatively, we might be in a conversation and calling a page that starts a new conversation with a pageflow so we need to end the conversation before we get there so we can start the new conversation and also the new pageflow. In a nutshell, we need to end any current conversation then re-direct to a new page where we will start a new conversation. We also need to be able to do this from a JSF action
since some menu components require actions instead of allowing you to add links.
One solution to this is to use a navigator component that will perform these tasks for us and we just pass in the view-id that we want to go to.
@Name("navigator") @Scope(ScopeType.STATELESS) public class Navigator { public void gotoView(String view) { if (Conversation.instance().isLongRunning()) { Conversation.instance().end(true); } Redirect.instance().setViewId(view); Redirect.instance().execute(); } }
With this, we can define our action methods as #{navigator.gotoView("/startBookingProcess")}
and when that menu item is selected, it will end the conversation and perform the redirect so on our new page we can start a new conversion and pageflow.
Conversational Best Practices
These are best practices that I’ve found working with Seam and conversations in general. Obviously, it’s a personal thing, and some people may disagree with me on them, but these rules have served me and my team well.
Always determine where a page fits in terms of conversations and always treat that page the same way. For example, edit and view pages I tend to isolate from other conversations. I tend to only use the temporary conversation for the view page so if you click refresh, it reloads the data which is really what you want, a refreshed view of what you are looking it. Any links to a view (or edit) page do not propagate the conversation at all.
I always start conversations in pages.xml
since this provides a single common place to do it. I’ve never really used the annotations since it requires knowledge of the target page to know which method you need to call. To edit a widget, with pages.xml you can just go to /widgetEdit.seam?widgetId=234
as opposed to calling a specific method annotated with @Begin
.
Hope this has helped guide you through some of the complexities of conversations in Seam. Once you understand the basic principles, it makes it much easier to figure out why something isn’t working quite right, and some of these bugs relating to stale and dirty data can be subtle and sneaky.
4 thoughts on “Conversational Pitfalls”
Comments are closed.
Thanks for the helpful guidelines Andy 🙂
We have use Seam at our company as well, and have found that even though conversations are quite nice conceptually, using it incorrectly can get you into a bit of trouble. In addition, we have found you have to be quite careful using SMPC with nested conversations, since even though the conversation is nested, the PC is not, so you may end up persisting things which you didn’t intend to persist.
Yes, I think I’ve wrote about that issue before in one of my Seam documents, relating to the fact that multiple nested conversations use the same entity manager. However, I tend not to use nested conversations.
Thanks Andy, great insight and a delightfully composed blog – I have to agree that you have to be careful using SMPC with nested conversations – its a real bugger and really gets under my skin! So from this day forward, I will use your line of thinking and no longer use nester conversations.
Many thanks.
Thanks alot for this wonderful sharing! I have been struggling with conversations for a while even after having implemented 2 SEAM applications and now in the midst of another. Finally, someone says it like it is. Oftentimes, the official SEAM documentation just glosses over all these ugly stuff and harps on the great functionalities that SEAM offers to the world until…poor developers like me keep banging their heads on the wall….haha. Reading your article is like the revelation. Good job, man! Many thanks. 😉