Monday Jun 01, 2009

Validation Error: Value is not valid

There's a reason why most JSF apps stick with string values when it comes to select boxes. Passing object values back and forth is fraught with annoying little gotchas. Here's the latest I ran into.

I set up a select box with an object value and a converter. The select items have object values. All is good. When I submit the form, I see "Validation Error: Value is not valid". My first reaction was that I didn't have any validation on the page, so how could a value be not valid?

This error happens, among other reasons, because the value of the select doesn't match any of the values in the select items. That means the value's class needs to define equals(). 

Friday May 08, 2009

Live Search in JSF

Suppose we have list of things, or a table of things, and a search box. The list should update based on what the user types. Additionally, the search should be live, in that the table updates as the user is typing; they are not required to hit enter, click a button, or otherwise take some action.

For the first attempt at this, let's add a valueChangeListener to your inputText component.

<ice:inputText
  value="#{aBean.searchFilter}"
  valueChangeListener=#{aHandler.search}/>

The problem is that this is not triggered until the form submits. You're going to need to add some Javascript to make this happen. For the next attempt, let's hook some Javascript into the component's keyup event,

<ice:inputText
  value="#{aBean.searchFilter}"
  onkeyup="doSubmit(this)"/>

 and,

function doSubmit(element) {
  iceSubmitPartial(
    document.getElementById("form"),
    element,
    MouseEvent.CLICK
  );
  setFocus(element);
}

Note that I'm using Icefaces, and making use of their utility Javascript function icePartialSubmit() to do a "partial submit". In a nutshell, this submits only this element and ignores the rest of the form. The call to setFocus() avoids losing focus in the input after the submission (another Icefaces utility).

This solution works fine, as long as the user is a slow typer. To understand what's wrong, suppose the user (quickly) types "java".

  1. user types "j"
  2. keyup occurs for "j"
  3. form is submitted
  4. bean is updated with value "j"
  5. user types "a"
  6. response is return from #3
  7. input is updated with value from backing bean: "j"

The response overwrites the "a" that the user just typed. What we want to do is add a delay in the form submission. If the keyup occurs, queue a form submission. If another keyup occurs, unqueue the first form submission and queue another, etc. It's set/cancelTimeout() to the rescure for this,

function doSubmit(element) {
        iceSubmitPartial(
            document.getElementById("form"),
            element,
            MouseEvent.CLICK
        );
        submitTimeout = null;
        setFocus(element);
}

function submitNow(element) {
    if (submitTimeout != null) {
        clearTimeout(submitTimeout);
    }
    submitTimeout = setTimeout(function(){doSubmit(element)}, 1000);
} 

The component binds keyup to submitNow(),

<ice:inputText
  value="#{aBean.searchFilter}"
  valueChangeListener=#{aHandler.search}
  onkeyup="submitNow(this)"/>

We use a delay of 1s. This still isn't perfect, because there's still a chance that the user will type something between a form submission and response. It is however much less likely considering average typing patterns.


Thursday May 07, 2009

JSF Actions on the URL

In a recent entry, I described a mechanism for passing a JSF view ID as a GET parameter to force JSF to a particular view. This entry will improve on that by allowing a JSF action to be specified a a GET parameter.

After writing the previous entry, I started thinking ... JSF already has a mapping from action to view ID, why not make use of that to simplify the URL? Instead of:

/faces/home.xhtml?viewId=/faces/other.xhtml

we have this,

/faces/home/xhtml?jsf.action=success

So, from the current view of /faces/home.xhtml, and action success, go to the view defined in faces-config.xml's navigation rules. For example,

     <navigation-rule>
        <from-view-id>/faces/summary.xhtml
        <navigation-case>
            <from-outcome>success</from-outcome>
            <to-view-id>/admin/facelet/finished.xhtml</to-view-id>
        </navigation-case>
        ...
     ...

If our current view is /faces/summary.xhtml, and the jsf.action=success parameter is present, then move to view ID /facelet/finished.xhtml. This is rather slick as it makes use of existing rules; no additional configuration is required. Anyway, here's the code:

public class ActionPhaseListener implements PhaseListener {

    public RedirectPhaseListener() {
    }

    public PhaseId getPhaseId() {
        return PhaseId.RESTORE_VIEW;
    }

    public void beforePhase(PhaseEvent phaseEvent) {
    }

    public void afterPhase(PhaseEvent phaseEvent) {
        if (navigationRules == null) {
            navigationRules = new NavigationRules();
        }

        FacesContext ctx = phaseEvent.getFacesContext();
        HttpServletRequest request =
                (HttpServletRequest) ctx.getExternalContext().getRequest();

        String action = request.getParameter("jsf.action");

        if (action != null) {
            String currentViewId = ctx.getViewRoot().getViewId();
            NavigationRule nr = navigationRules.getNavigationRules().get(currentViewId);
            if (nr == null) {
                nr = navigationRules.getNavigationRules().get(null);
            }
            NavigationCase nc = nr.getNavigationCases().get(action);
            if (nc != null) {
                String newViewId = nc.getToViewId();
                UIViewRoot page = ctx.getApplication().getViewHandler().createView(ctx, newViewId);
                ctx.setViewRoot(page);
                ctx.renderResponse();
            }
        }
    }

    private NavigationRules navigationRules = null;
} 

in the interest of not boring you, NavigationRules.java is linked. It reads faces-config.xml by using ServletContext.getResource() and parses it into bean-like objects using JDOM.

Alain commented on my previous post that the solution there was problematic as it allowed access to arbitrary view which may depend on objects being set up in other views (think of a work flow / wizard). This solution has no such problem, as you can only navigate according to the rules you've already defined in faces-config.xml.



Tuesday May 05, 2009

Poor Man's JSF Navigation By URL

Every wanted to access a JSF view by directly entering the view onto URL? This is a common request. In my case, I needed to link to specific places in my JSF app from a legacy non-JSF application. This is the type of thing you'd think would be straightforward, but JSF falls flat.

I scoured the web for solutions to this, and this is the simplest: implement a phase listener. In the interest of getting right to it, here it is:

public class RedirectPhaseListener implements PhaseListener {

    public RedirectPhaseListener() {
    }

    public PhaseId getPhaseId() {
        return PhaseId.RESTORE_VIEW;
    }

    public void afterPhase(PhaseEvent phaseEvent) {
    }

    public void beforePhase(PhaseEvent phaseEvent) {
        FacesContext ctx = phaseEvent.getFacesContext();
        HttpServletRequest request =
                (HttpServletRequest) ctx.getExternalContext().getRequest();

        String viewId = request.getParameter("viewId");

        if (viewId != null) {
            UIViewRoot page = ctx.getApplication().getViewHandler().createView(ctx, viewId);
            ctx.setViewRoot(page);
            ctx.renderResponse();

        }
    }
} 

The phase listener gets the request, and checks for a parameter. The parameter is the path to the view ID you want to visit. For example: /faces/someview.xhtml. With Facelets, these goto URLs end up looking funny, because the real view ID is in the URL also,

 /faces/home.xhtml?viewId=/faces/other.xhtml

Not nice, but it works. Other solutions I've seen try to get fancy by keeping a mapping a view+action result=new view. That's cleaner, and it's easy to do once you understand what's going on above.

Thursday Mar 26, 2009

icefaces: remove with effect

So you have your JSF+Icefaces application that displays some list of things, and you want the user to be able to remove items from the list. Add in a small twist: the removed item should make use of the Icefaces Effect SDK and fade out the removed item.

Sounds good so far. Add the effect attribute to your component,

<ice:panelGroup
  effect="#{bean.effect}"
  ...

Now go to your remove action (listener) method and add the effect initialization to your bean,

public void removeListener(ActionEvent e) {
  beanList.remove(bean);
  Effect e = new Fade();
  e.setSubmit(true);
  e.setTransitory(false);
  bean.setEffect(e);

When you run this code, you won't see the effect. A little thought makes the problem obvious. When the bean is removed from the list, the view is updated immediately. Since the bean is no longer in the list, it is not rendered, at all, with any effect. How do we get our remove effect then?

There's actually two solutions. The first is to simply not remove the item from the list, and keep a “visible” flag indicating if the item is really in the list. The most obvious way to do this is to change removeListener() above to,

public void removeListener(ActionEvent e) {
  beanList.setVisible(false);
  Effect e = new Fade();
  e.setSubmit(true);
  e.setTransitory(false);
  bean.setEffect(e);

and change your view,

<ice:panelGroup
  effect="#{bean.effect}"
  visible=”#{bean.visible}”
...


However, when you run this code, you get the same result. The item disappears immediately and you do not see the effect. It's the same problem. The visible flag is set immediately and the component is hidden in the view immediately. The trick here is an undocumented aspect of the Effect SDK. If the effected component has a visible attribute, and it's bound to a value, the effect will set the value to true or false depending on the type of effect. For example, a Fade sets it to false, and Appear sets it to true. Keeping this in mind, we remove the setVisisble(false) call from the removeListener() method and we're done.

This is not a great solution though, as it requires you to check the visible flag wherever you consider the list of items in your business logic. The second solution involves creating a phase listener that really removes the item from the list, after the effect completes. It goes like this. First create a bean to represent phase event,

public class PhaseEventAction {

public class PhaseEventAction {
  private PhaseId phaseId;
  private boolean doBeforePhase;
  private String action;
  private Object[] arguments;
  private Class[] parameters;

 

Now create an implementation of PhaseListener to process the above phase events,

public class QueuedActionListener implements PhaseListener {
  public PhaseId getPhaseId() {
    return PhaseId.ANY_PHASE;
  }

  public void beforePhase(PhaseEvent pe) {
    checkForOperations(true, pe);
  }
  public void afterPhase(PhaseEvent pe) {
    checkForOperations(false, pe);
  }
  private void checkForOperations(boolean doBeforePhase, PhaseEvent evt) {
    FacesContext fc = FacesContext.getCurrentInstance();
    QueuedActionBean qab = (QueuedActionBean)fc.getApplication().
      createValueBinding("#{queuedActionBean}").getValue(fc);
    List<PhaseEventAction> invoked = new ArrayList<PhaseEventAction>();

    for (PhaseEventAction pea : qab.getPhaseEventActions()) {
      if (pea.getPhaseId() == evt.getPhaseId()) {
        if (pea.isDoBeforePhase() == doBeforePhase) {
          javax.faces.application.Application a = fc.getApplication();
          MethodBinding mb = a.createMethodBinding(pea.getAction(), pea.getParameters());
          if (mb != null) {
            mb.invoke(fc, pea.getArguments());
            invoked.add(pea);
          }
        }
      }
    }

    qab.getPhaseEventActions().removeAll(invoked);
  }
}  

The idea is that we're creating a queue of events. Specifically in this case, clients will add remove events to the queue to be processed not until the RENDER_PHASE. Let's keep going. Create a bean to hold the queue,

public class QueuedActionBean implements Serializable {
  private List<PhaseEventAction> phaseEventActions = 
    new ArrayList<PhaseEventAction>();
  public List<PhaseEventAction> getPhaseEventActions() {
    return phaseEventActions;
  }
}

Now in faces-config.xml, declare the queue bean,

<managed-bean>
  <managed-bean-name>queuedActionBean</managed-bean-name>
  <managed-bean-class>com.sun.identity.admin.model.QueuedActionBean</managed-bean-class>
  <managed-bean-scope>session</managed-bean-scope>
</managed-bean>

and declare the phase listener,

<lifecycle>
  <phase-listener>
    com.sun.identity.admin.model.QueuedActionListener
  </phase-listener>
</lifecycle>

Client beans that want to play this game inject queuedActionBean into themselves,

<managed-property>
  <property-name>queuedActionBean</property-name>
    <value>#{queuedActionBean}</value>
  </managed-property>
</managed-bean>

and add PhaseEventAction objects onto the queue. JSF calls QueuedActionListener, which processes the events if the conditions are right. In this case, we want the phase ID to be RENDER_RESPONSE and doBeforePhase to be false. Here's our new removeListener() that includes the code to add the PhaseEventAction to the queue,

public void removeListener(ActionEvent e) {
  Effect e = new Fade();
  e.setSubmit(true);
  e.setTransitory(false);
  bean.setEffect(e);
  PhaseEventAction pea = new PhaseEventAction(); 
  pea.setDoBeforePhase(false);  
  pea.setPhaseId(PhaseId.RENDER_RESPONSE);
  pea.setAction("#{viewConditionsHandler.handleRemove}");
  pea.setParameters(new Class[] { SomeClass.class });
  pea.setArguments(new Object[] { someObject });

  getQueuedActionBean().getPhaseEventActions().add(pea);
}

While we used this to solve a very specific problem, the solution is generalized. Set into the PhaseEventAction the phase, whether to do the action before or after the phase, the action method, the signature of the action method, and the arguments to pass to the action method.

About

jtb

Search

Categories
Archives
« May 2015
SunMonTueWedThuFriSat
     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
      
Today