Loop / Zone / Form-submit broken(?)

classic Classic list List threaded Threaded
2 messages Options
Reply | Threaded
Open this post in threaded view
|

Loop / Zone / Form-submit broken(?)

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: [hidden email]
For additional commands, e-mail: [hidden email]

Reply | Threaded
Open this post in threaded view
|

Re: Loop / Zone / Form-submit broken(?)

Jens Breitenstein
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: [hidden email]
> For additional commands, e-mail: [hidden email]
>


---------------------------------------------------------------------
To unsubscribe, e-mail: [hidden email]
For additional commands, e-mail: [hidden email]