ASP.NET Core MVCの公式チュートリアルを丁寧に解説する記事の第3回です。
第2回ではスキャフォールディングによるCRUD機能の生成や、Entity Framework Coreを使ったDB連携について見ていきました。
第3回では、タグヘルパーやPOSTリクエストの処理を確認し、さらに検索機能を追加するところまでを扱います。
公式チュートリアルのパート6~7に対応します。
タグヘルパー
まずはタグヘルパーについて見ていきます。
しばらくは作成済みのコード内容の確認が続くので退屈かもしれませんが、とても重要なので確実に理解しておきたい部分です。
まずアプリをデバッグ実行したら、「http://localhost:ポート番号/Movies
」にアクセスします。
※事前に適当なデータを「Create New」から追加してあります。
Edit・Details・Deleteの各リンク部分のソースをブラウザで確認してみます。
<td>
<a href="/Movies/Edit/3">Edit</a> |
<a href="/Movies/Details/3">Details</a> |
<a href="/Movies/Delete/3">Delete</a>
</td>
href属性が「/コントローラー名/アクションメソッド名/ID」となっていますね。
これらはViews/Movies/Index.cshtml
の以下のコードから自動生成されています。
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
asp-action
でアクションメソッド名を、asp-route-id
でMovieモデル内のIDを指定して、href属性を動的に生成しています。
このようにタグヘルパーを使うと通常のHTMLのように記述できるので、C#に詳しくないWebデザイナー等でも直感的に理解しやすいというメリットがあります。
今回見たのは「アンカータグヘルパー」でしたが、他にも様々な種類のタグヘルパーがあります。
興味のある方は下記ドキュメントを参照してください。
POSTリクエストの処理
POSTリクエストの処理を確認するために、MoviesコントローラーのEditメソッドを見てみましょう。
Editメソッドは2つあり、上がGETメソッド、下がPOSTメソッドでアクセスした際の処理になります。
※以降、わかりやすさのために適宜コメントを付与しています。
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
// idやMovieテーブルがnullの場合はNotFoundを返す
if (id == null)
{
return NotFound();
}
// Movieテーブルからidが一致する映画データを取得し、ビューに返す
var movie = await _context.Movie.FindAsync(id);
if (movie == null)
{
return NotFound();
}
return View(movie);
}
// Edit画面で「Save」をクリックした際に実行される
// POST: Movies/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,Genre,Revenue,ReleaseDate")] Movie movie)
{
if (id != movie.Id)
{
return NotFound();
}
// モデル検証が成功した場合の処理
if (ModelState.IsValid)
{
try
{
// 送信されたデータをもとにMovieテーブルのデータを更新
_context.Update(movie);
await _context.SaveChangesAsync();
}
// 同時に同じデータが変更された(競合が発生した)場合の例外処理
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
// Indexページにリダイレクトする
return RedirectToAction(nameof(Index));
}
return View(movie);
}
メソッド内の3つの属性とデータを保存する処理について詳しく見てみましょう。
HttpPost属性
メソッドの前に[HttpPost]
属性を付けると、POSTメソッドによるリクエストのみ受け付けることができます。
何も記載しなかった場合は[HttpGet]
属性を付けた場合と同じ挙動になりますが、可読性を高めるためにあえて[HttpGet]
属性を付けるようにするのがよいと思います。
ValidateAntiForgeryToken属性
CSRF(クロスサイトリクエストフォージェリ)攻撃を防ぐための属性です。
Edit.cshtml
内のformタグヘルパー(<form asp-action="Edit">
)で自動生成される対策トークンとセットで使用します。
POSTメソッドには必ず[ValidateAntiForgeryToken]
を付与すると覚えておけばいいでしょう。
攻撃手法等については解説しませんが、詳細が気になる方は以下を参照してください。
ASP.NET Core でクロスサイト リクエスト フォージェリ (XSRF/CSRF) 攻撃を防止する
Bind属性
オーバーポスティング(過多ポスティング)攻撃を防ぐための属性で、フォーム画面から送信されるプロパティの名前を指定します。
モデルにPOST送信したくないプロパティがある場合に、それを送信・変更されるのを防ぐために使います。
例えばMovie.csに「Secret」というプロパティがあり、Edit画面からはSecretの値を変更されたくないとします。(もちろんSecretの入力フォームもない)
もしEditメソッドの引数にBind属性を指定していなかったとすると、悪意のあるユーザーがツール等を使ってSecretプロパティをPOST送信した場合、Editメソッド側では送信された値を通常通り受け取ってしまい、Secretの値が意図せずに更新されてしまいます。
少しわかりにくいかと思うので、こちらも詳細については以下を参照してください。
Bind属性も、POSTメソッドの場合に必ず指定すると覚えておけばとりあえずOKです。
ModelState.IsValidとSaveChangesAsync
モデルバインドシステムによって、Edit画面から送信されたフォームの値が取得され、Movieオブジェクトが作成されます。
ModelState.IsValid
で、そのMovieオブジェクトがフォームの値で変更できることを検証し、問題がなければコンテキストを更新します。
その後SaveChangesAsync
メソッドを実行することでUpdate文が実行され、テーブルのレコードが更新されます。
検索機能の追加
ここからは、ジャンルまたはタイトルで映画を絞り込める検索機能を追加していきます。
まずMoviesController.cs
のIndex
メソッドを次のように書き換えます。
// GET: Movies
public async Task<IActionResult> Index(string searchString)
{
// Movieテーブルから全てのデータを取得するLINQクエリ
var movies = _context.Movie.Select(m => m);
// タイトル検索処理
if (!string.IsNullOrEmpty(searchString))
{
// タイトルに検索文字列が含まれるデータを抽出するLINQクエリ
movies = movies.Where(s => s.Title!.Contains(searchString));
}
// ToListAsyncメソッドが呼び出されたらクエリが実行され(遅延実行)、その結果をビューに返す
return View(await movies.ToListAsync());
}
画面からserachStringをパラメータとして受け取り、タイトルと一致するデータをDBから検索して画面に表示します。
次は画面に検索窓を作成します。Views/Movies/Index.html
に以下の強調表示されたコードを追加してください。
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
Title: <input type="text" name="SearchString" />
<input type="submit" value="検索" />
</p>
</form>
<table class="table">
<省略>
formタグ内で「method=”get”」と指定しているのがポイントです。ここでは検索のみで値の更新は行わないので、POSTメソッドではなくGETメソッドを指定しています。
ではデバッグ実行してみましょう。
検索窓が表示され、タイトル名で表示を絞り込むことができるようになりました。
アドレスバーに「SearchString=<検索した文字列>」と表示されることも確認できます。
ジャンルによる検索機能の追加
次に、ジャンル名でも表示を絞り込めるように検索機能を追加します。
Modelsフォルダ内に新たにMovieGenreViewModel.cs
を追加しましょう。
using Microsoft.AspNetCore.Mvc.Rendering;
namespace MvcMovie.Models
{
public class MovieGenreViewModel
{
public List<Movie>? Movies { get; set; }
public SelectList? Genres { get; set; }
public string? MovieGenre { get; set; }
public string? SearchString { get; set; }
}
}
それぞれのプロパティの役割は次の通りです。
- Movies:Movieテーブルにある映画データをMovie型リストで格納
- Genres:Movieテーブルにある全ジャンルをSelectList型で格納
- MovieGenre:ドロップダウンリストで選択されたジャンルを格納
- SearchString:検索文字列を格納
SelectListクラスは、ドロップダウンリストとしてユーザーに1つの項目を選択させたい場合に使用します。(項目が重複しないリスト)
MoviesController.cs
のIndexメソッドを次のように書き換えます。
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
// Movieテーブルから全てのジャンルを取得する
var genreQuery = _context.Movie
.OrderBy(m => m.Genre)
.Select(m => m.Genre);
// Movieテーブルから全てのデータを取得する
var movies = _context.Movie.Select(m => m);
// タイトル検索処理
if (!string.IsNullOrEmpty(searchString))
{
// タイトルに検索文字列が含まれるデータを抽出する
movies = movies.Where(s => s.Title!.Contains(searchString));
}
// ジャンル検索処理
if (!string.IsNullOrEmpty(movieGenre))
{
// 選択したジャンルと一致するデータを抽出する
movies = movies.Where(x => x.Genre == movieGenre);
}
// ジャンルと抽出したMovieデータをそれぞれリストにしてプロパティに格納する
var movieGenreVM = new MovieGenreViewModel
{
Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
Movies = await movies.ToListAsync()
};
// ViewModelをビューに渡す
return View(movieGenreVM);
}
さらに、index.cshtmlを次のように書き換えます。(ついでに表記を一部日本語化しています)
@model MvcMovie.Models.MovieGenreViewModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">新規作成</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">全て</option>
</select>
タイトル: <input type="text" asp-for="SearchString" />
<input type="submit" value="検索" />
</p>
</form>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Revenue)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Revenue)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
ここまで修正できたら、デバッグ実行してみましょう。
無事にジャンル名でも検索できるようになりました。
まとめ
前半ではタグヘルパーの説明とPOSTリクエストをコントローラで受け付けた場合の挙動を確認し、後半ではViewModelを使った検索処理を追加しました。
第4回(最終回)では、モデルの検証についてより詳しく見ていきたいと思います。
【おすすめ書籍】