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.
It isn't clear to me why a
HasCell
is being returned fromgeneratePriceColumn
, since that can't be consumed by practically anything, exceptCompositeCell
- maybe you are trying to wrap up all of this in a bigger cell. Before asking, you might consider in the future breaking down your example further, the problem might become clear.I changed the 'column' creating code so it actually returned a Column - this meant changing AbstractValidatableColumn to extend Column. Along the way, I noticed that you were overriding getFieldUpdater, without modifying the underlying field, which will prevent other pieces of Column's internals from working, as they look for that field. As a result of this, my initial experiments were getting to
ValidatableInputCell.onBrowserEvent
's keyup case correctly, but there was noValueUpdater
instance to work with, since theFieldUpdater
was null in Column.At that point, the validation logic, which I didn't wire up, is being invoked - As of GWT 2.4.0, this is still tagged in every class as "EXPERIMENTAL", and as not for use in production code, so I've given it a pass until 2.5.0 or so, when the rough edges have been rounded off. If I were to continue though (and if you have issues), I'd start with the project at http://code.google.com/p/google-web-toolkit/source/browse/trunk/samples/validation/ - get that to work, and then steal details until mine worked as well.
A few other observations:
Don't extend classes to add functionality, except where you expect/allow any consumers of that class to use it as they would the subclass. Hard to tell in this case, but
generatePriceColumn
appears to be on aCellTable
subclass, whichCellTable
method - other column-focused methods actually add the column instead of returning itCellTable
(since that is what you subclass) while that method will work perfectly well otherwise withAbstractCellTable
subclasses likeDataTable
, a newerCellTable
In this case, I'd either change the method to be
addPriceColumn(...)
, and have it use a Column and add it to the list, or keep it out, either as a subclassed column, or entirely on its own as a utility method. My final AbstractValidationColumn ended up not having much reason to be a subclass at all, effectively just a convenience constructor for Column:The FieldUpdater is the interesting part here, that is what should be focused on, and leave as many other pieces to be reused as possible. This will allow any cell to run its own ValueUpdater when it is ready - perhaps not as frequently as you like, but it'll generally make things easier to use more quickly. Make a FieldUpdater impl that wraps another FieldUpdater, which can be specific to whatever field is being changed in that case.
I think another bug is lurking here, and might show up if you test the column/fieldupdater on its own - the new value isn't applied to the bean of type T until after the validation has run, so the bean is being validated with the old valid value.
doUpdate
needs to be called sooner.And finally, I'd encourage you to keep your example simpler as you go - some brain-dead 'is it null' check for validation, and a simple straightforward CellTable setup would let you see that the column itself only has the validation stuff working if the
Column.fieldUpdater
field is non null. Build up from a simpler configuration that works, so only one thing can go wrong at each stage.Coming up for air here. I finally figured out a solution! I opted to employ the GWT Validation library, see http://code.google.com/p/gwt-validation/wiki/GWT_Validation_2_0 (the code below is known to work with the 2.1 SNAPSHOT).
The trick when performing validation for a cell is to call validateValue rather than validate (the latter triggers validation for all entity's fields). As well, all input cell values are String, and converted to the respective entity field type before being validated. (Even works for nested entity fields).
Here's the revised impls for both AbstractValidatableColumn (AVC) and ValidatableInputCell.
Variants of AVC might look like...
A ConversionResult is consulted by a Column's fieldUpdater. Here's what it looks like...
Finally, here's how you might spec a column in a grid
Note the DTO's in the example above have their fields JSR-303 constraint annotated (e.g., with @Digits, @NotNull).
The above took some doing, and it may just be the most comprehensive solution out on the net at the moment. Enjoy!