All of these various kinds of events are mapped to Seam components via JSF EL method binding expressions. For a JSF event, this is defined in the JSF template:
For a jBPM transition event, it is specified in the jBPM process definition or pageflow definition:
You can find out more information about JSF events and jBPM events elsewhere. Lets concentrate for now upon the two additional kinds of events defined by Seam.
A Seam page action is an event that occurs just before we render a page. We declare page actions in WEB-INF/pages.xml
. We can define a page action for either a particular JSF view id:
<pages>
<page view-id="/hello.jsp" action="#{helloWorld.sayHello}"/>
</pages>
Or we can use a wildcard to specify an action that applies to all view ids that match the pattern:
<pages>
<page view-id="/hello/*" action="#{helloWorld.sayHello}"/>
</pages>
If multiple wildcarded page actions match the current view-id, Seam will call all the actions, in order of least-specific to most-specific.
The page action method can return a JSF outcome. If the outcome is non-null, Seam will use the defined navigation rules to navigate to a view.
Furthermore, the view id mentioned in the <page>
element need not correspond to a real JSP or Facelets page! So, we can reproduce the functionality of a traditional action-oriented framework like Struts or WebWork using page actions. For example:
TODO: translate struts action into page action
This is quite useful if you want to do complex things in response to non-faces requests (for example, HTTP GET requests).
A JSF faces request (a form submission) encapsulates both an "action" (a method binding) and "parameters" (input value bindings). A page action might also needs parameters!
Since GET requests are bookmarkable, page parameters are passed as human-readable request parameters. (Unlike JSF form inputs, which are anything but!)
Seam lets us provide a value binding that maps a named request parameter to an attribute of a model object.
<pages>
<page view-id="/hello.jsp" action="#{helloWorld.sayHello}">
<param name="firstName" value="#{person.firstName}"/>
<param name="lastName" value="#{person.lastName}"/>
</page>
</pages>
The <param>
declaration is bidirectional, just like a value binding for a JSF input:
-
When a non-faces (GET) request for the view id occurs, Seam sets the value of the named request parameter onto the model object, after performing appropriate type conversions.
-
Any <s:link>
or <s:button>
transparently includes the request parameter. The value of the parameter is determined by evaluating the value binding during the render phase (when the <s:link>
is rendered).
-
Any navigation rule with a <redirect/>
to the view id transparently includes the request parameter. The value of the parameter is determined by evaluating the value binding at the end of the invoke application phase.
-
The value is transparently propagated with any JSF form submission for the page with the given view id. (This means that view parameters behave like PAGE
-scoped context variables for faces requests.
The essential idea behind all this is that
however
we get from any other page to /hello.jsp
(or from /hello.jsp
back to /hello.jsp
), the value of the model attribute referred to in the value binding is "remembered", without the need for a conversation (or other server-side state).
This all sounds pretty complex, and you're probably wondering if such an exotic construct is really worth the effort. Actually, the idea is very natural once you "get it". It is definitely worth taking the time to understand this stuff. Page parameters are the most elegant way to propagate state across a non-faces request. They are especially cool for problems like search screens with bookmarkable results pages, where we would like to be able to write our application code to handle both POST and GET requests with the same code. Page parameters eliminate repetitive listing of request parameters in the view definition and make redirects much easier to code.
Note that you don't need an actual page action method binding to use a page parameter. The following is perfectly valid:
<pages>
<page view-id="/hello.jsp">
<param name="firstName" value="#{person.firstName}"/>
<param name="lastName" value="#{person.lastName}"/>
</page>
</pages>
You can even specify a JSF converter:
<pages>
<page view-id="/calculator.jsp" action="#{calculator.calculate}">
<param name="x" value="#{calculator.lhs}"/>
<param name="y" value="#{calculator.rhs}"/>
<param name="op" converterId="com.my.calculator.OperatorConverter" value="#{calculator.op}"/>
</page>
</pages>
<pages>
<page view-id="/calculator.jsp" action="#{calculator.calculate}">
<param name="x" value="#{calculator.lhs}"/>
<param name="y" value="#{calculator.rhs}"/>
<param name="op" converter="#{operatorConverter}" value="#{calculator.op}"/>
</page>
</pages>
You can use standard JSF navigation rules defined in faces-config.xml
in a Seam application. However, JSF navigation rules have a number of annoying limitations:
-
It is not possible to specify request parameters to be used when redirecting.
-
It is not possible to begin or end conversations from a rule.
-
Rules work by evaluating the return value of the action method; it is not possible to evaluate an arbitrary EL expression.
A further problem is that "orchestration" logic gets scattered between pages.xml
and faces-config.xml
. It's better to unify this logic into pages.xml
.
This JSF navigation rule:
<navigation-rule>
<from-view-id>/editDocument.xhtml</from-view-id>
<navigation-case>
<from-action>#{documentEditor.update}</from-action>
<from-outcome>success</from-outcome>
<to-view-id>/viewDocument.xhtml</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
Can be rewritten as follows:
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule if-outcome="success">
<redirect view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page>
But it would be even nicer if we didn't have to pollute our DocumentEditor
component with string-valued return values (the JSF outcomes). So Seam lets us write:
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}"
evaluate="#{documentEditor.errors.size}">
<rule if-outcome="0">
<redirect view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page>
Or even:
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule if="#{documentEditor.errors.empty}">
<redirect view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page>
The first form evaluates a value binding to determine the outcome value to be used by the subsequent rules. The second approach ignores the outcome and evaluates a value binding for each possible rule.
Of course, when an update succeeds, we probably want to end the current conversation. We can do that like this:
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule if="#{documentEditor.errors.empty}">
<end-conversation/>
<redirect view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page>
But ending the conversation loses any state associated with the conversation, including the document we are currently interested in! One solution would be to use an immediate render instead of a redirect:
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule if="#{documentEditor.errors.empty}">
<end-conversation/>
<render view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page>
But the correct solution is to pass the document id as a request parameter:
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule if="#{documentEditor.errors.empty}">
<end-conversation/>
<redirect view-id="/viewDocument.xhtml">
<param name="documentId" value="#{documentEditor.documentId}"/>
</redirect>
</rule>
</navigation>
</page>
Null outcomes are a special case in JSF. The null outcome is interpreted to mean "redisplay the page". The following navigation rule matches any non-null outcome, but
not
the null outcome:
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<rule>
<render view-id="/viewDocument.xhtml"/>
</rule>
</navigation>
</page>
If you want to perform navigation when a null outcome occurs, use the following form instead:
<page view-id="/editDocument.xhtml">
<navigation from-action="#{documentEditor.update}">
<render view-id="/viewDocument.xhtml"/>
</navigation>
</page>
Fine-grained files for definition of navigation, page actions and parameters
If you have a lot of different page actions and page parameters, or even just a lot of navigation rules, you will almost certainly want to split the declarations up over multiple files. You can define actions and parameters for a page with the view id /calc/calculator.jsp
in a resource named calc/calculator.page.xml
. The root element in this case is the <page>
element, and the view id is implied:
<page action="#{calculator.calculate}">
<param name="x" value="#{calculator.lhs}"/>
<param name="y" value="#{calculator.rhs}"/>
<param name="op" converter="#{operatorConverter}" value="#{calculator.op}"/>
</page>
Seam components can interact by simply calling each others methods. Stateful components may even implement the observer/observable pattern. But to enable components to interact in a more loosely-coupled fashion than is possible when the components call each others methods directly, Seam provides
component-driven events
.
We specify event listeners (observers) in components.xml
.
<components>
<event type="hello">
<action expression="#{helloListener.sayHelloBack}"/>
<action expression="#{logger.logHello}"/>
</event>
</components>
Where the
event type
is just an arbitrary string.
When an event occurs, the actions registered for that event will be called in the order they appear in components.xml
. How does a component raise an event? Seam provides a built-in component for this.
@Name("helloWorld")
public class HelloWorld {
public void sayHello() {
FacesMessages.instance().add("Hello World!");
Events.instance().raiseEvent("hello");
}
}
Or you can use an annotation.
@Name("helloWorld")
public class HelloWorld {
@RaiseEvent("hello")
public void sayHello() {
FacesMessages.instance().add("Hello World!");
}
}
Notice that this event producer has no dependency upon event consumers. The event listener may now be implemented with absolutely no dependency upon the producer:
@Name("helloListener")
public class HelloListener {
public void sayHelloBack() {
FacesMessages.instance().add("Hello to you too!");
}
}
The method binding defined in components.xml
above takes care of mapping the event to the consumer. If you don't like futzing about in the components.xml
file, you can use an annotation instead:
@Name("helloListener")
public class HelloListener {
@Observer("hello")
public void sayHelloBack() {
FacesMessages.instance().add("Hello to you too!");
}
}
You might wonder why I've not mentioned anything about event objects in this discussion. In Seam, there is no need for an event object to propagate state between event producer and listener. State is held in the Seam contexts, and is shared between components. However, if you really want to pass an event object, you can:
@Name("helloWorld")
public class HelloWorld {
private String name;
public void sayHello() {
FacesMessages.instance().add("Hello World, my name is #0.", name);
Events.instance().raiseEvent("hello", name);
}
}
@Name("helloListener")
public class HelloListener {
@Observer("hello")
public void sayHelloBack(String name) {
FacesMessages.instance().add("Hello #0!", name);
}
}