Интеграция XForms и Google Web Toolkit: Часть 4. Разрабатываем интерактивные формы с помощью GWT и XForms (исходники)

Майкл Галпин

  • Часть 1: Представляем JSNI - Java Script Native Interface
  • Часть 2: Создаем формы управления исполнителями и альбомами
  • Часть 3: Используем GWT для создания элементов XForms
  • Введение

    В завершающей статье серии мы создадим интерактивную форму для добавления новых альбомов к списку того или иного исполнителя. Для этого мы воспользуемся поддержкой Ajax в GWT, а также JSNI и элементами управления XForms. Вы увидите, как GWT может дополнять XForms: в данном случае для локализации содержимого формы. Вдобавок мы продемонстрируем использование одной малоизвестной возможности GWT, а именно: сортировки в стиле Java. Это станет еще одной иллюстрацией того, как использование GWT облегчает работу Java-программиста.

    Предварительные требования

    В данной статье используется GWT версии 1.4 и подключаемый модуль Mozilla XForms версии 0.8. Модуль Mozilla XForms работает с любым браузером на основе Mozilla, например, Firefox или Seamonkey. Использование GWT предполагает определенные знания Java, а так же Web-технологий, таких как HTML и CSS. Кроме этого, в статье широко используется JavaScript. Наконец, поскольку технология XForms разработана в соответствии с парадигмой MVC (Model-View-Control), желательно понимание принципов MVC. В то же время опыт использования GWT или XForms хотя и будет полезным, но не является обязательным.

    Создание интерактивной формы в XForms

    Нам потребуется простая форма ввода, содержащая два поля: одно - для названия альбома и второе - для года его записи. Далее, на форме должна быть кнопка для вызова удаленного сервиса. При использовании XForms подобные интерфейсы создаются очень легко, как показано в листинге 1.

    Листинг 1. Форма для добавления нового альбома, созданная с помощью элементов управления XForms

                    
    <xhtml:div id="albumForm">
             <xforms:input id="title">
                  <xforms:label>Title:</xforms:label>
             </xforms:input>
             <xforms:input id="year">
                  <xforms:label>Year:</xforms:label>
             </xforms:input>
             <xforms:trigger id="btn">
                  <xforms:label>Add Album</xforms:label>
                  <xforms:load resource="javascript:addAlbum()" 
            ev:event="DOMActivate"/>
             </xforms:trigger>
    </xhtml:div>
    

    Не стоит удивляться легкости создания форм с помощью XForms. Единственное, что может выглядеть необычно - это вызов JavaScript-функции при срабатывании триггера (xforms:trigger). Как правило, триггеры в XForms вызывают функцию отправки, объявленную в модели XForms. Однако в данном случае мы планируем использовать GWT для отправки Ajax-запросов к удаленному сервису, поэтому триггер вызывает функцию в JavaScript. Теперь взглянем на фрагмент кода на JavaScript, который вызывает удаленный сервис.

    Вызов удаленного сервиса из XForm

    В третьей части мы создали сервис (AlbumService), предоставляющий метод для добавления нового альбома - именно его мы и будем вызывать. Как следует из сигнатуры метода, нам сначала придется создать объект типа Album для передачи на сервер. Класс Album представлен в листинге 2.

    Листинг 2. Класс Album

                    
    package org.developerworks.rockstar.client;
    
    import com.google.gwt.user.client.rpc.IsSerializable;
    
    public class Album implements IsSerializable{
         private int id;
         private int artistId;
         private String title;
         private int year;
         
         public int getId() {
              return id;
         }
         public void setId(int id) {
              this.id = id;
         }
         public int getArtistId() {
              return artistId;
         }
         public void setArtistId(int artistId) {
              this.artistId = artistId;
         }
         public String getTitle() {
              return title;
         }
         public void setTitle(String title) {
              this.title = title;
         }
         public int getYear() {
              return year;
         }
         public void setYear(int year) {
              this.year = year;
         }
         
    }
    

    Отметим, что класс содержит поле для хранения идентификатора (ID) исполнителя. Следовательно, нам понадобится этот идентификатор при создании нового альбома, и мы используем значение, переданное на страницу в параметре запроса artistId. Далее это значение надо где-то сохранить и сделать доступным для скрипта, который вызывает удаленный сервис. Для этого существует немало способов. В частности, XForms предоставляет элегантную возможность сохранения идентификатора в своей модели данных, как показано в листинге 3.

    Листинг 3. Сохранение параметраartistId в модели данных XForms

                    
    <xforms:model id="request">
        <xforms:instance id="artist" xmlns="">
            <Data>
                 <ArtistId><%=request.getParameter("artistId") 
           %></ArtistId>
            </Data>
        </xforms:instance>
    </xforms:model>
    

    Подобным образом можно сохранять и другие параметры запроса в модели данных. Теперь взглянем на созданный ранее метод addAlbum(), вызываемый в триггере XForms. Поскольку мы используем GWT, это всего лишь очередной метод Java-класса AlbumLib, показанный в листинге 4.

    Листинг 4. Метод addAlbum()

                    
    public void addAlbum(){
         String title = this.getAlbumTitle();
         int year = Integer.parseInt(this.getAlbumYear());
         int artistId = Integer.parseInt(this.getArtistId());
         Album album = new Album();
         album.setArtistId(artistId);
         album.setTitle(title);
         album.setYear(year);
         AlbumServiceAsync albumService = this.getAlbumService();
         AsyncCallback callback = new AsyncCallback(){
    
              public void onFailure(Throwable caught) {
                   removeAlbum();
                   refreshXformsModel();
              }
    
              public void onSuccess(Object result) {
                   // added optimistically, so nothing to do here
              }
                   
         };
         albumService.addAlbum(album, callback);
         this.addAlbumToModel(album);
         this.refreshXformsModel();
    }
    

    Метод достаточно прост: сначала создается новый альбом, затем запрашивается ссылка на удаленный сервис, и, наконец, вызывается метод сервиса addAlbum(). Кроме этого вызываются несколько вспомогательных методов, приведенных в листинге 5.

    Листинг 5. Вспомогательные методы, вызывающиеся из метода addAlbum()

                    
    private native String getAlbumTitle()/*-{
         return $doc.getElementById("title").value;
    }-*/;
         
    private native String getAlbumYear()/*-{
         return $doc.getElementById("year").value;
    }-*/;
         
    private native String getArtistId()/*-{
         var model = $doc.getElementById("request");
         var instance = model.getInstanceDocument("artist");
         var dataElement = instance.getElementsByTagName("Data")[0];
         return dataElement.getElementsByTagName("ArtistId")[0].firstChild.getTextValue();
    }-*/;
         
    private native void removeAlbum()/*-{
         var model = $doc.getElementById("albums");
         var instance = model.getInstanceDocument("albumData");
         var dataElement = instance.getElementsByTagName("Data")[0];
         dataElement.removeChild(dataElement.lastChild);
    }-*/;
    

    Все эти методы являются собственными методами JavaScript, созданными с помощью GWT через интерфейс JSNI. Методы getAlbumTitle() и getAlbumYear() просто используют JavaScript DOM для получения значений полей формы. Метод getArtistId() использует XForms JavaScript API для доступа к хранящемуся в модели идентификатору исполнителя, как показано выше в листинге 3. Наконец, метод removeAlbum() также использует XForms JavaScript API для удаления последнего созданного альбома из модели XForms. Как следует из листинга 4, removeAlbum() вызывается в случае ошибки при добавлении альбома на сервере. Подобные действия необходимы, т.к. клиентский метод addAlbum(), приведенный в листинге 4, заранее включает добавляемый альбом в модель и обновляет привязанные к ней элементы интерфейса.

    Примечание: Остальные два метода, вызываемые из addAlbum() - addAlbumToModel() и refreshXformsModel(), приведены в предыдущей статье, а также включены в исходный код к данной статье.

    Создание элементов управления XForms через GWT JSNI

    В предыдущей статье мы уже создавали элементы управления XForms через интерфейс JSNI в GWT. С тех пор мы добавили новую форму с полями для ввода альбомов. Эта форма также может быть создана через GWT JSNI. Все, что для этого требуется - это метод createEntryForm(), показанный в листинге 6.

    Листинг 6. Метод createEntryForm()

                    
    private native void createEntryForm()/*-{
         var xfNs = "http://www.w3.org/2002/xforms";
         // get the container div
         var container = $doc.getElementById("albumForm");
             
         var titleIn = $doc.createElementNS(xfNs, "xforms:input");
        titleIn.setAttribute("id","title");
        var titleInLabel = $doc.createElementNS(xfNs, "xforms:label");
        titleInLabel.appendChild($doc.createTextNode("Year:"));
        titleIn.appendChild(titleInLabel);
        container.appendChild(titleIn);
             
        var yearIn = $doc.createElementNS(xfNs, "xforms:input");
        yearIn.setAttribute("id","year");
        var yearInLabel = $doc.createElementNS(xfNs, "xforms:label");
        yearInLabel.appendChild($doc.createTextNode("Year:"));
        yearIn.appendChild(yearInLabel);
        container.appendChild(yearIn);
             
        var trigger = $doc.createElementNS(xfNs, "xforms:trigger");
        trigger.setAttribute("id", "btn");
        var btnLbl = $doc.createElementNS(xfNs, "xforms:label");
        btnLbl.appendChild($doc.createTextNode("Add Album"));
        trigger.appendChild(btnLbl);
        var loader = $doc.createElementNS(xfNs, "xforms:load");
        loader.setAttribute("resource", "javascript:addAlbum()");
        loader.setAttributeNS("http://www.w3.org/2001/xml-events", "ev:event", "DOMActivate");
        trigger.appendChild(loader);
        container.appendChild(trigger);
    }-*/;
    

    Метод опять-таки очень прост. Мы всего-навсего создаем различные элементы XForms, используя обычный DOM API на JavaScript, и добавляем их в дерево DOM. В частности, мы добавили два поля ввода и триггер. При этом приходится использовать специальные версии методов createElement и setAttribute, принимающие пространства имен в качестве параметров, т.к. по умолчанию XForms всегда использует собственное пространство имен, добавляемое к XHTML-схеме. Все что нам осталось - это добавить вызов createEntryForm() к скрипту, который исполняется при старте приложения, - и можно начинать тестирование.

    Тестирование приложения

    Тестирование приложения необходимо производить в Web-режиме. Можно сначала запустить приложение в режиме хоста, а уже затем переключиться в Web-режим, как показано на рисунке 1.

    Рисунок 1. Запуск приложения в Web-режиме
    launching Web mode

    Теперь приложение запущено в Web-режиме. Для перехода на страницу альбомов достаточно кликнуть на одном из исполнителей, как показано на рисунке 2.

    Рисунок 2. Страница показа альбомов в Web-режиме
    The albums page in Web mode

    Добавьте заголовок, год записи и нажмите на кнопку Add Album. Результат должен быть как на рисунке 3.

    Рисунок 3. Добавление нового альбома
    New album added

    Итак, мы создали интерфейс на основе GWT и XForms, а также определили сценарий взаимодействия с невидимыми для пользователя сервисами. Теперь, после того как вы научились использовать GWT и XForms совместно, посмотрим, какие в этом есть преимущества.

    Использование GWT-локализации в XForms

    GWT изначально проектировался как инструментарий, облегчающий решение широкого круга проблем при создании Web-приложений. Одной из таких распространенных проблем является локализация Web-приложений. Благодаря тому, что Google используется во многих странах, его разработчики хорошо осведомлены об этой проблеме. Таким образом, неудивительно, что GWT предоставляет интересные возможности для локализации, в отличие от XForms, которая является гораздо более узконаправленной технологией. Как следствие, GWT-локализация может оказаться очень полезной в приложении на XForms.

    Локализация в GWT: Интерфейс Constants

    Когда дело доходит до локализации, многие Java-разработчики используют только одну вещь: наборы ресурсов (resource bundles). Они, безусловно, полезны, но их возможности небезграничны. В GWT, в свою очередь, используется паттерн "подразумеваемый интерфейс" (implied interface). Он заключается в том, что интерфейс создается с привязкой к файлу свойств. Таким образом, при компиляции заключается своего рода соглашение между локализованным содержимым (контентом) и Java-кодом, включая и код GWT. Посмотрим, как можно использовать подобный метод в нашем приложении.

    На нашей странице, отображающей альбомы, существует пять основных типов содержимого: метки для заголовка альбома и года записи, поля ввода заголовка и года, а также метка кнопки "Add". Все это определяет интерфейс, показанный в листинге 7.

    Листинг 7. Java-интерфейс для локализации контента на странице для добавления альбомов

                    
    package org.developerworks.rockstar.client;
    
    import com.google.gwt.i18n.client.Constants;
    
    public interface AlbumContent extends Constants{
         public String titleLabel();
         public String yearLabel();
         public String titleInputLabel();
         public String yearInputLabel();
         public String addButtonLabel();
    }
    

    Теперь создадим соответствующий файл свойств. Важно помнить, что раз имя интерфейса AlbumContent, то файл обязан называться AlbumContent.properties и находиться в том же пакете, что и интерфейс. Файл приведен в листинге 8.

    Листинг 8. Файл AlbumContent.properties

                    
    titleLabel=Title:
    yearLabel=Year:
    titleInputLabel=Title:
    yearInputLabel=Year:
    addButtonLabel=Add Album
    

    Свойства в файле также должны соответствовать именам методов интерфейса, например, как показано в листинге 8. При этом можно создавать версии файла для любого количества языков и сохранять их в том же пакете. Во время исполнения GWT самостоятельно определит нужный язык, и вы получите локализованную версию интерфейса, к которой можно будет обращаться как из обычных клиентских методов на JavaScript, так и из собственных методов JavaScript, созданных с помощью JSNI. Пример такого обращения приведен в листинге 9.

    Листинг 9. Обращение к локализованному контенту

                    
    private AlbumContent getContent(){
         return (AlbumContent) GWT.create(AlbumContent.class);
    }
    
    private native void createEntryForm()/*-{
          // get the localized content
         var content = this.@org.developerworks.rockstar.client.AlbumLib::getContent();
    // ...
        var titleInLabel = $doc.createElementNS(xfNs, "xforms:label");
        var titleLabelTxt = content.titleInputLabel();
        titleInLabel.appendChild($doc.createTextNode(titleInputTxt));
    // ...         
        var yearInLabel = $doc.createElementNS(xfNs, "xforms:label");
        var yearLabelTxt = content.yearInputLabel();
        yearInLabel.appendChild($doc.createTextNode(yearLabelTxt));
    // ...
        var btnLbl = $doc.createElementNS(xfNs, "xforms:label");
        var btnLblTxt = content.addButtonLabel();
        btnLbl.appendChild($doc.createTextNode("Add Album"));
    
    }-*/;
    

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

    Конкурс рок-звезд или скрытые жемчужины GWT

    Мы потратили уйму времени, совершенствуя страницу для показа и добавления альбомов, сочетая использование GWT и XForms. Теперь пришло время вернуться чуть назад и добавить функциональности на страницу исполнителей. Например, давайте добавим возможность голосования за того или иного исполнителя. Для этого нам придется добавить поля в XML и в наш класс Artist, а также новый метод в сервис ArtistService. В итоге, реализация сервиса примет вид, как в листинге 10.

    Листинг 10. Реализация метода voteForArtist

                    
    public void voteForArtist(int artistId) {
         Artist artist = null;
         for (Artist a : this.artists){
              if (a.getId() == artistId){
                   artist = a;
                   break;
              }
         }
         if (artist != null){
              int votes = artist.getVotes() + 1;
              artist.setVotes(votes);
         }
         dao.saveArtists(this.artists);
    }
    

    Все, что требуется - это найти исполнителя, увеличить счетчик проголосовавших и сохранить его в списке. Разумеется, это не самый оптимальный способ, но вполне подойдет для нашего примера. Далее мы чуть модифицируем нашу таблицу для вывода списка исполнителей, как показано в листинге 11.

    Листинг 11. Добавление кнопок для голосования на страницу показа исполнителей

                    
    private void populateTable(){
          // clear the table
           int rowCount = this.artistTable.getRowCount();
           for (int i=0;i<rowCount;i++){
                this.artistTable.removeRow(i);
           }
          // create the header
          this.artistTable.getRowFormatter().addStyleName(0, "tableHeader");
          this.artistTable.setText(0, 0, "Name");
          this.artistTable.setText(0, 1, "Genre");
          this.artistTable.setText(0,2, "Vote Now!");
          // now add artists
          for (int i=0;i<artistList.size();i++){
               //this.artistTable.setText(i+1, 0, artists[i].getName());
               final Artist artist = (Artist) artistList.get(i);
               String html = "<a 
    href=\"Albums.jsp?artistId="+artist.getId()+"\">"+artist.getName()+"</a>";
               this.artistTable.setHTML(i+1, 0, html);
               this.artistTable.setText(i+1, 1, artist.getGenre());
               final Button btn = new Button("Vote for:" + artist.getName());
               
               ClickListener cl = new ClickListener(){
    
                   public void onClick(Widget sender) {
                        voteForArtist(artist);
                   }
                    
               };
               btn.addClickListener(cl);
               this.artistTable.setWidget(i+1, 2, btn);
          }
          this.artistTable.setBorderWidth(4);
      }
    

    Заметьте, что мы добавили кнопку Vote рядом с каждым из исполнителей. При нажатии на кнопку выполнится метод voteForArtist(), код которого показан в листинге 12.

    Листинг 12. Метод voteForArtist()

                    
    private void voteForArtist(final Artist artist){
          ArtistServiceAsync artistService = this.getArtistService();
           AsyncCallback callback = new AsyncCallback(){
    
              public void onFailure(Throwable caught) {
                   artist.setVotes(artist.getVotes() - 1);
              }
    
              public void onSuccess(Object result) {
                   // Nothing to do here
              }
                
           };
           artistService.voteForArtist(artist.getId(), callback);
           artist.setVotes(artist.getVotes() + 1);
      }
      

    Теперь нам осталось как-то отобразить результаты голосования. Для этого мы создадим таблицу-сетку (grid). Мы будем использовать сетку, а не FlexTable, потому что в данном случае мы точно знаем, сколько строк нам понадобится. Кроме этого, мы также создадим кнопку для показа и скрытия таблицы лидеров опроса (см. листинг 13).

    Листинг 13. Кнопка для показа и скрытия списка лидеров голосования

                    
    final Panel lbPanel = new VerticalPanel();
    final Button leaderButton = new Button("Show Leaderboard");
    public void onModuleLoad() {
         // add the outer panel, then add to it
         RootPanel.get().add(outerPanel);
         outerPanel.add(artistTable);
         outerPanel.add(formPanel);
         outerPanel.add(leaderButton);
         outerPanel.add(lbPanel);
    // ... More code omitted for brevity
         ClickListener lbListener = new ClickListener(){
    
              public void onClick(Widget sender) {
                   if (leaderButton.getText().equals("Show Leaderboard")){
                        showLeaderBoard();
                        leaderButton.setText("Hide Leaderboard");
                   } else {
                        lbPanel.clear();
                        leaderButton.setText("Show Leaderboard");
                   }
              }
              
         };
         leaderButton.addClickListener(lbListener);
        // ...
      }
      

    Теперь можно показывать и скрывать таблицу результатов, обновляя при этом надпись на кнопке. Метод для построения и вывода таблицы приведен в листинге 14.

    Листинг 14. Показ списка лидеров голосования

                    
    private void showLeaderBoard(){
           int size = this.artistTable.getRowCount() ;
           Grid leaderBoard = new Grid(size,2);
           leaderBoard.getRowFormatter().addStyleName(0, "tableHeader");
           leaderBoard.setText(0, 0, "Aritst");
           leaderBoard.setText(0, 1, "Votes");
           // sort the artists
           List sorted = sortArtists();
           for (int i=0;i<size-1;i++){
                Artist artist = (Artist) sorted.get(i);
                leaderBoard.setText(i+1, 0, artist.getName());
                leaderBoard.setText(i+1,1, ""+artist.getVotes());
           }
           lbPanel.add(leaderBoard);
      }
    

    Пока все было настолько просто, включая использование GWT, что даже не очень понятно, зачем было так подробно об этом рассказывать. Но интересный момент: мы вызываем некий загадочный метод sortArtists(). Как вы понимаете, список исполнителей не хранится заранее в отсортированном виде, но, разумеется, он должен быть отсортирован в таблице лидеров. Конечно, можно запросить сортировку на сервере, но лучше сделать это на клиентской стороне. Вы думаете, для этого придется писать что-то на JavaScript? Посмотрите, как это можно сделать в стиле GWT, как показано в листинге 15.

    Листинг 15. Сортировка списка исполнителей

                    
    private List sortArtists(){
           List sorted = new ArrayList(artistList);
           Collections.sort(sorted, new ArtistComparator());
           return sorted;
      }
      
      static class ArtistComparator implements Comparator{
    
         public int compare(Object arg0, Object arg1) {
              Artist a0 = (Artist) arg0;
              Artist a1 = (Artist) arg1;
              return a1.getVotes() - a0.getVotes();
         }
           
      }
    

    Мы отсортировали список исполнителей, как это обычно делается в Java, т.е. с помощью класса, реализующего Comparator и метода Collections.sort(). В соответствии с документацией JDK, этот метод реализован на основе модифицированной сортировки слиянием, что гарантирует сложность N*log(N). Не думаю, что кому-то захочется осуществлять реализацию сортировки слиянием в JavaScript, и использование GWT избавляет вас от этой необходимости. Теперь давайте протестируем наше приложение (см. рисунок 4).

    Рисунок 4. Страница исполнителей при скрытом списке лидеров
    Hidden leaderboard

    Теперь нажмите на кнопку Show Leaderboard для показа таблицы лидеров голосования (см. рисунок 5).

    Рисунок 5. Страница исполнителей при показанном списке лидеров
    Show leaderboard

    Похоже, что Dojo Darling заслуживает чуть больше голосов. Проголосуйте за него и выведите заново таблицу лидеров (см. рисунок 6).

    Рисунок 6. Обновленный список лидеров голосования
    Updated leaderboard

    Заключение

    В последней части нашей серии о совместном использовании GWT и XForms было продемонстрировано, как добавлять интерактивные формы в ваше приложение. При этом вы можете использовать GWT для создания элементов управления XForms, которые, в свою очередь, будут вызывать GWT-сервисы через Ajax. Результаты обработки запросов также могут быть переданы в GWT-классы, использующие JSNI для модификации модели данных и обновления интерфейсных элементов XForms. Такое тесное взаимодействие между двумя технологиями позволяет использовать некоторые ключевые возможности GWT на страницах XForms. В качестве примера такой возможности мы продемонстрировали локализацию страницы XForms. Вдобавок, вы увидели, как можно использовать одно из малоизвестных преимуществ GWT, а именно: сортировку в Java-стиле. Подобные возможности вкупе с локализацией и новинками типа пакетов изображений (image bundles) делают GWT очень удобным инструментарием для использования не только в новых проектах, но и для улучшения существующих приложений и технологий.


    Страница сайта http://test.interface.ru
    Оригинал находится по адресу http://test.interface.ru/home.asp?artId=17822