7

I'm trying to get a Primefaces 5.2 selectOneMenu to display images along with their filenames. This is what my xhtml currently looks like:

<h:form>
<h:panelGrid id="createPanelGrid" columns="2">
    <p:outputLabel value="Service Logo:" />
    <p:selectOneMenu value="#{imageBean.selectedImage}" var="l">
        <f:selectItem itemLabel="Select a logo" itemValue="" />
        <f:selectItems value="#{imageBean.imageList}" var="logo" itemLabel="#{logo}" itemValue="#{logo}" />
        <p:column>
            <p:graphicImage value="#{imageBean.imageFolder}/#{l}" style="max-width:50px;max-height:50px;" />
        </p:column>
        <p:column>#{l}</p:column>
    </p:selectOneMenu>
</h:panelGrid>

The ManagedBean (imageBean) has

public List<String> getImageList () {
    List<String> imageList = new ArrayList<String>();
    File[] files = absoluteImageFolder.listFiles();
    for (File file : files) {
        imageList.add(file.getName());
    }        
    return imageList;
}

and

private String selectedImage;

public String getSelectedImage() {
    return selectedImage;
}

public void setSelectedImage(String selectedImage) {
    this.selectedImage = selectedImage;
}

However, the images are not rendered on the webpage, just the filenames (I'd post a screenshot but I don't have enough reputation). I don't get two columns (first the image, then the filename), I just get the filename itself.

When I wrap the filename Strings into a POJO and use a converter it works - but just with Strings it doesn't.

How can I get this to work with just Strings?

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
Ginkobonsai
  • 177
  • 3
  • 13
  • No 404s in the log - also the same URLs do work when I use a wrapper class around the filename Strings. If I'd found a clue in the logs that I could interpret I wouldn't have posted. – Ginkobonsai Sep 23 '15 at 10:19
  • Sorry, I don't mean to say that there are no clues in the logs, just that I can't spot any. Can you give me a hint what I'm looking for (other than 404s, for which I checked)? Also, what confounds me is that changing things on the server side (adding the wrapper class) makes it work. – Ginkobonsai Sep 23 '15 at 11:01
  • No, the desired element doesn't end up in the HTML output - it's omitted. Instead, I get just the text (filename). If I use the wrapper class, I get the tag without problems. – Ginkobonsai Sep 23 '15 at 12:33

2 Answers2

13

This awkward behavior is confirmed by SelectOneMenuRenderer source code (line numbers match 5.2):

260            if(itemValue instanceof String) {
261                writer.startElement("td", null);
262                writer.writeAttribute("colspan", columns.size(), null);
263                writer.writeText(selectItem.getLabel(), null);
264                writer.endElement("td");
265            } 
266            else {
267                for(Column column : columns) {
268                    writer.startElement("td", null);
269                    renderChildren(context, column);
270                    writer.endElement("td");
271                }
272            }

So, if the item value is an instance of String, custom content via <p:column> is totally ignored. This does indeed not make any sense. The intuitive expectation is that the custom content is toggled by presence of var attribute and/or <p:column> children. You'd best report an issue to PrimeFaces guys to explain/improve this.

The work around, apart from providing non-String-typed item values, is to override the SelectOneMenuRenderer with a custom renderer which wraps the String in another object which happens to return exactly the same value in its toString(), such as StringBuilder. This way the renderer will be fooled that the values aren't an instance of String. Glad they didn't check for instanceof CharSequence.

public class YourSelectOneMenuRenderer extends SelectOneMenuRenderer {

    @Override
    protected void encodeOptionsAsTable(FacesContext context, SelectOneMenu menu, List<SelectItem> selectItems) throws IOException {
        List<SelectItem> wrappedSelectItems = new ArrayList<>();

        for (SelectItem selectItem : selectItems) {
            Object value = selectItem.getValue();

            if (value instanceof String) {
                value = new StringBuilder((String) value);
            }

            wrappedSelectItems.add(new SelectItem(value, selectItem.getLabel()));
        }

        super.encodeOptionsAsTable(context, menu, wrappedSelectItems);
    }

}

In order to get it to run, register it as below in faces-config.xml:

<render-kit>
    <renderer>
        <component-family>org.primefaces.component</component-family>
        <renderer-type>org.primefaces.component.SelectOneMenuRenderer</renderer-type>
        <renderer-class>com.example.YourSelectOneMenuRenderer</renderer-class>
    </renderer>
</render-kit>
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • Wow... Thanks a ton! I would never have thought that this behaviour is intentional... And double thanks for providing the nice workaround! There's a small glitch with the workaround when you use (non-String) POJOs if you have a "non item" like the `` in my example - you get a PropertyNotFoundException because the POJO's properties can't be found on the StringBuilder object. My (lazy) fix is to use in the renderer `if (value instanceof String && !"".equals((String) value)) {` – Ginkobonsai Sep 23 '15 at 17:04
  • You're welcome. As to the glitch, you should have used `itemValue="#{null}"`. See also http://stackoverflow.com/questions/11360030/best-way-to-add-a-nothing-selected-option-to-a-selectonemenu-in-jsf – BalusC Sep 23 '15 at 17:12
  • Ah, thanks - I'll use that. BTW: I've posted in the Primefaces forum and asked about this issue, let's see what they say. – Ginkobonsai Sep 23 '15 at 17:41
  • The more I look at it, the more I suspect that this special behaviour for Strings in Primefaces is meant just for the case of adding a non-option. Sadly, if you use #{null} as the itemValue, the condition in line 260 is false, so you go to the else-branch. This then renders a missing image with an empty filename. I assume I do have to add special treatment for null and/or empty strings. From your answer here [http://stackoverflow.com/a/11397505/1685666] I assume that this is not standard JSF behaviour. Oh well. – Ginkobonsai Sep 24 '15 at 08:21
  • but it is associated with string not with object, so if i want to pass object to bean how i can do that? – Irfan Nasim Dec 17 '15 at 05:27
0

The reason is that Primefaces library is especting a bean in the var attribute of the selectOneMenu component, but you are giving String objects which are not beans . So the library doesn't render any column overlay at all. You will need a bean (wrapper) in the var attribute and the corresponding Converter in the converter attribute.

Javier Haro
  • 1,255
  • 9
  • 14
  • Yeah, that's the workaround that works for me - I had just hoped that there was a simpler way... – Ginkobonsai Sep 23 '15 at 12:38
  • That is the question, there is no workaround because there is nothing to workaround. That is the way for putting images in a Primefaces selectOneMenu. If my answer clarify your question please accept my answer. To mark an answer as accepted, click on the check mark beside the answer to toggle it from hollow to green. Thank you. – Javier Haro Sep 23 '15 at 13:02
  • So a String is not a POJO? – Kukeltje Sep 23 '15 at 14:13
  • I meant bean, instead of pojo. – Javier Haro Sep 23 '15 at 15:25
  • but it is associated with string not with object, so if i want to pass object to bean how i can do that? – Irfan Nasim Dec 17 '15 at 05:27
  • create a bean class with a property returning the String value. See also http://stackoverflow.com/questions/6848970/how-to-populate-options-of-hselectonemenu-from-database – Javier Haro Dec 17 '15 at 20:37