はじめに
ASP.NETでは、いくつかのページレベルの状態維持メカニズムが、ViewStateと新しいControlStateによって実現されています。
これらのメカニズムは有効に機能しますが、どちらもアプリケーション開発者にとって確定的でないという制限があります。ViewStateはオフにすることが可能ですし、非常に「かさばる」メカニズムのため扱いきれない場合があります。また、ControlStateはコントロールの実装内からしか設定できません。そこで本稿では、より柔軟な状態メカニズムを実現するための新しい手段として、フィールド値の永続化と復元を自動的に行うことのできるプロパティ永続化コントロール(PreservePropertyControl)をViewStateなしで作成してみたいと思います。
ASP.NET 1.xにはViewStateが用意されており、ASP.NET 2.0ではControlStateが追加されています。どちらのメカニズムも、状態データを__VIEWSTATEという隠しフォーム変数としてページに埋め込むことにより、1つのページのポストバック間でページ固有の状態を維持できるようにします。これらは特定の状況では有効ですが、どちらも大きな制限があるため、状況によっては不適切で、必要以上に使いにくくなるように思います。
コントロールやページのプロパティ値を宣言的に格納し、ページのポストバック時に値が自動的に復元される組み込みのメカニズムがあれば便利ではないでしょうか?
たしかにViewStateもそれに近い働きをしますが、ViewStateは宣言的ではなく、制御も容易ではありません。私はできるだけViewStateをオフにするようにしていますが、そうするとViewStateで値を追跡するという機能をまったく利用できなくなります。コントロールの状態は追跡不能になり、アプリケーションコードで生成した値をViewStateコレクションに格納することもできなくなります。一方、ViewStateをオンにすると、変更された非ポストバックの値を持つ(かつViewStateが有効になっている)すべてのコントロールの変更された値が一つ残らず収集されるようになります。
つまり、ViewStateはオール・オア・ナッシングの手法なのですが、実際のプログラミング現場では、ページの1つか2つの値だけを永続化したい場合がほとんどです。そこでこのミスマッチに悩まされることになります。
私は数日前に、複雑なデータグリッドを処理していて、まさにそのような状況に遭遇しました。このデータグリッドはViewStateをオフにしてセットアップされています。さらに、各行からポストバックを発生させる「削除」や「状態更新」などさまざまなアクションがあります。また、ページングや並べ替えの機能もあります。ViewStateを使わずにこのデータグリッドのCurrentPageIndexを正しく追跡しようとすると、実に面倒なことになってしまいます。
このような状況で、もし次のようなことができたら便利だとは思いませんか?
this.gdGrid.PreserveProperty("CurrentPageIndex")
もしこれが可能になれば、ViewStateを使う必要はなくなります。しかも、データグリッドのすべてのデータを永続化するのではなく、特定の値を選択して永続化できます。他のコントロールのプロパティ(例えばボタンのForeColorなど)も、同じ方法で永続化できるようになります。
this.btnSubmit.PreserveProperty("ForeColor")
この仕組みはViewStateを有効にしなくとも機能し、Page_Load()
や、要求に応じて実行される他のコードの先頭に適切なコードを貼り付けるだけで利用可能になります。最初に必要なコードさえ記述しておけば、ASP.NETが要求の終わりに値を自動的に書き出し、ポストバックでページが再び読み込まれるときに値を復元してくれます。これこそ使いやすくて柔軟性のあるページレベルの状態メカニズムです。
これと同じ仕組みをコントロールで使用することもできます。そうすると、ASP.NET 2.0でControlStateが行うのと同様の方法で、コントロールの状態をこの同じ状態バッグに確定的に永続化することができます(ControlStateは ASP.NET 2.0の新機能であり、詳しくは後で説明します)。
ただし、この仕組みの大前提となっているのは、Control
クラスにPreserveProperty
メソッドを追加して、すべてのコントロールでこのメソッドを利用できるようにすることです。しかし、Control
クラスに変更を加えるという解決策はMicrosoftのみに許された選択肢です。一般の開発者がControlを拡張し、拡張した方のControl
クラスを他のストックコントロールに継承させることはできません。
我々にできる次善の対応策は、これと同じ機能を実現する外部コントロールを作成し、ページ上にドロップしたりコントロールに追加したりできるようにすることです。本稿では、このようなエクステンダコントロールを作成する方法を紹介します。
PreservePropertyControlの概要
PreservePropertyControlは、ASP.NET 2.0カスタムサーバコントロールです。本稿のダウンロードサンプルにはASP.NET 1.1版のソースも含まれていますが、これは本コントロールを1.1に移植したPeter Brombergから提供されたものです。私は永続プロパティにジェネリックを利用したいと考えたので、主にASP.NET 2.0を使いました。.NET 2.0では、ジェネリックを使って厳密に型指定されたコレクションを容易に作成できます。さらに、後からデザイナを使い、デフォルトのコレクションエディタを使用してこれらのコレクションを編集することができます。
このプロパティ永続化コントロールでは、デフォルトのストレージメカニズムを実現するために、2.0のもう1つの機能であるControlStateを利用しています。ControlStateを使うと、内部構造を簡単に永続化でき、その構造をページに埋め込むためのエンコードについて気にせずに済みます。
今回紹介するPreservePropertyControlは、ページ上で宣言的に定義できる、デザイナサポートを備えたサーバコントロールです。このコントロールは例えば次のように宣言できます。
<ww:PreservePropertyControl ID="Persister" runat="server"> <PreservedProperties> <ww:PreservedProperty ID="PreservedProperty1" runat="server" ControlId="btnSubmit" Property="ForeColor" /> <ww:PreservedProperty ID="PreservedProperty2" runat="server" ControlId="__Page" Property="CustomerPk" /> </PreservedProperties> </ww:PreservePropertyControl>
この宣言を行うには、ASP.NETページ内にスクリプトとしてマークアップするか、Visual Studioでデザイナおよびデフォルトのコレクションエディタで、永続化したいプロパティを入力します。値を永続化するには、コントロールのUniqueIDと、永続化するプロパティまたはフィールドの名前を指定します。コントロールの値だけでなく、Page
オブジェクトの値も永続化できます(上記のControlId="__Page"
の例を参照)。
もちろん、同様の宣言をコーディングで行うこともできます。
protected PreservePropertyControl Persister=null; protected void Page_Load(object sender, EventArgs e) { this.Persister=new PreservePropertyControl(); this.Persister.ID = "Persister"; this.Controls.Add(Persister); this.Persister.PreserveProperty(this.btnSubmit, "ForeColor"); this.Persister.PreserveProperty(this, "CustomerPk"); }
コードを使用するときは、文字列のIDよりも実際のコントロール参照を渡すほうが効率的です。PreservePropertyControlはこのようなコントロール参照をキャッシュし、後からページサイクルでその参照を使って値をストレージコンテナに書き込みます。
このメカニズムによって柔軟性が大きく向上します。コントロールインスタンスを通じて参照できるものであれば、基本的には何でもこの状態コンテナに格納できるようになります。Page
オブジェクトもコントロールなので、永続化したいものすべてにProtected
プロパティをアタッチできます。例えば、ページにPk参照を追加して、ViewStateや隠し変数を使わずに現在の編集コンテキストを追跡できるようにするのにとても便利です。
オブジェクト全部を格納することさえできます。ViewStateと同様に、PreservePropertyControlにはすべてのシリアル化可能オブジェクトを格納できます。この手法のメリットは、いったんPreserveProperty()
呼び出しをセットアップすると、後はプロパティを参照するだけで済むことです。従って、上に示すようにCustomerPkを永続化する場合は、ページのCustomerPk
プロパティを参照するだけでよく、状態メカニズムに明示的にアクセスしなくても常に正しい値が得られます。this.CustomerPk
と指定するだけで、値が最新の状態になります。
状態コンテナとやり取りする必要がなくなるため、この手法はViewStateよりも簡単です。状態バッグにアクセスしたり、NULL値を再確認したりする必要はありません。そのような処理はコントロールが管理してくれます。
コントロール開発者はPreservePropertyControlを内部で使い、PreservePropertyControlのプライベートコピーを使用して独自のプロパティをマップすることもできます。
public class CustomControl : Control { PreservePropertyControl Persister = null; protected string value = null; protected override void OnLoad(EventArgs e) { this.Persister = new PreservePropertyControl(); this.Persister.ID = "__" + this.ID; this.Persister.StorageMode = PropertyStorageModes.HiddenVariable; this.Persister.PreserveProperty(this,"value"); this.Controls.Add(this.Persister); base.OnLoad(e); } }
いったんこれをセットアップすれば、コントロール開発者は永続化する値をViewState["value"]やカスタムコンテナなどの特別なストアに保存する必要がなくなり、ViewStateを一切アクティブにせずに、プロパティを通常どおり参照するだけで済むようになります。
本稿のサンプルコードをダウンロードして、実際のPreservePropertyControlの動作と小さなサンプルページを試すことができます。zipファイルには、2.0と1.1の両方のバージョンが含まれています。