You are viewing a plain text version of this content. The canonical link for it is here.
Posted to users@tapestry.apache.org by Jens Breitenstein <ma...@j-b-s.de> on 2013/11/04 08:14:40 UTC

Loop / Zone / Form-submit broken(?)

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-unsubscribe@tapestry.apache.org
For additional commands, e-mail: users-help@tapestry.apache.org


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

Posted by Jens Breitenstein <ma...@j-b-s.de>.
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-unsubscribe@tapestry.apache.org
> For additional commands, e-mail: users-help@tapestry.apache.org
>


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