項目属性もレンダリングできるようにリストコントロールを拡張する
ここには、実は2つのバグがあります。1つは、リストコントロールが項目属性をレンダリングしないこと、もう1つは、ListItemクラスが値をビューステートに保存しないこと(したがって、ポストバックで値が失われること)です。本稿で取り上げるのは、そのうち最初のバグ――それも、CheckBoxListにおける最初のバグ――の克服法だけです(2番目のバグについては、いずれ将来の記事で解決方法を考えます)。しかし、RadioButtonListもよく似たようなものですから、ここに示すロジック/コードは、RadioButtonListにも簡単に適用できるでしょう。DropDownListやListBoxで項目属性を設定する必要があるときは、DropDownListでの問題修正に必要なコードを取り上げたVictor Garcia Apreaのニュースグループ投稿がありますから、そちらを参考にしてください。
以降で見ていく解決方法は、リストコントロールクラスを拡張するという方法です。つまり、項目のレンダリングコードを書き換えて、属性のレンダリングを行わせるようにします(Victorがニュースグループ投稿で採用しているアプローチもこれです)。他に、Microsoftサポート技術情報の記事Q309338では、リスト上の項目ごとに属性を設定する必要があるときはリストコントロールなど使わず、HTMLコントロールを使用するという方法を提案しています。
今回ご紹介する私のアプローチは、かなり無骨で力技に近いものです。実際のCheckBoxListクラスにはcontrolToRepeatという名前のCheckBox型パブリックメンバ変数があり、これが、CheckBoxリスト中の各項目のレンダリングに使われます。RenderItem()メソッドでは、このcontrolToRepeat変数のIDプロパティが、これからレンダリングする反復項目のカレントインデックスに設定されます。また、TextとCheckedの両プロパティが対応ListItemのTextプロパティとSelectedプロパティに基づいて設定され、TextAlignとEnabledの両プロパティがCheckBoxListのTextAlignプロパティとEnabledプロパティに基づいて設定されます。私たちがなすべきことは、このメソッドに踏み込み、現在のListItemの各属性をcontrolToRepeatに割り当ててやることです。
CheckBoxListのRenderItem()メソッドは、残念ながらオーバーライド不可のプライベートメソッドであるため、派生クラスでメソッドをオーバーライドするという簡単な方法はとれません。派生クラスでIRepeatInfoUserインターフェイスを実装し、IRepeatInfoUserに含まれるあらゆるメンバのコードを用意することが必要です(救いは、RenderItem()以外はすべて1行だけの簡単なメソッドであることです)。つまり、私たちのRenderItem()インスタンスでは、基本クラスのcontrolToRepeatプライベートインスタンスにアクセスできません。そのため、拡張クラスにcontrolToRepeatの独自コピーを持たせなければなりません。さらに、それに関連してもう1つ、CheckBoxListのメソッドのうち、controlToRepeatプライベートメンバを使用するすべてのメソッドのオーバーライド、という面倒な作業も発生します。これは、基本クラスではなく拡張クラスのcontrolToRepeatインスタンスを使用させるために欠かせない作業です。もっとも、この作業自体は、Reflectorから得られるコードリストのカットアンドペーストで間に合います(無骨で力技に近いアプローチだと申し上げたでしょう?)。
ここにコード全体を掲載することは控えます。全体を見たい方は、本稿のサンプルコードをダウンロードしてください。肝心なのはRenderItem()メソッドの実装ですから、その部分だけを示しておきましょう。太字で示す部分が、元のRenderItem()実装にはなく、私が付け加えたコードです。
public void RenderItem(System.Web.UI.WebControls.ListItemType itemType, int repeatIndex, RepeatInfo repeatInfo, HtmlTextWriter writer) { controlToRepeat.ID = repeatIndex.ToString( NumberFormatInfo.InvariantInfo); controlToRepeat.Text = this.Items[repeatIndex].Text; controlToRepeat.TextAlign = this.TextAlign; controlToRepeat.Checked = this.Items[repeatIndex].Selected; controlToRepeat.Enabled = this.Enabled; ''controlToRepeat.Attributes.Clear();'' ''foreach (string key in Items[repeatIndex].Attributes.Keys)'' ''controlToRepeat.Attributes.Add('' ''key, Items[repeatIndex].Attributes[key]);'' controlToRepeat.RenderControl(writer); }
ご覧のとおり、私が追加したコードではcontrolToRepeatのAttributesコレクションの内容をクリアし、そこに現在のListItemの属性(あれば)を入れています。
ListItemレベル属性の使用例
ListItemレベルの属性をあれこれ調べる気になったのは、妻の苦労がきっかけでした。妻は、データベースに直結した数個のチェックボックスの他に、[None]オプションを含んでいるCheckBoxListを作成しようとしていました。クライアント側の動作としては、[None]チェックボックスがクリックされたら、いま選択されているすべてのチェックボックスをクリアすること、同様に、[None]チェックボックスがオンの状態で別のチェックボックスがオンにされたときは、[None]チェックボックスを自動的にクリアすることを望んでいました。妻にとってどうということのない作業のはずでしたが、クライアント側のonclick属性を追加しようとしてもうまくいきませんでした。もちろん、CheckBoxListが項目属性をレンダリングしてくれないことが原因です。
妻はオンラインでいろいろと調べ、これが既知のバグであることを知ると、私のところへやってきて、「4Guysの記事用にちょうどいいネタがあるわよ」と言ったのでした。この拡張されたCheckBoxListコントロールを使用すれば、CheckBoxList中の項目に属性を追加できます。ここで、いま述べたクライアント側動作がどのようなコードで実現されるかを、サンプルとして紹介しておきます。
<%@ Register TagPrefix="cc1" Namespace="skmExtendedControls" Assembly="skmExtendedControls" %> <form runat="server"> Select the Peripherals You'd Like to Include in Your Purchase:<br /> <cc1:skmCheckBoxList id="addOns" runat="server" /> </form>
Private Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) _ Handles MyBase.Load If Not Page.IsPostBack Then 'Populate CheckBoxList from database 'Also be sure to add on "None" option End If 'Add none client-side script for CheckBoxList AddNoneScriptToCheckBoxList(addOns, 0) End Sub Private Sub AddNoneScriptToCheckBoxList(ByVal cbl As CheckBoxList, _ ByVal noneIndex As Integer) 'Now, Add client-side actions Dim i As Integer For i = 0 To cbl.Items.Count - 1 If i = noneIndex Then cbl.Items(i).Attributes("onclick") = _ String.Format("skm_Uncheck('{0}', {1}, {2}, true);", _ cbl.ClientID, noneIndex, cbl.Items.Count) Else cbl.Items(i).Attributes("onclick") = _ String.Format("skm_Uncheck('{0}', {1}, {2}, false);", _ cbl.ClientID, noneIndex, cbl.Items.Count) End If Next 'Finally, add the skm_Uncheck client-side function Const SKM_UNCHECK_KEY As String = "skm_Uncheck" If Not Page.IsClientScriptBlockRegistered(SKM_UNCHECK_KEY) Then Page.RegisterClientScriptBlock(SKM_UNCHECK_KEY, _ "<script language=""JavaScript"">" & vbCrLf & _ "function skm_Uncheck(cbID, offset, total, uncheckAllButThisOne) {" & _ _vbCrLf & _ " if (uncheckAllButThisOne)" & vbCrLf & _ " for (var i = 0; i < total; i++) { " & vbCrLf & _ " var cb = document.getElementById(cbID + '_' + i);" & vbCrLf & _ " if (cb && offset != i) cb.checked = false;" & vbCrLf & _ " }" & vbCrLf & _ " else {" & vbCrLf & _ " var cb = document.getElementById(cbID + '_' + offset);" & _ vbCrLf & " if (cb) cb.checked = false;" & vbCrLf & _ " }" & vbCrLf & _ "} </script>") End If End Sub
初めてのページ訪問では、Page_LoadイベントハンドラによってCheckBoxList中の項目に値が設定されます。一般的には、ここで何らかのデータベース呼び出しが使われます。CheckBoxListに[None]オプションを追加することを忘れないでください。これは、Itemsコレクションに新しいListItemをInsert()することで実行できます。リストコントロールにプログラムでListItemを追加する方法については、『Adding a Default ListItem in a Databound Listbox in ASP.NET』を参照してください。
次に、AddNoneScriptToCheckBoxList()が呼び出され、CheckBoxListインスタンスと、CheckBoxList内での[None]チェックボックスのオフセット(位置)が渡されます(この例では、[None]チェックボックスをリストの先頭に置いているので、オフセットは0です)。この呼び出しはIf Not Page.IsPostBack条件の外にあることに注意してください。これは、CheckBoxListの項目に対する属性設定がビューステートに対してシリアル化されていないためです。つまり、ポストバックごとに属性が失われます。したがって、ポストバックのたびにクライアント側スクリプトを追加し直さなければなりません。
必要なクライアント側スクリプトをCheckBoxListの各項目に追加する作業は、AddNoneScriptToCheckBoxList()メソッドで行います。最初のループでは、CheckBoxList中のListItemを反復処理しながら、クライアント側のonclick属性にskm_UncheckというJavaScript関数の呼び出しを設定しています。skm_Uncheck関数は、もっと下にあるRegisterClientScriptBlock()呼び出しでページに挿入されます。skm_Uncheck関数は、引数として4つの値を受け取ります(CheckBoxListのID、チェックボックスのオフセット、CheckBoxList内のチェックボックスの総数、さらにオン状態のチェックボックスをクリアするのか、他のすべてのチェックボックスをクリアするのかを示す値)。ASP.NETページのサーバ側ソースコード部分からクライアント側スクリプトを作成する作業の詳細については、『Working with Client-Side Script』をお読みください。
終わりに
ASP.NET 1.xのリストコントロールが項目属性をレンダリングしないのは、ASP.NETの組み込みコントロールに内在する問題であり、バグです。リストコントロールの項目属性をいじりたければ、HTMLコントロールを使うのが最も簡単な迂回策ですが、本稿で見たとおり、独自のWebコントロールクラスを作成して、属性のレンダリングを行うこともできます。
本稿では、項目属性もレンダリングされるようにCheckBoxList Webコントロールを拡張する方法を見てきました。これで一応の問題は解決されるものの、まだ、項目属性がビューステートに保存されないという不便さが残ります。たぶん、将来の記事ではこの問題に再度触れることになるでしょう。ビューステート問題は不便ではありますが、項目のAttributesコレクションへの書き込みを、初回ページロード時だけでなく毎回行うことで迂回できます。
この拡張版CheckBoxListコントロールを使って、CheckBoxList中の各チェックボックスでクライアント側イベントを利用するデモを作成してみました。CheckBoxList中の[None]チェックボックスをクリックしてオンにすると、自動的に他のすべてのCheckBoxListチェックボックスがクリアされるようになっています。ライブデモをぜひご覧ください。
それでは、ハッピープログラミング!
