次の方法で共有


Cutting Edge

ASP.NET 4.0 の Web フォームやその他の機能の詳細

Dino Esposito

ASP.NET は、機能が豊富で強力な Web アプリケーションを作成するためのプラットフォームとしては安定していて、既に成熟の域に達しているため、新しく魅力的な機能セットが追加されることはなかなか想像できません。しかし、昨年秋の ASP.NET 3.5 Service Pack 1 のリリースに伴い、マイクロソフトは、データ ドリブン アプリケーションやデータ入力アプリケーションのニーズに合うように特別にデザインされたコンポーネントの新しいフレームワークとして Dynamic Data コントロールを出荷することによって、プラットフォーム組み込みの AJAX サポートを強化し、その生産性を向上しました。

同時に、ASP.NET MVC と呼ばれる、まったく新しいプログラミング モデルを開発しました。ASP.NET MVC を使用すると、従来の Web フォーム モデルとは異なり、広く認知されているデザイン パターンであるモデル ビュー コントローラー (MVP: Model View Controller) に従って Web アプリケーションを作成できるようになります。
現状、ASP.NET プラットフォーム全体は、Web フォーム、ASP.NET MVC、Dynamic Data コントロール、および ASP.NET AJAX といういくつか独立したコンポーネントから構成されています。新しい ASP.NET 4.0 プラットフォームは前回の 3.5 SP1 バージョンと同じ基盤に基づいていますが、Web フォーム、Dynamic Data コントロール、および (これは大事なことですが) ASP.NET AJAX の領域が強化されています。

今回のコラムでは、Web フォーム モデルの新機能と強化された機能について紹介します。Dynamic Data コントロールの全体像、および ASP.NET AJAX 環境における開発の詳細については、今後のコラムで扱う予定です。

ASP.NET Web Forms 4.0 の概要

ASP.NET 4.0 プラットフォーム全般の新機能を表すキーワードは、"きめ細かい制御" です。ASP.NET 4.0 は、画期的な変更が加えられたわけでも、既存のアーキテクチャがリファクタリングされたわけでもありません。どちらかと言えば、小規模な変更が多数施され、これらを組み合わせることで、既存のフレームワークに含まれ、頻繁に使用する特定の機能をこれまでよりもきめ細かく制御できるようにしています。

たとえば、ASP.NET 4.0 の Web フォームでは、viewstate の管理、データ バインド コントロールのコンテキストでの ID の生成、なんらかのテンプレート ベースのコントロールから生成される HTML などを、これまで以上にきめ細かく制御できます。また、以前のバージョンの ASP.NET の機能の中で、2009 年公開のプロバイダー モデルをサポートしていなかった機能向けに、プラグ可能なコンポーネントの新しいファミリが存在するようになり、ScriptManager コントロール経由で外部スクリプト ファイルのリンクをさらに細かく制御できるようにもなっています。では、viewstate の管理から説明していきましょう。

viewstate のきめ細かい制御

viewstate については、ASP.NET プラットフォームの登場以来、最も賛否の分かれる機能の 1 つであるというだけで、改めて説明することはありません。あまりにも多くの開発者が、viewstate はすべての ASP.NET ページで帯域幅を浪費し、受け入れ難い負荷をもたらすと、いまだに信じています。こうした開発者のほぼすべては、viewstate がまったく含まれていないという理由から、ASP.NET MVC を歓迎しています。最近、ASP.NET MVC クラスの講義でマスター/詳細のシナリオについて説明しました。このシナリオでは、ユーザーが一覧から顧客を選択して、その顧客の詳細を表示できます。ご想像どおり、ページを読み込んでいる間に顧客一覧を設定しました。次に、顧客の選択変更イベントを処理すると同時に、その顧客の詳細を設定する方法を示しました。ただし、別の顧客を選択できるように、一覧を明示的に再設定しなければなりませんでした。

受講生たちは、サーバーでの操作のたびに一覧を設定し直す追加作業が必要であることにすぐに気付きました。Web フォームのように、一覧の内容を自動的に設定することはできないのでしょうか。実は、ASP.NET Web フォームでポストバックを使ってデータ バインド コントロールを再設定する必要がないのは、viewstate のおかげなのです。

つまり、viewstate は単なる帯域幅の削減対象ではありません。viewstate は、ページ内のコントロールのコンテンツの一部をキャッシュするように、Web フォーム モデルに備えられた機能です。その後、ASP.NET インフラストラクチャが viewstate から情報を読み取り、ページ内の各コントロールを最後に既知であった適切な状態に復元します。

広く知られているのに、見落とされがちなのは viewstate がオプション機能だということです。viewstate のサポートは各ページで既定で有効になっていますが、開発者は、この既定の設定の変更に使用できるブール型プロパティを使って、viewstate なしでページを実行することもできます。このプロパティは EnableViewState というプロパティで、System.Web.UI.Control クラスで定義されます。System.Web.UI.Page クラスは Control クラスから継承されています。viewstate に関する限り、個々のコントロールとページは 1 つの同じものです。

ASP.NET ページの場合、viewstate を無効にするのは簡単ではありません。次のように、ページのライフ サイクル中に、宣言またはプログラムによって EnableViewState を false に設定します。

void Page_Load(object sender, EventArgs e)
{

msdn magazine
2
Cutting Edge
// Disable viewstate for the page
// and ALL of its child controls
this.EnableViewState = false;
...
}

EnableViewState プロパティは、コントロールが UI に関連する独自の状態をキャッシュできるかどうかを示します。ASP.NET での viewstate の設定には階層的な性質があります。つまり、親コントロールで viewstate が有効になっていれば、その子コントロールではいずれも viewstate を無効にできません。ページ レベルまたはコンテナー レベルで viewstate が有効になっている場合、次のコードはテキスト ボックスの動作に影響しません。

protected void Page_Load(object sender, EventArgs e)
{
TextBox1.EnableViewState = false;
}

IsViewStateEnabled プロパティ (実は保護されているプロパティ) は、コントロールの viewstate の現在状態を報告します。しかし、viewstate のこうした性質は、開発者にとってどのような意味があるのでしょう。

ページ レベルで viewstate が有効になっていれば (これは既定の設定です)、そのページ内の個別のコントロールの状態を保存しないようにする方法はありません。ASP.NET 3.5 で viewstate を制御するには、まず、ページ レベルで viewstate を無効にし、viewstate の階層的性質に注意しながら、必要に応じて有効に設定し直す必要があります。コンテナー コントロールで viewstate が有効になっていると、そのコントロールの viewstate の設定が子コントロールの一覧に強制的に適用されます。この事実により、ある種のパラドックスを招きます。同じのコントロールで、IsViewStateEnabled プロパティは true に設定し、
EnableViewState プロパティは false に設定することができてしまいます。

viewstate は ASP.NET Web フォームのアーキテクチャの基本部分であるため、パフォーマンスを向上する目的で viewstate をプラットフォームから完全に削除することは、ほぼ間違いなく、最適な選択ではありません。長年の経験から、より実用に耐えうる選択肢は、ページの viewstate を既定で無効にすることだと証明されています。ASP.NET 4.0 では、これを上回る機能変更が行われており、個々のコントロールで viewstate を制御できるようになります。
ASP.NET 4.0 では、System.Web.UI.Control クラスによって、ViewStateMode という新しいプロパティが公開されます。

public virtual ViewStateMode ViewStateMode { get; set; }

このプロパティは、ViewStateMode 列挙型から利用可能な値を受け取ります (図 1 参照)。

互換性を保つために、このプロパティの既定値は Inherit に設定されています。ただし、このプロパティでは、親ページや親コンテナーの viewstate オプションに関係なく、個々のコントロールの viewstate 設定を制御できます。

ASP.NET ページのコントロールの中で、実際に viewstate を必要とするコントロールはごくわずかです。たとえば、テキストの入力だけに使用するテキスト ボックスでは、viewstate はまったく必要ありません。コントロールで使用するプロパティが Text だけであれば、Text プロパティの値がポストされる値になることから、ページ間のポストバックでも値が保持されます。実際には、viewstate に格納されたあらゆる値は、ポストされる値で定期的に上書きされています。このような場合、viewstate はまったく必要ありません。しかし、他にも注意することがあります。作成時に既定値以外の特定の値が設定されていて、ポストバック中に値が変更されないすべての副次的なプロパティ (ReadOnly、BackColor など) は、viewstate に格納する必要はありません。たとえば、常に同じキャプションを保持する Button コントロールには、viewstate はまったく必要ありません。ASP.NET 4.0 が登場するまで、個々のコントロールで viewstate を無効にすることには問題が発生しがちでした。新しいバージョンでは、事態が変わります。これが、ViewStateMode プロパティについて知っておくべき重要なことです。

自動生成される ID のきめ細かい制御

ASP.NET ページでは、2 つのサーバー コントロールに同じ ID を使用することはできません。この状況が発生すると、ページはコンパイルされません。それで終わりです。ただし、HTML では、2 つ以上の要素で同じ ID を共有できます。この場合は、document.getElementById を使用して要素を検索すると、単純に DOM 要素の配列が取得されます。入れ子になっている ASP.NET コントロールについてはどうでしょうか。

ほとんどのデータ バインドでは、データ バインド項目ごとに HTML テンプレートを繰り返すことによって、テンプレート ベースのコントロールから出力が生成されます。つまり、テンプレート内で一意 ID が定義されている子コントロールが、複数回繰り返されます。この場合、元の ID は一意にできません。このため、開発の初期から、ASP.NET チームは、ASP.NET コントロールによって出力されるすべての HTML 要素に一意 ID が付与されるようなアルゴリズムを定義していました。このアルゴリズムで生成される ID では、コントロールの ID と名前付けコンテナーの ID が連結されます。さらに、繰り返されるコントロール (テンプレートなど) の場合、あいまいさをなくすために、数値インデックスが付加されます。長年にわたって、自動生成された ID の読みやすさを論じる人がいなかったため、次のような文字列が頻繁に使用されるようになりました。

ctl00$ContentPlaceHolder1$GridView11$TextBox1

これを見て真っ先に思い浮かべる問題点は、使用可能な文字列の長さです。この文字列が複数の要素で繰り返されれば、ダウンロードのサイズが大きくなります。さらに重要なことは、クライアント スクリプトの観点からこういった手法が問題になります。クライアント側でスクリプトを記述する際に特定のコントロールの ID を予測することが困難になるだけでなく、読みにくいコードになります。まず、特定の HTML 要素に対して生成された詳細な名前を知る必要がありますが、この HTML 要素は、グリッドの折り重なった部分や、その他の名前付けコンテナーのコントロールに埋もれています。次に、階層に従ってサーバー コントロールの 1 つの名前が変更されると、それに応じて階層内のコントロールの名前が変わることがあります。よく使用される方法を次に示します。

var btn = document.getElementById("<% =Button1.ClientID %>");

この方法の本質は、特定のコントロールに対して生成される実際のクライアント ID を HTML ソース コードで挿入するために、ASP.NET コード ブロックを使用しているところにあります。マスター ページやテンプレート ベースのコントロールを使用する場合、単純なコントロールの名前付けコンテナーが、最終的に、開発者が名前を付けないままにすることが多い、非常に複雑なコントロール階層になるため、この方法が非常に役に立ちます。このような場合、コントロールの実際の名前はいくつかの自動生成された ID になります。

コード ブロックの使用に加えて、ASP.NET 4.0 ではもう 1 つオプションがサポートされ、サーバー コントロールがクライアント ID を生成するアルゴリズムをきめ細かく制御できるようになります。

System.Web.UI.Control クラスに、ClientIDMode という新しいプロパティが含まれるようになりました。このプロパティには、あらかじめ定義されたいくつかの値を設定できます。

確かに、ID 生成アルゴリズムを理解するのは簡単ではありません (マスター ページが含まれる場合はなおさらです)。一意 ID が生成されることは間違いありませんが、最終的に、予測が非常に難しい文字列になってしまいます。主にデータ バインド コントロールで使用するために、予測が容易なオプションが導入されます。このオプションにより、開発者は、n 番目のデータ項目の特定のプロパティをレンダリングするために使用する Label コントロールの ID を簡単に予測できます。この場合、(親コントロールへの名前付けコンテナーの構造を簡略化する) コントロールの階層を反映し、かつ特定のキー値も反映する ID が希望です。次のコードを考えます。

<asp:GridView ID="GridView1" runat="server"
ClientIDMode="Predictable"
RowClientIdSuffix="CustomerID">
...
</asp:GridView>

この場合、グリッドの各行は、データ ソースの 1 つ以上の列に末尾インデックスを添えて識別されます。以下に例を示します。

Panel1_GridView1_ALFKI_1

GridView は、複数の列をサポートする唯一のコントロールです。複数の列を連結する場合は、コンマを使用して名前を区切ります。ListView が 1 つの列のみを受け取るのに対して、Repeater コントロールは 0 から始まるインデックスを末尾に付けるだけで、列名を受け取らないよう制限されます。

最後に、ClientIDMode プロパティは、結果として表示される HTML 要素の ID 属性のみに影響します。仕様ではname 属性は変更されません。

表示コントロールの改良

ほとんどのデータ バインド コントロールでは、特定の表示要素 (主に行) を選択できます。以前のバージョンの ASP.NET では、どの項目が選択されているかは、ページ内で選択された項目のインデックスとして格納されていました。つまり、ページ切り替え可能なコントロール (GridView コントロールなど) の場合、ページ切り替えイベントの処理の中でプログラムから選択項目をリセットしない限り、たとえば、ページ 1 で選択された項目が、ページ 2 でも同じように選択されてしまいます。

ASP.NET 4.0 では、データ バインド コントロールで現在選択している要素が、DataKeyNames プロパティで指定したデータ キー フィールドの値によって追跡されます。この機能を有効にするには、PersistSelection という新しいブール型プロパティの値を true に設定します。互換性を確保するため、既定値は false です。

また、FormView コントロールと ListView コントロールでは、生成される HTML マークアップをいくぶん細かく制御できるようになります。特に、FormView コントロールでは、RenderTable という新しいブール型プロパティが考慮されるようになります。このプロパティの値を false に設定すると (既定値は true です)、余分な HTML テーブル タグが出力されなくなり、CSS によるマークアップ全体のスタイル設定が容易になります。ASP.NET 4.0 では、ListView コントロールでレイアウト テンプレートを使用する必要がなくなりました。

<asp:ListView ID="ListView1" runat="server">
<ItemTemplate>
<% Eval("CompanyName")%>
<hr />
</ItemTemplate>
</asp:ListView>

上記のコード スニペットだけで、データ ソース内の要素ごとに CompanyName 列の内容が繰り返されます。

HTML の強化

当初、ASP.NET では、Web ページで使われる可能性のあるタグをすべてプログラムから制御できるわけではありませんでした。長い間、ページのタイトルは、Page クラスにアドホックなプロパティがありませんでした。他にも、よく使われる CSS ファイルのうな機能についても同じことが言えました。

ASP.NET 4.0 では、Page クラスで 2 つの新しい文字列プロパティが公開され、ページの <head> セクションでよく使われるいくつかのタグを設定できるようになります。この 2 つの新しいプロパティとは、Keywords プロパティと Description プロパティです。これらのサーバー プロパティの内容が、HTML リテラルとして、メタタグに指定した内容に置き換わります。

次の例のように、Keywords プロパティと Description プロパティを、@Page ディレクティブの属性として直接設定することもできます。

<%@ Page Language="C#"
AutoEventWireup="true"
CodeFile="Default.aspx.cs"
Inherits="_Default"
Keywords="ASP.NET, AJAX, 4.0"
Description="ASP.NET 4.0 Web Forms" %>

ASP.NET では、Response.Redirect を呼び出すときに、ブラウザーに HTTP 302 コードを返します。その結果、要求されたコンテンツが、別の (指定した) 場所から使用できるようになります。これに基づいて、ブラウザーは、指定されたアドレスに 2 番目の要求を作成して送信します。ただし、ページにアクセスする検索エンジンは、文字どおり、HTTP 302 コードを受け取ります。これが、HTTP 302 ステータス コードの本来の意味です (つまり、要求されたページが新しいアドレスに一時的に移動されます)。そのため、検索エンジンは内部テーブルを更新せず、ユーザーがページを表示しようとクリックすると、検索エンジンは元のアドレスを返します。次に、ブラウザーが HTTP 302 コードを取得して、2 番目の要求を送信することで、最終的に目的のページが表示されます。

このプロセス全体をスムーズに行うために、ASP.NET 4.0 では、RedirectPermanent というまったく新しいリダイレクト メソッドを利用できます。このメソッドは、呼び出し元が HTTP 301 ステータス コードを受け取ることを除いて、従来の Response.Redirect と同じ方法で使用します。コード 301 は、実際には、要求されたコンテンツが完全に移動されてしまっていることを意味します。このことは、ブラウザーにとってはそれほど大きな違いではありませんが、検索エンジンにとっては重要な違いです。

検索エンジンは、HTTP 301 コードを処理する方法と、その情報を使用してページの URL 参照を更新する方法を認識しています。次回、そのページを含む検索結果が表示されたときにリンクされる URL は新しい URL になります。このようにして、ユーザーはページをすばやく表示でき、2 回目のラウンドトリップを削減できます。

出力キャッシュのきめ細かい制御

ASP.NET 2.0 では、ASP.NET ランタイムの重要な部分がいくつかリファクタリングされ、柔軟性と構成可能性が大幅に向上しました。これは、プロバイダー モデルの導入によって実現されました。セッション状態、メンバーシップ、ロール管理など、一部の ASP.NET のコア サービスの機能は、ある種サービス コントラクトに抽象化されていたため、使用される特定のサービスのさまざまな実装が交換可能になり、管理者は構成ファイルで目的の実装を指定することができました。

たとえば、従来の Session オブジェクトの内部には、HttpSessionState クラスのインスタンスが存在していて、そのコンテンツが選択したセッション状態プロバイダーによって取得されます。既定のセッション状態プロバイダーは、プロセス内のメモリ (具体的には、Cache ディクショナリ内のスロット) からデータを取得しますが、データをデータベースや外部のホスト プロセスに格納するための追加のプロバイダーも存在します。

ASP.NET 4.0 のプロバイダー モデルでは、ASP.NET のもう 1 つの非常に重要な機能を扱います (これは、どういうわけか、以前のバージョンで忘れられていました)。すなわち、出力キャッシュです。

出力キャッシュによってパフォーマンス上の大きな利点がもたらされるなら、ページの応答が少し古くてもかまわないという状況は数多く存在します。たとえば、電子商取引アプリケーションと、製品カタログの一連のページについて考えてみましょう。これらのページを作成するには、1 回以上のデータベース呼び出しと、ある種のデータ結合が要求される可能性があるため、比較的コストがかかります。多くの製品ページは数週間変わることはなく、1 日に何度も更新することなどほとんどありません。だとしたら、同じページを毎秒 100 回のペースで再生成する理由はあるでしょうか。ASP.NET 1.0 から、出力キャッシュを使用してページの応答をキャッシュし、それ以降の要求に対してそのページを実行するのではなく、キャッシュされた出力を返すことにより要求を満たせるようになっています。図 3 は、アプリケーション イベントのシーケンスを示しています。このシーケンスには、システムが要求を解決しようとして出力キャッシュを確認する手順が含まれています。

これまでは、すべてのページ出力 (これは、フォームとクエリ文字列パラメーター、要求している URL、またはカスタム文字列によってグループ化できます) は、ASP.NET キャッシュ専用領域にあるメモリに格納されていました。長時間実行されていると、出力キャッシュの総量がメモリを圧迫し、Cache オブジェクトの更新が頻繁に行われることによって Web サーバー コンピューターに負荷をかけます。ASP.NET 4.0 の出力キャッシュのサブシステムは、プロバイダー モデルを完全にサポートします。そのため、ページ応答を ASP.NET ワーカー プロセス外に格納するチャンスが生まれます。カスタムの出力キャッシュ プロバイダーは、OutputCacheProvider から派生するクラスです。次のようにして、このクラスの名前を構成ファイルに登録する必要があります。

<caching>
<outputCache defaultProvider="AspNetInternalProvider">
<providers>
<add name="DiskCacheProvider"
type="Samples.DiskCacheProvider, MyProvider"/>
</providers>
</outputCache>
</caching>

通常は、複数のプロバイダーを登録しておき、outputCache ノードの defaultProvider 属性で既定のプロバイダーを選択できます。構成ファイルの内容を変更していなければ、既定の動作は、既定のプロバイダーとなる AspNetInternalProvider オブジェクトから提供されます。

すべてのページで同じ出力キャッシュ プロバイダーを使用する必要はありません。要求単位に異なるプロバイダーを選択したり、特定のユーザー コントロール、ページ、またはページのパラメーターの組み合わせによって、異なるプロバイダーを選択することもできます。プロバイダー名は、@OutputCache ディレクティブを指定できる場所 (ページやユーザー コントロール) であればどこでも指定できます。

<% @OutputCache Duration="3600"
VaryByParam="None"
providerName="DiskCache" %>

要求単位にプロバイダーを変更するには、global.asax で新しいメソッドをオーバーライドする必要があります (下図参照)。

{
// Decide which provider to use looking at the request
string providerName = ...;
return providerName;
}

ASP.NET 2.0 から、セッション状態をワーカー プロセスのメモリ外に格納できます。つまり、Session オブジェクトに格納されるすべてのデータは、プロセス外の環境との間でシリアル化が必要です。図 3 を再度見てみると、AcquireRequestState アプリケーション イベントの前後で、セッション状態が Session オブジェクトに読み込まれています。その後、要求処理の最後に、メモリ内のコンテンツがシリアル化されてストレージに戻されます。
ASP.NET 4.0 では、開発者が、セッション状態プロバイダーで送受信されるデータ ストリームを圧縮するよう要求できます (図 4 参照)。圧縮は、通常の Stream クラスを使用してなんらかのシリアル化を行うのではなく、GZipStream クラスを使用して実行します。

<sessionState mode="SqlServer" compressionEnabled="true" ... />

圧縮を有効にするには、構成ファイルの sessionState セクションに compressionEnabled 属性を追加します。既定では、圧縮は有効になっていません。

あらゆる JavaScript ライブラリは、多種多様な固有の JavaScript ファイルから構成されています。これらのファイルは、程度の差はあるものの、相互接続を表す精巧なグラフを伴います。それに対し、ASP.NET AJAX では、常に、ScriptManager コントロールによって JavaScript の詳細を開発者から見えないように抽象化し、いくぶんモノリシックな JavaScript クライアント ライブラリを提供しています。ASP.NET 4.0 では、これが一変します。Microsoft AJAX クライアント ライブラリがリファクタリングされて、図 5 に示すような別個のファイルに分割されます。図 6 は、ライブラリ全体内の個々のスクリプト ファイル間の依存関係を示しています。

ScriptManager コントロールに新しいプロパティが追加され、これを使用してライブラリの構成要素の処理方法を指定できます。これは MicrosoftAjaxMode というプロパティで、図 7 に示すような値を受け取ります。

プラットフォームの強化

ASP.NET 4.0 の Web フォームには、小規模な変更が数多く加えられ、これらを組み合わせることによって、開発プラットフォームが強化されます。Web フォームは成熟の域に達したフレームワークなので、必要なのは再設計ではなく改良です。Web フォームに完全に満足しているわけではなく、まったく異なるフレームワークをお探しの場合は、ASP.NET MVC が注目に値します。

Dino Esposito は、IDesign 社のアーキテクトであり、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2008 年) の共著者です。Esposito はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos (英語) で読むことができます。