0

Method on an@refed object inside a template in a included zul with form binding is called only once

asked 2016-04-14 08:14:55 +0800

MathieuPiette gravatar image MathieuPiette
1567 3

Hi everyone,

I have a problem using multiple ZK features at once. I have made an example code that I hosted on Github so you can clearly see what I mean:

[Github dot com]/MathieuPiette/zkMethodTemplateRefFormBinding (insuficient Karma to post links, sorry)

In my ViewModel, I have a list of MainBean. Each MainBean has just a name (String) and a reference to an other bean (SubBean). These MainBeans are displayed in the first ListBox (on the left).

When I click one of them, it must be edited in the right part of the window. So I have a form binding to handle that. There's a Textbox for the name, and an Include for the SubBean. This SubBean is passed to the Include with @ref.

A SubBean contains a List of Strings and a Map of <String,SubSubBean>. Inside the included zul, in a template, I loop over all these Strings ("One", "Two", "Three", etc.) and, for each of them, I display the corresponding SubSubBean in the second column. To get the SubSubBean, I call a the method getSubSubBean(each).

Here is my problem: the getSubSubBean(each) method is called only once, so every line of the list references the same SubSubBean. Why isn't this method called every time?

I also have an other issue: the Save button's enabled/disabled state is binded to fxStatus.dirty. When I change the name of the SubBean in the Textbox, fxStatus is correctly updated. But if I change the value of any SubSubBean, fxStatus doesn't change. However, the other parts of form binding work: SubSubBean's value is not updated until I click Save, SubSubBean's value is correctly saved when I click Save.

Thank you!

delete flag offensive retag edit

Comments

Mathieu, you fill the map subsubbeans when you call the getter of the map. Does this need to be like that or can I call the entry of the map directly in the zul? (without the getSubSubBean(each) )

chillworld ( 2016-04-18 06:51:55 +0800 )edit

Yes, filling the map in getSubSubBean() is a requirement. In the real app, the map is partially filled but getSubSubBean() can never return null. It would be the case if I accessed directly to the map, and I don't want that logic (check null, instantiate and put in map if so) to be in the zul.

MathieuPiette ( 2016-04-18 09:43:37 +0800 )edit

Thank you chillworld for your answer, this was the right thing to do, though I still don't get why the function was called only once. Form status sadly doesn't work either.

MathieuPiette ( 2016-04-19 09:14:15 +0800 )edit

found a workaround.

chillworld ( 2016-04-19 10:11:36 +0800 )edit

Thanks Chillworld and cor3000. Tried to rename the getSubSubBean() method or annotate it as @Transient, but it didn't change anything. The helper class trick doesn't work because the SubBean is a proxy and the method is not found, how did you manage to run it?

MathieuPiette ( 2016-04-20 09:58:06 +0800 )edit

2 Answers

Sort by ยป oldest newest most voted
0

answered 2016-04-18 11:51:07 +0800

chillworld gravatar image chillworld flag of Belgium
5367 4 9
https://github.com/chillw...

updated 2016-04-20 05:31:57 +0800

I can give a work around for the getting each value of the subsubBean :

public SubBean() {
    strings = new ArrayList<String>() {
        private static final long serialVersionUID = 1L;

        {
            add("One");
            add("Two");
            add("Three");
            add("Four");
            add("Five");
            add("Six");
            add("Seven");
            add("Eight");
            add("Nine");
            add("Ten");
        }
    };
    subSubBeans = new HashMap<String, SubSubBean>() {
        @Override
        public SubSubBean get(Object key) {
            SubSubBean subSubBean = super.get(key);
            if (subSubBean == null) {
                subSubBean = new SubSubBean();
                put((String)key, subSubBean);
            }
            return subSubBean;
        }
    };
}

So actually, I'm overriding the get of the HashMap and put your initializing code there.

Like this you could use the EL expressions in the zul :

<textbox value="@bind(bean.subSubBeans[each].value)" hflex="1" />

For the formStatus, I inspected a little deeper.
First of all, I added a command with the onChange and passed the form status to the viewmodel.
Like this I could inspect if the form status was updated correctly or not.
I know it looks strange but take in mind that if the binder don't get notified, your status is also never updated.
The result is that the dirty status is correct. => so the binder isn't notified.

So a temp workaround :

@Command
public void change(@BindingParam("proxy")Form form) {
    System.out.println(form.getFormStatus().isDirty());
    BindUtils.postNotifyChange(null, null, form.getFormStatus(), ".");
}

and in the zul :

<textbox value="@bind(bean.subSubBeans[each].value)" onChange="@command('change', proxy = fx)" hflex="1" />

I hope this can help you, I know it's pitty you have to send the fx to your viewmodel.

Update:

Looks like I was a little to fast.
The first problem is resolved, but only when you mark the method getSubSubBeans @Immutable, otherwise you will never get in your get method.

Now the second problem can't(yet) be fixed when the @Immutable is on the getter.
If you go back to the previous with getSubSubBean(each) and remove the @Immutable => the solution will work perfect, but the first issue is back.

I'll be looking for a whole solution, maybe the best to fill the map directly with the possible values.

Solution :

Oke, after a good night sleep, I had new idea's to try.
First of all, I need to add some kind of warning :
You proxy the map so the get method is also proxied.
By this, it means that the get method actually will never trigger the real get method so putting the code in the Map is a bad idea, and will give problems later on.
Besides that, initializing a bean when the form object is already created => this means that your form object is directly dirty.
In our solution, we don't see it by the problem of notifying the dirty status on deeper objects.
But remember, when a fix for this will come => save button is enabled when you load the page(at least when 1 SubSubBean needed to be created).

Let's go further with the solution :
So somewhere I was thinking the problem could be that the binder see's bean.getSubSubBean(each) as always the same call, therefore calling it just once.
So I did go back to normal Map and moved the getSubSubBean method to the vm.

public SubSubBean getBean(SubBean bean, String key) {
    System.out.println("getting bean");
    SubSubBean subSubBean = bean.getSubSubBeans().get(key);
    if (subSubBean == null) {
        subSubBean = new SubSubBean();
        bean.getSubSubBeans().put((String) key, subSubBean);
    }
    return subSubBean;
}

Oke, this works in combination with the dirty status fix.
Now I'm thinking that this code doesn't belong in the VM, so I need to move it.
As you can see, it's a kind of helper method we created.
So next thing I did is making a helper class.

public final class HelperClass {
    public static Object getBean(SubBean bean, String key) {
        SubSubBean subSubBean = bean.getSubSubBeans().get(key);
        if (subSubBean == null) {
            subSubBean = new SubSubBean();
            bean.getSubSubBeans().put((String) key, subSubBean);
        }
        return subSubBean;
    }
}

Now, the code is a little more separated.
How do we use this in code, well we have 2 options.
Prior ZK 8 we needed a taglib to call this method from a zul.
So start with creating a tld file (WEB-INF/tld):

<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.1" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd">
    <tlib-version>1.0</tlib-version>
    <short-name>helper</short-name>
    <uri>/tld/helper</uri>
    <function>
        <name>getBean</name>
        <function-class>zk.method.template.ref.form.binding.HelperClass</function-class>
        <function-signature>
            java.lang.Object getBean(zk.method.template.ref.form.binding.SubBean bean, java.lang.String key)
        </function-signature>
        <description>
            Return the bean from a proxy object or create a new one.
        </description>
    </function>
</taglib>

and in the zul :

<?taglib uri="/WEB-INF/tld/helper.tld" prefix="help"?>
<textbox value="@bind(help:getBean(bean,each).value)" onChange="@command('change', proxy=fx)" hflex="1" />

Works like a charm.

With ZK8 we have now acces to static method's in the zul, so we don't need the TLD file but we can do this :

<?import class="zk.method.template.ref.form.binding.HelperClass" ?>
<textbox value="@bind(HelperClass.getBean(bean,each).value)" onChange="@command('change', proxy=fx)" hflex="1" />

Remember to set the import, that's really important.

So the both solutions works as you desired.
Hope this work around could fit in your design.

Greetz Chill.

link publish delete flag offensive edit

Comments

I could make the helper class work: with ZK8 the tld file is not mandatory. However, if you use it, ZK will use it to use the correct method. Otherwise it won't find it in the case of a proxy object.

MathieuPiette ( 2016-04-21 08:06:51 +0800 )edit

On my real application, one field of the SubSubBean is a ListModelList. In that case the form status update doesn't work. But with a normal list, it works!

MathieuPiette ( 2016-04-21 09:18:48 +0800 )edit
0

answered 2016-04-20 06:06:31 +0800

cor3000 gravatar image cor3000
6280 2 7

updated 2016-04-20 07:24:17 +0800

A helper class is not needed when giving the method a name that does not start with get... (or mark it @Transient) The formproxy will intercept methods starting with get/set/is (other methods call through to the original object) e.g.

public SubSubBean ensureSubSubBean(String key) {
    SubSubBean subSubBean = getSubSubBeans().get(key);
    if (subSubBean == null) {
        subSubBean = new SubSubBean();
        getSubSubBeans().put(key, subSubBean);
    }
    return subSubBean;
}

(as Chill pointed out correctly: access the map using the getSubSubBeans() method to modify the proxied map)


and in case tired of writing if(map.contains(...)) {map.put(.., new Bean...)} you can use map.computeIfAbsent(...) java 8 to streamline this a lot:

public SubSubBean ensureSubSubBean(String key) {
    return getSubSubBeans().computeIfAbsent(key, k -> new SubSubBean());
}

Isn't that a beauty ;)

link publish delete flag offensive edit

Comments

Thank you cor3000. I tried renaming the method or marking it @Transient, but that didn't work. The Java 8 tip is nice, but we are still in Java 7 (though we have started the migration).

MathieuPiette ( 2016-04-21 08:39:08 +0800 )edit

I did it and it worked... so maybe I don't have all the details you have. can you be more precise on "how it did not work?"

cor3000 ( 2016-04-21 09:02:54 +0800 )edit

The method is still called only once, regardless if it's @Transient or renamed (not starting with "get"). I changed other things in my code, I'm going to retry it now.

MathieuPiette ( 2016-04-21 09:23:30 +0800 )edit

Well, sorry, it works perfectly, I just renamed the function!

MathieuPiette ( 2016-04-21 09:28:29 +0800 )edit

thanks for the update !! I am glad to hear that it's working ... :)

cor3000 ( 2016-04-21 10:28:32 +0800 )edit
Your answer
Please start posting your answer anonymously - your answer will be saved within the current session and published after you log in or create a new account. Please try to give a substantial answer, for discussions, please use comments and please do remember to vote (after you log in)!

[hide preview]

Question tools

Follow
2 followers

RSS

Stats

Asked: 2016-04-14 08:14:55 +0800

Seen: 33 times

Last updated: Apr 20 '16

Support Options
  • Email Support
  • Training
  • Consulting
  • Outsourcing
Learn More