ViewData を使用して ViewModel クラスを実装する

マイクロソフトより

PDF をダウンロードする

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

手順 6 では、より豊富なフォーム編集シナリオのサポートを有効にする方法と、コントローラーからビューにデータを渡すために使用できる 2 つの方法 (ViewData と ViewModel) についても説明します。

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

NerdDinner 手順 6: ViewData と ViewModel

さまざまなフォーム投稿シナリオについて説明し、作成、更新、削除 (CRUD) のサポートを実装する方法について説明しました。 DinnersController の実装をさらに進め、より豊富なフォーム編集シナリオのサポートを有効にします。 ここでは、コントローラーからビューにデータを渡すために使用できる 2 つの方法 (ViewData と ViewModel) について説明します。

コントローラーから View-Templates にデータを渡す

MVC パターンの定義特性の 1 つは、アプリケーションのさまざまなコンポーネント間で適用するのに役立つ厳密な "懸念事項の分離" です。 モデル、コントローラー、ビューはそれぞれ、適切に定義された役割と責任を持ち、明確に定義された方法で相互に通信します。 これは、テスト容易性とコードの再利用を促進するのに役立ちます。

コントローラー クラスは、HTML 応答をクライアントにレンダリングする場合、応答をレンダリングするために必要なすべてのデータをビュー テンプレートに明示的に渡す役割を担います。 ビュー テンプレートでは、データの取得やアプリケーション ロジックを実行しないでください。代わりに、コントローラーによって渡されるモデル/データから駆動されるレンダリング コードのみに制限する必要があります。

現在、DinnersController クラスによってビュー テンプレートに渡されるモデル データは単純で簡単です。Index() の場合は Dinner オブジェクトの一覧、Details()、Edit()、Create()、Delete() の場合は 1 つの Dinner オブジェクトです。 アプリケーションに UI 機能を追加するにつれて、多くの場合、ビュー テンプレート内で HTML 応答をレンダリングするために、このデータ以上のものを渡す必要があります。 たとえば、[編集] ビューと [作成] ビュー内の [国] フィールドを HTML テキスト ボックスからドロップダウン リストに変更できます。 ビュー テンプレートで国と地域の名前のドロップダウン リストをハードコーディングするのではなく、動的に設定するサポートされている国と地域の一覧から生成することをお勧めします。 Dinner オブジェクトとサポートされている国 地域のリストの両方をコントローラーからビュー テンプレートに渡す方法が必要です。

これを実現する 2 つの方法を見てみましょう。

ViewData ディクショナリの使用

Controller 基本クラスは、コントローラーからビューに追加のデータ項目を渡すために使用できる "ViewData" ディクショナリ プロパティを公開します。

たとえば、編集ビュー内の "Country" テキスト ボックスを HTML テキスト ボックスからドロップダウン リストに変更するシナリオをサポートするために、Edit() アクション メソッドを更新して、"Countries" ドロップダウン リストのモデルとして使用できる SelectList オブジェクトを (Dinner オブジェクトに加えて) 渡すことができます。

//
// 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 が更新され、テキスト ボックスではなく国と地域のドロップダウン リストが表示されることがわかります。

国と地域のドロップダウン リストが赤い矢印で強調表示されている編集ユーザー インターフェイスのスクリーンショット。

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

//
// 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 編集シナリオで DropDownList がサポートされるようになりました。

ViewModel パターンの使用

ViewData ディクショナリのアプローチには、非常に高速で簡単に実装できるという利点があります。 ただし、入力ミスはコンパイル時にキャッチされないエラーにつながる可能性があるため、文字列ベースのディクショナリを使用するのが好きではない開発者もいます。 型指定されていない ViewData ディクショナリでは、ビュー テンプレートで C# のような厳密に型指定された言語を使用する場合は、"as" 演算子またはキャストを使用する必要もあります。

使用できる別のアプローチは、多くの場合、"ViewModel" パターンと呼ばれます。 このパターンを使用する場合は、特定のビュー シナリオ用に最適化され、ビュー テンプレートで必要な動的な値/コンテンツのプロパティを公開する、厳密に型指定されたクラスを作成します。 コントローラー クラスは、これらのビュー最適化クラスを設定し、使用するビュー テンプレートに渡すことができます。 これにより、ビュー テンプレート内のタイプ セーフ、コンパイル時のチェック、エディターの IntelliSense が可能になります。

たとえば、ディナー フォームの編集シナリオを有効にするには、次のような "DinnerFormViewModel" クラスを作成します。このクラスでは、厳密に型指定された 2 つのプロパティ (Dinner オブジェクトと、"Countries" ドロップダウン リストを設定するために必要な 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);
    }
}

その後、Edit() アクション メソッドを更新して、リポジトリから取得した Dinner オブジェクトを使用して DinnerFormViewModel を作成し、それをビュー テンプレートに渡すことができます。

//
// 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 型のオブジェクト モデルが反映されます。

ドロップダウン リストと Dinner リスト項目が青い四角形で強調表示されているコード エディター ウィンドウのスクリーンショット。

ドロップダウン リストとアドレス リスト項目が灰色の点線の四角形で強調表示されているコード エディター ウィンドウのスクリーンショット。

次に、ビューコードを更新して、それを基に動作するようにすることができます。 作成する入力要素の名前を変更しない方法 (フォーム要素は引き続き "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 メソッドを更新します。

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

また、Create() アクション メソッドを更新して、まったく同じ DinnerFormViewModel クラスを再利用して、それらの中で "Countries" DropDownList を有効にすることもできます。 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));
}

これで、[編集] 画面と [作成] 画面の両方で、国または地域を選択するためのドロップダウン リストがサポートされるようになりました。

カスタムシェイプの ViewModel クラス

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

そうでないシナリオの場合、使用できる 1 つのオプションは、オブジェクト モデルがビューによって使用できるように最適化され、基になるドメイン モデル オブジェクトとは完全に異なる可能性があるカスタムシェイプの ViewModel クラスを作成することです。 たとえば、複数のモデル オブジェクトから収集された異なるプロパティ名や集計プロパティを公開する可能性があります。

カスタムシェイプの ViewModel クラスを使用すると、コントローラーからビューにデータを渡してレンダリングしたり、コントローラーのアクション メソッドにポストバックされたフォーム データを処理したりできます。 この後のシナリオでは、アクション メソッドで ViewModel オブジェクトをフォームにポストされたデータで更新し、ViewModel インスタンスを使用して実際のドメイン モデル オブジェクトをマップまたは取得する場合があります。

カスタムシェイプの ViewModel クラスは、非常に柔軟性を提供でき、ビュー テンプレート内のレンダリング コードやアクション メソッド内のフォーム投稿コードが複雑になり始めた場合にいつでも調査する必要があります。 これは、多くの場合、ドメイン モデルが生成する UI に適切に対応していないこと、および中間カスタムシェイプの ViewModel クラスが役立つ兆候です。

次のステップ

ここでは、部分ページとマスター ページを使用して、アプリケーション全体で UI を再利用して共有する方法を見てみましょう。