SpringBootで画面と値のやり取りをしたい(ModelとModelAndView)

ModelとModelAndViewの使い方

Controllerクラスのメソッドの引数について 画面と値をやりとりするのに、ModelもしくはModelAndViewを使用する。

参考:https://spring.pleiades.io/spring-framework/docs/5.3.10/reference/html/web.html#mvc-controller

Modelを引数とした場合

以下は初期画面を表示するControllerクラスである。

@Controller
public class HomeController {

    /**
     * Webアプリのトップ画面を表示する。<br>
     *
     * @param model
     *            Model
     * @return 遷移先画面
     */
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index(Model model) {
        model.addAttribute("message", "Calculation page.");
        // 書かないと実行時にIllegalStateExceptionが起きる
        // Neither BindingResult nor plain target object for bean name 'calculationModel' available as request attribute
        model.addAttribute("calculationModel", new CalculationModel());
        return "index";
    }
}

上記のControllerで遷移する初期画面のHTMLは以下の通り。

model.addAttribute("calculationModel", new CalculationModel());がない場合、Thymeleafで参照したいオブジェクトがModelに登録されていないので参照できなくてIllegalStateExceptionが発生している。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8">
    <title>Calculation</title>
  </head>
  <body>

    <h2 th:text="${message}"></h2>

    <form action="#" th:action="@{/result}" th:object="${calculationModel}" method="post">
      <div><input type="number" th:field="*{num1}" /> + <input type="number" th:field="*{num2}" /></div>
      <p><input type="submit" value="計算" /></p>
    </form>
  </body>
</html>

次に初期画面から画面遷移する場合を考える。 その場合のコントローラーは以下の通り。

@Controller
public class CalculationController {

    /** Service */
    private final CalculationService service;

    /**
     * コンストラクタ。<br>
     *
     * @param service
     *            CalculationService
     */
    public CalculationController(CalculationService service) {
        this.service = service;
    }

    // 引数CalculationModelに付与している@ModelAttributeは付与しなくても、引数には画面で入力された値が入っている
    // 引数の名前はcalculationModelでなくてもよい
    @RequestMapping(value = "/result", method = RequestMethod.POST)
    public String result(@ModelAttribute CalculationModel calculationModel, Model model) {
        int sum = service.sum(calculationModel.getNum1(), calculationModel.getNum2());
        calculationModel.setResult(sum);
        // 書かなくても画面で値が表示できる
        model.addAttribute("calculationModel", calculationModel);
        return "result";
    }
}

遷移先の画面のHTMLは以下の通り。

初期画面表示の際にmodel.addAttribute("calculationModel", calculationModel);をしているために、Modelの中には既にcalculationModelは追加されているので、

再度追加しなくても画面でモデルが参照できる。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8">
    <title>Calculation Result</title>
  </head>
  <body>
    <h2>Calculation Result</h2>
    <div>
      <p th:text="|計算式:${calculationModel.num1} + ${calculationModel.num2}|">
      <p th:text="|計算結果:${calculationModel.result}|"/>
    </div>
  </body>
</html>

上記をまとめると以下のようになる

  • Modelを使用した場合、1回はmodel.addAttribute("calculationModel", calculationModel);を実行してモデルを追加する必要があるが、1度追加したらその後モデルの値を変更しても再度addAttributeメソッドを実行する必要はない。
  • 引数名とaddAttributeメソッドの第一引数が異なっていても値が注入されていたことから、クラスの型を見て同じだったら値を入れているようにみえる

よって、初期画面から画面遷移する場合のコントローラーは以下のように書いても正常に動作する。

@Controller
public class CalculationController {

    /** Service */
    private final CalculationService service;

    /**
     * コンストラクタ。<br>
     *
     * @param service
     *            CalculationService
     */
    public CalculationController(CalculationService service) {
        this.service = service;
    }

    @RequestMapping(value = "/result", method = RequestMethod.POST)
    public String result(CalculationModel calculation) {
        int sum = service.sum(calculation.getNum1(), calculation.getNum2());
        calculation.setResult(sum);

        return "result";
    }
}

ModelAndView

以下は初期画面を表示するControllerクラスである。

@Controller
public class HomeController {

    /**
     * Webアプリのトップ画面を表示する。<br>
     *
     * @param model
     *            Model
     * @return 遷移先画面
     */
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public ModelAndView index() {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("index");
        modelAndView.addObject("message", "Calculation page.");
        // 書かないと実行時にIllegalStateExceptionが起きる
        // Neither BindingResult nor plain target object for bean name 'calculationModel' available as request attribute
        modelAndView.addObject("calculationModel", new CalculationModel());

        return modelAndView;
    }
}

上記のControllerで遷移する画面のHTMLは以下の通り。

modelAndView.addObject("calculationModel", new CalculationModel());がない場合、Thymeleafで参照したいオブジェクトがModelAndViewに登録されていないので参照できなくてIllegalStateExceptionが発生している。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8">
    <title>Calculation</title>
  </head>
  <body>

    <h2 th:text="${message}"></h2>

    <form action="#" th:action="@{/result}" th:object="${calculationModel}" method="post">
      <div><input type="number" th:field="*{num1}" /> + <input type="number" th:field="*{num2}" /></div>
      <p><input type="submit" value="計算" /></p>
    </form>
  </body>
</html>

次に初期画面から画面遷移する場合を考える。 その場合のコントローラーは以下の通り。

@Controller
public class CalculationController {

    /** Service */
    private final CalculationService service;

    /**
     * コンストラクタ。<br>
     *
     * @param service
     *            CalculationService
     */
    public CalculationController(CalculationService service) {
        this.service = service;
    }

    // 引数CalculationModelに付与している@ModelAttributeは付与しなくても、引数には画面で入力された値が入っている
    // 引数の名前はcalculationModelでなくてもよい
    @RequestMapping(value = "/result", method = RequestMethod.POST)
    public ModelAndView result(@ModelAttribute CalculationModel calculationModel) {
        int sum = service.sum(calculationModel.getNum1(), calculationModel.getNum2());
        calculationModel.setResult(sum);
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("result");
        // 書かなくても画面で値が表示できる
        modelAndView.addObject("calculationModel", calculationModel);
        return modelAndView;
    }
}

遷移先の画面のHTMLは以下。

初期画面表示の際にmodelAndView.addObject("calculationModel", new CalculationModel());をしているために、ModelAndViewの中には既にcalculationModelは追加されているので、再度追加しなくても画面でモデルが参照できる。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8">
    <title>Calculation Result</title>
  </head>
  <body>
    <h2>Calculation Result</h2>
    <div>
      <p th:text="|計算式:${calculationModel.num1} + ${calculationModel.num2}|">
      <p th:text="|計算結果:${calculationModel.result}|"/>
    </div>
  </body>
</html>

上記をまとめると以下のようになる

  • ModelAndViewを使用した場合、1回はmodelAndView.addObject("calculationModel", new CalculationModel());を実行してモデルを追加する必要があるが、1度追加したらその後モデルの値を変更しても再度addObjectメソッドを実行する必要はない。
  • 引数名とaddObjectメソッドの第一引数が異なっていても値が注入されていたことから、クラスの型を見て同じだったら値を入れている

よって、初期画面から画面遷移する場合のコントローラーは以下のように書いても正常に動作する。

@Controller
public class CalculationController {

    /** Service */
    private final CalculationService service;

    /**
     * コンストラクタ。<br>
     *
     * @param service
     *            CalculationService
     */
    public CalculationController(CalculationService service) {
        this.service = service;
    }

    @RequestMapping(value = "/result", method = RequestMethod.POST)
    public ModelAndView result(CalculationModel calculation) {
        int sum = service.sum(calculation.getNum1(), calculation.getNum2());
        calculation.setResult(sum);
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("result");
        return modelAndView;
    }
}