I found a "workaround" and for those of you who are interested how it's working:

a) create a fresh instance of your bean in "onBeginSubmit".
b) Tapestry will transfer all form values from the particular bean to this new instance between "onBeginSubmit" and "onAfterSubmit" c) loosing the PK can be avoided by adding a redundant hidden field in the tml d) make sure submitNotifier is correctly positioned around the fields edited not wider.

Thanks

Jens




---- FINAL TML ----

<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd";
      xmlns:p="tapestry:parameter">

<t:form id="XValueForm" t:id="XValueForm">
    <table>
<t:loop source="XValues" value="xValue" encoder="XValueEncoder" formState="iteration"> <tr t:type="zone" update="show" t:id="row" id="row_${xValue.pk}">
                <t:if test="!XValueChanged">
                    <t:delegate to="block:readOnly"/>
                </t:if>
                <t:if test="XValueChanged">
                    <t:delegate to="block:editable"/>
                </t:if>
            </tr>

            <t:block t:id="readOnly">
                <td>${xValue.pk}</td>
                <td>${xValue.s}</td>
<td><a t:type="eventlink" t:id="modifyXValue" context="xValue.pk" zone="row_${xValue.pk}">edit</a></td>
            </t:block>

            <t:block t:id="editable">
                <t:submitNotifier>
<td>${xValue.pk}<t:textfield value="xValue.pk" type="hidden"/></td>
                    <td><t:textfield value="xValue.s"/></td>
<td><a t:type="eventlink" t:id="revertXValue" context="xValue.pk" zone="row_${xValue.pk}">revert</a></td>
                </t:submitNotifier>
            </t:block>
        </t:loop>
    </table>
    <input t:type="submit" t:defer="true" name="save" value="save"/>
</t:form>

</html>



---- FINAL JAVA ----

(this class is slightly modified to avoid problems introduced by using static lists instead of services as it should be)

package de.threeoldcoders.grayhair.pages;

import de.threeoldcoders.grayhair.services.XValue;
import org.apache.tapestry5.Block;
import org.apache.tapestry5.ValueEncoder;
import org.apache.tapestry5.annotations.Id;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;

import javax.inject.Inject;
import java.util.*;

// http://localhost:8080/GH1Z/index.xvalueform
// we had a filter as workaround? mailinglist??
public class Index
{
    private static final List<XValue> _allXValues
= new ArrayList<XValue>(Arrays.asList(new XValue(1, "1"), new XValue(2, "2"), new XValue(3, "3")));

    private static final Set<XValue> _changes = new HashSet<XValue>();

    @Inject private Request _request;
    @Inject private AjaxResponseRenderer _arr;

    @Inject @Id("readOnly") private Block _blockReadOnly;
    @Inject @Id("editable") private Block _blockEdit;

    @Property XValue _xValue;

    void onBeginSubmit()
    {
        _xValue = new XValue(-1, "");
    }

    void onAfterSubmit()
    {
        _changes.remove(_xValue);
        _changes.add(_xValue);  // modified instance replaces old one
    }

    void onSuccessFromXValueForm()
    {
        // do whatever you want
        for (final XValue change : _changes) {
            _allXValues.remove(change);
            _allXValues.add(change);
        }
        _changes.clear();

        // keep ordering by PK for clarity reasons
        Collections.sort(_allXValues, new Comparator<XValue>()
        {
@Override public int compare(final XValue xValue1, final XValue xValue2)
            {
                return xValue1.getPk().compareTo(xValue2.getPk());
            }
        });
    }

    Object onModifyXValue(final long pk)
    {
        // just retrieve original instance and mark it as changed
        final XValue orig = getXValueEncoder().toValue(Long.toString(pk));
        _changes.add(new XValue(orig.getPk(), orig.getS()));

        if (_request.isXHR()) {
            _xValue = orig;
            _arr.addRender("row_" + _xValue.getPk(), _blockEdit);
        }
        return null;
    }

    Object onRevertXValue(final long pk)
    {
        // just retrieve original instance and mark it as changed
        final XValue orig = getXValueEncoder().toValue(Long.toString(pk));
        _changes.remove(orig);

        if (_request.isXHR()) {
            _xValue = orig;
            _arr.addRender("row_" + _xValue.getPk(), _blockReadOnly);
        }
        return null;
    }

    public List<XValue> getXValues()
    {
        return _allXValues;
    }

    public boolean isXValueChanged()
    {
        return _changes.contains(_xValue);
    }

    public ValueEncoder<XValue> getXValueEncoder()
    {
        return new ValueEncoder<XValue>()
        {
            @Override public String toClient(final XValue value)
            {
                return value.getPk().toString();
            }

            @Override public XValue toValue(final String clientValue)
            {
                return getXValues().get(Integer.parseInt(clientValue) - 1);
            }
        };
    }
}


---- THE BEAN ----

package de.threeoldcoders.grayhair.services;

public class XValue
{
    private Long _pk;
    private String _s;

    public XValue()
    {
    }

    public XValue(final long pk, final String s)
    {
        _pk = pk;
        _s = s;
    }

    public Long getPk()
    {
        return _pk;
    }

    public void setPk(final Long pk)
    {
        _pk = pk;
    }

    public void setS(final String s)
    {
        _s = s;
    }

    public String getS()
    {
        return _s;
    }

    @Override
    public boolean equals(final Object o)
    {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        final XValue xValue = (XValue) o;

        if (!_pk.equals(xValue._pk)) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode()
    {
        return _pk.hashCode();
    }

    @Override public String toString()
    {
        final StringBuilder sb = new StringBuilder("XValue{");
        sb.append("_pk=").append(_pk);
        sb.append(", _s='").append(_s).append('\'');
        sb.append('}');
        return sb.toString();
    }
}







Am 04.11.13 08:14, schrieb Jens Breitenstein:
Hi All!

I am struggling since days with a Tapestry Bug(?) and maybe one of you have an idea whats wrong or what my mistake may be...

Scenario: I use a loop to display multiple rows in a table. Each row allows inline editing if the user presses a link. Due to link pressing the particular row's zone is swapped to show edit fields instead of pure text. Everything works beside submit. Submit always changes the last element in the loop not the one it's iterating over. I dubugged it down to Tapestry's PropBinding and noticed there is always the last element used as "root" but maybe I am fooled by the proxies.

I can provide a WAR if you like, but maybe the sources are enough because I tried to strip down to bare minumum.

tml:

<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd";
      xmlns:p="tapestry:parameter">

<t:form>
    <table>
<t:loop source="XValues" value="xValue" encoder="XValueEncoder" formState="iteration">
            <t:submitNotifier>
<tr t:type="zone" update="show" t:id="row" id="row_${xValue.pk}">
                    <t:if test="!XValueChanged">
                        <t:delegate to="block:readOnly"/>
                    </t:if>
                    <t:if test="XValueChanged">
                        <t:delegate to="block:editable"/>
                    </t:if>
                </tr>

                <t:block t:id="readOnly">
                    <td>${xValue.pk}</td>
                    <td>${xValue.s}</td>
<td><a t:type="eventlink" t:id="modifyXValue" context="xValue.pk" zone="row_${xValue.pk}">edit</a></td>
                </t:block>

                <t:block t:id="editable">
                    <td>${xValue.pk}</td>
                    <td>
                        <t:textfield t:id="xs" value="xValue.s"/>
                    </td>
<td><a t:type="eventlink" t:id="revertXValue" context="xValue.pk" zone="row_${xValue.pk}">revert</a></td>
                </t:block>
            </t:submitNotifier>
        </t:loop>
    </table>
    <input t:type="submit" name="save" value="save"/>
</t:form>

</html>




Basically the loop displays 3 rows and uses a delegate to decide if it has to show edit fields or pure text. Each row has a unique zone id based on the pk. Each eventlink just transports the pk. Regardless of formState ("iterable" or "values") still the wrong element is updated.

Java:

package de.threeoldcoders.grayhair.pages;

import de.threeoldcoders.grayhair.services.XValue;
import org.apache.tapestry5.Block;
import org.apache.tapestry5.ValueEncoder;
import org.apache.tapestry5.annotations.Id;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;

import javax.inject.Inject;
import java.util.*;

public class Index
{
private static final List<XValue> _allXValues = Arrays.asList(new XValue(1, "1"), new XValue(2, "2"), new XValue(3, "3"));
    private static final List<XValue> _changes = new ArrayList<XValue>();

    @Inject private Request _request;
    @Inject private AjaxResponseRenderer _arr;

    @Inject @Id("readOnly") private Block _blockReadOnly;
    @Inject @Id("editable") private Block _blockEdit;

    @Property XValue _xValue;

    void onAfterSubmit()
    {
final XValue orig = getXValueEncoder().toValue(Long.toString(_xValue.getPk())); // trick to retrieve original instance
        if (! orig.getS().equals(_xValue.getS())) {
            orig.setS(_xValue.getS());
        }
        _changes.remove(orig);
    }

    Object onModifyXValue(final long pk)
    {
        // just retrieve original instance and mark it as changed
final XValue orig = getXValueEncoder().toValue(Long.toString(pk));
        _changes.add(orig);

        if (_request.isXHR()) {
            _xValue = orig;
            _arr.addRender("row_" + _xValue.getPk(), _blockEdit);
        }
        return null;
    }

    Object onRevertXValue(final long pk)
    {
        // just retrieve original instance and mark it as changed
final XValue orig = getXValueEncoder().toValue(Long.toString(pk));
        _changes.remove(orig);

        if (_request.isXHR()) {
            _xValue = orig;
            _arr.addRender("row_" + _xValue.getPk(), _blockReadOnly);
        }
        return null;
    }

    public List<XValue> getXValues()
    {
        return _allXValues;
    }

    public boolean isXValueChanged()
    {
        return _changes.contains(_xValue);
    }

    public ValueEncoder<XValue> getXValueEncoder()
    {
        return new ValueEncoder<XValue>()
        {
            @Override public String toClient(final XValue value)
            {
                return value.getPk().toString();
            }

            @Override public XValue toValue(final String clientValue)
            {
                return getXValues().get(Integer.parseInt(clientValue)-1);
            }
        };
    }
}


As I wanted it as small as possible this sample makes no use of services, modules and all, thus my datamodel is simply hardcoded as static members. I know this is not working in the real world. Each event handler returns the block according to the internal state so it toggles to edit mode or readonly mode, depending on the link, which is working.

When I now press "edit" in the second row and change "2" to "B" for example and press submit, the "B" appears in row "3" because this is the last element in the list. It will never change something else regardless how many rows are in edit mode. The submit notifier correctly iterates all three rows, but it looks like the "property set" call comes ways to late on a wrong instance. I even tried "defer" true / false without any difference.


Thanks in advance

Jens





---------------------------------------------------------------------
To unsubscribe, e-mail: users-unsubscr...@tapestry.apache.org
For additional commands, e-mail: users-h...@tapestry.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: users-unsubscr...@tapestry.apache.org
For additional commands, e-mail: users-h...@tapestry.apache.org

Reply via email to