🐕

ASP.NET Core MVC で 1 ページに複数個のフォームを置きたい

2022/05/21に公開

ASP.NET Core MVC や Razor pages では組み込みのフォームバリデーション機能があります。
結構便利なんですが、1つのフォームのみの例が多くて、以下のような複数フォームの時の例がそんなに多くなかったので自分のメモ用に書いておこうと思います。

複数個のフォームの実現方法ですが、本当に複数個の form タグを置くと、その form 内のデータしか POST されないので、他の form のデータがロストしてしまいます。
各 form の中に <input type="hidden" ... /> を使って他のフォームのデータを入れ込んでもいいですが、それも別 form で編集されたデータは反映されないので、結論からいうと 1 つの form タグで複数フォームあるようにエミュレートするのが一番楽です。

やってみよう

では、画面で表示するデータをまとめた ViewModel クラスを 1 つ作りましょう。

using System.ComponentModel.DataAnnotations;

namespace WebApplication7.Models;

public record Form1(
    [Required(ErrorMessage = "Form1.Name is invalid.")]
    [Display(Name = "Form1.Name")]
    string? Name = null);
public record Form2(
    [Required(ErrorMessage = "Form1.Name is invalid.")]
    [Display(Name = "Form1.Name")]
    string? Name = null);

public record HomeViewModel
{
    public Form1 Form1 { get; init; } = new();
    public Form2 Form2 { get; init; } = new();
}

こんな感じです。 Form1 と Form2 が、それぞれ 1 つのフォームに該当する感じです。
これを使って画面を作っていきます。ASP.NET Core MVC のプロジェクトを新規作成した状態の HomeController に実装してみましょう。

まず、HomeController の Index メソッドを以下のようにして HomeViewModel を View の Model に設定されるようにします。

HomeController.cs
public IActionResult Index()
{
    return View(new HomeViewModel());
}

そして、1 つの form タグを使って 2 つ入力フォームがあるような雰囲気の画面を作ります。

Index.cshtml
@model HomeViewModel
@{
    ViewData["Title"] = "Home Page";
}

<div>
    @TempData["Message"]
</div>

<form method="post">
    <h3>Form1</h3>
    <div>
        <label asp-for="Form1.Name"></label>
        <input asp-for="Form1.Name" />
        <span asp-validation-for="Form1.Name"></span>
        <button type="submit" asp-action="IndexForm1">Submit</button>
    </div>

    <h3>Form2</h3>
    <div>
        <label asp-for="Form2.Name"></label>
        <input asp-for="Form2.Name" />
        <span asp-validation-for="Form2.Name"></span>
        <button type="submit" asp-action="IndexForm2">Submit</button>
    </div>
</form>

ポイントは form の submit ボタンを 2 つ置いて、asp-action 属性で、それぞれ別のメソッドに POST するようにしているところです。
因みに asp-route-key-"Form1"asp-route-key="Form2" のように、どっちのボタンが押されたかを識別するためのルートパラメーターをつけて 1 つのメソッド内で if 文で分岐しても OK です。状況に応じて使い分ける感じでいいと思います。今回は Form1 と Form2 で別メソッドで受けるようにしました。

後は各メソッドで以下のように実装すれば完成です。ポイントは一度 ModelState をクリアした後に、Form1 なら homeController.Form1 をバリデーション、Form2 なら homeController.Form2 をバリデーションしているところです。

こうすることで、HomeViewModel 全体ではなく部分的にバリデーションが行えます。

HomeController.cs
[HttpPost]
public IActionResult IndexForm1(HomeViewModel homeViewModel)
{
    ModelState.Clear();
    if (!TryValidateModel(homeViewModel.Form1, nameof(homeViewModel.Form1)))
    {
        return View("Index", homeViewModel);
    }

    TempData["Message"] = "Form1 成功!";
    return RedirectToAction("Index");
}

[HttpPost]
public IActionResult IndexForm2(HomeViewModel homeViewModel)
{
    ModelState.Clear();
    if (!TryValidateModel(homeViewModel.Form2, nameof(homeViewModel.Form2)))
    {
        return View("Index", homeViewModel);
    }

    TempData["Message"] = "Form2 成功!";
    return RedirectToAction("Index");
}

実行すると、最初の gif アニメのような感じに動く画面が出来上がります。

まとめ

他のやり方知ってる人いたら教えて!

Microsoft (有志)

Discussion