Spring MVC form:options tag not wiring into my objects Id?

20,772

Solution 1

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.

Solution 2

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'm using Spring 2.5

Solution 3

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?

Share:
20,772
James McMahon
Author by

James McMahon

Blogging at https://dev.to/jamesmcmahon.

Updated on July 05, 2022

Comments

  • James McMahon
    James McMahon almost 2 years

    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.