web-dev-qa-db-ja.com

オブジェクトリストをthymeleafにバインドする方法は?

フォームをコントローラーにPOSTで戻すのに非常に苦労しています。これには、ユーザーが編集できるオブジェクトの配列リストのみが含まれている必要があります。

フォームは正しく読み込まれますが、投稿されると、実際には何も投稿されないようです。

私のフォームは次のとおりです。

<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がないためだと思いますが、とにかくここにコントローラー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:

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>

上記の負荷は問題ありません: 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コントローラーがclientWithSelectionListWrapperを取得していないと思います。理由はわかりません。ラッパーオブジェクトがFORMヘッダーの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>

また、ラッパークラスにデフォルトのparam-lessコンストラクターを追加し、POSTメソッドに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>

ここで、理想的には入力ボックスがない状態で、プレーンデータとして表示する方法を理解する必要があります...

39
benscabbia

送信されたデータを保持するには、次のようなラッパーオブジェクトが必要です。

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

さらに、input要素にはnamevalueが必要です。 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:

上記の例では、ClientFormList<String>のラッパーです。しかし、あなたの場合、ClientWithSelectionListWrapperにはArrayList<ClientWithSelection>が含まれています。そのため、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>

UPDATE 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>
47
Tobías

Thymeleafでオブジェクトを選択する場合、boolean selectフィールドを格納するためのラッパーを実際に作成する必要はありません。 dynamic fields構文でthymeleafガイドに従ってth:field="*{rows[__${rowStat.index}__].variety}"を使用することは、コレクション内の既存のオブジェクトのセットにアクセスする場合に適しています。不要な定型コードを作成し、一種のハッキングであるため、ラッパーオブジェクトIMOを使用して選択を行うようには設計されていません。

この簡単な例を考えてみましょう。Personは好きなDrinksを選択できます。注:明確にするために、コンストラクター、ゲッター、およびセッターは省略されています。また、これらのオブジェクトは通常データベースに保存されますが、概念を説明するためにメモリ配列で使用しています。

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

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

スプリングコントローラー

ここでの主なことは、PersonModelに格納し、th:object内のフォームにバインドできることです。次に、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ループと、selectableDrinksを使用して、選択可能なすべての飲み物を取得する方法に注意してください。

th:fieldPersonにバインドされ、person.drinksは単にPersonオブジェクトのプロパティを参照するためのショートカットであるため、チェックボックスth:objectは実際に*{drinks}に展開されます。これは、選択されたDrinksperson.drinksArrayListに配置されることをspring/thymeleafに伝えるだけと考えることができます。

<!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文字列を実際のDrink型に変換する方法を知る必要があります。注:th:value${drinks}を実行した場合、チェックボックスhtmlのvalueキーは、DrinktoString()表現になります。従っている場合、必要なのは、まだ作成されていない場合は独自のコンバーターを作成することです。

コンバータがなければ、Failed to convert property value of type 'Java.lang.String' to required type 'Java.util.List' for property 'drinks'のようなエラーを受け取ります

application.propertiesでログインをオンにして、エラーの詳細を確認できます。 logging.level.org.springframework.web=TRACE

これは、スプリングがdrink.idを表す文字列IDをDrinkに変換する方法を知らないことを意味します。以下は、この問題を修正するConverterの例です。通常、データベースにアクセスする際にリポジトリを挿入します。

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

エンティティに対応するスプリングデータリポジトリがある場合、Springは自動的にコンバーターを作成し、IDが提供されるとエンティティのフェッチを処理します(文字列IDも問題ないように見えるため、Springはルックによって追加の変換を行います)。これは本当にクールですが、最初は理解しにくいかもしれません。

2
reversebind