次の方法で共有


CRUD (作成、読み取り、更新、削除) データ フォーム エントリ サポートを提供する

提供元: Microsoft

PDF のダウンロード

これは、ASP.NET MVC 1 を使用して小規模ながら完全な Web アプリケーションをビルドする方法を説明する無料の "NerdDinner" アプリケーション チュートリアルの手順 5 です。

手順 5 では、DinnersController クラスをさらに強化し、Dinner の編集、作成、削除のサポートを可能にします。

ASP.NET MVC 3 を使用している場合は、MVC 3 の概要または MVC Music Store に関するチュートリアルに従うことをお勧めします。

NerdDinner 手順 5: フォームの作成、更新、削除シナリオ

コントローラーとビューを紹介し、それらを使用して、実際の Dinner の一覧の List/Details エクスペリエンスを実装する方法について説明しました。 次の手順では、DinnersController クラスをさらに強化し、Dinner の編集、作成、削除のサポートを可能にします。

DinnersController によって処理される URL

これまでに、次の 2 つの URL のサポートを実装したアクション メソッドを DinnersController に追加しました: /Dinners/Dinners/Details/[id]

URL 動詞 目的
/Dinners/ GET 今後のディナーの HTML リストを表示します。
/Dinners/Details/[id] GET 特定のディナーに関する詳細を表示します。

ここで、アクション メソッドを追加して、次の 3 つの URL をさらに実装します: /Dinners/Edit/[id]/Dinners/Create/Dinners/Delete/[id]。 これらの URL を使用すると、既存の Dinner の編集、新しい Dinner の作成、Dinner の削除がサポートされます。

これらの新しい URL では、HTTP GET 動詞と HTTP POST 動詞の両方の操作をサポートします。 これらの URL に対する HTTP GET 要求では、データの最初の HTML ビュー が表示されます ("Edit" の場合は Dinner データが入力されたフォーム、"Create" の場合は空白のフォーム、"Delete" の場合は削除確認画面)。 これらの URL に対する HTTP POST 要求は、DinnerRepository の Dinner データを保存、更新、削除します (そこからデータベースに送信されます)。

URL 動詞 目的
/Dinners/Edit/[id] GET Dinner データが設定された編集可能な HTML フォームを表示します。
投稿 特定の Dinner のフォーム変更をデータベースに保存します。
/Dinners/Create GET ユーザーが新しい Dinner を定義できる空の HTML フォームを表示します。
投稿 新しい Dinner を作成し、データベースに保存します。
/Dinners/Delete/[id] GET 削除確認画面を表示します。
投稿 指定した Dinner をデータベースから削除します。

編集のサポート

まず、"編集" シナリオを実装しましょう。

HTTP-GET の Edit アクション メソッド

まず、Edit アクション メソッドの HTTP "GET" 動作を実装します。 このメソッドは、/Dinners/Edit/[id] URL が要求されたときに呼び出されます。 実装は次のようになります。

//
// GET: /Dinners/Edit/2

public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);
    
    return View(dinner);
}

上記のコードでは、DinnerRepository を使用して Dinner オブジェクトを取得します。 次に、Dinner オブジェクトを使用して View テンプレートをレンダリングします。 テンプレート名を View() ヘルパー メソッドに明示的に渡していないため、規則に基づく既定のパスを使用してビュー テンプレート /Views/Dinners/Edit.aspx を解決します。

次に、このビュー テンプレートを作成してみましょう。 これを行うには、Edit メソッド内で右クリックし、[ビューの追加] コンテキスト メニュー コマンドを選びます。

Screenshot of creating a view template to add view in Visual Studio.

[ビューの追加] ダイアログで、ビュー テンプレートに Dinner オブジェクトをモデルとして渡すことを示し、"Edit" テンプレートを自動スキャフォールディングすることを選択します。

Screenshot of Add view to auto-scaffold an Edit template.

[追加] ボタンをクリックすると、Visual Studio によって "\Views\Dinners" ディレクトリ内に新しい "Edit.aspx" ビュー テンプレート ファイルが追加されます。 また、コード エディター内で新しい "Edit.aspx" ビュー テンプレートが開き、次のような最初の "Edit" スキャフォールディング実装が設定されます。

Screenshot of new Edit view template within the code-editor.

生成された既定の "Edit" スキャフォールディングにいくつかの変更を加え、編集ビュー テンプレートを更新して、以下の内容を取得してみましょう (これにより、公開したくないプロパティの一部が削除されます)。

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Edit: <%=Html.Encode(Model.Title)%>
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Edit Dinner</h2>

    <%=Html.ValidationSummary("Please correct the errors and try again.") %>  
    
    <% using (Html.BeginForm()) { %>

        <fieldset>
            <p>
                <label for="Title">Dinner Title:</label>
                <%=Html.TextBox("Title") %>
                <%=Html.ValidationMessage("Title", "*") %>
            </p>
            <p>
                <label for="EventDate">EventDate:</label>
                <%=Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate))%>
                <%=Html.ValidationMessage("EventDate", "*") %>
            </p>
            <p>
                <label for="Description">Description:</label>
                <%=Html.TextArea("Description") %>
                <%=Html.ValidationMessage("Description", "*")%>
            </p>
            <p>
                <label for="Address">Address:</label>
                <%=Html.TextBox("Address") %>
                <%=Html.ValidationMessage("Address", "*") %>
            </p>
            <p>
                <label for="Country">Country:</label>
                <%=Html.TextBox("Country") %>               
                <%=Html.ValidationMessage("Country", "*") %>
            </p>
            <p>
                <label for="ContactPhone">ContactPhone #:</label>
                <%=Html.TextBox("ContactPhone") %>
                <%=Html.ValidationMessage("ContactPhone", "*") %>
            </p>
            <p>
                <label for="Latitude">Latitude:</label>
                <%=Html.TextBox("Latitude") %>
                <%=Html.ValidationMessage("Latitude", "*") %>
            </p>
            <p>
                <label for="Longitude">Longitude:</label>
                <%=Html.TextBox("Longitude") %>
                <%=Html.ValidationMessage("Longitude", "*") %>
            </p>
            <p>
                <input type="submit" value="Save"/>
            </p>
        </fieldset>
        
    <% } %>
    
</asp:Content>

アプリケーションを実行し、"/Dinners/Edit/1" URL を要求すると、次のページが表示されます。

Screenshot of My M V C Application page.

ビューによって生成される HTML マークアップは次のようになります。 これは標準の HTML で、[保存] <input type="submit"/> ボタンが押されたときに、/Dinners/Edit/1 URL に対して HTTP POST を実行する <form> 要素が含まれます。 編集可能な各プロパティに対して HTML <input type="text"/> 要素が出力されました。

Screenshot of the generated H T M L markup.

Html.BeginForm() および Html.TextBox() Html ヘルパー メソッド

"Edit.aspx" ビュー テンプレートでは、次のような "Html ヘルパー" メソッドを使用しています: Html.ValidationSummary()、Html.BeginForm()、Html.TextBox()、Html.ValidationMessage()。 これらのヘルパー メソッドは、HTML マークアップを生成するだけでなく、エラー処理と検証のサポートが組み込まれています。

Html.BeginForm() ヘルパー メソッド

Html.BeginForm() ヘルパー メソッドは、マークアップで HTML の <form> 要素を出力するメソッドです。 Edit.aspx ビュー テンプレートで、このメソッドを使用するときに C# の "using" ステートメントを適用していることがわかります。 左中かっこは <form> コンテンツの先頭を示し、右中かっこは </form> 要素の末尾を示します。

<% using (Html.BeginForm()) { %>

   <fieldset>
   
      <!-- Fields Omitted for Brevity -->
   
      <p>
         <input type="submit" value="Save"/>
      </p>
   </fieldset>
   
<% } %>

このようなシナリオで "using" ステートメントを使用するアプローチが不自然だと思われる場合は、代わりに Html.BeginForm() と Html.EndForm() を組み合わせて使用できます (同じ処理が行われます)。

<% Html.BeginForm();  %>

   <fieldset>
   
      <!-- Fields Omitted for Brevity -->
   
      <p>
          <input type="submit" value="Save"/>
      </p>
   </fieldset>
   
<% Html.EndForm(); %>

パラメーターを指定せずに Html.BeginForm() を呼び出すと、現在の要求の URL に HTTP-POST を実行する form 要素が出力されます。 編集ビューで <form action="/Dinners/Edit/1" method="post"> 要素が生成されるのはそのためです。 別の URL にポストする場合は、Html.BeginForm() に明示的なパラメーターを渡すこともできます。

Html.TextBox() ヘルパー メソッド

Edit.aspx ビューでは、Html.TextBox() ヘルパー メソッドを使用して、<input type="text"/> 要素を出力します。

<%= Html.TextBox("Title") %>

上記の Html.TextBox() メソッドは 1 つのパラメーターを受け取ります。これは、出力する <input type="text"/> 要素の id/name 属性と、テキストボックスの値を設定するモデル プロパティの両方を指定するために使用されます。 たとえば、編集ビューに渡した Dinner オブジェクトの "Title" プロパティ値は ".NET Futures" であるため、Html.TextBox("Title") メソッドは次の出力を呼び出します: <input id="Title" name="Title" type="text" value=".NET Futures" />

または、最初の Html.TextBox() パラメーターを使用して要素の id/name を指定し、2 番目のパラメーターとして使用する値を明示的に渡すことができます。

<%= Html.TextBox("Title", Model.Title)%>

多くの場合、出力される値に対してカスタム書式設定を実行する必要があります。 このようなシナリオでは、.NET に組み込まれている String.Format() 静的メソッドが役立ちます。 Edit.aspx ビュー テンプレートでは、これを使用して EventDate 値 (DateTime 型) の書式設定を行い、時間の秒数が表示されないようにしています。

<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>

必要に応じて Html.TextBox() の 3 番目のパラメーターを使用して、追加の HTML 属性を出力できます。 次のコードスニペットは、<input type="text"/> 要素に追加の size="30" 属性と class="mycssclass" 属性をレンダリングする方法を示しています。 ここで、"@" 文字を使用して class 属性の名前をエスケープしていることにご注意ください。"class" は C# の予約キーワードであるためです。

<%= Html.TextBox("Title", Model.Title, new { size=30, @class="myclass" } )%>

HTTP-POST の Edit アクション メソッドを実装する

これで、Edit アクション メソッドの HTTP-GET バージョンが実装されました。 ユーザーが /Dinners/Edit/1 URL を要求すると、次のような HTML ページが表示されます。

Screenshot of H T M L output when user requests an Edit Dinner.

[保存] ボタンを押すと、/Dinners/Edit/1 URL へのフォーム ポストが行われ、HTTP POST 動詞を使用して HTML の <input> フォーム値が送信されます。 次に、Edit アクション メソッドの HTTP POST 動作を実装しましょう。これにより、Dinner の保存が処理されます。

まず、HTTP POST シナリオを処理することを示す "AcceptVerbs" 属性を持つ、オーバーロードされた "Edit" アクション メソッドを DinnersController に追加します。

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {
   ...
}

[AcceptVerbs] 属性がオーバーロードされたアクション メソッドに適用されると、ASP.NET MVC は、受信 HTTP 動詞に応じて、適切なアクション メソッドへのディスパッチ要求を自動的に処理します。 /Dinners/Edit/[id] URL への HTTP POST 要求は上記の Edit メソッドに送信されますが、/Dinners/Edit/[id] URL への他のすべての HTTP 動詞要求は、実装した最初の Edit メソッド ([AcceptVerbs] 属性を持たない) に送信されます。

サイド トピック: HTTP 動詞を使用して区別する理由
ここで、疑問に思われるかもしれません。"1 つの URL を使用しているのに、HTTP 動詞を使って動作を区別するのはどうして?" "編集の変更の読み込みと保存を処理する 2 つの個別の URL を使用しないのはどうして?" "たとえば、最初のフォームを表示する /Dinners/Edit/[id] と、フォームのポストを処理して保存する /Dinners/Save/[id] を使うのはどうか ?" といった具合です。 2 つの個別の URL を発行することには、欠点があります。/Dinners/Save/2 にポストした後、入力エラーのために HTML フォームを再表示する必要がある場合、エンドユーザーに対してブラウザーのアドレス バーに /Dinners/Save/2 URL が表示さることです (これがフォームのポスト先 URL だからです)。 エンドユーザーがこの再表示されたページをブラウザーのお気に入りリストにブックマークしたり、URL をコピー/貼り付けして友人にメールで送信したりする場合、機能しない URL が保存されることになります (その URL はポスト値に依存するためです)。 単一の URL (/Dinners/Edit/[id] など) を公開し、HTTP 動詞で処理を区別することで、エンドユーザーが編集ページをブックマークしたり、他のユーザーに URL を送信したりできます。

フォームのポスト値を取得する

HTTP POST の "Edit" メソッド内でポストされたフォーム パラメーターにアクセスするには、さまざまな方法があります。 1 つの簡単な方法は、Controller 基底クラスの Request プロパティを使用してフォーム コレクションにアクセスし、ポストされた値を直接取得することです。

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    // Retrieve existing dinner
    Dinner dinner = dinnerRepository.GetDinner(id);

    // Update dinner with form posted values
    dinner.Title = Request.Form["Title"];
    dinner.Description = Request.Form["Description"];
    dinner.EventDate = DateTime.Parse(Request.Form["EventDate"]);
    dinner.Address = Request.Form["Address"];
    dinner.Country = Request.Form["Country"];
    dinner.ContactPhone = Request.Form["ContactPhone"];

    // Persist changes back to database
    dinnerRepository.Save();

    // Perform HTTP redirect to details page for the saved Dinner
    return RedirectToAction("Details", new { id = dinner.DinnerID });
}

ただし、上記のアプローチは少し冗長であり、エラー処理ロジックを追加する場合は特にそうです。

このシナリオでのより良い方法は、Controller 基底クラスに組み込みの UpdateModel() ヘルパー メソッドを利用することです。 これは、受信フォーム パラメーターを使用して渡すオブジェクトのプロパティの更新をサポートします。 リフレクションを使用してオブジェクトのプロパティ名を判断し、クライアントによって送信された入力値に基づいて値を自動的に変換し、それらに割り当てます。

UpdateModel() メソッドで HTTP-POST の Edit アクションを簡略化するために、次のコードを使用できます。

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    UpdateModel(dinner);

    dinnerRepository.Save();

    return RedirectToAction("Details", new { id = dinner.DinnerID });
}

これで、/Dinners/Edit/1 URL にアクセスし、Dinner のタイトルを変更できます。

Screenshot of the Edit Dinner page.

[保存] ボタンをクリックすると、Edit アクションへのフォーム ポストが実行され、更新された値がデータベースに保持されます。 すると、Dinner の詳細 URL にリダイレクトされます (新しく保存された値が表示されます)。

Screenshot of the details URL for the Dinner.

編集エラーを処理する

現在の HTTP-POST の実装は正常に動作しますが、エラーが発生することもあります。

ユーザーがフォームの編集で間違いをした場合、フォームを修正するように案内する詳しいエラー メッセージが表示されるようにする必要があります。 これには、エンドユーザーが誤った入力をポストした場合 (日付文字列の形式が正しくない場合など) や、入力形式は有効だがビジネス規則違反がある場合が含まれます。 エラーが発生した場合、フォームはユーザーが最初に入力した入力データを保持し、変更を手動で再入力しなくて済むようにする必要があります。 このプロセスは、フォームが正常に入力されるまで、必要な回数を繰り返す必要があります。

ASP.NET MVC には、エラー処理とフォームの再表示を簡単にする優れた組み込み機能がいくつか含まれています。 これらの機能の動作を確認するために、次のコードで Edit アクション メソッドを更新しましょう。

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {

        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {

        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }
}

上記のコードは、以前の実装と似ていますが、今回は try/catch エラー処理ブロックをラップしています。 UpdateModel() の呼び出しまたは DinnerRepository の保存を行うときに例外が発生した場合、catch エラー処理ブロックが実行されます (保存しようとしている Dinner オブジェクトがモデル内の規則違反のために無効な場合は、例外が発生します)。 その中で、Dinner オブジェクトに存在するすべての規則違反をループ処理し、ModelState オブジェクトに追加します (これについては、後で説明します)。 次に、ビューを再表示します。

動作が正常に実行することを確認するために、アプリケーションを再実行し、Dinner を編集します。Title を空にし、EventDate を "BOGUS" にします。また、国/地域の値を "米国" にし、英国の電話番号を使用します。 [保存] ボタンを押すと、HTTP POST の Edit メソッドは (エラーがあるため) Dinner を保存できず、フォームが再表示されます。

Screenshot of the form redisplay due to errors using the H T T P S P O S T Edit method.

このアプリケーションは、十分なエラー エクスペリエンスを提供しています。 無効な入力を含むテキスト要素は赤で強調表示され、検証エラー メッセージがエンド ユーザーに表示されます。 また、ユーザーが最初に入力した入力データがフォームに保持されているため、再入力の必要がありません。

なぜこれが起こったのかと、疑問に思うかもしれません。 Title、EventDate、ContactPhone テキストボックスが赤で強調表示され、最初に入力されたユーザー値が出力されたのはなぜでしょうか? また、上部のリストにエラー メッセージが表示されたのはどうしてでしょうか? 喜ばしいことに、これは魔法ではありません。そうではなく、入力の検証とエラー処理のシナリオを簡単にする組み込みの ASP.NET MVC 機能をいくつか使用したためです。

ModelState と検証 HTML ヘルパー メソッドについて理解する

Controller クラスには "ModelState" プロパティ コレクションがあります。これは、View に渡されるモデル オブジェクトにエラーが存在することを示す方法を提供します。 ModelState コレクション内のエラー エントリは、問題のあるモデル プロパティの名前 ("Title"、"EventDate"、"ContactPhone" など) を特定し、わかりやすいエラー メッセージを指定できるようにします ("タイトルは必須です" など)。

UpdateModel() ヘルパー メソッドは、モデル オブジェクトのプロパティにフォーム値を割り当てようとしたときにエラーが発生した場合、自動的に ModelState コレクションを設定します。 たとえば、Dinner オブジェクトの EventDate プロパティは DateTime 型です。 上記のシナリオで UpdateModel() メソッドが文字列値 "BOGUS" を割り当てることができなかったときに、UpdateModel() メソッドはこのプロパティで割り当てエラーが発生したことを示すエントリを ModelState コレクションに追加しました。

開発者は、ModelState コレクションにエラー エントリを明示的に追加するコードを記述することもできます (以下の "catch" エラー処理ブロック内にこれを示します)。これにより、Dinner オブジェクトのアクティブな規則違反に基づくエントリが ModelState コレクションに設定されます。

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
    
        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }
}

HTML ヘルパーと ModelState の統合

HTML ヘルパー メソッド (HTML.TextBox() など) は、出力のレンダリング時に ModelState コレクションをチェックします。 項目のエラーが存在する場合、ユーザーが入力した値と CSS エラー クラスがレンダリングされます。

たとえば、"Edit" ビューで、Html.TextBox() ヘルパー メソッドを使用して Dinner オブジェクトの EventDate をレンダリングしています。

<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>

エラー シナリオでビューがレンダリングされたとき、Html.TextBox() メソッドは ModelState コレクションをチェックして、Dinner オブジェクトの "EventDate" プロパティに関連するエラーがあるかどうかを確認しました。 エラーが発生したと判断したときに、送信されたユーザー入力 ("BOGUS") が値としてレンダリングされ、生成された <input type="textbox"/> マークアップに CSS エラー クラスが追加されました。

<input class="input-validation-error"id="EventDate" name="EventDate" type="text" value="BOGUS"/>

CSS エラー クラスの外観は、必要に応じてカスタマイズできます。 既定の CSS エラー クラス ("input-validation-error") は、\content\site.css スタイルシートで定義され、次のようになります。

.input-validation-error
{
    border: 1px solid #ff0000;
    background-color: #ffeeee;
}

この CSS 規則により、無効な入力要素が次のように強調表示されました。

Screenshot of the highlighted invalid input elements.

Html.ValidationMessage() ヘルパー メソッド

Html.ValidationMessage() ヘルパー メソッドを使用して、特定のモデル プロパティに関連付けられている ModelState エラー メッセージを出力できます。

<%= Html.ValidationMessage("EventDate")%>

上記のコードは次のように出力します: <span class="field-validation-error"> 値 ‘BOGUS' は無効です</span>

Html.ValidationMessage() ヘルパー メソッドでは、2 番目のパラメーターもサポートされています。これにより、開発者は表示されるエラー テキスト メッセージをオーバーライドできます。

<%= Html.ValidationMessage("EventDate","*") %>

EventDate プロパティにエラーが存在する場合、上記のコードは既定のエラー テキストではなく、次のように出力します: <span class="field-validation-error">*</span>

Html.ValidationSummary() ヘルパー メソッド

Html.ValidationSummary() ヘルパー メソッドを使用して、概要エラー メッセージと共に、ModelState コレクション内のすべての詳細なエラー メッセージを表示する <ul><li/></ul> リストをレンダリングできます。

Screenshot of the list of all detailed error messages in the ModelState collection.

Html.ValidationSummary() ヘルパー メソッドは、任意の文字列パラメーターを受け取ります。これは、詳細なエラー リストの上に表示する概要エラー メッセージを定義します。

<%= Html.ValidationSummary("Please correct the errors and try again.") %>

エラー リストの外観をオーバーライドするために CSS を使用することもできます。

AddRuleViolations ヘルパー メソッドを使用する

最初の HTTP-POST の Edit の実装では、catch ブロック内の foreach ステートメントを使用して Dinner オブジェクトの規則違反をループし、コントローラーの ModelState コレクションに追加しました。

catch {
        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }

このコードを少しクリーンすることができます。これを行うには、NerdDinner プロジェクトに "ControllerHelpers" クラスを追加し、その中に "AddRuleViolations" 拡張メソッドを実装できます。これにより、ASP.NET MVC ModelStateDictionary クラスにヘルパー メソッドが追加されます。 この拡張メソッドは、RuleViolation エラーのリストを ModelStateDictionary に設定するために必要なロジックをカプセル化できます。

public static class ControllerHelpers {

   public static void AddRuleViolations(this ModelStateDictionary modelState, IEnumerable<RuleViolation> errors) {
   
       foreach (RuleViolation issue in errors) {
           modelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
       }
   }
}

その後、HTTP-POST の Edit アクション メソッドを更新して、この拡張メソッドを使用して ModelState コレクションに Dinner 規則違反を設定できます。

Edit アクション メソッドの実装を完了する

次のコードは、この Edit シナリオに必要なすべてのコントローラー ロジックを実装します。

//
// GET: /Dinners/Edit/2

public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);
    
    return View(dinner);
}

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
    
        ModelState.AddRuleViolations(dinner.GetRuleViolations());

        return View(dinner);
    }
}

この Edit の実装の素晴らしい点は、Controller クラスと View テンプレートのどちらも、Dinner モデルによって適用されている特定の検証規則やビジネス規則について何も把握する必要がないということです。 今後、モデルに規則を追加する場合、Controller や View でコードを変更しなくてもそれらはサポートされます。 これにより、コードの変更を最小限に抑えつつ、今後アプリケーション要件を簡単に進化させる柔軟性が得られます。

作成のサポート

DinnersController クラスの "編集" 動作の実装が完了しました。 次に、"作成" のサポートを実装します。これにより、ユーザーは新しい Dinner を追加できるようになります。

HTTP-GET の Create アクション メソッド

まず、Create アクション メソッドの HTTP "GET" 動作を実装します。 このメソッドは、ユーザーが /Dinners/Create URL にアクセスしたときに呼び出されます。 実装は次のようになります。

//
// GET: /Dinners/Create

public ActionResult Create() {

    Dinner dinner = new Dinner() {
        EventDate = DateTime.Now.AddDays(7)
    };

    return View(dinner);
}

上記のコードは、新しい Dinner オブジェクトを作成し、EventDate プロパティを 1 週間後に割り当てます。 次に、新しい Dinner オブジェクトに基づく View をレンダリングします。 View() ヘルパー メソッドに名前を明示的に渡していないため、規則に基づく既定のパスを使用してビュー テンプレート /Views/Dinners/Create.aspx を解決します。

次に、このビュー テンプレートを作成してみましょう。 これを行うには、Create アクション メソッド内で右クリックし、[ビューの追加] コンテキスト メニュー コマンドを選びます。 [ビューの追加] ダイアログで、ビュー テンプレートに Dinner オブジェクトを渡すことを示し、"Create" テンプレートを自動スキャフォールディングすることを選択します。

Screenshot of Add view to create a view template.

[追加] ボタンをクリックすると、Visual Studio によって新しいスキャフォールディングベースの "Create.aspx" ビューが "\Views\Dinners" ディレクトリに保存され、IDE 内で開きます。

Screenshot of the I D E to edit the code.

生成された既定の "Create" スキャフォールディング ファイルにいくつかの変更を加え、次のように変更してみましょう。

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
     Host a Dinner
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Host a Dinner</h2>

    <%=Html.ValidationSummary("Please correct the errors and try again.") %>
 
    <% using (Html.BeginForm()) {%>
  
        <fieldset>
            <p>
                <label for="Title">Title:</label>
                <%= Html.TextBox("Title") %>
                <%= Html.ValidationMessage("Title", "*") %>
            </p>
            <p>
                <label for="EventDate">EventDate:</label>
                <%=Html.TextBox("EventDate") %>
                <%=Html.ValidationMessage("EventDate", "*") %>
            </p>
            <p>
                <label for="Description">Description:</label>
                <%=Html.TextArea("Description") %>
                <%=Html.ValidationMessage("Description", "*") %>
            </p>
            <p>
                <label for="Address">Address:</label>
                <%=Html.TextBox("Address") %>
                <%=Html.ValidationMessage("Address", "*") %>
            </p>
            <p>
                <label for="Country">Country:</label>
                <%=Html.TextBox("Country") %>
                <%=Html.ValidationMessage("Country", "*") %>
            </p>
            <p>
                <label for="ContactPhone">ContactPhone:</label>
                <%=Html.TextBox("ContactPhone") %>
                <%=Html.ValidationMessage("ContactPhone", "*") %>
            </p>            
            <p>
                <label for="Latitude">Latitude:</label>
                <%=Html.TextBox("Latitude") %>
                <%=Html.ValidationMessage("Latitude", "*") %>
            </p>
            <p>
                <label for="Longitude">Longitude:</label>
                <%=Html.TextBox("Longitude") %>
                <%=Html.ValidationMessage("Longitude", "*") %>
            </p>
            <p>
                <input type="submit" value="Save"/>
            </p>
        </fieldset>
    <% } 
%>
</asp:Content>

ここでアプリケーションを実行し、ブラウザー内で "/Dinners/Create" URL にアクセスすると、Create アクションの実装から次のような UI がレンダリングされます。

Screenshot of Create action implementation when we run our application and access the Dinners U R L.

HTTP-POST の Create アクション メソッドを実装する

これで、Create アクション メソッドの HTTP-GET バージョンが実装されました。 ユーザーが [保存] ボタンをクリックすると、/Dinners/Create URL へのフォーム ポストが実行され、HTTP POST 動詞を使用して HTML <input> フォーム値が送信されます。

次に、Create アクション メソッドの HTTP POST 動作を実装してみましょう。 まず、HTTP POST シナリオを処理することを示す "AcceptVerbs" 属性を持つ DinnersController にオーバーロードされた "Edit" アクション メソッドを追加します。

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create() {
    ...
}

HTTP POST の "Edit" メソッド内でポストされたフォーム パラメーターにアクセスするには、さまざまな方法があります。

1 つの方法は、新しい Dinner オブジェクトを作成し、(Edit アクションと同様に) UpdateModel() ヘルパー メソッドを使用して、ポストされたフォーム値を設定することです。 その後、次のコードを使用して、DinnerRepository に追加し、データベースに永続化し、ユーザーを Details アクションにリダイレクトして、新しく作成された Dinner を表示できます。

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create() {

    Dinner dinner = new Dinner();

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Add(dinner);
        dinnerRepository.Save();

        return RedirectToAction("Details", new {id=dinner.DinnerID});
    }
    catch {
    
        ModelState.AddRuleViolations(dinner.GetRuleViolations());

        return View(dinner);
    }
}

または、Create() アクション メソッドが Dinner オブジェクトをメソッド パラメーターとして受け取るアプローチを使用することもできます。 これを行うと、ASP.NET MVC は自動的に新しい Dinner オブジェクトをインスタンス化し、フォーム入力を使用してそのプロパティを設定し、アクション メソッドに渡します。

//
//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Dinner dinner) {

    if (ModelState.IsValid) {

        try {
            dinner.HostedBy = "SomeUser";

            dinnerRepository.Add(dinner);
            dinnerRepository.Save();

            return RedirectToAction("Details", new {id = dinner.DinnerID });
        }
        catch {        
            ModelState.AddRuleViolations(dinner.GetRuleViolations());
        }
    }
    
    return View(dinner);
}

上記のアクション メソッドは、ModelState.IsValid プロパティをチェックすることで、Dinner オブジェクトにフォーム ポスト値が正常に設定されたことを確認します。 これにより、入力変換の問題 (EventDate プロパティに対する "BOGUS" の文字列など) がある場合は false を返します。また、問題がある場合、アクション メソッドはフォームを再表示します。

入力値が有効な場合、アクション メソッドは DinnerRepository に新しい Dinner を追加して保存しようとします。 この作業は try/catch ブロック内でラップされ、ビジネス規則違反がある場合にフォームを再表示します (この場合、dinnerRepository.Save() メソッドが例外を発生させます)。

このエラー処理の動作を確認するには、/Dinners/Create URL を要求し、新しい Dinner に関する詳細を入力します。 入力または値が正しくないと、作成フォームが再表示され、次のようなエラーが強調表示されます。

Screenshot of the form redisplayed with errors highlighted.

Create フォームで、Edit フォームとまったく同じ検証規則とビジネス規則が使用されていることにご注目ください。 これは、検証規則とビジネス規則がモデルで定義されており、アプリケーションの UI またはコントローラーに埋め込まれていないためです。 つまり、検証規則またはビジネス規則を後で 1 か所で変更し、アプリケーション全体に適用させることができます。 新しい規則や既存の規則の変更を自動的に適用するために、Edit または Create アクション メソッド内のコードを変更する必要はありません。

入力値を修正し、[保存] ボタンをもう一度クリックすると、DinnerRepository への追加が成功し、新しい Dinner がデータベースに追加されます。 その後、/Dinners/Details/[id] URL にリダイレクトされます。ここで、新しく作成された Dinner に関する詳細が表示されます。

Screenshot of the newly created Dinner.

削除のサポート

次に、DinnersController に "削除" のサポートを追加しましょう。

HTTP-GET の Delete アクション メソッド

まず、Edit アクション メソッドの HTTP "GET" 動作を実装します。 このメソッドは、ユーザーが /Dinners/Delete/[id] URL にアクセスしたときに呼び出されます。 実装を次に示します。

//
// HTTP GET: /Dinners/Delete/1

public ActionResult Delete(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (dinner == null)
         return View("NotFound");
    else
        return View(dinner);
}

アクション メソッドが、削除する Dinner の取得を試みます。 Dinner が存在する場合は、Dinner オブジェクトに基づいてビューがレンダリングされます。 オブジェクトが存在しない (または既に削除されている) 場合、"Details" アクション メソッド用に先ほど作成した "NotFound" ビュー テンプレートをレンダリングするビューが返されます。

"Delete" ビュー テンプレートを作成するには、Delete アクション メソッド内で右クリックし、[ビューの追加] コンテキスト メニュー コマンドを選択します。 [ビューの追加] ダイアログで、ビュー テンプレートに Dinner オブジェクトをモデルとして渡すことを示し、"Edit" テンプレートを自動スキャフォールディングすることを選択します。

Screenshot of creating the Delete view template as an an empty template.

[追加] ボタンをクリックすると、Visual Studio によって "\Views\Dinners" ディレクトリ内に新しい "Edit.aspx" ビュー テンプレート ファイルが追加されます。 テンプレートに HTML とコードをいくつか追加して、次のような削除確認画面を実装します。

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Delete Confirmation:  <%=Html.Encode(Model.Title) %>
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>
        Delete Confirmation
    </h2>

    <div>
        <p>Please confirm you want to cancel the dinner titled: 
           <i> <%=Html.Encode(Model.Title) %>? </i> 
        </p>
    </div>
    
    <% using (Html.BeginForm()) {  %>
        <input name="confirmButton" type="submit" value="Delete" />        
    <% } %>
     
</asp:Content>

上記のコードは、削除する Dinner のタイトルを表示し、エンド ユーザーが内部の [削除] ボタンをクリックした場合に /Dinners/Delete/[id] URL に POST を実行する <form> 要素を出力します。

アプリケーションを実行し、有効な Dinner オブジェクトの "/Dinners/Delete/[id]" URL にアクセスすると、次のような UI がレンダリングされます。

Screenshot of the Dinner delete confirmation U I in the H T T P G E T Delete action method.

サイド トピック: POST を実行する理由
ここで、疑問に思われるかもしれません。"削除確認画面で <form> をわざわざ作成したのはなぜか?" また、"標準のハイパーリンクを使用して、実際の削除操作を行うアクション メソッドにリンクしないのはなぜか?" という疑問が生じます。 これは、Web クローラーや検索エンジンが URL を検出し、リンクに従ったときにデータが誤って削除されるのを防ぐために、注意が必要であるためです。 HTTP-GET ベースの URL は、"安全" にアクセス/クロールできると見なされており、HTTP-POST に従わないと考えられています。 HTTP-POST 要求の背後に破壊的操作またはデータ変更操作を常に配置することをお勧めします。

HTTP-POST の Delete アクション メソッドを実装する

現時点で、削除確認画面を表示する Delete アクション メソッドの HTTP-GET バージョンが実装されました。 エンド ユーザーが [削除] ボタンをクリックすると、/Dinners/Dinner/[id] URL へのフォーム ポストが実行されます。

ここで、次のコードを使用して、Delete アクション メソッドの HTTP "POST" 動作を実装してみましょう。

// 
// HTTP POST: /Dinners/Delete/1

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Delete(int id, string confirmButton) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (dinner == null)
        return View("NotFound");

    dinnerRepository.Delete(dinner);
    dinnerRepository.Save();

    return View("Deleted");
}

Delete アクション メソッドの HTTP-POST バージョンは、削除する Dinner オブジェクトの取得を試みます。 (既に削除されているために) 見つからない場合は、"NotFound" テンプレートがレンダリングされます。 Dinner が見つかった場合、DinnerRepository から削除されます。 その後、"削除済み" テンプレートがレンダリングされます。

"削除済み" テンプレートを実装するには、アクション メソッドを右クリックし、[ビューの追加] コンテキスト メニューを選びます。 ビューに "Deleted" という名前を付け、空のテンプレートにします (厳密に型指定されたモデル オブジェクトにはしません)。 その後、それに HTML コンテンツをいくつか追加します。

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Dinner Deleted
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Dinner Deleted</h2>

    <div>
        <p>Your dinner was successfully deleted.</p>
    </div>
    
    <div>
        <p><a href="/dinners">Click for Upcoming Dinners</a></p>
    </div>
    
</asp:Content>

ここで、アプリケーションを実行し、有効な Dinner オブジェクトの "/Dinners/Delete/[id]" URL にアクセスします。すると、次のような Dinner 削除確認画面が表示されます。

Screenshot of the Dinner delete confirmation screen in the H T T P P O S T Delete action method.

[削除] ボタンをクリックすると、/Dinners/Delete/[id] URL への HTTP-POST が実行され、データベースから Dinner が削除され、"削除済み" ビュー テンプレートが表示されます。

Screenshot of the Deleted view template.

モデル バインド セキュリティ

ASP.NET MVC の 2 つの異なる組み込みのモデル バインド機能を使用する方法について説明しました。 1 つ目は、UpdateModel() メソッドを使用して既存のモデル オブジェクトのプロパティを更新することです。2 つ目は、アクション メソッド パラメーターとしてモデル オブジェクトを渡すための ASP.NET MVC のサポートを使用することです。 これらの手法はどちらも非常に強力で、非常に便利です。

この機能には責任も伴います。 ユーザー入力を受け入れるときは常にセキュリティの被害を予測することが重要です。これは、オブジェクトをフォーム入力にバインドする場合にも当てはまります。 HTML および JavaScript インジェクション攻撃を回避するために、ユーザーが入力した値を常に HTML エンコードし、SQL インジェクション攻撃に注意する必要があります (注: ここでは、アプリケーションに LINQ to SQL を使用しています。これは、これらの種類の攻撃を防ぐためにパラメーターを自動的にエンコードします)。 クライアント側の検証だけに頼ることはしないでください。常にサーバー側の検証を使用して、偽の値を送信しようとするハッカーから保護する必要があります。

ASP.NET MVC のバインド機能を使用する際に考慮すべきもう 1 つのセキュリティ項目は、バインドするオブジェクトのスコープです。 具体的には、バインドを許可するプロパティのセキュリティへの影響を理解し、エンドユーザーが実際に更新可能なプロパティのみを更新できるようにする必要があります。

既定では、UpdateModel() メソッドは、受信フォーム パラメーター値に一致するモデル オブジェクトのすべてのプロパティの更新を試みます。 同様に、アクション メソッド パラメーターとして渡されるオブジェクトも、既定では、フォーム パラメーターを使用してすべてのプロパティを設定できます。

使用ごとにバインドをロックダウンする

更新可能なプロパティの明示的な "インクルード リスト" を指定することで、使用ごとにバインド ポリシーをロックダウンできます。 これを行うには、次のように、追加の文字列配列パラメーターを UpdateModel() メソッドに渡します。

string[] allowedProperties = new[]{ "Title","Description", 
                                    "ContactPhone", "Address",
                                    "EventDate", "Latitude", 
                                    "Longitude"};
                                    
UpdateModel(dinner, allowedProperties);

アクション メソッド パラメーターとして渡されるオブジェクトでは、[Bind] 属性もサポートされています。これにより、許可されるプロパティの "インクルード リスト" を次のように指定できます。

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create( [Bind(Include="Title,Address")] Dinner dinner ) {
    ...
}

型ベースでのバインドのロックダウン

型ごとにバインド規則をロックダウンすることもできます。 これにより、バインド規則を 1 回指定し、これをすべてのコントローラーとアクション メソッドのすべてのシナリオ (UpdateModel とアクション メソッドパラメーターの両方のシナリオを含む) に適用することができます。

型ごとにバインド規則をカスタマイズするには、[Bind] 属性を型に追加するか、アプリケーションの Global.asax ファイル内に登録します (型を所有していないシナリオに役立ちます)。 その後、Bind 属性の Include プロパティと Exclude プロパティを使用して、特定のクラスまたはインターフェイスにバインド可能なプロパティを制御できます。

NerdDinner アプリケーションの Dinner クラスにこの手法を使用し、バインド可能なプロパティの一覧を次に制限する [Bind] 属性を追加します。

[Bind(Include="Title,Description,EventDate,Address,Country,ContactPhone,Latitude,Longitude")]
public partial class Dinner {
   ...
}

バインドによる RSVP コレクションの操作は許可しておらず、バインドによるDinnerID プロパティや HostedBy プロパティの設定も許可していないことにご注目ください。 セキュリティ上の理由から、そうするのではなく、アクション メソッド内で明示的なコードを使用してこれらの特定のプロパティのみを操作します。

CRUD のまとめ

ASP.NET MVC には、フォーム ポスト シナリオの実装に役立つ多くの組み込み機能が含まれています。 これらのさまざまな機能を使用して、DinnerRepository に CRUD UI のサポートを提供しました。

ここではアプリケーションの実装に、モデルに重点を置いたアプローチを使用しています。 つまり、すべての検証規則ロジックとビジネス規則ロジックは、コントローラーやビュー内ではなく、モデル レイヤー内で定義されます。 Controller クラスと View テンプレートのどちらも、Dinner モデル クラスによって適用される特定のビジネス規則について何も把握していません。

これにより、アプリケーション アーキテクチャがクリーンされ、テストが容易になります。 今後、モデル レイヤーにビジネス規則を追加する場合、Controller や View でコードを変更しなくてもそれらはサポートされます。 これにより、今後アプリケーションを進化させ、変更するための機敏性が大幅に向上します。

DinnersController では、Dinner の List/Details、および作成、編集、削除のサポートが有効になりました。 クラスの完全なコードは次のとおりです。

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    //
    // GET: /Dinners/

    public ActionResult Index() {

        var dinners = dinnerRepository.FindUpcomingDinners().ToList();
        return View(dinners);
    }

    //
    // GET: /Dinners/Details/2

    public ActionResult Details(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");
        else
            return View(dinner);
    }

    //
    // GET: /Dinners/Edit/2

    public ActionResult Edit(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);
        return View(dinner);
    }

    //
    // POST: /Dinners/Edit/2

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(int id, FormCollection formValues) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        try {
            UpdateModel(dinner);

            dinnerRepository.Save();

            return RedirectToAction("Details", new { id= dinner.DinnerID });
        }
        catch {
            ModelState.AddRuleViolations(dinner.GetRuleViolations());

            return View(dinner);
        }
    }

    //
    // GET: /Dinners/Create

    public ActionResult Create() {

        Dinner dinner = new Dinner() {
            EventDate = DateTime.Now.AddDays(7)
        };
        return View(dinner);
    }

    //
    // POST: /Dinners/Create

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Create(Dinner dinner) {

        if (ModelState.IsValid) {

            try {
                dinner.HostedBy = "SomeUser";

                dinnerRepository.Add(dinner);
                dinnerRepository.Save();

                return RedirectToAction("Details", new{id=dinner.DinnerID});
            }
            catch {
                ModelState.AddRuleViolations(dinner.GetRuleViolations());
            }
        }

        return View(dinner);
    }

    //
    // HTTP GET: /Dinners/Delete/1

    public ActionResult Delete(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");
        else
            return View(dinner);
    }

    // 
    // HTTP POST: /Dinners/Delete/1

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Delete(int id, string confirmButton) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");

        dinnerRepository.Delete(dinner);
        dinnerRepository.Save();

        return View("Deleted");
    }
}

次の手順

DinnersController クラス内で基本的な CRUD (作成、読み取り、更新、削除) のサポートが実装されました。

次に、ViewData クラスと ViewModel クラスを使用して、フォームでさらに豊富な UI を有効にする方法を見てみましょう。