Cross/weak validation in NatTable by Dirk Fauth

3 minute read

Sometimes it is necessary to implement logic that validates one value against another in the same object. Having a fromDate and a toDate in your model object, where the toDate can’t be before the fromDate, is a good example for that. This is also called “cross validation”. As in NatTable values will not be committed to the model if the validation fails, a mechanism for “weak validation” is needed. This means that the value will be committed to the model, even if the validation fails.

To implement such a “cross/weak validation” in NatTable, you should first consider to not use an IDataValidator. Instead move the validation logic to some static class/method, so it can be used in various places. For an example let’s use the following EventData class and static isEventDataValid(EventData) method.

public class EventData {
	private String title;
	private String description;
	private String where;
	private Date fromDate;
	private Date toDate;
	...
	//getters and setters
}

public static boolean isEventDataValid(EventData event) {
	return (event.getFromDate().before(event.getToDate()));
}

public static boolean isEventDataValid(Date fromDate, Date toDate) {
	return fromDate.before(toDate);
}

This validation method will be called by a custom IConfigLabelAccumulator that should apply a special label in case the validation of the cell content fails.

public class CrossValidationLabelAccumulator extends AbstractOverrider {

	private IRowDataProvider bodyDataProvider;

	public CrossValidationLabelAccumulator(IRowDataProvider bodyDataProvider) {
		this.bodyDataProvider = bodyDataProvider;
	}

	@Override
	public void accumulateConfigLabels(
			LabelStack configLabels,
			int columnPosition,
			int rowPosition) {

		//get the row object out of the dataprovider
		EventData rowObject = this.bodyDataProvider.getRowObject(rowPosition);

		//in column 3 and 4 there are the values that
		//are cross validated
		if (columnPosition == 3 || columnPosition == 4) {
			if (!isEventDataValid(rowObject)) {
				configLabels.addLabel("INVALID");
			}
		}
	}
}

To render the cell accordingly register a style against that label, so invalid data is visible to the user.

IStyle validationErrorStyle = new Style();
validationErrorStyle.setAttributeValue(
		CellStyleAttributes.BACKGROUND_COLOR,
		GUIHelper.COLOR_RED);
validationErrorStyle.setAttributeValue(
		CellStyleAttributes.FOREGROUND_COLOR,
		GUIHelper.COLOR_WHITE);

configRegistry.registerConfigAttribute(
		CellConfigAttributes.CELL_STYLE,
		validationErrorStyle,
		DisplayMode.NORMAL,
		"INVALID");

Doing the above steps, invalid data will be rendered differently, while the model is updated with that invalid data. As the model now contains invalid data you need to do additional checks, if the model data needs to be used afterwards. That’s why it is useful to extract the validation logic to a static class/method.

The approach described above has of course the downsite that you can’t tell the user why the current entered value is not valid. With NatTable 1.0.0 you will be able to work around this. One possible solution will be to keep the validator configuration or create an IDataValidator that uses the validation logic from the static class/method created before and register it against the cross validation cell labels.

class EventDataValidator extends DataValidator {
	private IRowDataProvider bodyDataProvider;

	EventDataValidator(
		IRowDataProvider bodyDataProvider) {
		this.bodyDataProvider = bodyDataProvider;
	}

	@Override
	public boolean validate(
		int columnIndex, int rowIndex, Object newValue) {

		//get the row object out of the dataprovider
		EventData rowObject = this.bodyDataProvider.getRowObject(rowIndex);

		//as the object itself is not yet updated, we need
		//to validate against the given new value
		Date fromDate = rowObject.getFromDate();
		Date toDate = rowObject.getToDate();
		if (columnIndex == 3) {
			fromDate = (Date) newValue;
		}
		else if (columnIndex == 4) {
			toDate = (Date) newValue;
		}
		if (!isEventDataValid(fromDate, toDate)) {
			throw new ValidationFailedException("fromDate is not before toDate");
		}
		return true;
	}
}

Then register a DialogErrorHandling that is configured to support cross validation.

configRegistry.registerConfigAttribute(
	EditConfigAttributes.VALIDATION_ERROR_HANDLER,
	new DialogErrorHandling(true),
	DisplayMode.EDIT,
	CrossValidationGridExample.DATE_LABEL);

If a validation error occurs, this will open a dialog showing the validation error message, giving the opportunity to changing, discarding or committing the value.

You can find the complete example named CrossValidationGridExample in the current master branch of the NatTable. For more information on how to clone the Git repository and setup the development environment, have a look here.

Updated: