Быстрое переключение между записями

Добрый день.

Столкнулись с проблемой, подскажите, как это можно решить.

У нас есть MasterDetailScreen, где edit часть реализована фрагментом.
Изначально запись свободна для редактирования, и для того, чтобы Cuba предложила сохранить изменения, достаточно поменять значение поля во фрагменте (то есть объект не заблокирован до нажатия кнопки “Изменить”, как в оригинале, и все его поля по умолчанию RW, перед началом измененияне требуется нажать кнопку “Изменить”.).
То есть если в оригинале экран выглядит вот так:
image
То в нашей реализации вот так:
image

При изменении записи срабатывает лиснер инстанс-контейнера ItemPropertyChangeListener.

У нас возникает проблема, если мы очень быстро переключаемся между записями: тогда экран может спонтанно перейти в режим сохранения, как будто я внес в него изменения, но по факту мы ничего не меняем.
Ранее это происходило, потому что по какой-то причине в системе у записей самостоятельно менялись системные атрибуты (даты и версии, и некоторые коллекции, но мы это устранили, отсеяв в листнере).

Мы проанализировали, какая информация поступает в листнер в момент “изменения”, и обнаружили, что система самостоятельно меняет поля даты, если они есть у объекта.
Причем система меняет значения полей на те, которые были у соседней записи.
То есть если я переключусь с записи СЗ00000021 на запись СЗ00000007, то система проставит для СЗ00000007 значения дат полей “Дата создания” и “Дата затрат” из объекта СЗ00000021, сработает листнер ItemPropertyChangeListener и попросит сохранить изменения.

Такое замечено только для значений дат.
Подскажите, пожалуйста, это нормально?
И что с этим можно сделать?

Добрый день, @alex2910sk!

К сожалению, без кода контроллера экрана и XML дескриптора очень сложно ответить на ваш вопрос и помочь вам. Вы могли бы их прислать?

С уважением,
Глеб

Добрый день.
Я создал тестовый класс.
Его реализация аналогичная вашей за исключением того, что в нем переопределен один метод initEditComponents (делает поля редактируемыми сразу), и есть 2 листнера:

ExpenseLineBrowseTest

package com.company.itam.web.screens.expenseline;

import com.haulmont.cuba.gui.ComponentsHelper;
import com.haulmont.cuba.gui.components.ComponentContainer;
import com.haulmont.cuba.gui.components.Form;
import com.haulmont.cuba.gui.components.TabSheet;
import com.haulmont.cuba.gui.components.Table;
import com.haulmont.cuba.gui.model.InstanceContainer;
import com.haulmont.cuba.gui.screen.*;
import com.company.itam.entity.ExpenseLine;

@UiController("itam_ExpenseLine.browseTest")
@UiDescriptor("expense-line-browse-test.xml")
@LookupComponent("table")
@LoadDataBeforeShow
public class ExpenseLineBrowseTest extends MasterDetailScreen<ExpenseLine> {
    @Override
    protected void initEditComponents(boolean enabled) {
        TabSheet tabSheet = getTabSheet();
        if (tabSheet != null) {
            ComponentsHelper.walkComponents(tabSheet, (component, name) -> {
                if (component instanceof Form) {
                    ((Form) component).setEditable(enabled);
                } else if (component instanceof Table) {
                    ((Table) component).getActions().forEach(action -> action.setEnabled(enabled));
                } else if (!(component instanceof ComponentContainer)) {
                    component.setEnabled(enabled);
                }
            });
        }
        getForm().setEditable(true);
        getLookupBox().setEnabled(true);
        editing = true;
    }

    @Subscribe(id = "expenseLineDc", target = Target.DATA_CONTAINER)
    public void onExpenseLineDcItemPropertyChange(InstanceContainer.ItemPropertyChangeEvent<ExpenseLine> event) {
        System.out.println();
    }

    @Subscribe(id = "expenseLineDc", target = Target.DATA_CONTAINER)
    public void onExpenseLineDcItemChange(InstanceContainer.ItemChangeEvent<ExpenseLine> event) {

    }



}

И его дескриптор:

<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        xmlns:c="http://schemas.haulmont.com/cuba/screen/jpql_condition.xsd"
        caption="msg://browseCaption"
        focusComponent="table"
        messagesPack="com.company.itam.web.screens.expenseline">
    <data>
        <collection id="expenseLinesDc"
                    class="com.company.itam.entity.ExpenseLine"
                    view="expenseLine-browse">
            <loader id="expenseLinesDl">
                <query>
                    <![CDATA[select e from itam_ExpenseLine e]]>
                </query>
            </loader>
        </collection>
        <instance id="expenseLineDc"
                  class="com.company.itam.entity.ExpenseLine"
                  view="expenseLine-edit">
            <loader/>
        </instance>
    </data>
    <actions>
        <action id="save" icon="icons/ok.png" caption="mainMsg://actions.Ok" shortcut="CTRL-ENTER"/>
        <action id="cancel" icon="icons/cancel.png" caption="mainMsg://actions.Cancel" description="Esc"/>
    </actions>
    <dialogMode height="600" width="800"/>
    <layout>
        <split id="split" height="100%" orientation="vertical" reversePosition="true" width="100%">
            <vbox id="lookupBox" expand="table" height="100%" margin="false,true,false,false" spacing="true">
                <filter id="filter" applyTo="table" dataLoader="expenseLinesDl">
                    <properties include=".*"/>
                </filter>
                <groupTable id="table"
                            width="100%"
                            dataContainer="expenseLinesDc">
                    <actions>
                        <action id="create" type="create"/>
                        <action id="edit" type="edit"/>
                        <action id="remove" type="remove"/>
                    </actions>
                    <columns>
                        <column id="code"/>
                        <column id="moneyValue"/>
                        <column id="expenseDate"/>
<!--                        <column id="lnkCostCenter"/>-->
<!--                        <column id="lnkPortfolio"/>-->
<!--                        <column id="expenseType"/>-->
                        <column id="name"/>
                    </columns>
                    <rowsCount/>
                    <buttonsPanel id="buttonsPanel"
                                  alwaysVisible="true">
                        <button id="createBtn" action="table.create"/>
                        <button id="editBtn" action="table.edit"/>
                        <button id="removeBtn" action="table.remove"/>
                    </buttonsPanel>
                </groupTable>
                <hbox id="lookupActions" spacing="true" visible="false">
                    <button action="lookupSelectAction"/>
                    <button action="lookupCancelAction"/>
                </hbox>
            </vbox>
            <vbox id="editBox" height="100%" margin="false,false,false,true" expand="fieldGroupBox" spacing="true">
                <scrollBox id="fieldGroupBox">
                    <form id="form" dataContainer="expenseLineDc">
                        <column width="250px">
                            <textField id="codeField" property="code"/>
                            <textField id="moneyValueField" property="moneyValue"/>
                            <dateField id="expenseDateField" property="expenseDate"/>
<!--                            <pickerField id="lnkCostCenterField" property="lnkCostCenter">-->
<!--                                <actions>-->
<!--                                    <action id="lookup" type="picker_lookup"/>-->
<!--                                    <action id="clear" type="picker_clear"/>-->
<!--                                </actions>-->
<!--                            </pickerField>-->
<!--                            <pickerField id="lnkPortfolioField" property="lnkPortfolio">-->
<!--                                <actions>-->
<!--                                    <action id="lookup" type="picker_lookup"/>-->
<!--                                    <action id="clear" type="picker_clear"/>-->
<!--                                </actions>-->
<!--                            </pickerField>-->
<!--                            <lookupField id="expenseTypeField" property="expenseType"/>-->
                            <textField id="nameField" property="name"/>
                        </column>
                    </form>
                </scrollBox>
                <hbox id="actionsPane" spacing="true" visible="false">
                    <button id="saveBtn" action="save"/>
                    <button id="cancelBtn" action="cancel"/>
                </hbox>
            </vbox>
        </split>
    </layout>
</window>

Чем больше полей, тем быстрее вызывается баг.
Но можно вызвать и с текущим набором.
image

Параллельно хочу уточнить, есть ли у системы возможность делать экран нередактируемым, пока он не прогрузит все данные до конца. Например мы выбираем запись, Cuba какое-то время крутит колесо загрузки по центру, пока выполняет свой код и ожидает окончания RPC взаимодействий, а потом отпускает весь экран, после чего пользователь опять может взаимодействовать с UI. Это бы помогло решить проблему.
Блокировка Vaadin сессии не помогает, так как UI все равно доступен для нажатий.
Через листнеры экрана сделать также не получается, так как они отрабатывают один за другим, и только после полной отработки отдают изменения на фронт.

Глеб, Добрый день.
Приложил ответы.
Возможно, не ответил на ваши сообщение напрямую, поэтому боюсь, что вы могли их не заметить раньше.

Добрый день, @alex2910sk!

Прошу прощения за долгий ответ. У меня получилось воспроизвести вашу проблему.

Как показано на данном скриншоте, для полей с типом DateTime меняется только значение дня (DateField), а время (TimeField) остается прежним.

Возможно проблема заключается в компоненте Vaadin. Rpc метод для смены значения даты в DateField компоненте помечен аннотацией com.vaadin.shared.annotations.@Delayed (см. com.vaadin.shared.ui.datefield.AbstractDateFieldServerRpc.#updateValueWithDelay). Это означает, что событие смены значения в DateField отправляется на сервер не сразу после изменения значения, а оно помещается в очередь и отправляется на сервер при вызове любого другого rpc метода, не помеченного аннотацией @Delayed.

А так как для отображения значений дат используется один и тот же компонент DateField, то возникает ситуация, когда вы уже сменили элемент в instance контейнере, а значение даты меняется в зависимости от rpc вызовов. Поэтому значение дня остается от старого элемента, а время от нового.

С уважением,
Глеб

Добрый день, Глеб.

Спасибо! Можете подсказать, можно ли с этим элементом что-то сделать?

Добрый день, @alex2910sk !

Вы можете воспользоваться стандартной реализацией MasterDetailScreen. Так как объект будет заблокирован для изменения при смене значений в главной коллекции, то описанная выше проблема не будет возникать.

С уважением,
Глеб

Других вариантов нет, как я понимаю?

Можете попробовать переопределить метод CubaDateFieldWidget#buildDate(), чтобы вызывать rpc метод без аннотации @Delayed.

    @Override
    public void buildDate() {
        // Save previous value
        String previousValue = getText();
        super.buildDate();

        // Restore previous value if the input could not be parsed
        if (!parsable) {
            setText(previousValue);
        }
        updateTextFieldEnabled();
        bufferedDateString = text.getText();
        updateBufferedResolutions();
        // send the Time changes.
        sendBufferedValues();

        updateTextState();
    }

Других решений я, к сожалению, не могу предложить.

С уважением,
Глеб

Глеб, спасибо!
Попробуем.
Подскажите, пожалуйста, мне переопределенный класс как-то нужно пометить? (аннотацией, или внести в какой-нибудь XML)

Добрый день, @alex2910sk!

Вам нужно выполнить несколько шагов, чтобы переопределить метод в виджете:

  1. Добавить web-toolkit модуль
  2. Создать наследника CubaDateFieldWidget, в котором переопределить метод buildDate()
package com.company.sample.web.toolkit.ui.customdatefield;

import com.haulmont.cuba.web.widgets.client.datefield.CubaDateFieldWidget;

public class CustomCubaDateFieldWidget extends CubaDateFieldWidget {

    @Override
    public void buildDate() {
        // Save previous value
        String previousValue = getText();
        super.buildDate();

        // Restore previous value if the input could not be parsed
        if (!parsable) {
            setText(previousValue);
        }
        updateTextFieldEnabled();
        bufferedDateString = text.getText();
        updateBufferedResolutions();
        // send the Time changes.
        sendBufferedValues();

        updateTextState();
    }
}
  1. Заменить CubaDateFieldWidgetна CustomCubaDateFieldWidget в AppWidgetSet.gwt.xml
<?xml version="1.0" encoding="UTF-8"?>
<module>

    <inherits name="com.haulmont.cuba.web.widgets.WidgetSet"/>

    <replace-with class="com.company.sample.web.toolkit.ui.customdatefield.CustomCubaDateFieldWidget">
        <when-type-is class="com.haulmont.cuba.web.widgets.client.datefield.CubaDateFieldWidget"/>
    </replace-with>
</module>

С уважением,
Глеб

1 симпатия

Глеб, Добрый день!

Наконец выбил время, чтобы проверить текущий вариант.
У меня возникает проблема при попытке собрать модуль web-toolkit (пустой, только что добавленный).
Пробовал менять его название, сокращать путь к конечному пакету с классами, но ничего не получается.

Подскажите, может сталкивались, как решить эту проблему?
image

Добрый день, @alex2910sk!

Попробуйте решение, описанное в следующем тикете.
Также можете попробовать мигрировать проект на gradle 6.1, там данная проблема исправлена.
Как временное решение можете попробовать сократить путь до jre от корня, сократить путь до gradle от корня или создать системную переменную GRADLE_USER_HOME C:\gradle-caches.

С уважением,
Глеб

1 симпатия

Добрый день!
Попробовал установить новую версию Gradle, через несколько устраненных ошибок запнулся на еще одной:
image

Пробовал переустанавливать JDK, делал ребилд с включенной настройкой чистки кеша в Idea, но пока ничего не помогает.
Причем запинается опять на этом же таске.
Подскажите, есть ли решение у этой проблемы…?

Подскажите, пожалуйста, в чем здесь может быть беда?

Добрый день,
В документацию добавлено подробное описание, как можно исключить ненужные для web-toolkit зависимости и избавиться от “error=206” без обновления версии Gradle:

https://doc.cuba-platform.com/manual-7.2-ru/widgetset_win_path_too_long.html

1 симпатия

Добрый день.
Я поэтапно проделала все манипуляции, описанные в инструкции под версией gradle 5.6.4-all.
Сначала я наблюдал ошибку 206, но после исключения транзитивных зависимостей модуля web-toolkit 206 ошибка пропала, теперь у меня возникает эксепшен при сборке:
Caused by: org.gradle.process.internal.ExecException: Process 'command 'C:\Program Files\Java\jdk1.8.0_261\bin\java.exe'' finished with non-zero exit value 1
Этот же эксепшен у меня возникал и ранее после того, когда я попытался собрать версию под gradle 6.1-all и gradle 6.6.1-all. При этом 206 ошибка не возникала.

Без установленного модуля web-toolkit ошибок не возникает.

Приложил ответ. Кстати, почему то когда я отвечаю на чей-то ответ у меня не всегда отображается вот это
image .
И поэтому я не до конца понимаю, получили ли вы мой ответ, или он просто прикрепился к посту без уведомления и прочего…

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

Запускайте сборку в консоли с флагом --stacktrace:

gradlew deploy --stacktrace > build.log

Тогда будет детальное сообщение об ошибке (почему java.exe не запустилась), которое можно будет разбирать.