【问题标题】:Composite Component inside ui:repeat: How to correctly save component stateui:repeat 中的复合组件:如何正确保存组件状态
【发布时间】:2020-03-21 15:11:21
【问题描述】:

我有一个实现UIInput 的自定义组件,它需要保存一些状态信息以供以后在回发请求中重用。独立使用它工作正常,但在<ui:repeat> 内,回发会找到最新呈现的数据行的保存状态。动作调用的日志输出是

INFORMATION: myData is "third foo"
INFORMATION: myData is "third foo"
INFORMATION: myData is "third foo"
INFORMATION: ok action

我期望的地方

INFORMATION: myData is "first foo"
INFORMATION: myData is "second foo"
INFORMATION: myData is "third foo"
INFORMATION: ok action

我知道myComponentui:repeat 内部的一个实例。那么保存组件状态以便为数据集中的每一行正确恢复它的最佳方法是什么?

我的 XHTML 表单:

<h:form>
    <ui:repeat var="s" value="#{myController.data}">
        <my:myComponent data="#{s}"/>
    </ui:repeat>

    <h:commandButton action="#{myController.okAction}" value="ok">
        <f:ajax execute="@form" render="@form"/>
    </h:commandButton>
</h:form>

我的豆子:

@Named
@ViewScoped
public class MyController implements Serializable {

    private static final long serialVersionUID = -2916212210553809L;

    private static final Logger LOG = Logger.getLogger(MyController.class.getName());

    public List<String> getData() {
        return Arrays.asList("first","second","third");
    }

    public void okAction() {
        LOG.info("ok action");
    }
}

复合组件 XHTML 代码:

<ui:component xmlns="http://www.w3.org/1999/xhtml"
  xmlns:h="http://xmlns.jcp.org/jsf/html"
  xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
  xmlns:cc="http://xmlns.jcp.org/jsf/composite">

  <cc:interface componentType="myComponent">
    <cc:attribute name="data"/>
  </cc:interface>

  <cc:implementation>
    <h:panelGrid columns="2">
      <h:outputLabel value="cc.attrs.data"/>
      <h:outputText value="#{cc.attrs.data}"/>
      <h:outputLabel value="cc.myData"/>
      <h:outputText value="#{cc.myData}"/>
    </h:panelGrid>
  </cc:implementation>
</ui:component>

复合组件支持类:

@FacesComponent
public class MyComponent extends UIInput implements NamingContainer {

    private static final Logger LOG=Logger.getLogger(MyComponent.class.getName());

    public String calculateData() {
        return String.format("%s foo", this.getAttributes().get("data") );
    }

    public String getMyData() {
        return (String)getStateHelper().get("MYDATA");
    }

    public void setMyData( String data ) {
        getStateHelper().put("MYDATA", data);
    }

    @Override
    public String getFamily() {
        return UINamingContainer.COMPONENT_FAMILY;
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        this.setMyData( calculateData() );
        super.encodeBegin(context);
    }

    @Override
    public void processDecodes(FacesContext context) {
        super.processDecodes(context);
        LOG.log(Level.INFO, "myData {0}", getMyData() );
    }
}

【问题讨论】:

  • 我不确定“正确的方法”是什么,我可以想到一些解决方法:1)使用"MYDATA" + getClientId()作为状态键 2)使用@987654332 @ 而不是 getStateHelper() 3) 使用 c:forEach 而不是 ui:repeat (这会导致完全不同的组件树,正如 Kukeltje 会在此处指出的那样;p)但是(3)在使用时没有帮助您在 dataTable 中的组件。
  • 感谢@Selaron,但在这种特殊情况下,我真的在寻找“正确的方法”。 c:forEachgetAttributes 在我们的例子中不是解决方法,使用 getClientId() 是一个很好的解决方法。但是,真的,这有点难看,不是吗?
  • 是的,它很丑。我很高兴看到这个问题得到解答,因为到目前为止我必须解决这个问题。
  • 这种方法既奇怪又令人困惑。我的第一次尝试是使用getStateHelper() 而不是this.getAttributes(),因为getStateHelper() 会知道迭代上下文。
  • @BalusC 好吧,getStateHelper() 的行为是我的问题。这里我们有两个请求。在第一个中,组件被编码,我们使用encodeBeginmyData 放入stateHelper。在第二个请求(回发)中,组件被解码。在processDecodes 中,stateHelper 向我们展示了意想不到的数据。这就是我的问题:如何以正确的方式使用stateHelper,以便即使在回发请求中它也能知道迭代器?根据我的预期,它应该开箱即用 - 但事实并非如此。

标签: jsf


【解决方案1】:

刚刚尝试重现您的问题,是的,现在我终于明白了。您只是想将 JSF 组件状态用作计算变量的某种视图范围。我可以理解。观察到的行为确实出乎意料。

简而言之,这在 Leonardo Uribe(MyFaces 提交者)的博客中进行了解释:JSF component state per row for datatables

这种行为背后的原因是像h:dataTableui:repeat这样的标签只保存与EditableValueHolder接口相关的属性(valuesubmittedValuelocalValueSetvalid)。因此,发现使其正常工作的一个常见技巧是从UIInput 扩展您的组件或使用EditableValueHolder 接口,并将您希望在“value”字段中保留每行的状态。

[...]

自 JSF 2.1 起,UIData 实现有一个名为 rowStatePreserved 的新属性。现在这个属性没有出现在h:dataTable 的facelets taglib 文档中,但在UIData 的javadoc 中有。所以修复很简单,只需在你的h:dataTable 标签中添加rowStatePreserved="true"

最后,你基本上有 3 个选择:

  1. 使用 UIInput#value 而不是像 MYDATA 这样的自定义内容

    按照上述博客的指示,只需将getMyData()setMyData() 替换为来自UIInput 的现有getValue()setValue() 方法。您的复合组件已经从它扩展而来。

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        this.setValue(calculateData()); // setValue instead of setMyData
        super.encodeBegin(context);
    }
    
    @Override
    public void processDecodes(FacesContext context) {
        super.processDecodes(context);
        LOG.log(Level.INFO, "myData {0}", getValue() ); // getValue instead of getMyData
    }
    

    同样在 XHTML 实现中(顺便说一句,the &lt;h:outputText&gt; is unnecessary here):

    <h:outputText value="#{cc.value}" /> <!-- cc.value instead of cc.myData -->
    

    但是,当我在 Mojarra 2.3.14 上尝试时,这并没有真正奏效。事实证明,Mojarra 对&lt;ui:repeat&gt; 的实现确实在restore view 期间恢复了EditableValueHolder 状态(耶!),但随后在decode 期间完全清除了它(嗯?),这有点没用了。坦率地说,我不确定它为什么会这样做。我还在 Mojarra 的 UIRepeat 源代码中发现它是 doesn't do that,而在另一个 UIDataUIRepeat 中是 nested。因此,以下将其放入另一个 UIRepeat 尝试迭代空字符串的小技巧使其工作:

    <ui:repeat value="#{''}">
        <ui:repeat value="#{myController.data}" var="s">
            <my:myComponent data="#{s}" />
        </ui:repeat>
    </ui:repeat>
    

    值得注意的是,这一切在 MyFaces 2.3.6 中都不起作用。我没有进一步调试它。


  2. &lt;ui:repeat&gt; 替换为&lt;h:dataTable rowStatePreserved="true"&gt;

    正如上述博客中所暗示的,这确实是UIData javadoc 中的documented。只需将&lt;ui:repeat&gt; 替换为&lt;h:dataTable&gt; 并将其rowStatePreserved 属性显式设置为true。您可以在状态中继续使用您的 MYDATA 属性。

    <h:dataTable value="#{myController.data}" var="s" rowStatePreserved="true">
        <h:column><my:myComponent data="#{s}" /></h:column>
    </h:dataTable>
    

    这在 Mojarra 2.3.14 和 MyFaces 2.3.6 中都适用于我。 不幸的是,UIRepeat 不支持此功能。因此,您将不得不忍受由&lt;h:dataTable&gt; 生成的可能不必要的 HTML &lt;table&gt; 标记。这是在 JSF 2.3 工作期间,但是 discussed 曾经将功能添加到 UIRepeat,但不幸的是在 JSF 2.3 发布之前没有做任何事情。


  3. 在状态键中包含getClientId()

    正如Selaron 在您的问题comments 中所建议的那样,将客户端 ID 作为密钥存储在状态中。

    public String getMyData() {
        return (String) getStateHelper().get("MYDATA." + getClientId());
    }
    
    public void setMyData(String data) {
        getStateHelper().put("MYDATA." + getClientId(), data);
    }
    

    虽然这是一个相对微不足道的变化,但很尴尬。这根本不能推断出可移植性。每次你实现一个应该保存在 JSF 状态的新的(复合)组件属性时,你必须犹豫和三思。你真的希望 JSF 自动处理这个问题。

【讨论】:

  • o:repeat Primefaces 也这样做过一次,原因 iirc... 甚至可能也是一个解决方案。我可以试一试...不确定是否可以独立于实现
  • 胡,感觉更像是一个错误而不是一个功能。无论如何,非常感谢您的努力!顺便说一句,你知道关于 jsf 2.4 的一些正在进行的讨论吗?我认为rowStatePreserved for ui:repeat 是必须的。
  • 我刚刚试了一下p:repeat:它也不保留行状态。
  • @kukeltje 不; p:repeat 只是 Mojarras ui:repeat 的一个分支,为 PRO 用户提供错误修复,这些错误修复仍在旧应用程序服务器上的旧 Mojarra 版本上。
猜你喜欢
  • 1970-01-01
  • 2020-03-27
  • 2019-09-29
  • 2022-01-05
  • 2012-10-12
  • 1970-01-01
  • 1970-01-01
  • 2013-08-23
相关资源
最近更新 更多