Custom Field updateValue does not update binder

Hi, i am using this custom component for customer contacts

i have noticed that the binder does not update when adding / removing a contact.

So i have added updateValue(), but then it only ever updates the binder’s entity the first time

so it works if i only add / remove a single contact a time,
as soon as i want to add / remove multiple at once it does not work

        ContactField contacts = new ContactField(getTranslation("bms.customer.contacts"));
        binder.forField(contacts).bind(CustomerEntity::getContacts, CustomerEntity::setContacts);
package ch.orca.ui.component;

import ch.orca.data.test.entity.ContactEntity;
import ch.orca.util.ApplicationUtil;
import com.vaadin.flow.component.accordion.Accordion;
import com.vaadin.flow.component.accordion.AccordionPanel;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.customfield.CustomField;
import com.vaadin.flow.component.details.DetailsVariant;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.GridVariant;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.provider.ListDataProvider;
import org.jooq.generated.enums.ContactType;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ContactField extends CustomField<List<ContactEntity>> {

    private Button saveButton;

    private enum EditMode { ADD, EDIT }

    private EditMode editMode = EditMode.ADD;

    private final List<ContactEntity> items = new ArrayList<>();
    private final Binder<ContactEntity> binder = new Binder<>();
    private final Grid<ContactEntity> grid = new Grid<>();

    public ContactField(String label) {
        add(buildAccordion(label));
    }

    private Accordion buildAccordion(String label) {
        VerticalLayout layout = new VerticalLayout(buildGrid(), buildInputLayout());
        layout.setPadding(false);

        Accordion accordion = new Accordion();
        AccordionPanel panel = accordion.add(label, layout);
        panel.addThemeVariants(DetailsVariant.LUMO_FILLED, DetailsVariant.LUMO_SMALL);

        binder.setBean(new ContactEntity());

        return accordion;
    }

    private Grid<ContactEntity> buildGrid() {
        grid.addThemeVariants(GridVariant.LUMO_COMPACT, GridVariant.LUMO_NO_BORDER, GridVariant.LUMO_NO_ROW_BORDERS);
        grid.setSelectionMode(Grid.SelectionMode.NONE);
        grid.setAllRowsVisible(true);
        grid.setDataProvider(new ListDataProvider<>(items));
        grid.addItemDoubleClickListener(event -> onEdit(event.getItem()));

        grid.addColumn(contact -> getTranslation(enumerationKey(contact.getType())))
                .setHeader(getTranslation("bms.contact.type")).setFlexGrow(0).setAutoWidth(true);
        grid.addColumn(ContactEntity::getValue)
                .setHeader(getTranslation("bms.contact.value"));
        grid.addColumn(ContactEntity::getComment)
                .setHeader(getTranslation("bms.contact.comment"));
        grid.addComponentColumn(this::buildRowControls)
                .setHeader(getTranslation("bms.common.controls"))
                .setFlexGrow(0)
                .setAutoWidth(true);

        return grid;
    }

    private HorizontalLayout buildInputLayout() {
        ComboBox<ContactType> type = new ComboBox<>(getTranslation("bms.contact.type"), ContactType.values());
        type.setItemLabelGenerator(contactType -> getTranslation(enumerationKey(contactType)));
        binder.bind(type, ContactEntity::getType, ContactEntity::setType);

        TextField value = new TextField(getTranslation("bms.contact.value"));
        binder.bind(value, ContactEntity::getValue, ContactEntity::setValue);

        TextField comment = new TextField(getTranslation("bms.contact.comment"));
        binder.bind(comment, ContactEntity::getComment, ContactEntity::setComment);

        saveButton = new Button(VaadinIcon.PLUS.create(), event -> onSave(binder.getBean()));
        saveButton.addThemeVariants(ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_SUCCESS);

        Button clearButton = new Button(VaadinIcon.REFRESH.create(), event -> onClear());
        clearButton.addThemeVariants(ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_WARNING);

        HorizontalLayout buttons = new HorizontalLayout(saveButton, clearButton);

        HorizontalLayout layout = new HorizontalLayout(type, value, comment, buttons);
        layout.setAlignItems(FlexComponent.Alignment.END);
        layout.setWidthFull();
        layout.setFlexGrow(1, type, value, comment);
        layout.setFlexGrow(0, buttons);

        return layout;
    }

    private HorizontalLayout buildRowControls(ContactEntity contact) {
        Button editButton = new Button(VaadinIcon.EDIT.create(), event -> onEdit(contact));
        editButton.addThemeVariants(ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_PRIMARY);

        Button deleteButton = new Button(VaadinIcon.TRASH.create(), event -> onDelete(contact));
        deleteButton.addThemeVariants(ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_PRIMARY);

        return new HorizontalLayout(editButton, deleteButton);
    }

    private String enumerationKey(ContactType contactType) {
        return "bms.enumeration." + ApplicationUtil.toPropertyCase(contactType.getClass()) + "." + contactType.name().toLowerCase();
    }

    private void onSave(ContactEntity bean) {
        if (editMode == EditMode.ADD) {
            items.add(bean);
        }
        grid.getDataProvider().refreshAll();
        binder.setBean(new ContactEntity());
        setEditMode(EditMode.ADD);

        updateValue();
    }

    private void onClear() {
        binder.setBean(new ContactEntity());
        setEditMode(EditMode.ADD);

        updateValue();
    }

    private void onEdit(ContactEntity contact) {
        binder.setBean(contact);
        setEditMode(EditMode.EDIT);

        updateValue();
    }

    private void onDelete(ContactEntity contact) {
        items.remove(contact);
        grid.getDataProvider().refreshAll();
        binder.setBean(new ContactEntity());

        updateValue();
    }

    private void setEditMode(EditMode editMode) {
        this.editMode = editMode;

        if (editMode == EditMode.ADD) {
            saveButton.setIcon(VaadinIcon.PLUS.create());
        } else {
            saveButton.setIcon(VaadinIcon.EDIT.create());
        }
    }

    @Override
    protected List<ContactEntity> generateModelValue() {
        return Collections.unmodifiableList(items);
    }

    @Override
    protected void setPresentationValue(List<ContactEntity> contactEntities) {
        items.clear();
        items.addAll(contactEntities);
    }
}

A quick guess is that this happens because the value is updated in-place which in turn means that valueEquals.test(newValue, oldValue) returns true which in turn leads to not firing any events and so on.

You could verify that assumption by changing generateModelValue to return a new copy instead.

Do you mean

@Override
    protected List<ContactEntity> generateModelValue() {
        return new ArrayList<>(items);
    }

by “new copy”

Basically yes, even though you might still also want to mark it as unmodifiable. The simplest way of doing that with modern Java would be return List.copyOf(items);.

I cant really understand why but it now works. Thanks <3