zkoss MVVM changes to model force grid to reload

ask-dev picture ask-dev · Dec 19, 2012 · Viewed 8.6k times · Source

I am using ZKOSS MVVM. So in View I am using a Listbox and it's bound (@load) to a list model object in ViewModel.

What I understand from documentation, if I change the model

1: Add an object to list model from View Model at index 0

I should see the latest object be appended at top of the Listbox.

2: Remove an item from model

I should see that particular row from Listbox be removed.

Note: It's an interface like social network e.g. Facebook wall when someone create a post and new post is appended to the posts list. If a post is deleted only that post is deleted from the list

Well, it does happen (new item gets appended/deleted item gets removed) but the whole Listbox reloads and not just that particular row which was added or removed.

Why is that? Why Listbox reloads fully on list model change.

Any idea?

Here are the code snippets (Use Case: Add new post is applicable. On creating new post whole Listbox reloads every time):

View

<z:div style="height: 100%; padding: 0px; margin: 0px;" apply="org.zkoss.bind.BindComposer"
    viewModel="@id('want_vm') @init('want.WantDesktopVM')">
<z:div zclass="content">
    <g:render template="../css/list/noEffectList"></g:render>
    <z:div hflex="1" width="100%" visible="@load(want_vm.toggleInput)" style="margin-bottom: 5px; padding: 5px">
        <z:vbox>
            <z:textbox id="postInput" multiline="true" value="" width="690px" height="50px"/>
            <z:div hflex="1" width="100%" style="text-align: right; padding-right: 5px">
                <z:button label="Post" zclass="button rect theme" onClick="@command('post', text=postInput.value)"/>
            </z:div>
        </z:vbox>           
    </z:div>
    <z:listbox model="@load(want_vm.posts)" emptyMessage="No new posts found." style="border:none;">
        <z:template name="model" var="iwant">
            <listitem style="margin-top: 10px"> 
                <listcell>
                    <hbox hflex="true">
                        <div zclass="dpFrame small">
                            <image height="50px" width="50px" content="@load(iwant.from) @converter('converter.UserActorDisplayPicConverter')" />
                        </div>
                        <vbox hflex="true" zclass="post"> 
                            <hbox hflex="true">
                                <label value="@load(iwant.from) @converter('converter.ActorDisplayNameConverter')" zclass="displayName"/>
                            </hbox>
                            <hbox hflex="true">
                                <label value="@load(iwant.textData)" zclass="post_data" multiline="true" maxlength="25"/>
                            </hbox>
                            <hbox>
                                <label value="@load(iwant.dateCreated) @converter('converter.SinceDateConverter')" zclass="since"/>
                            </hbox>
                        </vbox>
                    </hbox>
                </listcell> 
            </listitem>
        </z:template>
    </z:listbox>
</z:div>

ViewModel

class WantDesktopVM {
UserActorManagerService userActorManagerService
ActivityManagerService activityManagerService

UserActor me
UserActor profile

String error = null
String view = 'iwant'

@Wire
Textbox postInput

private List<Activity> posts = []

@Init
public void init(@ContextParam(ContextType.COMPONENT) Component component,
@ContextParam(ContextType.VIEW) Component view) {
    profile = Executions.current.getAttribute("profile")
    me = Executions.current.getAttribute("me")
    loadPosts()
}

@AfterCompose
public void afterCompose(@ContextParam(ContextType.VIEW) Component view) {
    Selectors.wireComponents(view, this, false);
}

public boolean isMyProfile() {
    return me.id == profile.id
} 

public UserActor getMe() {
    return this.me
}

public boolean isToggleInput() {
    return this.view == 'iwant' && isMyProfile()
}

public List<Activity> getPosts() {
    println "Getting posts ...${posts.size()}"
    return this.posts
}

private List<Activity> loadPosts() {
    if(view == 'iwant') {
        posts = Activity.createCriteria().list() {
            eq 'from', profile
            eq 'type', ACTIVITY_TYPE.WANT
            order("lastUpdated", "desc")
        }
    } else {
        posts = ActorActivitySpace.createCriteria().list() {
            projections {property("activity")}
            eq 'actor', profile
            activity {
                ne 'from', profile
                eq 'type', ACTIVITY_TYPE.WANT
            }
            order("lastUpdated", "desc")
        }
    }
    return posts
}

@NotifyChange(['posts', 'toggleInput'])
@Command
public void render(@BindingParam('view') String view) {
    println "Changing view ..."
    this.view = view
    loadPosts()
}

@NotifyChange('posts')
@Command
public void post(@BindingParam('text') String text) {
    println "Posting text: $text"
    postInput.setValue("")
    if(text) {
        Activity want = activityManagerService.want(me.id, text)
        println"Want ID : $want.id"
        posts.addAll(0, [want])
    }
}

}

Answer

Aaron Digulla picture Aaron Digulla · Dec 20, 2012

You use @NotifyChange('posts') to tell ZK that the whole list has changed. The grid doesn't try to examine the list, it simply replaces its current ListModel with the new list -> full reload.

If you don't want that, you will have to use the methods of the ListModel used by the grid to update the ui. That way, the grid will know exactly which rows have changed and only update those.

[EDIT] To achieve what you want, replace List<Activity> posts with ListModelList<Activity> posts = new ListModelList<Activity>()

When the activities change, you must update this list (i.e. call add() or addAll()) to update individual rows. You can no longer load everything from the database, you must merge changes in the database with the existing list.