Статьи

Тест вождения MVVM Pattern с ZK Ajax

Вступление

Эта статья является последней в серии, демонстрирующей альтернативные шаблоны проектирования GUI с использованием инфраструктуры ZK RIA AJAX. Две предыдущие статьи использовали шаблоны Model-View-Presenter и Model-View-Controller в контексте реализации простого экрана «напоминания задач». В этой статье демонстрируется шаблон Model-View-ViewModel для реализации того же простого экрана.

Пример кода для этой статьи — проект ZkToDo2, размещенный в SourceForge в рамках проекта ZKForge. Код построен с помощью Maven и предназначен для самостоятельной установки. Код даже запускается во встроенном контейнере сервлетов, который Maven будет загружать и запускать. Все, что вам нужно, это либо командная строка
Mavenсобрать систему или подключаемый модуль Maven для выбранной вами IDE. Самый простой способ начать работу с кодом — это создать и запустить приложение из командной строки, используя только стандартный дистрибутив Maven, используя приведенные здесь инструкции
. В качестве альтернативы шаги по извлечению, сборке, запуску и отладке приложения с использованием Eclipse с подключаемым модулем m2eclipse Maven Eclipse находятся
здесь .

Почему UI Design Patterns?

При построении системы удобно обсуждать требования с точки зрения экранов. Все же экраны системы, вероятно, изменятся полностью, чтобы жить и далее. Это происходит потому, что презентация не связана с тем, что логически делает пользователь системы; известен как вариант использования или история пользователя (например, пользователь покупает книгу). Хотя функции развиваются медленно с течением времени, экраны могут быстро меняться между проектными итерациями. Отделение логики бизнес-приложения от логики обновления экрана делает гибкий код.

Организация кода бизнес-логики пользовательского интерфейса в отдельный слой без зависимости времени компиляции от кода инфраструктуры пользовательского интерфейса позволяет более легко охватить тестирование JUnit. Отделение задач «Бизнес-логики» от «Логики рендеринга» облегчает как расширение функциональности приложения, так и переупорядочивание экранов. Напротив, размещение логики Use Case приложения непосредственно внутри классов визуализации экрана создает более жесткую и хрупкую кодовую базу.

Модель Model-View-ViewModel

Основная концепция GUI Design Patterns — это «отдельная презентация». Раздел ссылок ниже содержит ссылку на превосходное обсуждение этой темы Мартином Фаулером. Многие шаблоны проектирования имеют как логический уровень просмотра, так и модель. Хорошо известный шаблон MVC представляет уровень контроллера, который служит посредником между моделью и представлением. Шаблон Model-View-ViewModel заменяет концепцию контроллера на ViewModel. ViewModel — это POJO-модель логики приложения; это голая бизнес-логика пользовательского интерфейса без экрана. Поверх голой логики мы связываем один или несколько активных экранов. Ключом к этому подходу является интерфейс Binder. В этой статье будет описано, как технология ZK Binder анимирует View для визуализации измененного состояния ViewModel.Ключевая роль инфраструктуры пользовательского интерфейса Binder дает альтернативное имя для этого шаблона: Model-View-Binder (MVB).

Диаграмма выше показывает логическое разделение шаблона MVVM. Слой View — это рабочий стол ZK и поддерживающий его Binder, который будет описан ниже. Команды запускаются из View в ViewModel в ответ на действия пользователя. Изменяемые данные и состояние ViewModel затем загружаются обратно в View, чтобы показать влияние команд.

DomainModel под ViewModel состоит из сервисов приложений, объектов JPA и логики доступа к связанным данным типичного бизнес-уровня приложения. Книга Криса Ричардсона «POJO в действии» дает отличное введение в использование управляемой тестами разработки для создания многоуровневой модели домена. Образец приложения использует Spring и Hibernate JPA для своего внутреннего многоуровневого размещения и шаблона Detached Entity (без DTO).

Пример экрана

Версия MVVM экрана «напоминания о задачах » определяется в одном файле
zktodo_c.zul . Вот скриншот:

На экране отображается коллекция объектов Reminder, которые являются сущностями JPA, хранящимися в базе данных:

@Entity @Table(name = "REMINDER"
public class Reminder {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "REMINDER_ID")
    private Long id;
 
    @Column(name = "NAME")
    private String name = "";
 
    @Column(name = "PRIORITY")
    private int priority = 0;
 
    @Column(name = "DATE")
    private Date date = new Date(System.currentTimeMillis());
 
    ... // setters and getters, equals and hashcode, constructors
}

Синтаксис файла ZUL — это диалект XUL:

<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit" ?>
<?init class="org.zkforge.zktodo2.binding.CommandBinderInit"?>
<?variable-resolver class="org.zkoss.zkplus.spring.DelegatingVariableResolver" ?>
<zk xmlns="http://www.zkoss.org/2005/zul">
<window>
<listbox id="list" multiple="true" rows="12"
        model="@{toDoViewModel.reminders, load-after='add.onClick,update.onClick,delete.onClick'}"
        selectedItem="@{toDoViewModel.selectedReminder}">
    <listhead>
        <listheader label="Item" />
        <listheader label="Priority" width="80px" />
        <listheader label="Opened" width="90px" />
    </listhead>
    <listitem self="@{each=reminder}">
        <listcell label="@{reminder.name}"/>
        <listcell label="@{reminder.priority}"/>
        <listcell label="@{reminder.date, converter='org.zkforge.zktodo2.DateFormatConverter'}"/>
    </listitem>
</listbox>
<vbox>
    <hbox>
     Item:<textbox cols="40" constraint="no empty"
        value="@{Model.selectedReminder.name, load-after='add.onClick,delete.onClick'}"/>
     Priority:<intbox id="priority" cols="1" constraint="no empty"
        value="@{toDoViewModel.selectedReminder.priority, load-after='add.onClick,delete.onClick'}"
        />
     Date:<datebox id="date" cols="14" constraint="no empty"
        value="@{Model.selectedReminder.date, load-after='add.onClick,delete.onClick'}"
        onChange="@{Model, converter='org.zkforge.zktodo2.binding.InputEventCommandConverter'}"/>
    </hbox>
    <hbox id="buttons">
         <button id="add" label="Add" width="36px" height="24px" onClick="@{toDoViewModel}"/>
         <button id="update" label="Update" width="46px" height="24px" onClick="@{Model}"/>
         <button id="delete" label="Delete" width="46px" height="24px" onClick="@{toDoViewModel}"/>
    </hbox>
</vbox>
</window>
</zk>

Платформа ZK основана на технологии Java Servlet. Страница ZUL анализируется для создания рабочего стола компонентов Java на сервере. Затем это отображается как DHTML для браузера. Взаимодействие пользователей с DHTML запускается как AJAX-события обратно на сервер. Изменения на рабочем столе, сделанные с помощью кода обработчика событий Java AJAX, автоматически отображаются в браузере. Таким образом, богатое интернет-приложение может быть запрограммировано напрямую с помощью логики Java, работающей на сервере.

В верхней части страницы к экрану применяются инициаторы страниц AnnotateDataBinder и CommandBinder:

<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit" ?>
<?init class="org.zkforge.zktodo2.binding.CommandBinderInit"?>

Эти связующие читают синтаксис декларативных привязок данных «@ {binding}» на странице и связывают аннотированные компоненты ZK с нашим кодом Java. Целью привязок является переменная «toDoViewModel». Поскольку на странице нет компонента с этим идентификатором, переменная будет преобразована в bean-компонент Spring с этим именем. Это было достигнуто путем включения пружинного преобразователя переменных в верхней части страницы:

<?variable-resolver class="org.zkoss.zkplus.spring.DelegatingVariableResolver" ?>

В конфигурационном файле spring spring-context.xml bean-компонент toDoViewModel и его зависимости определены следующим образом:

<bean id="toDoViewModel" class="org.zkforge.zktodo2.ZkToDoViewModel"
    p:reminderService-ref="reminderService" scope="desktop" />
 
<bean id="reminderService" class="org.zkforge.zktodo2.ReminderService"
    p:basicDao-ref="basicDao" />
 
<bean id="basicDao" class="org.zkforge.zktodo2.BasicDao" />

Ключевой характер bean-компонента toDoViewModel заключается в том, что он имеет область действия «рабочий стол». Область рабочего стола — это настраиваемая область Spring, определяемая библиотекой интеграции ZKSpring. Это приведет к тому, что Spring сохранит один и только один экземпляр компонента, связанный с рабочим столом ZK пользователей, определенным страницей ZUL. Модельный компонент снабжен сервисным компонентом «RemderService». Служебный компонент предоставляется с компонентом доступа к данным «basicDao». Служебный компонент и компонент доступа к данным имеют область действия по умолчанию, поэтому они будут одноэлементными объектами. Подобная настройка областей действия позволяет компоненту модели быть компонентом с состоянием, обслуживающим рабочий стол ZK во многих взаимодействиях AJAX.

Привязки

В значениях свойств файла ZUL в формате «@ {binding}» определяется, как данные значения, состояние отображения (например, видимое / отключенное) и методы команд, предоставляемые моделью представления, связаны с представлением. Вид — это комбинированный рабочий стол компонентов экрана, которые описываются файлом ZUL. Список, который определяет список элементов, имеет привязку к своим свойствам «модель» и «выбранный элемент»:

<listbox
    model="@{toDoViewModel.reminders, load-after='add.onClick,update.onClick,delete.onClick'}"
    selectedItem="@{toDoViewModel.selectedReminder}">

Свойство «модель» списка содержит список отображаемых элементов. Это связано со свойством «памятки» компонента «toDoViewModel». В классе ViewModel это свойство является геттером, который запрашивает ReminderService:

protected ReminderService reminderService;
 
public List<Reminder> getReminders() {
    return this.reminderService.findAll();
}

Приведенный выше атрибут привязки «модель» имеет атрибуты load-after. Они указывают, что модель будет перезагружаться (и, следовательно, перерисовываться) всякий раз, когда запускаются события onClick кнопок добавления, обновления или удаления. Это приводит к автоматическому повторному отображению списка, когда ReminderService был обновлен в ответ на действия пользователя.

Отображение списка напоминаний определяется связыванием «@ {each = loopVar}»:

<listitem self="@{each=reminder}">
    <listcell label="@{reminder.name}"/>
    <listcell label="@{reminder.priority}"/>
    <listcell label="@{reminder.date, converter='org.zkforge.zktodo2.DateFormatConverter'}"/>
</listitem>

«Каждый» цикл перебирает коллекцию, связанную как «модель», и определяет переменную цикла «напоминание». На каждой итерации создается новый экземпляр ListItem, содержащий три элемента Listcells. Метки компонентов listcell привязаны к свойствам текущей переменной цикла объекта Reminder. Таким образом, список бизнес-объектов, возвращаемых ReminderService из базы данных, отображается на экране без написания какого-либо пользовательского кода. Атрибуты «load-after» привязки модели приводят к повторному извлечению и повторному отображению списка при каждом нажатии кнопок добавления, обновления или удаления.

Свойство selectedItem в списке обновляется всякий раз, когда пользователь щелкает элемент в списке. Это свойство связано со свойством selectedReminder бина toDoViewModel. В классе ViewModel это свойство является обычным получателем и установщиком переменной-члена:

protected Reminder selectedReminder;
 
public Reminder getSelectedReminder() {
    return selectedReminder;
}
 
public void setSelectedReminder(Reminder reminder) {
    this.selectedReminder = reminder;
    if( this.selectedReminder == null ){
        this.selectedReminder = new Reminder();
    }
}

Всякий раз, когда пользователь нажимает на другой элемент в списке, вызывается установщик ViewModel, передавая бизнес-объект Reminder, который использовался для визуализации определенного элемента списка. Таким образом, свойство selectedReminder в ViewModel всегда является текущим выбором пользователя.

Панель редактирования позволяет пользователю редактировать выбранную сущность напоминания. Это легко достигается путем привязки входных компонентов панели редактирования к свойству selectedItem в ViewModel:

<hbox>
 Item:<textbox cols="40" constraint="no empty"
    value="@{toDoViewModel.selectedReminder.name, load-after='add.onClick,delete.onClick'}"/>
 Priority:<intbox id="priority" cols="1" constraint="no empty"
    value="@{toDoViewModel.selectedReminder.priority, load-after='add.onClick,delete.onClick'}"
    />
 Date:<datebox id="date" cols="14" constraint="no empty"
    value="@{toDoViewModel.selectedReminder.date, load-after='add.onClick,delete.onClick'}"
    onChange="@{toDoViewModel, converter='org.zkforge.zktodo2.binding.InputEventCommandConverter'}"/>
</hbox>

Текстовое поле привязано к свойству name выбранного бизнес-объекта Reminder. Аналогичным образом Intbox привязывается к атрибуту приоритета, а Datebox — к свойству даты. Когда пользователь нажимает кнопки добавления или удаления, атрибут selectedReminder в ViewModel изменится. Поскольку Binder знает, что он изменил атрибут модели, он перезагрузит панель редактирования. Привязки также включают подсказки «load-after» для перезагрузки панели в ответ на нажатие кнопок, которые могут изменить «selectedReminder» ViewModel.

Привязки панели редактирования как для чтения, так и для записи. Когда пользователь вводит данные в текстовое поле, обработчик события onChange заставит Binder вызвать установщик «name» в выбранном в данный момент компоненте Reminder. Таким образом, не требуется код, чтобы заставить панель редактирования обновлять бизнес-объекты. Вся эта работа выполняется автоматически программой Binder в соответствии с привязками в файле ZUL.

Кнопки «добавить», «обновить» и «удалить»:

<button id="add" label="Add" width="36px" height="24px" onClick="@{toDoViewModel}"/>
<button id="update" label="Update" width="46px" height="24px" onClick="@{toDoViewModel}"/>
<button id="delete" label="Delete" width="46px" height="24px" onClick="@{toDoViewModel}"/>

События onClick привязаны к методам ViewModel. Имя целевого метода устанавливается соглашением; имя метода команды, вызываемой в ViewModel, определяется свойством ID компонента. Кнопки имеют идентификаторы «добавить», «обновить» и «удалить», так что это имена методов, которые вызываются в bean-компоненте toDoViewModel:

public void delete() {
    if( this.selectedReminder.getId() != null ){
        try {
            this.reminderService.delete(selectedReminder);
            this.selectedReminder = new Reminder();
        } catch (EntityNotFoundException e) {
            e.printStackTrace();
        }
    }
}
 
public void update() {
    if( this.selectedReminder.getId() != null ){
        try {
            this.reminderService.merge(selectedReminder);
        } catch (EntityNotFoundException e) {
            e.printStackTrace();
        }
    }
}
 
public void add() {
    if( this.selectedReminder.getId() == null ){
        this.reminderService.persist(this.selectedReminder);
        this.selectedReminder = new Reminder();
    }
}

Эти три бизнес-метода ViewModel работают напрямую с выбранной сущностью напоминания. Выбранное напоминание — это то, которое удерживается привязкой selectedItem, которая записывает в свойство selectedReminder ViewModel. Для извлечения правильного бизнес-объекта из списка не требуется код; технология ZK Binder делает свою работу. Напомним, что на панели редактирования есть подсказки «загрузить после», чтобы обновить панель после нажатия кнопок «добавить» и «удалить». Из приведенного выше кода видно, что это необходимо, поскольку соответствующие методы в ViewModel изменяют свойство selectedReminder.

Связыватель отслеживает зависимости компонентов, которые связаны с тем же свойством; когда он записывает в свойство ViewModel, он автоматически перезагружает все экранные переменные, связанные с тем же свойством. Когда вы используете панель редактирования, компоненты записывают непосредственно в сущность напоминания; и это немедленно отражается в списке. Нам нужно предоставлять подсказки «только после загрузки», когда бизнес-логика изменяет связанное свойство таким образом, который Binder не может предвидеть.

Хотя это не требуется для этого экрана, Binder может связывать атрибуты «видимого» и «отключенного» Компонентов с логическими свойствами ViewModel. В примере приложения есть экран weather-station-mvvm.zulчто делает динамически отключает поля панели редактирования на основе состояния свойства ViewModel. Используя «видимую» привязку, мы также можем сделать так, чтобы вся панель редактирования или отдельные поля отображались и скрывались, основываясь на логическом свойстве ViewModel.

ViewModel в фокусе

Логика ViewModel, представленная в этой статье, существенно отличается от Presenter в шаблоне MVP. Это не подталкивает бизнес-состояние к компонентам экрана. Вместо этого бизнес-состояние вытягивается в View из ViewModel с помощью AnnotateDataBinder. ViewModel также отличается от Controller в шаблоне ZK MVC. Контроллер ZK, реализованный как подкласс GenericForwardComposer, требует специально названных методов-обработчиков событий, которые принимают объект события ZK в качестве аргумента метода. В отличие от этого, класс ViewModel не является типом инфраструктуры пользовательского интерфейса; он не реализует методы каркаса пользовательского интерфейса. Вместо этого класс CommandBinder является посредником между логикой обработки событий и бизнес-методами.

Хотя конвертер не показан выше, его можно указать с помощью «onClick = $ {bean, converter}». Конвертер может извлекать бизнес-объекты из события onClick или его целевого компонента. Эти разрешенные бизнес-объекты затем будут переданы в качестве аргументов связанному бизнес-методу. Полное отделение ViewModel от платформы представления делает его чистой моделью логического интерфейса, независимого от структуры представления. Такая модель может быть представлена ​​через мыльный интерфейс и вызываться удаленными программами. Он также может быть привязан к нескольким экранам. Пользователь промышленного администратора может получить доступ к одной странице ZUL, оптимизированной для мыши и клавиатуры. Выездные специалисты могут получить доступ к той же модели ViewModel через другую страницу ZUL, оптимизированную для надежного устройства с сенсорным экраном и большими высококонтрастными кнопками.У нас может быть специальный набор экрана, на котором представлена ​​та же модель представления, которая подходит для слабовидящих.

Заключительные замечания

Экран, обсуждаемый в этой статье, был создан с удивительно небольшим количеством строк кода. Плагин eclipse metrics вычисляет количество «строк кода метода» для класса ZkToDoViewModel как 28 строк. Класс ZkToDoViewModel не имеет зависимостей времени компиляции от классов инфраструктуры ZK. Мы полностью полагаемся на возможности отражения классов ZK Binder для создания динамического экрана, который управляет моделью для обновления базы данных.

ViewModel — это поведение и состояние открытого экрана. Это настоящая ScreenModel или ApplicationModel с состоянием, которая является посредником между пользователем и службами системы. Это делает его многоразовым между разными экранами. JUnit также проще тестировать ViewModel как отдельный класс. Это технология Binder ZK, которая является ключом к этому сжатому ОО подходу. Вызов подхода Model-View-Binder (или, точнее, View-Binder-Model) может помочь большему количеству людей организовать свою логику пользовательского интерфейса вокруг бизнес-ориентированных моделей представления. Да здравствует Binder!

Рекомендации

  1. Шаблоны презентаций На слайдах ZK, представленных в британской группе пользователей ZK
  2. Вдохновляющая статья Microsoft .Net MVVM Джоша Смита
  3. Мартин Фаулер Графический интерфейс Шаблоны страниц Архитектура пользовательского интерфейса
  4. Книга о том, как построить многоуровневую модель домена с помощью Spring POJO в действии
  5. ZK MVC Экран для POJO в действии Код книги Test Driving ZK FoodToGo
  6. Книга о разработке «сначала доменные объекты» для гибкого кода Evans Domain Driven Design
  7. Статья MVC Настольные шаблоны MVC ZK, Spring & JPA
  8. Оригинальная статья MVP SmallTalks 2008 Лучшие паттерны MVC