Chapter 6: Implementing CRUD
Target Application
In this chapter, we are going to build an application with 4 basic operations, CRUD (Create, Read, Update, and Delete). The application's user interface looks like the images below:
Select an Item:
It is a personal todo list management system and it has following features:
- List all todo items
- Create a todo item.
- Finish a todo item.
- Click the checkbox in front of a todo item to mark it as finished and the item name will be decorated with line-through.
- Modify a todo item.
- Click an existing item and the detail editor appears. Then you can edit the item's details.
- Delete a todo item.
MVC Approach
If you have read previous chapters, constructing user interface for the example application should not be a big problem. Let's look at the layout first and ignore the details.
Layout in chapter6/todolist-mvc.zul
<?link rel="stylesheet" type="text/css" href="/style.css"?>
<window apply="org.zkoss.tutorial.chapter6.mvc.TodoListController"
border="normal" hflex="1" vflex="1" contentStyle="overflow:auto">
<caption src="/imgs/todo.png" sclass="fn-caption" label="Todo List (MVC)"/>
<borderlayout>
<center autoscroll="true" border="none">
<vlayout hflex="1" vflex="1">
<!-- todo creation function-->
<!-- todo list -->
</vlayout>
</center>
<east id="selectedTodoBlock" visible="false" width="300px" border="none" collapsible="false" splittable="true" minsize="300" autoscroll="true">
<vlayout >
<!-- detail editor -->
</vlayout>
</east>
</borderlayout>
</window>
- Line 5: We construct the user interface with Border Layout to separate layout into 2 areas.
- Line 6: The center area contains a todo creation function and a todo list.
- Line 12: The east area is a todo item editor which is invisible if no item selected.
Read
As we talked in previous chapters, we can use Template to define how to display a data model list with implicit variable each.
Display a To-do List
...
<listbox id="todoListbox" vflex="1">
<listhead>
<listheader width="30px" />
<listheader/>
<listheader hflex="min"/>
</listhead>
<template name="model">
<listitem sclass="${each.complete?'complete-todo':''}" value="${each}">
<listcell>
<checkbox forward="onCheck=todoListbox.onTodoCheck" checked="${each.complete}"/>
</listcell>
<listcell>
<label value="${each.subject}"/>
</listcell>
<listcell>
<button forward="onClick=todoListbox.onTodoDelete" image="/imgs/cross.png" width="36px"/>
</listcell>
</listitem>
</template>
</listbox>
...
- Line 8: The default value for required attribute name is "model".
- Line 9: We can implement simple presentation logic with EL expression. Here we apply different styles according to a flag each.complete. We also set a whole object in value attribute, and later we can get the object in the composer.
- Line 11: The each.complete is a boolean variable so that we can assign it to checked. By doing this, the Checkbox will be checked if the to-do item's compelete variable is true.
- Line 11, 17: The forward attribute is used to forward events to another component and we will talk about it in later sections.
In the composer, we should provide a data model for the Listbox.
public class TodoListController extends SelectorComposer<Component>{
//wire components
...
@Wire
Listbox todoListbox;
...
//services
TodoListService todoListService = new TodoListServiceChapter6Impl();
//data for the view
ListModelList<Todo> todoListModel;
ListModelList<Priority> priorityListModel;
Todo selectedTodo;
@Override
public void doAfterCompose(Component comp) throws Exception{
super.doAfterCompose(comp);
//get data from service and wrap it to list-model for the view
List<Todo> todoList = todoListService.getTodoList();
todoListModel = new ListModelList<Todo>(todoList);
todoListbox.setModel(todoListModel);
...
}
...
}
- Line 25 ~ 27: We initialize the data model in doAfterCompose(). Get data from the service class todoListService and create a ListModelList object. Then set it as the data model of todoListbox.
There is a priority radiogroup in to-do item detail editor appeared on the right hand side when you select an item.
In our application, its priority labels come from a enumeration Priority instead of a static text. We can still use Template to define how to create each Radio under a Radiogroup. The zul looks like as follows:
...
<grid hflex="1">
<columns>
<column align="right" hflex="min"/>
<column/>
</columns>
<rows>
<row>
<cell sclass="row-title">Priority :</cell>
<cell>
<radiogroup id="selectedTodoPriority">
<template name="model">
<radio label="${each.label}"/>
</template>
</radiogroup>
</cell>
</row>
...
- Line 12 ~14: Define how to create each Radio with Template and assign each.label to label attribute.
We also need to provide a data model for the Radiogroup in the composer:
public class TodoListController extends SelectorComposer<Component>{
//wire components
...
@Wire
Listbox todoListbox;
...
@Wire
Radiogroup selectedTodoPriority;
...
//services
TodoListService todoListService = new TodoListServiceChapter6Impl();
//data for the view
ListModelList<Todo> todoListModel;
ListModelList<Priority> priorityListModel;
Todo selectedTodo;
@Override
public void doAfterCompose(Component comp) throws Exception{
super.doAfterCompose(comp);
//get data from service and wrap it to list-model for the view
List<Todo> todoList = todoListService.getTodoList();
todoListModel = new ListModelList<Todo>(todoList);
todoListbox.setModel(todoListModel);
priorityListModel = new ListModelList<Priority>(Priority.values());
selectedTodoPriority.setModel(priorityListModel);
}
...
}
- Line 31, 32: Create a LisModelList with Priority and set it as a model of selectedTodoPriority.
Update
To update a to-do item, you should select an item first then detail editor will appear. The following codes demonstrate how to listen a "onSelect" event and display the item's detail.
public class TodoListController extends SelectorComposer<Component>{
//wire components
@Wire
Textbox todoSubject;
@Wire
Button addTodo;
@Wire
Listbox todoListbox;
@Wire
Component selectedTodoBlock;
@Wire
Checkbox selectedTodoCheck;
@Wire
Textbox selectedTodoSubject;
@Wire
Radiogroup selectedTodoPriority;
@Wire
Datebox selectedTodoDate;
@Wire
Textbox selectedTodoDescription;
@Wire
Button updateSelectedTodo;
//when user selects a todo of the listbox
@Listen("onSelect = #todoListbox")
public void doTodoSelect() {
if(todoListModel.isSelectionEmpty()){
//just in case for the no selection
selectedTodo = null;
}else{
selectedTodo = todoListModel.getSelection().iterator().next();
}
refreshDetailView();
}
private void refreshDetailView() {
//refresh the detail view of selected todo
if(selectedTodo==null){
//clean
selectedTodoBlock.setVisible(false);
selectedTodoCheck.setChecked(false);
selectedTodoSubject.setValue(null);
selectedTodoDate.setValue(null);
selectedTodoDescription.setValue(null);
updateSelectedTodo.setDisabled(true);
priorityListModel.clearSelection();
}else{
selectedTodoBlock.setVisible(true);
selectedTodoCheck.setChecked(selectedTodo.isComplete());
selectedTodoSubject.setValue(selectedTodo.getSubject());
selectedTodoDate.setValue(selectedTodo.getDate());
selectedTodoDescription.setValue(selectedTodo.getDescription());
updateSelectedTodo.setDisabled(false);
priorityListModel.addToSelection(selectedTodo.getPriority());
}
}
...
}
- Line 29: Use @Listen to listen onSelect event of the Listbox whose id is todoListbox.
- Line 30: This method checks todoListModel's selection and refreshes the detail editor.
- Line 35: Get user selection from data model by getSelection() which returns a Set
- Line 40: If an item is selected, it makes detail editor visible and pushes data into those input components of the editor by calling setter methods. If no item selected, it turns detail editor invisible and clear all input components' value.
- Line 60: Use addToSelection() to control a component's selection and it will automatically reflect to the corresponding widget's selection.
After modifying item's detail, you can click "Update" button to save the modification or "Reload" to revert back original data. The following codes demonstrate how to implement these functions:
Handle clicking "update" and "reload" button
//when user clicks the update button
@Listen("onClick = #updateSelectedTodo")
public void doUpdateClick(){
if(Strings.isBlank(selectedTodoSubject.getValue())){
Clients.showNotification("Nothing to do ?",selectedTodoSubject);
return;
}
selectedTodo.setComplete(selectedTodoCheck.isChecked());
selectedTodo.setSubject(selectedTodoSubject.getValue());
selectedTodo.setDate(selectedTodoDate.getValue());
selectedTodo.setDescription(selectedTodoDescription.getValue());
selectedTodo.setPriority(priorityListModel.getSelection().iterator().next());
//save data
selectedTodo = todoListService.updateTodo(selectedTodo);
//refresh listmodel for only 1 item
todoListModel.set(todoListModel.indexOf(selectedTodo), selectedTodo);
//show message for user
Clients.showNotification("Todo saved");
}
//when user clicks the update button
@Listen("onClick = #reloadSelectedTodo")
public void doReloadClick(){
refreshDetailView();
}
Complete a Todo
Click a Checkbox in front of a todo item means to finish it. To implement this feature, the first problem is: how do we know which Checkbox is checked for there are many of them. Although we can use @Listen("onCheck = #todoListbox checkbox") to listen all CheckEvent under todo listbox, here we introduce "Event Forwarding" feature to demonstrate ZK's flexibility. This feature can forward an event from a component to another component, so we can forward all onCheck events from Checkbox to the Listbox that encloses them, then we can just listen Listbox's events instead of all events of Checkbox.
extracted from chapter6/todolist-mvc.zul
...
<listbox id="todoListbox" vflex="1">
...
<template name="model">
<listitem sclass="${each.complete?'complete-todo':''}" value="${each}">
<listcell>
<checkbox forward="onCheck=todoListbox.onTodoCheck" checked="${each.complete}"/>
</listcell>
<listcell>
<label value="${each.subject}"/>
</listcell>
<listcell>
<button forward="onClick=todoListbox.onTodoDelete" image="/imgs/cross.png" width="36px"/>
</listcell>
</listitem>
</template>
- Line 7: Forward Checkbox's onCheck to an event onTodoCheck of a Listbox whose id is todoListbox. The onTodoCheck is a customized forward event name, and you can use whatever name you want. Then we can use @Listen to listen this special event name.
Next, we listen the customized event onTodoCheck and mark the todo as finished.
public class TodoListController extends SelectorComposer<Component>{
...
//when user checks on the checkbox of each todo on the list
@Listen("onTodoCheck = #todoListbox")
public void doTodoCheck(ForwardEvent evt){
//get data from event
Checkbox cbox = (Checkbox)evt.getOrigin().getTarget();
Listitem litem = (Listitem)cbox.getParent().getParent();
boolean checked = cbox.isChecked();
Todo todo = (Todo)litem.getValue();
todo.setComplete(checked);
//save data
todo = todoListService.updateTodo(todo);
if(todo.equals(selectedTodo)){
//refresh detail view
refreshDetailView();
}
//update listitem style
((Listitem)cbox.getParent().getParent()).setSclass(checked?"complete-todo":"");
}
...
}
- Line 5: Listen the customized event name onTodoCheck of a Listbox todoListbox for we already forward onCheck to the Listbox in the zul.
- Line 6: An event listener method can have a argument for event, but which type depends on which event you listen. As the customized event is forwarded from another component, the argument should be ForwardEvent. This method set the Todo object of the selected item as complete and decorate Listitem with line-through by changing its sclass.
- Line 8: You should call getOrigin() to get the original event that is forwarded. Every event object has a method getTarget() that allows you get the target component that receives the event.
- Line 9: Navigate component tree by getParent() or getChildren.
- Line 12: Here we get Todo object of the selected todo item from value attribute that we assigned in the zul.
Create
After typing todo item name, we can create it by either clicking the button with plus icon or pressing "Enter" key. Therefore, we have to listen 2 events: onClick and onOK. About handling other key pressing event, please refer to ZK_Developer's_Reference/UI_Patterns/Keystroke_Handling.
public class TodoListController extends SelectorComposer<Component>{
//wire components
@Wire
Textbox todoSubject;
@Wire
Button addTodo;
@Wire
Listbox todoListbox;
@Wire
Component selectedTodoBlock;
@Wire
Checkbox selectedTodoCheck;
@Wire
Textbox selectedTodoSubject;
@Wire
Radiogroup selectedTodoPriority;
@Wire
Datebox selectedTodoDate;
@Wire
Textbox selectedTodoDescription;
@Wire
Button updateSelectedTodo;
//services
TodoListService todoListService = new TodoListServiceChapter6Impl();
//data for the view
ListModelList<Todo> todoListModel;
ListModelList<Priority> priorityListModel;
Todo selectedTodo;
...
//when user clicks on the button or enters on the textbox
@Listen("onClick = #addTodo; onOK = #todoSubject")
public void doTodoAdd(){
//get user input from view
String subject = todoSubject.getValue();
if(Strings.isBlank(subject)){
Clients.showNotification("Nothing to do ?",todoSubject);
}else{
//save data
selectedTodo = todoListService.saveTodo(new Todo(subject));
//update the model of listbox
todoListModel.add(selectedTodo);
//set the new selection
todoListModel.addToSelection(selectedTodo);
//refresh detail view
refreshDetailView();
//reset value for fast typing.
todoSubject.setValue("");
}
}
...
}
- Line 37: Listen the button's onClick event and "Enter" key pressing event: onOK.
- Line 38: This method add a todo item with a service object, update the data model of Listbox, change the selection to newly created one, then reset the input field of the Textbox.
- Line 40: Get user input in the Textbox todoSubject by getValue().
- Line 42: Show a notification at the right side of the Textbox todoSubject.
- Line 47: When you add a new item in a ListModelList object, it will automatically reflect to Listbox's rendering.
Delete
Implement deletion feature is similar to completing a todo item. We also forward each delete button's () onClick to the Listbox that encloses those buttons.
Forward delete button's onClick
<listbox id="todoListbox" vflex="1">
...
<template name="model">
<listitem sclass="${each.complete?'complete-todo':''}" value="${each}">
<listcell>
<checkbox forward="onCheck=todoListbox.onTodoCheck" checked="${each.complete}"/>
</listcell>
<listcell>
<label value="${each.subject}"/>
</listcell>
<listcell>
<button forward="onClick=todoListbox.onTodoDelete" image="/imgs/cross.png" width="36px"/>
</listcell>
</listitem>
</template>
...
- Line 12: Forward delete button's onClick to the Listbox's with custom forward event name onTodoDelete.
Then we can listen the forwarded event to perform deletion.
//when user clicks the delete button of each todo on the list
@Listen("onTodoDelete = #todoListbox")
public void doTodoDelete(ForwardEvent evt){
Button btn = (Button)evt.getOrigin().getTarget();
Listitem litem = (Listitem)btn.getParent().getParent();
Todo todo = (Todo)litem.getValue();
//delete data
todoListService.deleteTodo(todo);
//update the model of listbox
todoListModel.remove(todo);
if(todo.equals(selectedTodo)){
//refresh selected todo view
selectedTodo = null;
refreshDetailView();
}
}
MVVM Approach
Building user interface for example application in MVVM approach is very similar to building in MVC approach, but you don't have to give id to components since we don't need to identify components for wiring. For defining the ViewModel's properties, we should analyse what data required to display on the user interface or to be kept as a View's state. There are 3 kinds of data, todo item's subject for creating a new todo item, todo item list for displaying all todo, and selected todo item for keeping user selection.
public class TodoListViewModel implements Serializable{
//data for the view
String subject;
ListModelList<Todo> todoListModel;
Todo selectedTodo;
...
}
Read
As we have talked in previous chapter, display a collection of data requires to prepare a data model in the ViewModel.
public class TodoListViewModel implements Serializable{
//services
TodoListService todoListService = new TodoListServiceChapter6Impl();
//data for the view
String subject;
ListModelList<Todo> todoListModel;
Todo selectedTodo;
@Init // @Init annotates a initial method
public void init(){
//get data from service and wrap it to model for the view
List<Todo> todoList = todoListService.getTodoList();
//you can use List directly, however use ListModelList provide efficient control in MVVM
todoListModel = new ListModelList<Todo>(todoList);
}
...
}
- Line 17: Initialize ListModelList with todoList retrieved with a service class.
Then we can bind Listbox's model to prepared data model with data binding expression.
<listbox model="@bind(vm.todoListModel)" selectedItem="@bind(vm.selectedTodo)" vflex="1" >
<listhead>
<listheader width="30px" />
<listheader/>
<listheader hflex="min"/>
</listhead>
<template name="model">
<listitem sclass="@bind(each.complete?'complete-todo':'')">
<listcell>
<checkbox checked="@bind(each.complete)" onCheck="@command('completeTodo',todo=each)"/>
</listcell>
<listcell>
<label value="@bind(each.subject)"/>
</listcell>
<listcell>
<button onClick="@command('deleteTodo',todo=each)" image="/imgs/cross.png" width="36px"/>
</listcell>
</listitem>
</template>
</listbox>
- Line 1: Set Listbox's data model by binding model attribute to a property of type ListModelList. Binding seleccteditem to vm.selectedTodo to keep selection state.
- Line 8: You can fill any valid EL expression In a data binding annotation, so that you can implement simple presentation logic with EL. Here we set sclass according to a Todo object's complete property.
- Line 10,13: Use implicit variable each to access each Todo object in the data model.
Prepare the data model for Radiogroup to display Priority enumeration.
public List<Priority> getPriorityList(){
return Arrays.asList(Priority.values());
}
Then binding model to the property.
<radiogroup model="@bind(vm.priorityList)" selectedItem="@bind(fx.priority)">
<template name="model">
<radio label="@bind(each.label)"/>
</template>
</radiogroup>
Update
How do we achieve the feature that selecting a todo item then detail editor becomes visible under MVVM? Simple, just determine editor's visibility upon selected todo item is null or not.
<east visible="@bind(not empty vm.selectedTodo)" width="300px"
border="none" collapsible="false" splittable="true" minsize="300" autoscroll="true">
<!-- todo item detail editor-->
</east>
- Line 1: ZK bind monitors all binding properties. If one property changes, ZK bind re-evaluates those binding expressions that bind to the changed property.
In order to make selected todo item's properties display in detail editor, we just binding input components in detail editor to corresponding selectedTodo's properties.
Binding input components to selected item's properties
<vlayout
form="@id('fx') @load(vm.selectedTodo) @save(vm.selectedTodo, before='updateTodo') @validator(vm.todoValidator)">
<hbox align="center" hflex="1">
<checkbox checked="@bind(fx.complete)"/>
<textbox value="@bind(fx.subject)" hflex="1" />
</hbox>
<grid hflex="1">
<columns>
<column align="right" hflex="min"/>
<column/>
</columns>
<rows>
<row>
<cell sclass="row-title">Priority :</cell>
<cell>
<radiogroup model="@bind(vm.priorityList)" selectedItem="@bind(fx.priority)">
<template name="model">
<radio label="@bind(each.label)"/>
</template>
</radiogroup>
</cell>
</row>
<row>
<cell sclass="row-title">Date :</cell>
<cell><datebox value="@bind(fx.date)" width="200px"/></cell>
</row>
<row>
<cell sclass="row-title">Description :</cell>
<cell>
<textbox value="@bind(fx.description)" multiline="true" hflex="1" height="200px" />
</cell>
</row>
</rows>
</grid>
..
</vlayout>
- Line 4: Here we create a form binding with id fx that contains properties of vm.selectedTodo. We will talk about @validator() in later section.
After modifying item's detail, you can click "Update" button to save the modification or "Reload" to revert back original data. These two functions are implemented in command methods:
@Command
@NotifyChange("selectedTodo")
public void updateTodo(){
//update data
selectedTodo = todoListService.updateTodo(selectedTodo);
//update the model, by using ListModelList, you don't need to notify todoListModel change
//by reseting an item , it make listbox only refresh one item
todoListModel.set(todoListModel.indexOf(selectedTodo), selectedTodo);
}
//when user clicks the update button
@Command @NotifyChange("selectedTodo")
public void reloadTodo(){
//do nothing, the selectedTodo will reload by notify change
}
- Line 9: ListModelList can reflect its change to the client, you don't have to notify change of todoListModel.
then we can invoke them by command binding:
<hlayout>
<button onClick="@command('updateTodo')" label="Update"/>
<button onClick="@command('reloadTodo')" label="Reload"/>
</hlayout>
Input Validation
ZK provides a validator to help developers perform user input validation. Validator is a reusable element that performs validation. If you bind a component's attribute to a validator, binder will use it to validate attribute's value automatically before saving to a ViewModel or to a middle object. Here we implement a validator to avoid empty value of todo's subject.
Define a validator in the ViewModel
//the validator is the class to validate data before set ui data back to todo
public Validator getTodoValidator(){
return new AbstractValidator() {
public void validate(ValidationContext ctx) {
//get the form that will be applied to todo
Form fx = (Form)ctx.getProperty().getValue();
//get filed subject of the form
String subject = (String)fx.getField("subject");
if(Strings.isBlank(subject)){
Clients.showNotification("Subject is blank, nothing to do ?");
//mark the validation is invalid, so the data will not update to bean
//and the further command will be skipped.
ctx.setInvalid();
}
}
};
}
- Line 2: Return a validator object by getter method make it as a ViewModel's property, so we can bind it to an attribute.
- Line 3: In most case, we can create a validator by extending AbstractValidator and override validate() instead of creating from scratch.
- Line 7: Get user input from ValidationContext. In our example, because we will apply this validator to form binding, we expect ctx.getProperty().getValue() returns a Form object.
- Line 9: You can get every field that middle object contains with a property name.
- Line 15: Call set Invalid() to fail the validation then further command execution will be skipped.
Then apply this validator with data binding expression.
<vlayout
form="@id('fx') @load(vm.selectedTodo) @save(vm.selectedTodo, before='updateTodo') @validator(vm.todoValidator)">
Hence, if vm.todoValidator fails validation, ZK won't execute updateTodo command. Then binder won't save value to selectedTodo.
Complete a Todo
We want clicking a Checkbox in front of each todo item can trigger
<template name="model">
<listitem sclass="@bind(each.complete?'complete-todo':'')">
<listcell>
<checkbox checked="@bind(each.complete)" onCheck="@command('completeTodo',todo=each)"/>
</listcell>
...
</listitem>
</template>
- Line 4: Command binding allows you to pass an arguments in key-value pairs. We pass each object with key todo.
Create
We can create new todo item by either clicking the button with plus icon () or pressing "Enter" key, therefore we can these two events to the same command method that adds a todo item.
Command method addTodo
@Command //@Command annotates a command method
@NotifyChange({"selectedTodo","subject"}) //@NotifyChange annotates data changed notification after calling this method
public void addTodo(){
if(Strings.isBlank(subject)){
Clients.showNotification("Subject is blank, nothing to do ?");
}else{
//save data
selectedTodo = todoListService.saveTodo(new Todo(subject));
//update the model, by using ListModelList, you don't need to notify todoListModel change
//it is efficient that only update one item of the listbox
todoListModel.add(selectedTodo);
todoListModel.addToSelection(selectedTodo);
//reset value for fast typing.
subject = null;
}
}
- Line 2: You can notify multiple properties change by filling an array of String. Here we specify {"selectedTodo","subject"}, since we change them in the method.
Binding to addTodo
<hbox align="center" hflex="1" >
<textbox value="@bind(vm.subject)" onOK="@command('addTodo')" hflex="1" placeholder="What needs to be done?" />
<button onClick="@command('addTodo')" image="/imgs/plus.png" width="36px"/>
</hbox>
- Line 2: Pressing "Enter" key triggers onOK
Delete
@Command
//@NotifyChange("selectedTodo") //use postnotifyChange to notify dynamically
public void deleteTodo(@BindingParam("todo") Todo todo){
//save data
todoListService.deleteTodo(todo);
//update the model, by using ListModelList, you don't need to notify todoListModel change
todoListModel.remove(todo);
if(todo.equals(selectedTodo)){
//refresh selected todo view
selectedTodo = null;
//for the case that notification is decided dynamically
BindUtils.postNotifyChange(null, null, this, "selectedTodo");
}
}
- Line 3 :
- Line 14:
<template name="model">
<listitem sclass="@bind(each.complete?'complete-todo':'')">
<listcell>
<checkbox checked="@bind(each.complete)" onCheck="@command('completeTodo',todo=each)"/>
</listcell>
<listcell>
<label value="@bind(each.subject)"/>
</listcell>
<listcell>
<button onClick="@command('deleteTodo',todo=each)" image="/imgs/cross.png" width="36px"/>
</listcell>
</listitem>
</template>
- Line 10: In order to know which Todo object we should delete, we pass the deleting Todo by todo=each. The todo is key and each is value.