PreservePropertyControlの仕組み
PreservePropertyControlの実装はきわめて単純です。PreservePropertyControlでは、永続化するコントロールのControlID(または、利用できる場合はインスタンス)とプロパティの名前を格納するためのPreservedPropertiesコレクションを用意します。コントロールのPreserveProperty()メソッドを呼び出すか、ページ上でコントロールを宣言的に定義すると、適切なControlIdとプロパティ名がコレクションに格納されます。このコレクションはジェネリックリスト(Generic List)です。今回のような目的にはジェネリックが威力を発揮します。子コントロール用のカスタムコレクションを作成せずに済むからです。
コレクションをデザイン可能なコレクションとして機能させるには、Controlクラスでコレクションプロパティと共にいくつかのカスタム属性を使う必要があります。また、宣言的スクリプト定義を通じて子コントロールの値をPreservedPropertyオブジェクトとして追加できるようにするために、AddParsedSubObjectを実装する(または、コレクションが1つしかない場合はDefaultProperty属性を使用する)必要もあります。リスト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のOnInit()およびOnPreRender()の中でフックの呼び出しを行います。OnInit()はページサイクルのごく初期の時点で呼び出されるので、これによって他の状態取得よりも先に、保存済みの値が取得されます。具体的には、ViewStateによって値が割り当てられる前、またはポストバック値が割り当てられる前に、プロパティ値が設定されます。その結果、永続化されたプロパティは値割り当ての優先度が最も低くなり、意図したとおりの動作になります。
値を格納するときは、PreservePropertyControl.OnPreRender()のAs部分で行われます。
ASP.NET 2.0のControlStateによる永続化
ControlStateはASP.NET 2.0の新機能です。これはコントロール内部の状態を実装する機能であり、ページのポストバック間で重要なデータを格納するために使われます。ストレージにはViewStateを使用しますが、ストックViewStateと異なり、EnableViewStateの設定に関係なく常に値を書き出します。
ControlStateは、PageクラスのSaveControlState()メソッドとLoadControlState()メソッドを実装することで簡単に操作できます。これらのメソッドは、ASP.NETが内部的に永続化しているオブジェクトの値を保存または取得します。対象となるオブジェクトは、単純型であるか、シリアル化が可能であるか、TypeConverterを実装している必要があります。ViewStateの場合と同様に、規則は.NETのLosFormatterクラス(ASP.NETがViewState文字列のエンコードに使用する最適化されたシリアライザ)を通じて決定されます。ControlStateはSystem.Web.UI名前空間にあります。
ControlStateを有効にするには、ControlStateを使用することを次のような方法でページに対して宣言します。
this.Page.RegisterRequiresControlState(this);
通常はこれをコントロールのOnInit()メソッド内で呼び出します。
次に、SaveControlState()をオーバーライドして、状態を含んでいるオブジェクトを返します。永続化する値が複数ある場合、この目的に使用される代表的なオブジェクトは、永続化したすべての値を含んでいるハッシュテーブルです。ページのポストバック時には、LoadControlState()を呼び出して、SaveControlState()で保存したオブジェクトを実質的に復元するオブジェクパラメータを引き渡します。
これはすべてのエンコードの詳細を処理する非常に簡単なメカニズムです。必要なのはオブジェクトを返すことだけです。リスト2に、ControlStateを管理するためのSaveControlState()メソッドとLoadControlState()メソッドを示します。
/// <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 }); } }
SaveControlState()はPreservedPropertiesコレクションを調べて、各コントロールインスタンスを見つけることから始めます。コードを使ってプロパティを追加した場合はおそらくコントロールのインスタンス参照を渡しているはずですが、宣言的スクリプトを使用した場合は文字列参照が格納されるので、FindControl()を使って実際にコントロールを見つける必要があります。
コントロール参照が利用できるようになると、プロパティの値がReflection経由で取得されます。フィールドとプロパティ、およびパブリックメンバとパブリックでないメンバを取得するフラグに注意してください。また、Pageオブジェクトのプライベートメンバは保持できないことにも注意してください。
次は、一意のControlIDとプロパティ名を連結したキーを使ってハッシュテーブルに値を追加します。ハッシュテーブルはキーと値のペアを格納するのに最適な軽量オブジェクトです。その後、SaveControlState()メソッドはこの永続化用のハッシュテーブルを返します。
ポストバック時のデータの読み込みでは、これと逆の処理が行われます。LoadControlState()はハッシュテーブルを入力として受け取り、コレクションを調べて各キーと値を取得します。LoadControlState()のコードがキーを元のControlIDとプロパティ名のコンポーネントに分割し、FindControl()を使用してインスタンスを取得した後、Reflectionでコントロールの値を設定します。
これは非常に単純明快な処理で、ControlStateを使うとほとんどコードを必要とせずにこのソリューションを実装できます。
