从 Vaadin 7 中的枚举创建一个 OptionGroup(单选按钮)?

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

我有一个 Java 枚举,其中 getter 用于所需的显示文本。我如何使用它来填充 Vaadin 7 中的 OptionGroup?

以下是在 Vaadin 7 中执行此操作的三种方法:

  • 一个class我建的,EnumBackedOptionGroup。 Vaadin 7 中 OptionGroup 的子class。
  • Roll-your-own 简短的甜蜜方式。
  • Roll-your-own更灵活的方式。

OptionGroup

的子class

这是我写的 OptionGroup 的新子 class 的源代码。

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 ) );
        }
    }

}

我希望您会使用像这样的枚举,DogBreed。请注意这个枚举如何有一个构造函数,我们在其中传递文本以用作向用户呈现的标签。我们添加了一种方法 getTitle 来检索此标题文本。

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;
    }

}

多亏了 this Answer by WillShackleford on my Question, Lambda syntax to pass and invoke a method reference,我才能做到 class。

要使用此 EnumBackedGroupOption class,请传递其 class 和该 title-rendering 方法的方法参考。这需要 Java 8 中的新 Lambda syntax。但无需掌握您对 Lambda 的理解,只需遵循您在此处看到的模式即可。

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

对于 quick-and-dirty 原型设计,您可以定义一个没有此类构造函数和 getter 的简单枚举。在这种情况下,只传递您的标题和枚举 class。 EnumBackedOptionGroup class 回退到使用 built-in toString method. Neither I nor the Enum class 文档推荐这条路线用于严肃的工作,其中 toString 应该仅用于调试.

package com.example;

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

    YES, NO, MAYBE;
}

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

有时您可能不想在您的 OptionGroup 中使用所有枚举的实例值。如果是这样,使用 this Question 中解释的隐式方法 values 提取这些实例的 Collection。删除不需要的。请注意我们如何从 Arrays.asList 的输出实例化一个新的 ArrayList 以允许此修改。然后将 collection 传递给 EnumBackedOptionGroup.

的另一个构造函数

您可以将 null 作为最后一个参数传递,以返回使用 toString 作为演示文稿标签。

您也许可以使用 EnumMap or EnumSet 而不是 .values,但我没有这方面的经验。

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

想象这个 CRITTER_FILTER 枚举嵌套在 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;
    }

}

添加构造函数使我们能够向每个枚举实例传递所需的显示文本,然后将该文本存储在私有成员字符串变量中。

简短的简单方法

如果在确定显示文本时没有花哨的工作要做,只需将 toString 方法重写为 return 存储的显示文本。

推荐这种方法。 documentation 建议仅当您想创建一个特殊值以在调试工作中显示给程序员时才覆盖 toString。但是,我确实尝试过这种方法并且确实有效。

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 方法的完整示例

A Person class,带有嵌套枚举。

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;
    }

}

还有一个很小的 ​​Vaadin 7.4.3 应用程序使用该嵌套枚举来填充选项组。查找注释 // Core of example. 以查看重要行。

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 {
    }
}

更灵活的方法

注意在上面的枚举中添加了 getTitle 方法。您可以使用您想要的任何方法名称,但 getNamename 除外,它们已在 Java.

中定义为枚举的一部分

创建一个 BeanItemContainer,填充我们枚举的实例,并告诉 Vaadin 提供显示文本的“属性”(用于反射性地找到匹配的 getter 方法)的名称。

除了更灵活之外,考虑到文档关于覆盖 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.

行得通。我希望它能在 Vaadin 6 和 7 中工作。

BeanItemContainer 方法的完整示例

让我们调整上面部分中显示的示例 Person 和 Vaadin 应用程序。

Personclass中,将toString方法替换为JavaBeans属性getter、getCaptionText .此方法的名称可以是任何名称,只要它与在下面的 Vaadin 应用程序中看到的对 setItemCaptionPropertyId 的调用相匹配即可。

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;
    }

}

Vaadin 应用更改为使用 BeanItemContainer。通过调用 setItemCaptionPropertyId,您可以指定该容器中的哪些属性应该用作要显示的文本。

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 {
    }
}

Viritin

Viritin, an add-on 对于 Vaadin,有一个非常方便的字段叫做 EnumSelect。它可以从编辑的 属性 中自动检测可用的属性。您也可以只传递一个在 UI.

上显示为标题的策略

基本用法

    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());

请注意,当前版本对输入内容进行了限制。我刚刚修复了它,显示的类型 api 将在下一个版本中。