As a follow-up to Why are there no decent examples of CompositeCell in use within a CellTable?
I am trying to add-on JSR-303 validation support. I have followed Koma's config advice here: How to install gwt-validation with gwt-2.4.0 (Note: I'm using GWT 2.4's built-in validation, not GWT-Validation).
Again, in order to get some re-use I crafted a pair of classes, ValidatableInputCell and AbstractValidatableColumn. I got inspiration for them from:
- http://code.google.com/p/google-web-toolkit/source/browse/trunk/samples/validation/src/main/java/com/google/gwt/sample/validation/client/ValidationView.java?r=10642
- http://gwt.google.com/samples/Showcase/Showcase.html#!CwCellValidation
Let's have a look at 'em...
public class ValidatableInputCell extends AbstractInputCell<String, ValidatableInputCell.ValidationData> {
interface Template extends SafeHtmlTemplates {
@Template("<input type=\"text\" value=\"{0}\" size=\"{1}\" style=\"{2}\" tabindex=\"-1\"></input>")
SafeHtml input(String value, String width, SafeStyles color);
}
private static Template template;
/**
* The error message to be displayed as a pop-up near the field
*/
private String errorMessage;
private static final int DEFAULT_INPUT_SIZE = 15;
/**
* Specifies the width, in characters, of the <input> element contained within this cell
*/
private int inputSize = DEFAULT_INPUT_SIZE;
public ValidatableInputCell() {
super("change", "keyup");
if (template == null) {
template = GWT.create(Template.class);
}
}
public void setInputSize(int inputSize) {
this.inputSize = inputSize;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = SafeHtmlUtils.htmlEscape(errorMessage);
}
@Override
public void onBrowserEvent(Context context, Element parent, String value,
NativeEvent event, ValueUpdater<String> valueUpdater) {
super.onBrowserEvent(context, parent, value, event, valueUpdater);
// Ignore events that don't target the input.
final InputElement input = (InputElement) getInputElement(parent);
final Element target = event.getEventTarget().cast();
if (!input.isOrHasChild(target)) {
return;
}
final Object key = context.getKey();
final String eventType = event.getType();
if ("change".equals(eventType)) {
finishEditing(parent, value, key, valueUpdater);
} else if ("keyup".equals(eventType)) {
// Mark cell as containing a pending change
input.getStyle().setColor("blue");
ValidationData viewData = getViewData(key);
// Save the new value in the view data.
if (viewData == null) {
viewData = new ValidationData();
setViewData(key, viewData);
}
final String newValue = input.getValue();
viewData.setValue(newValue);
finishEditing(parent, newValue, key, valueUpdater);
// Update the value updater, which updates the field updater.
if (valueUpdater != null) {
valueUpdater.update(newValue);
}
}
}
@Override
public void render(Context context, String value, SafeHtmlBuilder sb) {
// Get the view data.
final Object key = context.getKey();
ValidationData viewData = getViewData(key);
if (viewData != null && viewData.getValue().equals(value)) {
// Clear the view data if the value is the same as the current value.
clearViewData(key);
viewData = null;
}
/*
* If viewData is null, just paint the contents black. If it is non-null,
* show the pending value and paint the contents red if they are known to
* be invalid.
*/
final String pendingValue = viewData == null ? null : viewData.getValue();
final boolean invalid = viewData == null ? false : viewData.isInvalid();
final String color = pendingValue != null ? invalid ? "red" : "blue" : "black";
final SafeStyles safeColor = SafeStylesUtils.fromTrustedString("color: " + color + ";");
sb.append(template.input(pendingValue != null ? pendingValue : value, String.valueOf(inputSize), safeColor));
}
@Override
protected void onEnterKeyDown(Context context, Element parent, String value,
NativeEvent event, ValueUpdater<String> valueUpdater) {
final Element target = event.getEventTarget().cast();
if (getInputElement(parent).isOrHasChild(target)) {
finishEditing(parent, value, context.getKey(), valueUpdater);
} else {
super.onEnterKeyDown(context, parent, value, event, valueUpdater);
}
}
@Override
protected void finishEditing(Element parent, String value, Object key,
ValueUpdater<String> valueUpdater) {
final ValidationData viewData = getViewData(key);
final String pendingValue = viewData == null ? null : viewData.getValue();
final boolean invalid = viewData == null ? false : viewData.isInvalid();
if (invalid) {
final DecoratedPopupPanel errorMessagePopup = new DecoratedPopupPanel(true);
final VerticalPanel messageContainer = new VerticalPanel();
messageContainer.setWidth("200px");
final Label messageTxt = new Label(errorMessage, true);
messageTxt.setStyleName(UiResources.INSTANCE.style().error());
messageContainer.add(messageTxt);
errorMessagePopup.setWidget(messageContainer);
// Reposition the popup relative to input field
final int left = parent.getAbsoluteRight() + 25;
final int top = parent.getAbsoluteTop();
errorMessagePopup.setPopupPositionAndShow(new PopupPanel.PositionCallback() {
@Override
public void setPosition(int offsetWidth, int offsetHeight) {
errorMessagePopup.setPopupPosition(left, top);
}
});
}
// XXX let user continue or force focus until value is valid? for now the former is implemented
super.finishEditing(parent, pendingValue, key, valueUpdater);
}
/**
* The ViewData used by {@link ValidatableInputCell}.
*/
static class ValidationData {
private boolean invalid;
private String value;
public String getValue() {
return value;
}
public boolean isInvalid() {
return invalid;
}
public void setInvalid(boolean invalid) {
this.invalid = invalid;
}
public void setValue(String value) {
this.value = value;
}
}
}
and
public abstract class AbstractValidatableColumn<T> implements HasCell<T, String> {
private ValidatableInputCell cell = new ValidatableInputCell();
private CellTable<T> table;
public AbstractValidatableColumn(int inputSize, CellTable<T> table) {
cell.setInputSize(inputSize);
this.table = table;
}
@Override
public Cell<String> getCell() {
return cell;
}
@Override
public FieldUpdater<T, String> getFieldUpdater() {
return new FieldUpdater<T, String>() {
@Override
public void update(int index, T dto, String value) {
final Set<ConstraintViolation<T>> violations = validate(dto);
final ValidationData viewData = cell.getViewData(dto);
if (!violations.isEmpty()) { // invalid
final StringBuffer errorMessage = new StringBuffer();
for (final ConstraintViolation<T> constraintViolation : violations) {
errorMessage.append(constraintViolation.getMessage());
}
viewData.setInvalid(true);
cell.setErrorMessage(errorMessage.toString());
table.redraw();
} else { // valid
viewData.setInvalid(false);
cell.setErrorMessage(null);
doUpdate(index, dto, value);
}
}
};
}
protected abstract void doUpdate(int index, T dto, String value);
protected Set<ConstraintViolation<T>> validate(T dto) {
final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
final Set<ConstraintViolation<T>> violations = validator.validate(dto);
return violations;
}
}
I use the AbstractValidatableColumn like so...
protected HasCell<ReserveOfferDTO, String> generatePriceColumn(DisplayMode currentDisplayMode) {
HasCell<ReserveOfferDTO, String> priceColumn;
if (isInEditMode(currentDisplayMode)) {
priceColumn = new AbstractValidatableColumn<ReserveOfferDTO>(5, this) {
@Override
public String getValue(ReserveOfferDTO reserveOffer) {
return obtainPriceValue(reserveOffer);
}
@Override
protected void doUpdate(int index, ReserveOfferDTO reserveOffer, String value) {
// number format exceptions should be caught and handled by event bus's handle method
final double valueAsDouble = NumberFormat.getDecimalFormat().parse(value);
final BigDecimal price = BigDecimal.valueOf(valueAsDouble);
reserveOffer.setPrice(price);
}
};
} else {
priceColumn = new Column<ReserveOfferDTO, String>(new TextCell()) {
@Override
public String getValue(ReserveOfferDTO reserveOffer) {
return obtainPriceValue(reserveOffer);
}
};
}
return priceColumn;
}
Oh! And here's the DTO with JSR-303 annotations...
public class ReserveOfferDTO extends DateComparable implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull @Digits(integer=6, fraction=2)
private BigDecimal price;
@NotNull @Digits(integer=6, fraction=2)
private BigDecimal fixedMW;
private String dispatchStatus;
private String resourceName;
private String dateTime;
private String marketType;
private String productType;
...
}
Dropping a breakpoint in onBrowserEvent I would expect to have the validation trigger on each key stroke and/or after cell loses focus. It never gets invoked. I can enter whatever I like in the cell. Any clues as to an approach to fix?
My early thoughts... a) AbstractValidatableColumn#getFieldUpdater is never getting invoked and b) the logic in either ValidatableInputCell#onBrowserEvent or ValidatableInputCell#render needs an overhaul.
Ultimately, I'd like to see a popup appearing next to each cell that violates a constraint, and of course see that the appropriate coloring is applied.
UiResources.INSTANCE.style().error()
, as this won't compile without your own UiResources class. TheDateComparable
superclass probably ought to go too, andgeneratePriceColumn
could probably use some additional simplification, forisInEditMode
and theDisplayMode
class. – Dolores