MultipleSelect Component

Tapestry 5 doesn´t contain a multiple select component yet. So here is an implementation for a generic multiple select based on Daniel Jue's GenericSelectionModel and GenericValueEncoder classes and some modified base components. I leaved the unmodified parts in the code for better copy and paste.

Note: This renders as a Select element, so users have to control-click or shift-click to select multiple items. A better choice for most purposes is the Palette component or (for Tapestry 5.3) the Checklist component.

See also :

SelectMultiple.java

First we need a modified implementation of the core select class which i named SelectMultiple. You can store it anywhere in your package system.

import java.util.List;
import java.util.Locale;

import org.apache.tapestry.Binding;
import org.apache.tapestry.ComponentResources;
import org.apache.tapestry.FieldValidator;
import org.apache.tapestry.MarkupWriter;
import org.apache.tapestry.OptionModel;
import org.apache.tapestry.SelectModel;
import org.apache.tapestry.SelectModelVisitor;
import org.apache.tapestry.ValidationException;
import org.apache.tapestry.ValidationTracker;
import org.apache.tapestry.ValueEncoder;
import org.apache.tapestry.annotations.BeforeRenderTemplate;
import org.apache.tapestry.annotations.Environmental;
import org.apache.tapestry.annotations.Inject;
import org.apache.tapestry.annotations.Parameter;
import org.apache.tapestry.corelib.base.AbstractField;
import org.apache.tapestry.services.FieldValidatorDefaultSource;
import org.apache.tapestry.services.FormSupport;
import org.apache.tapestry.services.Request;
import org.apache.tapestry.services.ValueEncoderFactory;
import org.apache.tapestry.services.ValueEncoderSource;
import org.apache.tapestry.util.EnumSelectModel;

import pathToYourPackageSystem.models.MultiValueEncoder;
import pathToYourPackageSystem.models.SelectMultipleModelRenderer;

public final class SelectMultiple extends AbstractField
{
    private class Renderer extends SelectMultipleModelRenderer
    {

        public Renderer(MarkupWriter writer)
        {
            super(writer, _encoder);
        }

        
        @Override
        protected boolean isOptionSelected(OptionModel optionModel)
        {
                Object value = optionModel.getValue();
                return (_values == null) ? false : _values.contains(value);
        }
        
    }

    @Parameter
    private MultiValueEncoder _encoder;

    @Inject
    private FieldValidatorDefaultSource _fieldValidatorDefaultSource;

    @Inject
    private Locale _locale;

    @Parameter(required = true)
    private SelectModel _model;

    @Inject
    private Request _request;

    @Inject
    private ComponentResources _resources;

    @Environmental
    private ValidationTracker _tracker;

    /** Performs input validation on the value supplied by the user in the form submission. */
    @Parameter(defaultPrefix = "validate")
    @SuppressWarnings("unchecked")
    private FieldValidator<Object> _validate = NOOP_VALIDATOR;

    /** The list of value to read or update. */
    @Parameter(required = true, principal = true)
    private List<Object> _values;
    
    
    @Inject
    private ValueEncoderSource _valueEncoderSource;

    @Override
    @SuppressWarnings("unchecked")
    protected void processSubmission(FormSupport formSupport, String elementName)
    {
        String[] primaryKeys = _request.getParameters(elementName);
        List<Object> selectedValues = _encoder.toValue(primaryKeys);

        try
        {
            for (Object selectedValue : selectedValues) {
                        _validate.validate(selectedValue);
            }
                _values = selectedValues;
           
        }
        catch (ValidationException ex)
        {
            _tracker.recordError(this, ex.getMessage());
            return;
        }
    }

    void afterRender(MarkupWriter writer)
    {
        writer.end();
    }

    void beginRender(MarkupWriter writer)
    {
        writer.element("select", "name", getElementName() , "id", getClientId(), "multiple", "multiple");
    }

    @SuppressWarnings("unchecked")
    ValueEncoder defaultEncoder()
    {
        return _valueEncoderSource.createEncoder("value", _resources);
    }

    @SuppressWarnings("unchecked")
    SelectModel defaultModel()
    {
        Class valueType = _resources.getBoundType("value");

        if (valueType == null) return null;

        if (Enum.class.isAssignableFrom(valueType))
            return new EnumSelectModel(valueType, _resources.getContainerMessages());

        return null;
    }

    FieldValidator defaultValidate()
    {
        Class type = _resources.getBoundType("value");

        if (type == null) return null;

        return _fieldValidatorDefaultSource.createDefaultValidator(
                this,
                _resources.getId(),
                _resources.getContainerMessages(),
                _locale,
                type,
                _resources.getAnnotationProvider("value"));
    }

    Binding defaultValue()
    {
        return createDefaultParameterBinding("value");
    }

    @BeforeRenderTemplate
    void options(MarkupWriter writer)
    {
        SelectModelVisitor renderer = new Renderer(writer);

        _model.visit(renderer);
    }

    // For testing.

    void setModel(SelectModel model)
    {
        _model = model;
    }

    void setValues(List<Object> values)
    {
        _values = values;
    }
    
    void setValueEncoder(MultiValueEncoder encoder)
    {
        _encoder = encoder;
    }
}

As you can see, we need a multiple value encoder interface and a multiple select model renderer, so here they are.

MultipleValueEncoder.java

import java.util.List;

import org.apache.tapestry.SelectModel;
import org.apache.tapestry.corelib.components.Select;

public interface MultiValueEncoder<V>
{
    List<String> toClient(V value);

    List<V> toValue(String[] clientValue);
}

SelectMultipleModelRenderer.java

import java.util.Map;
import java.util.List;

import org.apache.tapestry.MarkupWriter;
import org.apache.tapestry.OptionGroupModel;
import org.apache.tapestry.OptionModel;
import org.apache.tapestry.SelectModelVisitor;

public class SelectMultipleModelRenderer implements SelectModelVisitor
{
    private final MarkupWriter _writer;

    private final MultiValueEncoder _encoder;

    public SelectMultipleModelRenderer(final MarkupWriter writer, MultiValueEncoder encoder)
    {
        _writer = writer;
        _encoder = encoder;
    }

    public void beginOptionGroup(OptionGroupModel groupModel)
    {
        _writer.element("optgroup", "label", groupModel.getLabel());

        writeDisabled(groupModel.isDisabled());
        writeAttributes(groupModel.getAttributes());
    }

    public void endOptionGroup(OptionGroupModel groupModel)
    {
        _writer.end(); // select
    }

    @SuppressWarnings("unchecked")
    public void option(OptionModel optionModel)
    {
        Object optionValue = optionModel.getValue();
        
        List<String> clientValues = _encoder.toClient(optionValue);

        for (String clientValue : clientValues) {
                _writer.element("option", "value", clientValue);
        
                if (isOptionSelected(optionModel)) _writer.attributes("selected", "selected");
        
                writeDisabled(optionModel.isDisabled());
                writeAttributes(optionModel.getAttributes());
        
                _writer.write(optionModel.getLabel());
        }
        
        _writer.end();
    }

    private void writeDisabled(boolean disabled)
    {
        if (disabled) _writer.attributes("disabled", "disabled");
    }

    private void writeAttributes(Map<String, String> attributes)
    {
        if (attributes == null) return;

        for (Map.Entry<String, String> e : attributes.entrySet())
            _writer.attributes(e.getKey(), e.getValue());
    }

    protected boolean isOptionSelected(OptionModel optionModel)
    {
        return false;
    }

}

Now we need the implementation of the modified generic value encoder. In case of no selected values it will return an empty list.

GenericMultipleValueEncoder.java

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.beanutils.BeanUtils;
public class GenericMultiValueEncoder<T> implements MultiValueEncoder<T> {

        private List<T> list;

        private String labelField;

        public GenericMultiValueEncoder(List<T> list, String labelField) {
                this.list = list;
                this.labelField = labelField;
        }

        public List<String> toClient(T obj) {
                try {
                        return Arrays.asList(BeanUtils.getArrayProperty(obj, labelField));
                } catch (IllegalAccessException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                } catch (InvocationTargetException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                } catch (NoSuchMethodException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                }
                return null;
        }

        public List<T> toValue(String[] strings) {
                try {
                        List<String> test = Arrays.asList(strings);
                        List<T> valueList = new ArrayList<T>();
                        for (T obj : list) {
                                if (test.contains(BeanUtils.getProperty(obj, labelField))) {
                                        valueList.add(obj);
                                }
                        }
                        return valueList;
                } catch (NullPointerException e) {
                        List<T> exceptionReturn = new ArrayList<T>();
                        return exceptionReturn; 
                } catch (IllegalAccessException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                } catch (InvocationTargetException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                } catch (NoSuchMethodException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                }
                return null;
        }
}

Usage

Now you can use the multiple select in your pages. First you need a List parameter in your page class. You will need the GenericSelectionModel and the GenericMultiValueEncoder also.

...
@Persist // Could be @Parameter if needed
private List<DemoObject> dOs;

public List<DemoObject> getDOs() {
        return dOs;
}

public void setDOs(List<DemoObject> new_dOs) {
        dOs = new_dOs;
}
...
public GenericSelectionModel<dO> getDOSelectModel() {
        return new GenericSelectionModel<dO>(your_model.method(),
                        "your_label");
}

public GenericMultiValueEncoder<dO> getDOValueEncoder() {
        return new GenericMultiValueEncoder<dO>(your_model.method(), "your_id");
}
...

The page template has to contain following code inside the form.

...
<t:selectmultiple values="dOs" 
                  model="DOSelectModel" 
                  encoder="DOValueEncoder" 
/>
...

Note that this implementation can´t be used with the automatic generated encoder for enums. In this case you have to implement the enum encoder manually.

Tapestry5MultipleSelectOnObjects (last edited 2011-06-17 10:38:51 by BobHarner)