-
FEATURED COMPONENTS
First time here? Check out the FAQ!
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:
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;
}
}
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.
Asked: 2022-09-21 18:59:19 +0800
Seen: 8 times
Last updated: Oct 20 '22
MVVM Validator: class not found ? [closed]
Conditional evaluation component in mvvm
Create Crud of a User that has a collection of books
Children Binding : UiException Callable only in the event listener
Attributes in macro was not updated after notifyChange
Problem binding values to a composite component
How to access static member field of a class in zul without zscript
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 )editThank 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