ASP.NET 2.0でプロパティ永続化コントロールを作成するはじめに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を有効にしなくとも機能し、 これと同じ仕組みをコントロールで使用することもできます。そうすると、ASP.NET 2.0でControlStateが行うのと同様の方法で、コントロールの状態をこの同じ状態バッグに確定的に永続化することができます(ControlStateは ASP.NET 2.0の新機能であり、詳しくは後で説明します)。 ただし、この仕組みの大前提となっているのは、 我々にできる次善の対応策は、これと同じ機能を実現する外部コントロールを作成し、ページ上にドロップしたりコントロールに追加したりできるようにすることです。本稿では、このようなエクステンダコントロールを作成する方法を紹介します。 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と、永続化するプロパティまたはフィールドの名前を指定します。コントロールの値だけでなく、 もちろん、同様の宣言をコーディングで行うこともできます。 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はこのようなコントロール参照をキャッシュし、後からページサイクルでその参照を使って値をストレージコンテナに書き込みます。 このメカニズムによって柔軟性が大きく向上します。コントロールインスタンスを通じて参照できるものであれば、基本的には何でもこの状態コンテナに格納できるようになります。 オブジェクト全部を格納することさえできます。ViewStateと同様に、PreservePropertyControlにはすべてのシリアル化可能オブジェクトを格納できます。この手法のメリットは、いったん 状態コンテナとやり取りする必要がなくなるため、この手法は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の両方のバージョンが含まれています。 PreservePropertyControlの仕組み PreservePropertyControlの実装はきわめて単純です。PreservePropertyControlでは、永続化するコントロールのControlID(または、利用できる場合はインスタンス)とプロパティの名前を格納するためのPreservedPropertiesコレクションを用意します。コントロールの コレクションをデザイン可能なコレクションとして機能させるには、 リスト1
[ParseChildren(true)] [PersistChildren(false)] [DefaultProperty("PreservedProperties")] public class PreservePropertyControl : Control { ///// <summary> ///// Collection of all the preserved properties ///// </summary> [DesignerSerializationVisibility( DesignerSerializationVisibility.Visible)] [PersistenceMode(PersistenceMode.InnerProperty)] public List<PreservedProperty> PreservedProperties { get { return _PreservedProperties; } } List<PreservedProperty> _PreservedProperties = new List<PreservedProperty>(); /// <summary> /// Required to add PreservedProperty Collection /// </summary> protected override void AddParsedSubObject(object obj) { if (obj is PreservedProperty) this.PreservedProperties.Add(obj as PreservedProperty); } /// <summary> /// Adds a control to the collection. At this point only the /// control and property are stored. /// </summary> public bool PreserveProperty(Control WebControl, string Property) { PreservedProperty pp = new PreservedProperty(); pp.ControlId = WebControl.UniqueID; pp.ControlInstance = WebControl; pp.Property = Property; this.PreservedProperties.Add(pp); return true; } /// <summary> /// Adds a control to the collection. At this point only the /// control and property are stored. /// </summary> public bool PreserveProperty(string ControlId,string Property) { Control ctl = this.Page.FindControl(ControlId); if (ctl == null) throw new ApplicationException("Can’t persist control:" + ControlId + "." + Property); return this.PreserveProperty(ctl, Property); } ...more implementation code here } このコードでは、厳密に型指定された単純なListコレクションでPreservedPropertiesの格納を処理しています。PreservePropertyControlはカスタムクラスを使用して、コントロールのID、利用できる場合はインスタンス参照、およびプロパティ名を保持します。PreservedPropertiesコレクションは、プロパティの識別情報を一時的に保持するコンテナです。値が実際に格納されるのは、後の要求サイクルの話になります。 永続データのエンコードとデコードエンコードおよびデコード用の実際のフックはStorageModeによって異なり、例えばControlState、HiddenVariable、SessionVariable、CachePerPageという選択肢があります。ControlState以外のモードでは、エンコードとデコードを行うために明示的なフックを起動する必要があります。 これらのモードでは、PreservePropertyControlの 値を格納するときは、 ASP.NET 2.0のControlStateによる永続化ControlStateはASP.NET 2.0の新機能です。これはコントロール内部の状態を実装する機能であり、ページのポストバック間で重要なデータを格納するために使われます。ストレージにはViewStateを使用しますが、ストックViewStateと異なり、EnableViewStateの設定に関係なく常に値を書き出します。 ControlStateは、 ControlStateを有効にするには、ControlStateを使用することを次のような方法でページに対して宣言します。 this.Page.RegisterRequiresControlState(this); 通常はこれをコントロールの 次に、 これはすべてのエンコードの詳細を処理する非常に簡単なメカニズムです。必要なのはオブジェクトを返すことだけです。リスト2に、ControlStateを管理するための リスト2
/// <summary> /// Internal persistance object used to serialize /// into the state store. Hashtable is Serializable /// and can be serialized by the LosFormatter /// </summary> protected Hashtable SerialzedProperties = new Hashtable(); protected override void OnInit(EventArgs e) { base.OnInit(e); if (this.Enabled) { if (this.StorageMode == PropertyStorageModes.ControlState) this.Page.RegisterRequiresControlState(this); else if (this.Page.IsPostBack) this.LoadStateFromLosStorage(); } /// <summary> /// Saves the preserved Properties into a Hashtabe where the key is /// a string containing the ControlID and Property name /// </summary> protected override object SaveControlState() { foreach (PreservedProperty Property in this.PreservedProperties) { // *** Try to get a control instance Control Ctl = Property.ControlInstance; if (Ctl == null) { // *** Nope - user stored a string or declarative Ctl = this.Page.FindControl(Property.ControlId); if (Ctl == null) continue; } string Key = Ctl.UniqueID + CTLID_PROPERTY_SEPERATOR + Property.Property; // *** If the property was already added skip over it // *** duplicates are always the same if (this.SerialzedProperties.Contains(Key)) continue; // *** Try to retrieve the property object Value = null; try { // *** Use Reflection to get the value out // *** Note: InvokeMember is easier here since // we support both fields and properties Value = Ctl.GetType().InvokeMember(Property.Property, BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.IgnoreCase, null, Ctl, null); } catch { throw new ApplicationException( "PreserveProperty() couldn’t read property " + Property.ControlId + " " + Property.Property); } // *** Store into our hashtable to persist later this.SerialzedProperties.Add(Key, Value); } // *** store the hashtable in control state (or return it return this.SerialzedProperties; } /// <summary> /// Overridden to store a HashTable of preserved properties. /// Key: CtlID + "|" + Property /// Value: Value of the control /// </summary> protected override void LoadControlState(object savedState) { Hashtable Properties = (Hashtable)savedState; IDictionaryEnumerator Enum = Properties.GetEnumerator(); while (Enum.MoveNext()) { string Key = (string)Enum.Key; string[] Tokens = Key.Split(CTLID_PROPERTY_SEPERATOR); string ControlId = Tokens[0]; string Property = Tokens[1]; Control Ctl = this.Page.FindControl(ControlId); if (Ctl == null) continue; Ctl.GetType().InvokeMember(Property, BindingFlags.SetField | BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.IgnoreCase, null, Ctl, new object[1] { Enum.Value }); } } コントロール参照が利用できるようになると、プロパティの値がReflection経由で取得されます。フィールドとプロパティ、およびパブリックメンバとパブリックでないメンバを取得するフラグに注意してください。また、 次は、一意のControlIDとプロパティ名を連結したキーを使ってハッシュテーブルに値を追加します。ハッシュテーブルはキーと値のペアを格納するのに最適な軽量オブジェクトです。その後、 ポストバック時のデータの読み込みでは、これと逆の処理が行われます。 これは非常に単純明快な処理で、ControlStateを使うとほとんどコードを必要とせずにこのソリューションを実装できます。 その他の永続化メカニズムControlStateは有効に機能し、おそらく永続データを格納するのに最も信頼の置ける方法ですが、ASP.NET 1.1を使用している場合や、ViewStateによる永続化を(たとえ恒久的でも)好まない場合は、別のストレージメカニズムを使用することもできます。PreservePropertyControlは、次の4つの永続化モードをサポートしています。
ControlState以外のモードでは、ページパイプラインへのカスタムフックと、データをシリアル化するための多少の追加処理が必要です。これの基になるコントロールを作成したときに、私は効率が悪くてかなり多くのコードを必要とする独自のシリアル化メカニズムを使っていました。私のブログに寄せられたいくつかの提案の中で、誰かがLosFormatterを使うことを提案してくれました。LosFormatterはSystem.Web.UI名前空間にある、あまり知られていない文字列シリアライザで、オブジェクトのシリアル化に利用できます。 このシリアライザで使われるフォーマットは、完全なバイナリフォーマッタよりも軽量です。一般的な型をより効率的にエンコードするうえに、多くの場合、コストのかかる完全な.NETシリアル化メカニズムを迂回するからです。リスト3に、PreservePropertyControlのControlState以外のモードのストレージメカニズムに関するコードを示します。 リスト3
/// <summary> /// Read in data of preserved properties in OnInit /// </summary> protected override void OnInit(EventArgs e) { base.OnInit(e); if (this.Enabled) { if (this.StorageMode == PropertyStorageModes.ControlState) this.Page.RegisterRequiresControlState(this); else if (this.Page.IsPostBack) this.LoadStateFromLosStorage(); } } /// <summary> /// Write out data for preserved properties in OnPreRender /// </summary> protected override void OnPreRender(EventArgs e) { if (this.Enabled && StorageMode != PropertyStorageModes.ControlState) this.SaveStateToLosStorage(); base.OnPreRender(e); } /// <summary> /// Saves state the specified storage mechanism by /// first serializing to a string with the LosFormatter /// </summary> private void SaveStateToLosStorage() { string Serialized = LosSerializeObject(this.SaveControlState()); if (this.StorageMode == PropertyStorageModes.HiddenVariable) this.Page.ClientScript.RegisterHiddenField( "__" + this.UniqueID, Serialized); else if (this.StorageMode == PropertyStorageModes.SessionVariable) HttpContext.Current.Session["__" + this.UniqueID] = Serialized; else if (this.StorageMode == PropertyStorageModes.CachePerPage) { if (this.PreservePropertyKey == null) this.PreservePropertyKey = Guid.NewGuid().ToString().GetHashCode().ToString("x"); HttpContext.Current.Cache[this.PreservePropertyKey] = Serialized; this.Page.ClientScript.RegisterHiddenField( "__PreservePropertyKey", this.PreservePropertyKey); } } /// <summary> /// Retrieves the serialized data from the Storage medium /// as string using LosFormatter formatting. /// </summary> private void LoadStateFromLosStorage() { string RawBuffer = null; if (this.StorageMode == PropertyStorageModes.HiddenVariable) { RawBuffer = HttpContext.Current.Request.Form["__" + this.UniqueID]; if (RawBuffer == null) return; } else if (this.StorageMode == PropertyStorageModes.SessionVariable) { RawBuffer = HttpContext.Current.Session["__" + this.UniqueID] as string; if (RawBuffer == null) return; } else if (this.StorageMode == PropertyStorageModes.CachePerPage) { this.PreservePropertyKey = HttpContext.Current.Request.Form["__PreservePropertyKey"]; if (this.PreservePropertyKey == null) return; RawBuffer = HttpContext.Current.Cache[this.PreservePropertyKey] as string; } if (RawBuffer == null) return; // *** Retrieve the persisted HashTable and pass to LoadControlState // *** to handle the assignment of property values this.LoadControlState(LosDeserializeObject(RawBuffer)); } private string LosSerializeObject(object obj) { LosFormatter output = new LosFormatter(); StringWriter writer = new StringWriter(); output.Serialize(writer, obj); return writer.ToString(); } private object LosDeserializeObject(string inputString) { LosFormatter input = new LosFormatter(); return input.Deserialize(inputString); } ControlState以外の永続化モードでは、 HiddenFormVariableでは、データは次のように格納されます。 this.Page.ClientScript.RegisterHiddenField( "__" + this.UniqueID, Serialized); この場合は、次のようなコードを使ってデータを読み込みます。 RawBuffer = HttpContext.Current.Request.Form[ "__" + this.UniqueID]; 取得されたローバッファは、シリアル化解除されてハッシュテーブルに戻り、 セッション変数ストレージは少し異なるため、少々説明が必要です。セッションストレージでは、保持されたプロパティの状態を、すべてのページに再利用されるセッション変数に格納します。従って、すべてのページが同じセッション変数インスタンスを取得し、ユーザーごとに1つの変数があります。 セッションを使うことには大きな利点があります。永続データを この手法を使うときは注意が必要です。同じブラウザセッションで2つのウィンドウを開いたり、同じセッションが同時にアクティブになるフレームページを使用したりすると、誤った状態が復元されるという問題が発生します。サンプルのデモページでこれを試すことができます。ページを実行し、色を設定し、送信します。 次にCtrl+nキー(またはタブがある場合はCtrl+tキー)を押して、同じブラウザセッションで新しいブラウザウィンドウを作成します。同じページを開き、別の色を選択して[Show]をクリックします。 今度は最初のページに戻り、ポストバックボタンをクリックします。2番目のインスタンスの色が最初のインスタンスに表示されるはずです。これは明らかに誤りです。最初のインスタンスが2番目のインスタンスの永続データを取得してしまっています。 複数のブラウザウィンドウやフレームを実行する必要がない内部アプリケーションでは、これが問題にならないこともありますが、そのような場合でもこのオプションの使用には十分に注意してください。セッション変数に対して一意のページ単位のIDを生成することで、この問題を解決できる場合もありますが、それによってセッション状態が大量の永続メモリでいっぱいになる可能性があります。セッション変数ストレージの使用は、大量の永続データが含まれるページを扱う場合で、ポストバックのたびにネットワーク経由でデータを送信するのを避けたい場合にのみ検討してください。 同様に、キャッシュオブジェクトを使用することもできます。セッションの手法と異なり、キャッシュの手法ではセッションに書き込み、ページごとに新しいGUIDを割り当てます。そのため、すべてのユーザーのすべての新しいページが新しいキャッシュエントリを作成します。 セッションの場合と同様に、トレードオフに注意してください。 それでも、キャッシュやセッションの手法によってページが軽量になり、状態ストレージのパフォーマンスが向上するので、これらのオプションを試してみることをお勧めします。 ControlStateとHiddenFormVariableの手法は、どちらもページ自体にPOSTデータとして格納されるので、こうした問題に見舞われることはありません。この2つでは、ControlStateの方が信頼性に勝ります。ControlStateにはページパイプラインの一部として発生する独自のイベントがあり、他のコントロールとのタイミングの問題が発生しかねない既存のイベントのフックが不要だからです。HiddenFormVariableの実装は、主にASP.NET 1.xをサポートするために提供しています。 無駄を省いて環境保全最近開発したいくつかのアプリケーションでPreservePropertyControlを使ってみて、これまで状態ストレージを検討すらしなかったような場面でこのコントロールが大いに役立つことが分かりました。永続化する対象を正確に制御できると、永続化された状態のサイズを小さく抑えつつ、永続化された値への真のプロパティアクセスを提供する柔軟なページを簡単に作成できます。私は自分のページでViewStateを全部まとめてオフにする傾向があるため、いくつかのアイテムを宣言的に永続化できるコントロールがあると非常に助かります。全体として、このコントロールはさまざまな形で役に立ちます。 私の場合、最もよくあるシナリオは、グリッドやリストのリストコントロール、ページング操作、および並べ替えの設定による このコントロールの良い点は、保持されたプロパティを操作するのがはるかに自然になることです。永続化メカニズムを一切気にせずに、プロパティを参照するだけで済むからです。いったんPreservePropertyControlに追加してしまえば、処理は完全に透過的です。基本的に、このコントロールを使うとASP.NETの無差別なViewStateポリシーを根本から変えることができます。何もかも自動的にViewStateに格納する代わりに、明確に格納する必要があるものだけを格納します。これにより、ページ内でViewStateに要求されるサイズを激減させることができます。このコントロールでページのサイズを大幅に削減できないかどうか、各自で試してみてください。 このコントロールが読者の役に立ち、本稿がさまざまなページレベルの状態維持メカニズムを理解する手がかりになることを願っています。実生活でもWebコードでも、無駄を省いて環境保全に努めましょう! 本稿に関するご意見、ご質問、ご提案などがあれば、こちらに投稿してください。 著者紹介Rick Strahl(Rick Strahl)
ハワイのマウイ島にあるWest Wind Technologies社の社長。同社はWebおよび分散アプリケーションの開発とツールを専門にしており、Windowsサーバー製品、. NET、Visual Studio、およびVisual FoxProに主軸を置いている。RickはWest Wind Web Connection、West Wind Web Store、およびWest Wind HTML Help Builderの作成者である。C# MVPで、雑誌や書籍に頻繁に寄稿し、国際的な開発者会議で頻繁に講演を行っている。『CoDe Magazine』誌の共同発行者でもある。詳細については、彼のWebサイト(www.west-wind.com)を参照。
|