복붙노트

[SPRING] 개체 목록을 thymeleaf에 바인딩하는 방법은 무엇입니까?

SPRING

개체 목록을 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>

위의 작품을 잘, 그것은 목록을 올바르게로드합니다. 그러나 POST 할 때 크기가 0 인 빈 개체를 반환합니다. 나는 이것이 th : field의 부족 때문이라고 생각하지만, 어쨌든 여기서 controller POST method :

...
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 : 필드를 추가하려고 시도했으나 내가하는 일과 관계없이 예외가 발생합니다.

난 노력 했어:

...
<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__]}" 

또한 컴파일 오류.

어떤 아이디어?

Tobias는 내 목록을 래퍼로 포장해야한다고 제안했습니다. 그래서 그것이 내가 한 일입니다.

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>

위의 벌금 이상 :

그럼 내 컨트롤러 :

@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);)

그런 다음 항목을 선택하면 첫 번째 항목을 틱합니다.

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

POST 컨트롤러가 clientWithSelectionListWrapper를 얻지 못하고있는 것 같습니다. 이유는 모르겠지만, 나는 wrapper 객체를 FOR : 헤더를 통해 th : object = "wrapper"를 통해 다시 게시하도록 설정했기 때문에.

나는 진전을 보았다! 마지막으로 제출 된 양식은 컨트롤러의 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>

또한 래퍼 클래스에 기본 param-less 생성자를 추가하고 bindingResult 매개 변수를 POST 메서드에 추가했습니다 (필요한 경우 확실하지 않음).

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

따라서 객체가 게시 될 때 이것이 어떻게 보이는지입니다.

물론 systemInfo는 null이어야하지만 (이 단계에서) clientID는 항상 0이고 ipAddress / Description은 항상 null입니다. 모든 속성에 대해 선택된 부울은 정확합니다. 나는 어딘가에서 부동산 중 하나에 실수를 범했다고 확신한다. 조사로 돌아 가라.

나는 모든 값을 정확하게 채울 수 있었다. 그러나 나는 내가 원했던 것이 아닌 을 포함하도록 td를 변경해야만했다. 그럼에도 불구하고, 값이 올바르게 채워지고있다. 봄은 데이터 매핑을 위해 아마도 입력 태그를 찾는다 고 제안 할까?

다음은 clientID 테이블 데이터를 변경 한 예제입니다.

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

이제 입력 상자가 없어도 일반 데이터로 표시하는 방법을 알아야합니다.

해결법

  1. ==============================

    1.다음과 같이 제출 된 데이터를 보관할 래퍼 객체가 필요합니다.

    다음과 같이 제출 된 데이터를 보관할 래퍼 객체가 필요합니다.

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

    processQuery 메서드에서 @ModelAttribute로 사용합니다.

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

    또한, 입력 요소에는 이름과 값이 필요합니다. 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을 포함 할 수 있습니다. 중간 위치. 예를 들어 게시 된 데이터가 다음과 같은 경우 :

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

    결과의 ArrayList는 다음과 같이됩니다 : [null, B, null, D]

    업데이트 1 :

    위의 예에서 ClientForm은 List 에 대한 래퍼입니다. 그러나 귀하의 경우에는 ClientWithSelectionListWrapper에 ArrayList 이 포함되어 있습니다. 그러므로 clientList [1]은 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>
    

    약간의 데모를 만들었으므로 테스트 할 수 있습니다.

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

    업데이트 1.B :

    다음은 th : field를 사용하고 다른 모든 속성을 숨겨진 값으로 다시 보내는 동일한 예제입니다.

     <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>
    
  2. ==============================

    2.thymeleaf에서 객체를 선택하려면 boolean select 필드를 저장할 목적으로 래퍼를 만들 필요가 없습니다. thymeleaf 가이드에 따라 동적 필드를 사용하는 구문은 다음과 같습니다. field : "* {rows {__ $ {rowStat.index} __. variety}}"는 컬렉션의 기존 개체 세트에 액세스하려는 경우 유용합니다. 불필요한 상용구 코드를 생성하고 일종의 해킹 인 래퍼 객체 IMO를 사용하여 선택 작업을 수행하도록 실제로 설계되지 않았습니다.

    thymeleaf에서 객체를 선택하려면 boolean select 필드를 저장할 목적으로 래퍼를 만들 필요가 없습니다. thymeleaf 가이드에 따라 동적 필드를 사용하는 구문은 다음과 같습니다. field : "* {rows {__ $ {rowStat.index} __. variety}}"는 컬렉션의 기존 개체 세트에 액세스하려는 경우 유용합니다. 불필요한 상용구 코드를 생성하고 일종의 해킹 인 래퍼 객체 IMO를 사용하여 선택 작업을 수행하도록 실제로 설계되지 않았습니다.

    이 간단한 예를 고려해 볼 때, 사람들은 자신이 좋아하는 음료를 선택할 수 있습니다. 참고 : 명확성을 위해 생성자, Getters 및 setter가 생략되었습니다. 또한, 이러한 개체는 일반적으로 데이터베이스에 저장되어 있지만 개념을 설명하기 위해 메모리 배열에 사용하고 있습니다.

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

    스프링 컨트롤러

    여기서 가장 중요한 점은 모델에 Person을 저장하여 객체를 폼 내에 바인딩 할 수 있다는 것입니다. 둘째, selectableDrinks는 사용자가 UI에서 선택할 수있는 음료입니다.

       @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 루프와 선택 가능한 음료가 선택 될 수있는 모든 음료수를 얻는 방법에주의를 기울이십시오.

    체크 상자 th : 필드는 실제로 person.drinks로 확장됩니다. 그 이유는 객체가 Person에 바인딩되고 * {drinks}이 Person 객체의 속성을 참조하는 바로 가기이기 때문입니다. 이것을 스프링 / 타임 플레이 (spring / thymeleaf)에 알리면 선택한 음료수가 person.drinks 위치의 ArrayList에 저장됩니다.

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

    어쨌든 ... 비밀스런 소스는 th : value = $ {drinks.id}를 사용하고 있습니다. 이것은 스프링 컨버터를 사용합니다. 폼이 게시되면, Spring은 Person을 다시 만들려고 시도하고 이것을하기 위해선 선택된 drink.id 문자열을 실제 음료 타입으로 변환하는 방법을 알아야합니다. 참고 : th : value $ {drinks} 체크 상자 html의 값 키는 원하는 것이 아닌 음료의 toString () 표현이므로 id를 사용해야합니다. 당신이 따라 다니는 경우, 당신이해야 할 일은 이미 생성되지 않은 경우 자신의 변환기를 만드는 것입니다.

    변환기가 없으면 다음과 같은 오류 메시지가 나타납니다. 속성 'drinks'에 대해 'java.lang.String'유형의 속성 값을 필수 유형 'java.util.List'로 변환하지 못했습니다.

    application.properties에서 로깅을 활성화하여 오류를 자세히 볼 수 있습니다. logging.level.org.springframework.web = TRACE

    이것은 스프링이 drink.id를 나타내는 문자열 id를 음료수로 변환하는 방법을 모른다는 것을 의미합니다. 아래는이 문제를 해결하는 변환기의 예입니다. 일반적으로 당신은 데이터베이스에 접근하기 위해 저장소를 삽입 할 것입니다.

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

    엔티티에 해당 스프링 데이터 저장소가 있으면 스프링이 자동으로 변환기를 만들고 ID가 제공되면 엔티티를 가져 오는 작업을 처리합니다 (문자열 ID도 괜찮아 보입니다. 그러면 스프링이 외관에서 일부 추가 변환을 수행합니다). 이것은 정말 멋지지만 처음에는 이해하기가 혼란 스러울 수 있습니다.

  3. from https://stackoverflow.com/questions/36500731/how-to-bind-an-object-list-with-thymeleaf by cc-by-sa and MIT license