Reputation: 338775
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
Reputation: 338775
Here are three ways to do this in Vaadin 7:
EnumBackedOptionGroup
. A subclass of OptionGroup
in Vaadin 7.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;
}
}
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 );
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.
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.
toString
ApproachA 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 {
}
}
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.
BeanItemContainer
ApproachLet'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
Reputation: 1934
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