【问题标题】:How to bind an object list with thymeleaf?如何将对象列表与百里香绑定?
【发布时间】:2016-07-29 18:48:56
【问题描述】:

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

表单加载正确,但在发布时,它似乎从未真正发布任何内容。

这是我的表格:

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

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" 发回。


更新 2:

我已经取得了一些进展!最后,提交的表单被控制器中的 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>

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

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

所以当一个对象被发布时,它的外观是这样的:

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


更新 3:

好的,我已经成功地正确填写了所有值!但是我不得不更改我的td 以包含一个&lt;input /&gt;,这不是我想要的......尽管如此,这些值正在正确填充,这表明spring 可能会寻找一个输入标签来进行数据映射?

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

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

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

【问题讨论】:

  • 绑定仅适用于input 元素,客户端回传到服务器。其他框架可以使用某种视图状态或会话并向开发人员隐藏详细信息,但 AFAIK timeleaf 不这样做。在这种特殊情况下,您可以将值绑定到隐藏字段。
  • @user1516873 是的,您是对的,实际上是在您发表评论前 30 秒发现的。是的,一定是 thymeleafff 相关的东西,我很确定当我在 asp.net 中做类似的事情时,它直接把它捡起来了。无论如何,我一定会写下来作为提醒!

标签: java spring spring-mvc spring-boot thymeleaf


【解决方案1】:

当您想在 thymeleaf 中选择对象时,您实际上不需要创建包装器来存储 boolean 选择字段。当您想要访问集合中已经存在的一组对象时,按照 thymeleaf 指南使用 dynamic fields 和语法 th:field="*{rows[__${rowStat.index}__].variety}" 非常适合。它并不是真正为使用包装器对象 IMO 进行选择而设计的,因为它创建了不必要的样板代码并且有点像 hack。

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

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.drinks,因为th:object 绑定到Person,而*{drinks} 只是引用Person 对象上的属性的快捷方式。你可以认为这只是告诉 spring/thymeleaf 任何选定的Drinks 都将被放入位置person.drinksArrayList

<!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键将是toString()表示Drink,这不是你想要的,因此需要使用id!。如果您跟随,您需要做的就是创建自己的转换器(如果尚未创建)。

如果没有转换器,您将收到类似的错误 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

这只是意味着 spring 不知道如何将表示 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 数据存储库,spring 会自动创建转换器,并在提供 id 时处理实体的获取(字符串 id 似乎也很好,所以 spring 会在那里进行一些额外的转换)。这真的很酷,但一开始可能会让人难以理解。

【讨论】:

  • 请参阅此处以获取有关上述示例的 Youtube 视频。 (不是我的视频)youtu.be/e9mlrHyn73w
  • 我在上面的例子中遇到的一个问题是,它需要对数据库的另一个服务请求来获取我们已经(显然)存在于表上的数据。并且对于我目前的需要,使用获取的 id 进行服务调用是不可用的。
【解决方案2】:

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

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是item在列表中的位置:

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

在我上面的例子中,ClientFormList&lt;String&gt; 的包装器。但在您的情况下,ClientWithSelectionListWrapper 包含ArrayList&lt;ClientWithSelection&gt;。因此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>

【讨论】:

  • 感谢您为帮助我付出的巨大努力!对此,我真的非常感激。但是,我仍然遇到了一些麻烦。如果我尝试使用stat.pos 设置名称,我会得到一个异常:org.springframework.expression.spel.SpelEvaluationException: EL1008E:(pos 5): Property or field 'pos' cannot be found on object of type 'org.thymeleaf.processor.attr.AbstractIterationAttrProcessor$StatusVar' - maybe not public? - 所以我尝试使用pos.index,但是如果我在没有勾选任何内容的情况下发布,则为空,但如果我勾选我会得到Validation failed for object='clientWithSelectionListWrapper'. Error count:1
  • 好的,我已经用 Update 1 更新了答案。我一定在某个地方犯了一些愚蠢的错误,同时我会继续尝试
  • 应该是stat.index或者stat.count @gudthing
  • @Tobías 我不认为您对为什么表单总是返回空有任何其他想法?我想这可能是一个绑定问题?我已经为 50 个代表提供了赏金(希望这会让它变得更有价值:)
  • @Tobías 我刚刚完成了你在更新 1B 中所做的事情,只是稍微有点混乱,但现在一切正常 :)。非常感谢您的所有帮助,真的,没有您的帮助就无法做到。我很高兴你获得了额外的声誉,尽管我仍然认为我欠你一桶啤酒;)。再次感谢!
猜你喜欢
  • 2016-10-11
  • 2019-12-26
  • 2017-01-18
  • 2015-05-18
  • 1970-01-01
  • 1970-01-01
  • 2017-02-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多