0

Revalidate Form via FormBinding after manipulating List on FormProxy

asked 2022-09-21 18:59:19 +0800

BlueOne gravatar image BlueOne
1 1

updated 2022-09-21 23:23:16 +0800

We're having difficulties with validation messages from an invalid object within a List<t> that are still being displayed after removal of the invalid object from said list via a command.

The setup is the following:

  • Using the Wizard-Templates that have been created by ZK
  • FormBinding, PropertyBinding
  • Custom Validators (Robert helped us a lot with these back then)

The Problem:
We have a list of objects that can be manipulated by the user through an "add" and a "remove" button. The objects inside the list are validated when clicking the "next"-Button and after focus is lost on the input field. Now when the user adds a new object and clicks "next" we see the error messages - as expected. On removal of the invalid object the error messages are not removed though. The vmsgs holder is not updated, which is why when we add a new object to the list again, we will immediately see error messages on the fields.

The question is: How do we resolve this? Our current idea is to force a validation to happen after the command has executed. But how? Maybe someone can help us with this..


Code:

IndexViewModel:
FormBinding before "Next" button is clicked using custom Validator "FormBeanValidator2"

<borderlayout viewModel="@id('vm') 
            @init('org.lwl.ozg.lbh.viewmodel.LaabIndexViewmodel')"
        validationMessages="@id('vmsgs')"
        onBookmarkChange="@command('gotoStep', stepId=event.bookmark)">

    <wizard wizardModel="@init(vm.wizardModel)" wrapperTemplate="formWrapper" showStepBar="true">
        <template name="formWrapper">
            <div form="@id('indexForm') 
                            @load(vm.antrag) 
                            @save(vm.antrag, before={vm.wizardModel.nextCommand, 
                                  vm.wizardModel.getStoreCommandName() })
                @validator('org.lwl.ozg.base.validator.FormBeanValidator2', 
                            prefix='p_', groups=wizardModel.currentStep.validationGroups)"
                    sclass="lwl-ozg-antragsDiv" align="left">
                <sh:apply template="wizardContent"
                        antrag="@init(indexForm)" savedAntrag="@init(vm.antrag)"/>
            </div>
        </template>             
    </wizard>
</borderlayout>

FormBeanValidator2:

public class FormBeanValidator2 extends AbstractValidator {

    @Override
    public void validate(ValidationContext ctx) {
        log.debug("Validation on Command "+ctx.getCommand());
        if (WizardViewModel.getStoreCommandName().equals(ctx.getCommand())) {
            // No validation executed in order for the command to execute and catch not yet saved inputs as well
            return;
        }

        var prefix = (String) ctx.getValidatorArg("prefix");
        var value = ctx.getProperty().getValue();
        var groups = ofNullable((Class<?>[]) ctx.getValidatorArg("groups"))
                .orElse(new Class<?>[]{});
        var beanValidator = BeanValidations.getValidator();

        var violations = beanValidator.validate(value, groups);

        //clear existing validation messages for returned keys
        violations.stream()
                .map(cv -> prefix + cv.getPropertyPath())
                .distinct()
                .forEach(key -> clearExistingValidationMessages(ctx, key));

        violations.stream()
                //optional sorting
                //.sorted(Comparator.comparing(cv -> cv.getPropertyPath().toString()))
                .forEach(cv -> {
                    String key = prefix + cv.getPropertyPath();
                    addInvalidMessage(ctx, key, cv.getMessage());
                });
    }

    public static void clearExistingValidationMessages(ValidationContext ctx, String key) {
        var validationMessages = ((AnnotateBinder) ctx.getBindContext().getBinder()).getValidationMessages();
        if (validationMessages != null) {
            validationMessages.clearKeyMessages(key);
        }
    }

}

step.zul
The validation messages should have updated / disappeared after "removeTeilaufgabe" has executed.

<zk xmlns:n="native" xmlns:sh="shadow" xmlns:ca="client/attribute"
    xmlns:h="xhtml">

    <div id="laabantragView"
        validationMessages="@id('vmsgs') @init(vmsgs)"
        viewModel="@id('vm') @init('org.lwl.ozg.lbh.viewmodel.LaabAntragViewmodel', antrag=antrag)"
        width="100%">

    <formRow type="errorlist" listItems="@ref(vmsgs)" />

    <div ca:aria-live="polite">
        <forEach items="@load(indexForm.teilaufgaben)">
            <if test="@load(!(indexForm.teilaufgaben.indexOf(each) eq 0))">
                <separator />
            </if>
                <div id="${ 'teilaufgabe_div'.concat(indexForm.teilaufgaben.indexOf(each)) }" sclass="lwl-ozg-dyn-section"
                    style="margin-bottom: 1rem; border-bottom: 1px solid #e5e5e5">

                    <n:h3>
                        ${c:l('laab.antrag.beschaeftigungssituation.teilaufgabe.listtitle')}
                        (${indexForm.teilaufgaben.indexOf(each)+1})
                    </n:h3>
                    <formRow type="textbox"
                        fieldId="${ 'teilaufgabe'.concat(indexForm.teilaufgaben.indexOf(each)) }"
                        label="${c:l('laab.antrag.beschaeftigungssituation.teilaufgabe')}"
                        maxlength="100" 
                        value="@ref(each.teilaufgabe)"
                        key="${c:cat3('p_teilaufgaben[',indexForm.teilaufgaben.indexOf(each),'].teilaufgabe')}"
                        groups="@ref(vm.liveValidationGroup)"
                        error="@ref(vmsgs[ 'p_teilaufgaben['.concat(indexForm.teilaufgaben.indexOf(each)).concat('].teilaufgabe')])" />

                    <formRow fieldId="${ 'teilaufgabeGewichtung'.concat(indexForm.teilaufgaben.indexOf(each)) }"
                        type="number"
                        label="${c:l('laab.antrag.beschaeftigungssituation.gewichtung')}"
                        value="@ref(each.gewichtung)"
                        changeEvent="calculateTeilaufgabenGesamt"
                        maxlength="3" groups="@ref(vm.liveValidationGroup)"
                        key="${c:cat3('p_teilaufgaben[',indexForm.teilaufgaben.indexOf(each),'].gewichtung')}"
                        error="@ref(vmsgs[ 'p_teilaufgaben['.concat(indexForm.teilaufgaben.indexOf(each)).concat('].gewichtung')])" />

                    <formRow type="textbox"
                        fieldId="${ 'teilaufgabeProbleme'.concat(indexForm.teilaufgaben.indexOf(each)) }"
                        label="${c:l('laab.antrag.beschaeftigungssituation.probleme')}"
                        maxlength="200" value="@ref(each.probleme)"
                        key="${c:cat3('p_teilaufgaben[',indexForm.teilaufgaben.indexOf(each),'].probleme')}"
                        groups="@ref(vm.liveValidationGroup)"
                        error="@ref(vmsgs[ 'p_teilaufgaben['.concat(indexForm.teilaufgaben.indexOf(each)).concat('].probleme')])" />

                    <!-- Teilaufgabe entfernen -->
                    <if
                        test="@load(!(indexForm.teilaufgaben.indexOf(each) eq 0))">
                        <n:br />
                        <button
                            ca:aria-label="${'Teilaufgabe Nr.'.concat(indexForm.teilaufgaben.indexOf(each)+1).concat(' entfernen')}"
                            sclass="z-align-left lwl-ozg-mainButton z-button"
                            label="${c:l('laab.antrag.beschaeftigungssituation.teilaufgabe.entfernen')}"
                            onClick="@command('removeTeilaufgabe', index=indexForm.teilaufgaben.indexOf(each))" />
                        <n:br />
                    </if>
                </div>
            </forEach>
        </div>
        <!-- Teilaufgabe hinzufügen -->
        <button sclass="z-align-left lwl-ozg-mainButton z-button"
            label="${c:l('laab.antrag.beschaeftigungssituation.teilaufgabe.hinzufuegen')}"
            onClick="@command('addEmptyTeilaufgabe')" />
    </div>
</zk>

Commands in LaabAntragViewModel:

@Command
@NotifyChange("antrag")
public void addEmptyTeilaufgabe() {
    if(antrag.getTeilaufgaben().size() < 10) { // maximal 10 Teilaufgaben
        antrag.getTeilaufgaben().add(new LaabTeilaufgabeDTO());
        // Fix-2097 - Binder verliert Liste nach dem Hochladen, muss daher manuell notified werden
        BindUtils.postNotifyChange(null, null, antrag, "teilaufgaben");
    }
}

@Command
@NotifyChange({"antrag", "teilaufgabenGewichtungGesamt"})
public void removeTeilaufgabe(@BindingParam("index") int index) {
    List<LaabTeilaufgabeDTO> teilaufgaben = antrag.getTeilaufgaben();
    if(antrag.getTeilaufgaben().size() > 0 && teilaufgaben.size() >= index+1 ) {
        teilaufgaben.remove(index);
        calculateTeilaufgabenGesamt();
        // Fix-2097 - Binder verliert Liste nach dem Hochladen, muss daher manuell notified werden
        BindUtils.postNotifyChange(null, null, antrag, "teilaufgaben");
    }
}

Property in "antrag" (LaabAntragDTO):

@Getter(onMethod_ = { @Valid })
private List<LaabTeilaufgabeDTO> teilaufgaben;

LaabTeilaufgabeDTO:

@Data
public class LaabTeilaufgabeDTO implements ValidationObject {

    @Getter(onMethod_ = { @NotBlank(groups = { SituationValidation.class,
            AntragLiveValidation.class }, message = "{laab.antrag.beschaeftigungssituation.teilaufgabe.beschreibung.notNull}"),
            @Size(groups = AntragValidation.class, max = 100, message = "{ozg.antrag.sizeError.100}") })
    private String teilaufgabe;

    @Getter(onMethod_ = { @NotNull(groups = { SituationValidation.class,
            AntragLiveValidation.class }, message = "{laab.antrag.beschaeftigungssituation.teilaufgabe.gewichtung.notNull}"),
            @Min(groups = { SituationValidation.class,
                    AntragLiveValidation.class }, value = 1, message = "{laab.antrag.beschaeftigungssituation.teilaufgabe.gewichtung.min}"),
            @Max(groups = { SituationValidation.class,
                    AntragLiveValidation.class }, value = 100, message = "{laab.antrag.beschaeftigungssituation.teilaufgabe.gewichtung.max}") })
    private Integer gewichtung;

    @Getter(onMethod_ = { @NotBlank(groups = { SituationValidation.class,
            AntragLiveValidation.class }, message = "{laab.antrag.beschaeftigungssituation.teilaufgabe.probleme.notNull}"),
            @Size(groups = AntragValidation.class, max = 200, message = "{ozg.antrag.sizeError.200}") })
    private String probleme;

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

    @Override
    public int hashCode() {
        final int prime = 29;
        int result = 1;
        result = prime * result + ((null == teilaufgabe) ? 0 : teilaufgabe.hashCode());
        result = prime * result + ((null == gewichtung) ? 0 : gewichtung.hashCode());
        result = prime * result + ((null == probleme) ? 0 : probleme.hashCode());
        return result;
    }

    @Transient
    @DependsOn({ "teilaufgabe", "gewichtung", "probleme" })
    public boolean isValid() {
        boolean valid = (StringUtils.hasText(this.getTeilaufgabe())
                && this.getTeilaufgabe().length() <= 100) && (null != this.getGewichtung())
                && (StringUtils.hasText(this.getProbleme())
                        && this.getProbleme().length() <= 200);
        return valid;
    }
}

LiveBeanValidator for PropertyBinding (updates vmsgs when a validation-error gets resolved):

public class LiveBeanValidator extends AbstractValidator {

    @Override
    public void validate(ValidationContext ctx) {
        var key = (String) ctx.getValidatorArg("key");
        // too simple doesn't work with `obj['prop']`
//      var base = ctx.getProperty().getBase();
//      var property = ctx.getProperty().getProperty();
        var valueReference = getValueReference(ctx);
        var property = (String) valueReference.getProperty();
        var base = valueReference.getBase();
        var groups = ofNullable((Class<?>[]) ctx.getValidatorArg("groups")).orElse(new Class<?>[] {});
        var beanValidator = BeanValidations.getValidator();
        var value = ctx.getProperty().getValue();

        clearExistingValidationMessages(ctx, key);

        if (ctx.getCommand() == null) { // in the context of form validation (triggered by command)
            beanValidator.validateValue(base.getClass(), property, value, groups).forEach(cv -> {
                ValidationMessages validationMessages = getValidationMessages(ctx);
                Binding binding = ctx.getBindContext().getBinding();
                if (binding instanceof PropertyBinding) {
                    String attr = ((PropertyBinding) binding).getFieldName();
                    if (attr != null && validationMessages != null) {
                        doLater(ctx.getBindContext().getComponent(), () -> {
                            validationMessages.addMessages(ctx.getBindContext().getComponent(), attr, key,
                                    new String[] { cv.getMessage() });
                            BindUtils.postNotifyChange(validationMessages, ".");
                        });
                    }
                }
            });
        }

    }

    private static final AtomicInteger seq = new AtomicInteger(0);

    private void doLater(Component comp, Runnable task) {
        final String uniqueEventName = "onDoLater_" + seq.getAndIncrement();
        comp.addEventListener(-1, uniqueEventName, new EventListener<Event>() {
            @Override
            public void onEvent(Event event) throws Exception {
                comp.removeEventListener(uniqueEventName, this);
                task.run();
            }
        });
        Events.postEvent(-10, new Event(uniqueEventName, comp));
    }

    private ValueReference getValueReference(ValidationContext ctx) {
        var bindContext = ctx.getBindContext();
        var binder = bindContext.getBinder();
        return binder.getEvaluatorX().getValueReference(bindContext, bindContext.getComponent(),
                ((SavePropertyBindingImpl) bindContext.getBinding()).getProperty());
    }

    public static void clearExistingValidationMessages(ValidationContext ctx, String key) {
        ValidationMessages validationMessages = getValidationMessages(ctx);
        if (validationMessages != null) {
            validationMessages.clearKeyMessages(key);
        }
    }

    private static ValidationMessages getValidationMessages(ValidationContext ctx) {
        var validationMessages = ((AnnotateBinder) ctx.getBindContext().getBinder()).getValidationMessages();
        return validationMessages;
    }
}
delete flag offensive retag edit

Comments

I was trying to replicate the error without succes. Can you post an example a bit more clean?

ImanolRP ( 2022-09-23 14:55:50 +0800 )edit

Thank you for trying but I guess I won't. Doesn't matter anyways because: We've come to the result that it is simply not possible to have the vmsgs cleared automatically.

Solution is in my answer below.

BlueOne ( 2022-10-20 21:31:06 +0800 )edit

1 Answer

Sort by » oldest newest most voted
0

answered 2022-10-20 21:34:17 +0800

BlueOne gravatar image BlueOne
1 1

We've come to the conclusion that what we wanted to achieve is not possible.

The workaround for the messages being cleared is the following adjustment to the LaabAntragViewModel::removeTeilaufgabe().

@Command
@NotifyChange({"antrag", "teilaufgabenGewichtungGesamt"})
public void removeTeilaufgabe(@BindingParam("index") int index, @BindingParam("vmsgs") ValidationMessages vmsgs) {
    List<LaabTeilaufgabeDTO> teilaufgaben = antrag.getTeilaufgaben();
    if(antrag.getTeilaufgaben().size() > 0 && teilaufgaben.size() >= index+1 ) {
        teilaufgaben.remove(index);
        calculateTeilaufgabenGesamt();
        // Fix-2097 - Binder verliert Liste nach dem Hochladen, muss daher manuell notified werden
        BindUtils.postNotifyChange(null, null, antrag, "teilaufgaben");

        String keyPrefix = "p_teilaufgaben[%d].".formatted(index);
        vmsgs.clearKeyMessages(keyPrefix + "teilaufgabe");
        vmsgs.clearKeyMessages(keyPrefix + "gewichtung");
        vmsgs.clearKeyMessages(keyPrefix + "probleme");
        BindUtils.postNotifyChange(vmsgs, "*");
    }
}

Note the additional function parameters, the clearKeyMessages calls and the postNotifyChange.

link publish delete flag offensive edit
Your answer
Please start posting your answer anonymously - your answer will be saved within the current session and published after you log in or create a new account. Please try to give a substantial answer, for discussions, please use comments and please do remember to vote (after you log in)!

[hide preview]

Question tools

Follow

RSS

Stats

Asked: 2022-09-21 18:59:19 +0800

Seen: 8 times

Last updated: Oct 20 '22

Support Options
  • Email Support
  • Training
  • Consulting
  • Outsourcing
Learn More