パート 5: 編集フォームとテンプレート
作成者: Jon Galloway
MVC Music Store は、ASP.NET MVC と Visual Studio を使用した Web 開発の手順を段階的に紹介し、説明するチュートリアル アプリケーションです。
MVC Music Store は、音楽アルバムをオンラインで販売する軽量なサンプル ストアの実装です。基本的なサイト管理、ユーザー サインイン、ショッピング カート機能を実装しています。
このチュートリアル シリーズでは、ASP.NET MVC Music Store サンプル アプリケーションを作成するために必要なすべての手順について詳しく説明します。 パート 5 では、編集フォームとテンプレートについて説明します。
前の章では、データベースからデータを読み込んで表示しました。 この章では、データの編集も有効にします。
StoreManagerController の作成
まず、StoreManagerController という新しいコントローラーを作成します。 このコントローラーでは、ASP.NET MVC 3 Tools Update で使用できるスキャフォールディング機能を利用します。 次に示すように、[コントローラーの追加] ダイアログの各オプションを設定します。
[追加] ボタンをクリックすると、ASP.NET MVC 3 のスキャフォールディング メカニズムによって多くの作業が自動的に行われることがわかります。
- ローカル Entity Framework 変数を使って新しい StoreManagerController が作成されます
- プロジェクトの Views フォルダーに StoreManager フォルダーが追加されます
- Album クラスに厳密に型指定された Create.cshtml、Delete.cshtml、Details.cshtml、Edit.cshtml、Index.cshtml ビューが追加されます
新しい StoreManager コントローラー クラスには CRUD (作成、読み取り、更新、削除) コントローラー アクションが含まれており、これらには Album モデル クラスを操作し、Entity Framework コンテキストを使ってデータベースにアクセスする方法が適切に設定されています。
スキャフォールディングされたビューの変更
このコードは自動で生成されたものですが、私たちがこのチュートリアル全体で記述しているコードと同じように、標準的な ASP.NET MVC コードであるという点に注意してください。 その目的は、手動でコントローラーの定型コードを記述し、厳密に型指定されたビューを作成するための時間を節約することですが、コードを変更してはならない旨の切迫した警告がコメントに記載されているような種類の生成コードではありません。 これは開発者のコードであり、開発者による変更が想定されています。
そこで、まずは StoreManager の Index ビュー (/Views/StoreManager/Index.cshtml) を簡単に編集してみましょう。 このビューには、ストア内のアルバムを [Edit]、[Details]、[Delete] のリンクと共に一覧表示するテーブルが表示され、アルバムのパブリック プロパティが含まれています。 AlbumArtUrl フィールドはこの表示ではあまり役に立たないので、削除しましょう。 以下の強調表示された行が示すように、ビュー コードの <table> セクションで、AlbumArtUrl 参照を囲んでいる <th> 要素と <td> 要素を削除します。
<table>
<tr>
<th>
Genre
</th>
<th>
Artist
</th>
<th>
Title
</th>
<th>
Price
</th>
<th>
AlbumArtUrl
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Genre.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Artist.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.AlbumArtUrl)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.AlbumId }) |
@Html.ActionLink("Details", "Details", new { id=item.AlbumId }) |
@Html.ActionLink("Delete", "Delete", new { id=item.AlbumId })
</td>
</tr>
}
</table>
変更したビュー コードは次のようになります。
@model IEnumerable<MvcMusicStore.Models.Album>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Create
New", "Create")
</p>
<table>
<tr>
<th>
Genre
</th>
<th>
Artist
</th>
<th>
Title
</th>
<th>
Price
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Genre.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Artist.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.AlbumId }) |
@Html.ActionLink("Details", "Details", new { id=item.AlbumId }) |
@Html.ActionLink("Delete", "Delete", new { id=item.AlbumId })
</td>
</tr>
}
</table>
ストア マネージャーの概観
次に、アプリケーションを実行し、/StoreManager/ を参照します。 変更したストア マネージャーの Index が表示され、ストア内のアルバムの一覧が [Edit]、[Details]、[Delete] のリンクと共に表示されます。
[Edit] リンクをクリックすると、そのアルバムのフィールドを含む編集フォームが表示されます (Genre と Artist のドロップダウンなど)。
下部にある [Back to List] リンクをクリックしてから、アルバムの [Details] リンクをクリックします。 そうすると、ある個別のアルバムの詳細情報が表示されます。
もう一度 [Back to List] リンクをクリックしてから、[Delete] リンクをクリックします。 そうすると確認ダイアログが表示され、そのアルバムの詳細と、本当に削除するかどうかを確認するメッセージが表示されます。
下部にある [Delete] ボタンをクリックすると、そのアルバムが削除されて Index ページに戻りますが、削除したアルバムが表示されます。
ストア マネージャーは完成していませんが、実際に動作するコントローラーと、出発点となる CRUD 操作用のビュー コードがあります。
ストア マネージャーのコントローラーのコードを確認する
ストア マネージャーのコントローラーには、かなりの量のコードが含まれています。 これを上から下に見ていきましょう。 このコントローラーには、MVC コントローラーの標準的な名前空間と、Models 名前空間への参照が含まれています。 このコントローラーは MusicStoreEntities のプライベート インスタンスを持っており、各コントローラー アクションでデータ アクセス用に使用します。
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcMusicStore.Models;
namespace MvcMusicStore.Controllers
{
public class StoreManagerController : Controller
{
private MusicStoreEntities db = new MusicStoreEntities();
ストア マネージャーの Index アクションと Details アクション
Index ビューでは、以前に Store の Browse メソッドを説明したときに見たように、各アルバムの参照されるジャンルとアーティストの情報を含むアルバムの一覧を取得します。 Index ビューはリンクされたオブジェクトへの参照に従って、各アルバムのジャンル名とアーティスト名を表示できます。そのため、コントローラーは効率的になり、元の要求でこの情報を照会できます。
//
// GET: /StoreManager/
public ViewResult Index()
{
var albums = db.Albums.Include(a => a.Genre).Include(a => a.Artist);
return View(albums.ToList());
}
StoreManager コントローラーの Details コントローラー アクションは、前に記述した Store コントローラーの Details アクションとまったく同じように機能します。Find() メソッドを使って ID でアルバムを照会し、ビューに返します。
//
// GET: /StoreManager/Details/5
public ViewResult Details(int id)
{
Album album = db.Albums.Find(id);
return View(album);
}
Create アクション メソッド
Create アクション メソッドは、フォーム入力を処理するため、これまでに見てきたメソッドとは少し異なります。 ユーザーが最初に /StoreManager/Create/ にアクセスすると、空のフォームが表示されます。 この HTML ページには、アルバムの詳細を入力できるドロップダウンとテキストボックスの入力要素を含む <form> 要素が含まれています。
ユーザーは、アルバムのフォームの値を入力した後、[Save] ボタンを押してそれらの変更内容をアプリケーションに送信し、データベース内に保存できます。 ユーザーが [Save] ボタンを押すと、<form> は /StoreManager/Create/ の URL に対して HTTP-POST を実行し、<form> の値を HTTP-POST の一部として送信します。
ASP.NET MVC では、StoreManagerController クラス内に 2 つの個別の "Create" アクション メソッドを実装することで、こうした 2 つの URL 呼び出しシナリオのロジックを簡単に分割できます。つまり、/StoreManager/Create/ の URL に対する最初の HTTP-GET 参照を処理するメソッドと、送信される変更内容の HTTP-POST を処理するメソッドです。
ViewBag を使ってビューに情報を渡す
このチュートリアルでは以前に ViewBag を使用しましたが、あまり多く説明していませんでした。 ViewBag を使うと、厳密に型指定されたモデル オブジェクトを使わずにビューに情報を渡すことができます。 この場合、Edit HTTP-GET コントローラー アクションでジャンルとアーティストのリストの両方をフォームに渡してドロップダウンを設定する必要があります。これを行う最も簡単な方法は、それらを ViewBag 項目として返すことです。
ViewBag は動的オブジェクトです。つまり、プロパティを定義するコードを記述せずに、ViewBag.Foo や ViewBag.YourNameHere と入力することができます。 この場合、コントローラーのコードでは ViewBag.GenreId と ViewBag.ArtistId を使用するため、フォームで送信されるドロップダウンの値は GenreId と ArtistId になります。これらは設定する Album のプロパティです。
これらのドロップダウン値は、この目的のために作成された SelectList オブジェクトを使ってフォームに返されます。 これは次のようなコードを使って実行します。
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name");
アクション メソッドのコードからわかるように、このオブジェクトを作成するために次の 3 つのパラメーターが使用されています。
- ドロップダウンに表示される項目の一覧。 これは単なる文字列ではなく、ジャンルのリストを渡していることに注意してください。
- 次に SelectList に渡されているパラメーターは、選択済みの値です。 これにより、SelectList はリスト内の項目をどのように事前選択するかがわかります。 これは、よく似ている編集フォームを見ると理解しやすくなるでしょう。
- 最後のパラメーターは、表示されるプロパティです。 この場合は、Genre.Name プロパティがユーザーに表示されることを示しています。
そのことを念頭に置いて、HTTP-GET 作成アクションは非常にシンプルです。2 つの SelectList が ViewBag に追加され、モデル オブジェクトはフォームに渡されません (まだ作成されていないため)。
//
// GET: /StoreManager/Create
public ActionResult Create()
{
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name");
ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name");
return View();
}
Create ビューでドロップダウンを表示する HTML ヘルパー
ドロップダウン値をビューに渡す方法について説明したので、ビューを簡単に確認してそれらの値がどのように表示されるかを見てみましょう。 ビュー コード (/Views/StoreManager/Create.cshtml) では、ジャンルのドロップダウンを表示するために次の呼び出しが行われます。
@Html.DropDownList("GenreId",
String.Empty)
これは HTML ヘルパーと呼ばれます。共通のビュー タスクを実行するユーティリティ メソッドです。 HTML ヘルパーは、ビュー コードを簡潔で読みやすくする上で非常に便利です。 Html.DropDownList ヘルパーは ASP.NET MVC によって提供されますが、後で説明するように、アプリケーションで再利用するビュー コード用の独自のヘルパーを作成することもできます。
Html.DropDownList の呼び出しには、2 つのことを伝える必要があります。表示するリストを取得する場所と、事前選択する必要がある値 (ある場合) です。 最初のパラメーター (GenreId) は、モデルまたは ViewBag で GenreId という名前の値を探すように DropDownList に指示します。 2 番目のパラメーターは、ドロップダウン リストで最初に選択されている値として表示する値を示すために使用されます。 このフォームは作成フォームなので、事前に選択する値は存在せず、String.Empty が渡されます。
ポストされたフォーム値の処理
前に説明したように、各フォームには 2 つのアクション メソッドが関連付けられています。 1 つ目は HTTP-GET 要求を処理し、フォームを表示します。 2 つ目は、送信されたフォーム値を含む HTTP-POST 要求を処理します。 コントローラー アクションには [HttpPost] 属性があることに注意してください。これは、HTTP-POST 要求にのみ応答する必要があることを ASP.NET MVC に伝えます。
//
// POST: /StoreManager/Create
[HttpPost]
public ActionResult Create(Album album)
{
if (ModelState.IsValid)
{
db.Albums.Add(album);
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
return View(album);
}
このアクションは、次の 4 つの役割を果たします。
-
- フォーム値を読み取る
-
- フォーム値が検証規則に合格するかどうかを確認する
-
- フォームの送信が有効な場合は、そのデータを保存し、更新された一覧を表示する
-
- フォームの送信が無効な場合は、検証エラーでフォームを再表示する
モデル バインドを使用したフォーム値の読み取り
コントローラー アクションは、(ドロップダウン リストの) GenreId と ArtistId の値と、Title、Price、AlbumArtUrl のテキストボックス値を含むフォーム送信を処理します。 フォーム値に直接アクセスすることもできますが、ASP.NET MVC に組み込まれているモデル バインド機能を使用する方法をお勧めします。 コントローラー アクションがパラメーターとしてモデル型を受け取ると、ASP.NET MVC は、フォーム入力 (およびルートとクエリ文字列の値) を使ってその型のオブジェクトを設定しようとします。 これは、名前がモデル オブジェクトのプロパティと一致する値を探すことによって行われます。たとえば、新しい Album オブジェクトの GenreId 値を設定するときに、GenreId という名前の入力を探します。 ASP.NET MVC の標準メソッドを使ってビューを作成する場合、フォームは常にプロパティ名を入力フィールド名として使用してレンダリングされるため、このフィールド名は単純に一致します。
モデルの検証
モデルは、ModelState.IsValid の単純な呼び出しによって検証されます。 Album クラスにはまだ検証規則を追加していませんが、すぐに追加します。今のところは、このチェックが行うことはあまりありません。 重要なのは、この ModelStat.IsValid チェックはモデルに設定した検証規則に順応するため、将来検証規則を変更しても、コントローラーのアクション コードを更新する必要はないということです。
送信された値の保存
フォームの送信が検証に合格した場合は、その値をデータベースに保存します。 Entity Framework では、モデルを Albums コレクションに追加して SaveChanges を呼び出すだけで済みます。
db.Albums.Add(album);
db.SaveChanges();
Entity Framework によって、値を永続化するための適切な SQL コマンドが生成されます。 データを保存したら、アルバムの一覧にリダイレクトして、更新内容を確認できるようにします。 これを行うには、表示するコントローラー アクションの名前を指定して RedirectToAction を返します。 この場合は、Index メソッドです。
検証エラーと共に無効なフォーム送信を表示する
無効なフォーム入力の場合、(HTTP-GET の場合と同様に) ドロップダウン値が ViewBag に追加され、バインドされたモデル値がビューに返されて表示されます。 検証エラーは、@Html.ValidationMessageFor HTML ヘルパーを使って自動的に表示されます。
作成フォームのテスト
これをテストするには、アプリケーションを実行して /StoreManager/Create/ を参照します。そうすると、StoreController の Create HTTP-GET メソッドによって返された空のフォームが表示されます。
いくつかの値を入力し、[Create] ボタンをクリックして、フォームを送信します。
編集の処理
Edit アクションのペア (HTTP-GET と HTTP-POST) は、先ほど見た Create アクション メソッドによく似ています。 編集シナリオでは既存のアルバムを操作する必要があるため、Edit HTTP-GET メソッドは、ルート経由で渡される "id" パラメーターに基づいてアルバムを読み込みます。 AlbumId によってアルバムを取得するためのこのコードは、以前に Details コントローラー アクションで見たコードと同じです。 Create / HTTP-GET メソッドと同様に、ViewBag を使用してドロップダウン値が返されます。 これにより、ViewBag を介して追加データ (ジャンルの一覧など) を渡しながら、アルバムを (Album クラスに厳密に型指定された) ビューにモデル オブジェクトとして返すことができます。
//
// GET: /StoreManager/Edit/5
public ActionResult Edit(int id)
{
Album album = db.Albums.Find(id);
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
return View(album);
}
Edit HTTP-POST アクションは、Create HTTP-POST アクションとよく似ています。 唯一の違いは、db.Albums コレクションに新しいアルバムを追加する代わりに、db.Entry(album) を使ってアルバムの現在のインスタンスを見つけて、その状態を Modified に設定するということです。 これにより、新しいアルバムを作成するのではなく既存のアルバムを変更するということが Entity Framework に伝わります。
//
// POST: /StoreManager/Edit/5
[HttpPost]
public ActionResult Edit(Album album)
{
if (ModelState.IsValid)
{
db.Entry(album).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
return View(album);
}
これをテストするには、アプリケーションを実行して /StoreManger/ を参照し、アルバムの [Edit] リンクをクリックします。
そうすると、Edit HTTP-GET メソッドによって編集フォームが表示されます。 いくつかの値を入力し、[Save] ボタンをクリックします。
そうすると、フォームが送信され、値が保存され、アルバムの一覧に戻り、値が更新されたことが表示されます。
削除の処理
削除は、編集や作成と同じパターンに従います。1 つのコントローラー アクションを使って確認フォームを表示し、もう 1 つのコントローラー アクションを使ってフォームの送信を処理します。
HTTP-GET Delete コントローラー アクションは、以前のストア マネージャーの Details コントローラー アクションとまったく同じです。
//
// GET: /StoreManager/Delete/5
public ActionResult Delete(int id)
{
Album album = db.Albums.Find(id);
return View(album);
}
Delete ビュー コンテンツ テンプレートを使用して、Album 型に厳密に型指定されたフォームを表示します。
Delete テンプレートはモデルのすべてのフィールドを表示しますが、これは大幅に簡略化できます。 /Views/StoreManager/Delete.cshtml のビュー コードを次のように変更します。
@model MvcMusicStore.Models.Album
@{
ViewBag.Title = "Delete";
}
<h2>Delete Confirmation</h2>
<p>Are you sure you want to delete the album titled
<strong>@Model.Title</strong>?
</p>
@using (Html.BeginForm()) {
<p>
<input type="submit" value="Delete" />
</p>
<p>
@Html.ActionLink("Back to
List", "Index")
</p>
}
これにより、簡略化された削除の確認が表示されます。
[Delete] ボタンをクリックすると、フォームがサーバーにポストバックされ、DeleteConfirmed アクションが実行されます。
//
// POST: /StoreManager/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
Album album = db.Albums.Find(id);
db.Albums.Remove(album);
db.SaveChanges();
return RedirectToAction("Index");
}
HTTP-POST Delete コントローラー アクションは、次のアクションを実行します。
-
- ID でアルバムを読み込む
-
- そのアルバムを削除し、変更を保存する
-
- Index にリダイレクトし、アルバムが一覧から削除されたことを示す
これをテストするには、アプリケーションを実行し、/StoreManager を参照します。 一覧からアルバムを選択し、[Delete] リンクをクリックします。
削除の確認画面が表示されます。
[Delete] ボタンをクリックすると、アルバムが削除され、ストア マネージャーの Index ページに戻って、そのアルバムが削除されたことを確認できます。
カスタム HTML ヘルパーを使ってテキストを切り詰める
ストア マネージャーの Index ページには、1 つの潜在的な問題があります。 アルバムのタイトルとアーティスト名のプロパティは、どちらも長すぎるとテーブルの書式設定に問題が発生するおそれがあります。 カスタム HTML ヘルパーを作成して、これらのプロパティやビュー内のその他のプロパティを簡単に切り詰めることができるようにします。
Razor の @helper 構文を使うと、ビューで使う独自のヘルパー関数を簡単に作成できます。 /Views/StoreManager/Index.cshtml ビューを開き、@model の行の直後に次のコードを追加します。
@helper Truncate(string
input, int length)
{
if (input.Length <= length) {
@input
} else {
@input.Substring(0, length)<text>...</text>
}
}
このヘルパー メソッドは、文字列と許容する最大長を受け取ります。 指定したテキストが指定した長さより短い場合、ヘルパーはそのまま出力します。 長い場合は、テキストを切り詰め、残りの部分に "…" をレンダリングします。
これで、Truncate ヘルパーを使って、アルバムのタイトルとアーティスト名のプロパティがどちらも 25 文字未満になることを保証できます。 新しい Truncate ヘルパーを使用した完全なビュー コードを次に示します。
@model IEnumerable<MvcMusicStore.Models.Album>
@helper Truncate(string input, int length)
{
if (input.Length <= length) {
@input
} else {
@input.Substring(0, length)<text>...</text>
}
}
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Create
New", "Create")
</p>
<table>
<tr>
<th>
Genre
</th>
<th>
Artist
</th>
<th>
Title
</th>
<th>
Price
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Genre.Name)
</td>
<td>
@Truncate(item.Artist.Name, 25)
</td>
<td>
@Truncate(item.Title, 25)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.AlbumId }) |
@Html.ActionLink("Details", "Details", new { id=item.AlbumId }) |
@Html.ActionLink("Delete", "Delete", new { id=item.AlbumId })
</td>
</tr>
}
</table>
これで、/StoreManager/ の URL を参照すると、アルバムとタイトルは設定した最大長未満で表示されます。
注: これは、1 つのビューでヘルパーを作成して使用するシンプルなケースを示しています。 サイト全体で使用できるヘルパーを作成する方法について詳しくは、私のブログ記事を参照してください: http://bit.ly/mvc3-helper-options