3

I cannot find out why the first ajax call causes the setter of my view parameter to be called again while every subsequent call does not call the setter again.

I have the following simple view bean:

package test;

import java.io.Serializable;

import javax.faces.view.ViewScoped;
import javax.inject.Named;

@Named
@ViewScoped
public class TestController implements Serializable {

    private static final long serialVersionUID = 1L;

    String param;

    public String getParam() {
        return param;
    }

    public void setParam(String param) {
        System.out.println("param set to " + param);
        this.param = param;
    }
}

I also have a very basic .xhtml page which only contains a single button:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:h="http://java.sun.com/jsf/html"
  xmlns:f="http://java.sun.com/jsf/core">

<h:head></h:head>

<f:metadata>
    <f:viewParam id="param" name="param" value="#{testController.param}"/>
</f:metadata>

<h:form id="form">
  <h:commandButton id="button" value="Test">
    <f:ajax execute="@this"></f:ajax>
  </h:commandButton>
</h:form>

</html>

Now when testing this page I call https://localhost:8443/test/test.xhtml?param=foo in my browser. As I expected the log claims that the view parameter was set to "foo". Now where I'm struggling is that when I first press the button the log again claims that param was set to "foo" proving that the setter was called again. I do not understand why the view parameter is set again by the ajax request. It also puzzles me that any subsequent button click will not call the view parameter's setter again, especially as the first and all subsequent calls look exactly alike.

So my questions are:

  • Why is the view parameter's setter called on the first ajax call but not on subsequent calls?
  • Is there any way to prevent this behavior?

I'm running the example on Wildfly 19 which uses Mojarra 2.3.9.SP06 if that is of any help.

EDIT 1: To make it clearer, why this question is different from f:viewParam lost after ajax call. The other question asks why the view parameters are lost after the first ajax call and how to always send them. This is question asks exactly the opposite: Why are the view parameters send the first time anyway and how to prevent this?

The answer to the other question claims that one can call FacesContext.getCurrentInstance().isPostback(). I'm aware of this. While it of course works in the sense of detecting the ajax recall and enables me to not reset the view parameters in this case it does not prevent the view parameter's setter from being called in the first place. This is what I ideally want to achieve. I would also content myself with at least understanding why the view parameters are treated differently on the first ajax call. I guess there is something conceptually I have not understood.

EDIT 2: I filed a bug report under https://github.com/eclipse-ee4j/mojarra/issues/4714.

Björn Zurmaar
  • 826
  • 1
  • 9
  • 22
  • Does this answer your question? [ – Kukeltje May 13 '20 at 10:00
  • Glad you're commenting on my question, Kukeltje. Your answers helped me many times. BalusC claims in his answer that the submits by default to an URL without the query string. This is exactly what I expected in the first place. I don't understand why it behaves differently the first time the ajax call is send. – Björn Zurmaar May 13 '20 at 10:14
  • Can you describe what is different between your question and the other one? Besides some wording, to me it seems the same issue. And the way to prevent it is also in the duplicate – Kukeltje May 19 '20 at 16:29
  • Thanks for having a second look. I updated the question to make the difference more distinct. – Björn Zurmaar May 19 '20 at 19:35
  • The other question, at least the title, was ambiguous. It is all related to the first ajax call, not the second. Sure it is set on your first call? Sure it is an ajax call? What if you add an action to call a method? Does the behaviour change? – Kukeltje May 20 '20 at 06:07
  • I'm sure it is set on the first call. I can verify it's an XHR in the browser's development tools in the network tab and on the server side via debugging. When adding an action nothing changes (besides the action being called as expected). – Björn Zurmaar May 22 '20 at 10:46
  • Ok, let me try to replicate the coming days. – Kukeltje May 22 '20 at 18:18
  • 1
    In the source code of the [UIViewParameter](https://github.com/alessandroce/Mojarra/blob/master/impl/src/main/java/javax/faces/component/UIViewParameter.java) around line 393 there are these two comments `// QUESTION is it okay that a null string value may be suppressing the view parameter value?` and `// ANSWER: I'm not sure.`. When the parameter is set null during the second ajax call the method is not called anymore. – fuggerjaki61 May 23 '20 at 14:44
  • Do you have the context parameter javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL? – Kukeltje May 23 '20 at 18:26
  • 1
    I did not set the context param INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL. "// ANSWER: I'm not sure." is exactly the kind of thing you want to read in a reference implementation... ^^ – Björn Zurmaar May 23 '20 at 19:44
  • @Kukeltje the problem is that every solution (except `o:form` solution) would result that a change of the param (e.g. first ajax `param=foo` and second ajax `param=bar`) isn't recognized. – fuggerjaki61 May 24 '20 at 13:12

2 Answers2

4

There is nothing you conceptually misunderstood. I don't understand it either.

I'm currently still investigating on the why the setter is called on the first and only on the first ajax callback. I would have expected it to be always or never called. The analysis of @fuggerjaki61 is somewhat in the right direction but it seems to be related to the bigger issue around null or not submitted values.

Lots of info can be read in what is the easiest solution: the OmniFaces o:viewParam instead of f:viewParam

And use

<o:viewParam id="param" name="param" value="#{testController.param}"/>

(do not forget to declare xmlns:o="http://omnifaces.org/ui", but since you should ;-) be using OmniFaces anyway, I assume it is already there :-) )

From al info I read I thought that maybe setting

<context-param>
    <param-name>javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL</param-name>
    <param-value>true</param-value>
</context-param>

might solve it as well, but it does not. The setter is still called with the old value on the first ajax call and on the second and subsequent calls it only explicitly sets the value to null if it is not submitted. Also not what you seem to be wanting.

More details

The solution of @fuggerjaki61 might work, but I'm not sure about the consequences in other situations, since I can also get a fix for this issue by changing other things but breaking other cases. And if I try to compare the basics of o:viewParam with f:viewParam the submitted value (as referred to by @fuggerjaki61 in the other answer) does play a role. It is kept local in the o:viewParam

@Override
public String getSubmittedValue() {
    return submittedValue;
}

@Override
public void setSubmittedValue(Object submittedValue) {
    this.submittedValue = (String) submittedValue; // Don't delegate to statehelper to keep it stateless.
}

while in the f:viewParam it is being read from and set to the stateHelper

@Override
public Object getSubmittedValue() {
    return getStateHelper().get(PropertyKeys.submittedValue);
}

/**
 * PENDING (docs)  Interesting that submitted value isn't saved by the parent
 * @param submittedValue The new submitted value
 */
@Override
public void setSubmittedValue(Object submittedValue) {
    getStateHelper().put(PropertyKeys.submittedValue, submittedValue);
} 

Reading the java docs here I'd personally say on the "why" in your question to me looks like there is a bug (or omission) somewhere, yet to be identified, but either accidentilly or explicitly solved by o:viewParam

Kukeltje
  • 12,223
  • 4
  • 24
  • 47
2

Quick Solutions

The best way to solve this problem is using the o:form with includeViewParams set to true (setParam called on every ajax request; only way when parameters can change in ajax requests).

OR

Already said by @Kukeltje using the o:viewParam (that does the same like overriding the UIViewParameter), so the setParam method is only called once at the beginning.


Explanation

Basically is the parameter value saved during the initial request to the first ajax request. After first ajax request the value is finally lost.

Probably the best way to understand this is to analyse phase for phase (looking at the source code to see what the methods do is also helpful):


Initial Request

Restore View Phase: nothing specific

Apply Request Values Phase: decode called and rawValue is set with the current parameter value

Process Validations Phase: nothing specific

Update Model Values Phase: setParam is called and after that UIInput.resetValues() that sets the submittedValue to null

Invoke Application Phase: nothing specific

Render Response Phase: setSubmittedValue (which was null) is called with rawValue (rawValue was already set; see Apply Request Values Phase)

First Ajax

Restore View Phase: rawValue is reinitialized to null

Apply Request Values Phase: decode called and rawValue is set with the current parameter value (parameter value is null)

Process Validations Phase: nothing specific

Update Model Values Phase: setParam is called with the submittedValue that was set to null but then set again in Render Response Phase; UIInput.resetValues() is called again and submittedValue is set to null.

Invoke Application Phase: nothing specific

Render Response Phase: setSubmittedValue is again called and set to rawValue which is null

Every following ajax request

submittedValue and rawValue is null so every possibility to restore the parameter value is destroyed. setParam is never called again.


All Solution

  • Overriding the encodeAll method to do nothing anymore, so UIInput.resetValues() resets values forever (see how to override components)
  • Using o:viewParam (doesn't have rawValue variable)
  • Using o:form

When the parameters don't change during ajax requests the top two solutions are the best.


Overriding UIViewParameter

To override the UIViewParameter create a class that extends the UIViewParameter and add this to the faces-config.xml:

<component>
    <component-type>javax.faces.ViewParameter</component-type>
    <component-class>com.example.CustomUIViewParameter</component-class>
</component>
fuggerjaki61
  • 822
  • 1
  • 11
  • 24
  • the link you also refer to had a different title initially, see the comments beneeath its answer and check the edit history. Maybe the title was correct... Maybe also add a comment on the answer there and refer to your findings – Kukeltje May 23 '20 at 17:37
  • @Kukeltje the only resource I used is the linked Mojarra JSF implementation of the `UIViewParameter`. also I only mentioned the link ( f:viewParam lost after ajax call) for other users to look at first. – fuggerjaki61 May 23 '20 at 17:50
  • I know why you referenced the other question, I just requested you to check the original title and see if it was originally correct and not any more. But your answer and my other investigation lead me to the sumitting of null values. Check my recent comments on the other question (posted there so BalusC gets informed) – Kukeltje May 24 '20 at 11:03
  • I had an extremely hard time deciding which answer should receive the bounty. Both of you spent lots of time investigating and helping me out and I really appreciate it. This is exactly what Stackoverflow is about. I also learned something new, namely that you can define your own components via faces-config.xml. It's a bit sad you edited the example for this out of your answer. Kukeltje's answer is a bit more on a conceptual layer and this tipped the scales for me. I feel that this is unfair and I'd love to have you both awarded the bounty. Thank you very much for your effort! – Björn Zurmaar May 27 '20 at 08:45