[
https://issues.apache.org/jira/browse/MYFACES-4777?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Thomas Andraschko resolved MYFACES-4777.
----------------------------------------
Resolution: Fixed
> With rowStatePreserved="true", a row without saved state of its own shows the
> local value of the row before it
> --------------------------------------------------------------------------------------------------------------
>
> Key: MYFACES-4777
> URL: https://issues.apache.org/jira/browse/MYFACES-4777
> Project: MyFaces Core
> Issue Type: Bug
> Affects Versions: 4.1.5
> Reporter: Bauke Scholtz
> Assignee: Thomas Andraschko
> Priority: Major
> Fix For: 5.0.0
>
>
> With {{rowStatePreserved="true"}}, an {{<h:dataTable>}} or {{<ui:repeat>}}
> restores the initial state of its children when it enters a row which has no
> saved state of its own yet, such as a row added to the model after the last
> render. When a child had nothing in its state helper initially, that restore
> keeps what the row before left in the child. So the new row shows the local
> value of the row before it.
> h3. Reproducer
> {code:xml}
> <h:form id="form">
> <h:dataTable id="table" value="#{bean.tableRows}" var="row"
> rowStatePreserved="true">
> <h:column>
> <h:panelGroup id="cell">
> <f:event type="postAddToView" listener="#{bean.addInput}" />
> </h:panelGroup>
> </h:column>
> </h:dataTable>
> <ui:repeat id="repeat" value="#{bean.repeatRows}" var="row"
> rowStatePreserved="true">
> <h:panelGroup id="cell">
> <f:event type="postAddToView" listener="#{bean.addInput}" />
> </h:panelGroup>
> </ui:repeat>
> <h:commandButton id="submit" value="Submit" />
> <h:commandButton id="addRow" value="Add row" action="#{bean.addRow}">
> <f:ajax execute="@this" render="@form" />
> </h:commandButton>
> </h:form>
> {code}
> {code:java}
> @Named
> @ViewScoped
> public class Bean implements Serializable {
> private List<String> tableRows = new ArrayList<>(List.of("0", "1"));
> private List<String> repeatRows = new ArrayList<>(List.of("0", "1"));
> // Adds an input with only an id, so that its initial state has nothing
> in its state helper.
> public void addInput(ComponentSystemEvent event) {
> UIComponent cell = event.getComponent();
> if (cell.getChildCount() == 0) {
> HtmlInputText input = new HtmlInputText();
> input.setId("in");
> cell.getChildren().add(input);
> }
> }
> public void addRow() {
> tableRows.add(String.valueOf(tableRows.size()));
> repeatRows.add(String.valueOf(repeatRows.size()));
> }
> // Getters.
> }
> {code}
> The input has no value expression, so it keeps its local value after the
> submit.
> # Type {{typed}} in row 1 of the table and of the repeat, and click Submit.
> # Click Add row. The ajax request adds row 2 to the model and re-renders the
> form.
> h3. Expected
> Row 1 shows {{typed}}, and the new row 2 is empty.
> h3. Actual
> Row 2 of the table and of the repeat shows {{typed}}, the local value of row
> 1.
> Reproduced on MyFaces 4.1.5-SNAPSHOT on Tomcat 11, with server side state
> saving.
> h3. Cause
> When {{UIData}} enters a row without saved state of its own,
> {{restoreFullDescendantComponentStates()}} restores the initial full state of
> each child:
> {code:java}
> component.clearInitialState();
> component.restoreState(facesContext, childState);
> component.markInitialState();
> {code}
> {{UIComponentBase.restoreState()}} passes the state helper slot of that full
> state to {{_DeltaStateHelper.restoreState()}}. The full state of a component
> which had nothing in its state helper has {{null}} in that slot, and
> {{_DeltaStateHelper.restoreState()}} then returns at once:
> {code:java}
> if (state == null)
> {
> return;
> }
> {code}
> So the state helper keeps what the row before put in it. For a {{UIInput}},
> that includes {{value}} and {{localValueSet}}. {{UIRepeat}} restores the
> initial state of its rows in the same way. The reproducer creates the input
> in Java with only an id, so that its state helper is empty initially.
> h3. Spec
> The javadoc of {{UIData.setRowStatePreserved()}}:
> {quote}
> To accomplish this, {{UIData}} must call {{StateHolder#saveState}} and
> {{TransientStateHolder#saveTransientState}} on its children to capture their
> state on exiting each row. When re-entering the row,
> {{StateHolder#restoreState}} and
> {{TransientStateHolder#restoreTransientState}} must be called in order to
> reinitialize the children to the correct state for the new row.
> {quote}
> The local value of row 1 applies to row 2, so the children of row 2 are not
> in the correct state for that row.
> The specification, section "StateHolder", on {{saveState()}} and
> {{restoreState()}}:
> {quote}
> Gets or restores the state of the instance as a _Serializable_ _Object_.
> {quote}
> Restoring the full state of a component which had nothing in its state helper
> does not restore that state, because the current state helper stays as it is.
> h3. Proposed fix
> When {{UIComponentBase.restoreState()}} restores a full state
> ({{values.length == FULL_STATE_ARRAY_SIZE}}) whose state helper slot is
> {{null}}, clear the state helper. A delta keeps its current meaning, where
> {{null}} means no change.
> Mojarra had the same bug, and fixed it the same way in 4.0.30 and 4.1.21
> ([eclipse-ee4j/mojarra#6128|https://github.com/eclipse-ee4j/mojarra/issues/6128]).
> TCK: {{Issue6128IT}} of the Jakarta Faces 5.0 TCK
> ([jakartaee/faces#2266|https://github.com/jakartaee/faces/pull/2266]).
> MyFaces 4.1.5-SNAPSHOT fails it on an equivalent page.
> 🤖 Generated with Claude Opus 5.5
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)