Hoe bind je een objectenlijst met tijmblad?

Ik heb veel moeite met het terugsturen van een formulier naar de controller, dat gewoon een arraylijst met objecten zou moeten bevatten die de gebruiker kan bewerken.

Het formulier wordt correct geladen, maar wanneer het is gepost, lijkt het nooit iets te posten.

Hier is mijn formulier:

<form action="#" th:action="@{/query/submitQuery}" th:object="${clientList}" method="post">
<table class="table table-bordered table-hover table-striped">
<thead>
    <tr>
        <th>Select</th>
        <th>Client ID</th>
        <th>IP Addresss</th>
        <th>Description</th>            
   </tr>
 </thead>
 <tbody>     
     <tr th:each="currentClient, stat : ${clientList}">         
         <td><input type="checkbox" th:checked="${currentClient.selected}" /></td>
         <td th:text="${currentClient.getClientID()}" ></td>
         <td th:text="${currentClient.getIpAddress()}"></td>
         <td th:text="${currentClient.getDescription()}" ></td>
      </tr>
  </tbody>
  </table>
  <button type="submit" value="submit" class="btn btn-success">Submit</button>
  </form>

Bovenstaande werkt prima, het laadt de lijst correct op. Wanneer ik echter POST, retourneert het een leeg object (van grootte 0). Ik geloof dat dit te wijten is aan het ontbreken van th:field, maar hoe dan ook, hier is de POST-methode van de controller:

...
private List<ClientWithSelection> allClientsWithSelection = new ArrayList<ClientWithSelection>();
//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
    ...
}

Ik heb geprobeerd een th:fieldtoe te voegen, maar wat ik ook doe, het veroorzaakt een uitzondering.

Ik heb geprobeerd:

...
<tr th:each="currentClient, stat : ${clientList}">   
     <td><input type="checkbox" th:checked="${currentClient.selected}"  th:field="*{}" /></td>
    <td th th:field="*{currentClient.selected}" ></td>
...

Ik heb geen toegang tot currentClient (compileerfout), ik kan niet eens clientList selecteren, het geeft me opties zoals get(), add(), clearAll()etc, dus het zou een array moeten hebben, maar ik kan geen array doorgeven.

Ik heb ook geprobeerd iets als th:field=${}te gebruiken, dit veroorzaakt runtime-uitzondering

Ik heb het geprobeerd

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

maar ook een compileerfout.

Enig idee?


UPDATE 1:

Tobias suggereerde dat ik mijn lijst in een wikkel moest doen. Dus dat is wat ik deed:

ClientWithSelectionWrapper:

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

Mijn pagina:

<form action="#" th:action="@{/query/submitQuery}" th:object="${wrapper}" method="post">
....
 <tr th:each="currentClient, stat : ${wrapper.clientList}">
     <td th:text="${stat}"></td>
     <td>
         <input type="checkbox"
                th:name="|clientList[${stat.index}]|"
                th:value="${currentClient.getClientID()}"
                th:checked="${currentClient.selected}" />
     </td>
     <td th:text="${currentClient.getClientID()}" ></td>
     <td th:text="${currentClient.getIpAddress()}"></td>
     <td th:text="${currentClient.getDescription()}" ></td>
 </tr>

Bovenstaande laadt prima:
voer hier de afbeeldingsbeschrijving in

Toen mijn controller:

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

De pagina wordt correct geladen, de gegevens worden weergegeven zoals verwacht. Als ik het formulier zonder enige selectie post, krijg ik dit:

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

Ik weet niet zeker waarom het klaagt

(In de GET-methode heeft het: model.addAttribute("wrapper", wrapper);)

voer hier de afbeeldingsbeschrijving in

Als ik dan een keuze maak, d.w.z. de eerste invoer aanvinken:

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

Ik vermoed dat mijn POST-controller de clientWithSelectionListWrapper niet krijgt. Ik weet niet zeker waarom, aangezien ik het wrapper-object heb ingesteld om terug te worden gepost via de th:object="wrapper"in de FORM-header.


UPDATE 2:

Ik heb wat vooruitgang geboekt! Eindelijk wordt het ingediende formulier opgehaald door de POST-methode in controller. Alle eigenschappen lijken echter nul te zijn, behalve of het item is aangevinkt of niet. Ik heb verschillende wijzigingen aangebracht, dit is hoe het eruit ziet:

<form action="#" th:action="@{/query/submitQuery}" th:object="${wrapper}" method="post">
....
 <tr th:each="currentClient, stat : ${clientList}">
     <td th:text="${stat}"></td>
     <td>
         <input type="checkbox"
                th:name="|clientList[${stat.index}]|"
                th:value="${currentClient.getClientID()}"
                th:checked="${currentClient.selected}"
                th:field="*{clientList[__${stat.index}__].selected}">
     </td>
     <td th:text="${currentClient.getClientID()}"
         th:field="*{clientList[__${stat.index}__].clientID}"
         th:value="${currentClient.getClientID()}"
     ></td>
     <td th:text="${currentClient.getIpAddress()}"
         th:field="*{clientList[__${stat.index}__].ipAddress}"
         th:value="${currentClient.getIpAddress()}"
     ></td>
     <td th:text="${currentClient.getDescription()}"
         th:field="*{clientList[__${stat.index}__].description}"
         th:value="${currentClient.getDescription()}"
     ></td>
     </tr>

Ik heb ook een standaard paramloze constructor toegevoegd aan mijn wrapper-klasse en een bindingResultparam toegevoegd aan de POST-methode (niet zeker of dat nodig is).

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

Dus wanneer een object wordt gepost, ziet het er zo uit:
voer hier de afbeeldingsbeschrijving in

Natuurlijk wordt systemInfo verondersteld null te zijn (in dit stadium), maar de clientID is altijd 0, en ipAddress/Description altijd null. De geselecteerde boolean is echter correct voor alle eigenschappen. Ik weet zeker dat ik ergens een fout heb gemaakt op een van de eigendommen. Terug naar onderzoek.


UPDATE 3:

Ok, ik heb alle waarden correct ingevuld! Maar ik moest mijn tdwijzigen om een ​​<input />op te nemen, wat niet is wat ik wilde… Desalniettemin worden de waarden correct ingevuld, wat een lente-look suggereert voor een invoertag misschien voor datamapping?

Hier is een voorbeeld van hoe ik de clientID-tabelgegevens heb gewijzigd:

<td>
 <input type="text" readonly="readonly"                                                          
     th:name="|clientList[${stat.index}]|"
     th:value="${currentClient.getClientID()}"
     th:field="*{clientList[__${stat.index}__].clientID}"
  />
</td>

Nu moet ik uitzoeken hoe ik het als gewone gegevens kan weergeven, idealiter zonder de aanwezigheid van een invoervak…


Antwoord 1, autoriteit 100%

Je hebt een wrapper-object nodig om de ingediende gegevens te bewaren, zoals deze:

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

en gebruik het als de @ModelAttributein uw processQuery-methode:

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

Bovendien heeft het input-element een nameen een valuenodig. Als je de html rechtstreeks bouwt, houd er dan rekening mee dat de naam clientList[i]moet zijn, waarbij ide positie van het item in de lijst is:

<tr th:each="currentClient, stat : ${clientList}">         
    <td><input type="checkbox" 
            th:name="|clientList[${stat.index}]|"
            th:value="${currentClient.getClientID()}"
            th:checked="${currentClient.selected}" />
     </td>
     <td th:text="${currentClient.getClientID()}" ></td>
     <td th:text="${currentClient.getIpAddress()}"></td>
     <td th:text="${currentClient.getDescription()}" ></td>
  </tr>

Merk op dat clientListnullkan bevatten bij
tussenstanden. Als geposte gegevens bijvoorbeeld zijn:

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

de resulterende ArrayListis: [null, B, null, D]

UPDATE 1:

In mijn voorbeeld hierboven is ClientFormeen wrapper voor List<String>. Maar in jouw geval bevat ClientWithSelectionListWrapperArrayList<ClientWithSelection>. Daarom moet clientList[1]clientList[1].clientIDzijn enzovoort met de andere eigenschappen die u terug wilt sturen:

<tr th:each="currentClient, stat : ${wrapper.clientList}">
    <td><input type="checkbox" th:name="|clientList[${stat.index}].clientID|"
            th:value="${currentClient.getClientID()}" th:checked="${currentClient.selected}" /></td>
    <td th:text="${currentClient.getClientID()}"></td>
    <td th:text="${currentClient.getIpAddress()}"></td>
    <td th:text="${currentClient.getDescription()}"></td>
</tr>

Ik heb een kleine demo gemaakt, zodat je hem kunt testen:

Applicatie.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<ClientWithSelection> clientList;
   public ArrayList<ClientWithSelection> getClientList() {
      return clientList;
   }
   public void setClientList(ArrayList<ClientWithSelection> clients) {
      this.clientList = clients;
   }
}

TestController.java

@Controller
class TestController {
   private ArrayList<ClientWithSelection> allClientsWithSelection = new ArrayList<ClientWithSelection>();
   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

<!DOCTYPE html>
<html>
<head></head>
<body>
   <form action="#" th:action="@{/query/submitQuery}" th:object="${wrapper}" method="post">
      <table class="table table-bordered table-hover table-striped">
         <thead>
            <tr>
               <th>Select</th>
               <th>Client ID</th>
               <th>IP Addresss</th>
               <th>Description</th>
            </tr>
         </thead>
         <tbody>
            <tr th:each="currentClient, stat : ${wrapper.clientList}">
               <td><input type="checkbox" th:name="|clientList[${stat.index}].clientID|"
                  th:value="${currentClient.getClientID()}" th:checked="${currentClient.selected}" /></td>
               <td th:text="${currentClient.getClientID()}"></td>
               <td th:text="${currentClient.getIpAddress()}"></td>
               <td th:text="${currentClient.getDescription()}"></td>
            </tr>
         </tbody>
      </table>
      <button type="submit" value="submit" class="btn btn-success">Submit</button>
   </form>
</body>
</html>

UPDATE 1.B:

Hieronder staat hetzelfde voorbeeld met gebruik van th:fielden alle andere attributen terugsturen als verborgen waarden.

<tbody>
    <tr th:each="currentClient, stat : *{clientList}">
       <td>
          <input type="checkbox" th:field="*{clientList[__${stat.index}__].selected}" />
          <input type="hidden" th:field="*{clientList[__${stat.index}__].clientID}" />
          <input type="hidden" th:field="*{clientList[__${stat.index}__].ipAddress}" />
          <input type="hidden" th:field="*{clientList[__${stat.index}__].description}" />
       </td>
       <td th:text="${currentClient.getClientID()}"></td>
       <td th:text="${currentClient.getIpAddress()}"></td>
       <td th:text="${currentClient.getDescription()}"></td>               
    </tr>
 </tbody>

Antwoord 2, autoriteit 6%

Als je objecten in thymeleaf wilt selecteren, hoef je eigenlijk geen wrapper te maken om een ​​booleanselectieveld op te slaan. Het gebruik van dynamic fieldsvolgens de thymeleaf-gids met syntaxis th:field="*{rows[__${rowStat.index}__].variety}"is goed voor wanneer u toegang wilt tot een reeds bestaande set objecten in een verzameling. Het is niet echt ontworpen om selecties uit te voeren met behulp van wrapper-objecten IMO, omdat het onnodige boilerplate-code creëert en een soort hack is.

Beschouw dit eenvoudige voorbeeld, een Personkan Drinksselecteren die hij lekker vindt. Opmerking: Constructors, Getters en setters zijn voor de duidelijkheid weggelaten. Deze objecten worden normaal gesproken ook opgeslagen in een database, maar ik gebruik geheugenarrays om het concept uit te leggen.

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

Veerregelaars

Het belangrijkste hier is dat we de Personopslaan in het Modelzodat we het kunnen binden aan het formulier binnen th:object.
Ten tweede zijn de selectableDrinksde drankjes die een persoon kan selecteren in de gebruikersinterface.

  @GetMapping("/drinks")
   public String getDrinks(Model model) {
        Person person = new Person(30L);
        // ud normally get these from the database.
        List<Drink> 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";
    }

Sjablooncode

Let goed op de li-lus en hoe selectableDrinkswordt gebruikt om alle mogelijke drankjes te krijgen die kunnen worden geselecteerd.

Het selectievakje th:fieldwordt echt uitgebreid naar person.drinksaangezien th:objectgebonden is aan Personen *{drinks}is gewoon de snelkoppeling naar het verwijzen naar een eigenschap op het Person-object. Je kunt dit zien als gewoon lente/tijmblad vertellen dat alle geselecteerde Drinksin de ArrayListop locatie person.drinkszullen worden geplaatst.

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" >
<body>
<div class="ui top attached segment">
    <div class="ui top attached label">Drink demo</div>
    <form class="ui form" th:action="@{/drinks}" method="post" th:object="${person}">
        <ul>
            <li th:each="drink : ${selectableDrinks}">
                <div class="ui checkbox">
                    <input type="checkbox" th:field="*{drinks}" th:value="${drink.id}">
                    <label th:text="${drink.name}"></label>
                </div>
            </li>
        </ul>
        <div class="field">
            <button class="ui button" type="submit">Submit</button>
        </div>
    </form>
</div>
</body>
</html>

Hoe dan ook… de geheime saus gebruikt th:value=${drinks.id}. Dit is afhankelijk van veerconverters. Wanneer het formulier is gepost, zal Spring proberen een Personopnieuw aan te maken en om dit te doen, moet het weten hoe de geselecteerde drink.id-strings worden omgezet in de daadwerkelijke Drink-type. Opmerking: als u th:value${drinks}deed, zou de value-sleutel in het selectievakje html de toString()-representatie zijn van een Drinkwat niet is wat je wilt, dus je moet de id gebruiken!. Als je meedoet, hoef je alleen maar je eigen converter te maken als die nog niet is gemaakt.

Zonder converter krijg je een foutmelding zoals
Failed to convert property value of type 'java.lang.String' to required type 'java.util.List' for property 'drinks'

Je kunt inloggen op application.propertiesinschakelen om de fouten in detail te zien.
logging.level.org.springframework.web=TRACE

Dit betekent alleen dat Spring niet weet hoe ze een string-ID die een drink.idvoorstelt, moet omzetten in een Drink. Het onderstaande is een voorbeeld van een Converterdie dit probleem verhelpt. Normaal gesproken zou je een repository injecteren om toegang te krijgen tot de database.

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

Als een entiteit een overeenkomstige lente-gegevensrepository heeft, maakt spring automatisch de converters en zal het ophalen van de entiteit afhandelen wanneer een id wordt opgegeven (string-ID lijkt ook goed te zijn, dus spring doet daar wat extra conversies, zo te zien). Dit is echt cool, maar kan in het begin verwarrend zijn om te begrijpen.

Other episodes