为什么第一个 AJAX 调用会重置我的视图参数?

Why does the first AJAX call reset my view parameter?

我无法找出为什么第一个 ajax 调用会导致我的视​​图参数的 setter 被再次调用,而每个后续调用都不会再次调用 setter。

我有以下简单的视图 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;
    }
}

我还有一个非常基本的 .xhtml 页面,它只包含一个按钮:

<?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>

现在,在测试此页面时,我在浏览器中调用 https://localhost:8443/test/test.xhtml?param=foo。正如我所料,日志声称视图参数设置为 "foo"。现在我正在努力的地方是,当我第一次按下按钮时,日志再次声称参数设置为 "foo" 证明 setter 被再次调用。我不明白为什么 ajax 请求再次设置视图参数。令我困惑的是,任何后续按钮点击都不会再次调用视图参数的 setter,尤其是第一次和所有后续调用看起来完全一样。

所以我的问题是:

我是 运行 Wildfly 19 上的示例,如果有任何帮助,它使用 Mojarra 2.3.9.SP06。

编辑 1: 为了更清楚,为什么这个问题与 f:viewParam lost after ajax call 不同。另一个问题询问为什么视图参数在第一次 ajax 调用后丢失以及如何始终发送它们。这是一个完全相反的问题:为什么视图参数是第一次发送,如何防止这种情况?

另一个问题的答案声称可以调用FacesContext.getCurrentInstance().isPostback()。我知道这一点。虽然它当然可以检测 ajax 召回并使我能够在这种情况下不重置视图参数,但它不会 阻止 视图参数的 setter 从一开始就被调用。这是我理想中想要实现的。我也会满足于至少理解为什么视图参数在第一次 ajax 调用时被区别对待。我猜有些概念上我还没有理解。

编辑 2: 我在 https://github.com/eclipse-ee4j/mojarra/issues/4714.

下提交了错误报告

快速解决方案

解决此问题的最佳方法是使用 o:form 并将 includeViewParams 设置为 truesetParam 在每个 ajax 请求上调用;仅ajax 请求中参数可以更改的方式)。

@Kukeltje 已经说过使用 o:viewParam(这与覆盖 UIViewParameter 一样),因此 setParam 方法仅在开始时调用一次。


说明

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

可能理解这一点的最好方法是逐阶段分析(查看 source code 以了解方法的作用也很有帮助):


初始请求

恢复视图阶段无特定内容

应用请求值阶段:已调用 decode 并使用当前参数值

设置了 rawValue

流程验证阶段无特定内容

更新模型值阶段:调用setParam,然后UIInput.resetValues()submittedValue设置为空

调用应用程序阶段无特定内容

Render Response PhasesetSubmittedValue(为 null)使用 rawValue 调用(rawValue 已设置;请参阅 Apply请求值阶段)

第一个Ajax

恢复视图阶段:rawValue 重新初始化为null

应用请求值阶段decode被调用并且rawValue被设置为当前参数值(参数值为null

流程验证阶段无特定内容

更新模型值阶段:使用 submittedValue 调用 setParam,该 submittedValue 设置为 null,但随后在 呈现响应阶段;再次调用 UIInput.resetValues() 并将 submittedValue 设置为 null

调用应用程序阶段无特定内容

渲染响应阶段:再次调用setSubmittedValue并设置为rawValue,即null

每个后续 ajax 请求

submittedValuerawValuenull 所以恢复参数值的所有可能性都被破坏了。 setParam 再也没有被调用过。


所有解决方案

  • 重写 encodeAll 方法不再执行任何操作,因此 UIInput.resetValues() 将永远重置值(参见 how to override components
  • 使用 o:viewParam(没有 rawValue 变量)
  • 使用o:form

当参数在ajax请求期间不改变时,前两个解决方案是最好的。


覆盖 UIViewParameter

要覆盖 UIViewParameter 创建一个扩展 UIViewParameter 的 class 并将其添加到 faces-config.xml:

<component>
    <component-type>javax.faces.ViewParameter</component-type>
    <component-class>com.example.CustomUIViewParameter</component-class>
</component>

您在概念上没有任何误解。我也不明白。

我目前仍在调查为什么 setter 在第一个且仅在第一个 ajax 回调中被调用。我本来希望它总是或永远不会被调用。 @fuggerjaki61 的分析在某种程度上是正确的,但它似乎与围绕 null 或未提交值的更大问题有关。

可以在最简单的解决方案中读取大量信息:OmniFaces o:viewParam 而不是 f:viewParam

并使用

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

(不要忘记声明 xmlns:o="http://omnifaces.org/ui",但既然你 应该 ;-) 无论如何都要使用 OmniFaces,我假设它已经存在 :-) )

根据我阅读的所有信息,我认为可能设置

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

也可能解决它,但没有。 setter 在第一次 ajax 调用时仍然使用旧值调用,在第二次和后续调用中,如果未提交,它只会显式地将值设置为 null。也不是你想要的。

更多详情

@fuggerjaki61 的解决方案可能有效,但我不确定在其他情况下的后果,因为我也可以通过更改其他内容来解决这个问题但打破了其他情况。如果我尝试将 o:viewParam 的基础知识与 f:viewParam 进行比较,则提交的值(如@fuggerjaki61 在另一个答案中所指)确实发挥了作用。它保存在本地 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.
}

而在 f:viewParam 中,它被读取并设置到 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);
} 

阅读这里的 java 文档我个人会说你的问题中的 "why" 看起来某处有错误(或遗漏),尚未确定,但要么o:viewParam

意外或明确解决