extending TextField to handle BigDecimal values

Hi everybody,

I need to write a simple server-side class to extend TextField to be able to handle BigDecimal values instead of Strings.
Before starting, I studied some APIs and some Vaadin sources (TextField and DateField), so I’m going to do the following:

a) extending TextField:
public class MyNumberField extends TextField;

b) overriding getType:
public Class<?> getType() {
return BigDecimal.class;
}

c) overriding setValue:
public void setValue(Object newValue) {
if ((newValue != null) && (!(newValue instanceof BigDecimal))) {
throw new IllegalArgumentException(…);
}
super.setValue(newValue);
}

d) overriding getValue to either return a BigDecimal, or null if the field is empty, or an exception (or null?) if the field does not contain a valid BigDecimal.

I’d like to know whether this path is correct.

  1. Is there anything else I should do (repainting for example)?
  2. Also, logically speaking, should getValue() return the logical value (a valid BigDecimal) or the display value (a String, representing anything)?

Thanks very much for your precious help.

Marco

  1. getValue() should return an instance that is compatible with the type returned by getType() i.e. BigDecimal
  2. AbstractTextField will still send the text value as String and get a String from changeVariables and tries to pass it to setValue() so you should implement setting BigDecimal value from a String.

Thanks Johannes,

one last question please: what’s setInternalValue() for? I mean, in my example, should it be used to store the String value, the BigDecimal one or anything else?

Thanks a lot,

Marco

A BigDecimal. I don’t think that you should store string representations of the value anywhere. If I recall correctly what the difference between setValue() and setInternalValue() in Vaadin components is, is that setValue() notifies the listeners of a valuechange while setInternalValue doesn’t. Additionally I think that setValue() usually calls setInternalValue(). Not sure when I didn’t check the code now, but look into what other components do, like the textfield.

Thank you very much!

Marco

Just thought that anyone could be interested in the code, so here’s the full class (“macaco” is another framework of mine, also providing some generic libraries: I used MNumberConverter to switch between a String and a BigDecimal).
Also, I’m using the deprecated getFormattedValue() - but I opened another thread to better understand why it is deprecated. :slight_smile:

Thanks, Marco


[...]


import com.marcozanon.macaco.conversion.MFormatConversionException;
import com.marcozanon.macaco.conversion.MNumberConverter;
import com.vaadin.ui.TextField;
import java.math.BigDecimal;
import java.util.Map;

@SuppressWarnings("serial")
public class RNumberField extends TextField {

    protected MNumberConverter numberConverter = null;

    protected BigDecimal value = null;
    protected boolean validText = true;

    /* */

    public RNumberField(MNumberConverter numberConverter) {
        super();
        //
        this.setNullRepresentation("");
        this.setNullSettingAllowed(true);
        //
        this.setWidth("10em");
        this.setStyleName("mzNumberField");
        //
        this.setNumberConverter(numberConverter);
    }

    public RNumberField(MNumberConverter numberConverter, String caption) {
        this(numberConverter);
        //
        this.setCaption(caption);
    }

    public RNumberField(MNumberConverter numberConverter, String caption, BigDecimal value) {
        this(numberConverter, caption);
        //
        this.setValue(value);
    }

    /* Number converter */

    public void setNumberConverter(MNumberConverter numberConverter) {
        if (null == numberConverter) {
            throw new IllegalArgumentException("Invalid 'numberConverter': null.");
        }
        //
        this.numberConverter = numberConverter;
    }

    public MNumberConverter getNumberConverter() {
        return this.numberConverter;
    }

    /* Events */

    public void changeVariables(Object source, Map<String, Object> variables) {
        super.changeVariables(source, variables);
        //
        if ((!this.isReadOnly()) && (variables.containsKey("text"))) {
            try {
                String text = (String)variables.get("text");
                BigDecimal value = this.getNumberConverter().getNumberFromString(text);
                this.setValue(value);
                this.setValidText(true);
            }
            catch (IllegalArgumentException exception) { // null or empty text
                this.setValue(null);
                this.setValidText(false);
            }
            catch (MFormatConversionException exception) {
                this.setValue(null);
                this.setValidText(false);
            }
        }
    }

    /* Value */

    public Class<?> getType() {
        return BigDecimal.class;
    }

    public void setValue(Object value) {
        if ((null == value) || (value instanceof BigDecimal)) {
            this.value = (BigDecimal)value;
            //
            super.setValue(this.getValue());
        }
    }

    public Object getValue() {
        return this.value;
    }

// FIXME
    @Deprecated
    protected String getFormattedValue() {
        if (null != this.getValue()) {
            return this.getNumberConverter().getStringFromNumber((BigDecimal)this.getValue());
        }
        else {
            return null;
        }
    }

    public void setValidText(boolean validValue) {
        this.validText = validText;
    }

    public boolean isValidText() {
        return this.validText;
    }

}

I skim-read this post originally, thinking that you wanted to do a client-side widget for Numeric values (or maybe I got it confused with another thread) - which is complex. I’ve done this for my project here at work, but I’m afraid it’s not shareable.

However - server-side can be done a little easier than what you’ve done here, using the
PropertyFormatter class

Essentially, it acts as a proxy between the real Property on the Field and the outside world, converting to and from strings.

It might make your life a little easier.

Cheers,

Charles.

Hi Charles, and thanks for the suggestion - but isn’t PropertyFormatter for datasources only? my extended TextField won’t be connected to datasources: would PropertyFormatter still be useful?

Also, I see that PropertyFormatter itself is going to be deprecated in Vaadin7 in favour of Converters - so I suppose I’ll have to change the code again. :slight_smile:

Thanks very much for your help,
Marco

Hi,

There is a better way to achieve this, extending the class CustomField. This way you will not have to represent your BigDecimal value as a String.

Here is a working implementation. This class can be adapted to edit any numeric type (just replace BigDecimal with the type).

It also formats the number according to the system Locale. You can also input the formatted number, and it will parse it correctly.


package br.com.personalsoft.engine.aplicacao.vaadin.controles;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

import com.vaadin.data.util.converter.Converter.ConversionException;
import com.vaadin.ui.Component;
import com.vaadin.ui.CustomField;
import com.vaadin.ui.TextField;

public class BigDecimalFieldPS extends CustomField<BigDecimal> {

	private static final long serialVersionUID = -5150605101121179296L;
	private TextField content;
	private NumberFormat numberFormat;
	private int recursions = 0;

	public BigDecimalFieldPS() {
		DecimalFormat numberFormat = (DecimalFormat) DecimalFormat.getNumberInstance(Locale
				.getDefault());
		numberFormat.setParseBigDecimal(true);

		this.numberFormat = numberFormat;

		init();
	}

	public BigDecimalFieldPS(NumberFormat numberFormat) {
		this.numberFormat = numberFormat;

		init();
	}

	private void init() {
		content = new TextField();
		content.setNullSettingAllowed(true);
		content.setNullRepresentation("");
		content.addValueChangeListener(new ValueChangeListener() {
			private static final long serialVersionUID = -4994220730867294055L;

			@Override
			public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) {
				if (recursions == 0) {
					recursions++;
					try {
						if (event.getProperty().getValue() == null)
							BigDecimalFieldPS.this.setValue(null);
						else {
							try {
								BigDecimal bigDecimal = (BigDecimal) numberFormat.parse((String) event
										.getProperty().getValue());
								BigDecimalFieldPS.this.setValue(bigDecimal);
							} catch (com.vaadin.data.Property.ReadOnlyException | ConversionException
									| ParseException e) {
							}
							if (BigDecimalFieldPS.this.getValue() == null)
								content.setValue(null);
							else
								content.setValue(numberFormat.format(BigDecimalFieldPS.this.getValue()));
						}
					} finally {
						recursions--;
					}
				}
			}
		});

		setImmediate(true);
	}

	@Override
	protected Component initContent() {
		return content;
	}

	@Override
	public Class<? extends BigDecimal> getType() {
		return BigDecimal.class;
	}

	@Override
	protected void setInternalValue(BigDecimal newValue) {
		super.setInternalValue(newValue);

		recursions++;
		try {
			if (newValue == null)
				content.setValue(null);
			else
				content.setValue(numberFormat.format(newValue));
		} finally {
			recursions--;
		}
	}

	@Override
	public void setImmediate(boolean immediate) {
		super.setImmediate(immediate);
		content.setImmediate(immediate);
	}

	@Override
	public void setSizeFull() {
		super.setSizeFull();
		content.setSizeFull();
	}

	@Override
	public void setSizeUndefined() {
		super.setSizeUndefined();
		content.setSizeUndefined();
	}

	@Override
	public void setWidth(String width) {
		super.setWidth(width);
		content.setWidth(width);
	}

	@Override
	public void setHeight(String height) {
		super.setHeight(height);
		content.setHeight(height);
	}

	public void selectAll() {
		content.selectAll();
	}

}

This could probably be made a little shorter using the class FieldWrapper in the CustomField add-on (or in Vaadin 7).

Hi, i didn´t find FieldWrapper class in Vaadin 7.

Hi all,

just to let anyone know that I rewrote the same code in a much better way in Vaadin 7, using Converters (which were not available in Vaadin 6).
I’m not posting the code because it is very straightforward - basically it’s the same as the examples found in the wiki.

Thanks,

Marco

I find that Converters are not doing the function of field validation correctly. One problem is that the exception text included in a ConversionException is not shown to the user, so they can a built-in meaningless exception like “Could not convert value to BigDecimal”.

It seems like a Validator should be used on the field to first ensure the input data is reasonable before creating the BigDecimal object. By trying to create a BigDecimal using unvalidated input, you get the odd converter exception without any better reasoning shown to the user. In general, creating an object by initializing it with unvalidated input is not a good choice when a validator has been set to check this first, but unfortunately the conversion takes place before the validation, which is just not right, so the validator is never called to give the user a nice error for invalid input (the purpose of the validator in the first place).

Clearly, it should validate the input using the presentation object (String), and only attempt to create the model object (Integer) if it passes that test. In general, validators are added to fields, but converters are more general purpose, so it makes sense that the user error should come from the field’s validator and not from the common model converter exception.

First, run the validator if set. If it passes, then convert that input into the model object.

I’m not sure that I totally agree; personally, I feel comfortable with converters converting Presentation ↔ Model, and validators Validating the model object. We have a fair few validators that validate the Model (date ranges, payment amounts) - it would seem daft to have to do the conversion twice.

However, I do agree that the there should be a way in Vaadin core to customize the field error message based on the message of the ConversionException. Most of the work there is done in AbstractField, making it tricky to customize - and anyway, most of the relevant methods are private making them difficult to customize.

Another possibility would be to have a separate PresentationValidator that " validates the Presentation value - e.g.

Field → PresentationValidator → Converter → Validator → DataSource

(This implies a separate interface, but it needn’t be. Perhaps two chains of Validators - current and AbstractField#addPresentationValidator(Validator) would do)

Cheers,
Charles.

Hey Charles, I guess if they fix so the converter exception message appears to the user, it will help resolve this issue for me. I can at least get rid of the user-unfriendly error message like “Could not convert value to EsfIPAddr”… or BigDecimal or anything other class name that is leaked to the UI but has no meaning to the user.

I am all for the presentation validation, though, if so many people rely on using the model objects to be invalidly constructed before they the test to see if the model object is valid. I really think there is no good reason to say that unvalidated user input should be passed into the constructor of a model object, though. This is the definition of an attack vector, applying unvalidated data directly into your business logic. Most model objects have no concept of holding an “invalid” value, only valid values, so if the data entered by the user is not correct, attempting to create the model based on it is not correct.

I am more than happy to switch to PresentationValidators, but alas there is no such thing.

Hi David,
I’m using vaadin 7.1.12 and I found a solution for this problem:

The solution is really simple and is explained in the javadoc of the setConversionError method of the AbstractField API:

So if you want to show your custom conversion-error-message on a TextField you can use this code:

textField.addConverter(new MyConverter());
textField.setConversionError("{1}");

In this way, if a new ConversionException(“My Custom error message”) is thrown by the MyConverter object, the user can see the message “My Custom error message” on the error-indicator.

I hope I have been helpful.

Are there any new on what @David Wall wrote?
I also think that generic error messages on invalid user input are not really helpful for production, eg:
“Could not convert value to BigDecimal”.

It should be able to customize conversation errors (at least to define a global conversation error) by type of the validator.

With reference to the post above me: is there also any placeholder for the actual input value of the textfield? {0} represents the type, and {1} the whole converter error.