Spring Web Flow allows you to handle JSF action events in a decoupled way, requiring no direct dependencies in your Java code on JSF API's. In fact, these events can often be handled completely in the flow definiton language without requiring any custom Java action code at all. This allows for a more agile development process since the artifacts being manipulated in wiring up events (JSF view templates and SWF flow definitions) are instantly refreshable without requiring a build and re-deploy of the whole application.
A simple but common case in JSF is the need to signal an event that causes manipulation of the model in
some way and then redisplays the same view to reflect the changed state of the model. The flow
definition language has special support for this in the
transition
element.
A good example of this is a table of paged list results. Suppose you want to be able to load and display
only a portion of a large result list, and allow the user to page through the results. The initial
view-state
definition to load and display the list would be:
<view-state id="reviewHotels"> <on-render> <evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels" result-type="dataModel" /> </on-render> </view-state>
You construct a JSF DataTable that displays the current
hotels
list, and then place a "More Results" link below the table:
<h:commandLink id="nextPageLink" value="More Results" action="next"/>
This commandLink signals a "next" event from its action attribute. You can then handle the event by
adding to the
view-state
definition:
<view-state id="reviewHotels"> <on-render> <evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels" result-type="dataModel" /> </on-render> <transition on="next"> <evaluate expression="searchCriteria.nextPage()" /> </transition> </view-state>
Here you handle the "next" event by incrementing the page count on the searchCriteria instance. The
on-render
action is then called again with the updated criteria, which causes the next page of results to be
loaded into the DataModel. The same view is re-rendered since there was no
to
attribute on the
transition
element, and the changes in the model are reflected in the view.
The next logical level beyond in-page events are events that require navigation to another view, with some manipulation of the model along the way. Achieving this with pure JSF would require adding a navigation rule to faces-config.xml and likely some intermediary Java code in a JSF managed bean (both tasks requiring a re-deploy). With the flow defintion language, you can handle such a case concisely in one place in a quite similar way to how in-page events are handled.
Continuing on with our use case of manipulating a paged list of results, suppose we want each row in the
displayed DataTable to contain a link to a detail page for that row instance. You can add a column to
the table containing the following
commandLink
component:
<h:commandLink id="viewHotelLink" value="View Hotel" action="select"/>
This raises the "select" event which you can then handle by adding another
transition
element to the existing
view-state
:
<view-state id="reviewHotels"> <on-render> <evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels" result-type="dataModel" /> </on-render> <transition on="next"> <evaluate expression="searchCriteria.nextPage()" /> </transition> <transition on="select" to="reviewHotel"> <set name="flowScope.hotel" value="hotels.selectedRow" /> </transition> </view-state>
Here the "select" event is handled by pushing the currently selected hotel instance from the DataTable
into flow scope, so that it may be referenced by the "reviewHotel"
view-state
.
JSF provides useful facilities for validating input at field-level before changes are applied to the model, but when you need to then perform more complex validation at the model-level after the updates have been applied, you are generally left with having to add more custom code to your JSF action methods in the managed bean. Validation of this sort is something that is generally a responsibility of the domain model itself, but it is difficult to get any error messages propagated back to the view without introducing an undesirable dependency on the JSF API in your domain layer.
With Spring Faces, you can utilize the generic and low-level
MessageContext
in your business code and any messages added there will then be available to the
FacesContext
at render time.
For example, suppose you have a view where the user enters the necessary details to complete a hotel
booking, and you need to ensure the Check In and Check Out dates adhere to a given set of business
rules. You can invoke such model-level validation from a
transition
element:
<view-state id="enterBookingDetails"> <transition on="proceed" to="reviewBooking"> <evaluate expression="booking.validateEnterBookingDetails(messageContext)" /> </transition> </view-state>
Here the "proceed" event is handled by invoking a model-level validation method on the booking instance,
passing the generic
MessageContext
instance so that messages may be recorded. The messages can then be displayed along with any other JSF
messages with the
h:messages
component,
Spring Faces provides some special
UICommand
components that go beyond the standard JSF components by adding the ability to do Ajax-based partial
view updates. These components degrade gracefully so that the flow will still be fully functional by
falling back to full page refreshes if a user with a less capable browser views the page.
Note | |
---|---|
Though the core JSF support in Spring Faces is JSF 1.1-compatible, the Spring Faces Ajax components require JSF 1.2. |
Revisiting the earlier example with the paged table, you can change the "More Results" link to use an
Ajax request by replacing the standard
commandButton
with the Spring Faces version (note that the Spring Faces command components use Ajax by default, but
they can alternately be forced to use a normal form submit by setting ajaxEnabled="false" on the
component):
<sf:commandLink id="nextPageLink" value="More Results" action="next" />
This event is handled just as in the non-Ajax case with the
transition
element, but now you will add a special
render
action that specifies which portions of the component tree need to be re-rendered:
<view-state id="reviewHotels"> <on-render> <evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels" result-type="dataModel" /> </on-render> <transition on="next"> <evaluate expression="searchCriteria.nextPage()" /> <render fragments="hotels:searchResultsFragment" /> </transition> </view-state>
The
fragments="hotels:searchResultsFragment"
is an instruction that will be interpreted at render time, such that only the component with the JSF
clientId "hotels:searchResultsFragment" will be rendered and returned to the client. This fragment will
then be automatically replaced in the page. The
fragments
attribute can be a comma-delimited list of ids, with each id representing the root node of a subtree
(meaning the root node and all of its children) to be rendered. If the "next" event is fired in a
non-Ajax request (i.e., if JavaScript is disabled on the client), the
render
action will be ignored and the full page will be rendered as normal.
In addition to the Spring Faces
commandLink
component, there is a corresponding
commandButton
component with the same functionality. There is also a special
ajaxEvent
component that will raise a JSF action even in response to any client-side DOM event. See the Spring
Faces tag library docs for full details.
An additional built-in feature when using the Spring Faces Ajax components is the ability to have the
response rendered inside a rich modal popup widget by setting
popup="true"
on a
view-state
.
<view-state id="changeSearchCriteria" view="enterSearchCriteria.xhtml" popup="true"> <on-entry> <render fragments="hotelSearchFragment" /> </on-entry> <transition on="search" to="reviewHotels"> <evaluate expression="searchCriteria.resetPage()"/> </transition> </view-state>
If the "changeSearchCriteria"
view-state
is reached as the result of an Ajax-request, the result will be rendered into a rich popup. If
JavaScript is unavailable, the request will be processed with a full browser refresh, and the
"changeSearchCriteria" view will be rendered as normal.