[ 
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)

Reply via email to