ValueChangeNotifier not triggering

Hey all, i’ve been trying to get this working for a while, and have been reading all the docs, but everything i’ve read seems to not work any more.

I have a BeanContainer backed table (editable), and I want to add a generated column to that table which takes the values from a couple of the fields, and caculates a total and displays. I want this to happen immediately on the user changing a value on any of the fields which are part of the calculation.

I have a field factory, and I set the affected fields to be immediate, and from the examples i’ve seen all over, I needed to add a listener to that field from that generated column. The listener gets added without any issue, it just never actually gets called when a value changes. I’ve added another listener to test in the field factory, and that one always gets called when a value is changed.

My code for the generated column is as follows:

productTable.addGeneratedColumn("probableExposure", (source, itemId, columnId) -> {
            Label label = new Label();

            Property qty = source.getItem(itemId).getItemProperty("estimatedQuantity");
            Property rebate = source.getItem(itemId).getItemProperty("rebate");


            if (qty instanceof Property.ValueChangeNotifier) {
                Property.ValueChangeNotifier qtyNotifier = (Property.ValueChangeNotifier) qty;
                qtyNotifier.addValueChangeListener(event -> {
                    label.setValue(calcPropableExposure((Integer) qty.getValue(), (rebate.getValue() != null) ? ((BigDecimal) rebate.getValue()).doubleValue() : null).toString());
                });
            }
            if (rebate instanceof Property.ValueChangeNotifier) {
                Property.ValueChangeNotifier rebateNotifier = (Property.ValueChangeNotifier) rebate;
                rebateNotifier.addValueChangeListener(event -> {
                    label.setValue(calcPropableExposure((Integer) qty.getValue(), (rebate.getValue() != null) ? ((BigDecimal) rebate.getValue()).doubleValue() : null).toString());
                });
            }
            return label;
        });

Any help would be greatly appreciated.
-Adam

Hello Adam,

I don’t see anything specific that would not work in the provided column generator. Could you provide more details on how you create the table and perhaps the field factory implementation.

-Matti

Sure thing. Here is my field factory implementation and table.

CustomFieldFactory productTableFieldFactory = new CustomFieldFactory() {
            @Override
            public Field createField(final Container container, final Object itemId, final Object propertyId, Component uiContext) {
                Field<?> field;

                if(propertyId.equals("estimatedQuantityUom") || propertyId.equals("rebateUom")) {
                    field = new NativeSelect();
                    ((NativeSelect)field).setContainerDataSource(uomContainer);
                    ((NativeSelect)field).setImmediate(true);
                    ((NativeSelect)field).setNullSelectionAllowed(false);
                    ((NativeSelect)field).setItemCaptionPropertyId("uomTypeDescription");
                    ((NativeSelect)field).addValueChangeListener(event -> {
                        FieldComparison fieldComparison = new FieldComparison(propertyId, itemId);
                        if (!productTableFields.containsKey(fieldComparison)) {
                            productTableFields.put(fieldComparison, field);
                        }
                    });
                    ((NativeSelect)field).addAttachListener(event -> {
                        FieldComparison fieldComparison = new FieldComparison(propertyId, itemId);
                        if (productTableFields.containsKey(fieldComparison)) {
                            ((NativeSelect)field).select(productTableFields.get(fieldComparison).getValue());

                        }
                    });
                }
                else
                    field = super.createField(container, itemId, propertyId, uiContext);

                if (propertyId.equals("ID")) {
                    field.setReadOnly(true);
                    ((TextField) field).setConverter(new StringToPlainIntegerConverter());
                }
                if (propertyId.equals("product.productName")) {
                    field.setReadOnly(true);
                }
                if (propertyId.equals("product.productNumber")) {
                    field.setReadOnly(true);
                }
                if (propertyId.equals("productGrouping.groupingDescription")) {
                    field.setReadOnly(true);
                }
                if (propertyId.equals("productGrouping.groupingCD")) {
                    field.setReadOnly(true);
                }
                if (propertyId.equals("fixedPrice")) {
                    ((TextField) field).setImmediate(true);
                    field.addValueChangeListener(event -> {
                        if (event.getProperty().getValue() != null) {
                            if (!container.getContainerProperty(itemId, "fixedRate").isReadOnly()) {
                                container.getContainerProperty(itemId, "fixedRate").setReadOnly(true);
                            }
                        } else {
                            if (container.getContainerProperty(itemId, "fixedRate").isReadOnly()) {
                                container.getContainerProperty(itemId, "fixedRate").setReadOnly(false);
                            }
                        }
                    });
                }
                if (propertyId.equals("fixedRate")) {
                    ((TextField) field).setImmediate(true);
                    field.addValueChangeListener(event -> {
                        if (event.getProperty().getValue() != null) {
                            if (!container.getContainerProperty(itemId, "fixedPrice").isReadOnly()) {
                                container.getContainerProperty(itemId, "fixedPrice").setReadOnly(true);
                            }
                        } else {
                            if (container.getContainerProperty(itemId, "fixedPrice").isReadOnly()) {
                                container.getContainerProperty(itemId, "fixedPrice").setReadOnly(false);
                            }
                        }
                    });
                }
                if (propertyId.equals("estimatedQuantity")) {
                    ((TextField) field).setImmediate(true);
                    field.addValueChangeListener(event -> {
                        return;
                    });
                }
                if (propertyId.equals("rebate")) {
                    ((TextField) field).setImmediate(true);
                    field.addValueChangeListener(event -> {
                        return;
                    });
                }
                field.setBuffered(true);
                field.addValueChangeListener(event -> {
                    if (field.isModified() && !productTableFields.containsKey(new FieldComparison(propertyId, itemId))) {
                        productTableFields.put(new FieldComparison(propertyId, itemId), field);
                    }
                });
                field.addAttachListener(event -> {
                    FieldComparison fieldComparison = new FieldComparison(propertyId, itemId);
                    if(productTableFields.containsKey(fieldComparison)) {
                        if(field instanceof TextField)
                            ((TextField) field).setValue((String) productTableFields.get(fieldComparison).getValue());
                        if(field instanceof NativeSelect)
                            ((NativeSelect)field).select(productTableFields.get(fieldComparison).getValue());
                        if(field instanceof CheckBox)
                            ((CheckBox)field).setValue((Boolean) productTableFields.get(fieldComparison).getValue());
                    }
                });
                return field;
            }
        };
        productTableFieldFactory.getInputPrompt().put("estimatedQuantity", "0,000");
        productTableFieldFactory.getInputPrompt().put("rebate", "$0.00");
        productTableFieldFactory.getInputPrompt().put("fixedPrice", "$0.00");
        productTableFieldFactory.getInputPrompt().put("fixedRate", "$0.00");
        productTable.setWidth("100%");
        productTable.setHeight("400px");
        productTable.setContainerDataSource(productContainer);
        productTable.setImmediate(true);
        productTable.setEditable(true);
        productTable.setBuffered(true);
        productTable.setTableFieldFactory(productTableFieldFactory);
        productTable.setVisibleColumns("ID", "product.productName", "product.productNumber", "productGrouping.groupingDescription", "productGrouping.groupingCD", "estimatedQuantity", "estimatedQuantityUom", "rebate", "rebateUom", "fixedPrice", "fixedRate", "probableExposure", "active");
        productTable.setColumnHeader("product.productName", "productName");
        productTable.setColumnHeader("product.productNumber", "productNumber");
        productTable.setColumnHeader("productGrouping.groupingDescription", "groupingDescription");
        productTable.setColumnHeader("productGrouping.groupingCD", "groupingCD");

Thinking about it more, I think I know why it’s happening. The generated column is adding a listener to the property which the data is bound to, where as I need it added to the field because the field is set to buffered, and the property only gets updated when a commit on that field is called.

Any pointers on how to get a field from a table would be greatly appreciated.

Thanks for the additional details, Adam. And yes, I think you are right about the reason why you are not seeing the value change events. You could iterate through all visible components in a Table using iterator, but this is of course a bit clumsy because all the components also from previous rows are returned by the iterator.

How about if you would create all the fields for one item (row) in one go? This way you could get rid of quite a lot of if(propertyid.equals(…) stuff and have opportunity to bind the fields to each other the way you like. All you need to do is to refactor your field factory so that it creates all the row components for you whenever it starts to process new item and stores them internally for a while. Then it would just return the cached components in following calls to createField.

As a side note: Since you are already heavily customizing the way the edit fields are created, you might also want to consider changing the table in non-editable mode and creating the editable columns using columngenerators.

As a secondary side note: It is probably not a good idea to attach generated fields to properties in table, because the properties in your case are persistent where as the fields come and go when you scroll the table. If you add a field as value change listener to a property, you probably end up leaking memory at some point.

-Matti

I really appreciate your help Matti.

I actually was messing around and hacked a solution last night before you had replied.
All I did was create a map that holds a field, and a custom class I made that holds a itemid and propertyid, which overrides the hash and equals methods for easy comparison:

    protected Map<FieldComparison, Field> productTableGeneratedColumnFields = new HashMap<>();

After that I add every estimatedQuantity and rebate field that is created to that map in my FieldFactory:

if (propertyId.equals("estimatedQuantity")) {
    ((TextField) field).setImmediate(true);
    ((TextField) field).setValidationVisible(true);
    ((TextField) field).setConverter(new StringToIntegerConverter());
    productTableGeneratedColumnFields.put(new FieldComparison(propertyId, itemId), field);
}
if (propertyId.equals("rebate")) {
    ((TextField) field).setImmediate(true);
    ((TextField) field).setValidationVisible(true);
    ((TextField) field).setConverter(new StringToBigDecimalConverter());
    productTableGeneratedColumnFields.put(new FieldComparison(propertyId, itemId), field);
}

Keep in mind that even if a new field is created (adding a new item, etc), it will simply override the existing field for that propertyId and itemId within that map, so no memory leak there.
Now that we have easy access to each field without iterating over a table, I can use the map to add the notifiers:

productTable.addGeneratedColumn("probableExposure", (source, itemId, columnId) -> {
    Label label = new Label();


    Field quantityField = productTableGeneratedColumnFields.get(new FieldComparison("estimatedQuantity", itemId));
    Field rebateField = productTableGeneratedColumnFields.get(new FieldComparison("rebate", itemId));


    if(quantityField.isValid() && rebateField.isValid())
        label.setValue(calcPropableExposure((Integer)((TextField) quantityField).getConvertedValue(), (BigDecimal)((TextField) rebateField).getConvertedValue()).toString());

    if (quantityField instanceof Property.ValueChangeNotifier) {
        Property.ValueChangeNotifier qtyNotifier = quantityField;
        qtyNotifier.addValueChangeListener(event -> {
            if(quantityField.isValid() && rebateField.isValid())
                label.setValue(calcPropableExposure((Integer)((TextField) quantityField).getConvertedValue(), (BigDecimal)((TextField) rebateField).getConvertedValue()).toString());
        });
    }
    if (rebateField instanceof Property.ValueChangeNotifier) {
        Property.ValueChangeNotifier rebateNotifier = rebateField;
        rebateNotifier.addValueChangeListener(event -> {
            if(quantityField.isValid() && rebateField.isValid())
                label.setValue(calcPropableExposure((Integer)((TextField) quantityField).getConvertedValue(), (BigDecimal)((TextField) rebateField).getConvertedValue()).toString());
        });
    }
    return label;
});

I will look into the column generators though, I haven’t seen those yet. Maybe they’ll be a better solution.

Again, I really appreciate the help. I’m still kinda new to this. My day job is a database administrator, and i’ve only been messing around with Java web development for about 7 months now, and even then it’s just a hobby at night.

Happy to hear that you got it working. Good work! Holding the field references in the map should not be a problem unless you have a container with hundreds or shousands of items. On the other hand, why not clean up the field from the map as soon as you have attached the valuechangelistener to it?

-Matti

Yeah, it will hold at most a couple hundred or so items in that map at absolute worst case, so it shouldn’t be an issue. But I do like the idea of cleaning up. I’ll do that.

-Adam