Part 6. ASP.NET MVC Coreによるデータ更新アプリ
では引き続き、更新系アプリを作成してみます。Web API でデータ更新アプリを作るのはちょっと骨が折れるので、まずは ASP.NET MVC Core でデータ更新アプリを作ってみることにします。若干遠回りではありますが、こちらをいったん理解しておくと、Web API でのデータ更新アプリの作り方も理解しやすくなるでしょう。
■ ファイルの追加
ここまで作ってきたプロジェクトに /Controllers/Sample02Controller.cs、/Views/Sample02/ListAuthors.cshtml、/Views/Sample02/EditAuthor.cshtml ファイルを追加しておきます。
■ 著者一覧ページの実装
まずは Part 3 で解説した方法をもとに、著者一覧ページを作成しましょう。
using Decode2016.WebApp.Models;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Decode2016.WebApp.Controllers
{
public class Sample02Controller : Controller
{
[HttpGet]
public ActionResult ListAuthors()
{
using (PubsEntities pubs = new PubsEntities())
{
var query = pubs.Authors
.Select(a => new AuthorOverview()
{
AuthorId = a.AuthorId,
AuthorName = a.AuthorFirstName + " " + a.AuthorLastName,
Phone = a.Phone,
State = a.State,
Contract = a.Contract
});
ViewData["Authors"] = query.ToList();
}
return View();
}
}
}
ListAuthors.cshtml ファイル側では、著者データ編集ページに遷移できるように、著者 ID 部分をハイパーリンク化しておきます。例えば、”172-32-1176" のデータをクリックした際には、/Sample02/EditAuthor/172-32-1176 に遷移するようにしておきます。このようにしておくと、URL の 3 つ目の値を、EditAuthor() アクションメソッドの id パラメータにより受け取ることができるようになります。(この辺については、ASP.NET MVC の書籍に詳しく解説されていますので、調べてみてください。)
@using Decode2016.WebApp.Models
@{
ViewBag.Title = "編集対象の著者選択";
}
<h4>編集対象となる著者を選択してください。</h4>
@{
if (ViewData["Authors"] != null)
{
var data = ViewData["Authors"] as List<AuthorOverview>;
<div class="table-responsive">
<table class="table table-condensed table-striped table-hover">
<thead>
<tr>
<th>著者ID</th>
<th>著者名</th>
<th>電話番号</th>
<th>州</th>
<th>契約有無</th>
</tr>
</thead>
<tbody>
@foreach (AuthorOverview a in data)
{
<tr>
<td><a href="/Sample02/EditAuthor/@a.AuthorId">@a.AuthorId</a></td>
<td>@a.AuthorName</td>
<td>@a.Phone</td>
<td>@a.State</td>
<td><input type="checkbox" disabled @(a.Contract ? "checked" : "") /></td>
</tr>
}
</tbody>
</table>
</div>
}
}
<hr />
<p>
<a href="/">業務メニューに戻る</a>
</p>
■ データ編集ページの実装
続いてデータ編集ページを実装します。データ編集ページは、① 一覧ページからハイパーリンクで飛んできて画面が表示され(HTTP-GET)、② フォームデータを入力し、再度呼び出す(HTTP-POST)ことになります。EditAuthor ページ側の実装については、HTTP-GET, HTTP-POST のプロトコルで、初回/ポストバックのどちらであるのかを見分けるとよいでしょう。
さて、このような更新系アプリケーションの実装の厄介なところは、単体入力チェックを、ブラウザ上/サーバ側の両方で実装しなければならない点です。このポイントについては、以前、2009 年に書いたこちらのエントリの考え方と同じですが、ASP.NET MVC を使う場合、二重実装の回避には、より洗練された手法であるデータアノテーション方式を使います。
まず、入力フォームと同じデータ構造を持つ構造体クラス(ViewModel クラスと呼ばれます)を作成し、ここにデータアノテーションを使って、単体入力エラーチェックの内容を指定します。(ViewModel のコードはどこに定義してもよいですが、この Sample02 でしか利用しないので、Sample02Controller.cs クラスの内部クラスとして定義してしまうとよいでしょう。)
public class EditViewModel
{
public string AuthorId { get; set; }
[Required(ErrorMessage = "著者名(名)は必須入力項目です。")]
[RegularExpression(@"^[\u0020-\u007e]{1,20}$", ErrorMessage = "著者名(名)は半角 20 文字以内で指定してください。")]
public string AuthorFirstName { get; set; }
[Required(ErrorMessage = "著者名(姓)は必須入力項目です。")]
[RegularExpression(@"^[\u0020-\u007e]{1,40}$", ErrorMessage = "著者名(姓)は半角 40 文字以内で指定してください。")]
public string AuthorLastName { get; set; }
[Required(ErrorMessage = "電話番号は必須入力項目です。")]
[RegularExpression(@"^\d{3} \d{3}-\d{4}$", ErrorMessage = "電話番号は 012 345-6789 のような形式で指定してください。")]
public string Phone { get; set; }
[Required(ErrorMessage = "州は必須入力項目です。")]
[RegularExpression(@"^[A-Z]{2}$", ErrorMessage = "州は半角大文字 2 文字で指定してください。")]
public string State { get; set; }
}
続いて、アクションメソッドを定義します。要点は以下の 2 つです。
- アクションメソッドの引数
- string id パラメータをつけておくと、URL の第 3 引数を受け取ることができます。
- ビューへのデータ引き渡し
- データベースからデータを取ってきて、EditViewModel クラスのインスタンスに代入し、return View() の引数とすることで、View 側にデータを引き渡すことができます。(※ 通常、ビューへは ViewData[…] を使ってデータを引き渡しますが、ViewModel クラスから JavaScript のエラーチェックロジックを動的に生成させたい場合には、後述するように *.cshtml 側の先頭に @model を定義し、データを return View() の引数として渡します。なお、コードからわかるように、ViewData[…] との併用は可能です。)
[HttpGet]
public ActionResult EditAuthor(string id)
{
// 当該著者 ID のデータを読み取る
Author editAuthor = null;
using (PubsEntities pubs = new PubsEntities())
{
var query = from a in pubs.Authors
where a.AuthorId == id
select a;
editAuthor = query.FirstOrDefault();
}
// View に引き渡すデータを準備する
EditViewModel vm = new EditViewModel()
{
AuthorId = editAuthor.AuthorId,
AuthorFirstName = editAuthor.AuthorFirstName,
AuthorLastName = editAuthor.AuthorLastName,
Phone = editAuthor.Phone,
State = editAuthor.State
};
// View にデータを引き渡すにあたり、入力データと周辺データを分けておく。
// (ViewModel に周辺データを入れることで、ViewModel を完全にフォームモデルに一致させるように設計)
using (PubsEntities pubs = new PubsEntities())
{
var query = pubs.Authors.Select(a => a.State).Distinct();
ViewData["AllStates"] = query.ToList();
}
return View(vm); // 編集画面を作成して返す
}
次に、*.cshtml ファイルを作成します。ASP.NET MVC では、jQuery Validation を内部で利用するため、JavaScript ライブラリを追加で組み込みます。後で再利用できるように、/Views/Shared/_ImportsLibraryValidation.cshtml と /Views/Shared/_ImportsStyleValidation.cshtml に共有ファイルとして切り出しておきましょう。
[/Views/Shared/_ImportsLibraryValidation.cshtml]
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/localization/messages_ja.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/mvc/5.2.3/jquery.validate.unobtrusive.min.js"></script>
[/Views/Shared/_ImportsStyleValidation.cshtml]
<style type="text/css">
@@media only screen and (min-width : 0px) and (max-width : 767px) {
}
@@media only screen and (min-width : 768px) and (max-width : 991px) {
dl {
width: 738px; /* 750-12 */
margin: 6px;
}
dl dt {
float: left;
}
dl dd {
margin-left: 200px;
}
}
@@media only screen and (min-width : 992px) and (max-width : 1199px) {
dl {
width: 958px; /* 970-12 */
margin: 6px;
}
dl dt {
float: left;
}
dl dd {
margin-left: 200px;
}
}
@@media only screen and (min-width : 1200px) {
dl {
width: 1158px; /* 1170-12 */
margin: 6px;
}
dl dt {
float: left;
}
dl dd {
margin-left: 200px;
}
}
/* エラーメッセージ用 */
/* jQuery unobtrusive validation 用 */
.field-validation-error {
color: #ff0000;
}
.field-validation-valid {
display: none;
}
.input-validation-error {
border: 2px solid #ff0000;
background-color: #ffeeee;
}
.validation-summary-errors {
font-weight: bold;
color: #ff0000;
}
.validation-summary-valid {
display: none;
}
/* jQuery Validation 用 */
.error {
color:red
}
input.error, select.error, textarea.error {
border: 2px solid red;
background-color: #ffeeee;
}
</style>
続いて /Views/Sample02/EditAuthor.cshtml ファイルを実装します。(要点は後述)
@model Decode2016.WebApp.Controllers.Sample02Controller.EditViewModel
@{
ViewBag.Title = "著者データの編集";
}
@section Libraries {
@Html.Partial("_ImportsLibraryValidation")
}
@section Styles {
@Html.Partial("_ImportsStyleValidation")
}
<h4>著者データを修正してください。</h4>
@using (Html.BeginForm("EditAuthor", "Sample02", new { id = Model.AuthorId }))
{
<dl>
<dt>著者ID</dt>
<dd>@Model.AuthorId</dd>
</dl>
<dl>
<dt>著者名(名)</dt>
<dd>@Html.TextBoxFor(m => m.AuthorFirstName, new { data_val_specialnamecheck = "指定された名前(名・姓の組み合わせ)は使えません。" }) @Html.ValidationMessageFor(m => m.AuthorFirstName, "*")</dd>
</dl>
<dl>
<dt>著者姓(姓)</dt>
<dd>@Html.TextBoxFor(m => m.AuthorLastName, new { data_val_specialnamecheck = "指定された名前(名・姓の組み合わせ)は使えません。" }) @Html.ValidationMessageFor(m => m.AuthorLastName, "*")</dd>
</dl>
<dl>
<dt>電話番号</dt>
<dd>@Html.TextBoxFor(m => m.Phone) @Html.ValidationMessageFor(m => m.Phone, "*")</dd>
</dl>
<dl>
<dt>州</dt>
<dd>
@{
List<string> states = (List<string>)ViewData["AllStates"];
}
@Html.DropDownList("State", states.Select(s => new SelectListItem() { Text = s, Value = s, Selected = (s == Model.State) }))
</dd>
</dl>
<p>
<input type="submit" value="登録" />
<input type="button" id="btnCancel" value="キャンセル" />
</p>
@Html.ValidationSummary("入力にエラーがあります。修正してください。")
}
<hr />
<p>
<a href="/">業務メニューに戻る</a>
</p>
@section Scripts {
<script type="text/javascript">
$(function () {
$("#btnCancel").click(function () {
window.location = "@Url.Action("ListAuthors")";
return false;
});
});
</script>
}
上記コードの要点は以下の通りです。
- モデルクラスの指定
- ファイルの先頭に記述している “@model Decode2016.WebApp.Controllers.Sample02Controller.EditViewModel” が重要です。JavaScript エラーチェックロジックの動的生成機能を利用したい場合には、このモデルクラスの指定が必要になります。
- 入力フォームの作成
- データ入力フォームを作成するために、@using (Html.BeginForm("EditAuthor", "Sample02", new { id = Model.AuthorId })) を使います。これにより、HTTP-POST でデータを送信するフォームを作成することができます。
- 単体入力エラーチェック機能つきテキストボックスの作成
- モデルクラスを指定した上で、@Html.TextBoxFor(m => m.XXX) という指定を行うことにより、入力エラーチェック機能つきテキストボックスを作成することができます。
- 単体入力エラー一括表示部分の作成
- @Html.ValidationSummary(…) 命令により、単体入力エラーを一括して表示する領域を作成することができます。
以上でクライアント側の実装は終わりです。続いて、登録ボタンを押下して HTTP-POST データを送信した際の、サーバ側の受け取りロジックを実装します。
[HttpPost]
public ActionResult EditAuthor(string id, EditViewModel model)
{
using (PubsEntities pubs = new PubsEntities())
{
var query = pubs.Authors.Select(a => a.State).Distinct();
ViewData["AllStates"] = query.ToList();
}
// 送信されてきたデータを再チェック
if (ModelState.IsValid == false)
{
// 前画面を返す
// ID フィールドがロストしているので補完する
model.AuthorId = id;
return View(model);
}
model.AuthorId = id;
// データベースに登録を試みる
using (PubsEntities pubs = new PubsEntities())
{
Author target = pubs.Authors.Where(a => a.AuthorId == model.AuthorId).FirstOrDefault();
target.AuthorFirstName = model.AuthorFirstName;
target.AuthorLastName = model.AuthorLastName;
target.Phone = model.Phone;
target.State = model.State;
pubs.SaveChanges();
}
// 一覧画面に帰る
return RedirectToAction("ListAuthors");
}
コードの要点は以下の通りです。
- ビューモデルによる送信データの受け取り
- ブラウザから送られてくるデータは、public ActionResult EditAuthor(string id, string AuthorFirstName, string AuthorLastName, string Phone, …) などのようにして、パラメータを使って受け取ることができます。(これをパラメータバインディングと呼びます)
- しかし、上記のコードに示すように、構造体クラスを使って一括して受け取ることもできます。(これをモデルバインディングと呼びます)
- モデルバインディングによる単体入力エラーチェック
- モデルバインディングを使ってデータを受け取った場合、データアノテーションで指定した単体入力チェックにエラーがあるか否かを、ModelState.IsValid メソッドで簡単に確認することができます。また、サーバ側で単体入力エラーが見つかった場合、ビューを使ってエラーメッセージつき画面を簡単に返すこともできます。
以上により、ViewModel クラスにデータアノテーションにより付与した単体入力エラーチェックロジックを、ブラウザ上での JavaScript チェックとサーバ側での再チェックの両方に利用したデータ更新アプリケーションを作成することができます。
なお、今回は話を簡単にするため、以下の 2 点については説明を割愛しています。興味がある方は、各自で調査・実装してみてください。
- アンチリクエストフォージェリ対策
- 現在の実装の場合、サーバ側ではねつ造されたフォーム送信データも受け取って処理してしまいます。(いわゆる「なりすまし書き込み」ができてしまう)
- この問題を避けるため、ASP.NET MVC ではアンチリクエストフォージェリ機能が備わっています。([ValidateAntiForgeryToken()]) 非常に簡単にこの機能を使うことができるようになっていますので、必ず追加で実装するようにしてください。
- 楽観同時実行制御機能
- 現在の実装では、データベース上のデータが他のユーザにより書き換えられたとしても、何も考えずに上書き更新してしまいます。
- これを避けるために、通常は楽観同時実行制御機能による制御ロジックを組み込みますが、今回は簡単のため、これを実装していません。EF Core でも楽観同時実行制御機能の利用は可能ですので、こちらもぜひ組み込んでみてください。
引き続き最後のエントリでは、同じアプリケーションを Web API 方式(SPA 型)で開発してみたいと思います。