Статьи

ZK в действии: MVVM — совместная работа с клиентским API ZK

В предыдущих статьях мы реализовали следующие функциональные возможности с MVKM ZK:

Ключевым отличием реализации ZK MVVM от ZK MVC является то, что мы не обращаемся к компонентам пользовательского интерфейса и не манипулируем ими непосредственно в классе контроллера (ViewModel). В этом посте мы увидим, как мы можем делегировать некоторые манипуляции с пользовательским интерфейсом клиентскому коду, а также как передать параметры из View в ViewModel.

Задача

Создайте функцию обновления для нашей простой функции инвентаризации CRUD. Пользователи могут редактировать записи на месте в таблице, и им предоставляется выбор обновить или отменить внесенные изменения. Измененные записи выделены красным.

ZK Особенности в действии

  • ZK Клиентские API
  • ZK Style Class
  • MVVM: передать параметры из View в ViewModel

Реализация в шагах  

Включите редактирование на месте в списке, чтобы мы могли редактировать записи:

1
2
3
4
5
6
7
8
<listcell>
       <textbox inplace="true" value="@load(each.name)" ...</textbox>
   </listcell>
   ....
   <listcell>
       <doublebox inplace="true" value="@load(each.price)" ...</textbox>
   </listcell>
   ...
  • inplace = ”true” отображает элементы ввода, такие как Textbox, без границ, отображая их в виде простых меток; границы появляются, только если выбран элемент ввода
  • строки 2, 6, «каждый» относится к каждому объекту Item в сборе данных

После того, как запись отредактирована, мы хотим дать пользователям возможность обновить или отменить изменение.
Кнопки «Обновить» и «Отменить» должны быть видны только в том случае, если пользователь внес изменения в записи списка. Сначала мы определяем функции JavaScript для отображения и скрытия кнопок «Обновить» и «Отменить»:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<toolbar>
    ...
    <span id="edit_btns" visible="false" ...>
        <toolbarbutton label="Update" .../>
        <toolbarbutton label="Discard" .../>
    </span>
</toolbar>
 
    <script type="text/javascript">
        function hideEditBtns(){
     jq('$edit_btns').hide();
        }
   
        function showEditBtns(){
     jq('$edit_btns').show();
        }
 
    </script>
    ...
  • строка 2, мы обертываем Update и Discard и устанавливаем видимость в false
  • в строках 9, 13 мы определяем функции, которые скрывают и показывают кнопки « Обновить» и « Отменить»
  • в строке 11, 15 мы используем селектор jQuery jq (‘$ edit_btns’), чтобы получить виджет ZK, идентификатор которого равен «edit_btns»; обратите внимание, что шаблон селектора для идентификатора виджета ZK равен ‘$’, а не ‘#’

Когда записи в списке будут изменены, мы сделаем видимыми кнопки «Обновить / Отменить» и сделаем измененные значения красными. После нажатия кнопки «Обновить» или «Отменить» мы хотели бы снова скрыть кнопки

Поскольку это чисто пользовательский интерфейс, мы будем использовать клиентские API ZK:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<style>
   .inputs { font-weight: 600; }
   .modified { color: red; }
</style>
...
    <toolbar xmlns:w="client" >
    ...
    <span id="edit_btns" visible="false" ...>
         <toolbarbutton label="Update" w:onClick="hideEditBtns()" .../>
         <toolbarbutton label="Discard" w:onClick="hideEditBtns()" .../>
    </span>
    </toolbar>
 
    <script type="text/javascript">
        //show hide functions
 
        zk.afterMount(function(){
            jq('.inputs').change(function(){
            showEditBtns();
            $(this).addClass('modified');
     })
        });
    </script>
    ...
    <listcell>
       <doublebox inplace="true" sclass="inputs" value="@load(each.price)" ...</textbox>
   </listcell>
   ...
  • В строке 2 мы указываем класс стиля для наших элементов ввода (Textbox, Intbox, Doublebox, Datebox) и назначаем его атрибуту sclass элементов ввода, например. строка 26; sclass определяет класс стиля для виджетов ZK
  • В строке 18-20 мы получаем все входные элементы, сопоставляя их имя в классе и назначая обработчик события onChange. После изменения значения в элементе ввода кнопки «Обновить / Отменить» станут видимыми, а измененное значение будет выделено красным цветом.
  • строка 17, zk.afterMount запускается при создании виджетов ZK
  • В строке 6 мы указываем пространство имен клиента, чтобы мы могли зарегистрировать прослушиватели событий onClick на стороне клиента с синтаксисом «w: onClick». Обратите внимание, что мы все еще можем зарегистрировать наш обычный приемник событий onClick, который обрабатывается на сервере одновременно.
  • в строке 9, 10 мы назначаем клиентский приемник события onClick; функция hideEditBtns будет вызвана, чтобы сделать кнопки снова невидимыми

Определите метод для хранения измененных объектов Item в коллекции, чтобы изменения могли обновляться в пакетном режиме, если пользователь решит это сделать:

1
2
3
4
5
6
7
8
9
public class InventoryVM {
 
    private HashSet<Item> itemsToUpdate = new HashSet<item>();
    ...
 
    @Command
    public void addToUpdate(@BindingParam("entry") Item item){
        itemsToUpdate.add(item);
    }
  • В строке 6 мы аннотируем этот метод как командный метод, чтобы его можно было вызывать из View
  • строка 7, @BindingParam («entry») Элемент Item связывает произвольно названный параметр, называемый «entry»; мы ожидаем, что параметр будет иметь тип Item

Создайте метод для обновления изменений, внесенных в представлении, в модель данных.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class InventoryVM {
 
    private List<Item> items;
    private HashSet<Item> itemsToUpdate = new HashSet<item>();
    ...
 
    @NotifyChange("items")
    @Command
    public void updateItems() throws Exception{
        for (Item i : itemsToUpdate){
            i.setDatemod(new Date());
            DataService.getInstance().updateItem(i);
        }
        itemsToUpdate.clear();
        items = getItems();
    }

Когда внесены изменения в записи Listbox, вызовите метод addToUpdate и передайте ему отредактированный объект Item, который, в свою очередь, сохраняется в коллекции itemsToUpdate.

1
2
3
4
5
6
7
8
<listitem>
 <listcell>
  <doublebox value="@load(each.price)
                @save(each.name, before='updateItems')" 
                onChange="@command('addToUpdate',entry=each)" />
 </listcell>
 ...
</listitem>
  • @save (each.name, before = ‘updateItems’) гарантирует, что измененные значения не будут сохранены, пока не будет вызван updateItems (т. е. когда пользователь нажимает кнопку «Обновить»)

Наконец, когда пользователь нажимает кнопку «Обновить», мы вызываем метод updateItems для обновления изменений в модели данных. Если нажать Discard, мы вызываем getItems, чтобы обновить список без применения каких-либо изменений.

1
2
3
4
...
 <toolbarbutton label="Update" onClick="@command('updateItems')" .../>
 <toolbarbutton label="Discard" onClick="@command('getItems')" .../>
 ...

В двух словах

  • В соответствии с шаблоном MVVM мы стремимся сохранить код ViewModel независимым от любых компонентов View
  • Поскольку у нас нет прямой ссылки на компоненты пользовательского интерфейса в коде ViewModel, мы можем делегировать код манипуляции пользовательским интерфейсом (в нашем примере кода, показать / скрыть, изменение стиля) клиенту с помощью клиентских API ZK.
  • Мы можем использовать селекторы jQuery и API на стороне клиента ZK
  • Мы можем легко передавать параметры из View в ViewModel с помощью @BindingParam

Далее мы еще немного поговорим о ZK Styling, а затем рассмотрим валидаторы и конвертеры MVVM.  

ViewModel (ZK в действии [0] ~ [3]):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
public class InventoryVM {
 
 private List<Item> items;
 private Item newItem;
 private Item selected;
 private HashSet<Item> itemsToUpdate = new HashSet<Item>();
  
 public InventoryVM(){}
  
 //CREATE
 @NotifyChange("newItem")
 @Command
 public void createNewItem(){
  newItem = new Item("", "",0, 0,new Date());
 }
  
 @NotifyChange({"newItem","items"})
 @Command
 public void saveItem() throws Exception{
  DataService.getInstance().saveItem(newItem);
  newItem = null;
  items = getItems();
 }
   
 @NotifyChange("newItem")
 @Command
 public void cancelSave() throws Exception{
  newItem = null;
 }
  
 //READ
 @NotifyChange("items")
 @Command
 public List<Item> getItems() throws Exception{
  items = DataService.getInstance().getAllItems();
  for (Item j : items){
   System.out.println(j.getModel());
  }
  Clients.evalJavaScript("zk.afterMount(function(){jq('.inputs').removeClass('modified').change(function(){$(this).addClass('modified');showEditBtns();})});"); //how does afterMount work in this case?
  return items;
 }
  
 //UPDATE
 @NotifyChange("items")
 @Command
 public void updateItems() throws Exception{
  for (Item i : itemsToUpdate){
   i.setDatemod(new Date());
   DataService.getInstance().updateItem(i);
  }
  itemsToUpdate.clear();
  items = getItems();
 }
  
 @Command
 public void addToUpdate(@BindingParam("entry") Item item){
  itemsToUpdate.add(item);
 }
  
 //DELETE
 @Command
 public void deleteItem() throws Exception{
  if (selected != null){
   
   String str = "The item with name \""+selected.getName()+"\" and model \""+selected.getModel()+"\" will be deleted.";
   Messagebox.show(str,"Confirm Deletion", Messagebox.OK|Messagebox.CANCEL, Messagebox.QUESTION,
    new EventListener<Event>(){
     @Override
     public void onEvent(Event event) throws Exception {
      if (event.getName().equals("onOK")){
       DataService.getInstance().deleteItem(selected);
       items = getItems();
       BindUtils.postNotifyChange(null, null, InventoryVM.this, "items");
      }
     }
   });
    
  } else {
   Messagebox.show("No Item was Selected");
  }
 }
  
 
 public Item getNewItem() {
  return newItem;
 }
 
 public void setNewItem(Item newItem) {
  this.newItem = newItem;
 }
 
 public Item getselected() {
  return selected;
 }
 
 public void setselected(Item selected) {
  this.selected = selected;
 }
}

Вид (ZK в действии [0] ~ [3]):

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
<zk>
 <style>
  .z-toolbarbutton-cnt { font-size: 17px;} .edit-btns {border: 2px
  solid #7EAAC6; padding: 6px 4px 10px 4px; border-radius: 6px;}
  .inputs { font-weight: 600; } .modified { color: red; }
 </style>
 <script type="text/javascript">
  function hideEditBtns(){ jq('$edit_btns').hide(); }
 
  function showEditBtns(){ jq('$edit_btns').show(); }
 
  zk.afterMount(function(){ jq('.inputs').change(function(){
  $(this).addClass('modified'); showEditBtns(); }) });
 </script>
 <window apply="org.zkoss.bind.BindComposer"
  viewModel="@id('vm') @init('lab.sphota.zk.ctrl.InventoryVM')"
  xmlns:w="client">
  <toolbar width="100%">
   <toolbarbutton label="Add"
    onClick="@command('createNewItem')" />
   <toolbarbutton label="Delete"
    onClick="@command('deleteItem')"
    disabled="@load(empty vm.selected)" />
   <span id="edit_btns" sclass="edit-btns" visible="false">
    <toolbarbutton label="Update"
     onClick="@command('updateItems')" w:onClick="hideEditBtns()"/>
    <toolbarbutton label="Discard"
     onClick="@command('getItems')" w:onClick="hideEditBtns()" />
   </span>
  </toolbar>
  <groupbox mold="3d"
   form="@id('itm') @load(vm.newItem) @save(vm.newItem, before='saveItem')"
   visible="@load(not empty vm.newItem)">
   <caption label="New Item"></caption>
   <grid width="50%">
    <rows>
     <row>
      <label value="Item Name" width="100px"></label>
      <textbox value="@bind(itm.name)" />
     </row>
     <row>
      <label value="Model" width="100px"></label>
      <textbox value="@bind(itm.model)" />
     </row>
     <row>
      <label value="Unit Price" width="100px"></label>
      <decimalbox value="@bind(itm.price)"
       format="#,###.00" constraint="no empty, no negative" />
     </row>
     <row>
      <label value="Quantity" width="100px"></label>
      <spinner value="@bind(itm.qty)"
       constraint="no empty,min 0 max 999:
       Quantity Must be Greater Than Zero" />
     </row>
     <row>
      <cell colspan="2" align="center">
       <button width="80px" label="Save" mold="trendy"
        onClick="@command('saveItem')"  />
       <button width="80px" label="Cancel" mold="trendy"
        onClick="@command('cancelSave')" />
      </cell>
     </row>
    </rows>
   </grid>
  </groupbox>
  <listbox selectedItem="@bind(vm.selected)" model="@load(vm.items) ">
   <listhead>
    <listheader label="Name" sort="auto" hflex="2" />
    <listheader label="Model" sort="auto" hflex="1" />
    <listheader label="Quantity" sort="auto" hflex="1" />
    <listheader label="Unit Price" sort="auto" hflex="1" />
    <listheader label="Last Modified" sort="auto" hflex="2" />
   </listhead>
   <template name="model">
    <listitem>
     <listcell>
      <textbox inplace="true" width="110px" sclass="inputs"
       value="@load(each.name) @save(each.name, before='updateItems')"
       onChange="@command('addToUpdate',entry=each)">
      </textbox>
     </listcell>
     <listcell>
      <textbox inplace="true" width="110px" sclass="inputs"
       value="@load(each.model) @save(each.model, before='updateItems')"
       onChange="@command('addToUpdate',entry=each)" />
     </listcell>
     <listcell>
      <intbox inplace="true" sclass="inputs"
       value="@load(each.qty) @save(each.qty, before='updateItems')"
       onChange="@command('addToUpdate',entry=each)" />
     </listcell>
     <listcell>
      <doublebox inplace="true" sclass="inputs" format="###,###.00"
       value="@load(each.price) @save(each.price, before='updateItems')"
       onChange="@command('addToUpdate',entry=each)" />
     </listcell>
     <listcell label="@load(each.datemod)" />
    </listitem>
   </template>
  </listbox>
 </window>
</zk>

Ссылка: ZK в действии [3]: MVVM — Совместная работа с ZK Client API от нашего партнера JCG Лэнса Лу в блоге Tech Dojo