Grid with Editor can't save changed data

I implemented a vaadin grid in buffered mode with a simple client pojo, it is pretty much the same as in the vaadin examples for buffered mode. Everything is ok, beside that saving is not possible when I try to change something in the edited fields. As soon as I change something, the save button and the editor will get stuck. All other things do as expected.

@Route(value = "")
public class ClientView extends VerticalLayout {

    private final Logger LOGGER = LoggerFactory.getLogger(ClientView.class);
    private List<Client> clientArrayList = new ArrayList<>();
    private Grid<Client> grid = new Grid<>();

    public ClientView() {

        LOGGER.info("init clientview");

        // test data
        clientArrayList.add(new Client("tenant1", "client1", "name client1", "server1"));
        clientArrayList.add(new Client("tenant1", "client2", "name client2", "server1"));
        clientArrayList.add(new Client("tenant2", "client3", "name client3", "server1"));
        clientArrayList.add(new Client("tenant2", "client4", "name client4", "server1"));

        // button add new element
        final Button btnAdd = new Button("Neue Einheit");
        add(btnAdd);

        // build grid
        grid.setWidthFull();
        grid.setHeight("800px");
        final Grid.Column<Client> colClientId = grid.addColumn(Client::getClientId).setHeader("Kennung").setAutoWidth(true).setSortable(true);
        final Grid.Column<Client> colClientName = grid.addColumn(Client::getClientName).setHeader("Name der Einheit").setAutoWidth(true).setSortable(true);
        final Grid.Column<Client> colClientServerUrl = grid.addColumn(Client::getClientServerUrl).setHeader("Url zum Server").setAutoWidth(true).setSortable(true);
        grid.setItems(clientArrayList);

        // editing fields
        final Binder<Client> binder = new Binder<>(Client.class);
        final Editor<Client> editor = grid.getEditor();
        editor.setBuffered(true);
        editor.setBinder(binder);

        // all three editing fields
        final TextField tfClientId = new TextField();
        tfClientId.setMinWidth("120px");
        tfClientId.setPlaceholder("Kennung");
        binder.forField(tfClientId)
                .withValidator(value -> (value.length() > 0),"Kennung muss einen Wert enthalten").bind("clientId");
        colClientId.setEditorComponent(tfClientId);

        final TextField tfClientName = new TextField();
        tfClientName.setMinWidth("240px");
        tfClientName.setPlaceholder("Name der Einheit");
        binder.forField(tfClientName)
                .withValidator(value -> (value.length() > 0), "Name muss einen Wert enthalten").bind("clientName");
        colClientName.setEditorComponent(tfClientName);

        final TextField tfClientServerUrl = new TextField();
        tfClientServerUrl.setMinWidth("360px");
        tfClientServerUrl.setPlaceholder("http://localhost/");
        binder.forField(tfClientServerUrl).bind("clientServerUrl");
        colClientServerUrl.setEditorComponent(tfClientServerUrl);

        // add editor column
        final Collection<Button> editButtons = Collections.newSetFromMap(new WeakHashMap<>());
        final Grid.Column<Client> editorColumn = grid.addComponentColumn(client -> {
            final Button edit = new Button("Ändern");
            edit.addClassName("edit");
            edit.addClickListener(event -> {
                editor.editItem(client);
                tfClientId.focus();
                LOGGER.info("editor click");
            });
            edit.setEnabled(!editor.isOpen());
            editButtons.add(edit);
            final Button delete = new Button("Löschen");
            delete.addClassName("delete");
            delete.addClickListener(event -> {
                try {
                    delete(client);
                    reload();
                } catch (Exception e) {
                    Notification.show("Die geänderte Einheit konnte nicht gespeichert werden!", 8000, Notification.Position.TOP_END);
                }
                LOGGER.info("delete click");
            });
            editButtons.add(delete);
            final Div outsideEditor = new Div(edit, delete);
            return outsideEditor;
        });

        // editor open listener
        editor.addOpenListener(event -> {
            editButtons.stream().forEach(button -> button.setEnabled(!editor.isOpen()));
            LOGGER.info("addOpenListener");
        });

        // editor close listener
        editor.addCloseListener(event -> {
            editButtons.stream().forEach(button -> button.setEnabled(!editor.isOpen()));
            LOGGER.info("addCloseListener");
        });

        // buttons for inside editor-mode
        final Button save = new Button("Speichern", e -> {
            editor.save();
            LOGGER.info("save");
        });
        save.addClassName("save");

        final Button cancel = new Button("Abbrechen", e -> {
            editor.cancel();
            LOGGER.info("cancel");
        });
        cancel.addClassName("cancel");

        final Div insideEditor = new Div(cancel, save);
        insideEditor.add(cancel, save);
        editorColumn.setEditorComponent(insideEditor);

        grid.getElement().addEventListener("keyup", event -> editor.cancel()).setFilter("event.key === 'Escape' || event.key === 'Esc'");

        // editor save listener
        editor.addSaveListener(event -> {
            LOGGER.info("addSaveListener");
            final Client client = event.getItem();
            save(client);
            editor.refresh();
        });

        // editor cancel listener
        editor.addCancelListener(event -> {
            LOGGER.info("addCancelListener");
        });

        // add a new client into list and edit that
        btnAdd.addClickListener(event -> {
            LOGGER.info("add click listener");
            Client client = null;
            try {
                client = new Client("test", "", "", "");
                clientArrayList.add(client);
                reload();
                editor.editItem(client);
            } catch (Exception e) {
                Notification.show("Die neue Einheit konnte nicht gespeichert werden!", 8000, Notification.Position.TOP_END);
            }
        });

        // sets the max number of items to be rendered on the grid for each page
        grid.setPageSize(15);

        add(grid);

        LOGGER.info("finish clientview");
    }

    private void reload() {
        grid.setItems(clientArrayList);
    }

    private void delete(final Client client) {
        // TODO
    }

    private void save(final Client client) {
        // TODO
    }
}

The client pojo is a simple class

public class Client implements Serializable {
    private String tenantId;
    private String clientId;
    private String clientName;
    private String clientServerUrl;
	
	... all getters & setter & equals & hashcode & toString
}

I used a standard generated maven pom for vaadin with version 17.0.6 and deployed it to jetty to test on localhost.

What have I missed - I searched all available samples in the free world, but I didn’t see the hint, what I have to do??? Could you please give me a hint? Thanks!

I have the exact same issue after 4 years and with vaadin 14.

Same issue, Vaadin 14. Save, Cancel buttons (and other editor fields) don’t close after change any field and saving in buffered editor.
If i pressing Save or Cancel without editing fields, editor close as normal.

I was struggling with this problem too and finally it turned out that the EqualsAndHaschCode was not set correctly or not set at all for the object. So adding @EqualsAndHashCode(of = “my-property”) solved my problem (using lombok).

I hope this answer can helps someone of you guys.
Cheers.

Ok, I experimented with equals/hashCode.
I found that the editor works fine WITHOUT equals/hashCode methods.
When I generate methods manually or using Lombok - the editor does not close as described above.
Strange behavior…

When I generate methods manually or using Lombok

The generated methods by Lombok include the whole bean content, thus the identity of the bean changes when you edit it and thus item to be refreshed from DataProvider is not found.

If you want to use equals/hashCode as defined by Lombok, you need to extend DataProvider yourself and override getId() method there, the default implementation returns item itself as id. As you know the Bean, you can use some Bean#getId() as better invariant id of the bean.

/**
 * Gets an identifier for the given item. This identifier is used by the
 * framework to determine equality between two items.
 * <p>
 * Default is to use item itself as its own identifier. If the item has
 * {@link Object#equals(Object)} and {@link Object#hashCode()} implemented
 * in a way that it can be compared to other items, no changes are required.
 * <p>
 * <strong>Note:</strong> This method will be called often by the Framework.
 * It should not do any expensive operations.
 *
 * @param item
 *            the item to get identifier for; not {@code null}
 * @return the identifier for given item; not {@code null}
 */
default Object getId(T item) {
	Objects.requireNonNull(item, "Cannot provide an id for a null item.");
	return item;
}

Thank you! Now it works fine.

 @Override
  public int hashCode() {
    if (getId() != null) {
      return getId().hashCode();
    }
    return super.hashCode();
  }
  

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (getClass() != obj.getClass()) {
      return false;
    }
    AbstractEntity other = (AbstractEntity) obj;
 if (getId() == null || other.getId() == null) {
      return false;
    }
    return getId().equals(other.getId());
  }
}