Java 如何用百里香叶绑定对象列表?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/36500731/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 17:56:57  来源:igfitidea点击:

How to bind an object list with thymeleaf?

javaspringspring-mvcspring-bootthymeleaf

提问by benscabbia

I am having a lot of difficulty with POSTing back a form to the controller, which should contain simply an arraylist of objects that the user may edit.

我在将表单回传到控制器时遇到了很多困难,该表单应该只包含用户可以编辑的对象数组列表。

The form loads up correctly, but when it's posted, it never seems to actually post anything.

表单正确加载,但是当它被发布时,它似乎从来没有真正发布过任何东西。

Here is my form:

这是我的表格:

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

Above works fine, it loads up the list correctly. However, when I POST, it returns a empty object (of size 0). I believe this is due to the lack of th:field, but anyway here is controller POST method:

以上工作正常,它正确加载了列表。但是,当我 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
    ...
}

I have tried adding a th:fieldbut regardless of what I do, it causes an exception.

我曾尝试添加 ath:field但无论我做什么,它都会导致异常。

I've tried:

我试过了:

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

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

I cannot access currentClient (compile error), I can't even select clientList, it gives me options like get(), add(), clearAll()etc, so it things it should have an array, however, I cannot pass in an array.

我无法访问currentClient(编译错误),我甚至不能选择客户端列表,它给了我类似的选项get()add()clearAll()等等,所以它的东西应该有一个数组,但是,我不能在传递数组。

I've also tried using something like th:field=${}, this causes runtime exception

我也试过使用类似的东西th:field=${},这会导致运行时异常

I've tried

我试过了

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

but also compile error.

但也会编译错误。

Any ideas?

有任何想法吗?



UPDATE 1:

更新1:

Tobias suggested that I need to wrap my list in a wraapper. So that's what I did:

Tobias 建议我需要将我的列表包装在一个 wrapper 中。所以这就是我所做的:

ClientWithSelectionWrapper:

ClientWithSelectionWrapper:

public class ClientWithSelectionListWrapper {

private ArrayList<ClientWithSelection> clientList;

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

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

My page:

我的页面:

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

Above loads fine: enter image description here

以上负载很好: 在此处输入图片说明

Then my controller:

然后我的控制器:

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

The page loads correctly, the data is displayed as expected. If I post the form without any selection I get this:

页面加载正确,数据按预期显示。如果我在没有任何选择的情况下发布表单,我会得到:

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

Not sure why it's complaining

不知道为什么会抱怨

(In the GET Method it has: model.addAttribute("wrapper", wrapper);)

(get方法有:model.addAttribute("wrapper", wrapper);

enter image description here

在此处输入图片说明

If I then make a selection, i.e. tick the first entry:

如果我然后进行选择,即勾选第一个条目:

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

I'm guessing my POST controller is not getting the clientWithSelectionListWrapper. Not sure why, since I have set the wrapper object to be posted back via the th:object="wrapper"in the FORM header.

我猜我的 POST 控制器没有得到 clientWithSelectionListWrapper。不知道为什么,因为我已经将包装对象设置为通过th:object="wrapper"FORM 标头中的 回发。



UPDATE 2:

更新 2:

I've made some progress! Finally the submitted form is being picked up by the POST method in controller. However, all the properties appear to be null, except for whether the item has been ticked or not. I've made various changes, this is how it is looking:

我取得了一些进展!最后提交的表单被控制器中的 POST 方法接收。但是,所有属性似乎都为空,除了项目是否已被勾选。我进行了各种更改,这就是它的外观:

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

I also added a default param-less constructor to my wrapper class and added a bindingResultparam to POST method (not sure if needed).

我还在我的包装类中添加了一个默认的无参数构造函数,bindingResult并向 POST 方法添加了一个参数(不确定是否需要)。

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

So when an object is being posted, this is how it is looking: enter image description here

所以当一个对象被发布时,它是这样的: 在此处输入图片说明

Of course, the systemInfo is supposed to be null (at this stage), but the clientID is always 0, and ipAddress/Description always null. The selected boolean is correct though for all properties. I'm sure I've made a mistake on one of the properties somewhere. Back to investigation.

当然,systemInfo 应该为空(在此阶段),但 clientID 始终为 0,并且 ipAddress/Description 始终为空。尽管对于所有属性,选定的布尔值都是正确的。我确定我在某处的其中一个属性上犯了错误。回到调查。



UPDATE 3:

更新 3:

Ok I've managed to fill up all the values correctly! But I had to change my tdto include an <input />which is not what I wanted... Nonetheless, the values are populating correctly, suggesting spring looks for an input tag perhaps for data mapping?

好的,我设法正确填写了所有值!但是我不得不改变我的td以包含一个<input />不是我想要的......尽管如此,这些值正在正确填充,这表明 spring 寻找可能用于数据映射的输入标签?

Here is an example of how I changed the clientID table data:

以下是我如何更改 clientID 表数据的示例:

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

Now I need to figure out how to display it as plain data, ideally without any presence of an input box...

现在我需要弄清楚如何将它显示为纯数据,理想情况下没有任何输入框......

采纳答案by Tobías

You need a wrapper object to hold the submited data, like this one:

您需要一个包装器对象来保存提交的数据,如下所示:

public class ClientForm {
    private ArrayList<String> clientList;

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

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

and use it as the @ModelAttributein your processQuerymethod:

@ModelAttribute在您的processQuery方法中使用它:

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

Moreover, the inputelement needs a nameand a value. If you directly build the html, then take into account that the name must be clientList[i], where iis the position of the item in the list:

此外,input元素需要 aname和 a value。如果直接构建html,那么考虑到名称必须是clientList[i],其中iitem在列表中的位置:

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

Note that clientListcan contain nullat intermediate positions. Per example, if posted data is:

注意clientList可以包含null在中间位置。例如,如果发布的数据是:

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

the resulting ArrayListwill be: [null, B, null, D]

结果ArrayList将是:[null, B, null, D]

UPDATE 1:

更新1:

In my exmple above, ClientFormis a wrapper for List<String>. But in your case ClientWithSelectionListWrappercontains ArrayList<ClientWithSelection>. Therefor clientList[1]should be clientList[1].clientIDand so on with the other properties you want to sent back:

在我上面的例子中,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>

I've built a little demo, so you can test it:

我已经构建了一个小演示,所以你可以测试它:

Application.java

应用程序.java

@SpringBootApplication
public class Application {      
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }       
}

ClientWithSelection.java

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

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

测试控制器.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

测试.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:

更新1.B:

Below is the same example using th:fieldand sending back all other attributes as hidden values.

下面是使用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>

回答by reversebind

When you want to select objects in thymeleaf, you dont actually need to create a wrapper for the purpose of storing a booleanselect field. Using dynamic fieldsas per the thymeleaf guide with syntax th:field="*{rows[__${rowStat.index}__].variety}"is good for when you want to access an already existing set of objects in a collection. Its not really designed for doing selections by using wrapper objects IMO as it creates unnecessary boilerplate code and is sort of a hack.

当您想在 thymeleaf 中选择对象时,您实际上不需要创建包装器来存储boolean选择字段。使用dynamic fields为每thymeleaf引导语法th:field="*{rows[__${rowStat.index}__].variety}"是很好的,因为当你要访问一个已经存在的对象集收藏。它并不是真正为通过使用包装器对象 IMO 进行选择而设计的,因为它会创建不必要的样板代码并且有点像黑客。

Consider this simple example, a Personcan select Drinksthey like. Note: Constructors, Getters and setters are omitted for clarity. Also, these objects are normally stored in a database but I am using in memory arrays to explain the concept.

考虑这个简单的例子,aPerson可以选择Drinks他们喜欢的。注意:为清楚起见,省略了构造函数、Getter 和 setter。此外,这些对象通常存储在数据库中,但我在内存数组中使用来解释这个概念。

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

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

Spring controllers

弹簧控制器

The main thing here is that we are storing the Personin the Modelso we can bind it to the form within th:object. Secondly, the selectableDrinksare the drinks a person can select on the UI.

这里的主要内容是我们将 存储在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";
    }

Template code

模板代码

Pay close attention to the liloop and how selectableDrinksis used to get all possible drinks that can be selected.

密切注意li循环以及如何selectableDrinks用于获取可以选择的所有可能的饮料。

The checkbox th:fieldreally expands to person.drinkssince th:objectis bound to Personand *{drinks}simply is the shortcut to referring to a property on the Personobject. You can think of this as just telling spring/thymeleaf that any selected Drinksare going to be put into the ArrayListat location person.drinks.

复选框th:field实际上扩展为person.drinks因为th:object绑定到Person并且*{drinks}只是引用Person对象上的属性的快捷方式。您可以认为这只是告诉 spring/thymeleaf 任何选定的Drinks都将被放入ArrayListat 位置person.drinks

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

Any way...the secret sauce is using th:value=${drinks.id}. This relies on spring converters. When the form is posted, spring will try recreate a Personand to do this it needs to know how to convert any selected drink.idstrings into the actual Drinktype. Note: If you did th:value${drinks}the valuekey in the checkbox html would be the toString()representation of a Drinkwhich is not what you want, hence need to use the id!. If you are following along, all you need to do is create your own converter if one isn't already created.

无论如何......秘密酱正在使用th:value=${drinks.id}。这依赖于弹簧转换器。当表单发布时,spring 将尝试重新创建 aPerson并且为此它需要知道如何将任何选定的drink.id字符串转换为实际Drink类型。注意:如果您在复选框 html 中输入th:value${drinks}value键,则toString()表示 aDrink这不是您想要的,因此需要使用 id!。如果您正在跟进,那么您需要做的就是创建您自己的转换器(如果尚未创建)。

Without a converter you will receive an error like Failed to convert property value of type 'java.lang.String' to required type 'java.util.List' for property 'drinks'

如果没有转换器,您将收到类似的错误 Failed to convert property value of type 'java.lang.String' to required type 'java.util.List' for property 'drinks'

You can turn on logging in application.propertiesto see the errors in detail. logging.level.org.springframework.web=TRACE

您可以打开登录application.properties以查看错误的详细信息。 logging.level.org.springframework.web=TRACE

This just means spring doesn't know how to convert a string id representing a drink.idinto a Drink. The below is an example of a Converterthat fixes this issue. Normally you would inject a repository in get access the database.

这只是意味着 spring 不知道如何将表示 a 的字符串 id 转换drink.id为 a 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);
    }
}

If an entity has a corresponding spring data repository, spring automatically creates the converters and will handle fetching the entity when an id is provided (string id seems to be fine too so spring does some additional conversions there by the looks). This is really cool but can be confusing to understand at first.

如果实体具有相应的 spring 数据存储库,则 spring 会自动创建转换器并在提供 id 时处理获取实体(字符串 id 似乎也很好,因此 spring 通过外观在那里进行了一些额外的转换)。这真的很酷,但一开始可能会让人难以理解。