'vaadin cross form validation fails as bean not bound

<vaadin.version>22.0.7</vaadin.version>

I have built a vaadin edit form with an attached Save button. When the user clicks 'Save' I want to validate the form and then save the validated data to the bean.

This works as expected.

The problem comes when I went to add in cross field validation. In this case I want to validate that a start/end date pair are in the correct order.

The problem is that when I add the form validator, vaadin starts throwing exceptions when I call validate.

@Override
    public void bindFields(CrudFieldBinder<DiscountCode> binder)
    {
        binder.forField(this.discountCode).asRequired("Please enter the unique Discount Code.").bind(
                DiscountCode::getDiscountCode,
                DiscountCode::setDiscountCode);

        binder.forField(this.startDate).asRequired("Please enter a Start Date.").bind(DiscountCode::getStartDate,
                DiscountCode::setStartDate);

        binder.forField(this.endDate).asRequired("Please enter an End Date.")
                .bind(DiscountCode::getEndDate,
                        DiscountCode::setEndDate);

        binder.withValidator(new DateRangeValidator());
        
    }

I've tried a few variations all with the same result. Here is the latest iteration:

    protected void saveEdits(E currentEntity) {
        try
        {
            binder.writeBean(currentEntity);
        }
        catch (ValidationException e)
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        // this line throws the below error
        BinderValidationStatus<E> status = binder.validate();
 }

The call to writeBean runs without error but the call to binder.validate() fails 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]
... EditorFormLayout.saveEdits(EditorFormLayout.java:92) ~[classes/:?]

This seems to suggest that form level validation only works if you make a call to setBean, however my understanding is the call to setBean will result in the form autosaving rather than waiting for the user to click the save button.



Solution 1:[1]

I don't believe there is a supported method for doing this.

I've raised a feature request here: https://github.com/vaadin/platform/issues/2868

Here is my hack that relies on overloading the binder and making the validation method think a bean is bound.


import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.BinderValidationStatus;


public class MyBinder<E> extends Binder<E>
{
    public MyBinder()
    {

    }

    /**
     * Apparently when doing cross field validation by calling
     * binder.withValidator there is no way to the list of errors back
     * due to the following issue.
     *   https://github.com/vaadin/platform/issues/2868
     *   
     *   Which essentially says that unless you are using setBean you can't
     *   do cross form validation.
     *   
     *   This code caches the bean sent to the binder when readBean is called
     *   and then uses that bean to fake a bound bean during validation.
     *   
     */
    private boolean validating = false;
    private E bean;

    @Override
    public void readBean(E bean)
    {
        this.bean = bean;
        super.readBean(bean);
    }

    @Override
    public void setBean(E bean)
    {
        throw new RuntimeException("The MyBinder only works with read/writeBean");
    }

    @Override
    public E getBean()
    {
        if (validating)
        {
            return bean;
        }
        /// this call should always return null as setBean hasn't been
        // called but we do this try to reduce the likelihood of this overload
        // causing problems if someone accicentially uses this class
        // when using setBean.
        return super.getBean();
    }

    @Override
    public BinderValidationStatus<E> validate()
    {
        try
        {
            // force getBean to return a bean during validation.
            validating = true;
            return super.validate();
        }
        finally
        {
            validating = false;
        }

    }

}```

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Brett Sutton