Как связать список объектов с thymeleaf?

у меня много трудностей с отправкой формы на контроллер, которая должна содержать просто arraylist объектов, которые пользователь может редактировать.

форма загружается правильно, но когда она публикуется, кажется, что она никогда ничего не публикует.

вот моя форма:

<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>

выше работает нормально, он загружает список правильно. Однако, когда я публикую, он возвращает пустой объект (размером 0). Я считаю, что это связано с отсутствием th:field, но в любом случае вот метод контроллера POST:

...
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
    ...
}

я пробовал добавлять th:field но независимо от того, что я делаю, это вызывает исключение.

я пробовал:

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

    <td th th:field="*{currentClient.selected}" ></td>
...

я не могу получить доступ к currentClient (ошибка компиляции), я даже не могу выбрать clientList, это дает мне такие параметры, как get(), add(), clearAll() и т. д., Поэтому у него должен быть массив, однако я не могу передать массив.

я также пробовал использовать что-то как th:field=${}, это вызывает исключение во время выполнения

я пробовал

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

, но и ошибка компиляции.

какие идеи?


обновление 1:

Тобиас предложил, что мне нужно, чтобы обернуть мой список в wraapper. Вот что я сделал:--24-->

ClientWithSelectionWrapper:

public class ClientWithSelectionListWrapper {

private ArrayList<ClientWithSelection> clientList;

public List<ClientWithSelection> getClientList(){
    return clientList;
}

public void setClientList(ArrayList<ClientWithSelection> clients){
    this.clientList = clients;
}
}

мою страницу:

<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>

загрузится нормально: enter image description here

затем мой контроллер:

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

страница загружается правильно, данные отображаются так, как ожидалось. Если я опубликую форму без какого-либо выбора, я получу следующее:

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

не уверен, почему он жалуется

(в методе GET он имеет:model.addAttribute("wrapper", wrapper);)

enter image description here

если я сделаю выбор, то есть поставлю галочку на первой записи:

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

я думаю, что мой POST controller не получает clientWithSelectionListWrapper. Не уверен, почему, так как я установил объект-оболочку для отправки обратно через th:object="wrapper" в заголовке формы.


обновление 2:

я сделал некоторый прогресс! Наконец, представленная форма подбирается методом POST в контроллере. Однако все свойства отображаются как null, за исключением того, был ли элемент отмечен галочкой или нет. Я внесла различные изменения, вот как это бывает. смотрю:

<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>

я также добавил конструктор без параметров по умолчанию в свой класс-оболочку и добавил bindingResult param для публикации метода (не уверен, если это необходимо).

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

поэтому, когда объект публикуется, вот как он выглядит: enter image description here

конечно, systemInfo должен быть null (на данном этапе), но clientID всегда равен 0, а ipAddress/Description всегда null. Выбранное логическое значение является правильным, хотя для всех свойства. Я уверен, что где-то ошибся в одном из домов. Вернемся к расследованию.


обновление 3:

Ok мне удалось правильно заполнить все значения! Но я должен был изменить свой td включить <input /> чего я не хотел... Тем не менее, значения заполняются правильно, предполагая, что spring ищет входной тег, возможно, для отображения данных?

вот пример того, как я изменил данные таблицы clientID:

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

теперь мне нужно выяснить, как отобразить его как простые данные, в идеале без какого-либо присутствия поля ввода...

2 ответов


вам нужен объект-оболочка для хранения отправленных данных, например:

public class ClientForm {
    private ArrayList<String> clientList;

    public ArrayList<String> getClientList() {
        return clientList;
    }

    public void setClientList(ArrayList<String> clientList) {
        this.clientList = clientList;
    }
}

и использовать его как @ModelAttribute в своем processQuery способ:

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

кроме того,input элемент нужен name и value. Если вы непосредственно создаете html, то учтите, что имя должно быть clientList[i], где i позиция элемента в списке:

<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>

обратите внимание, что clientList может содержать null at промежуточное положение. Например, если размещенные данные:

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

в результате ArrayList будет: [null, B, null, D]

обновление 1:

в моем exmple выше, ClientForm - это оболочка для List<String>. Но в твоем случае ... --24--> содержит ArrayList<ClientWithSelection>. Поэтому!--26--> должно быть clientList[1].clientID и так далее с другими свойствами вы хотите отправить обратно:

<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>

я создал небольшое демо, так что вы можете проверить это:

приложение.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";
   }
}

когда вы хотите выбрать объекты в thymeleaf, вам на самом деле не нужно создавать обертку для хранения boolean выберите поле. Используя dynamic fields согласно руководству thymeleaf с синтаксисом th:field="*{rows[__${rowStat.index}__].variety}" это хорошо, когда вы хотите получить доступ к уже существующему набору объектов в коллекции. Его не предназначен для выполнения выбора с помощью объектов-оболочек IMO, поскольку он создает ненужный шаблонный код и является своего рода взломом.

рассмотрим этот простой пример, a Person выберите Drinks они любят. Примечание: конструкторы, геттеры и сеттеры опущены для ясности. Кроме того, эти объекты обычно хранятся в базе данных, но я использую в массивах памяти для объяснения концепции.

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

public class Drink {
    private Long id;
    private String name;
}

Весна контроллеров

главное здесь то, что мы храним Person на Model таким образом, мы можем привязать его к форме внутри th:object. Во-вторых,selectableDrinks напитки, которые человек может выбрать в пользовательском интерфейсе.

   @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";
    }

шаблон код

обратите пристальное внимание на li петли и как selectableDrinks используется для получения всех возможных напитков, которые могут быть выбраны.

флажок th:field действительно расширяется до person.drinks С th:object обязан Person и *{drinks} просто является ярлыком для ссылки на свойство на