vaadin / flow

Vaadin Flow is a Java framework binding Vaadin web components to Java. This is part of Vaadin 10+.
Apache License 2.0
618 stars 167 forks source link

Make Binder.validate() work in buffered mode with bean-level validations. #15701

Open mvysny opened 1 year ago

mvysny commented 1 year ago

Describe your motivation

Vaadin 14.9.4.

I am using binder in a buffered mode:

I have to call binder.validate() otherwise the HasValue.errorMessage wouldn't get populated.

The problem is that binder.validate() fails in buffered mode with

java.lang.IllegalStateException: Cannot validate binder: bean level validators have been configured but no bean is currently set
    at com.vaadin.flow.data.binder.Binder.validate(Binder.java:2479) ~[flow-data-9.0.8.jar:9.0.8]
    at com.vaadin.flow.data.binder.Binder.validate(Binder.java:2463) ~[flow-data-9.0.8.jar:9.0.8]

Describe the solution you'd like

Essentially a reopen of https://github.com/vaadin/flow/issues/13393

I don't think that Binder.setValidationStatusHandler(...) would be helpful in this case:

mvysny commented 1 year ago

Workaround: clear Binder.validators via reflection, then call validate(), then populate Binder.validators back. I'll prototype this solution then I'll post a demo code.

mvysny commented 1 year ago

Example code of a buffered binder which:

  1. calls all validators,
  2. writes stuff to the bean, but only if all validators pass
  3. If any validator fails, appropriate fields are marked red
  4. If any validator fails, a notification error message is shown
public class BinderUtils {
    /**
     * Writes values to given bean, but only if all validations pass. If any of the validation fails, a nice validation error message
     * is shown to the user.
     * @param binder the binder, not null.
     * @param bean the bean to write the values to, not null.
     * @param <T> the type of the bean
     * @return true if everything went okay and the new values are written to the bean, false if not.
     */
    public static <T> boolean writeBeanIfValid(@NotNull Binder<T> binder, @NotNull T bean) {
        // temporarily remove bean-level validators. Workaround for https://github.com/vaadin/flow/issues/15701
        final List<Validator<? super T>> beanValidators = getBeanValidators(binder);
        setBeanValidators(binder, new ArrayList<>());
        final boolean isFieldLevelValidationsOk;
        try {
            isFieldLevelValidationsOk = binder.validate().isOk();
        } finally {
            setBeanValidators(binder, beanValidators);
        }

        String errorMessage = null;
        try {
            if (isFieldLevelValidationsOk) {
                binder.writeBean(bean);
                // all OK, return true.
                return true;
            }
        } catch (ValidationException ex) {
            if (!ex.getBeanValidationErrors().isEmpty()) {
                final ValidationResult err = ex.getBeanValidationErrors().get(0);
                errorMessage = err.getErrorMessage();
            }
        }
        if (errorMessage == null || errorMessage.isBlank()) {
            errorMessage = tr(I18NCommon.ERRORS_IN_FORM);
        }
        MessageNotification.warningMessage(errorMessage);
        return false;
    }

    @SuppressWarnings("unchecked")
    @NotNull
    private static <T> List<Validator<? super T>> getBeanValidators(@NotNull Binder<T> binder) {
        try {
            final Field f = Binder.class.getDeclaredField("validators");
            f.setAccessible(true);
            return new ArrayList<>((List<Validator<? super T>>) f.get(binder));
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    private static <T> void setBeanValidators(@NotNull Binder<T> binder, @NotNull List<Validator<? super T>> validators) {
        try {
            final Field f = Binder.class.getDeclaredField("validators");
            f.setAccessible(true);
            f.set(binder, new ArrayList<>(validators));
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
}