Multiselect OptionGroup and BeanFieldGroup

Hello everyone!

I’m having some fun with an multiselect
OptionGroup
binded to a
BeanFieldGroup
, and I wonder if you could help me out with this :slight_smile:

Let me explain the situation (code here:
https://gist.github.com/juanghurtado/1f0662b8e7c9da1f9ce5
)

I have some related beans (for the sake of simplicity in the example, let’s talk about
CountryDTO
and
CityDTO
). A
CountryDTO
have a field “cities”, which points to a
List
(files CountryDTO.java and CityDTO.java in the Gist).

Of course, I would like to have a form where I can choose some cities for the country. So I though on using a multiselect
OptionGroup
to acomplish that. But, smart as I am, I chose to use a
BeanFieldGroup
to arrange all this stuff together (files AddCountryUI.java and EditCountryUI.java in the Gist).

After some struggle, I saw that I needed a converter for the
OptionGroup
, to convert the values between IDs and DTO, so I did (AbstractMultiselectToDTOConverter.java file in the Gist).

But now I’m stuck with the following:

  • In the case of saving cities in the country, I have to do it manually calling
    setCities()
    of the
    CountryDTO
    right after the
    commit()
    of the
    BeanFieldGroup
    (see comments in AddCountryUI.java file in the Gist).

  • In the case of editing an existing country, and that country having cities, the
    OptionGroup
    does not automatically checks the cities for the given
    CountryDTO
    . I have to do it manually because the
    setValue()
    of the
    OptionGroup
    never gets called. (see comments in EditCountryUI.java).

Thanks a lot for your time, and your efforts on making Vaading an outstanding framework!

Hi, I have currently no time test this, but it should work something like this. If you cant get it working, please ask further advices!

        formGroup = new BeanFieldGroup<CountryDTO>(CountryDTO.class) {

            @Override
            public <T extends Field> T buildAndBind(String caption,
                    Object propertyId, Class<T> fieldType) throws BindException {
                if (fieldType.equals(OptionGroup.class)) {
                    final OptionGroup field = new OptionGroup();
                    field.setImmediate(true);
                    field.setMultiSelect(true);
                    field.setContainerDataSource(getCitiesContainer());
                    field.setItemCaptionPropertyId("name");
                    field.setCaption(caption);
                    bind(field, propertyId);
                    return (T) field;
                } else {
                    return super.buildAndBind(caption, propertyId, fieldType);
                }
            }
            
        };
        
        OptionGroup citiesOptionGroup = formGroup.buildAndBind("_Cities", "cities", OptionGroup.class);
        formGroup.setItemDataSource(new BeanItem<CountryDTO>(country));
        formLayout.addComponent(citiesOptionGroup); 

I can’t get it to work. It keeps giving me some:

Caused by: com.vaadin.data.util.converter.Converter$ConversionException: Unable to convert value of type org.hibernate.collection.internal.PersistentBag to presentation type interface java.util.Set. No converter is set and the types are not compatible.
    at com.vaadin.data.util.converter.ConverterUtil.convertFromModel(ConverterUtil.java:116)
    at com.vaadin.ui.AbstractField.convertFromModel(AbstractField.java:708)
    at com.vaadin.ui.AbstractField.convertFromModel(AbstractField.java:693)
    at com.vaadin.ui.AbstractField.setPropertyDataSource(AbstractField.java:629)
    ... 49 more

It’s weird, because my field DOES have a
Converter
, but the exception says that it doesn’t.

This is my custom
buildAndBindOptionGroup()
for the OptionGroups. This method is defined in my
CustomBeanFieldGroup
class, and I use it to build the OptionGroups instead of the classic
buildAndBind()

    public OptionGroup buildAndBindOptionGroup(String caption, Object propertyId, Container container, String captionPropertyId, Boolean multiselect) throws BindException {
        OptionGroup field;

        if (multiselect) {
            field = proxy.buildMultiselectOptionGroup(propertyId, container, captionPropertyId);
        } else {
            field = proxy.buildOptionGroup(propertyId, container, captionPropertyId);
        }

        field.setCaption(caption);
        bind(field, propertyId);

        return field;
    }

Notice that
proxy
is just a helper to create the fields. Nothing fancy:

    public OptionGroup buildOptionGroup(Object fieldId, Container container, String captionPropertyId) {
        OptionGroup field = new OptionGroup();

        if (container.size() > 0) {
            field.setContainerDataSource(container);
            field.setConverter(new AbstractSelectToDTOConverter(field));
            field.setItemCaptionPropertyId(captionPropertyId);
        } else {
            field.setVisible(false);
        }

        return field;
    }

    public OptionGroup buildMultiselectOptionGroup(Object fieldId, Container container, String captionPropertyId) {
        OptionGroup field = buildOptionGroup(fieldId, container, captionPropertyId);

        field.setMultiSelect(true);
        field.setConverter(new AbstractMultiSelectToDTOConverter(field));

        return field;
    }

Thanks a lot for your time and advice!

Can you add full example project (preferably with mockup data instead of database stuff) into GitHub? I can clone it and try to find the solution.

Of course! I’ll do it as soon as I can!

Thank you so much!

Here it is:

https://github.com/juanghurtado/vaadin-beanfieldgroup-optiongroup

AppUI.java => Initial screen. Sample data is created, OptionGroup is built and attached to the UI
CountryDTO.java and CityDTO.java => Beans. One country has many cities.
BaseDTO.java => Abstract class for the DTOs. All of them extends this one.
CustomBeanFieldGroup.java => Has a custom buildAndBindOptionGroup() to create and bind the OptionGroup with our needs
FieldGroupProxy.java => Helper to actually build form fields
AbstractMultiSelectToDTOConverter.java => Converter for the OptionGroup: ids<=>models

Thanks!

I did little bit fixing for your converter. Tell me again if get stuck…

https://gist.github.com/johannest/e0d09a0984d565c9b59d

Hi Juan,

I’m really bit shamed such an easy task is so tricky with vanilla Vaadin. Thats why, when I saw your message, I just had to make one helper I had planned a long ago to Viritin. Check the brand new CheckBoxGroup class from it. It, and its cousin, MultiSelectTable, are “multiselect fields” that really modify the edited collection. I have been using the MultiSelectTable in couple of this kind of cases and I believe this is probably exactly what you want as well.

Your project has Maddon as dependency so upgrade that
to Viritin
(new name since couple of releases ago). In the tests there is
one example
that edits List groups field. It uses AbstractForm, but those fields can be used with raw BeanFieldGroup as well.

cheers,
matti

@Johannes: it works like a charm! Thank you so much. I tried to change the List to a Set in my initial version of the Converter, but I was doing it for all the Lists, not just for the itemIds and, of course, it was not working, hehe :slight_smile: Your solution works perfectly. Thank you.

@Matti, it looks fantastic! I saw Viritin before, but never used it. I don’t know why I have the dependency to Maddon, though… Maybe because of copy&paste, I guess… Hehehe. I’ll give it a try to see if it fits our needs for the project. But, at a first glance, it seems that it fits all of the, for sure! Thank you!

@Johannes : I needed to create a crud for an entity that had a many to many relationship to another table and was using a twin column select. I was using Vaadin JPA. So i ended up editing the code you posted to use a Vaadin Container. Thank you for posting that code.

[code]
public class AbstractMultiSelectToEntityConverter implements Converter<Set, List> {

private static final long serialVersionUID = -4042387995585589967L;

private final static Logger logger = LogManager.getLogger(AbstractMultiSelectToEntityConverter.class);
    
private JPAContainer<T> jpaContainer;

private String excludedFields;

private final List<T> MODEL_TYPE_INSTANCE = new ArrayList<T>();
private final Set<Object> PRESENTER_TYPE_INSTANCE = new HashSet<Object>();

@SuppressWarnings("unchecked")
public AbstractMultiSelectToEntityConverter(AbstractSelect field, String excludedFields) {
    this.jpaContainer = (JPAContainer<T>) field.getContainerDataSource();
    this.excludedFields = excludedFields;
}

@Override
public List<T> convertToModel(Set<Object> itemIds,
        Class<? extends List<T>> targetType, Locale locale)
        throws com.vaadin.data.util.converter.Converter.ConversionException {
    List<T> models = new ArrayList<T>();
    if (jpaContainer.size() > 0) {
        for (Object itemId : itemIds) {
            if (jpaContainer.containsId(itemId)) {
                models.add(jpaContainer.getItem(itemId).getEntity());
            }
        }
    }
    return models;
}

@Override
public Set<Object> convertToPresentation(List<T> models,
        Class<? extends Set<Object>> targetType, Locale locale)
        throws com.vaadin.data.util.converter.Converter.ConversionException {
      Set<Object> ids = new HashSet<Object>();
      for (Object itemId : jpaContainer.getItemIds()) {
          T bean = jpaContainer.getItem(itemId).getEntity();
          if(models!=null){
              for (Object model : models) {
                  if (compare(model,bean)) {
                      ids.add(itemId);
                  } 
              }  
          }
      }
      return ids;
}

@Override
public Class<List<T>> getModelType() {
    return (Class<List<T>>) MODEL_TYPE_INSTANCE.getClass();
}

@Override
public Class<Set<Object>> getPresentationType() {
    return (Class<Set<Object>>) PRESENTER_TYPE_INSTANCE.getClass();
}

public boolean compare(Object a, Object b) {
    return EqualsBuilder.reflectionEquals(a, b, excludedFields);
}

}
[/code]Called from the UI as such

        protected TwinColSelect servicesSelect;       
        ....
        services = JPAContainerFactory.make(Service.class,"itmspersistence");
        ....
        servicesSelect.setContainerDataSource(services);
        servicesSelect.setItemCaptionPropertyId("serviceName");
        //Fields in the entity bean that I didn't want to equalsbuilder to use
        String excludedFields = {"agencyRates","invoiceDetails","category","syndicateDiscounts","syndicateDiscountServices","syndicateRates"};
        servicesSelect.setConverter(new AbstractMultiSelectToEntityConverter(servicesSelect,excludedFields));

22207.java (2.81 KB)