NerdDinnerチュートリアル

NerdDinnerステップ5:作成、更新、削除フォームのシナリオ

Scott Guthrie 著/Chica
2009/08/11

 本記事は、Microsoftの本社副社長であり、ASP.NETやSilverlightなどの開発チームを率いるScott Guthrie氏が公開している「NerdDinner Tutorial」を翻訳したものです。氏の許可を得て転載しています。

[これは無償の"NerdDinner"アプリケーション・チュートリアルのステップ5で、ASP.NET MVCを使用して、小さいながらも完全なWebアプリケーションを構築する手順を紹介しています。]

 すでにコントローラとビューを紹介し、それらを使ってサイト上で夕食会の一覧/詳細表示を実装する方法についてカバーしました。次のステップは、DinnersControllerクラスにさらに踏み込んで、夕食会の編集、作成、削除をサポートできるようにします。

DinnersControllerが処理するURL

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

URL 動詞 目的
/Dinners/ GET 未開催の夕食会のHTML一覧を表示
/Dinners/Details/[id] GET 特定の夕食会の詳細を表示

 いまからは、3つのURL、/Dinners/Edit/[id]/Dinners/Create/Dinners/Delete/[id]を実装するアクション・メソッドをさらに追加します。これらのURLは、既存の夕食会の編集、夕食会の新規作成、夕食会の削除をサポートできるようにします。

 これらの新しいURLについては、HTTP GETおよびHTTP POSTの動詞を両方サポートします。これらのURLへのHTTP GET要求は、データの初回のHTMLビューを表示します(“編集”の場合はその夕食会データにひも付いたフォーム、“作成”の場合は空のページ、“削除”の場合は削除の確認画面)。これらのURLへのHTTP POST要求では、DinnerRepositoryにある夕食データを(そして、そこからデータベースへ)保存/更新/削除します。

URL 動詞 目的
/Dinners/Edit/[id] GET 夕食会のデータにひも付いた編集可能なHTMLフォームを表示
POST 特定の夕食会へのフォーム変更をデータベースへ保存
/Dinners/Create GET 新しい夕食会をユーザーが定義できる空のHTMLフォームを表示
POST 新規の夕食会を作成し、データベースに保存
/Dinners/Delete/[id] GET 削除確認画面を表示
POST 指定した夕食会をデータベースから削除

編集サポート

 まず“編集”のケースを実装していきましょう。

■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ヘルパー・メソッドへテンプレート名を引き渡していないので、そのビュー・テンプレートを解決するために、規定に基づいたデフォルトのパス、/Views/Dinners/Edit.aspxを使用します。

 ではこのビュー・テンプレート作成しましょう。これを行うには、Editメソッド内で右クリックして、“Add View”コンテキスト・メニュー・コマンドを選択します。


図1

 “Add View”ダイアログ内では、Dinnerオブジェクトをビュー・テンプレートにそのモデルとして引き渡すことを示し、“Edit”テンプレートを自動スキャフォールドするように選択します。


図2

 “Add”ボタンをクリックすると、Visual Studioは新しい“Edit.aspx”ビュー・テンプレート・ファイルを“\Views\Dinners”ディレクトリ内に追加します。またコード・エディタ内でその新しい“Edit.aspx”ビュー・テンプレートを開きます。そこには以下のように、初回の“Edit”スキャフォールド実装がひも付いています。


図3

 生成されたデフォルトの“Edit”スキャフォールドにいくつか変更を加えて、以下のようなコンテンツとなるように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>

  <% 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をリクエストすると、以下のようなページが表示されます。


図4

 ビューが生成したHTMLタグは以下のようになります。“Save”の<input type=“submit”/>ボタンが押されたときに/Dinners/Edit/1 URLへHTTP POSTを実行する<form>要素が含まれた標準のHTMLです。編集可能な各プロパティに対しては、HTMLの<input type=“text”/>要素が出力されます。


図5

■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>

    <!-- 簡潔化のためフィールドを省略 -->

    <p>
     <input type="submit" value="Save"/>
    </p>
   </fieldset>

<% } %>

 または、今回のようなケースで“using”文による方法が不自然と感じる場合、Html.BeginForm()とHtml.EndForm()の組み合わせも使えます(これは同じことを行います)。

<% Html.BeginForm(); %>

   <fieldset>

    <!-- 簡潔化のためフィールドを省略 -->

    <p>
      <input type="submit" value="Save"/>
    </p>
   </fieldset>

<% Html.EndForm(); %>

 パラメータなしでHtml.BeginForm()を呼び出すと、現在の要求のURLにHTTP-POSTを行うフォーム要素が出力されます。このため、Editビューが<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属性と、テキストボックスからの値をひも付けるためのモデルのプロパティの両方を指定するために使用します。例えば、Editビューへ引き渡すDinnerオブジェクトに“.NET Futures”という“Title”プロパティの値があるとすると、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”はC#の予約キーワードなので、“@”記号を使用してclass属性の名前をエスケープしているところをご確認ください。

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

■HTTP-POST Editアクション・メソッドを実装

 Editアクション・メソッドのHTTP-GET版が実装できました。ユーザーが/Dinners/Edit/1 URLを要求したときに受信するHTMLページは、次のようになります。


図6

 “Save”ボタンを押すと、フォームは/Dinners/Edit/1のURLにポストされ、HTTP POST動詞を使用してHTMLの<input>フォームの値が送信されます。それでは次に、Editアクション・メソッドのHTTP POST動作を実装しましょう。これは夕食会を保存する処理を行います。

 まずオーバーロードされた“Edit”アクション・メソッドをDinnersControllerへ追加します。その上には“AcceptVerbs”属性が付いており、HTTP POSTケースを処理することが示されています。

//
// 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動詞の要求は、実装した最初の([AcceptVerbs]属性のない)Editメソッドへ行きます。

サイド・トピック:なぜ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が依存するため)。

 1つのURL(例:/Dinners/Edit/[id])を公開してHTTP動詞で処理を分けると、編集ページのブックマークや、ほかの人へのURLの送信などの際にも、エンドユーザーには安全です。

■フォームの送信値を取得

 HTTP POST“Edit”メソッドの送信されたフォームのパラメータにアクセスする方法はさまざまです。1つの簡単な方法は、単純にコントローラの基本クラス上のRequestプロパティを使って、フォームのコレクションにアクセスし、送信された値を直接取得します。

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

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

  // 既存の夕食会を取得
  Dinner dinner = dinnerRepository.GetDinner(id);

  // フォーム送信された値で夕食会を更新
  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"];

  // データベースに変更を保存
  dinnerRepository.Save();

  // 保存された夕食会の詳細ページへHTTPリダイレクトを実行
  return RedirectToAction("Details", new { id = dinner.DinnerID });
}

 ですが、上記の方法は、特にエラー処理のロジックを追加した場合に少し冗長になります。

 このケースでよりよい方法は、コントローラの基本クラス上でビルトインのUpdateModelヘルパー・メソッドを利用することです。これは受信フォームのパラメータを使用して引き渡したオブジェクトのプロパティを更新するサポートを行います。オブジェクトのプロパティ名を決定するのにはリフレクションを使っており、クライアントによって送信された入力値をベースに、それらを自動的にコンバートし、割り当てます。

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

//
// 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を見ると、夕食会のタイトルが変更されています。


図7

 “Save”ボタンをクリックすると、Editアクションへのフォームの送信が実行され、その更新された値はデータベースに保存されます。その後、その夕食会のDetails URLへリダイレクトします(それは新しく保存された値で表示されます)。


図8

■編集エラー処理

 現在の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を保存しようとしたとき(モデル内でのルール違反が原因で保存しようとしたDinnerオブジェクトが無効な場合には例外が発生します)のいずれかでもし例外が発生した場合、エラー処理のcatchブロックが実行されます。その中で、Dinnerオブジェクトに存在するすべてのルール違反をループして、ModelStateオブジェクト(まもなく解説します)に追加します。その後、そのビューを再表示します。

 この動作を確認するために、アプリケーションを再起動して、夕食会を編集し、Titleを空にして、EventDateを“BOGUS”にし、国はUSAにして、UKの電話番号を使って変更します。“Save”ボタンをクリックすると、HTTP POST Editメソッドはその夕食会が保存できず(エラーがあるため)、フォームを再表示します。


図9

 今回のアプリケーションは適切なエラー機能があります。無効な入力のあるテキスト要素は赤でハイライトされ、検証エラー・メッセージはそれらの上にエンドユーザーに向けて表示されます。フォームはそのユーザーがもともと入力した入力データの保存も行うため、何も再入力する必要がありません。

 これがどのようにして起こっているのか疑問に思われるかもしれません。どのようにしてTitle、EventDate、ContactPhoneのテキストボックスが赤でハイライトされるのか、どのようにして元のユーザーの入力値を出力することを知ることができるのか? よいお知らせですが、これは魔法ではありません。正しくは、ビルトインのASP.NET MVC機能をいくつか使っているからで、それらが検証やエラー処理のシナリオを簡単にするのです。

■ModelStateと検証HTMLヘルパー・メソッドの理解

 コントローラ・クラスには“ModelState”プロパティ・コレクションがあり、ビューへ引き渡されるモデル・オブジェクトとともに、エラーの存在を示す方法を提供します。ModelStateコレクション内のエラー・エントリは、問題点のあるモデルのプロパティの名前を特定するもので(例えば、“Title”、“EventDate”、“ContactPhone”)、ユーザーフレンドリーなエラー・メッセージを指定することができます(例えば、“Title is required”)。

 モデル・オブジェクト上のプロパティに値を割り当てようとしているときにエラーが発生した場合、UpdateModelヘルパー・メソッドは自動的にModelStateコレクションをひも付けます。例えば、DinnerオブジェクトのEventDateプロパティはDateTime型です。上記のケースでUpdateModelメソッドが文字列の値“BOGUS”をそこへ割り当てられなかった場合、UpdateModelメソッドは、そのプロパティで割り当てエラーが発生したことを示したエントリをModelStateコレクションに追加します。

 開発者はまた、以下の“catch”エラー処理ブロックで行っているように、明示的にエラー・エントリをModelStateコレクションに追加するようなコードを書くことも可能です。これは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);
  }
}

■ModelStateとHTMLヘルパーの統合

 HTMLヘルパー・メソッド、例えばHtml.TextBox()は、出力を描画するときにModelStateコレクションをチェックします。もしその項目に関するエラーが存在すると、HTMLヘルパー・メソッドはユーザーが入力した値と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ルールにより、無効な入力要素は以下のようにハイライトされます。


図10

・Html.ValidationMessageヘルパー・メソッド

 Html.ValidationMessageヘルパー・メソッドは、ある特定のモデルのプロパティに関連しているModelStateエラー・メッセージを出力するために使用できます。

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

 上記コードの出力は、次のようになります。

<span class=")field-validation-error")>‘BOGUS'という値は無効です。</span>

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

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

 上記コードの出力は、エラーがEventDateプロパティに対して表示されるとき、デフォルトのエラー・テキストの代わりに次のような表示になります。

<span class="field-validation-error">*</span>

・Html.ValidationSummaryヘルパー・メソッド

 Html.ValidationSummaryヘルパー・メソッドは、ModelStateにあるすべての詳細エラー・メッセージの<ul><li/></ul>リストとともに、エラー・メッセージの要約を描画します。


図11

 Html.ValidationSummaryヘルパー・メソッドは、オプションで文字列のパラメータを取ります。これは上記の詳細エラーのリストの上に表示するエラー・メッセージの概要を定義します。

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

 オプションでCSSを使ってエラー・リストの外観をオーバーライドできます。

■AddRuleViolationsヘルパー・メソッドの使用

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

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

    return View(dinner);
  }

 “ControllerHelpers”クラスをNerdDinnerプロジェクトに追加することにより、このコードを少しクリーンにでき、ASP.NET MVCのModelStateDictionaryクラスへヘルパー・メソッドを追加するための“AddRuleViolations”拡張メソッドをその中で実装できます。この拡張メソッドは、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コレクションをひも付けるために、この拡張メソッドを使います。

■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の実装の良いところは、コントローラ・クラスおよびビュー・テンプレートのどちらも、Dinnerモデルが強制する特定の検証やビジネス・ルールについて何も知らなくてもいいという点です。将来的にモデルへルールを追加することができ、それらをサポートするために、コントローラやビューに対していずれのコードも変更する必要はありません。これにより、今後のアプリケーション要求を最少のコード変更で簡単に展開できる柔軟性が提供されます。

作成サポート

 DinnersControllerクラスの“Edit”動作の実装が完了しました。次にそのクラスでの“Create”サポートの実装へ移りましょう。これによりユーザーは新しい夕食会を追加できます。

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

 Createアクション・メソッドのHTTP“GET”動作をまず実装します。このメソッドは、誰かが/Dinners/CreateURLを訪れたときに呼ばれます。実装は次のようになります。

//
// GET: /Dinners/Create

public ActionResult Create() {

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

  return View(dinner);
}

 上記のコードは新しいDinnerオブジェクトを作成し、その EventDateプロパティに1週間後を割り当てます。そして、その新しいDinnerオブジェクトに基づいてビューを描画します。Viewヘルパー・メソッドに明示的に名前を引き渡していないので、そのビュー・テンプレートを解決するために規定に基づいたデフォルト・パス、/Views/Dinners/Create.aspxが使用されます。

 ではビュー・テンプレートを作成しましょう。これを行うには、Createアクション・メソッド内で右クリックして、“Add View”コンテキスト・メニュー・コマンド選択します。“Add View”ダイアログで、ビュー・テンプレートへDinnerオブジェクトを引き渡すことを示し、“Create”テンプレートの自動スキャフォールドを選択します。


図12

 “Add”ボタンをクリックすると、Visual Studioは新しいスキャフォールド・ベースの“Create.aspx”ビューを“\Views\Dinners”ディレクトリに保存し、IDE内にそれを開きます。


図13

 生成されたデフォルトの“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は以下のように描画されます。


図14

■HTTP-POST Create アクション・メソッドの実装

 Createアクション・メソッドのHTTP-GET版が実装できました。ユーザーが“Save”ボタンをクリックしたときに、/Dinners/Create URLへのフォーム送信が実行され、HTMLの<input>フォームの値がHTTP POST動詞を使用して送信されます。

 では、Createアクション・メソッドのHTTP POST動作を実装しましょう。まずオーバーロードされた“Create”アクション・メソッドを、HTTP POSTのケースを処理するように示した“AcceptVerbs”属性のあるDinnersControllerに追加します。

//
// POST: /Dinners/Create

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

 “Create”メソッドで可能な、HTTP-POST内で送信されたフォームのパラメータにアクセスする方法はさまざまです。

 1つの方法は、新しいDinnerオブジェクトを作成し、送信されたフォーム値とひも付けるためにUpdateModelヘルパー・メソッドを使用します(Editアクションと同様)。そして、以下のようなコードを使用してDinnerRepositoryに追加し、それをデータベースに保存し、新しく作成された夕食会を表示するためにユーザーをDetailsアクションへリダイレクトします。

//
// 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へ追加して保存します。この作業はtry/catchブロックでラップし、ビジネス・ルール違反があった場合、そのフォームを再表示します(これは例外を発生させるためにdinnerRepository.Saveメソッドを呼び出します)。

 このエラー処理動作を見るために、/Dinners/Create URLをリクエストし、新しい夕食会についての詳細を書き込みます。無効な入力や値については以下のようにエラーがハイライトされ、作成フォームが再表示されます。


図15

 CreateフォームにEditフォームとまったく同じ検証とビジネス・ルールが適用されているところをご確認ください。これは検証とビジネス・ルールがモデルで定義されていて、アプリケーションのUIやコントローラに埋め込まれていないからです。つまり、将来的に検証やビジネス・ルールは1つの場所で変更/展開でき、アプリケーション全体にわたってそれらを適用できます。新しいルールや既存のルールへの修正を自動的に受け入れるため、EditまたはCreateアクション・メソッドのどちらにおいてもコードを変更する必要はありません。

 入力値を修正して“Save”ボタンを再度クリックすると、DinnerRepositoryへの追加が成功し、新しい夕食会はデータベースに追加されます。そして/Dinners/Details/[id] URLへリダイレクトされます。そこでは新しく作成された夕食会についての詳細が表示されます。


図16

削除のサポート

 では“Delete”のサポートをDinnersControllerに追加しましょう。

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

 まずDeleteアクション・メソッドの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オブジェクトに基づいてビューを描画します。もしその夕食会が存在しない場合(またはすでに削除されている場合)、“Details”アクション・メソッドに対して以前に作成した“NotFound”ビュー・テンプレートを描画するビューを返します。

 “Delete”ビュー・テンプレートは、Deleteアクション・メソッド内で右クリックして、“Add View”コンテキスト・メニュー・コマンドを選択すると作成できます。“Add View”ダイアログで、ビュー・テンプレートへDinnerオブジェクトをそのモデルとして引き渡すことを示し、空のテンプレートの作成を選択します。


図17

 “Add”ボタンをクリックすると、Visual Studioは新しい“Delete.aspx”ビュー・テンプレートのファイルを“\Views\Dinners”ディレクトリに追加します。いくつかのHTMLとコードをそのテンプレートに追加して、削除確認画面を以下のように実装します。

<asp:Content ID="Title" ContentPlaceHolderID="head" 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>

 上記のコードは、削除される夕食会のタイトルを表示し、その中でエンドユーザーが“Delete”ボタンをクリックすると、/Dinners/Delete/[id] URLへPOSTを行う<form>要素を出力します。

 アプリケーションを実行して、有効なDinnerオブジェクトに対して“/Dinners/Delete/[id]” URLにアクセスすると、UIは以下のように描画されます。


図18

サイド・トピック:なぜPOSTを行うのか?

 なぜ、削除確認画面で<form>を作成するところまで行うのか? なぜ、標準のハイパーリンクを使用して、実際の削除作業を行うアクション・メソッドにリンクしないのか? と疑問を持たれるかもしれません。

 この理由は、Webクローラや検索エンジンがURLを発見し、そのリンクをたどったときに、うっかりデータが削除されることにならないよう気を付けたいからです。HTTP-GETベースのURLは、それらがアクセス/クロールしても“安全”だと考えられており、それらは本来HTTP-POSTをたどらないとされています。

 よい習慣としては、削除あるいはデータ修正の処理を常にHTTP-POST要求の背後に置くことです。

■HTTP-POST Deleteアクション・メソッドの実装

 いま削除確認画面を表示するDeleteアクション・メソッドのHTTP-GET版が実装できました。エンドユーザーが“Delete”ボタンをクリックすると、/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”テンプレートが描画されます。もしその夕食会が見つかった場合、DinnerRepositoryからそれを削除します。そして、“Deleted”テンプレートを描画します。

 “Deleted”テンプレートを実装するには、アクション・メソッド上で右クリックして、“Add View”コンテキスト・メニューを選択します。そのビューに“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にアクセスすると、その夕食会の削除確認画面が以下のように描画されます。


図19

 “Delete”ボタンをクリックすると、/Dinners/Delete/[id] URLへHTTP-POSTを実行します。これはデータベースからその夕食会を削除し、“Deleted”ビュー・テンプレートを表示します。


図20

モデルのバインディング・セキュリティ

 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 ) {
  ……
}

■型ベースでバインディングをロックダウン

 型ベースでバインディング・ルールをロックダウンすることもできます。これにより、一度バインディング・ルールを指定すると、すべてのコントローラとアクション・メソッドにわたるすべてのケースで、それらが適用されます(UpdateModelおよびアクション・メソッド・パラメータの両方のケースを含む)。

 型に[Bind]属性を追加するか、アプリケーションのGlobal.asaxファイル内でそれを登録する(型を持たないケースに便利)ことで、各型のバインディング・ルールをカスタマイズできます。そして、そのBind属性のIncludeおよびExcludeプロパティを使用して、特定のクラスまたはインターフェイスに対して、どのプロパティがバインド可能かを制御できます。

 NerdDinnerアプリケーションでは、Dinnerクラスに対してこのテクニックを使用し、[Bind]属性を追加して、次のようにバインド可能なプロパティの一覧を制限しましょう。

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

 バインディングを通じてRSVPsコレクションが操作できないようになっており、かつ、バインディングを通じてDinnerIDまたはHostedByプロパティが設定できないようになっていることをご確認ください。セキュリティの理由から、代わりにアクション・メソッド内で明示的なコードを使用して、これらの特定のプロパティを操作します。

CRUDのまとめ

 ASP.NET MVCには数多くのビルトインの機能があり、フォーム送信のシナリオの実装をサポートします。これらのさまざまな機能を使用して、DinnerRepository上でCRUD UIサポートを提供できます。

 アプリケーションの実装には、モデル重視のアプローチを採用しています。つまり、すべての検証とビジネス・ルールのロジックはモデル層内で定義されていて、コントローラやビューの中ではありません。コントローラ・クラスもビュー・テンプレートも、Dinnerモデルのクラスが強制しているビジネス・ルールについて何も知りません。

 これにより、アプリケーション構造がクリーンに保て、テストを比較的簡単にできます。将来的にモデル層へビジネス・ルールを追加でき、それらをサポートするためにコントローラまたはビューへ何も変更を加える必要はありません。これは今後アプリケーションを展開または変更するときに、非常に優れた敏捷性を提供します。

 DinnersControllerはいまや、夕食会の一覧/詳細、そして作成、編集、削除のサポートが可能になりました。このクラスのすべてのコードは以下で確認できます。

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

次のステップ

 さて、基本的なCRUD(作成、読み込み、更新、削除)のサポートをDinnersControllerクラスに実装できました。

 では、フォーム上にもっとリッチなUIを可能にするために、ViewDataとViewModelクラスを使用する方法を見てみましょう。

[注:NerdDinnerアプリケーションの完成版はhttp://nerddinner.codeplex.com/からダウンロードできます。] End of Article

 
インデックス・ページヘ  「NerdDinnerチュートリアル」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間