Basil Bourque
Basil Bourque

Reputation: 338775

Make an OptionGroup (radio buttons) from an enum in Vaadin 7?

I have a Java enum with a getter for the desired display text. How can I use this to populate an OptionGroup in Vaadin 7?

Upvotes: 2

Views: 4015

Answers (2)

Basil Bourque
Basil Bourque

Reputation: 338775

Here are three ways to do this in Vaadin 7:

  • A class I built, EnumBackedOptionGroup. A subclass of OptionGroup in Vaadin 7.
  • Roll-your-own the short sweet way.
  • Roll-your-own the more flexible way.

Subclass of OptionGroup

Here is the source code of a new subclass of OptionGroup I wrote.

package com.basilbourque;

import com.vaadin.ui.OptionGroup;
import java.util.Arrays;
import java.util.Collection;
import java.util.function.Function;
import org.slf4j.LoggerFactory;

/**
 * A subclass of the Vaadin 7 OptionGroup (radio buttons or bunch of checkboxes) widget, taking as its set of options
 * the instances of an Enum.
 *
 * In canonical usage, pass the class of your Enum and a reference to the method to be called for obtaining a textual
 * label for display to the user.
 *
 * Alternatively, if your Enum overrides the `toString` method, you may pass only the class of the Enum without a
 * Function. This approach is not recommended per the class documentation which explains `toString` should only be used
 * for debugging message. Nevertheless, some people override `toString` to provide a user-readable label, so we support
 * this.
 *
 * Even if your Enum does not override `toString` you may choose to omit passing the Function argument. As a default,
 * the Enum’s built-in `toString` method will be called, returning the "name" of the Enum’s instance. This is handy for
 * quick-and-dirty prototyping. Again, neither I nor the class doc recommend this approach for serious work.
 *
 * If you want to display a subset of your enum’s instances rather than all, pass a Collection.
 *
 * This source code available under terms of ISC License.  https://en.wikipedia.org/wiki/ISC_license
 * 
 * Copyright (c) 2015, Basil Bourque
 * Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby
 * granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS
 * PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT,
 * OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
 * SOFTWARE.
 *
 * @author Basil Bourque
 * @version 2015-08-27T21:00:00Z
 * @since 2015-08-27T21:00:00Z
 */
public class EnumBackedOptionGroup<T extends Enum> extends OptionGroup
{

    final org.slf4j.Logger logger = LoggerFactory.getLogger( this.getClass() );

    /**
     * Constructor. The usual constructor for automatically detecting all the instances of an enum for use as the
     * options in a Vaadin 7 OptionGroup. Pass a function to be called for providing each option’s displayed labeling.
     *
     * Example usage:
     *
     * myRadios = new EnumBackedOptionGroup<DogBreed>( "Choose breed:" , DogBreed.class , DogBreed :: getTitle );
     *
     * @param caption
     * @param enumClass
     * @param f
     */
    public EnumBackedOptionGroup ( final String caption , final Class<T> enumClass , final Function<T , String> f ) {
        super( caption );
        Function<T , String> func = f;
        // If passed a null for the Function, fallback to using 'toString'.
        if ( func == null ) {
            func = T -> T.toString();
        }
        this.buildAndAssignCaptions( enumClass , func );
    }

    /**
     * Constructor. Similar to usual constructor, but here you may additionally pass a Collection of the subset of Enum
     * instances.
     *
     * For use where business logic dictates that you give only some of the Enum values an options rather than all of
     * them. The omitted options are effectively hidden from the user.
     *
     * @param caption
     * @param enumClass
     * @param enumValues
     * @param f
     */
    public EnumBackedOptionGroup ( final String caption , final Class<T> enumClass , final Collection<T> enumValues , final Function<T , String> f ) {
        super( caption );
        Function<T , String> func = f;
        // If passed a null for the Function, fallback to using 'toString'.
        if ( func == null ) {
            func = T -> T.toString();
        }
        Collection<T> ev = enumValues;
        // Handle where calling method passed us a null or empty collection.
        if ( ( ev == null ) || ev.isEmpty() ) {
            this.buildAndAssignCaptions( enumClass , f ); // Fallback to assiging all the instances of enum as options in our OptionGroup.
        } else {
            this.addItems( enumValues );  // Add the passed subset of instances of the enum as items backing our OptionGroup.
            this.assignCaptions( enumValues , f );
        }
    }

    /**
     * Constructor. Similar to the usual constructor, but omits the method for providing on-screen labeling. Instead
     * uses the 'toString' method defined either explicitly in the Enum subclass or implicitly calls to the Enum class’
     * own 'toString'.
     *
     * Not recommended, as the Enum documentation strongly suggests the 'toString' method on an Enum be used only for
     * debugging. Nevertheless this is handy for quick-and-dirty prototyping.
     *
     * @param caption
     * @param enumClass
     */
    public EnumBackedOptionGroup ( final String caption , final Class<T> enumClass ) {
        super( caption );
        // User passed no Function to call for getting the title. So fallback to using 'toString'.
        this.buildAndAssignCaptions( enumClass , T -> T.toString() );
    }

    // Helper method. (sub-routine)
    // Extracts all the instances of the enum, and uses them as options in our OptionGroup.
    // Also assigns each option a labeling using String returned by passed method to be called for each instance of enum.
    private void buildAndAssignCaptions ( final Class<T> enumClass , final Function<T , String> f ) {
        if ( enumClass.isEnum() ) {  // This check may be unnecessary with Generics code "<T extends Enum>" at top of this class.
            Collection<T> enumValues = Arrays.asList( enumClass.getEnumConstants() );
            this.addItems( enumValues );  // Add all the instances of the enum as items backing our OptionGroup.
            this.assignCaptions( enumValues , f );
        } else {
            // Else the passed class is not an enum.
            // This case should not be possible because of the Generics marked on this class "<T extends Enum>".
            logger.error( "Passed a class that is not a subclass of Enum. Message # f2098672-ab47-47fe-b720-fd411411052e." );
            throw new IllegalArgumentException( "Passed a class that is not a subclass of Enum." );
        }
    }

    // Helper method. (sub-routine)
    // Assigns each option a labeling using String returned by passed method to be called for each instance of enum
    private void assignCaptions ( Collection<T> enumValues , final Function<T , String> f ) {
        for ( T option : enumValues ) {
            // For each option in our OptionGroup, determine and set its title, the label displayed for the user next to each radio button or checkbox.
            // To determine the label (the second argument), we invoke the passed method which must return a String. Using Lambda syntax.
            this.setItemCaption( option , f.apply( option ) );
        }
    }

}

I expect you would use with an enum like this one, DogBreed. Note how this enum has a constructor in which we pass the text to be used as a label for presentation to the user. We added a method getTitle to retrieve this titling text.

package com.example;

/**
 * Bogus example Enum.
 */
public enum DogBreed {

    AUSSIE("Australian Shepherd") ,
    BORDER_COLLIE("Border Collie"),
    BLACK_LAB("Labrador, Black"),
    MUTT("Mixed Breed");

    private String title = null;

    DogBreed ( final String titleArg) {
        this.title = titleArg;
    }

    public String getTitle() {
        return this.title;
    }

}

screen shot of radio buttons showing dog breeds using presentation labeling

I was only able to accomplish that class thanks to this Answer by WillShackleford on my Question, Lambda syntax to pass and invoke a method reference.

To use this EnumBackedGroupOption class, pass its class and a method reference for that title-rendering method. This requires the new Lambda syntax in Java 8. But no need to have yet mastered your understanding of Lambda yet, just follow the pattern you see here.

OptionGroup optionGroup = new EnumBackedOptionGroup<DogBreed>( "Choose Breed:" , DogBreed.class , DogBreed :: getTitle );
    

For quick-and-dirty prototyping, you can define a simple enum with no such constructor and getter. Pass just your caption and the enum class in this case. The EnumBackedOptionGroup class falls back to using the built-in toString method. Neither I nor the Enum class doc recommend this route for serious work where toString should be used only for debugging.

package com.example;

/**
 * Bogus example Enum.
 */
public enum SaySo {

    YES, NO, MAYBE;
}

OptionGroup optionGroup = new EnumBackedOptionGroup<SaySo>( "Says you:" , SaySo.class );

Occasionally you may not want to use all of the enum’s instance values in your OptionGroup. If so, extract a Collection of those instances, using the implicit method values explained in this Question. Remove the unwanted ones. Note how we instantiated a fresh ArrayList from output of Arrays.asList to allow this modification. Then pass that collection to another constructor of EnumBackedOptionGroup.

You can pass null as the last argument to fall back on using toString as the presentation labeling.

You might be able to use either an EnumMap or EnumSet instead of .values, but I have no experience with that.

Collection<T> enumValues = new ArrayList( Arrays.asList( SaySo.values() ) );
enumValues.remove( SaySo.MAYBE );
OptionGroup optionGroup = new EnumBackedOptionGroup<SaySo>( "Says you:" , SaySo.class , null );

Roll-Your-Own

Imagine this CRITTER_FILTER enum nested in SomeClass.

public enum CRITTER_FILTER
{

    CANINE ( "Dogs" ), // Pass the text to be displayed to user as the radio button’s Caption (label).
    FELINE ( "Cats" ),
    COCKATIEL ( "Cockatiel birds" );

    private String title;

    CRITTER_FILTER ( String t )
    {
        this.title = t;
    }

    // Add this method for the more flexible approach.
    // JavaBeans "getter" for use in BeanItemContainer.
    public String getTitle ()
    {
        return this.title;
    }

    // Add this method for the short simple approach.
    @Override
    public String toString ()
    {
        return this.title;
    }

}

Adding a constructor enables us to pass in to each enum instance the desired display text, and then store that text in a private member String variable.

Short Simple Approach

If there is no fancy work to be done in determining the display text, simply override the toString method to return the stored display text.

I am not recommending this approach. The documentation recommends overriding toString only if you want to create an special value for display to the programmer in debugging work. However, I did try this approach and it does work.

public String toString()

… This method may be overridden, though it typically isn't necessary or desirable. An enum type should override this method when a more "programmer-friendly" string form exists.

this.filterRadios = new OptionGroup( "Filter:" , Arrays.asList( SomeClass.CRITTER_FILTER.values() ) );  // Convert plain array of the enum instances (the values) into a `Collection` object by calling utility method `Arrays.asList`.
this.filterRadios.setMultiSelect( false ); // Radio buttons are single-select.

Full Example Of toString Approach

A Person class, with a nested enum.

package com.example.vaadinradiobuttons;

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author Basil Bourque
 */
public class Person {

    // Members
    String name;
    Person.VITAL_STATUS vitalStatus;

    public enum VITAL_STATUS {

        LIVING( "Alive and Kicking" ),
        DECEASED( "Dead" ),
        UNKNOWN( "DUNNO" );

        private String captionText;

        VITAL_STATUS ( String t ) {
            this.captionText = t;
        }

        @Override
        public String toString () {
            return this.captionText;
        }

    }

    // Constructor
    public Person ( String nameArg , VITAL_STATUS vitalStatusArg ) {
        this.name = nameArg;
        this.vitalStatus = vitalStatusArg;
    }

}

And a tiny little Vaadin 7.4.3 app using that nested enum to populate an Option Group. Look for the comment // Core of example. to see the important lines.

package com.example.vaadinradiobuttons;

import javax.servlet.annotation.WebServlet;

import com.vaadin.annotations.Theme;
import com.vaadin.annotations.VaadinServletConfiguration;
import com.vaadin.annotations.Widgetset;
import com.vaadin.data.Property;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinServlet;
import com.vaadin.ui.OptionGroup;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;
import java.util.Arrays;
import java.util.Collection;

/**
 *
 */
@Theme ( "mytheme" )
@Widgetset ( "com.example.vaadinradiobuttons.MyAppWidgetset" )
public class MyUI extends UI {

    @Override
    protected void init ( VaadinRequest vaadinRequest ) {
        final VerticalLayout layout = new VerticalLayout();
        layout.setMargin( true );
        setContent( layout );

        // Core of example.
        Collection<Person.VITAL_STATUS> v = Arrays.asList( Person.VITAL_STATUS.values() );
        OptionGroup radios = new OptionGroup( "Vital Status :" , v );
        radios.setImmediate( true );
        radios.addValueChangeListener( ( Property.ValueChangeEvent event ) -> {
            Person.VITAL_STATUS vitalStatus = ( Person.VITAL_STATUS ) event.getProperty().getValue();
            System.out.println( "User selected a vital status name: " + vitalStatus.name() + ", labeled: " + vitalStatus.toString() );
        } );
        layout.addComponent( radios );

    }

    @WebServlet ( urlPatterns = "/*" , name = "MyUIServlet" , asyncSupported = true )
    @VaadinServletConfiguration ( ui = MyUI.class , productionMode = false )
    public static class MyUIServlet extends VaadinServlet {
    }
}

More Flexible Approach

Note the addition of the getTitle method in our enum above. You can use any method name you want, except getName or name which is already defined as part of an enum in Java.

Create a BeanItemContainer, fill with the instances of our enum, and tell Vaadin the name of the "property" (used to reflectively find a matching getter method) providing the display text.

Besides being more flexible, this approach may be wiser given the doc’s cautions about overriding toString.

BeanItemContainer<SomeClass.CRITTER_FILTER> radiosBic = new BeanItemContainer<SomeClass.CRITTER_FILTER>( SomeClass.CRITTER_FILTER.class );
radiosBic.addAll( Arrays.asList( SomeClass.CRITTER_FILTER.values() ) );  // Convert array of values to a `Collection` object.
this.filterRadios = new OptionGroup( "Critter Filter:" , radiosBic );
this.filterRadios.setMultiSelect( false ); // Radio buttons are single-select.
this.filterRadios.setItemCaptionMode( AbstractSelect.ItemCaptionMode.PROPERTY );  
this.filterRadios.setItemCaptionPropertyId( "title" );  // Matches the getter method defined as part of the enum.

That works. I expect it would work in Vaadin 6 as well as 7.

Full Example Of BeanItemContainer Approach

Let's adjust the example Person and Vaadin app shown in section above.

In the Person class, replace the toString method with a JavaBeans Property getter, getCaptionText. The name of this method can be anything, as long as it matches the call to setItemCaptionPropertyId seen in the Vaadin app further below.

package com.example.vaadinradiobuttons;

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author Basil Bourque
 */
public class Person {

    // Members
    String name;
    Person.VITAL_STATUS vitalStatus;

    public enum VITAL_STATUS {

        LIVING( "Alive and Kicking" ),
        DECEASED( "Dead" ),
        UNKNOWN( "DUNNO" );

        private String captionText;
        static public String CAPTION_TEXT_PROPERTY_NAME = "captionText";  //

        VITAL_STATUS ( String t ) {
            this.captionText = t;
        }

        // JavaBeans Property getter.
        public String getCaptionText () {
            return this.captionText;
        }

    }

    // Constructor
    public Person ( String nameArg , VITAL_STATUS vitalStatusArg ) {
        this.name = nameArg;
        this.vitalStatus = vitalStatusArg;
    }

}

The Vaadin app is changed to use a BeanItemContainer. With a call to setItemCaptionPropertyId, you specify which of the properties in that container should be used as the text to display.

package com.example.vaadinradiobuttons;

import javax.servlet.annotation.WebServlet;

import com.vaadin.annotations.Theme;
import com.vaadin.annotations.VaadinServletConfiguration;
import com.vaadin.annotations.Widgetset;
import com.vaadin.data.Property;
import com.vaadin.data.util.BeanItemContainer;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinServlet;
import com.vaadin.ui.OptionGroup;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;
import java.util.Arrays;
import java.util.Collection;

/**
 *
 */
@Theme ( "mytheme" )
@Widgetset ( "com.example.vaadinradiobuttons.MyAppWidgetset" )
public class MyUI extends UI {

    @Override
    protected void init ( VaadinRequest vaadinRequest ) {
        final VerticalLayout layout = new VerticalLayout();
        layout.setMargin( true );
        setContent( layout );

        // Core of example.
        Collection<Person.VITAL_STATUS> v = Arrays.asList( Person.VITAL_STATUS.values() );
        BeanItemContainer<Person.VITAL_STATUS> bic = new BeanItemContainer<>( Person.VITAL_STATUS.class , v );
        OptionGroup radios = new OptionGroup( "Vital Status :" , bic );
        radios.setItemCaptionPropertyId(Person.VITAL_STATUS.CAPTION_TEXT_PROPERTY_NAME );  // …or… ( "captionText" );
        radios.setImmediate( true );
        radios.addValueChangeListener( ( Property.ValueChangeEvent event ) -> {
            Person.VITAL_STATUS vitalStatus = ( Person.VITAL_STATUS ) event.getProperty().getValue();
            System.out.println( "User selected a vital status name: " + vitalStatus.name() + ", labeled: " + vitalStatus.toString() );
        } );
        layout.addComponent( radios );

    }

    @WebServlet ( urlPatterns = "/*" , name = "MyUIServlet" , asyncSupported = true )
    @VaadinServletConfiguration ( ui = MyUI.class , productionMode = false )
    public static class MyUIServlet extends VaadinServlet {
    }
}

Upvotes: 6

mstahv
mstahv

Reputation: 1934

Viritin

Viritin, an add-on for Vaadin, has a really handy field called EnumSelect. It can detect the available properties automatically from the edited property. You can also just pass a strategy what show as caption on UI.

Basic usage

    EnumSelect<AddressType> select = new EnumSelect<AddressType>()
            .withSelectType(OptionGroup.class);
    select.setStyleName(ValoTheme.OPTIONGROUP_HORIZONTAL);

    // The Enum type is detected when the edited property is bound to select
    // This typically happens via basic bean binding, but here done manually.
    ObjectProperty objectProperty = new ObjectProperty(AddressType.Home);
    select.setPropertyDataSource(objectProperty);

    // Alternatively, if not using databinding at all, you could just use 
    // basic TypedSelect, or the method from it
    // select.setOptions(AddressType.values());

Note that the current release has limited typing. I just fixed that and the shown typed api will be in next release.

Upvotes: 3

Related Questions