NerdDinnerチュートリアル

NerdDinnerステップ6:ViewDataとViewModel

Scott Guthrie 著/Chica
2010/02/19

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

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

 数多くのフォーム送信のケースをカバーし、作成、更新、削除(CRUD)サポートの実装方法を述べました。ここではDinnersControllerの実装を詳しく見ていき、よりリッチなフォーム編集のシナリオをサポートできるようにします。これを行うに当たり、コントローラからビューへデータを引き渡すために使用できる2つの方法、ViewDataとViewModelについて説明していきます。

コントローラからビュー・テンプレートへデータの引き渡し

 MVCパターンで定義されている性質の1つが、厳密な“関心の分離”であり、アプリケーションの異なるコンポーネント間で、それが施行されるようサポートします。モデル、コントローラ、ビューはそれぞれ、明確に定義された役割と担当があり、明確に定義された方法で、お互いに通信します。これはテスト性とコードの再利用を促進します。

 コントローラ・クラスがクライアントに返すHTMLレスポンスを描画することになった場合、コントローラ・クラスには、そのレスポンスを描画するのに必要なデータをすべてビュー・テンプレートへ明示的に引き渡す役割があります。ビュー・テンプレートは、データの取得やアプリケーション・ロジックの実行を絶対に行うべきではなく、コントローラが引き渡したモデル/データを取り去った描画するコードだけしか持てないように制限すべきです。

 現在、DinnersControllerクラスによってビュー・テンプレートへ引き渡されるモデルのデータは単純かつ直接的で、Indexアクション・メソッドの場合はDinnerオブジェクトの一覧、Details/Edit/Create/Deleteアクション・メソッドの場合は1つのDinnerオブジェクトです。アプリケーションにUI機能を追加するにつれ、通常はビュー・テンプレート内で描画するデータを複数引き渡す必要があります。例えば、EditやCreateビュー内の“Country”フィールドをHTMLテキストボックスからドロップダウンリストへ変更したくなるかもしれません。ビュー・テンプレートで国名のドロップダウンリストをハード・コーディングするよりは、サポートされている国の一覧を動的に取得して生成したくなるかもしれません。コントローラからビュー・テンプレートへ、Dinnerオブジェクトおよびサポートしている国の一覧の両方を引き渡す方法が必要です。

 これを行う2つの方法を見てみましょう。

ViewDataディクショナリの使用

 コントローラの基本クラスは“ViewData”ディクショナリ・プロパティを公開しており、コントローラからビューへ追加データを引き渡すのに使用できます。

 例えば、Editビュー内の“Country”テキストボックスをHTMLテキストボックスからドロップダウンリストへ変更するケースをサポートするために、(Dinnerオブジェクトに加え)国のドロップダウンリストのモデルとして使用するSelectListオブジェクトをEditアクション・メソッドへ引き渡すように更新できます。

//
// GET: /Dinners/Edit/5

[Authorize]
public ActionResult Edit(int id) {

  Dinner dinner = dinnerRepository.GetDinner(id);

  ViewData["Countries"] = new SelectList(PhoneValidator.AllCountries, dinner.Country);

  return View(dinner);
}

 上記のSelectListのコンストラクタは、ドロップダウンリストにひも付ける国の一覧と現在の選択値を受け取ります。

 その後、以前使用していたHtml.TextBoxヘルパー・メソッドの代わりに、Html.DropDownListヘルパー・メソッドを使用するようにEdit.aspxビュー・テンプレートを更新できます。

<%= Html.DropDownList("Country", ViewData["Countries"] as SelectList) %>

 上記のHtml.DropDownListヘルパー・メソッドは2つのパラメータを取ります。1つ目は出力するHTMLフォーム要素の名前です。2つ目はViewDataディクショナリを通じて引き渡す“SelectList”モデルです。C#の“as”キーワードを使用して、ディクショナリ内の型をSelectListとしてキャストします。

 それではアプリケーションを実行して、ブラウザで/Dinners/Edit/1 URLにアクセスすると、編集UIは更新され、テキストボックスの代わりに、国のドロップダウンリストが表示されていることが分かります。


図1

 (エラー発生の場合)HTTP-POST EditメソッドからEditビュー・テンプレートの描画も行うため、エラーのシナリオでそのビュー・テンプレートが描画されるときにも、ViewDataへSelectListが追加されるように、このメソッドを更新します。

//
// POST: /Dinners/Edit/5

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

  Dinner dinner = dinnerRepository.GetDinner(id);

  try {

    UpdateModel(dinner);

    dinnerRepository.Save();

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

    ModelState.AddModelErrors(dinner.GetRuleViolations());

    ViewData["countries"] = new SelectList(PhoneValidator.AllCountries, dinner.Country);

    return View(dinner);
  }
}

 これでDinnersControllerの編集のシナリオで、ドロップダウンリストがサポートされます。

ViewModelパターンを使用

 ViewDataディクショナリの方法は、かなり速く簡単に実装できる利点があります。開発者の中には、コンパイル時に捕捉できないタイポがエラーにつながる可能性があることから、文字列ベースのディクショナリを使用したくない人もいます。型付けされていないViewDataディクショナリも、ビュー・テンプレートでは、C#のような強く型付けされた言語を使用する場合は、“as”演算子やキャストが必要です。

 使用できる別の方法は、しばしば“ViewModel”パターンとして言及されるものです。このパターンを使用するときは、特定のビューのシナリオに最適化された強く型付けされたクラス作成します。それは、ビュー・テンプレートで必要となる、動的な値/コンテンツのためのプロパティを公開します。そうするとコントローラ・クラスは、ビュー・テンプレートが使用するそれらのビュー最適化クラスをひも付けて引き渡すことができます。これにより、ビュー・テンプレート内での、タイプ・セーフ、コンパイル時チェック、エディタのIntelliSenseが実現できます。

 例えば、夕食会のフォーム編集のシナリオを可能にするために、以下のように“DinnerFormViewModel”クラスを作って、2つの強く型付けされたプロパティを公開します。Dinnerオブジェクトと国のドロップダウンリストへひも付ける必要のあるSelectListモデルです。

public class DinnerFormViewModel {

  // Properties
  public Dinner     Dinner  { get; private set; }
  public SelectList Countries { get; private set; }

  // Constructor
  public DinnerFormViewModel(Dinner dinner) {
    Dinner = dinner;
    Countries = new SelectList(PhoneValidator.AllCountries, dinner.Country);
  }
}

 その後、リポジトリから取得するDinnerオブジェクトを使用してDinnerFormViewModelを作成し、ビュー・テンプレートへそれを引き渡すように、Editアクション・メソッドを修正できます。

//
// GET: /Dinners/Edit/5

[Authorize]
public ActionResult Edit(int id) {

  Dinner dinner = dinnerRepository.GetDinner(id);

  return View(new DinnerFormViewModel(dinner));
}

 その後、以下のようにedit.aspxの一番上にある“inherits”属性を変更して、“Dinner”オブジェクトの代わりに“DinnerFormViewModel”となるようにビュー・テンプレートを修正します。

Inherits="System.Web.Mvc.ViewPage<NerdDinner.Controllers.DinnerFormViewModel>

 これを行うと、ビュー・テンプレート内の“Model”プロパティのIntelliSenseは、それに引き渡されたDinnerFormViewModel型のオブジェクトのモデルを反映するように更新されます(図2、図3)。


図2


図3

 その後、それに対応するようにビューのコードを更新できます。以下では、作成している入力要素の名前を変更していないのですが(フォームの要素は“Title”、“Country”のままです)、DinnerFormViewModelクラスを使用して値を取得するようにHTMLヘルパー・メソッドを更新している様子をご確認ください。

<p>
  <label for="Title">Dinner Title:</label>
  <%= Html.TextBox("Title", Model.Dinner.Title) %>
  <%=Html.ValidationMessage("Title", "*") %>
</p>

<p>
  <label for="Country">Country:</label>
  <%= Html.DropDownList("Country", Model.Countries) %>
  <%=Html.ValidationMessage("Country", "*") %>
</p>

 エラーが表示されるときもDinnerFormViewModelクラスを使用するよう、Editのポスト・メソッドも更新します。

//
// POST: /Dinners/Edit/5

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

  Dinner dinner = dinnerRepository.GetDinner(id);

  try {
    UpdateModel(dinner);

    dinnerRepository.Save();

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

    return View(new DinnerFormViewModel(dinner));
  }
}

 国のドロップダウンリストをそれらの中でも利用可能にするため、まったく同じDinnerFormViewModelクラスを再利用できるように、Createアクション・メソッドも更新できます。以下はHTTP-GETの実装です。

//
// GET: /Dinners/Create

public ActionResult Create() {

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

  return View(new DinnerFormViewModel(dinner));
}

 以下はHTTP-POST Createメソッドの実装です。

//
// 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.AddModelErrors(dinner.GetRuleViolations());
    }
  }

  return View(new DinnerFormViewModel(dinner));
}

 これでEditおよびCreateの両方の画面で、国の選択にドロップダウンリストがサポートされます。

独自形状のViewModelクラス

 上記のシナリオでは、DinnerFormViewModelクラスは、SelectListモデルのプロパティのサポートとともに、Dinnerモデルのオブジェクトをプロパティとして直接公開しています。ビュー・テンプレート内で作成したいHTML UIが、ドメイン・モデルのオブジェクトと比較的密接に対応しているようなシナリオでは、この方法はうまく動作します。

 そのようなケースでないシナリオでは、利用できるオプションの1つは、ビューでの利用に対して、さらに最適化されたオブジェクト・モデルとなるような、独自の形状のViewModelクラスを作成することです。それは基礎となるドメイン・モデルのオブジェクトとはまったく違う見栄えになるかもしれません。例えば、それは異なるプロパティ名を公開するかもしれないし、また複数のモデル・オブジェクトから集められたプロパティを統合したものになるかもしれません。

 独自形状のViewModelクラスは、描画するためにコントローラからビューへデータを引き渡すときと、コントローラのアクション・メソッドへ送り返すフォーム・データの処理のサポートの両方で使用できます。後者のシナリオでは、フォーム送信されたデータでアクション・メソッドがViewModelを更新してから、実際のドメイン・モデルのオブジェクトをマップしたり取得したりするために、ViewModelのインスタンスを使用する場合もあります。

 独自形状のViewModelクラスは、非常に大きな柔軟性を持っており、ビュー・テンプレート内で描画コードを見つけた場合や、アクション・メソッド内のフォーム送信コードが非常に複雑になりかけている場合に、まず吟味すべき部分です。これは、生成しているUIにあなたのドメイン・モデルがクリーンに対応しておらず、中間となる独自形状のViewModelクラスが助けになるということの表れである場合がよくあります。

次のステップ

 それでは、アプリケーションを横断してUIを再利用または共有するために、パーシャルとマスター・ページを利用する方法を見てみましょう。

[注: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 記事ランキング

本日 月間