ASP.NET Core MVCでドロップダウンの選択値をコントローラーに渡す方法(hidden+モデルバインディング)

ASP.NET Core MVC の詳細画面でドロップダウンを選ばせ、その選択値をフォーム送信でコントローラーへ渡したい。ところが、ID 文字列で詳細取得する設計だと、選択値をどこに持たせるか迷いがちです。本記事では hidden と name の基本から、ViewModel を使った実務向けの形、次の画面へ値を渡す定番まで、動くコードで整理します。

目次

やりたいことを整理すると「ID+選択値」を次の処理で使いたい

状況を、ASP.NET Core MVC でよくある「3画面フロー」として噛み砕くと次のイメージです。

画面アクションHTTP主な入力/表示次へ渡したい値
ID入力GetItemIDGET → POSTITEM_ID を入力して送信ITEM_ID
詳細+ドロップダウンItemDetailsGET → POSTITEM_ID で詳細を表示し、ddPick を選んで送信ITEM_ID と ddPick
結果表示ItemDetailsAndValueSelectedGETITEM_ID+ddPick を使って表示/処理(必要ならさらに次へ)

ここでつまずきやすいのは、2画面目の GET(詳細表示)では URL パラメーターとして ITEM_ID を受け取れても、同じ画面の POST(フォーム送信)では フォームが送った値しかコントローラーに届かない、という点です。つまり、GET の URL にあった ITEM_ID は、何もしなければ POST には自動で付いてきません。

結論:hidden でIDを保持し、select の name を引数に合わせる

最短で解決するなら、次の2点を満たすだけでOKです。

  • ID(ITEM_ID)を hidden input に入れて POST でも送る
  • ドロップダウン(select)に name を付け、POST アクションの引数名と一致させる

この2点が揃うと、POST アクションを ItemDetails(string ITEM_ID, string ddPick) のように定義するだけで、モデルを丸ごと受け取らなくても「ID と選択値」の両方が引数に入ります。

全体像のサンプル:ID入力→詳細→結果までをつなげる

まずは「動く形」を一度作ると理解が早いです。実装は最小構成のまま、3画面の流れが分かるサンプルを置きます。

GetItemID:ID入力画面

using Microsoft.AspNetCore.Mvc;

public class ItemsController : Controller
{
[HttpGet]
public IActionResult GetItemID()
{
return View();
}


[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult GetItemID(string ITEM_ID)
{
    if (string.IsNullOrWhiteSpace(ITEM_ID))
    {
        ModelState.AddModelError("", "ITEM_ID を入力してください。");
        return View();
    }

    // 入力されたIDで詳細画面へ
    return RedirectToAction(nameof(ItemDetails), new { ITEM_ID });
}


} 
<form asp-action="GetItemID" method="post">
    @Html.AntiForgeryToken()

    <div>
        <label for="ITEM_ID">ITEM_ID</label>
        <input id="ITEM_ID" name="ITEM_ID" />
    </div>

    <div asp-validation-summary="All"></div>

    <button type="submit">送信</button>
</form>

ここでは name="ITEM_ID" を付けています。これが POST の引数 string ITEM_ID にバインドされる鍵です。

最小構成で解決する実装例(ViewData で候補を渡す)

次は「質問の実装に近い形(ID文字列で詳細を取得する)」を崩さずに、選択値を渡すパターンです。ViewModel を作らず ViewData を使います。

ItemDetails の GET:IDで詳細を取得し、候補リストを用意する

GET では、(1) ITEM_ID を受け取り、(2) 詳細データを取得し、(3) ドロップダウンの候補(SelectListItem)を作って View に渡します。

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;

public class ItemsController : Controller
{
    // 例:詳細取得用(実際はDIでRepositoryやServiceを注入)
    private static readonly Dictionary<string, string> _dummyDb = new()
    {
        ["A001"] = "サンプル商品A",
        ["B002"] = "サンプル商品B"
    };

    [HttpGet]
    public IActionResult ItemDetails(string ITEM_ID)
    {
        if (string.IsNullOrWhiteSpace(ITEM_ID))
        {
            return BadRequest("ITEM_ID が指定されていません。");
        }

        if (!_dummyDb.TryGetValue(ITEM_ID, out var itemName))
        {
            return NotFound();
        }

        // ドロップダウン候補を作る(例:ステータス選択)
        var options = new List<SelectListItem>
        {
            new SelectListItem { Value = "", Text = "選択してください" },
            new SelectListItem { Value = "Normal", Text = "通常" },
            new SelectListItem { Value = "Rush", Text = "至急" },
            new SelectListItem { Value = "Hold", Text = "保留" }
        };

        ViewData["ITEM_ID"] = ITEM_ID;
        ViewData["ItemName"] = itemName;
        ViewData["Options"] = options;

        // 例:TempDataのエラーを表示したい場合に備える
        ViewData["Error"] = TempData["Error"];

        return View();
    }
}

ItemDetails の View:hidden と select の name を用意する

Razor View 側です。フォーム送信でコントローラーに届くのは、基本的に フォーム要素の name と value です。だからこそ、hidden と name が重要になります。

<form asp-action="ItemDetails" method="post">
    @Html.AntiForgeryToken()

    <!-- POSTでもITEM_IDを送れるようにhiddenで保持 -->
    <input type="hidden" name="ITEM_ID" value="@ViewData["ITEM_ID"]" />

    <h3>商品詳細</h3>
    <p>ID:@ViewData["ITEM_ID"]</p>
    <p>商品名:@ViewData["ItemName"]</p>

    @if (ViewData["Error"] != null)
    {
        <div class="text-danger">@ViewData["Error"]</div>
    }

    <label for="ddPick">ステータス</label>
    <select id="ddPick" name="ddPick">
        @foreach (var opt in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewData["Options"])
        {
            <option value="@opt.Value">@opt.Text</option>
        }
    </select>

    <button type="submit">送信</button>
</form>

ここでの重要点は次のとおりです。

  • hidden の name は POST 引数名(ITEM_ID)と合わせる
  • select の name は POST 引数名(ddPick)と合わせる
  • id 属性は見た目や JavaScript 用で、モデルバインディングには name が効く

ItemDetails の POST:ID と選択値を引数で受け取る

View が正しく name を送れていれば、POST アクションはシンプルです。

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult ItemDetails(string ITEM_ID, string ddPick)
{
    if (string.IsNullOrWhiteSpace(ITEM_ID))
    {
        return BadRequest("ITEM_ID が空です。");
    }

    // 未選択チェック(例)
    if (string.IsNullOrWhiteSpace(ddPick))
    {
        // 本来はModelStateでエラーにして同じ画面を再表示するのが定番
        TempData["Error"] = "ドロップダウンを選択してください。";
        return RedirectToAction(nameof(ItemDetails), new { ITEM_ID });
    }

    // ここで「ITEM_ID + ddPick」を使った処理ができる

    // 次画面へ渡す(PRG: Post/Redirect/Get)
    return RedirectToAction(nameof(ItemDetailsAndValueSelected), new { ITEM_ID, ddPick });
}

[HttpGet]
public IActionResult ItemDetailsAndValueSelected(string ITEM_ID, string ddPick)
{
    ViewData["ITEM_ID"] = ITEM_ID;
    ViewData["ddPick"] = ddPick;
    return View();
}
<h3>結果</h3>
<p>ITEM_ID:@ViewData["ITEM_ID"]</p>
<p>選択値:@ViewData["ddPick"]</p>

この形なら、質問の「モデルを丸ごと受け取るサンプルはあるが、ID文字列で詳細を取る形だと選択値をどう渡すか分からない」という悩みを、最小の変更で解消できます。

実務でおすすめ:ViewModel を作って model binding させる

ViewData でも動きますが、業務開発では 入力(ITEM_ID と ddPick)を1つの ViewModel にまとめるほうが、後から仕様変更が入ったときに圧倒的に楽です。バリデーション(必須、範囲チェックなど)も書きやすくなります。

ViewModel 定義:ID+選択値+候補をまとめる

using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Rendering;

public class ItemDetailsViewModel
{
[Required]
public string ITEM_ID { get; set; } = "";


[Required(ErrorMessage = "ドロップダウンを選択してください。")]
public string ddPick { get; set; } = "";

// ドロップダウン候補
public List<SelectListItem> Options { get; set; } = new();

// 表示用(例:商品名など)
public string ItemName { get; set; } = "";


} 

GET:ViewModel を組み立てて View に渡す

[HttpGet]
public IActionResult ItemDetailsVm(string ITEM_ID)
{
    if (string.IsNullOrWhiteSpace(ITEM_ID))
    {
        return BadRequest();
    }


if (!_dummyDb.TryGetValue(ITEM_ID, out var itemName))
{
    return NotFound();
}

var vm = new ItemDetailsViewModel
{
    ITEM_ID = ITEM_ID,
    ItemName = itemName,
    Options = new List<SelectListItem>
    {
        new SelectListItem { Value = "", Text = "選択してください" },
        new SelectListItem { Value = "Normal", Text = "通常" },
        new SelectListItem { Value = "Rush", Text = "至急" },
        new SelectListItem { Value = "Hold", Text = "保留" }
    }
};

return View(vm);


} 

View:Tag Helpers を使うと name のズレ事故が減る

Tag Helpers(asp-for, asp-items)を使うと、name を手打ちしなくても ViewModel のプロパティ名に合わせて自動生成されます。今回のような「name が合わずに値が届かない」事故を減らせます。

@model ItemDetailsViewModel

&lt;form asp-action="ItemDetailsVm" method="post"&gt;
    @Html.AntiForgeryToken()

    &lt;input asp-for="ITEM_ID" type="hidden" /&gt;

    &lt;h3&gt;商品詳細&lt;/h3&gt;
    &lt;p&gt;ID:@Model.ITEM_ID&lt;/p&gt;
    &lt;p&gt;商品名:@Model.ItemName&lt;/p&gt;

    &lt;div asp-validation-summary="All"&gt;&lt;/div&gt;

    &lt;div&gt;
        &lt;label asp-for="ddPick"&gt;&lt;/label&gt;
        &lt;select asp-for="ddPick" asp-items="Model.Options"&gt;&lt;/select&gt;
        &lt;span asp-validation-for="ddPick"&gt;&lt;/span&gt;
    &lt;/div&gt;

    &lt;button type="submit"&gt;送信&lt;/button&gt;
&lt;/form&gt;

hidden は <input asp-for="ITEM_ID" type="hidden" /> の1行で済みます。さらに、プロパティ名の変更が入っても一括で追従しやすく、保守性が上がります。

POST:ViewModel を受け取り、必要なら同じ画面を再表示する

POST 側は次の流れにすると実務で扱いやすいです。

  • ModelState が無効なら、候補リストを再構築して View を返す(候補はPOSTで戻ってこないため)
  • 有効なら処理して、PRG で次画面へリダイレクトする
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult ItemDetailsVm(ItemDetailsViewModel vm)
{
    // 候補リストはPOSTで消えるので、必要ならここで再セット
    vm.Options = new List&lt;SelectListItem&gt;
    {
        new SelectListItem { Value = "", Text = "選択してください" },
        new SelectListItem { Value = "Normal", Text = "通常" },
        new SelectListItem { Value = "Rush", Text = "至急" },
        new SelectListItem { Value = "Hold", Text = "保留" }
    };

    // 表示用情報も再取得(hiddenは改ざん可能なのでDBで検証する)
    if (!_dummyDb.TryGetValue(vm.ITEM_ID, out var itemName))
    {
        return NotFound();
    }
    vm.ItemName = itemName;

    // ddPick の許可値チェック(例)
    var allowed = new HashSet&lt;string&gt; { "Normal", "Rush", "Hold" };
    if (!string.IsNullOrWhiteSpace(vm.ddPick) &amp;&amp; !allowed.Contains(vm.ddPick))
    {
        ModelState.AddModelError(nameof(vm.ddPick), "不正な選択値です。");
    }

    if (!ModelState.IsValid)
    {
        // 入力エラー:同じ画面へ
        return View(vm);
    }

    // ここで「vm.ITEM_ID + vm.ddPick」を使った処理
    return RedirectToAction(nameof(ItemDetailsAndValueSelected), new { ITEM_ID = vm.ITEM_ID, ddPick = vm.ddPick });
}

「候補リストを POST で再構築する」点は地味ですが重要です。ドロップダウンの候補を ViewData / ViewModel に入れても、POST ではそのまま戻ってきません。入力エラーで画面を再表示する設計なら、必ず再セットします。

3画面目へ値を渡す方法の選び方

「2画面目から結果画面へ ITEM_ID と ddPick を渡す」方法はいくつかあります。目的に合わせて使い分けると事故が減ります。

方法特徴向いている場面注意点
RedirectToAction(new { ITEM_ID, ddPick })URL(クエリ/ルート)に値が乗る。PRGと相性が良い。短い値で、リロード対策もしたいときURLに出るので機密情報は避ける。長いデータにも不向き。
TempDataリダイレクト1回分だけ値を保持できる。完了メッセージ、ワンタイムの状態を渡したいときセッション依存になりやすい。多用すると追跡が難しい。
セッション/DB長期保持できる。ウィザード形式に強い。複数画面にまたぐ入力や、途中保存が必要なときライフサイクル管理が必要。スケールや整合性に注意。

今回のように「ID+ドロップダウン選択値」のような軽い情報なら、まずは RedirectToAction が分かりやすいです。URL に値が乗るのが困る場合だけ TempData やDBを検討すると整理しやすくなります。

hidden 以外で ITEM_ID を渡す選択肢もある

基本は hidden で問題ありませんが、設計次第では「form の action 自体に ITEM_ID を含める」方法もあります。たとえば、POST 先 URL に route/query を付けてしまえば、hidden を置かなくても受け取れます。

&lt;form asp-action="ItemDetails" asp-route-ITEM_ID="@ViewData["ITEM_ID"]" method="post"&gt;
    @Html.AntiForgeryToken()
    &lt;select name="ddPick"&gt;...&lt;/select&gt;
    &lt;button type="submit"&gt;送信&lt;/button&gt;
&lt;/form&gt;

ただし、実務では「送る値が増えてきたときに見通しが悪くなる」「ルーティング変更の影響が出やすい」などの理由で、結局 hidden + ViewModel に寄せるケースが多いです。

hidden を使うときの注意点:改ざん前提でサーバー側で検証する

hidden は便利ですが、ブラウザー側の値なので 利用者が書き換えられる前提で設計します。具体的には次の対策を入れておくと安心です。

  • ITEM_ID は必ずサーバー側で存在確認する(「そのIDが本当にあるか」)
  • ddPick は候補に含まれるか検証する(想定外の値を弾く)
  • 権限が絡むなら「そのユーザーがその ITEM_ID を扱えるか」も確認する

よくあるハマりどころとチェックリスト

「送ったはずなのに ddPick が null になる」「ITEM_ID が空になる」といったトラブルは、ほぼ名前・フォーム・ルーティングのどれかが原因です。確認順をチェックリスト化しておくと復旧が早いです。

症状主な原因まず確認する場所対処
ddPick が null/空select に name がない/引数名と不一致View の select タグname=”ddPick” を付ける。Tag Helpers を使う。
ITEM_ID が null/空POST に ITEM_ID を送っていないView の hidden input / form actionhidden を追加、または form の action に asp-route-ITEM_ID を付ける。
POST で 400/419 になるAntiForgeryToken 不一致フォーム内@Html.AntiForgeryToken() を入れる/[ValidateAntiForgeryToken] を揃える。
入力エラーで再表示すると候補が消えるOptions を POST で再セットしていないPOST アクションModelState が無効なときにも Options を再構築する。

デバッグのコツ:Network と Request.Form を見る

原因切り分けを早くするには、まず「ブラウザーが何を送っているか」を見るのが一番です。

  • Chrome/Edge の DevTools → Network → 該当リクエスト → Payload で ITEM_ID と ddPick が送られているか確認
  • 送られていないなら View の name/hidden/form action を修正
  • 送られているのに受け取れないなら、アクション引数名・ViewModel・ルーティングを確認

サーバー側では、一時的に次のように書いてフォーム内容を確認するのも有効です(検証が終わったら消します)。

foreach (var key in Request.Form.Keys)
{
    Console.WriteLine($"{key} = {Request.Form[key]}");
}

まとめ:フォーム送信の基本に立ち返ると解決が早い

ASP.NET Core MVC で「ドロップダウンの選択値をコントローラーに渡したい」問題は、突き詰めると フォームが送るのは name/value であるという基本に戻ります。

  • 詳細表示に使った ITEM_ID は POST でも必要なら hidden(または form action の route)で保持する
  • ドロップダウンは name を引数名に合わせる(Tag Helpers を使うと安全)
  • 次画面へ渡すなら PRG(RedirectToAction)をベースに、必要に応じて TempData/セッション/DB を検討する

この型を押さえておけば、ドロップダウン以外(チェックボックス、ラジオボタン、複数入力)でも同じ考え方でスムーズに値を受け渡せます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次