[jsr-314-open] Behavior API overview

Andy Schwartz andy.schwartz at ORACLE.COM
Tue Mar 10 14:10:47 EDT 2009


Gang -

I wanted to send one last email on the new Behavior API to help folks
who were not able to keep up with the progress on this API as it
evolved.  Much of this information has been covered in previous emails,
but some of the API details are new, so please read on if you are
interested.

* Core APIs

Just to remind everyone why we started down this path...  As of the
public review draft, we had a single non-generic "behavior" - the
AjaxBeahvior - which could only be attached to EditableValueHolders and
ActionSource components, and even then, only for valueChange/action
events.  So, two obvious limitations that we decided to address:

1. We wanted to allow other types of "behaviors" to be provided - not
limit the solution to AjaxBehavior.
2. We wanted to allow components other than
EditableValueHolders/ActionSources to host "behaviors".

In order to address #1, we introduced the generic
javax.faces.component.behavior.Behavior class.  The class provides the
base contract for all behavior implementations.  Note that early on we
decided that a "behavior" was something distinct from a "component".
Behaviors, like converters/validators, are objects that are attached to
existing components in order to enhance the component with functionality
that is not provided by the component itself.

The main responsibility of a Behavior implementation is to cough up a
script - ie. a bit of JavaScript code that can be attached to one of the
component's client-side event handlers (eg. attach to "onclick").  This
responsibility is implemented via the Behavior.getScript() method.

In order to address #2, we introduced the
javax.faces.component.behavior.BehaviorHolder interface.  Components
that want to allow behaviors to be attached implement this interface.
This allows the component to communicate the valid attach points
(getEventNames()) and the default attach point (getDefaultEventName()),
provides a mechanism for registering behaviors (addBehavior()), and also
provides access to any registered behaviors (getBehaviors()).

While UIComponentBase does not implement the BehaviorHolder interface
itself, it provides default implementations of the BehaviorHolder
methods in order to make it easier for subclasses to implement
BehaviorHolder.  The minimal requirement for a UIComponentBase subclass
to implement BehaviorHolder is to provide an implementation of
getEventNames() that returns a non-empty set of the event names
supported by the component.

These two contracts, Behavior and BehaviorHolder, form the foundation of
our new component behavior solution.  Hopefully the above should be
familiar since it has been discussed the number of times on the EG list
and was also covered in Appendix C of the recent spec drafts.   The
actual set of APIs that we need has grown over the last few months, so I
wanted to also discuss some of the more recent additions...

* BehaviorRenderer

Once we got the core Behavior/BehaviorHolder contract in place, we
(Roger, Ted, Alex and I) quickly realized that additional flexibility
was needed.  In particular, we felt that the scripts that are produced
by Behaivor implementations may need to vary based on the RenderKit.  In
order to support RenderKit-specific Behavior script generation, we
followed the existing UIComonent/Renderer pattern and introduced a
BehaviorRenderer API.  BehaviorRenderers are similar in spirit to
component Renderers, but provide a contract that is geared towards
Behaviors.  So, while component Renderers are designed for HTML content
generation and write directly to the ResponseWriter, BehaviorRenderers
simply cough up a script - ie. the main API on BehaviorRenderer is
getScript() (mirrors Behavior.getScript()).

Of course, as with component Renderers, BehaviorRenderers are entirely
optional.  A Behavior implementation does not need to use a
BehaviorRenderer at all - it can simply override Behavior.getScript()
and produce the script locally.

* Behavior Event Handling

The next enhancement that we tackled was the ability for Behaviors to
participate in decoding.  This was necessary in order to support a very
important use case: the ability for Behavior implementations to deliver
events to registered listeners.  The main case that we had in mind was
AjaxBehavior, ie. we wanted to make it possible to do this:

  <h:commandButton>
      <f:ajax event="mouseover"
listener="#{bean.doSomethingInResponseToMouseOver}"/>
  </h:commandButton>

In order to support this we added the following APIs:

- Behavior.decode()
- BehaviorRenderer.decode()
- Behavior.broadcast()
- BehaviorEvent (extends FacesEvent)
- BehaviorListener (extends FacesListener)

We also specified the mechanism/conditions under which BehaviorHolder
components must give attached Behaviors the opportunity to decode.

* BehaviorBase

Around this time we also introduced a BehaviorBase base class that
provides base functionality for Behavior implementations (such as
listener registration, event broadcast support, BehaviorRenderer
delegation wiring).  BehaviorBase is to Behavior what UIComponentBase is
to UIComponent.

* Behavior.Parameter

As we started integrating the new behavior contract with our existing
renderers, we realized that Behavior/BehaviorRenderer getScript()
implementations may need access to various pieces of information to help
them generate the correct scripts.  For example, in the h:commandButton
case, the component may have f:params attached.  These f:param
names/values must be included in the request, whether the request is a
full page postback provided by the component, or an Ajax postback
provided by the AjaxBehavior.  In order to support this case, we added a
trivial static inner Parameter class to Behavior.  This allows
components/renderers to pass (resolved) parameter name/values into
Behavior.getScript().

* BehaviorContext

At this point we realized that there is danger in adding more arguments
to the Behavior.BehaviorRenderer.getScript() methods.  The problem is
that we may not be able to foresee all of the possible information that
getScript() implementations may need access to.  Rather than risk having
to later define new overrides of getScript(), we decided to introduce a
BehaviorContext class.  The BehaviorContext is used to pass information
(FacesContext, component, parameters, source id) into
Behavior.getScript().  If we find that we need to pass additional
information into getScript() in a later release, we can do so by adding
new methods to the BehaviorContext.

BehaviorContext is defined as an abstract class, but provides access to
a implementation via the BehaviorContext.createBehaviorContext() factory
method.  Note that we explicitly avoid going through the FactoryFinder
mechanism here since this is a trivial data transfer object - and one
which may need to be instantiated many times during a typical request -
so we want to keep the overhead low.

I keep going back and forth as to whether Behavior.decode() should also
take a BehaviorContext.  Currently it just takes a FacesContext and the
UIComponent.  I am tempted to pass in the BehaviorContext instead,
though certain information on the BehaviorContext (like the parameters)
are clearly specific to script generation.

* BehaviorHint

Another issue that arose while integrating Behavior support into
h:commandButton is that the component renderer may itself need to vary
its content depending on whether or not it has a Behavior attached, and
in particular depending on whether that Behavior provides a script that
performs a postback.  The issue here is that certain renderers - eg.
commandButton/Link renderers - may generate their own postback scripts
(in Mojarra these renderers generate calls to mojarra.jsfcljs()).
However, when an AjaxBehavior is attached, it provides its own postback
script that should replace/suppress the postback script that the
component renderer normally produces.  Since not all Behavior
implementations actually perform postbacks (AjaxBehavior does, but most
won't), the component renderer needs some way to determine whether or
not an attached Behavior is going to perform a postback.

In order to solve this problem, we added a new enum, BehaviorHint, and a
new method to Behavior: Behavior.getHints().  getHints() returns a
Set<BehaviorHint> (implemented under the covers as an EnumSet of course)
that provides information about the Behavior that the consuming
component/renderer may be interested in.

At the moment, the BehaviorHint enum only defines a single value -
BehaviorHint.SUBMITTING.  However, we chose to use an enum rather than a
boolean (ie. rather than a Behavior.isSubmitting() method) to allow for
additional hints in the future.  If people feel that it is too awkward
to have a single-valued enum in our API, please let me know.  We can
consider reverting to a boolean method, though an enum is more future proof.

* Behavior configuration

In order to provide complete support for configuring Behavior and
BehaviorRenderer implementations, we had to touch a number of places,
including:

- javax.faces.application.Application now supports methods for adding
and creating Behavior instances
- faces-config.xml now supports <behavior> and <behavior-renderer> elements
- We have added annotations for identifying Behavior and
BehaviorRenderer implementations so that faces-config entries are not
required
- The Facelets taglib file now supports <behavior> tags.

In all cases we followed the precedents already set by converter/validator.

* Standard HTML component BehaviorHolders

Finally, once we had agreement our generic Behavior solution, we also
decided that it was silly not to enhance the standard HTML component set
to take advantage of this.  Most of the standard HTML components now
implement BehaviorHolder and thus allow Behaviors to be attached.  The
attach points for the standard HTML components correspond to the
standard "on<Event>" attributes.  So, whereas before it was only
possible to attach behaviors to EditableValueHolder/ActionSource
components, it is now possible to attach Behaviors to other standard
components, eg. <h:panelGroup>:

  <h:panelGroup>
      <f:ajax event="mouseover" listener="#{bean.foo}"/>
  </h:panelGroup>

This gives us significantly more flexibility - and a much more powerful
solution than we had outlined in our original plans.

One quick note on event names... The event names/attach points do not
include the "on" prefix that is used in the component attribute names.  So:

      <f:ajax event="mouseover"/>

Not:

      <f:ajax event="onMouseover"/>

The standard ActionSource components still support a logical "action"
event/attach point.  And the standard EditableValueHolder components
still support a logical "valueChange" event/attach point.  This allows
page authors to attach Behaviors to these components without having to
be aware of the unerlying DOM event that triggers the action/value
change.  Also note that the "action" and "valueChange" events are
defined as default events for these components, which makes it possible
to do omit the event attribute, eg:

   <!-- Fires Ajax requests in response to actions -->
   <h:commandButton>
      <f:ajax/>
  </h:commandButton>

  <!-- Fires Ajax requests in response to value changes -->
  <h:inputText>
      <f:ajax/>
  </h:inputText>

BTW, Jeffrey Stephenson (JSR-276) and I have been having some offline
discussions on the possibility of getting event name/default event name
metadata in place.  This would clearly help the development tool experience.

* Limitations

While I think that the Behavior API is a great addition to JSF, there
are a few limitations that we have not been able to address for 2.0.
These include:

1. Custom renderer wiring

Implementing the BehaviorHolder contract is trivial.  However, actually
rendering the Behavior scripts - possibly multiple scripts which may
need to be chained with other component/renderer-defined scripts - can
be tedious.  To help simplify this, we have introduced a new utility
method into our JavaScript API: jsf.util.chain().  This method takes an
arbitrary number of script arguments and executes each one until it
finds one that returns false, at which point it short-circuits.  While
this simplifies things somewhat, it is still nontrivial for a Renderer
author to produce the proper call to jsf.util.chain().

If we had more time, I would recommend trying to get some utility
methods into the JSF API so that we can make it easier for custom
Renderer authors to produce the properly chained scripts.  However,
given how little have time we have left, I do not think that we have
time to get this right.  My current plan is to try to provide some open
source utilities to help Renderer authors solve this problem.  Once
we've worked out the right APIs in open source, I am hoping that we can
incorporate these back into the 2.1 spec.

2. Behavior attribute-property transparency

I have been thinking that, like components, Behaviors should expose an
attribute map that provides attribute-property transparency.  That way,
BehaviorRenderers do not have to worry about the actual Behavior type -
they can simply grab attributes out of the attribute map.

I briefly considered implementing this functionality, but after a quick
look at UIComponentBase.AttributeMap I got scared away.  I think the
right solution is to:

- Break UIComponentBase.AttributeMap out into its own top-level public
class.
- Introduce an AttributeHolder contract
- Have UIComponentBase and BehaviorBase implement AttributeHolder and
use AttributeMap to hold their attributes.

However, for obvious reasons I decided not to push this for 2.0.
Perhaps we can think about this for 2.1.

Since we do not have a Behavior.getAttributes() API, at the moment,
BehaviorRenderer implementations are forced to cast to the actual
Behaivor type (eg. AjaxBehaviorRenderer casts the Behavior to
AjaxBehavior).  I am not happy about this, but willing to live with it
for now.

3. Behavior resources

Behavior implementations clearly are going to want to pull in JavaScript
libraries so that these do not need to be pulled in explicitly by the
application developer.  However, at the moment we do not have a
solution/recommendation for this.  (Well, there might be some solution -
just haven't identified what it is yet.)

This is another one that will need to wait for 2.1.

I think that's it!  If you have any questions/comments on this new API,
please follow up here.

I am hoping to do some blogging on all of this if I can ever find the
time. :-)

Andy






More information about the jsr-314-open-mirror mailing list