ASP.NET Core MVC の詳細画面でドロップダウンを選ばせ、その選択値をフォーム送信でコントローラーへ渡したい。ところが、ID 文字列で詳細取得する設計だと、選択値をどこに持たせるか迷いがちです。本記事では hidden と name の基本から、ViewModel を使った実務向けの形、次の画面へ値を渡す定番まで、動くコードで整理します。
やりたいことを整理すると「ID+選択値」を次の処理で使いたい
状況を、ASP.NET Core MVC でよくある「3画面フロー」として噛み砕くと次のイメージです。
| 画面 | アクション | HTTP | 主な入力/表示 | 次へ渡したい値 |
|---|---|---|---|---|
| ID入力 | GetItemID | GET → POST | ITEM_ID を入力して送信 | ITEM_ID |
| 詳細+ドロップダウン | ItemDetails | GET → POST | ITEM_ID で詳細を表示し、ddPick を選んで送信 | ITEM_ID と ddPick |
| 結果表示 | ItemDetailsAndValueSelected | GET | ITEM_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
<form asp-action="ItemDetailsVm" method="post">
@Html.AntiForgeryToken()
<input asp-for="ITEM_ID" type="hidden" />
<h3>商品詳細</h3>
<p>ID:@Model.ITEM_ID</p>
<p>商品名:@Model.ItemName</p>
<div asp-validation-summary="All"></div>
<div>
<label asp-for="ddPick"></label>
<select asp-for="ddPick" asp-items="Model.Options"></select>
<span asp-validation-for="ddPick"></span>
</div>
<button type="submit">送信</button>
</form>
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<SelectListItem>
{
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<string> { "Normal", "Rush", "Hold" };
if (!string.IsNullOrWhiteSpace(vm.ddPick) && !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 を置かなくても受け取れます。
<form asp-action="ItemDetails" asp-route-ITEM_ID="@ViewData["ITEM_ID"]" method="post">
@Html.AntiForgeryToken()
<select name="ddPick">...</select>
<button type="submit">送信</button>
</form>
ただし、実務では「送る値が増えてきたときに見通しが悪くなる」「ルーティング変更の影響が出やすい」などの理由で、結局 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 action | hidden を追加、または 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 を検討する
この型を押さえておけば、ドロップダウン以外(チェックボックス、ラジオボタン、複数入力)でも同じ考え方でスムーズに値を受け渡せます。

コメント