印刷用ページ       送信     
クリックして評価とフィードバックをお寄せください
 ASP.NET MVC: Web フォームを使用しないで Web アプ...
Related Articles

Microsoft の製品チームに課せられたセキュリティ開発ライフサイクル (SDL) に関する主な要件の一部についての開発者とセキュリティ専門家の会話を紹介します。

Michael Howard

MSDN Magazine May 2009

...

Read more!

.NET RIA Services には、認証、ロール、プロファイル管理などの一連のサーバー コンポーネントおよび ASP.NET の拡張機能が用意されています。ここではそのしくみについて説明します。

Jonathan Carter

MSDN Magazine May 2009

...

Read more!

ピアツーピアの処理プラットフォームを作成するデモを示します。このプラットフォームでは、複数のプレイヤーが共に機能して、共通目的 (作業を完了する) を達成できます。

Matt Neely

MSDN Magazine June 2009

...

Read more!

開発者は、ワークステーションおよび関連するクラスのバージョン管理にいつも頭を悩ませています。ワークフローのバージョン管理の主要な問題点と、ワークフロー定義、アクティビティ、およびワークフロー サービスへの変更に関する推奨事項について、Matt Milner が説明します。

Matthew Milner

MSDN Magazine May 2009

...

Read more!

Cobra は Python の子孫であり、特に、動的または静的に型指定されたプログラミング モデルを組み合わせて使用することや、組み込みの単体テスト機能、スクリプト機能、およびいくつかの契約による設計の定義が特徴です。そのすばらしい能力をご紹介します。

Ted Neward

MSDN Magazine June 2009

...

Read more!

Popular Articles

この記事では、Windows Presentation Foundation でのプログラムおよび宣言によるデータ バインドと表示の手法を説明します。

Josh Smith

MSDN Magazine July 2008

...

Read more!

James Avery does it again with his popular list of developer tools. This time he covers the best Visual Studio add-ins available today that you can download for free.

James Avery

MSDN Magazine December 2005

...

Read more!

ここでは、新しい F# 言語の基になるいくつかの概念について説明します。F# 言語は、関数型 .NET 言語とオブジェクト指向 .NET 言語の両方の要素を持っています。また、単純なプログラムを記述する方法についても説明します。

Ted Neward

MSDN Magazine Launch 2008

...

Read more!

WPF は、.NET Framework 3.0 の最も重要な新しいテクノロジの 1 つです。今月は、John Papa がそのデータ バインド機能について紹介します。

John Papa

MSDN Magazine December 2007

...

Read more!

ワンタイム パスワードは、ディクショナリ攻撃、フィッシング、傍受などのさまざまなセキュリティ違反に対するソリューションを提供します。ここでは、そのしくみを説明します。

Dan Griffin

MSDN Magazine May 2008

...

Read more!

ASP.NET MVC
Web フォームを使用しないで Web アプリケーションを作成する
Chris Tavares

この記事は、ASP.NET MVC Framework のプレリリース版に基づいて書かれています。ここに記載されている情報は変更される可能性があります。
この記事の内容 : :
  • Model View Controller パターン
  • コントローラとビューの作成
  • フォームの構築とポストバック
  • コントローラ ファクトリとその他の拡張ポイント
この記事は次のテクノロジを使用しています:
ASP.NET
コードのダウンロード : : MVCFramework2008_03.exe (189 KB)
Browse the Code Online
筆者は現時点で約 15 年の経験を持つプロフェッショナルな開発者であり、その前の少なくとも 10 年は趣味で開発を行う個人ユーザーでした。同世代のほとんどの人と同様に、8 ビット マシンから開始し、PC プラットフォームに移行しました。複雑さを増していくマシンで徐々に技術を向上させ、小さなゲームから個人データ管理、さらには外部ハードウェアの制御まで、あらゆることを行うアプリケーションを作成しました。
しかし、経歴の最初の半分については、作成したすべてのソフトウェアに共通する点が 1 つありました。それは、ユーザーのデスクトップで実行するローカル アプリケーションであることです。90 年代の初めに、World Wide Web と呼ばれる新しいものについて耳にするようになりました。当時、実際に作業現場からオフィスに足を引きずって戻らなくてもタイムカード情報を入力できるようにする Web アプリケーションを作成する機会がありました。
その経験は、一言で言えば困惑でした。ステートレス Web への取り組みは、筆者のデスクトップ指向の考え方になじみませんでした。うんざりするデバッグ、ルート アクセス権のない UNIX サーバー、そして見慣れない山かっこの中身が追加され、若かった筆者は恥ずかしながらさらに数年間はデスクトップ開発に戻りました。
筆者は Web 開発から離れました。明らかに重要であるにもかかわらず、筆者が本当に理解していなかったのは、プログラミング モデルです。その後、Microsoft® .NET Framework と ASP.NET がリリースされました。最終的に、Web アプリケーションに取り組めて、デスクトップ アプリケーションのプログラミングとほぼ同じであるフレームワークが登場しました。ウィンドウ (ページ) を作成でき、コントロールをイベントに関連付けることができ、デザイナではいまいましい山かっこを処理する必要がなくなりました。最もすばらしいのは、ASP.NET がビュー ステートで Web のステートレスな性質を自動的に処理してくれることです。筆者は幸せなプログラマに戻りました。少なくともしばらくの間は。
経験を積むにつれて、設計における選択肢も増えました。デスクトップ アプリケーションに取り組むときに適用できるいくつかのベスト プラクティスも学習しました。その 2 つは次のとおりです。
  • 懸念事項の分離 : UI ロジックと基になる動作を混在させないこと
  • 自動化された単体テスト : コードが予想どおりに動作するかどうかを確認する自動化されたテストを作成すること
ここに示した基本原則は、テクノロジに関係なく適用されます。懸念事項の分離は、複雑さを処理するのに役立つ基本的な原則です。残りの労働時間の計算、データの書式設定、グラフの描画など、異なる役割を同じオブジェクト内に混在させると、メンテナンスの問題が発生します。また、自動化されたテストは、既存のプロジェクトを更新しているときには特に、正気を保ったまま実稼働品質のコードを作成するのに不可欠です。
ASP.NET Web フォームによって非常に簡単に着手できるようになりましたが、その他の方法では、Web アプリケーションにこの設計原則を適用するのが困難でした。Web フォームは執拗なまでに UI 中心です。基本単位はページです。まず、UI をデザインし、コントロールをドラッグします。(Windows® アプリケーションに対して有効になっている Visual Basic® と同じように) アプリケーション ロジックをページのイベント ハンドラに渡すだけでよいのは非常に魅力的です。
それに加え、ページの単体テストは難しい場合がほとんどです。ASP.NET のすべてをスピンアップせずに、ライフサイクル全体を通して Page オブジェクトを実行することはできません。HTTP 要求をサーバーに送信するかブラウザを自動化することで Web アプリケーションをテストすることもできますが、その種のテストは脆弱で (1 つのコントロール ID を変更するとテストが無効になります)、設定が難しく (すべての開発者のマシンで完全に同じようにサーバーを設定する必要があります)、実行が低速です。
より高度な Web アプリケーションの構築を開始したとき、コントロール、ビュー ステート、ページ ライフサイクルなど、Web フォームが提供する抽象化は役に立つどころか苛立つものでした。データ バインドの構成 (および正しく構成するための多数のイベント ハンドラの作成) には非常に多くの時間を費やしました。ページを高速に読み込むためにビュー ステートのサイズを削減する方法を調べる必要がありました。Web フォームでは、物理的なファイルがすべての URL に存在する必要があります。これは動的サイト (wiki など) によって難しくなります。また、カスタム WebControl を正しく作成するのは際立って複雑なプロセスであり、ページ ライフサイクルと Visual Studio® デザイナの両方に関する広範囲の知識が必要です。
マイクロソフトで働くようになってから、.NET のさまざまな苦痛の種について学んだことを共有し、その苦痛のいくつかを和らげることを期待できる機会がありました。最近のこのような機会は、patterns & practices Web Client Software Factory プロジェクト (codeplex.com/websf) に開発者として参加することで生じました。特に、patterns & practices が成果物に組み込んだものの 1 つは、自動化された単体テストです。Web Client Software Factory で、筆者は、Model View Presenter (MVP) パターンを使用してテスト可能な Web フォームを構築することを提案しました。
簡単に言うと、ロジックをページに挿入する代わりに、MVP でページを作成し、ページ (ビュー) が Presenter という別のオブジェクトを呼び出すようにします。Presenter オブジェクトは、通常は他のオブジェクト (Model など) を使用してデータベースにアクセスしたり、ビジネス ロジックを実行したりすることなどによって、ビューに対するアクティビティに応答するために必要なロジックを実行します。これらの手順が完了すると、Presenter はビューを更新します。このアプローチによって、プレゼンタが ASP.NET パイプラインから分離されるため、テストが容易になります。インターフェイスを通じてビューと通信し、ページから分離してテストできます。
MVP は動作しますが、実装は少し厄介な場合があります。独立したビュー インターフェイスが必要であり、分離コード ファイルに多数のイベント転送関数を作成する必要があります。しかし、Web フォーム アプリケーションにテストが容易な UI が必要な場合には、やってみるのが最善です。改良には、基礎となるプラットフォームの変更が必要です。

Model View Controller パターン
さいわい、ASP.NET チームは筆者のような開発者の話に耳を傾け、新しい Web アプリケーション フレームワークの開発を開始しました。このフレームワークは、皆さんがよくご存知で好んでいる Web フォームと共存しますが、明らかに異なる設計目標を持っています。
  • HTTP と HTML を利用し、これを隠さない。
  • テスト容易性が最初から組み込まれている。
  • ほとんどすべてのポイントで拡張できる。
  • 出力を完全に制御できる。
この新しいフレームワークは、Model View Controller (MVC) パターンに基づいているため、ASP.NET MVC という名前が付いています。MVC パターンは、元々は 70 年代に Smalltalk の一部として考案されました。この記事で示すように、実際に Web の性質に非常によく適合します。MVC は、UI を 3 つの異なるオブジェクトに分割します。それは、入力を受け取って処理するコントローラ、ドメイン ロジックを含むモデル、および出力を生成するビューです。Web のコンテキストでは、入力は HTTP 要求であり、要求フローは図 1 のようになります。
Figure 1 MVC パターンの要求フロー (画像を拡大するには、ここをクリックします)
これは、実際には Web フォームのプロセスとかなり異なります。Web フォーム モデルでは、入力はページ (ビュー) に送られ、ビューは入力の処理と出力の生成の両方を行います。一方 MVC の場合は、役割が分かれています。
ここで、おそらく皆さんの頭に浮かんでいるのは次の 2 つのいずれかでしょう。「それはすばらしい。どうやって使うのかな」でしょうか。それとも、「以前は 1 つしか作成する必要のなかったオブジェクトをどうして 3 つも作成しなければならないのだろうか」でしょうか。どちらもすばらしい疑問であり、例を見るのが最もよい説明になります。そのため、MVC Framework を使用した小さな Web アプリケーションを作成して、その利点を示します。

コントローラを作成する
先に進むためには、Visual Studio 2008 をインストールし、MVC Framework を入手する必要があります。この記事の執筆時点では、2007 年 12 月の ASP.NET Extensions の Community Technology Preview (CTP) の一部として入手できます (asp.net/downloads/3.5-extensions)。非常に有用なヘルパ オブジェクトを含む拡張 CTP と MVC Toolkit の両方を入手できます。CTP をダウンロードしてインストールした後で、ASP.NET MVC Web アプリケーションという新しいプロジェクトの種類が [新しいプロジェクト] ダイアログに作成されます。
MVC Web アプリケーション プロジェクトを選択すると、通常の Web サイトまたはアプリケーションとは少々異なる外観のソリューションが与えられます。ソリューション テンプレートは、(図 2 に示すように) いくつかの新しいディレクトリと共に Web アプリケーションを作成します。具体的には、Controllers ディレクトリにはコントローラ クラスが含まれ、Views ディレクトリ (およびそのすべてのサブディレクトリ) にはビューが含まれます。
Figure 2 MVC プロジェクトの構造 
これから、URL で渡された名前を返す非常に単純なコントロールを作成します。[コントローラ] フォルダを右クリックし、[項目の追加] を選択すると、MVC Controller クラスやいくつかの MVC View コンポーネントなどが新たに追加された通常の [新しい項目の追加] ダイアログが表示されます。ここでは、非常に想像力に富んだ HelloController という名前のクラスを追加します。
using System;
using System.Web;
using System.Web.Mvc;

namespace HelloFromMVC.Controllers
{
    public class HelloController : Controller
    {
        [ControllerAction]
        public void Index()
        {
            ...
        }
    }
}
コントローラ クラスは、ページよりもずっと軽量です。実際に、本当に必要なのは、System.Web.Mvc.Controller から派生させ、アクション メソッドに [ControllerAction] 属性を追加することだけです。アクションは、特定の URL に対する要求への応答として呼び出されるメソッドです。アクションは、必要な処理の実行と、ビューのレンダリングを行います。まず、次に示すように、ビューに名前を渡す単純なアクションを作成します。
[ControllerAction]
 public void HiThere(string id)
 {
     ViewData["Name"] = id;
     RenderView("HiThere");
 }
アクション メソッドは、id パラメータを通じて URL から名前を受け取り (詳細は後述します)、それを ViewData コレクションに格納してから、HiThere という名前のビューをレンダリングします。
このメソッドの呼び出し方法やビューの外観を説明する前に、テストの容易性について説明します。Web フォーム ページ クラスをテストするのがどれほど難しいかについての筆者の発言を覚えていますか。そうです、コントローラの方がはるかに簡単にテストできます。実際に、追加のインフラストラクチャがなくても、コントローラを直接インスタンス化し、アクション メソッドを呼び出すことができます。HTTP コンテキストは不要で、単なるテスト ハーネスとしてのサーバーも不要です。例として、このクラスの Visual Studio Team System (VSTS) 単体テストを図 3 に示します。
namespace HelloFromMVC.Tests
{
    [TestClass]
    public class HelloControllerFixture
    {
        [TestMethod]
        public void HiThereShouldRenderCorrectView()
        {
            TestableHelloController controller = new 
              TestableHelloController();
            controller.HiThere("Chris");

            Assert.AreEqual("Chris", controller.Name);
            Assert.AreEqual("HiThere", controller.ViewName);
        }

    }

    class TestableHelloController : HelloController
    {
        public string Name;
        public string ViewName;

        protected override void RenderView(
            string viewName, string master, object data)
        {
            this.ViewName = viewName;
            this.Name = (string)ViewData["Name"];
        }
    }

}

ここで行うことはいくつかあります。実際のテストは非常に簡単です。コントローラをインスタンス化し、予期されるデータでメソッドを呼び出し、正しいビューがレンダリングされたことを確認します。RenderView メソッドを上書きするテスト固有のサブクラスを作成することで、この確認を行います。これにより、実際の HTML 作成を省略できます。留意するのは、正しいデータがビューに送信されたことと、正しいビューがレンダリングされたことだけです。このテストでは、ビューの基礎となる詳細は気にしません。

ビューを作成する
もちろん最終的にはなんらかの HTML を生成する必要があるため、その HiThere ビューを作成しましょう。そのためには、まずソリューションで Views フォルダの下に Hello という名前の新しいフォルダを作成します。既定では、コントローラは Views\<ControllerPrefix> フォルダ内でビューを探します (コントローラ プレフィックスは、コントローラ クラスの名前から "Controller" という単語を取り除いたものです)。したがって、HelloController によってレンダリングされるビューの場合は、Views\Hello を探します。最終的に、ソリューションは図 4 のようになります。
Figure 4 プロジェクトへのビューの追加 (画像を拡大するには、ここをクリックします)
ビューの HTML は次のようになります。
<html  >
<head runat="server">
    <title>Hi There!</title>
</head>
<body>
    <div>
        <h1>Hello, <%= ViewData["Name"] %></h1>
    </div>
</body>
</html>
いくつかのことが目に留まります。runat="server" タグがありません。form タグもありません。コントロール宣言もありません。実際に、これは ASP.NET よりも従来の ASP によく似ています。MVC ビューは出力の生成のみ行うため、Web フォーム ページが行うイベント処理や複雑な制御は不要です。
MVC Framework は、便利なテキスト テンプレート作成言語として .aspx ファイル形式を借用します。必要に応じて分離コードも使用できますが、既定では分離コード ファイルは次のようになります。
using System;
using System.Web;
using System.Web.Mvc;

namespace HelloFromMVC.Views.Hello
{
    public partial class HiThere : ViewPage
    {
    }
}
ページの Init メソッドや load メソッドはなく、イベント ハンドラもありません。基本クラスの宣言以外には何もありませんが、この基本クラスは Page ではなく ViewPage です。MVC ビューに必要なものはこれがすべてです。アプリケーションを実行し、http://localhost:<port>/Hello/HiThere/Chris にナビゲートすると、図 5 のような画面が表示されます。
Figure 5 正常な MVC ビュー (画像を拡大するには、ここをクリックします)
図 5 の代わりに、厄介に見える例外が発生しても慌てないでください。F5 キーを押したときに Visual Studio にアクティブなドキュメントとして HiThere.aspx ファイル セットがある場合は、Visual Studio が .aspx ファイルに直接アクセスしようとします。MVC ビューでは、表示の前にコントローラを実行する必要があるため、ページに直接ナビゲートしようとしても動作しません。図 5 に表示されている内容に合わせて URL を編集すると、正常に動作します。
MVC Framework は、アクション メソッドの呼び出し方法をどのようにして知るのでしょうか。その URL のファイル拡張子さえありませんでした。その答えは URL ルーティングです。global.asax.cs ファイルの内部を見ると、図 6 のコード チャンクがあります。グローバル RouteTable は、Route オブジェクトのコレクションを格納します。各 Route は、URL フォームと、そのフォームで行う処理を記述します。既定では、2 つのルートがテーブルに追加されます。最初のルートは魔法のような処理を行います。つまり、サーバー名の後に 3 つの部分が続く各 URL について、最初の部分がコントローラ名、2 番目の部分がアクション名、3 番目の部分が ID パラメータとして取得されます。
public class Global : System.Web.HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        // Change Url= to Url="[controller].mvc/[action]/[id]" 
        // to enable automatic support on IIS6 

        RouteTable.Routes.Add(new Route
        {
            Url = "[controller]/[action]/[id]",
            Defaults = new { action = "Index", id = (string)null },
            RouteHandler = typeof(MvcRouteHandler)
        });

        RouteTable.Routes.Add(new Route
        {
            Url = "Default.aspx",
            Defaults = new { 
                controller = "Home", 
                action = "Index", 
                id = (string)null },
            RouteHandler = typeof(MvcRouteHandler)
        });
    }
}

Url = "[controller]/[action]/[id]"
この既定のルートは、HiThere メソッドで有効にされて呼び出されます。http://localhost/Hello/HiThere/Chris という URL を覚えていますか。このルートは Hello をコントローラにマップし、HiThere をアクションにマップし、Chris を ID にマップしました。MVC Framework は HelloController インスタンスを作成し、HiThere メソッドを呼び出し、Chris を ID パラメータの値として渡しました。
この既定のルートは多くの機能を与えてくれますが、独自のルートを追加することもできます。たとえば、パーソナライズされたあいさつ文に対して自分の名前を入力することのみ必要な非常にフレンドリーなサイトが必要だとします。このルートをルーティング テーブルの一番上に追加します。
  RouteTable.Routes.Add(new Route
  {
    Url = "[id]",
    Defaults = new { 
        controller = "Hello", 
        action = "HiThere" },
    RouteHandler = typeof(MvcRouteHandler)
  });
すると、http://localhost/Chris にアクセスするだけでアクションが呼び出され、見慣れたフレンドリーなあいさつ文が表示されます。
システムは、呼び出すコントローラとアクションをどのようにして知ったのでしょうか。その答えは Defaults パラメータです。このパラメータは、新しい C# 3.0 匿名型を使用して擬似辞書を作成します。Route の Defaults オブジェクトには任意の追加情報を含めることができますが、MVC の場合は、いくつかの既知のエントリを含めることもできます。それは、コントローラとアクションです。URL で指定されたコントローラまたはアクションがない場合は、Defaults 内の名前が使用されます。そのため、これらを URL から省いても、要求は正しいコントローラとアクションにマッピングされます。
注意しておく点がもう 1 つあります。「テーブルの一番上に追加する」と言ったのを覚えていますか。一番下に追加するとエラーが発生します。ルーティングは上から順に機能します。URL を処理するときに、ルーティング システムはテーブルを上から順に調べ、最初に一致したルートが使用されます。この場合は、アクションと ID の既定値があるため、既定のルート "[controller]/[action]/[id]" が一致します。したがって、ChrisController を探し、コントローラがないためエラーになります。

より大きな例
MVC Framework の基礎については説明したので、文字列を表示する以外のことを行うより大きな例を示します。wiki はブラウザで編集できる Web サイトです。ページは簡単に追加または編集できます。MVC Framework を使用して小さなサンプル wiki を作成しました。[Edit this page] 画面を図 7 に示します。
Figure 7 ホーム ページの編集 (画像を拡大するには、ここをクリックします)
この記事のコードをダウンロードして、基になる wiki ロジックがどのように実装されているかを確認できます。ここでは、MVC Framework によって Web への wiki の表示がどれほど簡単になるかに注目します。まず、URL 構造を設計します。目的は次のとおりです。
  • /[pagename] は、その名前のページを表示します。
  • /[pagename]?version=n は、要求されたバージョンのページを表示します。0 = 最新バージョン、1 = 1 つ前のバージョンなどです。
  • /Edit/[pagename] は、そのページの編集画面を開きます。
  • /CreateNewVersion/[pagename] は、編集内容を送信するためにポストされる URL です。
wiki ページの基本的な表示から始めましょう。これを行うために、WikiPageController という名前の新しいクラスを作成しました。次に、ShowPage という名前のアクションを追加しました。WikiPageController は、最初は図 8 のようになります。ShowPage メソッドはかなり単純です。WikiSpace クラスと WikiPage クラスは、それぞれ一連の wiki ページと特定のページ (およびそのリビジョン) を表します。このアクションは、単にモデルを読み込んで RenderView を呼び出します。しかし、そこにある "new WikiPageViewData" 行は何でしょうか。
public class WikiPageController : Controller 
{
  ISpaceRepository repository;

  public ISpaceRepository Repository 
  {
    get {
      if (repository == null) 
      {
        repository = new FileBasedSpaceRepository(
            Request.MapPath("~/WikiPages"));
      }
      return repository;
    }

    set { repository = value; }
  }

  [ControllerAction]
  public void ShowPage(string pageName, int? version) 
  {
    WikiSpace space = new WikiSpace(Repository);
    WikiPage page = space.GetPage(pageName);

    RenderView("showpage", 
      new WikiPageViewData 
      { 
        Name = pageName,
        Page = page,
        Version = version ?? 0 
      });
  }
}

前の例では、コントローラからビューにデータを受け渡す 1 つの方法を示しました。それは ViewData 辞書です。辞書は便利ですが、危険もあります。辞書には何でも含めることができ、コンテンツに対する IntelliSense® はありません。また、ViewData 辞書はそのコンテンツを使用するために Dictionary<string, object> 型となっているため、すべてをキャストする必要があります。
ビューに必要なデータがわかっている場合は、代わりに厳密に型指定された ViewData オブジェクトを渡すことができます。この例では、図 9 に示すように、WikiPageViewData という単純なオブジェクトを作成しました。このオブジェクトは、wiki マークアップの HTML バージョンの取得などを行ういくつかのユーティリティ メソッドと共に wiki ページ情報をビューに渡します。
public class WikiPageViewData {

    public string Name { get; set; }
    public WikiPage Page { get; set; }
    public int Version { get; set; }

    public WikiPageViewData() {
        Version = 0;
    }

    public string NewVersionUrl {
        get {
            return string.Format("/CreateNewVersion/{0}", Name);
        }
    }

    public string Body {
        get { return Page.Versions[Version].Body; }
    }

    public string HtmlBody {
        get { return Page.Versions[Version].BodyAsHtml(); }
    }

    public string Creator {
        get { return Page.Versions[Version].Creator; }
    }

    public string Tags {
        get { return string.Join(",", Page.Versions[Version].Tags); }
    }
}

これでビュー データが定義されましたが、どのように使用するのでしょうか。ShowPage.aspx.cs を見るとわかります。
namespace MiniWiki.Views.WikiPage {
    public partial class ShowPage : ViewPage<WikiPageViewData>
    {
    }
}
基本クラスを ViewPage<WikiPageViewData> 型に定義したことに注目してください。これは、ページの ViewData プロパティが WikiPageViewData 型であり、前の例のような Dictionary ではないことを意味します。
.aspx ファイル内の実際のマークアップは非常に単純です。
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
  AutoEventWireup="true" CodeBehind="ShowPage.aspx.cs" 
  Inherits="MiniWiki.Views.WikiPage.ShowPage" %>
<asp:Content 
  ID="Content1"  
  ContentPlaceHolderID="MainContentPlaceHolder" 
  runat="server">
  <h1><%= ViewData.Name %></h1>
  <div id="content" class="wikiContent">
    <%= ViewData.HtmlBody %>
  </div>
</asp:Content>
ViewData を参照するときに、インデックス付け演算子 [] を使用していないことに注意してください。代わりに、厳密に型指定された ViewData があるため、プロパティに直接アクセスできます。キャストは不要であり、Visual Studio が IntelliSense を提供します。
注意してみると、このファイルに <asp:Content> タグがあることがわかります。そうです。マスタ ページは MVC ビューと連動します。そして、マスタ ページはビューになることもできます。マスタ ページ分離コードを見てみましょう。
namespace MiniWiki.Views.Layouts
{
    public partial class Site :  
        System.Web.Mvc.ViewMasterPage<WikiPageViewData>
    {
    }
}
関連付けられているマークアップを図 10 に示します。ここでは、マスタ ページはビューが取得するのとまったく同じ ViewData オブジェクトを取得します。マスタ ページの基本クラスは ViewMasterPage<WikiPageViewData> として宣言したため、適切な型の ViewData があります。その後、さまざまな DIV タグを設定してページをレイアウトし、バージョン リストを入力し、通常のコンテンツ プレースホルダで終わります。
<%@ Master Language="C#" 
  AutoEventWireup="true" 
  CodeBehind="Site.master.cs" 
  Inherits="MiniWiki.Views.Layouts.Site" %>
<%@ Import Namespace="MiniWiki.Controllers" %>
<%@ Import Namespace="MiniWiki.DomainModel" %>
<%@ Import Namespace="System.Web.Mvc" %>
<html >
<head runat="server">
  <title><%= ViewData.Name %></title>
  <link href="http://../../Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <div id="inner">
    <div id="top">
      <div id="header">
        <h1><%= ViewData.Name %></h1>
      </div>
      <div id="menu">
        <ul>
          <li><a href="http://Home">Home</a></li>
          <li>
            <%= Html.ActionLink("Edit this page", 
                  new { controller = "WikiPage", 
                        action = "EditPage", 
                        pageName = ViewData.Name })%>
        </ul>
      </div>
    </div>
    <div id="main">
      <div id="revisions">
        Revision history:
        <ul>
          <% 
            int i = 0;
            foreach (WikiPageVersion version in ViewData.Page.Versions)
            { %>
              <li>
                <a href="http://<%= ViewData.Name %>?version=<%= i %>">
                  <%= version.CreatedOn %>
                  by
                  <%= version.Creator %>
                </a>
              </li>
          <%  ++i;
          } %>
        </ul>
      </div>
      <div id="maincontent">
        <asp:ContentPlaceHolder 
          ID="MainContentPlaceHolder" 
          runat="server">
        </asp:ContentPlaceHolder>
      </div>
    </div>
  </div>
</body>
</html>

また、Html.ActionLink の呼び出しにも注目してください。これはレンダリング ヘルパの例です。さまざまなビュー クラスには、Html および Url という 2 つのプロパティがあります。それぞれに、HTML のチャンクを出力する便利なメソッドがあります。この場合は、Html.ActionLink がオブジェクトを取得し (ここでは匿名型)、ルーティング システムを通じて実行します。これにより、指定したコントローラとアクションにルーティングする URL が生成されます。このようにして、ルートをどのように変更しても、[Edit this page] リンクは常に正しい場所を指します。
また、リンク (以前のページ バージョンへのリンク) を手動で構築する手段も必要であることがわかります。残念ながら、現在のルーティング システムは、クエリ文字列が含まれる URL の生成に対してうまく機能しません。この問題はフレームワークの今後のバージョンで修正されます。

フォームを作成し、ポストバックする
では、コントローラに対する EditPage アクションを見てみましょう。
[ControllerAction]
public void EditPage(string pageName)
{
  WikiSpace space = new WikiSpace(Repository);
  WikiPage page = space.GetPage(pageName);

  RenderView("editpage", 
    new WikiPageViewData { 
      Name = pageName, 
      Page = page });
}
この場合も、アクションはあまり処理を行いません。与えられたページでビューをレンダリングするだけです。図 11 に示すビューはもっとおもしろくなります。このファイルは HTML フォームを構築していますが、Runat="server" はありません。Url.Action ヘルパは、フォームがポストバックする URL の生成に使用されます。TextBox、TextArea、SubmitButton などのさまざまな HTML ヘルパもいくつか使用されています。これらはほぼ期待どおりのことを行います。つまり、さまざまな入力フィールドの HTML を生成します。
<%@ Page Language="C#" 
  MasterPageFile="~/Views/Shared/Site.Master" 
  AutoEventWireup="true" 
  CodeBehind="EditPage.aspx.cs" 
  Inherits="MiniWiki.Views.WikiPage.EditPage" %>
<%@ Import Namespace="System.Web.Mvc" %>
<%@ Import Namespace="MiniWiki.Controllers" %>
<asp:Content ID="Content1" 
  ContentPlaceHolderID="MainContentPlaceHolder" 
  runat="server">
  <form action="<%= Url.Action(
    new { controller = "WikiPage", 
    action = "NewVersion", 
    pageName = ViewData.Name })%>" method=post>
    <%
      if (ViewContext.TempData.ContainsKey("errors"))
      {
    %>
    <div id="errorlist">
      <ul>
      <%
        foreach (string error in 
          (string[])ViewContext.TempData["errors"])
        {
      %>
        <li><%= error%></li>
      <% } %>
      </ul>
    </div>
    <% } %>
    Your name: <%= Html.TextBox("Creator",
                   ViewContext.TempData.ContainsKey("creator") ? 
                   (string)ViewContext.TempData["creator"] : 
                   ViewData.Creator)%>
    <br />
    Please enter your updates here:<br />
    <%= Html.TextArea("Body", ViewContext.TempData.ContainsKey("body") ? 
        (string)ViewContext.TempData["body"] : 
        ViewData.Body, 30, 65)%>
    <br />
    Tags: <%= Html.TextBox(
              "Tags", ViewContext.TempData.ContainsKey("tags") ? 
              (string)ViewContext.TempData["tags"] : 
              ViewData.Tags)%>
    <br />
    <%= Html.SubmitButton("SubmitAction", "OK")%>
    <%= Html.SubmitButton("SubmitAction", "Cancel")%>
  </form>
</asp:Content>

Web プログラミングで処理するのが面倒なものの 1 つに、フォーム上のエラーがあります。より具体的に言うと、エラー メッセージを表示する一方で、前に入力されたデータを保持する必要があります。私たちは皆、35 個のフィールドのあるフォームで間違いを犯し、大量のエラー メッセージと新しい空白フォームのみ表示されたという経験を持ちます。MVC Framework は、以前に入力された情報を格納してフォームを再設定できるようにするための場所として TempData を提供しています。ViewState では、コントロールのコンテンツの保存がほぼ自動化されているため、Web フォームでのこの処理が非常に簡単になります。
これは MVC でも行う必要があり、ここで TempData が役立ちます。TempData は、型のない ViewData とよく似た辞書です。ただし、TempData のコンテンツは 1 つの要求に対してのみ存続し、その後は削除されます。この使用方法を確認するために、図 12 の NewVersion アクションを見てみましょう。
[ControllerAction]
public void NewVersion(string pageName) {
  NewVersionPostData postData = new NewVersionPostData();
  postData.UpdateFrom(Request.Form);

  if (postData.SubmitAction == "OK") {
    if (postData.Errors.Length == 0) {
      WikiSpace space = new WikiSpace(Repository);
      WikiPage page = space.GetPage(pageName);
      WikiPageVersion newVersion = new WikiPageVersion(
        postData.Body, postData.Creator, postData.TagList);
      page.Add(newVersion);
    } else {
      TempData["creator"] = postData.Creator;
      TempData["body"] = postData.Body;
      TempData["tags"] = postData.Tags;
      TempData["errors"] = postData.Errors;

      RedirectToAction(new { 
        controller = "WikiPage", 
        action = "EditPage", 
        pageName = pageName });
      return;
    }
  }

  RedirectToAction(new { 
    controller = "WikiPage",
    action = "ShowPage", 
    pageName = pageName });
}

まず、NewVersionPostData オブジェクトを作成します。これは、ポストといくつかの検証のコンテンツを格納するプロパティとメソッドを持つ別のヘルパ オブジェクトです。postData オブジェクトを読み込むために、MVC Toolkit のヘルパ関数を使用しています。UpdateFrom は、実際にはツールキットによって提供されている拡張メソッドであり、リフレクションを使用してフォーム フィールドの名前をオブジェクトのプロパティの名前と照合します。最終的に、すべてのフィールド値が postData オブジェクトに読み込まれます。ただし、UpdateFrom を使用すると、HttpRequest からフォーム データを直接取得するため単体テストが困難になるというデメリットがあります。
NewVersion が最初にチェックするのは SubmitAction です。ユーザーが [OK] ボタンをクリックし、編集したページを実際にポストする場合はこれでかまいません。他の値がここにある場合は、アクションが最終的に ShowPage にリダイレクトされるため、元のページが再表示されるだけです。
ユーザーが [OK] をクリックした場合は、postData.Errors プロパティをチェックします。これによって、ポストのコンテンツに対して単純な検証が実行されます。エラーがない場合は、ページの新しいバージョンを wiki に記述するための処理を行います。一方、エラーがある場合はおもしろいことになります。
エラーがある場合は、TempData 辞書のさまざまなフィールドを設定して、PostData のコンテンツが含まれるようにします。次に、[Edit] ページにリダイレクトします。ここで、TempData が設定されているため、ページには、ユーザーが前回ポストした値で初期化されたフォームが再表示されます。
ポスト、検証、および TempData を処理するこのプロセスは、現在は少々面倒で、実際に必要であるより多くの手動処理が必要です。将来のリリースには、TempData のチェックの少なくとも一部を自動化するヘルパ メソッドが含まれるようになります。最後に TempData について注意しておきます。TempData のコンテンツはユーザーのサーバー側セッションに格納されます。セッションをオフにした場合は TempData が動作しません。

コントローラの作成
wiki の基礎は動作するようになりましたが、実装には、先に進む前にクリーンアップが必要な未完成の点がいくつかあります。たとえば、Repository プロパティは wiki のロジックを物理的な記憶域と分離するために使用されます。ファイル システム (ここで使用しました)、データベース、またはその他任意の目的の場所にコンテンツを格納するリポジトリを提供できます。あいにく、解決が必要な問題が 2 つ生じました。
まず、コントローラ クラスは具象 FileBasedSpaceRepository クラスと緊密に結合されています。プロパティが設定されていない場合でも適切に使用できるものが存在するように、既定値が必要です。さらに悪いことに、ディスク上のファイルへのパスもここでハードコーディングされています。少なくとも、この内容は構成から取得する必要があります。
第 2 に、リポジトリは実際には必須の依存関係です。オブジェクトはリポジトリがないと動作しません。優れた設計では、リポジトリをプロパティではなくコンストラクタ パラメータにする必要があることが指定されます。ただし、MVC Framework ではコントローラに引数なしのコンストラクタが必要であるため、これをコンストラクタに追加できません。
さいわい、この束縛から逃れることのできる拡張フックがあります。それはコントローラ ファクトリです。コントローラ ファクトリは、名前が示すとおりのことを行います。つまり、Controller インスタンスを作成します。必要なのは、IControllerFactory インターフェイスを実装し、それを MVC システムに登録するクラスを作成することだけです。コントローラ ファクトリは、すべてのコントローラまたは特定の種類のコントローラに対して登録できます。図 13 に、リポジトリをコンストラクタ パラメータとして渡す WikiPageController のコントローラ ファクトリを示します。
public class WikiPageControllerFactory : IControllerFactory {

  public IController CreateController(RequestContext context, 
    Type controllerType)
  {
    return new WikiPageController(
      GetConfiguredRepository(context.HttpContext.Request));
  }

  private ISpaceRepository GetConfiguredRepository(IHttpRequest request)
  {
    return new FileBasedSpaceRepository(request.MapPath("~/WikiPages"));
  }
}

この場合、実装はきわめて簡単ですが、これにより、(依存関係注入コンテナの場合は特に) はるかに強力なツールを使用するコントローラを作成できます。いずれにしても、コントローラの依存関係をオブジェクトに分離して管理と保守を簡単にすることについて詳細をすべて入手できました。
この作業の最後の手順は、フレームワークへのファクトリの登録です。この処理は、Global.asax.cs の Application_Start メソッド (ルートの前または後) に次の行を追加することにより、ControllerBuilder クラスを通じて行います。
ControllerBuilder.Current.SetControllerFactory(
  typeof(WikiPageController), typeof(WiliPageControllerFactory));
これにより、WikiPageController のファクトリが登録されます。このプロジェクトに他のコントローラがある場合、それらはこのファクトリを使用しません。このファクトリは、WikiPageController 型についてのみ登録されているためです。すべてのコントローラに使用するファクトリを設定する必要がある場合は、SetDefaultControllerFactory を呼び出すこともできます。

その他の拡張ポイント
コントローラ ファクトリは、フレームワーク拡張の始まりでしかありません。この記事ではそのすべてを詳細に説明するスペースがないため、ハイライトのみ示します。まず、HTML 以外のものを出力する場合、または Web フォーム以外のテンプレート作成エンジンを使用する場合は、コントローラの ViewFactory を他のものに設定できます。IViewFactory インターフェイスを実装し、出力の生成方法を完全に制御できます。これは、RSS、XML、またはグラフィックスの生成に役立ちます。
既に説明したように、ルーティング システムは柔軟性に優れています。ただし、ルーティング システムには MVC に固有のものがありません。すべてのルートには RouteHandler プロパティがあります。今までは、これを常に MvcRouteHandler に設定していました。しかし、IRouteHandler インターフェイスを実装し、ルーティング システムを他の Web テクノロジにフックできます。フレームワークの将来のドロップには WebFormsRouteHandler が付属し、将来は他のテクノロジが汎用ルーティング システムを利用できるようになります。
コントローラは、System.Web.Mvc.Controller から派生する必要がありません。コントローラが行う必要があるのは、IController インターフェイスの実装だけです。このインターフェイスには、Execute という単一のメソッドだけがあります。そこから、どのような操作でも行うことができます。一方、Controller 基本クラスの動作のいくつかを調整するだけの場合、Controller にはオーバーライドできる多くの仮想関数があります。
  • OnPreAction、OnPostAction、および OnError では、実行されるすべてのアクションに対して汎用的な前処理および後処理をフックします。OnError では、コントローラ全体のエラー処理メカニズムが提供されます。
  • HandleUnknownAction は、コントローラがルートで要求されているアクションを実装していない場合に、URL がコントローラにルーティングされたときに呼び出されます。既定では、このメソッドは例外をスローしますが、任意の処理を行うようにオーバーライドできます。
  • InvokeAction は、呼び出すアクション メソッドを調べて、そのアクション メソッドを呼び出すメソッドです。(たとえば [ControllerAction] 属性の必要性をなくすために) プロセスをカスタマイズする場合は、このメソッドで行います。
Controller には他にもいくつかの仮想メソッドがありますが、基本的には拡張ポイントとしてではなくテスト フックとして存在しています。たとえば、RedirectToAction は、実際にはリダイレクトしない派生クラスを作成できるため仮想的です。これにより、完全な Web サーバーを実行しなくても、リダイレクトするアクションをテストできます。

Web フォームとはお別れか
この時点で、「Web フォームはどうなるのか。MVC によって置換されるのか」と疑問を感じているかもしれません。答えはノーです。Web フォームは汎用的なテクノロジであり、マイクロソフトはサポートと拡張を継続します。Web フォームが非常によく機能する多くのアプリケーションがあります。たとえば、典型的なイントラネット データベース レポート作成アプリケーションは、Web フォームを使用すると、MVC での作成に要する時間の数分の 1 で作成できます。さらに、Web フォームは広大なコントロールの市場をサポートしています。コントロールの多くは非常に洗練されており、作業時間を大幅に節約できます。
それでは、Web フォームではなく MVC を選択する必要があるのはどのような場合でしょうか。たいていは、要件と優先事項によって決まります。意図したとおりの URL を生成するのに苦労していますか。UI を単体テストしますか。このいずれかのシナリオでは、MVC に軍配が上がります。一方、編集可能なグリッドと手の込んだツリー ビュー コントロールを使用して大量のデータ表示を行いますか。その場合は、おそらく今のところは Web フォームのほうがよいでしょう。
いずれ MVC Framework は UI コントロール部門で遅れを取り戻すと思われますが、機能のほとんどが単なるドラッグ アンド ドロップである Web フォームほど簡単に使い始められるようにはならない可能性が高いでしょう。しかし、ASP.NET MVC Framework は Web 開発者に Microsoft .NET Framework で Web アプリケーションを構築する新しい方法を提供します。Framework はテストを容易にするように設計され、抽象化を試みる代わりに HTTP を利用し、あらゆる点で拡張可能です。Web アプリケーションの完全な制御を望む開発者にとっては、Web フォームを強力に補完します。

Chris Tavares は、マイクロソフトの patterns & practices チームの開発者であり、マイクロソフト プラットフォームにシステムを構築するためのベスト プラクティスを開発コミュニティが理解できるよう努めています。また、ASP.NET MVC チームの仮想メンバでもあり、新しいフレームワークの設計を支援しています。連絡先は cct@tavaresstudios.com です。

Page view tracker