Cómo vincular una lista de objetos con thymeleaf?

Tengo muchas dificultades para devolver un formulario al controlador, que debe contener simplemente una lista de objetos que el usuario puede editar.

El formulario se carga correctamente, pero cuando se publica, nunca parece publicar nada.

Aquí está mi formulario:

Select Client ID IP Addresss Description

Arriba funciona bien, carga la lista correctamente. Sin embargo, cuando PUBLICO, devuelve un objeto vacío (de tamaño 0). Creo que esto se debe a la falta de th:field , pero de todos modos aquí está el método de control POST:

 ... private List allClientsWithSelection = new ArrayList(); //GET method ... model.addAttribute("clientList", allClientsWithSelection) .... //POST method @RequestMapping(value="/submitQuery", method = RequestMethod.POST) public String processQuery(@ModelAttribute(value="clientList") ArrayList clientList, Model model){ //clientList== 0 in size ... } 

He intentado agregar un th:field pero independientemente de lo que haga, causa una excepción.

He intentado:

 ...    ... 

No puedo acceder a currentClient (error de comstackción), ni siquiera puedo seleccionar la lista de clientes, me da opciones como get() , add() , clearAll() etc, por lo que debe tener una matriz, sin embargo, no puedo pasar una matriz.

También intenté usar algo como th:field=${} , esto causa una excepción de tiempo de ejecución

He intentado

 th:field = "*{clientList[__currentClient.clientID__]}" 

pero también comstack el error.

¿Algunas ideas?


ACTUALIZACIÓN 1:

Tobias me sugirió que debía envolver mi lista en un wraapper. Entonces eso es lo que hice:

ClientWithSelectionWrapper:

 public class ClientWithSelectionListWrapper { private ArrayList clientList; public List getClientList(){ return clientList; } public void setClientList(ArrayList clients){ this.clientList = clients; } } 

Mi página:

 
....

Por encima carga bien: enter image description here

Entonces mi controlador:

 @RequestMapping(value="/submitQuery", method = RequestMethod.POST) public String processQuery(@ModelAttribute ClientWithSelectionListWrapper wrapper, Model model){ ... } 

La página se carga correctamente, los datos se muestran como se esperaba. Si publico el formulario sin ninguna selección, obtengo esto:

 org.springframework.expression.spel.SpelEvaluationException: EL1007E:(pos 0): Property or field 'clientList' cannot be found on null 

No estoy seguro de por qué se queja

(En el método GET tiene: model.addAttribute("wrapper", wrapper); )

enter image description here

Si hago una selección, es decir, marque la primera entrada:

 There was an unexpected error (type=Bad Request, status=400). Validation failed for object='clientWithSelectionListWrapper'. Error count: 1 

Supongo que mi controlador POST no está obteniendo el clienteWithSelectionListWrapper. No estoy seguro de por qué, ya que he configurado el objeto envoltorio para que se publique a través de th:object="wrapper" en el encabezado FORM.


ACTUALIZACIÓN 2:

¡He progresado un poco! Finalmente, el formulario enviado se está recogiendo mediante el método POST en el controlador. Sin embargo, todas las propiedades parecen ser nulas, excepto si el elemento se ha marcado o no. He hecho varios cambios, así es como está buscando:

  ....          

También agregué un constructor param-less predeterminado a mi clase contenedora y agregué un param bindingResult al método POST (no estoy seguro si es necesario).

 public String processQuery(@ModelAttribute ClientWithSelectionListWrapper wrapper, BindingResult bindingResult, Model model) 

Entonces, cuando se publica un objeto, así es como se ve: enter image description here

Por supuesto, se supone que systemInfo es nulo (en este momento), pero el ID de cliente siempre es 0, y ipAddress / Description siempre nulo. Sin embargo, el booleano seleccionado es correcto para todas las propiedades. Estoy seguro de haber cometido un error en alguna de las propiedades. De regreso a la investigación


ACTUALIZACIÓN 3:

Ok, he logrado llenar todos los valores correctamente! Pero tuve que cambiar mi td para incluir una que no es lo que quería … Sin embargo, los valores se están poblando correctamente, lo que sugiere que la spring busca una etiqueta de entrada tal vez para el mapeo de datos.

Aquí hay un ejemplo de cómo cambié los datos de la tabla clientID:

    

Ahora necesito descubrir cómo mostrarlo como datos simples, idealmente sin ninguna presencia de un cuadro de entrada …

Necesita un objeto contenedor para contener los datos enviados, como este:

 public class ClientForm { private ArrayList clientList; public ArrayList getClientList() { return clientList; } public void setClientList(ArrayList clientList) { this.clientList = clientList; } } 

y @ModelAttribute como @ModelAttribute en tu método @ModelAttribute :

 @RequestMapping(value="/submitQuery", method = RequestMethod.POST) public String processQuery(@ModelAttribute ClientForm form, Model model){ System.out.println(form.getClientList()); } 

Además, el elemento de input necesita un name y un value . Si construye directamente el html, tenga en cuenta que el nombre debe ser clientList[i] , donde i es la posición del elemento en la lista:

        

Tenga en cuenta que clientList puede contener null en posiciones intermedias. Por ejemplo, si los datos publicados son:

 clientList[1] = 'B' clientList[3] = 'D' 

el ArrayList resultante será: [null, B, null, D]

ACTUALIZACIÓN 1:

En mi ejemplo anterior, ClientForm es un contenedor para List . Pero en su caso ClientWithSelectionListWrapper contiene ArrayList . Por clientList[1] tanto, clientList[1] debería ser clientList[1].clientID y así sucesivamente con las demás propiedades que desea enviar de vuelta:

       

Creé una pequeña demostración para que puedas probarla:

Application.java

 @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 

ClientWithSelection.java

 public class ClientWithSelection { private Boolean selected; private String clientID; private String ipAddress; private String description; public ClientWithSelection() { } public ClientWithSelection(Boolean selected, String clientID, String ipAddress, String description) { super(); this.selected = selected; this.clientID = clientID; this.ipAddress = ipAddress; this.description = description; } /* Getters and setters ... */ } 

ClientWithSelectionListWrapper.java

 public class ClientWithSelectionListWrapper { private ArrayList clientList; public ArrayList getClientList() { return clientList; } public void setClientList(ArrayList clients) { this.clientList = clients; } } 

TestController.java

 @Controller class TestController { private ArrayList allClientsWithSelection = new ArrayList(); public TestController() { /* Dummy data */ allClientsWithSelection.add(new ClientWithSelection(false, "1", "192.168.0.10", "Client A")); allClientsWithSelection.add(new ClientWithSelection(false, "2", "192.168.0.11", "Client B")); allClientsWithSelection.add(new ClientWithSelection(false, "3", "192.168.0.12", "Client C")); allClientsWithSelection.add(new ClientWithSelection(false, "4", "192.168.0.13", "Client D")); } @RequestMapping("/") String index(Model model) { ClientWithSelectionListWrapper wrapper = new ClientWithSelectionListWrapper(); wrapper.setClientList(allClientsWithSelection); model.addAttribute("wrapper", wrapper); return "test"; } @RequestMapping(value = "/query/submitQuery", method = RequestMethod.POST) public String processQuery(@ModelAttribute ClientWithSelectionListWrapper wrapper, Model model) { System.out.println(wrapper.getClientList() != null ? wrapper.getClientList().size() : "null list"); System.out.println("--"); model.addAttribute("wrapper", wrapper); return "test"; } } 

test.html

      
Select Client ID IP Addresss Description

ACTUALIZACIÓN 1.B:

A continuación se muestra el mismo ejemplo que utiliza th:field y el envío de todos los demás atributos como valores ocultos.

               

Cuando desee seleccionar objetos en thymeleaf, no necesita crear un contenedor con el fin de almacenar un campo de selección boolean . El uso de dynamic fields según la guía de hoja de tomillo con la syntax th:field="*{rows[__${rowStat.index}__].variety}" es th:field="*{rows[__${rowStat.index}__].variety}" cuando desea acceder a un conjunto de objetos ya existente en una colección. No está realmente diseñado para hacer selecciones mediante el uso de objetos de contenedor IMO, ya que crea un código repetitivo innecesario y es una especie de truco.

Considere este simple ejemplo, una Person puede seleccionar las Drinks les gusta. Nota: Los constructores, getters y setters se omiten para mayor claridad. Además, estos objetos normalmente se almacenan en una base de datos, pero los utilizo en matrices de memoria para explicar el concepto.

 public class Person { private Long id; private List drinks; } public class Drink { private Long id; private String name; } 

Controladores de spring

Lo principal aquí es que estamos almacenando la Person en el Model para que podamos vincularlo a la forma dentro de th:object . En segundo lugar, las Bebidas selectableDrinks son las bebidas que una persona puede seleccionar en la interfaz de usuario.

  @GetMapping("/drinks") public String getDrinks(Model model) { Person person = new Person(30L); // ud normally get these from the database. List selectableDrinks = Arrays.asList( new Drink(1L, "coke"), new Drink(2L, "fanta"), new Drink(3L, "sprite") ); model.addAttribute("person", person); model.addAttribute("selectableDrinks", selectableDrinks); return "templates/drinks"; } @PostMapping("/drinks") public String postDrinks(@ModelAttribute("person") Person person) { // person.drinks will contain only the selected drinks System.out.println(person); return "templates/drinks"; } 

Código de plantilla

Preste mucha atención al lazo de li y cómo se usa la opción Bebidas selectableDrinks para obtener todas las bebidas posibles que se pueden seleccionar.

La checkbox th:field realmente se expande a person.drinks ya que th:object está vinculado a Person y *{drinks} simplemente es el atajo para referirse a una propiedad en el objeto Person . Usted puede pensar en esto como simplemente decirle Spring / thymeleaf que cualquier bebida seleccionada se va a poner en ArrayList en la persona de ubicación. person.drinks .

    
Drink demo

De cualquier forma … la salsa secreta está usando th:value=${drinks.id} . Esto se basa en los convertidores de spring. Cuando se publique el formulario, Spring intentará recrear a una Person y para ello necesita saber cómo convertir cualquier cadena de drink.id seleccionada en el tipo de Drink real. Nota: Si lo hizo th:value${drinks} la clave de value en la checkbox html sería la representación toString() de una Drink que no es lo que quiere, por lo tanto, ¡necesita usar la identificación! Si está siguiendo, todo lo que necesita hacer es crear su propio convertidor si aún no lo ha creado.

Sin un convertidor, recibirá un error como Failed to convert property value of type 'java.lang.String' to required type 'java.util.List' for property 'drinks'

Puede activar el registro en application.properties para ver los errores en detalle. logging.level.org.springframework.web=TRACE

Esto solo significa que la spring no sabe cómo convertir una identificación de cadena que representa una Drink . Es una Drink . El siguiente es un ejemplo de un Converter que soluciona este problema. Normalmente, se inyectaría un repository para acceder a la base de datos.

 @Component public class DrinkConverter implements Converter { @Override public Drink convert(String id) { System.out.println("Trying to convert id=" + id + " into a drink"); int parsedId = Integer.parseInt(id); List selectableDrinks = Arrays.asList( new Drink(1L, "coke"), new Drink(2L, "fanta"), new Drink(3L, "sprite") ); int index = parsedId - 1; return selectableDrinks.get(index); } } 

Si una entidad tiene un repository de datos de spring correspondiente, Spring crea automáticamente los conversores y gestionará la búsqueda de la entidad cuando se proporcione una identificación (el id. De cadena también parece estar bien, por lo que Spring realiza algunas conversiones adicionales según el aspecto). Esto es realmente genial, pero puede ser confuso de entender al principio.