Edit メソッドと Edit ビューの確認 (C#)
作成者: Rick Anderson
Note
ASP.NET MVC 5 と Visual Studio 2013 を使用するこのチュートリアルの更新版は、こちらで入手できます。 より安全で、より簡単に操作でき、より多くの機能を備えています。
このチュートリアルでは、Microsoft Visual Web Developer 2010 Express Service Pack 1 (Microsoft Visual Studio の無料版) を使用した ASP.NET MVC Web アプリケーション構築の基本事項を説明します。 開始する前に、以下に示す前提条件がインストールされていることを確認してください。 次のリンクをクリックすると、これらをすべてインストールできます: Web Platform Installer。 また、次のリンクを使用して前提条件となるソフトウェアを個別にインストールすることもできます。
- Visual Studio Web Developer Express SP1 の前提条件
- ASP.NET MVC 3 Tools Update
- SQL Server Compact 4.0 (ランタイム + ツールのサポート)
Visual Web Developer 2010 ではなく Visual Studio 2010 を使用する場合は、Visual Studio 2010 の前提条件のリンクをクリックして、前提条件をインストールします。
このトピックに関連する、Visual Web Developer プロジェクトと C# ソース コードを使用できます。 C# バージョンをダウンロードします。 Visual Basic を使用する場合は、このチュートリアルの Visual Basic バージョンを参照してください。
このセクションでは、映画コントローラー用に生成されるアクション メソッドとビューを詳しく見ていきます。 その後、カスタム検索ページを追加します。
アプリケーションを実行し、ブラウザーのアドレス バーに入力されている URL に "/Movies" を追加して Movies
コントローラーを表示します。 Edit リンクの上にマウス ポインターを置くと、リンク先の URL が表示されます。
Edit リンクは、Views\Movies\Index.cshtml ビューで Html.ActionLink
メソッドによって生成されたものです。
@Html.ActionLink("Edit", "Edit", new { id=item.ID })
Html
オブジェクトは、WebViewPage
基底クラスのプロパティで公開されているヘルパーです。 このヘルパーの ActionLink
メソッドを使用すると、コントローラー上のアクション メソッドにリンクする HTML ハイパーリンクの動的な生成を簡単に行えます。 ActionLink
メソッドの第 1 引数は、リンクとして表示するテキストです (例: <a>Edit Me</a>
)。 第 2 引数は、呼び出すアクション メソッドの名前です。 最後の引数は、ルート データを生成する匿名オブジェクトです (この場合は ID 4)。
前の図では http://localhost:xxxxx/Movies/Edit/4
というリンクが生成されています。 既定ルートの場合、{controller}/{action}/{id}
という URL パターンが使用されます。 したがって、ASP.NET は、http://localhost:xxxxx/Movies/Edit/4
を、Movies
コントローラーの Edit
アクション メソッドへの要求に変換し、パラメーター ID
は 4 になります。
クエリ文字列を使用してアクション メソッドのパラメーターを渡すこともできます。 たとえば、URL http://localhost:xxxxx/Movies/Edit?ID=4
でも Movies
コントローラーの Edit
アクション メソッドに ID
パラメーターとして 4 が渡されます。
Movies
コントローラーを開きます。 2 つの Edit
アクション メソッドを以下に示します。
//
// GET: /Movies/Edit/5
public ActionResult Edit(int id)
{
Movie movie = db.Movies.Find(id);
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
メソッドを使用してムービーを検索し、選択したムービーを編集ビューに返します。 スキャフォールディング システムが編集ビューを作成したときは、そのシステムが Movie
クラスを調べて、クラスの各プロパティの <label>
および <input>
要素をレンダリングするコードを作成しました。 生成された [Edit (編集)] ビューの例を次に示します。
@model MvcMovie.Models.Movie
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@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>
ビュー テンプレートのファイルの冒頭に置かれた @model MvcMovie.Models.Movie
ステートメントは、このビューにおいて、ビュー テンプレートのモデルとして Movie
型が想定されていることを示します。
このスキャフォールディングされたコードでは、HTML マークアップを合理的に行うためにいくつかのヘルパー メソッドを使用しています。 Html.LabelFor
ヘルパーは、フィールドの名前 ("Title"、"ReleaseDate"、"Genre"、または "Price") を表示します。 Html.EditorFor
ヘルパーは HTML <input>
要素を表示します。 Html.ValidationMessageFor
ヘルパーは、指定のプロパティに関連付けられている検証メッセージを表示します。
アプリケーションを実行し、/Movies URL に移動します。 [編集] リンクをクリックします。 ブラウザーで、ページのソースを表示します。 このページの HTML は次の例のようになります (見やすいようにメニューのマークアップを省略しています)。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Edit</title>
<link href="/Content/Site.css" rel="stylesheet" type="text/css" />
<script src="/Scripts/jquery-1.5.1.min.js" type="text/javascript"></script>
<script src="/Scripts/modernizr-1.7.min.js" type="text/javascript"></script>
</head>
<body>
<div class="page">
<header>
<div id="title">
<h1>MVC Movie App</h1>
</div>
...
</header>
<section id="main">
<h2>Edit</h2>
<script src="/Scripts/jquery.validate.min.js" type="text/javascript"></script>
<script src="/Scripts/jquery.validate.unobtrusive.min.js" type="text/javascript"></script>
<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-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="9.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>
<div>
<a href="/Movies">Back to List</a>
</div>
</section>
<footer>
</footer>
</div>
</body>
</html>
<input>
要素は HTML <form>
要素内にあり、このフォームの action
属性は /Movies/Edit URL への post に設定されています。 フォーム データは、[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 フレームワーク モデル バインダーは、POST されたフォーム値を受け取り、movie
パラメーターとして渡された Movie
オブジェクトを作成します。 コード内の ModelState.IsValid
チェックでは、フォームで送信されたデータを使用して Movie
オブジェクトに変更を加えられるかどうかを確認します。 データが有効であれば、映画データを MovieDBContext
インスタンスの Movies
コレクションに保存します。 次に、新しい映画データをデータベースに保存するために MovieDBContext
の SaveChanges
メソッドを呼び出します。変更内容はデータベースに保持されます。 データを保存した後は、ユーザーを MoviesController
クラスの Index
アクション メソッドにリダイレクトします。これにより、更新された映画が映画の一覧に表示されます。
POST された値が有効でない場合は、それらがフォームに再度表示されます。 Edit.cshtml ビュー テンプレートの Html.ValidationMessageFor
ヘルパーは、該当するエラー メッセージの表示を処理します。
ロケールに関する注意: 通常の作業を英語以外のロケールで行う場合は、「英語以外のロケールでの ASP.NET MVC 3 検証のサポート」を参照してください。
Edit メソッドの堅牢性を高める
スキャフォールディング システムによって生成された HttpGet
Edit
メソッドは、渡された ID が有効であることを確認しません。 ユーザーによって URL (http://localhost:xxxxx/Movies/Edit
) から ID セグメントが削除された場合、次のエラーが表示されます。
また、データベースにない ID (例: http://localhost:xxxxx/Movies/Edit/1234
) がユーザーから渡される可能性もあります。 この制限に対処するために、 HttpGet
Edit
アクション メソッドに 2 つの変更を加えることができます。 まず、ID が明示的に渡されない場合の既定値が 0 になるように ID
パラメーターを変更します。 さらに、映画オブジェクトをビュー テンプレートに返す前に、Find
メソッドで実際に映画が見つかるかどうかを確認します。 更新後の Edit
メソッドは以下のようになります。
public ActionResult Edit(int id = 0)
{
Movie movie = db.Movies.Find(id);
if (movie == null)
{
return HttpNotFound();
}
return View(movie);
}
映画が見つからないときは、HttpNotFound
メソッドが呼び出されます。
HttpGet
のメソッドは、すべて同様のパターンに従います。 映画 オブジェクト (Index
の場合はオブジェクトのリスト) を取得し、モデルをビューに渡します。 Create
メソッドは、空の映画オブジェクトを Create ビューに渡します。 データの作成、編集、削除、またはそれ以外の変更を行うすべてのメソッドは、メソッドの HttpPost
のオーバーロードでそれを行います。 HTTP GET メソッドでデータを変更することは、セキュリティ上のリスクです。 また、GET メソッドでのデータ変更は HTTP のベスト プラクティスに反するほか、アーキテクチャ REST パターンの、GET 要求でアプリケーションの状態を変更すべきでないという規定にも違反します。 つまり、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
パラメーターに文字列が含まれている場合は、その検索文字列の値を使ってフィルター処理をするよう、次のコードで movies クエリを変更します。
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
LINQ クエリは、定義されたときや、Where
、OrderBy
などのメソッドで変更されたときには実行されません。 クエリの実行が先延ばしされるため、式の評価は、実現された値が実際に反復評価されるか ToList
メソッドが呼び出される時点まで遅延されます。 SearchIndex
サンプルの場合、クエリは SearchIndex ビューで実行されます。 クエリの遅延実行の詳細については、「クエリの実行」を参照してください。
以上で、フォームをユーザーに表示する SearchIndex
ビューを実装できるようになりました。 SearchIndex
メソッド内を右クリックし、[Add View (ビューの追加)] をクリックします。 [Add View (ビューの追加)] ダイアログ ボックスで、ビュー テンプレートに Movie
オブジェクトをモデル クラスとして渡すことを指定します。 [スキャフォールディング テンプレート] で [リスト] を選択し、[追加] をクリックします。
[Add (追加)] ボタンをクリックすると、Views\Movies\SearchIndex.cshtml ビュー テンプレートが作成されます。 [Scaffold template (スキャフォールディング テンプレート)] の一覧で [List (リスト)] を選択したため、Visual Web Developer によってビュー内に既定のコンテンツが自動生成 (スキャフォールディング) され、 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 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")
<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()){
<p>Genre: @Html.DropDownList("movieGenre", "All")
Title: @Html.TextBox("SearchString")
<input type="submit" value="Filter" /></p>
}
</p>
アプリケーションを実行し、/Movies/SearchIndex を参照します。 ジャンル、映画名、および両方の条件で検索してみてください。
このセクションでは、フレームワークによって生成される CRUD のアクション メソッドとビューについて説明しました。 ユーザーにタイトルとジャンルでの映画検索機能を提供する検索アクション メソッドとビューを作成しました。 次のセクションでは、Movie
モデルにプロパティを追加する方法と、テスト データベースを自動的に作成する初期化子を追加する方法について説明します。