ASP.NET MVC での連動ドロップダウンリストの実装方法を備忘録として書いておきます。
ASP.NET Web Forms アプリの場合は先の記事「DetailsView 中の連動 DropDownList」に書きました。それと同様な連動機能を MVC アプリで実装してみます。
記事「DetailsView 中の連動 DropDownList」と同様に、Northwind サンプルデータベースの Categories テーブルと Products テーブルを使って例を書きます。
上の画像に示すように 2 つのドロップダウンリストを配置し、1 つめのドロップダウンリストで分類を選択すると、2 つめのドロップダウンリストにはその分類に属する製品が絞り込まれて表示されるようにします。
データベースへのアクセスやデータの取得は先の記事「スキャフォールディング機能」のステップ 1 ~ 10 で書きました Entity Data Model (EDM) を利用します。Categories テーブルと Products テーブルの EDM ダイアグラムは以下の通りです。
記事「DetailsView 中の連動 DropDownList」の場合は、分類ドロップダウンリストの選択が変更されるとポストバックがかかり、サーバー側で変更に応じて製品ドロップダウンリストの内容を書き換えてページ全体を再描画するというものでした。いちいちポストバックして再描画というのがちょっとやりすぎの感があります。
Ajax Control Toolkit の中に CascadingDropDown というものがありますが、こちらは Ajax を利用して、上位ドロップダウンリストの変更があると、サーバー側の Web サービスに下位ドロップダウンリストに表示するデータを要求し、Web サービスから戻ってきた JSON 形式のデータで下位ドロップダウンリストの内容を書き換えるというものです。
この記事の MVC 版連動ドロップダウンリストでも、Ajax Control Toolkit の CascadingDropDown のやり方にならって、jQuery ajax を利用して、分類ドロップダウンリストの選択に応じて製品ドロップダウンリストに表示する ProductID と ProductName を JSON 形式で取得し、製品ドロップダウンリストの内容を書き換えるようにしました。
コードは以下の通りです。説明はコード内にコメントとして書きましたのでそちらを見てください。手抜きでスミマセン。
Model
using System.ComponentModel.DataAnnotations;
namespace Mvc5App.Models
{
public class Sales
{
public int Id { get; set; }
[Display(Name = "分類")]
[Required(ErrorMessage = "{0} の選択は必須")]
public int CategoryID { get; set; }
[Display(Name = "製品")]
[Required(ErrorMessage = "{0} の選択は必須")]
public int ProductID { get; set; }
[Display(Name = "コメント")]
[Required(ErrorMessage = "{0} は必須")]
public string Comment { get; set; }
}
}
Controller / Action Method
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Net;
using System.Web;
using System.Web.Mvc;
using Mvc5App;
namespace Mvc5App.Controllers
{
public class ProductsController : Controller
{
private NORTHWINDEntities db = new NORTHWINDEntities();
public ActionResult CascadingDropDown()
{
ViewBag.CategoryID =
new SelectList(db.Categories, "CategoryID", "CategoryName");
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CascadingDropDown(
[Bind(Include = "ProductID,CategoryID,Comment")]
Mvc5App.Models.Sales sales)
{
if (ModelState.IsValid)
{
return RedirectToAction("Index");
}
// 検証結果 NG で再描画する際は以下のように再度 ViewBag
// に SelectList を設定しないと分類ドロップダウンの中身
// が表示されないので注意
ViewBag.CategoryID =
new SelectList(db.Categories, "CategoryID", "CategoryName");
return View(sales);
}
// 製品ドロップダウンに表示する ProductID と ProductName を
// JSON 形式で取得するアクションメソッド。引数 id が分類ドロ
// ップダウンで選択された CategoryID
public async Task<ActionResult> GetProducts(int id)
{
var products = from p in db.Products
where p.CategoryID == id
select new
{
ProductID = p.ProductID,
ProductName = p.ProductName
};
return Json(await products.ToListAsync(),
JsonRequestBehavior.AllowGet);
}
}
}
View
@model Mvc5App.Models.Sales
@{
ViewBag.Title = "CascadingDropDown";
}
<h2>CascadingDropDown</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Sales</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(model => model.CategoryID,
htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownList("CategoryID", null,
"▼分類を選択してください▼",
htmlAttributes: new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.CategoryID,
"", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ProductID,
htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
<select class="form-control" id="ProductID" name="ProductID"></select>
@Html.ValidationMessageFor(model => model.ProductID,
"", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Comment,
htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Comment,
new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Comment,
"", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
<script type="text/javascript">
//<![CDATA[
// 長くなるので JavaScript / jQuery のコードは下に別掲
//]]>
</script>
}
JavaScript / jQuery
実際は以下のコードは View の @section Scripts { ... } ブロックの中にインラインで書いています。View のコードが長くなって見難くなるので別掲にしました。
$(function () {
// 分類ドロップダウンの jQuery オブジェクトを取得。
// html コードの id は html ヘルパの Id メソッドで取得
var categoryDDL = $('#@Html.Id("CategoryID")');
// 製品ドロップダウンの jQuery オブジェクトを取得。
// こちらは <select id="ProductID" とハードコーディン
// グされているので以下で OK (Id メソッドは不要)
var productDDL = $("#ProductID");
// DropDownList の第 3 引数 "▼分類を選択してください▼"
// は以下の option 要素になる (必ず value="" になると書
// いた Microsoft の文書が見つからないのが不安要素):
// <option value="">▼分類を選択してください▼</option>
// 以下は初期画面および検証結果 NG での再描画の際必要。
// 初期画面では categoryDDL.val() は上の option 要素の
// value="" を取得するので if 文の条件は false となる。
// 検証結果 NG で再描画される際は分類ドロップダウンの
// 選択に応じて製品ドロップダウンの内容を書き換える
if (categoryDDL.val() != "") {
productDDL.children().remove();
productDDL.append(
'<option value="">▼製品を選択してください▼</option>');
productDDL.removeAttr('disabled');
// 分類ドロップダウンの選択結果 (CategoryID) を
// jQuery ajax でサーバー側の GetProducts アクション
// メソッドに送信。アクションメソッドは Products テ
// ーブルに SELECT クエリを発行し、CategoryID に属す
// 製品の ProductID, ProductName を取得して JSON 文
// 字列として返す。その JSON 文字列から option 要素
// の文字列を組み立てて製品ドロップダウンに追加する
$.ajax({
url: '/Products/GetProducts/' + categoryDDL.val(),
method: 'get',
}).done(function (data) {
$.each(data, function (key, val) {
productDDL.append('<option value=' +
val.ProductID + '>' + val.ProductName +
'</option>');
});
}).fail(function (jqXHR, textStatus, errorThrown) {
alert('Error getting products!');
});
} else {
productDDL.children().remove();
productDDL.attr('disabled', 'disabled');
}
// 分類ドロップダウンの選択が変更されると change イベント
// が発生 するのでそのリスナで製品ドロップダウンの内容を
// 分類に応じて書き換え。リスナの中身は上と同じコード
categoryDDL.on("change", function () {
if (categoryDDL.val() != "") {
productDDL.children().remove();
productDDL.append(
'<option value="">▼製品を選択してください▼</option>');
productDDL.removeAttr('disabled');
$.ajax({
url: '/Products/GetProducts/' + categoryDDL.val(),
method: 'get',
}).done(function (data) {
$.each(data, function (key, val) {
productDDL.append('<option value=' +
val.ProductID + '>' + val.ProductName +
'</option>');
});
}).fail(function (jqXHR, textStatus, errorThrown) {
alert('Error getting products!');
});
} else {
productDDL.children().remove();
productDDL.attr('disabled', 'disabled');
}
});
});
このコードで、この記事の上の画像のとおり表示されます。
分類ドロップダウンリストの選択変更に連動して製品ドロップダウンリストの内容が変わります。送信ボタンクリックの際のドロップダウンリストの検証は、クライアント側ではかかりませんが、サーバー側では検証されてエラーメッセージは期待通り表示されます。
サーバー側での検証結果が NG の場合、同じ画面が再描画されユーザーに選択の修正を促すようにしていますが、その際のドロップダウンリストの連動も OK なことを確認しています。