ムービー コントローラーに関するアクション メソッドとビューを調べる

作成者: Rick Anderson

Note

ASP.NET MVC 5 と Visual Studio 2013 を使用するこのチュートリアルの更新版は、こちらで入手できます。 より安全で、より簡単に操作でき、より多くの機能を備えています。

このセクションでは、ムービー コントローラーに関する生成対象のアクション メソッドとビューを調べます。 その後、カスタム検索ページを追加します。

アプリケーションを実行し、ブラウザーのアドレス バーに入力されている 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 オブジェクトは、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 が渡されます。

EditQueryString

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 コレクションに保存されます。 新しいムービー データは、MovieDBContextSaveChanges メソッドを呼び出すことによってデータベースに保存されます。 データを保存した後、コードはユーザーを MoviesController クラスの Index アクション メソッドにリダイレクトします。そこでは、行われたばかりの変更を含むムービー コレクションが表示されます。

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

abcNotValid

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

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

AddSearchView

[追加] ボタンをクリックすると、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 に追加します。 フィルターされたムービーが表示されます。

SearchQryStr

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

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")<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>"; 
}

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