In search of a GWT validation example... where art thou?
Asked Answered
H

2

10

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:

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 &lt;input&gt; 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.

Hydroquinone answered 3/2, 2012 at 2:6 Comment(5)
Reviewing now - you might want to remove UiResources.INSTANCE.style().error(), as this won't compile without your own UiResources class. The DateComparable superclass probably ought to go too, and generatePriceColumn could probably use some additional simplification, for isInEditMode and the DisplayMode class.Dolores
UiResources is my own ClientBundle. DateComparable is long gone. All dates are ISO String. We convert on server side so we can convert appropriate time zone.Hydroquinone
Is it just me or is validation in GWT a stupidly complex problem, when it should be really simple? We're now at the stage where Google is allowing the open-source community to take control of GWT and there is still no way to easily validate the input on a field... or am I missing something?Blooded
@slugmandrew, if anything GWT has taught me, it's patience. I've had to refine my approach and it took several iterations before I had something worth sharing.Hydroquinone
@ChrisPhillipson Agreed! After a decent amount of research I think I'll wait until later on to do validation as it's not critical at this stage of development. Thanks for the code though :)Blooded
H
3

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.

/**
 * A {@link Column} implementation that encapsulates a {@link ValidatableInputCell}.
 * Performs JSR-303 validation on a field (or nested field) of the type.
 * @author cphillipson
 *
 * @param <T> the type
 * @param <O> the owning type of the field to be validated; in many cases T may have only primitive or wrapper types, therefore O will be the same type as T
 */
public abstract class AbstractValidatableColumn<T, O> extends Column<T, String> {

/**
 * Preferred constructor.
 * Allows for definition of tabIndex but uses a default for the input cell size.
 * @param tabIndex the <code>tabindex</code> attribute's value for the input cell
 * @param table the grid instance
 */
public AbstractValidatableColumn(int tabIndex, final AbstractHasData<T> table) {
    this(App.INSTANCE.defaultValidatableInputCellSize(), tabIndex, table);
}

/**
 * Overloaded constructor.
 * Allows for definition of tabIndex and allows for an override to the default for the input cell size.
 * @param inputSize the <code>size</code> attribute's value for the input cell
 * @param tabIndex the <code>tabindex</code> attribute's value for the input cell
 * @param table the grid instance
 */
public AbstractValidatableColumn(int inputSize, int tabIndex, final AbstractHasData<T> table) {
    super(new ValidatableInputCell());
    getCell().setInputSize(inputSize);
    getCell().setTabIndex(tabIndex);
    init(table);
}

// meat and potatoes
private void init(final AbstractHasData<T> table) {
    setFieldUpdater(new FieldUpdater<T, String>() {
        @Override
        public void update(int index, T dto, String newValue) {
            final ConversionResult cr = attemptValueConversion(newValue);
            final ValidationData viewData = getCell().getViewData(dto);
            if (cr.wasConvertedSuccessfully()) {
                final Set<ConstraintViolation<O>> violations = validate(cr.getValue());
                if (!violations.isEmpty()) {  // invalid
                    final StringBuffer errorMessage = new StringBuffer();
                    for (final ConstraintViolation<O> constraintViolation : violations) {
                        errorMessage.append(constraintViolation.getMessage());
                    }
                    viewData.setInvalid(true);
                    getCell().setErrorMessage(errorMessage.toString());
                } else {  // valid
                    viewData.setInvalid(false);
                    getCell().setErrorMessage("");
                    doUpdate(index, dto, newValue);
                }
            } else { // conversion exception
                viewData.setInvalid(true);
                getCell().setErrorMessage(UiMessages.INSTANCE.improper_input_format());
            }
        }
    });
}


/**
 * Attempts conversion of a String value into another type
 * Instances are responsible for the conversion logic as it may vary from type to type
 * @param value a String value to be converted into an owning class's property type
 * @return a ConversionResult
 */
protected abstract ConversionResult attemptValueConversion(String value);

@Override
public ValidatableInputCell getCell() {
    return (ValidatableInputCell) super.getCell();
}

/**
 * Template method for updating a field (or nested field) value within a DTO
 * @param index the row index for the instance of the DTO within the grid
 * @param dto the object whose field we wish to update
 * @param value the new value that will be set on a field (or nested field) of the DTO
 */
protected abstract void doUpdate(int index, T dto, String value);

/**
 * Template method for specifying the property name of an owning class
 * @return the field name of the owning class whose value is to be updated
 */
protected abstract String getPropertyName();

/**
 * Template method for specifying the owning class
 * @return the owning class of the field whose value is to be updated
 */
protected abstract Class<O> getPropertyOwner();

/**
 * Validates a value against a set of constraints (i.e., JSR-303 annotations on a field)
 * @param newValue the value to be validated
 * @return the set of constraint violations induced by an inappropriate value
 */
protected Set<ConstraintViolation<O>> validate(Object newValue) {
    final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
    final Set<ConstraintViolation<O>> violations = validator.validateValue(getPropertyOwner(), getPropertyName(), newValue);
    return violations;
}

}

/**
 * <p>A cell that will update its styling and provide feedback upon a validation constraint violation.</p>
 * <p>Implementation based upon GWT Showcase's <a href="http://gwt.google.com/samples/Showcase/Showcase.html#!CwCellValidation">Cell Validation</a> example.</p>
 * @author cphillipson
 *
 */
 public class ValidatableInputCell extends AbstractInputCell<String, ValidatableInputCell.ValidationData> {

interface Template extends SafeHtmlTemplates {
    @Template("<input type=\"text\" value=\"{0}\" size=\"{1}\" style=\"{2}\" tabindex=\"{3}\"></input>")
    SafeHtml input(String value, String width, SafeStyles color, String tabIndex);
}

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 = App.INSTANCE.defaultValidatableInputCellSize();

/**
 * Specifies the width, in characters, of the &lt;input&gt; element contained within this cell
 */
private int inputSize = DEFAULT_INPUT_SIZE;

/**
 * Specifies the tab index for this cell
 */
private int tabIndex = -1;

public ValidatableInputCell() {
    // since onBrowserEvent method is overridden, we must register all events that handled in overridden method impl
    super("change", "keyup", "focus", "blur", "keydown");
    if (template == null) {
        template = GWT.create(Template.class);
    }
}

public void setInputSize(int inputSize) {
    this.inputSize = inputSize;
}

public void setTabIndex(int index) {
    tabIndex = index;
}

public void setErrorMessage(String errorMessage) {
    this.errorMessage = SafeHtmlUtils.fromSafeConstant(errorMessage).asString();
}

@Override
public void onBrowserEvent(Context context, Element parent, String value,
        NativeEvent event, ValueUpdater<String> valueUpdater) {
    super.onBrowserEvent(context, parent, value, event, valueUpdater);

    final InputElement input = (InputElement) getInputElement(parent);
    final Object key = context.getKey();
    final String eventType = event.getType();

    if ("keyup".equals(eventType)) {

        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);
    }
}

@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 ? App.INSTANCE.invalidCellInputTextColor() : App.INSTANCE.pendingCellInputTextColor() : App.INSTANCE.defaultCellInputTextColor();
    final String backgroundColor = pendingValue != null ? invalid ? App.INSTANCE.invalidCellInputTextBackgroundColor() : App.INSTANCE.pendingCellInputTextBackgroundColor() : App.INSTANCE.defaultCellInputTextBackgroundColor();
    final SafeStyles style = SafeStylesUtils.fromTrustedString("color: " + color + "; background-color: " + backgroundColor + ";");
    sb.append(template.input(pendingValue != null ? pendingValue : value, String.valueOf(inputSize), style, String.valueOf(tabIndex)));
}

/*
@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 onEnterKeyDown(Context context, Element parent, String value,
        NativeEvent event, ValueUpdater<String> valueUpdater) {
    // do nothing
}

@Override
protected void finishEditing(Element parent, String value, Object key,
        ValueUpdater<String> valueUpdater) {

    // Update the value updater, which updates the field updater.
    if (valueUpdater != null) {
        valueUpdater.update(value);
    }

    final InputElement input = (InputElement) getInputElement(parent);
    final ValidationData viewData = getViewData(key);

    /*
     * 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 ? App.INSTANCE.invalidCellInputTextColor() : App.INSTANCE.pendingCellInputTextColor() : App.INSTANCE.defaultCellInputTextColor();
    final String backgroundColor = pendingValue != null ? invalid ? App.INSTANCE.invalidCellInputTextBackgroundColor() : App.INSTANCE.pendingCellInputTextBackgroundColor() : App.INSTANCE.defaultCellInputTextBackgroundColor();
    input.getStyle().setColor(color);
    input.getStyle().setBackgroundColor(backgroundColor);

    if (invalid) {
        final DecoratedPopupPanel errorMessagePopup = new DecoratedPopupPanel(true);
        final FlowPanel messageContainer = new FlowPanel();
        messageContainer.setWidth(App.INSTANCE.errorMessagePopupWidth());
        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() +5;
        final int top = parent.getAbsoluteTop() - 5;

        errorMessagePopup.setPopupPositionAndShow(new PopupPanel.PositionCallback() {
            @Override
            public void setPosition(int offsetWidth, int offsetHeight) {
                errorMessagePopup.setPopupPosition(left, top);
            }
        });
    }

}

/**
 * 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;
    }
}

}

Variants of AVC might look like...

/**
 * A variant of {@link AbstractValidatableColumn} that works with {@link BigDecimal} field types.
 * @author cphillipson
 *
 * @param <T> the type
 * @param <O> the owning type of the field to be validated; in many cases T may have only primitive or wrapper types, therefore O will be the same type as T
 */
public abstract class BigDecimalValidatableColumn<T, O> extends AbstractValidatableColumn<T, O> {

public BigDecimalValidatableColumn(int tabIndex, AbstractHasData table) {
    super(tabIndex, table);
}

public BigDecimalValidatableColumn(int inputSize, int tabIndex, final AbstractHasData<T> table) {
    super(inputSize, tabIndex, table);
}

@Override
protected ConversionResult attemptValueConversion(String value) {
    return doConversion(value);
}

public static ConversionResult doConversion(String value) {
    ConversionResult result = null;
    try {
        final Double dblValue = Double.valueOf(value);
        final BigDecimal convertedValue = BigDecimal.valueOf(dblValue);
        result = ConversionResult.converted(convertedValue);
    } catch (final NumberFormatException nfe) {
        result = ConversionResult.not_converted();
    }
    return result;
}
}

A ConversionResult is consulted by a Column's fieldUpdater. Here's what it looks like...

/**
 * An attempted conversion result.
 * Returns both the converted value (from <code>String</code>) and whether or not the conversion was successful.
 * E.g., if you tried to convert from a <code>String</code> to a <code>Number</code>, in the failure case this would result in a <code>NumberFormatException</code>.
 * On failure, the boolean would be false and the value would be null.
 * On success, the boolean would be true and the value would be of the type needed to continue validation against a set of constraints
 * @author cphillipson
 *
 */
public  class ConversionResult {
private Object value;
private boolean convertedSuccessfully;

private ConversionResult () {}

/**
 * Use this method when a successful conversion is made to return a result
 * @param value the convertedValue
 * @return the result of the conversion containing the converted value and a success flag
 */
public static ConversionResult converted(Object value) {
    final ConversionResult result = new ConversionResult();
    result.setConvertedSuccessfully(true);
    result.setValue(value);
    return result;
}

/**
 * Use this method when an attempt to convert a String value failed
 * @return the result of a failed conversion
 */
public static ConversionResult not_converted() {
    return new ConversionResult();
}

private void setValue(Object value) {
    this.value = value;
}

public Object getValue() {
    return value;
}

private void setConvertedSuccessfully(boolean flag) {
    convertedSuccessfully = flag;
}

public boolean wasConvertedSuccessfully() {
    return convertedSuccessfully;
}
}

Finally, here's how you might spec a column in a grid

new BigDecimalValidatableColumn<EnergyOfferDTO, OfferPriceMwPairDTO>(nextTabIndex(), getGrid()) {

            @Override
            public String getValue(EnergyOfferDTO energyOffer) {
                return obtainPriceValue(colIndex, energyOffer, false);
            }

            @Override
            public void doUpdate(int index, EnergyOfferDTO energyOffer, String value) {
                if (value != null && !value.isEmpty()) {
                    // 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);
                    final List<OfferPriceMwPairDTO> offerPriceCurve = energyOffer.getCurve();
                    final OfferPriceMwPairDTO offerPriceMwPairDTO = offerPriceCurve.get(colIndex);
                    if (offerPriceMwPairDTO == null) {  // we have a new price value
                        newOfferPriceMwPair.setPrice(price);
                        offerPriceCurve.add(newOfferPriceMwPair);
                    } else {
                        offerPriceMwPairDTO.setPrice(price);
                    }

                }
            }

            @Override
            protected String getPropertyName() {
                return "price";
            }

            @Override
            protected Class<OfferPriceMwPairDTO> getPropertyOwner() {
                return OfferPriceMwPairDTO.class;
            }

        };

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!

Hydroquinone answered 16/5, 2012 at 16:36 Comment(0)
D
1

It isn't clear to me why a HasCell is being returned from generatePriceColumn, since that can't be consumed by practically anything, except CompositeCell - 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 no ValueUpdater instance to work with, since the FieldUpdater 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 a CellTable subclass, which

  1. Lets any code that uses it change how the rest of the celltable is set up,
  2. Doesn't really act like a CellTable method - other column-focused methods actually add the column instead of returning it
  3. Might lock you in to always use CellTable (since that is what you subclass) while that method will work perfectly well otherwise with AbstractCellTable subclasses like DataTable, a newer CellTable

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:

public abstract class AbstractValidatableColumn<T> extends Column<T, String> {

  public AbstractValidatableColumn(int inputSize, final AbstractCellTable<T> table) {
    super(new ValidatableInputCell());
    ((ValidatableInputCell) getCell()).setInputSize(inputSize);

    setFieldUpdater(new FieldUpdater<T, String>() {
      public void update(int index, T dto, String value) {
        final Set<ConstraintViolation<T>> violations = validate(dto);
        final ValidationData viewData = getCell().getViewData(dto);
        if (!violations.isEmpty()) {  // invalid
          final StringBuffer errorMessage = new StringBuffer();
          for (final ConstraintViolation<T> constraintViolation : violations) {
            errorMessage.append(constraintViolation.getMessage());
          }
          viewData.setInvalid(true);
          getCell().setErrorMessage(errorMessage.toString());
          table.redraw();
        } else {  // valid
          viewData.setInvalid(false);
          getCell().setErrorMessage(null);
          doUpdate(index, dto, value);
        }
      }
    });
  }

  @Override
  public ValidatableInputCell getCell() {
    return (ValidatableInputCell)super.getCell();
  }

  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;
  }
}

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.

Dolores answered 27/2, 2012 at 5:3 Comment(15)
Colin, thanks! You've given me quite a bit to chew on. I'll need to digest this and come back. To answer some of your points...Hydroquinone
ToggleableGrid is a subclass I designed to help with dual mode displays (our users want grids to be rendered either read-only or input only). I have since swapped CellTable for DataGrid. I suppose I could have opted for composition over inheritance and hidden the impl used underneath. generatePriceColumn and generateMwColumn are used to build up a composite column. Since HasCell is an interface I presumed this was the way to go about crafting individual Columns to add to a composite column. I named that method, addPriceMwColumn. Ultimately, I need to validate column cells individually.Hydroquinone
"Build up from a simpler configuration that works, so only one thing can go wrong at each stage." Believe me... it's been a tough road! IMO, the Cell family of widgetry is some of the most complex API to understand in GWT, but also perhaps some of the most powerful. Trying to craft solutions based on simple examples available around the net has been a research project, and I certainly appreciate the time you've taken to look at a sliver of my codebase (albeit a chunky one).Hydroquinone
Finally, I understand the validation feature is "experimental", but what other options are there?Hydroquinone
Nothing I've tried is working. Currently, looking into retreading with gwt-vl (a sourceforge.net lib). When I have something working I'll post back. Otherwise, if anyone else has any ideas I'd welcome them.Hydroquinone
Sorry, been a busy wk for me, and I can't seem to find the github link you were going to put up. Cells are complex because their goal is to provide efficiency at the cost of easy of use, validation is hard because JRS303 is designed to run in a JVM with all its reflection goodness - and all validation has to be redone on the server (never trust the client!) - even the example you point to uses a timer to wait for the server to return the validation error, if any.Dolores
GWT-VL was last updated in late 2009, so I'm not sure it'll work well with existing code. I've also heard decent things about code.google.com/p/gwt-validation, most recent commits are about two weeks old. My post was meant to point out that there are other things (beyond cells and validation) that are preventing your code from working - stripping it down to individual pieces of functionality will start to show those so you can build the complexity back up again.Dolores
Colin, thanks. So far, I've looked into GWT's built-in capabilities, the googlecode validation library you mentioned, and gwt-vl. In fact I started with the googlecode lib first, but I was never able to get past compilation problems w.r.t. dependencies (people have reported similar problems). I feel like I'm not far off using GWT's built-in JSR-303 support. I'm aware I should rely primarily on validations server side. gwt-vl looks interesting primarily because there's no reliance on reflection and validation logic is wrapped up in client and server side validator classes.Hydroquinone
Trying to package my existing code base has been a project in and of itself. I will try to work on this tomorrow. If you're still inclined to help I'd very much appreciate it. Providing a working canonical example of validation on a DataGrid with composite columns would be worth sharing back to the community (I think).Hydroquinone
I'm actually about to take a week off, and unplug myself (hence the busy week), but I can normally be found in ##gwt on freenode, ready and willing to play with other people's problems. Hit me up there, and update this as you boil your problem down further. But I would still like to point out that even with handwritten validation (just some simple range checks or something) your code isn't going to work as you've posted it, since you validate before you assign new values. That, and the missing FieldUpdater are the two biggest missing pieces before you even get to the validation libs.Dolores
Even at that stage, with manual validation working, it will be difficult to suggest it as a canonical example - some fields only make sense to validate when other fields are done as well, so all fields should be checked. GWT cells also don't make it terribly easy to draw error messages in a non-obtrusive way, and alerts are very annoying as a way of pointing out mistakes. That said, in the general approach, the FieldUpdater given to a Column is the place for post-blur validation.Dolores
A fun lib I wrote about a year ago: github.com/niloc132/celltable-tools This provides some generator-assisted boilerplate generation, and could be made to hook in to some other validation mechanism as well (it already builds FieldUpdaters as needed, could be also instructed to refer to some delegate to validate those changes). Use the 2.4.0 branch, or it wont work with 2.4 - might be fun to build validation into this in some abstract way.Dolores
Colin, I am relinquishing my bounty to you. Though I still haven't a working solution, I think there's enough here that will eventually lead to an answer.Hydroquinone
Colin, and anyone else, I finally got the "go ahead" and the time to release some code. You can check it out here: github.com/fastnsilver/gwt-ui-experimentsHydroquinone
Great - I'll play (and update this ans) once I'm back on a real computer.Dolores

© 2022 - 2024 — McMap. All rights reserved.