Global Command Binding

Overview

Global Command Binding is similar to command binding but the target becomes a global command. The local command can only be triggered by events of a ViewModel's Root View Component and its child components. The main difference from command binding is the event doesn't have to belong to the ViewModel's root view component or its child component. By default we can bind an event to any ViewModel's global command within the same desktop. When we trigger a global command, all matched global command in all ViewModels of the same scope will be executed.

MVVM Global Command Overview

Default Global Command

Since 6.5.1

Global command also supports @DefaultGlobalCommand to mark a method as a default global command method which is invoked only if a global command binding doesn't match any other global command methods. When a binder receives a global command, it starts to find ViewModel's command methods by matching its name. Only if the binder cannot find a matched method, it invokes default global command method.

Assume that there are only two global command methods in the below ViewModel. If we trigger a global command "exit", a binder invokes the default global command method defaultAction() because it cannot find a command method named "exit".

ViewModel with default command method

public class OrderVM {

    @GlobalCommand
    public void newOrder() {
        // ...
    }

    @DefaultGlobalCommand
    public void defaultAction() {
        // ...
    }
}

Usage

Notification

Assume we have 2 areas in a page, one is main area for adding items and another is list area for displaying items. Each area is bound to a ViewModel. Two ViewModels can access the same item list from single data source, for simplification we use a static list as data source. The main issue is how to notify another ViewModel to refresh item list after we click the "Add" button (add a new item). We can achieve it by binding "Add" button's onClick event to a global command of list area's ViewModel and in that global command we refresh the item list and notify related properties change to update View.

The interface looks like this:

MVVM Global Command Simple

A zul with 2 ViewModels

<hlayout>
    <vbox id="mainArea" width="200px" height="300px"
        style="border:dashed 2px" visible="@bind(vm.visible)"
        apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('org.zkoss.mvvm.examples.globalcommand.AddViewModel')"
        validationMessages="@id('vmsgs')">
        <label value="Main Panel" style="font-size:30px"/>

        Enter a Item (at least 3 characters):
        <textbox id="iBox"
            value="@load(vm.item) @save(vm.item, before='add') @validator(vm.itemValidator)"/>
        <label value="@load(vmsgs[iBox])" style="color:red" />
        <button label="Add" onClick="@command('add') @global-command('refresh')"/>
        <separator height="20px"/>
        <label value="@load(vm.msg)"/>
    </vbox>

    <vbox id="listArea" width="400px" height="300px"
        visible="@bind(vm.visible)" apply="org.zkoss.bind.BindComposer"
        style="border:solid 2px"
        viewModel="@id('vm') @init('org.zkoss.mvvm.examples.globalcommand.ListViewModel')">
        <listbox model="@load(vm.items)">
            <listhead>
                <listheader label="Items"/>
            </listhead>
            <template name="model">
                <listitem>
                    <listcell label="@load(each)"/>
                </listitem>
            </template>
        </listbox>
        <label value="@load(vm.lastUpdate)"/>
    </vbox>
</hlayout>
  • We bind onClick event to a local command "add" and a global command "refresh".

AddViewModel.java

public class MainViewModel {

    private String msg;

    @NotifyChange("msg")
    @Command
    public void add() {
        ItemList.add(item); //add an item
        msg = "Added " + item; //update message
    }
    //other code
}

ListViewModel.java

public class ListViewModel {

    private List<String> items;
    private Date lastUpdate;

    @NotifyChange({"items", "lastUpdate"})
    @GlobalCommand
    public void refresh() {
        items = ItemList.getList();
        lastUpdate = Calendar.getInstance().getTime();
    }
    //other code
}
  • Declare a global command and notify the change. (line 6-7)
  • The command method reloads items and set current time as last update time. (line 8-11)

A new item "hat" is added

MVVM Global Command Simple Add

In above example, we bind onClick event to local and global commands, the global command will always be executed after local command is executed. If the local command is not executed for validation failure, the global command will not be executed. We have a validator to validate item's name, its length can not be less than 3. When we enter a short item name to violate the validation rule, the local command is not executed. So, the global command is not executed, either. We can confirm this behaviour because the message in main area and last update in list area doesn't change.

Validation failure blocks command execution

MVVM Global Command Simple Fail

One to Many Communication

We can also communicate with multiple ViewModels at one event firing by a global command binding. If an event bound to a global command, "show", ZK willl execute all global commands with the name "show" in all ViewModels in the same desktop. Another difference from local command is that you can bind an event to a global command without the command existence. If the global command you bound doesn't exist, it has no response and doesn't throw any exception. The relationship between event and global command is like publisher-subscriber. Publisher publishes an event, only those ViewModels who subscribes it execute the corresponding global command.

Based on the previous example, assume that we want to hide and show 2 areas simultaneously with menuitems. We can bind a menuitem's onClick event to a global command named "show', and 2 ViewModels implements the "show" command to show itself. So does "hide" command.

The UI is:

MVVM Global Command Menu

One to many communication

<vlayout >
    <menubar width="600px" apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('org.zkoss.mvvm.examples.globalcommand.ControlViewModel')">
        <menuitem label="Show" onClick="@global-command('show')"/>
        <menuitem label="Hide" onClick="@global-command('hide')"/>
    </menubar>
    <!-- main area-->
    <!-- list area-->
</vlayout>
  • Add a menubar with 2 menuitem: show and hide that are bound to global commands.

ViewModel implement show & hide command

public class MainViewModel {

    private boolean visible = true;

    @NotifyChange("visible")
    @GlobalCommand
    public void show() {
        visible = true;
    }

    @NotifyChange("visible")
    @GlobalCommand
    public void hide() {
        visible = false;
    }
}
  • ListViewModel's implementation should have identical command methods.

When clicking hide menuitem, both ViewModel's global command "hide" will be executed thus 2 areas disappear.

Command Execution

Global command's execution is quite simple. It just executes a command method and then reloads those properties that specified in @NotifyChange annotation. But if you bind an event to a local and global command at the same time, binder always executes local command first. If any reason blocks the local command's execution, e.g. validation fails, it won't execute the global command.

MVVM Global Command Execution

Background Concept

When we apply a BindComposer to an component, it automatically creates a binder for us. As the ViewModel is a POJO, binder is like a broker to communicate with others. By default all binders subscribe to a default desktop-scoped event queue, thus this is a common communication mechanism among binders.

When we trigger a global command by an event, a binder posts an event to the queue it subscribes. All binders (including the binder that posts the event) that subscribe to the same queue in the same scope will receive this event and try to look up requested global command in their associated ViewModel. If a binder finds a matched global command, it will execute it. Otherwise, it ignores the event without throwing any exception.

MVVM Binder

In above image, when we trigger a global command "show" in View A, binder A posts an event to the event queue. Binder B which subscribes to the same event queue and binder A itself will both receive the event and try to look up for global command "show" in their associated ViewMoels (ViewModel A and ViewModel B)

ZK allows you to change the name and scope of the event queue a binder subscribes to. Please refer to Data Binding/Binder.

Trigger a Command Dynamically

Except triggering a global command in a ZUL, we can also do it by calling API. For above example, we can trigger global command in the local command "add" and the code is as follows:

@NotifyChange("msg")
@Command
public void add() {
    ItemList.add(item); //add an item
    msg = "Added " + item; //update message
    BindUtils.postGlobalCommand(null, null, "refresh", null);
}
  • The first parameter of BindUtils.postGlobalCommand() is queue name, and the second one is queue scope.

You can call this method in a composer, and those pages written in MVC pattern can communicate with ViewModel. A sample code snippet is as follows:

public class MyComposer extends SelectorComposer{

    //other codes

    @Listen("onClick=button#postx")
    public void postX() {
        Map<String,Object> args = new HashMap<String,Object>();
        args.put("data", "postX");
        BindUtils.postGlobalCommand("myqueue", EventQueues.DESKTOP, "cmdX", args);
    }
}