Converter disappears in TextChangeListener

Hi!

I stumbled upon a little problem where I want to use a wrapper to delegate TextFields in order to dynamically validate input from the user in a table.

I assign an IntegerRangeValidator and set a StringToIntegerConverter to my TextField and provide this TextField to the wrappers constructor (this happens in the createField()-method). Inside the wrapper, the TextField is set to an instance object. The converter is still there. The wrapper then overrides the TextChangeListeners textChange()-method and validates the users input.

The problem comes when the validation is at hand. The converter is suddenly gone (null) from the TextField object and there is no conversion from String to Integer, whereupon the validation process fails (because an IntegerRangeValidator naturally cannot validate a String).


Why does the converter disappear?
I did check the TextField that is being validated and it still has the same object id as the “original” TextField but, as I mentioned, the converter is now set to null.


The createField-method:

@Override
public Field<?> createField(Container container, Object itemId, Object propertyId, com.vaadin.ui.Component uiContext) {

    TextField tField = null;

    tField = (TextField) super.createField(container, itemId, propertyId, uiContext);
    tField.setBuffered(true);
    addFieldListeners(tField);

    if (propertyId.equals("age") {
        tField.setRequired(true);
        tField.setRequiredError("This field is required!");
        tField.setConverter(new StringToIntegerConverter());
        tField.addValidator(new IntegerRangeValidator("Only Integers allowed!", 1, 150));
        @SuppressWarnings({ "unchecked", "rawtypes" })
        TableDataValidatingWrapper<TextField> wField = new TableDataValidatingWrapper(tField);
        return wField;
    } else {
        return null;
    }
}


The wrapper:

public class TableDataValidatingWrapper<T> extends CustomField<T> {

    private static final long serialVersionUID = 1L;
    protected Field<T> delegate;

    public TableDataValidatingWrapper(final Field<T> delegate) {
        this.delegate = delegate;

        if (delegate instanceof TextField) {
            final TextField textField = (TextField) delegate;
            textField.setTextChangeEventMode(AbstractTextField.TextChangeEventMode.TIMEOUT);
            textField.setTextChangeTimeout(200);

            textField.setCaption("");

            textField.addTextChangeListener(new FieldEvents.TextChangeListener() {

                private static final long serialVersionUID = 1L;

                @Override
                public void textChange(FieldEvents.TextChangeEvent event) {
                    try {
                        textField.setValue(event.getText());
                        textField.validate();
                    } catch (EmptyValueException e) {
                        System.out.println("Caught exception " + e);
                    } catch (InvalidValueException e) {
                        System.out.println("Caught exception " + e);
                    } catch (Exception e) {
                        System.out.println("Caught unknown exception " + e);
                    }
                }
            });
        }
    }
    
    // ... some other overridden methods
}

I extracted all the variables that are changed…


This is the TextField
before
validation:

val$textField TextField (id=413)
changingVariables false
connectorId null
converter StringToIntegerConverter (id=470)
dataSource null
isFiringTextChangeEvent false
isListeningToPropertyEvents false
lastKnownCursorPosition 0
lastKnownTextContent null
parent null
value “” (id=468)


This is the TextField when the validation occurs:

source TextField (id=413)
changingVariables true
connectorId “55” (id=550)
converter null
dataSource IndexedContainer$IndexedContainerProperty (id=551)
isFiringTextChangeEvent true
isListeningToPropertyEvents true
lastKnownCursorPosition 2
lastKnownTextContent “61” (id=548)
parent FormLayout (id=554)
value “6” (id=553)

Hi,

There must be something that ends up calling setConverter(null) for your text field. Nothing in Vaadin should do that, except when setting a data source for which no compatible converter can be found. Are you sure that’s not what’s happening somehow? Please try adding a breakpoint in AbstractField.setConverter(Converter<T,?>) and see what happens. If you can’t find anything, please provide a minimal self-contained test application, preferably a single UI class.

Thanks for your reply!

I have debugged the application now, trying to see what happens. It was confusing and it is not entirely clear to me what is wrong.

I know this may not be the best way to explain it, but maybe the error is clear to you by just looking at the chain of events. If not, I’ll get started with a small test application…

  1. In createField() → a call to tField.setConverter(new StringToIntegerConverter()); is made
  2. In AbstractField → sets the converter to the tField object
  3. In createField() → calls TableDataValidatingWrapper wField = new TableDataValidatingWrapper(tField); →
    here I can see that the converter is still on the tField object
  4. In CustomTable.class → method bindPropertyToField(Object rowId, Object colId, Property property, Field field) is called →
    here I can see that the “field” object has a converter variable that is null, but it also holds the MT (this) object, that still has the STIConverter
  5. In the wrapper → the overridden setPropertyDataSource (AbstractField) is called →
    the converter still being present
    on the delegate

  6. In AbstractField → the overridden method setPropertyDataSource is called →
    the converter still being present on MainTables TextField (this)
  7. In AbstractField → Now, the setConverter(Class<?> datamodelType) is called and it is, in its turn, calling ConverterUtil.getConverter(getType(), datamodelType, getSession()); [b] with the arguments (String, String, null) [/b], and via a call to setConverter(Converter converter)
    the converter is actually set to null

I guess what’s happening is that the setConverter method is trying to set a converter for me, but as the field type is String and the user inputs a String it will not create any converter (as a conversion from String to String is not necessary of course) and return null, overwriting my StringToIntegerConverter.

I think I realized what is wrong. Please confirm this…

What I am trying to do is to use a String in the field and
validate
it as an Integer when the user types something in the field (it needs to be a String, because the database stores it as a String (CHAR)), but maybe this is not possible?

The only other way I could think of is to make this String an Integer, but then I would have to change the model, because the tables container is setting the properties from the model.


Is there any other way to validate a String as an Integer in a table
(i.e. the users should only be able to input Integers in this TextField), without changing the model?

Why can’t your validator validate strings? eg. tField.addValidator(new Validator(){ private IntegerRangeValidator intValidator = new new IntegerRangeValidator("Only Integers allowed!", 1, 150); public void validate(Object value) throws Validator.InvalidValueException{ try { Integer intValue = Integer.valueOf((String)value); intValidator.validate(intValue); } catch (NumberFormatException e) { throw new Validator.InvalidValueException("Not an integer"); } } });


Update:

I commented the setPropertyDataSource() call in the overridden setPropertyDataSource method in my wrapper with the result being that when entering editing mode the field has the text “null”. When I remove this text and type some numbers into the field it seems to be working as intended! However, I feel like I’m losing control of what I have done now and I’m not sure how to remove the initial “null” text =)

   @Override
    public void setPropertyDataSource(@SuppressWarnings("rawtypes") Property newDataSource) {
//delegate.setPropertyDataSource(newDataSource);
    }

Your analysis is correct. Converters are meant to convert from the presentation type (what the user sees) to the model type (what’s stored in the container/persistence layer). If the types are same, you don’t need a converter. You need to write a custom validator that accepts strings (extend AbstractValidator), check that the input can be parsed as an integer and that the result is within the given bounds - to do the latter you might want to have an IntegerRangeValidator as a member of your validator and simply delegate to it.

You have removed binding of the text field with bean field with the above code.
You can use setNullRepresentation(String nullRepresentation) to disappear
null
text from a text field.

Thanks a lot Johannes and Agata!

I will use your solutions and come back with the result!

I got it working! I created my own CustomIntegerRangeValidator and used that to validate the fields that should only validate Integers. For future reference, here’s what I did:

public class CustomIntegerRangeValidator extends AbstractValidator<String> {

    private static final long serialVersionUID = 1L;
    private IntegerRangeValidator integerRangeValidator;

    public CustomIntegerRangeValidator(String errorMessage, Integer minValue, Integer maxValue) {
        super(errorMessage);
        this.integerRangeValidator = new IntegerRangeValidator(errorMessage, minValue, maxValue);
    }

    @Override
    protected boolean isValidValue(String value) {
        try {
            Integer result = Integer.parseInt(value);
            integerRangeValidator.validate(result);
            return true;
        } catch (NumberFormatException nfe) {
            // TODO: log and handle exception
            System.out.println("Cannot be parsed as Integer: " + nfe);
            return false;
        } catch (Exception e) {
            // TODO: log and handle exception
            System.out.println("Unknown exception: " + e);
            return false;
        }

    }

    @Override
    public Class<String> getType() {
        return String.class;
    }

}