codecentric

Multiple Selects mit Spring MVC

Während unserer coding night, zu der mit Sicherheit noch ein separater Blogeintrag folgt, setzen wir voll auf Spring Technologien. Dabei hat sich eine vermutlich simple Anforderung als ziemlich halsbrecherisch herausgestellt.

Wie kann man mit Spring MVC eine Select box darstellen, bei der man multiple Elemente auswählen kann, welche dann in eine Collection der Bean hinzugefügt werden?

Wir implementiere eine einfache Zeiterfassung. In dem Domänenmodell gibt es Projekte, denen Mitarbeiter zugeordnet sind. Zudem können Projekte Aufgaben haben, denen auch Mitarbeiter zugeordnet sein können. In der View zum erstellen der Aufgaben benötigen wir also eine Selectbox aus allen verfügbaren Mitarbeitern.

Modell

Hier das wichtigste der Aufgaben und Mitarbeiter:

Task

@Entity
public class Task implements Serializable {
	@Id
	@GeneratedValue
	private long id;
 
	@ManyToMany
	private Set<Staff> staffs = new HashSet<Staff>(0);
 
	//...
}

Staff

@NamedQuery(name = "staff.activeStaff", query = "select s from Staff s where s.disabled = false")
@Entity
@DiscriminatorValue("STAFF")
public class Staff extends Person {
 
	//...
}

Person

@NamedQuery(name = "person.findByUsername", query = "from Person p where p.login.username = :username")
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.STRING)
public abstract class Person implements Serializable {
 
	@Id
	@GeneratedValue
	private long id;
 
	//...
}

View

In der view, in der die Tasks erstellt werden sollen, brauchen wir also eine select box, welche alle verfügbaren Mitarbeiter anzeigt. Die ausgewählten Mitarbeiter sollen dann der Aufgabe hinzugefügt werden. Die View ist mit der Task hinterlegt, und fügt alle aktiven Mitarbeiter zu einem Attribut hinzu:

@Controller
@SessionAttributes("project")
public class ProjectController {
	@RequestMapping(method = RequestMethod.GET, value = "/project/createTask.action")
	public void createTaskView(@ModelAttribute Task task, Model model) {
		List<Staff> activeStaff = timetrackingService.getActiveStaff();
		model.addAttribute("activeStaff", activeStaff);
	}
 
	//...

Die entsprechende JSP sieht folgendermaßen aus:

<td><label for="staffs">Chose Staff: </label></td>
<td><form:select path="staffs" multiple="true" items="${activeStaff}" itemLabel="fullName" itemValue="id"/></td>
<td><form:errors path="staffs" /></td>

Das Problem, sobald man die Form submitted, bekommt man eine ServletRequestBindingException, denn Spring weiß nicht wie man aus einem String (bzw. String[] wenn mehrere Personen markiert waren) ein Set erstellt.

org.springframework.web.bind.ServletRequestBindingException: Errors binding onto object 'task'; nested exception is org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'task' on field 'staffs': rejected value [3]; codes [typeMismatch.task.staffs,typeMismatch.staffs,typeMismatch.java.util.Set,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [task.staffs,staffs]; arguments []; default message [staffs]]; default message [Failed to convert property value of type [java.lang.String] to required type [java.util.Set] for property 'staffs'; nested exception is java.lang.IllegalArgumentException: Cannot convert value of type [java.lang.String] to required type [de.codecentric.timetracking.model.Staff] for property 'staffs[0]': no matching editors or conversion strategy found]

Zudem ist der generierte HTML-code lückenhaft, er enthält keine id für die einzelnen Mitarbeiter!

<select id="staffs" multiple="multiple" name="staffs">
<option value="">firstname lastname</option>
<option value="">aaaa aaaa</option>
<option value="">firstname lastName</option>
<option value="">firstname aaaa</option>
<option value="">firstname lastname</option>
</select>
<input type="hidden" value="1" name="_staffs"/>

Id als String

Um die ID des Mitarbeiters in das value-Attribut der option zu bekommen, kann man einen eigenen Getter auf der Person implementieren, und diesen dann in der JSP statt der ID verwenden:

public abstract class Person implements Serializable {
	//...
	public String getIdAsString() {
		return new Long(id).toString();
	}
}
<form:select path="staffs" itemValue="idAsString" multiple="true" items="${activeStaff}" itemLabel="fullName"/>

InitBinder

Die Lösung für das Binding-Problem ist, dass wir für das Attribut ‘staffs’ einen eigenen PropertyEditor registrieren müssen. Spring bringt eine Reihe eigener PropertyEditoren mit, in diesem Falle können wir den CustomCollectionEditor wiederverwenden. Um von der String-ID wieder auf den Mitarbeiter mappen zu können, müssen wir uns auch noch eine Map initialisieren, die dieses Mapping vorhält.

public class ProjectController {
 
private Map<String, Staff> staffCache;
 
@RequestMapping(method = RequestMethod.GET, value = "/project/createTask.action")
public void createTaskView(@ModelAttribute Task task, Model model) {
	List<Staff> activeStaff = timetrackingService.getActiveStaff();
	staffCache = new HashMap<String, Staff>();
	for (Staff staff : activeStaff) {
		staffCache.put(staff.getIdAsString(), staff);
	}
	model.addAttribute("activeStaff", activeStaff);
}
 
@InitBinder
protected void initBinder(WebDataBinder binder) throws Exception {
	binder.registerCustomEditor(Set.class, "staffs", new CustomCollectionEditor(Set.class) {
		protected Object convertElement(Object element) {
			if (element instanceof Staff) {
				System.out.println("Converting from Staff to Staff: " + element);
				return element;
			}
			if (element instanceof String) {
				Staff staff = staffCache.get(element);
				System.out.println("Looking up staff for id " + element + ": " + staff);
				return staff;
			}
			System.out.println("Don't know what to do with: " + element);
			return null;
		}
	});
}

Der Code enthält noch etwas Debug-output nach System.out, der natürlich noch entfernt werden muss. Er zeigt aber sehr schön, dass der Code sehr (zu?) häufig aufgerufen wird. Außerdem wird erwartetn, dass die Property in beide Richtungen konvertiert werden kann!

Allein wenn die select box angezeit werden soll, steht folgendes im Log:

Looking up staff for id 1: Staff(firstname lastname)
Looking up staff for id 1: Staff(firstname lastname)
Converting from Staff to Staff: Staff(firstname lastname)
Looking up staff for id 2: Staff(aaaa aaaa)
Looking up staff for id 2: Staff(aaaa aaaa)
Converting from Staff to Staff: Staff(aaaa aaaa)
Looking up staff for id 3: Staff(firstname lastName)
Looking up staff for id 3: Staff(firstname lastName)
Converting from Staff to Staff: Staff(firstname lastName)
Looking up staff for id 4: Staff(firstname aaaa)
Looking up staff for id 4: Staff(firstname aaaa)
Converting from Staff to Staff: Staff(firstname aaaa)
Looking up staff for id 5: Staff(firstname lastname)
Looking up staff for id 5: Staff(firstname lastname)
Converting from Staff to Staff: Staff(firstname lastname)

Wenn man einen Mitarbeiter aus der Select-Box auswählt und die Form submitted, findet sich dann aber wie erwartet nur ein Eintrag im Log:

Looking up staff for id 3: Staff(firstname lastName)

Fazit

Angesichts der Masse an Webframeworks, die es gibt, finde ich es erschreckend Kompliziert so etwas einfaches wie eine multiple select Box mit Spring MVC zu implementieren. Da ich mir Spring MVC aber auch erst seit gestern genauer angesehen habe, verstehe ich es vielleicht noch nicht richtig, von daher bin ich sehr für Vorschläge zu haben, wie man das beschriebene Szenario mit Spring MVC-Mitteln eleganter und einfacher implementieren kann.

  • Facebook
  • Delicious
  • Digg
  • StumbleUpon
  • Mister Wong
  • Reddit
  • Instapaper
  • Technorati
  • Blogger
Andreas Ebbert-Karroum

 

Weitere Beiträge aus dem codecentric Blog

25 Antworten auf Multiple Selects mit Spring MVC

  1. Chandresh sagt:

    Many thanks to you Andreas ! Yours is the best explanation of how to get this multiple select going – for use to set a “Collection” into a bean.

  2. Glad you found it helpful, it indeed took a few hours to figure that out.

  3. Maarten Donders sagt:

    Ein Hinweis dazu:

    Die Anmeldung eines CustomCollectionEditor ist, wenn die anzubindenden Collections mittels Generics typisiert sind, nicht erforderlich. Hier reicht ein PropertyEditor für den jeweiligen Typ. Spring kümmert sich dann darum, daß auch die Collections korrekt gebunden werden.

    Allerdings führt die Modifizierung von Klassen mittels CGLIB, wie sie beispielsweise von Hibernate durchgeführt wird, dazu, daß die Typisierung der Collections verlorengeht. In diesem Fall kann Spring sie nicht mehr auflösen und ist ein explizites Anmelden eines CustomCollectionEditors notwendig.

    Eigentlich ist es also ganz einfach mit Spring-MVC eine multiple select box zu implementieren. Nur wenn die Typinformationen nicht zur Verfügung stehen, wird es schwierig. Wie soll es auch ohne Typinformationen gehen?

  4. Madhuri sagt:

    Just Perfect! I have read many blogs/documents for the multi select, but no luck. This worked just right in a few minutes! You made my day. Thank you so much!!!

  5. Pingback: Confluence: Development Space

  6. lance sagt:

    This sounded great. I tried it verbatim but didn’t have success. The form:select tag worked as expected but it would never bind the user’s selections to the List. Spring can be too finicky. It must be version dependent.

  7. I lance, sorry to hear that the instructions did not work for you. I documented what I did to get it working. What error message are you getting now? I doubt that this behaviour is dependant on the spring version.

  8. Karl Minor sagt:

    Nothing pleases me more than Googling a problem I’ve run into and finding a kind soul like yourself who has already seen the problem, solved it, and documented the solution for the benefit of perfect strangers. Thank you much, sir.

    And remember that for every person that responds, there are probably ten others you’ve helped that were in too much of a hurry to say thanks.

    Please forward this to your parents to remind them that they done good.

    Karl

  9. Hi Karl,

    thanks for your kind words! I will forward them to my parents and also my boss ;)

    Andreas

  10. john sagt:

    can you update this leveraging spring 3 (type-conversion)?

  11. Hi John,

    thanks for asking. When we run into this problem again with Spring 3 we will certainly update the blog. In the meantime, you might do so and link to your results? :)

    Andreas

  12. Sudheendra sagt:

    Thanks. I was struggling for a while but now have finally got it working.

  13. Aruna sagt:

    Hi Andreas,

    This guide help let.
    Is there any way to register the editor without specifying the field name? Bcoz when I registering with field name, i cannot use the editor globally.
    Thanks

  14. ugender sagt:

    Found interesting that if your bean/object has a constructor with String argument, everything works seamlessly (working with Spring 3).
    Also, multiple=”true” is not required if you are mapping it to a List, spring will consider those options as multiple :)

  15. Creative sagt:

    Is there another way to get this done without the use of Custom editors?
    Seems like a very cumbersome process!

  16. I can confirm that with Spring 3.0 it’s a lot easier.

    Just use something like:

    <form:select path="bears" multiple="true" items="${allbears}" itemLabel="name" itemValue="id" />

    And binded object’s setBears method will be called with a collection populated from selected members (looked up by ID-s of course).

    However, if using JPA entities, relation objects must be properly managed. Eg for ManyToOne relation, a setter must take care that changes are propagated to objects that manage the relation (hold the foreign key).

    	public void setBears(Collection<Bear> bears) {
    		if(bears == null) {
    			this.bears = new ArrayList<Bear>();
    		} else {
    			this.bears = bears;
    		}
    		for(Bear b: this.bears) {
    			if(b.getCage() != this) {
    				b.setCage(this);
    			}
    		}
    	}
    • Hi Antti,

      thanks a lot for your update regarding Spring 3! I will mention your answer in the article :)

      Thanks,
      Andreas

    • Draco sagt:

      Can you give a full example please?
      I tried the your method with Spring 3.0 but i get the same conversion error.

    • Rick sagt:

      @Antti,
      I’m not seeing your solution work either.
      Can someone confirm that Antti’s solution should work (Spring 3.1) ?

      For example I have

      Dim
      int key
      String descr
      
      MassExportVO
      private List biaItems; 
      //settter/getters
      
      
      
      

      When this form submits however I end up with:

      [Failed to convert property value of type 'java.lang.String[]‘ to required type ‘java.util.List’ for property ‘biaItems’; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [com.foot.domainmodel.Dim] for property ‘biaItems[0]‘: no matching editors or conversion strategy found]

  17. Neha sagt:

    Hi,
    I am trying to by default select all the values populated in the list.

    I want all the countries to be selected by default.
    Could you help me with this?
    Thanks

  18. Neha sagt:

    Hi..
    I am using the form:select tag.I could not find any way to select all the options by default.

     

    I want to select all the countries by default.
    Please advise.
    Thanks

  19. cyril sagt:

    Excellent tutorial!

  20. matiangul sagt:

    This is great. I’m new with Spring and it helps me a lot.
    Thank you.

  21. Andrei Asmarandei sagt:

    @Rick & Draco:

    To solve this you need to add a Converter where you create a new Dim object from the id of type String.

    public class StringToDim implements Converter
    @Override
    public Dim convert(String source) {
    Dim d = new Dim ();
    d.setId(Integer.parseInt(source));
    return d;
    }

    The Converter must be mapped in MvcConfig.java like this:
    public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new StringToDim ());
    }

    Also it’s a good ideea to create equals and hashset methods for class Dim :)

    • Mikko Suonio sagt:

      @Andrei: Thanks for mentioning the usefulness of equals method here.

      I found that it seems to be necessary for the entity class (Dim in the example above) to have equals defined. Otherwise, the selected attribute of the options is not generated and the current values are not shown as selected. Therefore, it cannot decide to which options the current value is equal to.

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>