フォームをコントローラーに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__]}"
ただし、コンパイルエラーもあります。
何か案は?
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を取得していないと思います。理由はわかりません。ラッパーオブジェクトがFORMヘッダーの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コンストラクターを追加し、POSTメソッドにbindingResult
paramを追加しました(必要な場合はわかりません)。
public String processQuery(@ModelAttribute ClientWithSelectionListWrapper wrapper, BindingResult bindingResult, Model model)
オブジェクトが投稿されているとき、これはどのように見えるかです:
もちろん、systemInfoは(この段階では)nullになるはずですが、clientIDは常に0で、ipAddress/Descriptionは常にnullです。ただし、選択したブール値はすべてのプロパティに対して正しいです。どこかのプロパティで間違いを犯したと確信しています。調査に戻ります。
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>
ここで、理想的には入力ボックスがない状態で、プレーンデータとして表示する方法を理解する必要があります...
送信されたデータを保持するには、次のようなラッパーオブジェクトが必要です。
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
要素には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
を含めることができます。たとえば、投稿されたデータが次の場合:
clientList[1] = 'B'
clientList[3] = 'D'
結果のArrayList
は次のようになります:[null, B, null, D]
更新1:
上記の例では、ClientForm
はList<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>
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;
}
スプリングコントローラー
ここでの主なことは、Person
をModel
に格納し、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:field
はPerson
にバインドされ、person.drinks
は単にPerson
オブジェクトのプロパティを参照するためのショートカットであるため、チェックボックスth:object
は実際に*{drinks}
に展開されます。これは、選択されたDrinks
がperson.drinks
のArrayList
に配置されることを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
キーは、Drink
のtoString()
表現になります。従っている場合、必要なのは、まだ作成されていない場合は独自のコンバーターを作成することです。
コンバータがなければ、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はルックによって追加の変換を行います)。これは本当にクールですが、最初は理解しにくいかもしれません。