Статьи

Первый взгляд на MVVM в ZK 6

MVVM против MVC

В предыдущем посте мы видели, как Ajax Framework ZK использует контроллер, вдохновленный селектором CSS, для подключения компонентов пользовательского интерфейса в View и прослушивания их событий. Согласно этому шаблону ZK MVC компоненты пользовательского интерфейса в представлении не должны быть привязаны к каким-либо методам контроллера или объектам данных. Гибкость использования шаблонов селектора в качестве средства отображения состояний и событий View на контроллер делает код более адаптивным к изменениям.

MVVM приближается к разделению концерна в обратном направлении. В соответствии с этим шаблоном в контроллере размещаются модель представления и механизм связывания. Связыватель отображает запросы от View к логике действия в View-Model и обновляет любое значение (данные) с обеих сторон, позволяя View-Model быть независимой от любого конкретного View.

Анатомия МВВМ в ЗК 6

Ниже приведена принципиальная схема шаблона MVKM ZK 6:

Вот некоторые дополнительные моменты, которые не показаны на диаграмме:
BindComposer:

  • реализует стандартные интерфейсы контроллера ZK (Composer & ComposerExt)
  • реализация по умолчанию достаточна, никаких изменений не требуется

Посмотреть:

  • сообщает связующему, какой метод вызывать и какие свойства обновлять в View-Model

Вид-модель:

  • просто POJO
  • связь со связующим осуществляется через аннотацию Java

MVVM в действии

Рассмотрим задачу отображения упрощенного инвентаря без знания точной разметки пользовательского интерфейса. Инвентарь — это набор предметов, поэтому у нас есть объектное представление такого:

1
2
3
4
5
6
7
8
public class Item {
 private String ID;
 private String name;
 private int quantity;
 private BigDecimal unitPrice;
 
        //getters & setters
}

Также имеет смысл ожидать, что элемент в списке может быть выбран и обработан. Таким образом, основываясь на наших знаниях и предположениях, мы можем пойти дальше и реализовать View-Model.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public class InventoryVM {
  
    ListModelList<Item> inventory;
    Item selectedItem;
   
    public ListModelList<Item> getInventory(){
        inventory = new ListModelList<Item>(InventoryDAO.getInventory());
        return inventory;
    }
 
    public Item getSelectedItem() {
        return selectedItem;
    }
  
    public void setSelectedItem(Item selectedItem) {
        this.selectedItem = selectedItem;
    }
 
}

Здесь у нас есть типичный POJO для реализации View-Model, данные с их геттерами и сеттерами.

Посмотреть реализацию, «Возьми один»

Теперь предположим, что позже мы узнали, что требования к представлению — это просто табличное отображение:

Возможная наценка для достижения пользовательского интерфейса, как указано выше:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
<window title='Inventory' border='normal' apply='org.zkoss.bind.BindComposer'
 viewModel='@id('vm') @init('lab.zkoss.mvvm.ctrl.InventoryVM')' >
 <listbox model='@load(vm.inventory)' width='600px' >
  <auxhead><auxheader label='Inventory Summary' colspan='5' align='center'/> </auxhead>
  <listhead>
   <listheader width='15%' label='Item ID' sort='auto(ID)'/>
   <listheader width='20%' label='Name' sort='auto(name)'/>
   <listheader width='20%' label='Quantity' sort='auto(quantity)'/>
   <listheader width='20%' label='Unit Price' sort='auto(unitPrice)'/>
   <listheader width='25%' label='Net Value'/>
  </listhead>
  <template name='model' var='item'>
   <listitem>
    <listcell><label value='@load(item.ID)'/></listcell>
    <listcell><label value='@load(item.name)'/></listcell>
    <listcell><label value='@load(item.quantity)'/></listcell>
    <listcell><label value='@load(item.unitPrice)'/></listcell>
    <listcell><label value='@load(item.unitPrice * item.quantity)'/></listcell>
   </listitem>
  </template>
 </listbox>
</window>

Давайте уточним немного о наценке здесь.

  • В строке 1 мы применяем BindComposer по умолчанию к компоненту Window, что делает все дочерние компоненты Window объектом эффекта BindComposer.
  • В следующей строке мы указываем BindComposer, какой класс View-Model создавать, и даем экземпляру View-Model идентификатор, чтобы мы могли ссылаться на него.
  • Так как мы загружаем коллекцию данных в Listbox, в строке 3 мы присваиваем свойству инвентаризации нашего экземпляра View-Model, который является коллекцией объектов Item, атрибут Listbox «model».
  • В строке 12 мы используем модель нашего компонента Template. Шаблон перебирает свои вложенные компоненты в соответствии с моделью, которую он получает. В этом случае у нас есть 5 элементов списка, которые составляют строку в списке.
  • В каждом Listcell мы загружаем свойства каждого объекта и отображаем их в метках.

Через систему привязки ZK мы смогли получить доступ к данным в нашем экземпляре View-Model и загрузить их в View с помощью аннотаций.

Посмотреть реализацию, возьми два

Предположим, позже в процессе разработки было решено, что текущий табличный дисплей занимает слишком много места в нашей презентации, и теперь нас просят показать детали элемента только тогда, когда элемент выбран в комбинированном списке, как показано ниже:

Хотя и представление, и поведение (детали отображаются только по выбору пользователя) отличаются от нашей предыдущей реализации, класс View-Model не нуждается в значительном изменении. Поскольку детали элемента будут отображаться только тогда, когда он выбран в Combobox, очевидно, что нам нужно обработать событие «onSelect», давайте добавим новый метод doSelect :

01
02
03
04
05
06
07
08
09
10
11
12
public class InventoryVM {
  
    ListModelList<Item> inventory;
    Item selectedItem;
 
    @NotifyChange('selectedItem')
    @Command
    public void doSelect(){ }
     
    //getters & setters
 
}

Метод, аннотированный @Command, позволяет нам вызываться из нашей разметки по его имени, в нашем случае:

1
<combobox onSelect='@command('doSelect')' >

Аннотация @NotifyChange (‘selectedItem’) позволяет автоматически обновлять свойство selectedItem всякий раз, когда пользователь выбирает новый элемент в комбинированном списке. Для наших целей не требуется никакой дополнительной реализации для метода doSelect. С этим небольшим изменением мы можем теперь видеть, как эта слегка измененная View-Model будет работать с нашей новой разметкой:

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
<window title='Inventory' border='normal' apply='org.zkoss.bind.BindComposer'
 viewModel='@id('vm') @init('lab.zkoss.mvvm.ctrl.InventoryVM')' width='600px'>
 ...
  <combobox model='@load(vm.inventory)'
     selectedItem='@bind(vm.selectedItem)'
      onSelect='@command('doSelect')' >
   <template name='model' var='item'>
    <comboitem label='@load(item.ID)'/>
   </template>
   <comboitem label='Test'/>
  </combobox>
  <listbox  visible='@load(not empty vm.selectedItem)' width='240px'>
   <listhead>
    <listheader ></listheader>
    <listheader ></listheader>
   </listhead>
   <listitem>
    <listcell>
     <label value='Item Name: ' />
    </listcell>
    <listcell>
     <label value='@load(vm.selectedItem.name)' />
    </listcell>
   </listitem>
   <listitem>
    <listcell>
     <label value='Unit Price: ' />
    </listcell>
    <listcell>
     <label value='@load(vm.selectedItem.unitPrice)' />
    </listcell>
   </listitem>
   <listitem>
    <listcell>
     <label value='Units in Stock: ' />
    </listcell>
    <listcell>
     <label value='@load(vm.selectedItem.quantity)' />
    </listcell>
   </listitem>
   <listitem>
    <listcell>
     <label value='Net Value: ' />
    </listcell>
    <listcell>
     <label value='@load(vm.selectedItem.unitPrice * vm.selectedItem.quantity)' />
    </listcell>
   </listitem>
  </listbox>
 ...
</window>
  • В строке 4 мы загружаем инвентарь сбора данных в атрибут модели Combobox, чтобы он мог итеративно отображать идентификатор каждого объекта Item в модели данных, используя компонент Template, объявленный в строке 7.
  • В строке 5 атрибут selectedItem указывает на последний выбранный элемент в этом списке объектов Item.
  • В строке 6 мы сопоставили событие onSelect с методом doSelect View-Model
  • В строке 12 мы делаем список, содержащий детали элемента видимым, только если свойство selectedItem в View-Model не пусто (selectedItem останется пустым, пока элемент не будет выбран в комбинированном списке).
  • Свойства selectedItem затем загружаются для заполнения списка.

резюмировать

В соответствии с шаблоном MVVM наш класс View-Model предоставляет свои данные и методы связывателю; нет ссылки на какой-либо конкретный компонент View. Реализации View осуществляют доступ к данным или вызывают обработчики событий через связыватель.

В этом посте мы знакомимся только с основами работы механизмов MVKM в ZK. Связыватель явно не ограничивается только загрузкой данных из View-Model. Помимо сохранения данных из View в ViewModel, мы также можем внедрить преобразователи и валидаторы данных в сочетание соединений View и View-Model. Шаблон MVVM также может работать в сочетании с моделью MVC. То есть мы можем также связывать компоненты и прослушивать fired-события через механизм селектора MVC, если мы хотим это сделать.

Мы углубимся в некоторые из этих тем позже.

Ссылка: первый взгляд на MVVM в ZK 6 от нашего партнера по JCG Лэнса Лу в блоге Tech Dojo .