次の方法で共有


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 Web Developer 2010 ではなく Visual Studio 2010 を使用する場合は、Visual Studio 2010 の前提条件のリンクをクリックして、前提条件をインストールします。

このトピックに関連する、Visual Web Developer プロジェクトと C# ソース コードを使用できます。 C# バージョンをダウンロードします。 Visual Basic を使用する場合は、このチュートリアルの Visual Basic バージョンを参照してください。

このセクションでは、映画コントローラー用に生成されるアクション メソッドとビューを詳しく見ていきます。 その後、カスタム検索ページを追加します。

アプリケーションを実行し、ブラウザーのアドレス バーに入力されている URL に "/Movies" を追加して Movies コントローラーを表示します。 Edit リンクの上にマウス ポインターを置くと、リンク先の URL が表示されます。

EditLink_sm

Edit リンクは、Views\Movies\Index.cshtml ビューで Html.ActionLink メソッドによって生成されたものです。

@Html.ActionLink("Edit", "Edit", new { id=item.ID })

Html.ActionLink

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 が渡されます。

EditQueryString

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 コレクションに保存します。 次に、新しい映画データをデータベースに保存するために MovieDBContextSaveChanges メソッドを呼び出します。変更内容はデータベースに保持されます。 データを保存した後は、ユーザーを MoviesController クラスの Index アクション メソッドにリダイレクトします。これにより、更新された映画が映画の一覧に表示されます。

POST された値が有効でない場合は、それらがフォームに再度表示されます。 Edit.cshtml ビュー テンプレートの Html.ValidationMessageFor ヘルパーは、該当するエラー メッセージの表示を処理します。

abcNotValid

ロケールに関する注意: 通常の作業を英語以外のロケールで行う場合は、「英語以外のロケールでの ASP.NET MVC 3 検証のサポート」を参照してください。

Edit メソッドの堅牢性を高める

スキャフォールディング システムによって生成された HttpGet Edit メソッドは、渡された ID が有効であることを確認しません。 ユーザーによって URL (http://localhost:xxxxx/Movies/Edit) から ID セグメントが削除された場合、次のエラーが表示されます。

Null_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 クエリは、定義されたときや、WhereOrderBy などのメソッドで変更されたときには実行されません。 クエリの実行が先延ばしされるため、式の評価は、実現された値が実際に反復評価されるか ToList メソッドが呼び出される時点まで遅延されます。 SearchIndex サンプルの場合、クエリは SearchIndex ビューで実行されます。 クエリの遅延実行の詳細については、「クエリの実行」を参照してください。

以上で、フォームをユーザーに表示する SearchIndex ビューを実装できるようになりました。 SearchIndex メソッド内を右クリックし、[Add View (ビューの追加)] をクリックします。 [Add View (ビューの追加)] ダイアログ ボックスで、ビュー テンプレートに Movie オブジェクトをモデル クラスとして渡すことを指定します。 [スキャフォールディング テンプレート][リスト] を選択し、[追加] をクリックします。

AddSearchView

[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 に追加します。 フィルターされたムービーが表示されます。

SearchQryStr

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 セグメント) として検索タイトルを渡すことができます。

SearchRouteData

ただし、ユーザーがムービーを検索するたびに 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 ヘルパーにより、フォームがそれ自体に送信されます。

アプリケーションを実行し、映画を検索してみてください。

SearchIndxIE9_title

SearchIndex メソッドの HttpPost オーバーロードはありません。 このメソッドはデータをフィルター処理するだけで、アプリケーションの状態を変更しないため、必要がないからです。

以下の HttpPost SearchIndex メソッドを追加できます。 この場合、アクション呼び出し元は HttpPost SearchIndex メソッドと一致し、HttpPost SearchIndex メソッドが下の図のように実行されます。

[HttpPost]
public string SearchIndex(FormCollection fc, string searchString)
{
    return "<h3> From [HttpPost]SearchIndex: " + searchString + "</h3>";
}

SearchPostGhost

ただし、この 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))

BeginFormPost_SM

ここで検索を送信すると、URL に検索クエリ文字列が含められます。 HttpPost SearchIndex メソッドがある場合でも、検索時には HttpGet SearchIndex アクション メソッドにも移動します。

SearchIndexWithGetURL

ジャンル別検索を追加する

先ほど 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 モデルにプロパティを追加する方法と、テスト データベースを自動的に作成する初期化子を追加する方法について説明します。