先の記事「DropDownList を使って絞込み」の .NET 7.0 ASP.NET Core MVC 版を作ってみました。
上の画像がそれで、Microsoft の SQL Server サンプルデータベース Northwind の Orders テーブルから、ドロップダウンリストで選択された Customer (注文主) および Employee (注文を扱った従業員) を WHERE 句の選択条件にしてレコードを抽出し、抽出されたレコードを一覧表示するものです。
SQL Server の Orders テーブルは下の画像の構造となっており、複数の顧客の過去の注文データが 830 レコード含まれています。CustomerID, EmployeeID, ShipVia フィールドは、それぞれ Customers, Employees, Shippers テーブル (顧客、従業員、商品の輸送者の詳細) に FK 制約が張られています。
この Orders テーブルから、この記事の一番上の画像のように、ドロップダウンリストの選択結果に応じてレコードを抽出し、そのレコード一覧を表示する ASP.NET Core MVC アプリを作ります。作り方の概要を以下に書きます。
(1) プロジェクトの作成
Visual Studio 2022 のテンプレートの中から「ASP.NET Core Web アプリ (Model-View-Controller)」を選び ASP.NET Core MVC アプリのプロジェクトを作成します。
この記事の例ではターゲットフレームワークを .NET 7.0 にしています。その他のバージョンではこの記事とは多少違いがあるかもしれません。
(2) コンテキストクラスとエンティティクラスの作成
Entity Framework を利用するので、リバースエンジニアリングという手法を使って、SQL Server データベース Northwind からコンテキストクラスとエンティティクラスを生成します。
作成方法は Microsoft のドキュメント「スキャフォールディング (リバース エンジニアリング)」を見てください。各パラメータについては、こちらの記事 Scaffold-DbContext がまとまっていて分かりやすいと思います。
参考に、EF Core パッケージマネージャーコンソール (PMC) ツールを使った場合の Scaffold-DbContext コマンドの例を下に載せておきます。
Scaffold-DbContext -Connection "Data Source=(localdb)\MSSQLLocalDb;Initial Catalog=Northwind;Integrated Security=True" -Provider Microsoft.EntityFrameworkCore.SqlServer -ContextDir Data -OutputDir Models -Tables Products, Categories, Suppliers, Customers, Orders, Employees, Shippers, "Order Details" -DataAnnotations
生成されたコンテキストクラスとエンティティクラスのダイアグラムを EF Core Power Tools を使って表示すると以下のようになります。
上の画像の内、この記事で関係するのは Order, Customer, Employee, Shipper です。
(3) Controller と View の作成
Program.cs で DI コンテナに NorthwindContext を追加します。コードは以下の通りです。
builder.Services.AddDbContext<NorthwindContext>(options =>
options.UseSqlServer(builder.Configuration
.GetConnectionString("NorthwindConnection")));
appsettings.json に接続文字列を追加します。名前は上のコードの "NorthwindConnection" とし、接続文字列本体はリバースエンジニアリングの Scaffold-DbContext コマンドで使ったものと同じにします。ただし、文字列中の \ は \\ にエスケープする必要があるので注意してください。
その後で、Visual Studio のスキャフォールディング機能を使って、Orders テーブルの CRUD (Create / Read / Upadate / Delete) を行う Controller と View 一式を生成します。
アプリを実行してみて CRUD 機能が期待通り動くことを確認します。
(4) アクションメソッドの追加
ドロップダウンリストによる絞り込み機能を持つアクションメソッドを、上のステップ (3) で作った Controller に追加します。下のコードの Search メソッドと SearchResult メソッドが追加したアクションメソッドです。
基本的には、先の記事「jQuery ajax で部分ビューの呼出・表示 (CORE)」で書いたのと同様に、ajax によって部分ビューを呼び出して抽出結果のレコード一覧を表示します。
まず、Search メソッドに対応する Search.cshtml (コードは下のステップ(5)参照) にあるボタンクリックで、jQuery ajax を使ってアクションメソッド SearchResult を呼び出します。
アクションメソッド SearchResult が呼び出される時、ドロップダウンリストの選択結果が引数 customerId, employeeId に渡されます。SearchResult は渡された引数の内容に応じてレコードを抽出し、結果を Model として部分ビュー SearchResult.cshtml に渡します。
部分ビューSearchResult.cshtml がレンダリングした html ソース (抽出結果のテーブル) が SearchResult メソッドの応答として返されますので、それをドロップダウンリストの下に表示するようにしています。その結果がこの記事の一番上の画像です。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using MvcNet7App.Data;
using MvcNet7App.Models;
namespace MvcNet7App.Controllers
{
public class OrdersController : Controller
{
private readonly NorthwindContext _context;
public OrdersController(NorthwindContext context)
{
_context = context;
}
// ・・・中略・・・
// Customer と Employee の DropDownList で絞り込みが行えるページを追加
public IActionResult Search()
{
ViewData["CustomerId"] =
new SelectList(_context.Customers, "CustomerId", "CompanyName");
// Employees テーブルは FirstName と LastName でフィールドが別れて
// いるので、以下のようにして FirstName と LastName を結合する
var employeeList = _context.Employees
.Select(e => new
{
EmployeeId = e.EmployeeId.ToString(),
EmployeeName = e.FirstName + " " + e.LastName
});
ViewData["EmployeeId"] =
new SelectList(employeeList, "EmployeeId", "EmployeeName");
return View();
}
// 部分ビュー用のアクションメソッド。DropDownList で ALL を選ぶと
// 引数の customerId, employeeId には null が渡される
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SearchResult(string customerId,
int? employeeId)
{
// Include が返すのは IIncludableQueryable<TEntity,TProperty>
// 型なので、var order = ... とすると下の Where メソッドの所
// で IQueryable<Order> を IIncludableQueryable<Order,Shipper>
// に暗黙的に変換できませんというエラーになるので注意
IQueryable<Order> orders = _context.Orders
.Include(o => o.Customer)
.Include(o => o.Employee)
.Include(o => o.ShipViaNavigation);
// null は ALL (全顧客) なのでスキップ
if (customerId != null)
{
orders = orders.Where(o => o.CustomerId == customerId);
}
// null は ALL (全従業員) なのでスキップ
if (employeeId != null)
{
orders = orders.Where(o => o.EmployeeId == employeeId);
}
return PartialView(await orders.ToListAsync());
}
// ・・・中略・・・
}
}
(5) Search.cshtml
上のステップ (4) のアクションメソッド Search に対応する View です。jQuery ajax を使ってアクションメソッド SearchResult を呼び出し、応答として返された部分ビューの html ソースをドロップダウンリストの下の div 要素に表示するための JavaScript / jQuery のコードを含んでいます。
@{
ViewData["Title"] = "Search";
}
<h1>Search by Customer and Employee</h1>
@*asp-action 属性を入れて Tag ヘルパーだと認識させないと
CSRF 用のトークンが発行されないので注意*@
<form asp-action="Search">
Customer:
<select id="customerId" name="customerId"
asp-items="ViewBag.CustomerId">
<option value="">ALL</option>
</select>
Employee:
<select id="employeeId" name="employeeId"
asp-items="ViewBag.EmployeeId">
<option value="">ALL</option>
</select>
<input type="submit" value="Search" />
</form>
<div id="result"></div>
@section Scripts {
<script type="text/javascript">
//<![CDATA[
var form = document.querySelector("form");
// ボタンクリックで form が submit されるという
// 動きになるので、form 要素の submit イベントに
// リスナをアタッチして jQuert ajax で処理する
$(form).on("submit", function (event) {
// submit されては困るのでキャンセル
event.preventDefault();
// jQuery ajax を使って、部分ビューを応答とし
// て返すアクションメソッド Orders/SeachResult
// にフォームデータを POST 送信する
$.ajax({
type: "POST",
url: "/Orders/SearchResult",
// フォームデータを取得、送信 data に設定
data: $(this).serialize(),
dataType: "html",
// 要求後、応答が返ってくるまで Loading...
// というメッセージを出す
beforeSend: () => {
$("#result").empty();
$("#result").append(
'<span>Loading...</span>'
);
}
}).done(function (data) {
$("#result").empty();
$("#result").append(data);
}).fail(function (jqXHR, status, error) {
$('#result').text('Status: ' + status +
', Error: ' + error);
});
})
//]]>
</script>
}
(6) SearchResult.cshtml
アクションメソッド SearchResult からドロップダウンリストの選択結果に従って作成された Model を渡され、それを使って結果をブラウザに表示する html ソースを生成する部分ビューです。
@model IEnumerable<MvcNet7App.Models.Order>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.OrderDate)
</th>
<th>
@Html.DisplayNameFor(model => model.RequiredDate)
</th>
<th>
@Html.DisplayNameFor(model => model.ShippedDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Freight)
</th>
<th>
@Html.DisplayNameFor(model => model.ShipName)
</th>
<th>
@Html.DisplayNameFor(model => model.ShipAddress)
</th>
<th>
@Html.DisplayNameFor(model => model.ShipCity)
</th>
<th>
@Html.DisplayNameFor(model => model.ShipRegion)
</th>
<th>
@Html.DisplayNameFor(model => model.ShipPostalCode)
</th>
<th>
@Html.DisplayNameFor(model => model.ShipCountry)
</th>
<th>
@Html.DisplayNameFor(model => model.Customer)
</th>
<th>
@Html.DisplayNameFor(model => model.Employee)
</th>
<th>
@Html.DisplayNameFor(model => model.ShipViaNavigation)
</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.OrderDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.RequiredDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.ShippedDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Freight)
</td>
<td>
@Html.DisplayFor(modelItem => item.ShipName)
</td>
<td>
@Html.DisplayFor(modelItem => item.ShipAddress)
</td>
<td>
@Html.DisplayFor(modelItem => item.ShipCity)
</td>
<td>
@Html.DisplayFor(modelItem => item.ShipRegion)
</td>
<td>
@Html.DisplayFor(modelItem => item.ShipPostalCode)
</td>
<td>
@Html.DisplayFor(modelItem => item.ShipCountry)
</td>
<td>
@Html.DisplayFor(modelItem =>
item.Customer!.CompanyName)
</td>
<td>
@{
string name = (item.Employee != null) ?
item.Employee.FirstName + " " +
item.Employee.LastName : "";
}
@Html.DisplayFor(modelItem => name)
</td>
<td>
@Html.DisplayFor(modelItem =>
item.ShipViaNavigation!.CompanyName)
</td>
</tr>
}
</tbody>
</table>