Is it possible to use records with the CollaborationBinder?

I’m migrating from the BeanValidationBinder to the CollaborationBinder. Everything compiles, but now I’m getting strange errors that I don’t understand and I wonder if maybe the CollaborationBinder is not compatible with records?

I know I need to do some more debugging on this one, but I decided to post this sooner rather than later to just see if there was something obviously wrong I was doing.

java.util.concurrent.ExecutionException: com.vaadin.flow.data.binder.BindingException: An exception has been thrown inside binding logic for the field element [label=‘End’]
Caused by: com.vaadin.flow.data.binder.BindingException: An exception has been thrown inside binding logic for the field element [label=‘End’]
Caused by: java.lang.NullPointerException: Cannot invoke “com.vaadin.collaborationengine.CollaborationBinder$JsonHandler.serialize(Object)” because “handler” is null

public class UpdateProjectFormBinder {
    private final UpdateProjectForm form;
    private final UpdateProjectService service;
    private final ProjectRepository repository;
    private final Account account;
    private final CollaborationBinder<UpdateProjectDto> binder;

    public UpdateProjectFormBinder(UpdateProjectForm form, UpdateProjectService service,
                                   ProjectRepository repository, Account account, UserInfo userInfo) {
        this.form = form;
        this.service = service;
        this.repository = repository;
        this.account = account;
        this.binder = new CollaborationBinder<>(UpdateProjectDto.class, userInfo);
    }

    public void addBindingAndValidation() {
        binder.bindInstanceFields(form);
        binder.setStatusLabel(form.errorMessageField());
        binder.addValueChangeListener(_ -> form.updateButton().setEnabled(binder.hasChanges()));
        form.updateButton().addClickListener(_ -> handleFormSubmission(binder));
        form.resetButton().addClickListener(_ -> binder.refreshFields());

        // server side validation for 'start'
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime threeYearsLater = now.plusYears(3);
        String errorMessageStart = "Start must be after %s and before %s".formatted(now, threeYearsLater);
        binder.forField(form.start())
                .withValidator(new DateTimeRangeValidator(errorMessageStart, now, threeYearsLater))
                .bind("start");

        // server side validation for 'end'
        LocalDateTime start = form.start().getValue();
        String errorMessageEnd = "Start must be after %s and before %s".formatted(start, threeYearsLater);
        binder.forField(form.end())
                .withValidator(new DateTimeRangeValidator(errorMessageEnd, start, threeYearsLater))
                .bind("end");
    }

    private void handleFormSubmission(CollaborationBinder<UpdateProjectDto> binder) {
        try {
            UpdateProjectDto dto = binder.writeRecord();
            RichResult<Project> result = service.with(dto, account);
            result.handle(
                    _ -> handleUpdateSuccess(dto),
                    this::handleUpdateFailure
            );
        } catch (ValidationException e) {
            form.updateButton().setEnabled(true);
        } catch (IllegalArgumentException e) {
            form.errorMessageField().setText(e.getMessage());
            form.updateButton().setEnabled(true);

            form.start().setInvalid(true);
            form.end().setInvalid(true);
        }
    }

    private void handleUpdateSuccess(UpdateProjectDto dto) {
        Notification notification = Notification.show("Project updated! 🎉");
        notification.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
        notification.setPosition(Notification.Position.TOP_CENTER);
        ProjectUpdateEvent event = new ProjectUpdateEvent(account.person().name(), dto, account.id());
        String organizationId = account.organization().id();
        Broadcaster.broadcast(organizationId, event);
        UI.getCurrent().navigate(ProjectsView.class);
    }

    private void handleUpdateFailure(String error) {
        String errorText = "Schedule update failed: " + error;
        form.errorMessageField().setText(errorText);
        form.updateButton().setEnabled(true);
    }

    public void populateForm(ProjectId projectId, String topic) {
        UI.getCurrent().getPage().retrieveExtendedClientDetails(extendedClientDetails -> {
            String timezone = extendedClientDetails.getTimeZoneId();

            UpdateProjectDto dto = repository.findById(projectId)
                    .map(project -> UpdateProjectDto.from(project, timezone))
                    .orElseThrow();

            binder.bindInstanceFields(form);

            binder.setTopic(topic, () -> dto);
            form.updateButton().setEnabled(false);
        });
    }
}
1 Like

I highly doubt it’s optimized for records.

1 Like

Very good catch!
Honestly we in Flow team lost the CollaborationBinder from our radar, when we did records support for regular Binder.
Likely it simply doesn’t fully support records yet.
Let us make a ticket first and start testing it.

To follow up: Java Records support for CollaborationBinder · Issue #83 · vaadin/collaboration-kit · GitHub.

1 Like

Ah, ok. So I should just use POJOs then?

IMHO always use Pojos in a Binder for full flexibility.

1 Like

Yes, I’d recommend to use POJOs until we fix/confirm records support for CollaborationBinder.

1 Like