Chapter 6. Conversations and workspace management

It's time to understand Seam's conversation model in more detail.

Historically, the notion of a Seam "conversation" came about as a merger of three different ideas:

By unifying these ideas and providing deep support in the framework, we have a powerful construct that lets us build richer and more efficient applications with less code than before.

6.1. Seam's conversation model

The examples we have seen so far make use of a very simple conversation model that follows these rules:

  • There is always a conversation context active during the apply request values, process validations, update model values, invoke application and render response phases of the JSF request lifecycle.

  • At the end of the restore view phase of the JSF request lifecycle, Seam attempts to restore any previous long-running conversation context. If none exists, Seam creates a new temporary conversation context.

  • When an @Begin method is encountered, the temporary conversation context is promoted to a long running conversation.

  • When an @End method is encountered, any long-running conversation context is demoted to a temporary conversation.

  • At the end of the render response phase of the JSF request lifecycle, Seam stores the contents of a long running conversation context or destroys the contents of a temporary conversation context.

  • Any faces request (a JSF postback) will propagate the conversation context. By default, non-faces requests (GET requests, for example) do not propagate the conversation context, but see below for more information on this.

  • If the JSF request lifecycle is foreshortened by a redirect, Seam transparently stores and restores the current conversation context—unless the conversation was already ended via @End(beforeRedirect=true).

Seam transparently propagates the conversation context across JSF postbacks and redirects. If you don't do anything special, a non-faces request (a GET request for example) will not propagate the conversation context and will be processed in a new temporary conversation. This is usually - but not always - the desired behavior.

If you want to propagate a Seam conversation across a non-faces request, you need to explicitly code the Seam conversation id as a request parameter:

<a href="main.jsf?conversationId=#{conversation.id}">Continue</a>

Or, the more JSF-ish:

<h:outputLink value="main.jsf">
    <f:param name="conversationId" value="#{conversation.id}"/>
    <h:outputText value="Continue"/>
</h:outputLink>

If you use the Seam tag library, this is equivalent:

<h:outputLink value="main.jsf">
    <s:conversationId/>
    <h:outputText value="Continue"/>
</h:outputLink>

If you wish to disable propagation of the conversation context for a postback, a similar trick is used:

<h:commandLink action="main" value="Exit">
    <f:param name="conversationPropagation" value="none"/>
</h:commandLink>

If you use the Seam tag library, this is equivalent:

<h:commandLink action="main" value="Exit">
    <s:conversationPropagation type="none"/>
</h:commandLink>

Note that disabling conversation context propagation is absolutely not the same thing as ending the conversation.

The conversationPropagation request parameter, or the <s:conversationPropagation> tag may even be used to begin and end conversation, or begin a nested conversation.

<h:commandLink action="main" value="Exit">
    <s:conversationPropagation type="end"/>
</h:commandLink>
<h:commandLink action="main" value="Select Child">
    <s:conversationPropagation type="nested"/>
</h:commandLink>
<h:commandLink action="main" value="Select Hotel">
    <s:conversationPropagation type="begin"/>
</h:commandLink>
<h:commandLink action="main" value="Select Hotel">
    <s:conversationPropagation type="join"/>
</h:commandLink>

This conversation model makes it easy to build applications which behave correctly with respect to multi-window operation. For many applications, this is all that is needed. Some complex applications have either or both of the following additional requirements:

  • A conversation spans many smaller units of user interaction, which execute serially or even concurrently. The smaller nested conversations have their own isolated set of conversation state, and also have access to the state of the outer conversation.

  • The user is able to switch between many conversations within the same browser window. This feature is called workspace management.

6.2. Nested conversations

A nested conversation is created by invoking a method marked @Begin(nested=true) inside the scope of an existing conversation. A nested conversation has its own conversation context, and also has read-only access to the context of the outer conversation. (It can read the outer conversation's context variables, but not write to them.) When an @End is subsequently encountered, the nested conversation will be destroyed, and the outer conversation will resume, by "popping" the conversation stack. Conversations may be nested to any arbitrary depth.

Certain user activity (workspace management, or the back button) can cause the outer conversation to be resumed before the inner conversation is ended. In this case it is possible to have multiple concurrent nested conversations belonging to the same outer conversation. If the outer conversation ends before a nested conversation ends, Seam destroys all nested conversation contexts along with the outer context.

A conversation may be thought of as a continuable state. Nested conversations allow the application to capture a consistent continuable state at various points in a user interaction, thus insuring truly correct behavior in the face of backbuttoning and workspace management.

TODO: an example to show how a nested conversation prevents bad stuff happening when you backbutton.

6.3. Starting conversations with GET requests

JSF does not define any kind of action listener that is triggered when a page is accessed via a non-faces request (for example, a HTTP GET request). This can occur if the user bookmarks the page, or if we navigate to the page via an <h:outputLink>.

Sometimes we want to begin a conversation immediately the page is accessed. Since there is no JSF action method, we can't solve the problem in the usual way, by annotating the action with @Begin.

A further problem arises if the page needs some state to be fetched into a context variable. We've already seen two ways to solve this problem. If that state is held in a Seam component, we can fetch the state in a @Create method. If not, we can define a @Factory method for the context variable.

If none of these options works for you, Seam lets you define a page action in the pages.xml file.

<pages>
    <page view-id="/messageList.jsp" action="#{messageManager.list}"/>
    ...
</pages>

This action method is called at the beginning of the render response phase, any time the page is about to be rendered. If a page action returns a non-null outcome, Seam will process any appropriate JSF navigation rules, possibly resulting in a completely different page being rendered.

If all you want to do before rendering the page is begin a conversation, you could use a built-in action method that does just that:

<pages>
    <page view-id="/messageList.jsp" action="#{conversation.begin}"/>
    ...
</pages>

Note that you can also call this built-in action from a JSF control, and, similarly, you can use #{conversation.end} to end conversations.

If you want more control, to join existing conversations or begin a nested conversion, to begin a pageflow or an atomic conversation, you should use the <begin-conversation> element.

<pages>
    <page view-id="/messageList.jsp">
       <begin-conversation nested="true" pageflow="AddItem"/>
    <page>
    ...
</pages>

There is also an <end-conversation> element.

<pages>
    <page view-id="/home.jsp">
       <end-conversation/>
    <page>
    ...
</pages>

To solve the first problem, we now have five options:

  • Annotate the @Create method with @Begin

  • Annotate the @Factory method with @Begin

  • Annotate the Seam page action method with @Begin

  • Use <begin-conversation> in pages.xml.

  • Use #{conversation.begin} as the Seam page action method

6.4. Using <s:link> and <s:button>

JSF command links always perform a form submission via JavaScript, which breaks the web browser's "open in new window" or "open in new tab" feature. In plain JSF, you need to use an <h:outputLink> if you need this functionality. But there are two major limitations to <h:outputLink>.

  • JSF provides no way to attach an action listener to an <h:outputLink>.

  • JSF does not propagate the selected row of a DataModel since there is no actual form submission.

Seam provides the notion of a page action to help solve the first problem, but this does nothing to help us with the second problem. We could work around this by using the RESTful approach of passing a request parameter and requerying for the selected object on the server side. In some cases—such as the Seam blog example application—this is indeed the best approach. The RESTful style supports bookmarking, since it does not require server-side state. In other cases, where we don't care about bookmarks, the use of @DataModel and @DataModelSelection is just so convenient and transparent!

To fill in this missing functionality, and to make conversation propagation even simpler to manage, Seam provides the <s:link> JSF tag.

The link may specify just the JSF view id:

<s:link view=“/login.xhtml” value=“Login”/>

Or, it may specify an action method (in which case the action outcome determines the page that results):

<s:link action=“#{login.logout}” value=“Logout”/>

If you specify both a JSF view id and an action method, the 'view' will be used unless the action method returns a non-null outcome:

<s:link view="/loggedOut.xhtml"  action=“#{login.logout}” value=“Logout”/>

The link automatically propagates the selected row of a DataModel using inside <h:dataTable>:

<s:link view=“/hotel.xhtml” action=“#{hotelSearch.selectHotel}” value=“#{hotel.name}”/>

You can leave the scope of an existing conversation:

<s:link view=“/main.xhtml” propagation=“none”/>

You can begin, end, or nest conversations:

<s:link action=“#{issueEditor.viewComment}” propagation=“nest”/>

If the link begins a conversation, you can even specify a pageflow to be used:

<s:link action=“#{documentEditor.getDocument}” propagation=“begin” 
        pageflow=“EditDocument”/>

The taskInstance attribute if for use in jBPM task lists:

<s:link action=“#{documentApproval.approveOrReject}” taskInstance=“#{task}”/>

(See the DVD Store demo application for examples of this.)

Finally, if you need the "link" to be rendered as a button, use <s:button>:

<s:button action=“#{login.logout}” value=“Logout”/>

6.5. Success messages

It is quite common to display a message to the user indicating success or failure of an action. It is convenient to use a JSF FacesMessage for this. Unfortunately, a successful action often requires a browser redirect, and JSF does not propagate faces messages across redirects. This makes it quite difficult to display success messages in plain JSF.

The built in conversation-scoped Seam component named facesMessages solves this problem. (You must have the Seam redirect filter installed.)

@Name("editDocumentAction")
@Stateless
public class EditDocumentBean implements EditDocument {
    @In EntityManager em;
    @In Document document;
    @In FacesMessages facesMessages;
    
    public String update() {
        em.merge(document);
        facesMessages.add("Document updated");
    }
}

Any message added to facesMessages is used in the very next render response phase for the current conversation. This even works when there is no long-running conversation since Seam preserves even temporary conversation contexts across redirects.

You can even include JSF EL expressions in a faces message summary:

facesMessages.add("Document #{document.title} was updated");

You may display the messages in the usual way, for example:

<h:messages globalOnly="true"/>

6.6. Using an "explicit" conversation id

Ordinarily, Seam generates a meaningless unique id for each conversation in each session. You can customize the id value when you begin the conversation.

This feature can be used to customize the conversation id generation algorithm like so:

@Begin(id="#{myConversationIdGenerator.nextId}") 
public void editHotel() { ... }

Or it can be used to assign a meaningful conversation id:

@Begin(id="hotel#{hotel.id}") 
public String editHotel() { ... }
@Begin(id="hotel#{hotelsDataModel.rowData.id}") 
public String selectHotel() { ... }
@Begin(id="entry#{params['blogId']}")
public String viewBlogEntry() { ... }
@BeginTask(id="task#{taskInstance.id}") 
public String approveDocument() { ... }

Clearly, these example result in the same conversation id every time a particular hotel, blog or task is selected. So what happens if a conversation with the same conversation id already exists when the new conversation begins? Well, Seam detects the existing conversation and redirects to that conversation without running the @Begin method again. This feature helps control the number of workspaces that are created when using workspace management.

6.7. Workspace management

Workspace management is the ability to "switch" conversations in a single window. Seam makes workspace management completely transparent at the level of the Java code. To enable workspace management, all you need to do is:

  • Provide description text for each view id (when using JSF navigation rules) or page node (when using jPDL pageflows). This description text is displayed to the user by the workspace switchers.

  • Include one or more of the standard workspace switcher JSP or facelets fragments in your pages. The standard fragments support workspace management via a drop down menu, a list of conversations, or breadcrumbs.

6.7.1. Workspace management and JSF navigation

When you use JSF navigation rules, Seam switches to a conversation by restoring the current view-id for that conversation. The descriptive text for the workspace is defined in a file called pages.xml that Seam expects to find in the WEB-INF directory, right next to faces-config.xml:

<pages>
    <page view-id="/main.xhtml">Search hotels: #{hotelBooking.searchString}</page>
    <page view-id="/hotel.xhtml">View hotel: #{hotel.name}</page>
    <page view-id="/book.xhtml">Book hotel: #{hotel.name}</page>
    <page view-id="/confirm.xhtml">Confirm: #{booking.description}</page>
</pages>

Note that if this file is missing, the Seam application will continue to work perfectly! The only missing functionality will be the ability to switch workspaces.

6.7.2. Workspace management and jPDL pageflow

When you use a jPDL pageflow definition, Seam switches to a conversation by restoring the current jBPM process state. This is a more flexible model since it allows the same view-id to have different descriptions depending upon the current <page> node. The description text is defined by the <page> node:

<pageflow-definition name="shopping">

   <start-state name="start">
      <transition to="browse"/>
   </start-state>
   
   <page name="browse" view-id="/browse.xhtml">
      <description>DVD Search: #{search.searchPattern}</description>
      <transition to="browse"/>
      <transition name="checkout" to="checkout"/>
   </page>
   
   <page name="checkout" view-id="/checkout.xhtml">
      <description>Purchase: $#{cart.total}</description>
      <transition to="checkout"/>
      <transition name="complete" to="complete"/>
   </page>
   
   <page name="complete" view-id="/complete.xhtml">
      <end-conversation />
   </page>
   
</pageflow-definition>

6.7.3. The conversation switcher

Include the following fragment in your JSP or facelets page to get a drop-down menu that lets you switch to any current conversation, or to any other page of the application:

<h:selectOneMenu value="#{switcher.conversationIdOrOutcome}">
    <f:selectItem itemLabel="Find Issues" itemValue="findIssue"/>
    <f:selectItem itemLabel="Create Issue" itemValue="editIssue"/>
    <f:selectItems value="#{switcher.selectItems}"/>
</h:selectOneMenu>
<h:commandButton action="#{switcher.select}" value="Switch"/>

In this example, we have a menu that includes an item for each conversation, together with two additional items that let the user begin a new conversation.

6.7.4. The conversation list

The conversation list is very similar to the conversation switcher, except that it is displayed as a table:

<h:dataTable value="#{conversationList}" var="entry"
        rendered="#{not empty conversationList}">
    <h:column>
        <f:facet name="header">Workspace</f:facet>
        <h:commandLink action="#{entry.select}" value="#{entry.description}"/>
        <h:outputText value="[current]" rendered="#{entry.current}"/>
    </h:column>
    <h:column>
        <f:facet name="header">Activity</f:facet>
        <h:outputText value="#{entry.startDatetime}">
            <f:convertDateTime type="time" pattern="hh:mm a"/>
        </h:outputText>
        <h:outputText value=" - "/>
        <h:outputText value="#{entry.lastDatetime}">
            <f:convertDateTime type="time" pattern="hh:mm a"/>
        </h:outputText>
    </h:column>
    <h:column>
        <f:facet name="header">Action</f:facet>
        <h:commandButton action="#{entry.select}" value="#{msg.Switch}"/>
        <h:commandButton action="#{entry.destroy}" value="#{msg.Destroy}"/>
    </h:column>
</h:dataTable>

We imagine that you will want to customize this for your own application.

The conversation list is nice, but it takes up a lot of space on the page, so you probably don't want to put it on every page.

Notice that the conversation list lets the user destroy workspaces.

6.7.5. Breadcrumbs

Breadcrumbs are useful in applications which use a nested conversation model. The breadcrumbs are a list of links to conversations in the current conversation stack:

<t:dataList value="#{conversationStack}" var="entry">
    <h:outputText value=" | "/> 
    <h:commandLink value="#{entry.description}" action="#{entry.select}"/>
</t:dataList>

Notice that here we are using the MyFaces <t:dataList> component, since JSF amazingly does not provide any standard component for looping.

Please refer to the Seam Issue Tracker demo to see all this functionality in action!

6.8. Seam-managed persistence contexts and atomic conversations

Seam provides built-in components for EJB 3.0 and Hibernate persistence context management that support the use of persistence contexts scoped to the conversation. This useful feature allows you to program optimistic transactions that span multiple requests to the server without the need to use the merge() operation or to re-load data at the beginning of each request, and without the need to wrestle with the dreaded LazyInitializationException or NonUniqueObjectException. Please see the configuration chapter for information about configuring Seam-managed persistence contexts and Seam-managed transactions.

As with any optimistic transaction management, transaction isolation and consistency can be achieved via use of optimistic locking. Fortunately, both Hibernate and EJB 3.0 make it very easy to use optimistic locking, by providing the @Version annotation.

By default, the persistence context is flushed (synchronized with the database) at the end of each transaction. This is sometimes the desired behavior. But very often, we would prefer that all changes are held in memory and only written to the database when the conversation ends successfully. This allows for truly atomic conversations. As the result of a truly stupid and shortsighted decision by certain non-JBoss, non-Sun and non-Sybase members of the EJB 3.0 expert group, there is currently no simple, usable and portable way to implement atomic conversations using EJB 3.0 persistence. However, Hibernate provides this feature as a vendor extension to the FlushModeTypes defined by the specification, and it is our expectation that other vendors will soon provide a similar extension.

Seam lets you specify FlushModeType.MANUAL when beginning a conversation. Currently, this works only when Hibernate is the underlying persistence provider, but we plan to support other equivalent vendor extensions.

@In EntityManager em; //a Seam-managed persistence context

@Begin(flushMode=MANUAL)
public void beginClaimWizard() {
    claim = em.find(Claim.class, claimId);
}

Now, the claim object remains managed by the persistence context for the rest ot the conversation. We can make changes to the claim:

public void addPartyToClaim() {
    Party party = ....;
    claim.addParty(party);
}

But these changes will not be flushed to the database until we explicitly force the flush to occur:

@End
public void commitClaim() {
    em.flush();
}

6.9. Seam and Servlets

Requests sent direct to some servlet other than the JSF servlet are not processed through the JSF lifecycle, so Seam provides a servlet filter that can be applied to any servlet that needs access to Seam components.

<filter>
    <filter-name>Seam Servlet Filter</filter-name>
    <filter-class>org.jboss.seam.servlet.SeamServletFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>Seam Servlet Filter</filter-name>
    <url-pattern>*.ajax</url-pattern>
</filter-mapping>

This servlet filter is responsible for initializing all Seam contexts before passing control to the servlet. It expects to find the conversation id of any conversation context in a request parameter named conversationId. You are responsible for ensuring that it gets sent in the request.

You are also responsible for ensuring propagation of any new conversation id back to the client. Seam exposes the conversation id as a property of the built in component conversation.

Seam also provides the Seam Remoting framework, a simple way to expose any method of a Seam component for invocation by an asynchronous JavaScript request simply by annotating the methods that should be accessible in the client. See the Seam Remoting chapter for further information.

6.10. Seam and SOAP

TODO