null

Liferay 7.2 + Primefaces: обновление одной строки в таблице.

Доброго времени суток.

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

ManagedBean:

package com.tuneit.itc;

import lombok.AllArgsConstructor;
import lombok.Data;

import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Data
@ManagedBean
@ViewScoped
public class TableTestBean implements Serializable {

    private List<TableRow> rows;

    @PostConstruct
    public void init() {
        rows = fillData();
    }

    private List<TableRow> fillData() {
        List<TableRow> result = new ArrayList<>();
        for (int i = 0; i < 3; ++ i) {
            int fst = 0;
            int snd = 0;
            result.add(new TableRow(i, fst, snd));
        }
        return result;
    }


    @Data
    @AllArgsConstructor
    public static class TableRow implements Serializable {
        private long id;
        private int fst;
        private int snd;
        public int getResult() {
            return fst * snd;
        }
    }

}

Facelet-шаблон:

<?xml version="1.0"?>

<f:view
        xmlns="http://www.w3.org/1999/xhtml"
        xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:h="http://xmlns.jcp.org/jsf/html"
        xmlns:p="http://primefaces.org/ui"
        xmlns:ui="http://java.sun.com/jsf/facelets"
>
    <h:head>
    </h:head>
    <h:body>
        <h:form>
            <p:dataTable id="table" value="#{tableTestBean.rows}" var="row">
                <p:column headerText="Id">
                    <h:outputText value="#{row.id}"/>
                </p:column>
                <p:column headerText="First data">
                    <p:inputText value="#{row.fst}"/>
                </p:column>
                <p:column headerText="Second data">
                    <p:inputText value="#{row.snd}"/>
                </p:column>
                <p:column>
                    <p:commandButton value="*" update="@form" process="@form"/>
                </p:column>
                <p:column headerText="Result">
                    <h:outputText value="#{row.result}"/>
                </p:column>
            </p:dataTable>
        </h:form>
    </h:body>
</f:view>

Вычисление результата происходит в методе модели строки; при нажатии на кнопку происходит отправка формы, содержащей таблицу, и форма перерисовывается.

До отправки формы:

После нажатия на кнопку (в одной строке):

Такой подход работает, но приводит к отправке большого количества лишних данных: при изменении всего одной строки приходится отправлять и обновлять всю форму. Кроме того, ошибка конвертации/валидации в одной строке приведёт к тому, что значения в модели обновлены не будут (и результат для строки не будет пересчитан), что не всегда соответствует логике приложения. Благо, в Primefaces встроено большое количество различных выражений, которые можно писать в атрибутах update и process (Primefaces Search, [1], [2]), среди которых есть селектор @row, который выбирает строку под заданным номером. Кроме того, у компонента p:dataTable есть атрибут rowIndexVar, через который можно задать имя переменной, доступной через EL и равной номеру текущей строки. Попробуем модифицировать Facelets-шаблон в соответствии с полученными знаниями.

<?xml version="1.0"?>

<f:view
        xmlns="http://www.w3.org/1999/xhtml"
        xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:h="http://xmlns.jcp.org/jsf/html"
        xmlns:p="http://primefaces.org/ui"
        xmlns:ui="http://java.sun.com/jsf/facelets"
>
    <h:head>
    </h:head>
    <h:body>
        <h:form>
            <p:dataTable id="table" value="#{tableTestBean.rows}" rowIndexVar="idx" var="row">
                <p:column headerText="Id">
                    <h:outputText value="#{row.id}"/>
                </p:column>
                <p:column headerText="First data">
                    <p:inputText value="#{row.fst}"/>
                </p:column>
                <p:column headerText="Second data">
                    <p:inputText value="#{row.snd}"/>
                </p:column>
                <p:column>
                    <p:commandButton value="*" update="@form:table:@row(#{idx})" process="@form:table:@row(#{idx})"/>
                </p:column>
                <p:column headerText="Result">
                    <h:outputText value="#{row.result}"/>
                </p:column>
            </p:dataTable>
        </h:form>
    </h:body>
</f:view>

Но, увы, при нажатии на кнопку ничего не происходит - значение в последней колонке не обновляется. Тем не менее, запрос при нажатии на кнопку отправляется:

А ответ содержит данные, похожие на то, что могло бы содержаться в одной строке таблицы:

Если внимательно посмотреть на последние элементы update, то в них можно обнаружить правильный результат умножения 11 на 12.

<update id="_tabletestportlet_WAR_tabletestportlet_:j_idt3:table:0:j_idt13">132</update>

Значит, сервер сгенерировал новую разметку, которую клиентская часть приложения по какой-то причине не смогла применить к DOM'у.

Попробуем поискать элементы с id, указанными в ответе, на странице. Получим интересный результат:

Получается, что по какой-то причине не всем элементам был присвоен идентификатор (а именно - h:outputText'у). Попробуем форсировать назначение ему идентификатора путём задания соответствующего атрибута:

<p:column headerText="Result">
    <h:outputText id="result" value="#{row.result}"/>
</p:column>

И, ура, это помогло!

Кроме того, видно, что селектор @row возымел должный эффект - значение обновилось только в той строке, где была нажата кнопка.

Вместо заключения

Не один раз столкнувшись с этой проблемой, я взял себе за правило оборачивать весь контент колонки в какой-то контейнер (h:panelGroup, например) и всегда задавать ему атрибут id.

Засим откланиваюсь, прощайте.

Вперед