views:

1827

answers:

3

I am trying to display my command objects collection field inside a list box. Inside said collection is a field, id and name. I want use the id as the html option value and the name as the option text. See the code below;

<form:select id="customCollection" path="customCollection" size="10">
    <form:options items="${command.customCollection}" itemValue="id" itemLabel="name"/>
</form:select>

Name prints out fine, but value is left blank. Here is the output HTML;

<option selected="selected" value="">name-value</option>


My initial assumption was that my data was incorrect, but after putting the following code in my page;

<c:forEach items="${command.customCollection}" var="c">
    ${c.id} : ${c.name} <br>
</c:forEach>

both the id and the name are correctly printed out. So my data is correctly being delivering to my view. Which makes me assume I am either using form:options incorrectly or hitting some bug in form:options.

Can anyone help me out here?

EDIT:
Thanks to the help of BacMan and delfuego, I've been able to narrow down this issue to my binder.

Previously I was assigning the value in my element to the name of the row, here is my initial binder;

binder.registerCustomEditor(Collection.class, "customCollection",
        new CustomCollectionEditor(Collection.class) {

    @Override
    protected Object convertElement(Object element) {
        String name = null;

        if (element instanceof String) {
            name = (String) element;
        }
        return name != null ? dao.findCustomByName(name) : null;
    }
});

When I remove this code from my initBinder method the row value is correctly inserted into the form, but I need a customEditor to convert said value into a database object.

So this is my new attempt at a binder;

binder.registerCustomEditor(Collection.class, "customCollection",
        new CustomCollectionEditor(Collection.class) {

    @Override
    protected Object convertElement(Object element) {
        Integer id = null;

        if (element instanceof Integer) {
            id = (Integer) element;
        }
        return id != null ? dao.find(Custom.class, id) : null;
    }
});

However this is causing the same behavior as the previous binder and making the value not show up. Any ideas about what I am doing wrong here?

EDIT 2:
As I mentioned above, if I comment out my custom binder then the Custom object does load its id and name correctly for the view portion of the form, but then never binds back into the parent object when I attempt to save it. So I really think the issue is with my binder.

I've placed debugging statements inside my convertElement method. Everything looks like it should be worked, the dao is correctly pulling objects from the database. The only behavior that strikes me as suspect is that the convertElement method is called twice for each Custom item.

A: 

One thing that doesn't seem right to me is that command.customCollection is being used both to populate the possible values for your form's select input AND to bind to the ultimate value(s) selected by the user with that select input. This doesn't make sense, at least to me... for example, if I had a form select for choosing the U.S. state of an address, I'd populate that select with a collection of valid states, but I'd bind the value of the select to the one state which was ultimately chosen by the user.

Try this: create your customCollection object outside of the context of your command object. In other words, right now your customCollection is a property of your command object; instead of this, pull that object out of the command object and make it its own page attribute. In the Spring MVC model, things like that that'll be used as the data sources for pulldowns and whatnot are typically known as reference data; in a SimpleFormController, this data gets populated in the appropriately-named SipleFormController#referenceData method. This separates the two different concepts -- the possible values for the select live in the reference data, and the ultimate value(s) chosen by the user live in the command object bound to the form and/or the select input.

So assuming this is in a SimpleFormController, try to add (or appropriately modify) referenceData as such:

@Override
protected Map<?, ?> referenceData(HttpServletRequest request, Object command, Errors errors) throws Exception {
  CustomCollection customCollection = new CustomCollection();
  // ...populate your custom collection
  request.setAttribute("customCollection", customCollection);
  return super.referenceData(request, command, errors);
}

And then change your to:

<form:select id="customCollection" path="command" size="10">
  <form:options items="${customCollection}" itemValue="id" itemLabel="name"/>
</form:select>

Does this make sense?

delfuego
Makes sense, giving it a try now. Just to give you an idea of what I am doing; I have two list boxes of states, to borrow from your example, one box has selected states and the other has the states not selected. You move states into the selected state box to select them, and vice versa.
James McMahon
Just tried it out, when referring to a customCollection in the reference data I get the exact same behavior as pulling the data from inside the command object.
James McMahon
So then the list of all possible states is reference data, and should be populated in the the `referenceData` method; the list of chosen states will then end up in your command object. Reduce it to the least-complex example: a form select input that allows multiple selections (`<select name="foo" id="foo" multiple>...</select>`) -- the options come from your reference pool of the possible choices for a user, the ultimate choices the user makes are sent back with the form submission and used by Spring MVC to populate your command object.
delfuego
That is what I am doing now.
James McMahon
What version of Spring are you using? (Just trying to eliminate differences...)
delfuego
2.5 (Not sure of the sub-version, I am using the jar that comes standard with Netbeans 6.7).
James McMahon
+1  A: 

I'll attempt to provide a working sample, which may help to debug the issue you are having:

<form:form commandName="client" method="post" action="${edit_client}">
                <form:select path="country">
                  <form:options items="${requestScope.countries}" itemValue="id" itemLabel="name"/>
                </form:select></form:form>

Here is my Country class, which is member variable in my command object.

public class Country {

private Integer id;
private String name;

public Integer getId() {
    return id;
}

public void setId(Integer id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

@Override
public boolean equals(final Object anObject) {
    if (anObject == null) {
        return false;
    } else if (this == anObject) {
        return true;
    } else if (anObject instanceof Country) {
        final Country aCountry = (Country) anObject;
        Integer aCountryId = aCountry.getId();
        if (aCountryId != null) {
            return aCountry.getId().equals(id);
        }
    }
    return false;
}

@Override
public int hashCode() {
    return id;
}

}

I use a custom property editor in the initBinder method of my Controller. I'll leave out the implementation because it uses a generic implementation.

binder.registerCustomEditor(Country.class, "country", editorServiceFactory.getPropertyEditor(Country.class, CustomPropertyEditor.class));

Here is the reference data (this method is called from the referenceData method of the Controller):

public Map<String, List<?>> getDemographicReferenceData() {
    Map<String, List<?>> referenceData = new HashMap<String, List<?>>();
    referenceData.put("countries", countryDAO.findAll());
    return referenceData;
}

I should mention I'm using Spring 2.5

BacMan
I am going to pick this up on Monday, but just a quick question does your binder affect the data being placed on the formers, I thought that the binding didn't take place under after the data was submited. That binding custom editor may be my issue, or it could be my equals method.
James McMahon
It does, to determine if the option is selected and again on submit to convert the id back to an object.
BacMan
Let me rephrase that. The equals method determines if the option is selected. To convert the id to an object, the custom editor is used.
BacMan
A: 

This is one of those issues that once I understood what was going wrong I don't understand how it ever worked in the first place.

I was using CustomCollectionEditor in completely the wrong way. According to Marten Deinum's post in this thread,

As I stated in the other thread already the CustomCollectionEditor is to create Collections (List, Set, ?). So it will populate the desired collection with elements of the desired type.

However it is not intended to convert single elements into a value. It is designed to work on Collections, not on a single Role instance. You want 1 PropertyEditor to do 2 tasks for you.

So it was creating a unique collection for each element that eventually got nulled out in the Spring code when it attempted to generate the HTML.

This is what I ended up doing,

binder.registerCustomEditor(Custom.class,
        new PropertyEditorSupport() {

            @Override
            public void setAsText(String text) {
                Custom custom = dao.find(Custom.class,
                        Integer.parseInt(text));
                setValue(Custom);
            }
        });

I have no idea why my previous CustomCollectionEditor ever worked with the names as values.

James McMahon