ムービー コントローラーに関するアクション メソッドとビューを調べる
作成者: Rick Anderson
Note
ASP.NET MVC 5 と Visual Studio 2013 を使用するこのチュートリアルの更新版は、こちらで入手できます。 より安全で、より簡単に操作でき、より多くの機能を備えています。
このセクションでは、ムービー コントローラーに関する生成対象のアクション メソッドとビューを調べます。 その後、カスタム検索ページを追加します。
アプリケーションを実行し、ブラウザーのアドレス バーに入力されている URL に "/Movies" を追加して Movies
コントローラーを表示します。 Edit リンクの上にマウス ポインターを置くと、リンク先の URL が表示されます。
Edit リンクは、Views\Movies\Index.cshtml ビューで Html.ActionLink
メソッドによって生成されたものです。
@Html.ActionLink("Edit", "Edit", new { id=item.ID })
Html
オブジェクトは、System.Web.Mvc.WebViewPage 基底クラスのプロパティを使用して公開されるヘルパーです。 このヘルパーの ActionLink
メソッドを使用すると、コントローラー上のアクション メソッドにリンクする HTML ハイパーリンクを簡単かつ動的に生成できるようになります。 ActionLink
メソッドの第 1 引数は、リンクとして表示するテキストです (例: <a>Edit Me</a>
)。 第 2 引数は、呼び出すアクション メソッドの名前です。 最後の引数は、ルート データを生成する匿名オブジェクトです (この場合は ID 4)。
上記のイメージでは、http://localhost:xxxxx/Movies/Edit/4
というリンクが生成されていることが示されています。 既定値のルート (App_Start\RouteConfig.cs で確立される) は、URL パターン {controller}/{action}/{id}
を受け取ります。 したがって、ASP.NET は、http://localhost:xxxxx/Movies/Edit/4
を、Movies
コントローラーの Edit
アクション メソッドへの要求に変換し、パラメーター ID
は 4 になります。 App_Start\RouteConfig.cs ファイル内の以下のコードを調べます。
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index",
id = UrlParameter.Optional }
);
}
クエリ文字列を使用してアクション メソッドのパラメーターを渡すこともできます。 たとえば、URL http://localhost:xxxxx/Movies/Edit?ID=4
でも Movies
コントローラーの Edit
アクション メソッドに ID
パラメーターとして 4 が渡されます。
Movies
コントローラーを開きます。 2 つの Edit
アクション メソッドを以下に示します。
//
// GET: /Movies/Edit/5
public ActionResult Edit(int id = 0)
{
Movie movie = db.Movies.Find(id);
if (movie == null)
{
return HttpNotFound();
}
return View(movie);
}
//
// POST: /Movies/Edit/5
[HttpPost]
public ActionResult Edit(Movie movie)
{
if (ModelState.IsValid)
{
db.Entry(movie).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(movie);
}
2 番目の Edit
アクション メソッドの前に HttpPost
属性が付いていることに注意してください。 この属性は、Edit
メソッドのオーバーロードは POST 要求の場合にのみ呼び出し可能であることを指定するものです。 第 1 の Edit メソッドには HttpGet
属性を適用できますが、そちらは既定で適用済みの扱いになるため必須ではありません (HttpGet
属性が暗黙的に割り当てられているアクション メソッドを HttpGet
メソッドと呼びます)。
HttpGet
Edit
メソッドは movie ID パラメーターを受け取り、Entity Framework Find
メソッドを使用してムービーを検索し、選択したムービーを編集ビューに返します。 Edit
メソッドがパラメーターなしで呼び出される場合、ID パラメーターには 既定値の 0 が指定されます。 ムービーが見つからない場合は、HttpNotFound が返されます。 スキャフォールディング システムが編集ビューを作成したときは、そのシステムが Movie
クラスを調べて、クラスの各プロパティの <label>
および <input>
要素をレンダリングするコードを作成しました。 生成された [Edit (編集)] ビューの例を次に示します。
@model MvcMovie.Models.Movie
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Movie</legend>
@Html.HiddenFor(model => model.ID)
<div class="editor-label">
@Html.LabelFor(model => model.Title)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Title)
@Html.ValidationMessageFor(model => model.Title)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.ReleaseDate)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.ReleaseDate)
@Html.ValidationMessageFor(model => model.ReleaseDate)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Genre)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Genre)
@Html.ValidationMessageFor(model => model.Genre)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Price)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Price)
@Html.ValidationMessageFor(model => model.Price)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
ビュー テンプレートのファイルの冒頭に置かれた @model MvcMovie.Models.Movie
ステートメントは、このビューにおいて、ビュー テンプレートのモデルとして Movie
型が想定されていることを示します。
このスキャフォールディングされたコードでは、HTML マークアップを合理的に行うためにいくつかのヘルパー メソッドを使用しています。 Html.LabelFor
ヘルパーは、フィールドの名前を表示します ("Title"、"ReleaseDate"、"Genre"、"Price")。 Html.EditorFor
ヘルパーは、HTML の <input>
要素をレンダリングします。 Html.ValidationMessageFor
ヘルパーは、そのプロパティに関連付けられている検証メッセージを表示します。
アプリケーションを実行し、/Movies URL に移動します。 [編集] リンクをクリックします。 ブラウザーで、ページのソースを表示します。 form 要素に関する HTML を以下に示します。
<form action="/Movies/Edit/4" method="post"> <fieldset>
<legend>Movie</legend>
<input data-val="true" data-val-number="The field ID must be a number." data-val-required="The ID field is required." id="ID" name="ID" type="hidden" value="4" />
<div class="editor-label">
<label for="Title">Title</label>
</div>
<div class="editor-field">
<input class="text-box single-line" id="Title" name="Title" type="text" value="Rio Bravo" />
<span class="field-validation-valid" data-valmsg-for="Title" data-valmsg-replace="true"></span>
</div>
<div class="editor-label">
<label for="ReleaseDate">ReleaseDate</label>
</div>
<div class="editor-field">
<input class="text-box single-line" data-val="true" data-val-date="The field ReleaseDate must be a date." data-val-required="The ReleaseDate field is required." id="ReleaseDate" name="ReleaseDate" type="text" value="4/15/1959 12:00:00 AM" />
<span class="field-validation-valid" data-valmsg-for="ReleaseDate" data-valmsg-replace="true"></span>
</div>
<div class="editor-label">
<label for="Genre">Genre</label>
</div>
<div class="editor-field">
<input class="text-box single-line" id="Genre" name="Genre" type="text" value="Western" />
<span class="field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
</div>
<div class="editor-label">
<label for="Price">Price</label>
</div>
<div class="editor-field">
<input class="text-box single-line" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" type="text" value="2.99" />
<span class="field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
</form>
<input>
要素は、/Movies/Edit URL に送信するように action
属性が設定された HTML の <form>
要素に含まれます。 フォーム データは、[Edit (編集)] ボタンがクリックされるとサーバーに POST 送信されます。
POST 要求の処理
次のリストでは、Edit
アクション メソッドの HttpPost
バージョンを示します。
[HttpPost]
public ActionResult Edit(Movie movie)
{
if (ModelState.IsValid)
{
db.Entry(movie).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(movie);
}
ASP.NET MVC モデル バインダーは、送信されたフォーム値を取得し、movie
パラメーターとして渡される Movie
オブジェクトを作成します。 ModelState.IsValid
メソッドは、フォームで送信されたデータを使って Movie
オブジェクトを変更 (編集または更新) できることを検証します。 データが有効な場合、ムービー データは db(MovieDBContext
インスタンスの Movies
コレクションに保存されます。 新しいムービー データは、MovieDBContext
の SaveChanges
メソッドを呼び出すことによってデータベースに保存されます。 データを保存した後、コードはユーザーを MoviesController
クラスの Index
アクション メソッドにリダイレクトします。そこでは、行われたばかりの変更を含むムービー コレクションが表示されます。
送信された値が有効でない場合は、フォームに再表示されます。 Edit.cshtml ビュー テンプレートの Html.ValidationMessageFor
ヘルパーは、該当するエラー メッセージの表示を処理します。
Note
小数点にコンマ (「,」) を使用する英語以外のロケールで jQuery 検証をサポートするには、(https://github.com/jquery/globalize からの) globalize.js と特定の cultures/globalize.cultures.js ファイル、および Globalize.parseFloat
を使用する JavaScript を含める必要があります。 以下のコードは、「fr-FR」カルチャに対応するように Views\Movies\Edit.cshtml ファイルに加えた変更を示しています。
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
<script src="~/Scripts/globalize.js"></script>
<script src="~/Scripts/globalize.culture.fr-FR.js"></script>
<script>
$.validator.methods.number = function (value, element) {
return this.optional(element) ||
!isNaN(Globalize.parseFloat(value));
}
$(document).ready(function () {
Globalize.culture('fr-FR');
});
</script>
<script>
jQuery.extend(jQuery.validator.methods, {
range: function (value, element, param) {
//Use the Globalization plugin to parse the value
var val = $.global.parseFloat(value);
return this.optional(element) || (
val >= param[0] && val <= param[1]);
}
});
</script>
}
10 進フィールドに小数点ではなくコンマが必要な場合があります。 一時的な修正として、globalization 要素をプロジェクトのルートの web.config ファイルに追加できます。 以下のコードは、カルチャが米国英語に設定された globalization 要素を示しています。
<system.web>
<globalization culture ="en-US" />
<!--elements removed for clarity-->
</system.web>
すべての HttpGet
メソッドは同様のパターンに従います。 映画 オブジェクト (Index
の場合はオブジェクトのリスト) を取得し、モデルをビューに渡します。 Create
メソッドは、空の映画オブジェクトを Create ビューに渡します。 データの作成、編集、削除、またはそれ以外の変更を行うすべてのメソッドは、メソッドの HttpPost
のオーバーロードでそれを行います。 ブログ記事エントリ「ASP.NET MVC Tip #46 – Don't use Delete Links because they create Security Holes」で説明されているように、HTTP GET メソッドでデータを変更することはセキュリティ上のリスクになります。 GET メソッドでデータを変更することは、HTTP のベスト プラクティスや、GET 要求ではアプリケーションの状態を変更してはならないというアーキテクチャの REST パターンにも違反しています。 つまり、GET 操作の実行は、副作用がなく、永続化されたデータを変更しない、安全な操作である必要があります。
検索メソッドと検索ビューの追加
このセクションでは、映画をジャンルや名前で検索できる SearchIndex
アクション メソッドを追加します。 そのためには、/Movies/SearchIndex URL を使用します。 この要求により、ムービーを検索するためにユーザーが入力できる入力要素が含まれる HTML フォームが表示されます。 ユーザーがこのフォームを送信すると、ユーザーが送信した検索値がこのアクション メソッドにより取得され、その値を使用してデータベースが検索されます。
SearchIndex フォームを表示する
まず、既存の MoviesController
クラスに SearchIndex
アクション メソッドを追加します。 このメソッドは、HTML フォームを含んだビューを返します。 のコードを次に示します。
public ActionResult SearchIndex(string searchString)
{
var movies = from m in db.Movies
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(movies);
}
SearchIndex
メソッド内の 1 行目では、次のように、映画を選択するための LINQ クエリを作成しています。
var movies = from m in db.Movies
select m;
この時点でクエリが定義されますが、まだデータ ストアに対して実行はされません。
searchString
パラメーターに文字列が含まれる場合、以下のコードを使用して、検索文字列の値でフィルターするようにムービー クエリが変更されます。
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
上の s => s.Title
コードはラムダ式です。 ラムダは、メソッド ベースの LINQ クエリで、上のコードで使用されている Where メソッドなど、標準クエリ演算子メソッドの引数として使用されます。 LINQ クエリは、Where
や OrderBy
などのメソッドの呼び出しで定義または変更されたときには実行されません。 クエリの実行が先延ばしされるため、式の評価は、実現された値が実際に反復評価されるか ToList
メソッドが呼び出される時点まで遅延されます。 SearchIndex
サンプルの場合、クエリは SearchIndex ビューで実行されます。 クエリの遅延実行の詳細については、「クエリの実行」を参照してください。
以上で、フォームをユーザーに表示する SearchIndex
ビューを実装できるようになりました。 SearchIndex
メソッド内を右クリックし、[Add View (ビューの追加)] をクリックします。 [Add View (ビューの追加)] ダイアログ ボックスで、ビュー テンプレートに Movie
オブジェクトをモデル クラスとして渡すことを指定します。 [スキャフォールディング テンプレート] で [リスト] を選択し、[追加] をクリックします。
[追加] ボタンをクリックすると、Views\Movies\SearchIndex.cshtml ビュー テンプレートが作成されます。 [スキャフォールディング テンプレート] リストで [リスト] を選択したので、Visual Studio によりビューに既定値のマークアップが自動的に生成 (スキャフォールディング) されました。 このスキャフォールディングにより HTML フォームが作成されました。 これは、スキャフォールディングによって Movie
クラスが分析され、クラス プロパティごとに <label>
要素をレンダリングするコードが作成されたためです。 生成された Create ビューの C# HTML コードを次に示します。
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewBag.Title = "SearchIndex";
}
<h2>SearchIndex</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
<th>
Title
</th>
<th>
ReleaseDate
</th>
<th>
Genre
</th>
<th>
Price
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<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.Price)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.ID }) |
@Html.ActionLink("Details", "Details", new { id=item.ID }) |
@Html.ActionLink("Delete", "Delete", new { id=item.ID })
</td>
</tr>
}
</table>
アプリケーションを実行し、/Movies/SearchIndex に移動します。 ?searchString=ghost
などのクエリ文字列を URL に追加します。 フィルターされたムービーが表示されます。
id
という名前のパラメーターを使用するために SearchIndex
メソッドのシグネチャを変更すると、id
パラメーターは、Global.asax ファイルで設定されている既定ルートの {id}
プレースホルダーと一致するようになります。
{controller}/{action}/{id}
元の SearchIndex
メソッドは以下のとおりです。
public ActionResult SearchIndex(string searchString)
{
var movies = from m in db.Movies
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(movies);
}
変更後の SearchIndex
メソッドは次のようになります。
public ActionResult SearchIndex(string id)
{
string searchString = id;
var movies = from m in db.Movies
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(movies);
}
これで、クエリ文字列の値ではなく、ルート データ (URL セグメント) として検索タイトルを渡すことができます。
ただし、ユーザーがムービーを検索するたびに URL の変更を求めることはできません。 そのため、映画をフィルター処理する UI を追加することにします。 ルート バインドされた ID パラメーターを渡す方法をテストするために SearchIndex
メソッドのシグネチャを変更した場合は、SearchIndex
メソッドが searchString
という名前の文字列パラメーターを受け取るようにシグネチャを元に戻します。
public ActionResult SearchIndex(string searchString)
{
var movies = from m in db.Movies
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(movies);
}
Views\Movies\SearchIndex.cshtml ファイルをオープンし、@Html.ActionLink("Create New", "Create")
の直後に以下を追加します。
@using (Html.BeginForm()){
<p> Title: @Html.TextBox("SearchString")<br />
<input type="submit" value="Filter" /></p>
}
以下の例は、フィルター処理マークアップが追加された Views\Movies\SearchIndex.cshtml ファイルの一部を示しています。
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewBag.Title = "SearchIndex";
}
<h2>SearchIndex</h2>
<p>
@Html.ActionLink("Create New", "Create")
@using (Html.BeginForm()){
<p> Title: @Html.TextBox("SearchString") <br />
<input type="submit" value="Filter" /></p>
}
</p>
Html.BeginForm
ヘルパーは、開始 <form>
タグを作成します。 [Filter (フィルター)] ボタンのクリック操作でフォームが送信されると、Html.BeginForm
ヘルパーにより、フォームがそれ自体に送信されます。
アプリケーションを実行し、映画を検索してみてください。
SearchIndex
メソッドの HttpPost
オーバーロードはありません。 このメソッドはデータをフィルター処理するだけで、アプリケーションの状態を変更しないため、必要がないからです。
以下の HttpPost SearchIndex
メソッドを追加できます。 この場合、アクション呼び出し元は HttpPost SearchIndex
メソッドと一致し、HttpPost SearchIndex
メソッドが下の図のように実行されます。
[HttpPost]
public string SearchIndex(FormCollection fc, string searchString)
{
return "<h3> From [HttpPost]SearchIndex: " + searchString + "</h3>";
}
ただし、この HttpPost
バージョンの SearchIndex
メソッドを追加しても、実装方法は制限されます。 たとえば、特定の検索をブックマークするか、友だちにリンクを送信し、友だちがそれをクリックしてムービーのフィルターされた同じリストを表示できるようにするとします。 HTTP POST 要求の URL は、GET 要求の URL (localhost:xxxxx/Movies/SearchIndex) と同じであり、URL 自体には検索情報がないことに注意してください。 この時点で、検索文字列情報はフォーム フィールド値としてサーバーに送信されます。 つまり、その検索情報をキャプチャしてブックマーク登録したり、URL で友人に送信したりすることはできません。
解決するには、BeginForm
のオーバーロードを使用して、POST 要求で検索情報を URL に追加して SearchIndex
メソッドの HttpGet バージョンにルーティングする必要があることを指定します。 既存のパラメーターなしの BeginForm
メソッドを以下に置き換えます。
@using (Html.BeginForm("SearchIndex","Movies",FormMethod.Get))
ここで検索を送信すると、URL に検索クエリ文字列が含められます。 HttpPost SearchIndex
メソッドがある場合でも、検索時には HttpGet SearchIndex
アクション メソッドにも移動します。
ジャンル別検索を追加する
先ほど SearchIndex
メソッドの HttpPost
バージョンを追加した場合は、ここで削除してください。
次は、映画のジャンル別検索をユーザーに提供する機能を追加します。 SearchIndex
メソッドを次のコードで置き換えます。
public ActionResult SearchIndex(string movieGenre, string searchString)
{
var GenreLst = new List<string>();
var GenreQry = from d in db.Movies
orderby d.Genre
select d.Genre;
GenreLst.AddRange(GenreQry.Distinct());
ViewBag.movieGenre = new SelectList(GenreLst);
var movies = from m in db.Movies
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
if (string.IsNullOrEmpty(movieGenre))
return View(movies);
else
{
return View(movies.Where(x => x.Genre == movieGenre));
}
}
SearchIndex
メソッドのこのバージョンは、movieGenre
という名前の追加のパラメーターを受け取ります。 このコードの先頭部分では、データベースから映画のジャンル情報を取得して保持するための List
オブジェクトを作成しています。
次のコードは、データベースからすべてのジャンルを取得する LINQ クエリです。
var GenreQry = from d in db.Movies
orderby d.Genre
select d.Genre;
このコードでは、ジェネリックな List
コレクションの AddRange
メソッドを使用して、異なる映画ジャンル名をすべてリストに追加します (もし Distinct
修飾子を指定しないと、同じジャンルが複数回出現することになります。たとえば、このサンプルではコメディが 2 つになります)。 その後、ジャンルのリストを ViewBag
オブジェクトに格納します。
以下のコードは、movieGenre
パラメーターを検査する方法を示しています。 このコードは、このパラメーターが空でない場合にはムービー クエリの制約を厳しくし、指定したジャンルのムービーだけが選択されるように制限されるようにします。
if (string.IsNullOrEmpty(movieGenre))
return View(movies);
else
{
return View(movies.Where(x => x.Genre == movieGenre));
}
SearchIndex ビューにマークアップを追加してジャンル別の検索をサポートする
Views\Movies\SearchIndex.cshtml ファイル内の TextBox
ヘルパーの直前に Html.DropDownList
ヘルパーを追加します。 完成したマークアップを以下に示します。
<p>
@Html.ActionLink("Create New", "Create")
@using (Html.BeginForm("SearchIndex","Movies",FormMethod.Get)){
<p>Genre: @Html.DropDownList("movieGenre", "All")
Title: @Html.TextBox("SearchString")
<input type="submit" value="Filter" /></p>
}
</p>
アプリケーションを実行し、/Movies/SearchIndex を参照します。 ジャンル、映画名、および両方の条件で検索してみてください。
このセクションでは、フレームワークによって生成される CRUD のアクション メソッドとビューについて説明しました。 ユーザーにタイトルとジャンルでの映画検索機能を提供する検索アクション メソッドとビューを作成しました。 次のセクションでは、Movie
モデルにプロパティを追加する方法と、テスト データベースを自動的に作成する初期化子を追加する方法について説明します。