Friday, March 16, 2012

Reset non-processed input components on ajax update

Introduction

When JSF validation has failed for a particular form submit and you happen to need to update the values of invalidated input fields later by a different ajax action or even a different ajax form (e.g. populating a field depending on a dropdown selection or the result of some modal dialog form, or clearing out all values with some clear action, etc), then you basically need to reset the target input components in order to get JSF to display the model value which was edited during invoke action. Otherwise JSF will still display its local value as it was during the validation failure and keep them in an invalidated state.

This problem is the easiest to understand if you see it yourself. Use this simple testcase view:


<style>.error { background-color: #fee; }</style>
<h:form id="form1">
    <p>input1: Enter something (but don't enter "Updated!").</p>
    <p>
        <h:inputText id="input1" value="#{bean.input1}" required="true"
                     styleClass="#{component.valid ? '' : 'error'}" />
        <h:message for="input1" errorClass="error" />
    </p>
    <p>input2: Leave this field empty or enter a non-numeric value to cause a validation failure.</p>
    <p>
        <h:inputText id="input2" value="#{bean.input2}" required="true"
                     styleClass="#{component.valid ? '' : 'error'}" />
        <h:message for="input2" errorClass="error" />
    </p>
    <p>Press "submit" and then "update". The "update" simulates changing model values externally.</p>
    <p>
        <h:commandButton value="Submit" action="#{bean.submit}">
            <f:ajax execute="@form" render="@form" />
        </h:commandButton>
        <h:commandButton value="Update" action="#{bean.update}">
            <f:ajax execute="@this" render="@form" />
        </h:commandButton>
    </p>
</h:form>

With this bean:


@ManagedBean
@ViewScoped
public class Bean {

    private String input1;
    private Integer input2;

    public void submit() {
        //
    }

    public void update() {
        input1 = "Updated!";
        input2 = 42;
    }

    // Getters/setters.
}

Problem: input1 is not updated with text "Updated!" and input2 is still marked invalid! The problem can be understood by the following JSF facts:

  • When JSF validation succeeds for a particular input component during the validations phase, then the submitted value is set to null and the validated value is set as local value of the input component.
  • When JSF validation fails for a particular input component during the validations phase, then the submitted value is kept in the input component.
  • When at least one input component is invalid after the validations phase, then JSF will not update the model values for any of the input components. JSF will directly proceed to render response phase.
  • When JSF renders input components, then it will first test if the submitted value is not null and then display it, else if the local value is not null and then display it, else it will display the model value.
  • As long as you're interacting with the same JSF view, you're dealing with the same component state.

Ideally, when JSF needs to update/re-render an input component by an ajax request, and that input component is not included in the process/execute of the ajax request, then JSF should reset the input component's value. This has been requested as JSF spec issue 1060.

OmniFaces to the rescue

OmniFaces has recently got a ResetInputAjaxActionListener which solves exactly this problem (source code here).

If you register it as <action-listener> in faces-config.xml, or add it as <f:actionListener> to the update command button as follows,


    <h:commandButton value="Update" action="#{bean.update}">
        <f:ajax execute="@this" render="@form" />
        <f:actionListener type="org.omnifaces.eventlistener.ResetInputAjaxActionListener" />
    </h:commandButton>

Then this problem will completely disappear. The principle is rather simple, here's a (simplified) extract of relevance from its source code:


FacesContext context = FacesContext.getCurrentInstance();
PartialViewContext partialViewContext = context.getPartialViewContext();

if (!partialViewContext.isAjaxRequest()) {
    return; // Ignore synchronous requests. All executed and rendered inputs are the same anyway.
}

Collection<String> renderIds = partialViewContext.getRenderIds();

if (renderIds.isEmpty()) {
    return; // Nothing to render, thus also nothing to reset.
}

UIViewRoot viewRoot = context.getViewRoot();
Set<EditableValueHolder> inputs = new HashSet<EditableValueHolder>();

// First find all to be rendered inputs and add them to the set.
for (String renderId : renderIds) {
    findAndAddEditableValueHolders(viewRoot.findComponent(renderId), inputs);
}

// Then find all executed inputs and remove them from the set.
for (String executeId : partialViewContext.getExecuteIds()) {
    findAndRemoveEditableValueHolders(viewRoot.findComponent(executeId), inputs);
}

// The set now contains inputs which are to be rendered, but which are not been executed. Reset them.
for (EditableValueHolder input : inputs) {
    input.resetValue();
}

17 comments:

smith said...

Hello Balusc,
nice to hear from you again!

In out project we had some discussion how we should handle this case.
We think that " JSF should reset the input component's value" is the best approach in general, but it may be uncomfortable for the user, when she changed a long string, like "92827272744" or an email adress, which validation fails just as the last position.
In this case it would be kind to give the user the chance to chance the wrong digit?!

BalusC said...

The action listener is intented to be used on forms/buttons which are supposed to update the form by preparing the values in the action(listener) method. If you don't want to do this, just don't use it :)

BalusC said...

Also, if the input component is included in the ajax process, it won't be resetted.

pandoo said...

very nice idea! There seems to be a problem if you decide to register the ResetInputAjaxActionListener globally in your faces-config.xml. You need to delegate the event to the next action listener (which normally is the jsf action listener called 'ActionListenerImpl'). Just make sure that the constructor from ResetInputAjaxActionListener takes a ActionListener parameter and assigns it to a member variable. At the end of the processAction method delegate the event to the next ActionLister which is the saved member variable.

BalusC said...

@pandoo: the aforementioned issue has been fixed! Thank you for reporting.

spentmiles said...

I'm trying to use this with an actionLink that uses EL 2.2 / JSF2 to call an action method with a parameter:





I've abbreviated the above, but I know from debugging that the reset-listener is getting executed. However, JSF then throws a NullPointerExcetion when it tries to bind the method argument to the action method. It seems like the method argument has been removed from the submission values.

Any ideas?

Asif Raza said...

nice

Antibrumm said...

Hi BalusC

I'm currently getting my hands dirty with my first JSF2 project and I got always a little feeling like fixing the framework with each step I try.

First f:ajax broken because formIds need to be defined all the time and now this annoying feature of stale expression evaluation.

Thanks to your ResetListener! It helps a great deal.

I think i discovered one small issue in your code if the component is not rendered in between two requests.

Currently my pages are mostly looking like this:

- table and query engine on top
- (multitab)editor component below
- Ajax rendering

Imagine an editor panel which is submitting invalid values, then is discarded (reset instance and render empty editor) and then is rendered again for another entity. Bam, old values.

I didn't fully dig into your code and just saw that you use the VisitHint.SKIP_UNRENDERED. I was able to fix this behaviour by changing it to SKIP_TRANSIENT.

Praveen said...

Hello

We are facing a critical issue with our application. It would be helpful if anyone can give some ideas

We are having 4 tabs ( using prime faces), the page is same ( using render condition to display based on tab). The page is bind to entity and on every click of tab we are hitting the DB and getting the data and setting in view scope.

The problem we are facing is 1 st tab input holder values are getting propagated to 2nd tab ( thin its posting) and getting displayed. If we use labels it's not an issue.

Daniel said...

I'm not sure if this is the place to ask it, but... It seems that the doing too much "visitTree" , In my scenario this causes a not event rendered elements to try to invoke their EL expression in the value attribute which cause eventually Null Pointer Exceptions, But I don't want to change my code by adding Null Pointer , that's why I made their render condition to be false, so , is it possible to no not visit a not rendered elements , or even better is it possible to specify the id of the element that its children should be reset ?

Thanks ahead,
Daniel.

Gigi said...

I read that omnifaces is not designed for portlet, but this listener in particular, works in portlet context?

Bauke Scholtz said...

@Gigi: feel free to fork your own for portlets.

Tiago said...
This comment has been removed by the author.
Tiago said...

Nice solution, but for those using a primefaces datatable with rowSelection as their "updater", unfortunatelly "f:actionListener" doesn't seem to be an option. My workaround was to use the attribute "listener" in "p:ajax". I registered as my p:ajax listener a method in my been that calls "ResetInputAjaxActionListener#processAction". As parameter for the "processAction" method I passed null, since p:ajax doesn't pass the jsf event as parameter.

It doesn't seem to be a very elegant solution, but it works.

Tiago said...

I just found out that Primefaces also has a solution for this problem, so if you're having the problem I described in my past post, just use "RequestContext.getCurrentInstance().reset("your_form_id");" instead of "ResetInputAjaxActionListener#processAction" with null as parameter.

CoolPics said...

i want to update p:output label from back bean without any call to method, i am running thread from which i want to update this component after every second... Please help me as soon as possible

Julián said...

Is any library compatible with JSF 1.2 ? I will appreciate any help.