GWT-GXT пример c Grid

Часто можно услышать «ХХ1-ый век - это век информации», «Кто владеет информацией тот владеет миром». Сегодня можно сказать, что информации много не бывает. Сложнее бывает представить и правильно структурировать всю информацию. Для этих целей лучше всего подходит табличная форма отображения информации, в которой весь объем данных можно представить в виде отдельных сущностей (строк), имеющих определенные атрибуты (ячейки).

В статье на основе примера gxt-table рассматривается представление информации в табличном виде. Исходный код примера в виде проекта Eclipse можно скачать. В качестве данных используется XML-файл "posts.xml" из примеров разработчиков Sencha GXT 3.1.1. Структура XML-файла и чтение данных этого файла подробно описаны здесь.

Описание примера

На следующем скриншоте представлен графический интерфейс GWT-приложения с использованием табличного компонента Grid библиотеки GXT, описание которого представлено ниже на странице.

Основным элементом графического представления является таблица, в которой информация представлена в трех колонках. В первой колонке располагается компонент CheckBox, позволяющий выделять несколько строк таблицы. Поскольку не всю информацию можно визуально представить в таблице, то дополнительную информацию в виде расширенной строки можно получить, нажав на кнопку «+» во второй колонке. Данную функцию демонстрирует вторая строка.

В верхней части таблицы располагается ToolBar, в котором размещается компонент ComboBox с выпадающим списком [Строка, Ячейка]. Выбирая одну из записей этого списка можно установить соответствующее выделение записей таблицы - либо строка, либо ячейка.

Объем данных в файле "posts.xml" превышает 2000 записей. В примере в нижней части таблицы размещается пейджинг (PagingToolBar), обеспечивающий постраничное представление данных. Пейджинг позволяет «листать» данные как вперед и назад, так и перейти на произвольную страницу. Кроме этого PagingToolBar отображает информацию об общем количестве записей и записях на текущей странице. Локализация интерфейса (визуальных компонентов) выполняется в файле конфигурации.

Структура примера

Структуру проекта gxt-table можно условно разделить на несколько частей :

  • файл конфигурации Gxt_table.gwt.xml;
  • клиентская часть (com.example.client), включающая :
    • главный модуль GXTTable.java;
    • модуль графического представления таблицы GXTGrid.java;
    • сервисы чтения данных (com.example.client.services);
  • серверная часть в виде сервиса(com.example.server), включающая :
    • сервиса чтения данных PagingServiceImpl.java;
    • XML-файл данных post.xml;
  • общая часть (com.example.shared) в виде класса Post.java. Поскольку этот класс используется как серверной частью проекта, так и клиентской частью, то он сериализуется. Необходимость сериализации класса, передаваемого по сети подробно описана здесь;
  • web-область проекта размещается в поддиректории «war».

На следующем скриншоте представлена структура GWT-проекта gxt-table.

Web-область проекта (поддиректория war) представляет собой структуру сборки приложения, которую необходимо размещать в web-контейнере (Tomcat, JBoss и т.п.). Если архиватором сжать директорию в zip-файл с расширением .war, то получим готовую сборку. Только наименование файла сборки должно совпадать с наименованием модуля, определенном в файле конфигурации Gxt_table.gwt.xml (тег <module rename-to='gxt_table'>).

Файл конфигурации Gxt_table.gwt.xml

<?xml version="1.0" encoding="UTF-8"?>
<module rename-to='gxt_table'>
    <inherits name='com.google.gwt.user.User' />

    <inherits name='com.google.gwt.user.theme.standard.Standard'/>
    <inherits name="com.sencha.gxt.theme.gray.Gray"             />

    <!-- Локализация интерфейса                      -->
    <extend-property name="locale" values="ru"        />
    <set-property name="locale" value="ru"            />
    <set-property-fallback name="locale" value="ru"   />

    <!-- Entry point class.                          -->
    <entry-point class='com.example.client.GXTTable'  />

    <source path='client'/>
    <source path='shared'/>

    <!-- Режим Super Dev Mode определим в GUI IDE    -->
    <!-- allow Super Dev Mode                        -->
    <!-- add-linker name="xsiframe"                / -->
</module>

Файл конфигурации содержит стандартные настройки - тема (Gray), точка входа (entry point), «расшаренные» директории (client, shared). Подключение режима «Super Dev Mode» осуществляется на уровне настроек проекта в IDE Eclipse.

Поскольку используется PagingToolBar, включающий строки сообщений и подсказки, то в файле конфигурации выполняется локализация интерфейса.

Листинг GXTTable.java

public class GXTTable implements IsWidget, EntryPoint
{
    GXTGrid                gxtGrid = null;
    SimpleComboBox<String> scb     = null;

    private ToolBar createToolBar()
    {
        ToolBar toolBar = new ToolBar();
        toolBar.add(new LabelToolItem("Режим выделения : "));

        scb = new SimpleComboBox<String>(
                     new StringLabelProvider<String>());
        scb.setTriggerAction(TriggerAction.ALL);
        scb.setEditable(false);
        scb.setWidth(100);
        scb.add("Строка");
        scb.add("Ячейка");
        scb.setValue("Строка");

        scb.addSelectionHandler(new SelectionHandler<String>() {
            @Override
            public void onSelection(SelectionEvent<String> event) {
                boolean cell = event.getSelectedItem().equals("Ячейка");
                gxtGrid.setSelectionModel(cell);
            }
        });
        toolBar.add(scb);
        return toolBar;
    }
    @Override
    public Widget asWidget()
    {
        ContentPanel panel = new ContentPanel();
        panel.setHeadingText("Пример GXT Grid");
        panel.setPixelSize(550, 500);
        panel.addStyleName("margin-15");

        VerticalLayoutContainer vlc = new VerticalLayoutContainer();
        vlc.setBorders(true);

        ToolBar toolBar = createToolBar();
        gxtGrid = new GXTGrid();

        PagingToolBar paging = gxtGrid.getPagingToolBar();

        vlc.add(toolBar, new VerticalLayoutData(1,-1));
        vlc.add(gxtGrid, new VerticalLayoutData(1, 1));
        vlc.add(paging , new VerticalLayoutData(1,-1));
        panel.setWidget(vlc);
        
        return panel;
    }
    @Override
    public void onModuleLoad()
    {
        RootPanel.get("wrapper").add(asWidget());
    }
}

Основной модуль приложения GXTTable.java реализует интерфейсы IsWidget, EntryPoint и включает переопределенные методы onModuleLoad, asWidget. Первый метод определяет точку входа в приложение в режиме run-time. Второй метод asWidget формирует графический интерфейс, представляемый в браузере. Функция createToolBar создает ToolBar, в котором размещает компонент SimpleComboBox<String> scb и подключает к нему обработчик события изменения режима выделения значаний/записей в таблице.

Табличный компонент и пейджинг вынесены в отдельный модуль GXTGrid.java. Для размещения компонентов на панели был использован VerticalLayoutContainer.

Определение сервиса чтения данных

Сервис чтения данных включают два клиентских модуля (PagingService, PagingServiceAsync) и один серверный модуль PagingServiceImpl. В клиентских модулях представлен только интерфейс функции чтения данных getPosts. В серверном модуле данный сервис «реализован».

Листинг PagingService

package com.example.client.services;

import com.example.shared.Post;

import com.sencha.gxt.data.shared.loader.PagingLoadConfig;
import com.sencha.gxt.data.shared.loader.PagingLoadResult;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
 
@RemoteServiceRelativePath("posts")
public interface PagingService extends RemoteService
{
    PagingLoadResult<Post> getPosts(PagingLoadConfig config);
}

Следует обратить внимание на аннотацию к методу @RemoteServiceRelativePath. Наименование "posts" используется в дескрипторе приложения web.xml.

Листинг PagingServiceAsync

package com.example.client.services;

import com.example.shared.Post;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.sencha.gxt.data.shared.loader.PagingLoadConfig;
import com.sencha.gxt.data.shared.loader.PagingLoadResult;

public interface PagingServiceAsync
{
    void getPosts(PagingLoadConfig config, AsyncCallback<PagingLoadResult<Post>> callback);
}

Вызов функции getPosts осуществляется с использованием асинхронных методов обратного вызова AsyncCallback.

Листинг PagingServiceImpl

// Класс сортировки
class SortedData implements Comparator<Post>
{
    private String sortField;
    SortedData (String sortField)
    {
        this.sortField = sortField;
    }
    public int compare(Post p1, Post p2) {
        if (sortField.equals("forum")) {
            return p1.getForum().compareTo(p2.getForum());
        } else if (sortField.equals("username")) {
            return p1.getUsername().toLowerCase().compareTo(
                   p2.getUsername().toLowerCase());
        } else if (sortField.equals("subject")) {
            return p1.getSubject().compareTo(p2.getSubject());
        } else if (sortField.equals("date")) {
            return p1.getDate().compareTo(p2.getDate());
        }
        return 0;
    }
};

public class PagingServiceImpl extends RemoteServiceServlet
                               implements PagingService
{
    private static final long serialVersionUID = 1L;

    private  List<Post>  posts     = null;
    private  String      sort_fld  = null;
    private  Post[]      data      = null;

    private String getValue(NodeList fields, int index)
    {
        // ...
    }
    private void loadPosts()
    {
        // ...
    }
    private void sortData(final SortInfo sort)
    {
        if (sort.getSortField() != null) {
            final String sortField = sort.getSortField();
            if ((sortField != null) && 
                 !sortField.equalsIgnoreCase(sort_fld)) {
                sort_fld = sortField;
                Collections.sort(posts, sort.getSortDir().comparator(
                                             new SortedData(sortField)));
            }
        }
    }
    @Override
    public PagingLoadResult<Post> getPosts(PagingLoadConfig config)
    {
        if (posts == null)
            loadPosts();

        if (config.getSortInfo().size() > 0) {
            SortInfo sort = config.getSortInfo().get(0);

            String sortField = sort.getSortField();
            if ((sortField != null) && (sort_fld != null)
                      && !sortField.equalsIgnoreCase(sort_fld)) {
                data = null;
                System.gc();
                sortData(sortField);
            }
        }
        ArrayList<Post> sublist = new ArrayList<Post>();
        int start = config.getOffset();
        int limit = posts.size();
        if (config.getLimit() > 0)
            limit = Math.min(start + config.getLimit(), limit);

        for (int i = config.getOffset(); i < limit; i++) {
            if (data != null)
                sublist.add((Post) data[i]);
            else
                sublist.add(posts.get(i));
        }
        return new PagingLoadResultBean<Post>(sublist, posts.size(), config.getOffset());
    }
}

Функция чтения информации получает в качестве параметра объект описания запрашиваемых данных config типа PagingLoadConfig, включающий параметры сортировки, количество записей и их смещение от начала списка.

Для чтения данных из файла используются методы loadPosts и getValue, которые загружают информацию в массив posts. Исходный код методов на странице не представлен (описан здесь).

Для получения параметров сортировки необходимо использовать функцию getSortInfo(), возвращающую список полей. В реализации сортировка данных осуществляется по первому полю. Если требуется сортировка данных, то информация сначала сортируется в массив data. Для этого используется метод sortData, в котором применяется класс SortedData, реализующий интерфейс Comparator.

Результирующие данные упаковываются в коллекцию sublist и отправляются клиенту в виде объекта PagingLoadResult.

Листинг web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
         http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5" xmlns="http://java.sun.com/xml/ns/javaee">
  <!-- Servlets -->
  <servlet>
      <servlet-name>pagingServlet</servlet-name>
      <servlet-class>com.example.server.PagingServiceImpl</servlet-class>
  </servlet>
  
  <servlet-mapping>
      <servlet-name>pagingServlet</servlet-name>
      <url-pattern>/gxt_table/posts</url-pattern>
  </servlet-mapping>

  <!-- Default page to serve -->
  <welcome-file-list>
      <welcome-file>Gxt_table.html</welcome-file>
  </welcome-file-list>
</web-app>

На что следует обратить внимание в дескрипторе приложения? Конечно же на то, как описывается сервис (сервлет) чтения данных. В секции <servlet> определяется наименование исполнительного модуля. В секции <servlet-mapping> определяется URL вызова (наименование модуля приложения, наименование сервиса). Наименование модуля приложения описано выше в Gxt_table.gwt.xml.

Создание и настройка таблицы Grid

В модуле GXTGrid определим поля, необходимые для создания визуального компонента (интерфейса) таблицы, загрузки и хранения данных : таблица grid, хранилище данных store, пейджинг pagingToolBar, загрузчик данных loader и прокси proxy. Все поля связаны с классом Post, структура которого описана в примере чтения данных XML.

GXTGrid реализует интерфейс IsWidget. В этом случае фреймворк GWT при размещении компонента на панели или в контейнере вызывает переопределяемый (@Override) метод asWidget, который должен возвращать компонент grid. Можно в этом методе создавать таблицу, а можно и в конструкторе. Ниже представлен листинг класса :

public class GXTGrid implements IsWidget
{
    Grid<Post>       grid          = null;
    ListStore<Post>  store         = null;
    PagingToolBar    pagingToolBar = null;
    PagingLoader<PagingLoadConfig, PagingLoadResult<Post>> loader = null;
    RpcProxy<PagingLoadConfig, PagingLoadResult<Post>>     proxy  = null;

    public GXTGrid()
    {
        createLoader       ();
        createPagingToolBar();
        createGrid         ();
    }
    @Override
    public Widget asWidget()
    {
        return grid;
    }
    private void createLoader()
    {
        // ... описание ниже
    }
    public PagingToolBar getPagingToolBar()
    {
        return pagingToolBar;
    }

    private void createPagingToolBar()
    {
        pagingToolBar = new PagingToolBar(15);
        pagingToolBar.getElement().getStyle().setProperty("borderBottom", "none");
        pagingToolBar.bind(loader);
    }
    private void createGrid()
    {
        // ... описание ниже
    }
    public void setSelectionModel(final boolean cell_celection)
    {
        // ... описание ниже
    }
    interface PostProperties extends PropertyAccess<Post>
    {
        // ... описание ниже
    }
}

Для создания таблицы с пейджингом необходимо выполнить следующие шаги:

  • создать загрузчик PagingLoader;
  • создать пейджинг PagingToolBar;
  • создать и настроить колонки таблицы;
  • создать и настроить таблицу.

Создание пейджинга (метод createPagingToolBar) не требует больших усилий. Для этого в конструктор PagingToolBar передаем количество записей на странице и в стилях определяем местоположения. Здесь самое главное связать пейджинг с загрузчиком loader.

Создание загрузчика PagingLoader

private void createLoader()
{
   final PagingServiceAsync service = GWT.create(PagingService.class);

    proxy = new RpcProxy<PagingLoadConfig, PagingLoadResult<Post>>() {
        @Override
        public void load(PagingLoadConfig loadConfig, 
                         AsyncCallback<PagingLoadResult<Post>> callback)
        {
            service.getPosts(loadConfig, callback);
        }
    };

    store = new ListStore<Post>(new ModelKeyProvider<Post>() {
        @Override
        public String getKey(Post item) {
            return "" + item.getId();
        }
    });

    LoadResultListStoreBinding <PagingLoadConfig, Post, 
                                PagingLoadResult<Post>> lrlsb; 
    lrlsb = new LoadResultListStoreBinding<PagingLoadConfig, Post, 
                                           PagingLoadResult<Post>>(store);

    loader = new PagingLoader<PagingLoadConfig, 
                              PagingLoadResult<Post>>(proxy);
    loader.setRemoteSort(true);
    loader.addLoadHandler(lrlsb);
}

В методе создания загрузчика createLoader необходимо определить сервис загрузки данных service и в proxy вызвать соответствующий метод (getPosts). В обработчик загрузки lrlsb передаем хранилище данных store. В загрузчике loader определяем удаленную сортировку на сервере (setRemoteSort (true)). В этом случае при нажатии мышкой на заголовок колонки таблицы будет вызываться метод getPosts сервиса загрузки данных с сервера. Если установить значение false, то сервис вызываться не будет и будут отсортированы только загруженные на страницу данные.

Листинг интерфейса класса PropertyAccess

interface PostProperties extends PropertyAccess<Post>
{
    ModelKeyProvider<Post>      id      ();

    ValueProvider<Post, Date  > date    ();
    ValueProvider<Post, String> forum   ();
//  ValueProvider<Post, String> subject ();
    ValueProvider<Post, String> username();
}

Листинг интерфейса PostProperties описывает методы типа ValueProvider, связывающие поля и их типы, реализованные в классе Post и используемые для представления значений в колонках таблицы. Метод id() определяет уникальный идентификатор, используемый в хранилище данных store. Колонка subject не используется. На примере этой колонки ниже показано, как можно настроить представление значений в интерфейсе.

Создание колонок таблицы

В модуле GXTGrid.java определим

  • три колонки типа ColumnConfig, которые будут использованы в интерфейсе таблицы для отображения в них соответствующей информации;
  • компоненты типа CheckBoxSelectionModel и RowExpander, которые будут размещаться в первых двух колонках и позволят выделять строки и показывать дополнительную информацию.
ColumnConfig<Post, String> forumColumn   ;
ColumnConfig<Post, String> usernameColumn;
ColumnConfig<Post, Date  > dateColumn    ;

IdentityValueProvider <Post> identity;
CheckBoxSelectionModel<Post> cbsm    ;
RowExpander           <Post> expander;

PostProperties props = GWT.create(PostProperties.class);

identity = new IdentityValueProvider<Post>();
cbsm = new CheckBoxSelectionModel<Post>(identity) {
    @Override
    protected void onRefresh(RefreshEvent event) {
        if (isSelectAllChecked())
            selectAll();
        super.onRefresh(event);
    }
};
expander = new RowExpander<Post>(identity, new AbstractCell<Post>() {
    @Override
    public void render(Context context, Post value, SafeHtmlBuilder sb) {
        sb.appendHtmlConstant("<p style='margin: 5px 5px 10px'><b>" + 
                                  value.getUsername() + "</b></p>");
        sb.appendHtmlConstant("<p style='margin: 5px 5px 10px'>" + 
                                  value.getSubject() + "</p>");
    }
});

forumColumn    = new ColumnConfig<Post, String>(props.forum()   , 250, "Forum"   );
usernameColumn = new ColumnConfig<Post, String>(props.username(), 100, "Username");
dateColumn     = new ColumnConfig<Post, Date  >(props.date()    , 100, "Date"    );

dateColumn.setCell(new DateCell(DateTimeFormat.getFormat(PredefinedFormat.DATE_SHORT)));

List<ColumnConfig<Post,?>> lst = new ArrayList<ColumnConfig<Post,?>>();
lst.add(cbsm.getColumn());
lst.add(expander);
lst.add(forumColumn);
lst.add(usernameColumn);
lst.add(dateColumn);

ColumnModel<Post> cm = new ColumnModel<Post>(lst);

Колонки таблицы упаковываются в коллекцию lst, на основе которой создается модель данных таблицы типа ColumnModel. Значение в колонке даты dateColumn форматируется с использованием класса DateTimeFormat.

Настройка колонок таблицы

Значение в ячейке таблицы можно модифицировать и по-разному выделять/подсвечивать. Для этого необходимо соответствующим образом настроить обработчик представления колонки. Ниже представлен код, как можно подключить колонку subjectColumn, в которой, в зависимости от значения, данные можно разделить на две строки с подсветкой второй строки красным цветом.

subjectColumn = new ColumnConfig<Post, String>(props.subject(),
                                                     120, "Subject");
subjectColumn.setCell(new AbstractCell<String>() {
    @Override
    public void render(com.google.gwt.cell.client.Cell.Context context,
                       String value, SafeHtmlBuilder sb)
    {
        int row = context.getIndex();
        Post post = grid.getStore().get(row);
        if (post.getSubject().startsWith("KEM")) {
            String temp = value;
            String firstLine  = temp.substring(0, temp.indexOf(" "));
            String secondLine = temp.substring(temp.indexOf(" ") + 1);
            temp = "<div style=\"white-space: nowrap;\">" + 
                           "<span style=\"color:gray;\">" + 
                                    firstLine + "</span>" +  
                           "<br>"                         +
                           "<span style=\"color:red;\">"  + 
                                   secondLine + "</span>" +
                    "</div>";
            sb.appendHtmlConstant(temp);
        } else
            sb.appendHtmlConstant("<div style=\"white-space:normal;\">"+ 
                                                      value + "</div>");
    }
});	    
...
lst.add(subjectColumn);

В методе render сначала определяется значение отображаемой строки и текущий объект Post. После этого соответствующее значение можно модифицировать и надлежащим образом отобразить в ячейке в формате HTML.

В метод render передается значение value соответствующее post.getSubject. Можно было бы не извлекать текущий объект post в данной ситуации. Но если потребуется модифицировать значение в зависимости от других полей записи, то объект пригодится.

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

Создание и настройка таблицы

После того, как колонки таблицы и модель данных подготовлены, можно создавать таблицу. Ниже представлен код создания таблицы.

grid = new Grid<Post>(store, cm)
{
    @Override
    protected void onAfterFirstAttach()
    {
        super.onAfterFirstAttach();
        Scheduler.get().scheduleDeferred(new ScheduledCommand()
        {
            @Override
            public void execute() {
                loader.load();
            }
        });
    }
};
grid.setSelectionModel(cbsm);
grid.getView().setForceFit(true);
grid.setLoadMask(true);
grid.setLoader(loader);

expander.initPlugin(grid);	    

К таблице подключается обработчик загрузки данных при первичном отображении в интерфейсе. Если возникнет необходимость принудительно загрузить или обновить данные, то можно вызывать метод load загрузчика loader.

Метод setSelectionModel определяет механизм выделения строк таблицы. Интерес представляет метод setForceFit, позволяющий автоматически пропорционально «расширить» колонки таблицы при наличии свободного пространства в интерфейсе. Метод setLoadMask будет отображать подгрузку данных в виде изменения изображения курсора. Ну и метод setLoader связывает таблицу с загрузчиком данных. В последней строке кода связываем expander с таблицей.

Скачать исходный код примера

Исходный код рассмотренного GWT-GXT примера с использованием табличного компонента Grid в виде проекта Eclipse можно скачать здесь (3.24 Мб).

В архив примера не включен библиотечный файл gwt-servlet.jar из GWT SDK для уменьшения размера архивного файла. Его необходимо подключить самостоятельно, как это представлено на следующем скриншоте («Build Path/Configure Build Path»).

Библиотека GXT (gxt-3.1.1.jar) размещена в поддиректории WEB-INF/lib. Можно библиотеку подключить как пользовательскую.

  Рейтинг@Mail.ru