はじめに
ASP.NET 1.xには、リストコントロールとして使えるWebコントロールが4つあります。
- DropDownList
- CheckBoxList
- RadioButtonList
- ListBox
いずれもSystem.Web.UI.WebControls.ListControlクラスから派生したコントロールで、任意の数のListItemインスタンスを表示できます。その限りでは、4つとも基本的に同じものと言えますが、各コントロールの本体とその関連ListItem群のレンダリング方法はそれぞれ異なっています。たとえば、CheckBoxListの場合は本体がHTMLの<table>としてレンダリングされ、表のセル内にはCheckBox Webコントロールが配置されますが、DropDownListの場合は本体がHTMLの<select>要素、各ListItemが<option>要素としてレンダリングされます。
ただ、どのコントロールにも共通する難点として、「項目属性がレンダリングされない」という事実があります。たとえば、CheckBoxListを表示し、リスト中のいくつかのCheckBoxを特定のCSSクラスで表示したいとしましょう。あるいは、RadioButtonListコントロール中の特定のRadioButtonが選択されたときに、何らかのクライアント側JavaScriptが実行されるようにしたいとします。一般的には、CheckBoxまたはRadioButton WebコントロールのAttributesコレクションを使って設定できる機能ですが、残念なことに、リストコントロールのレンダリングでは、項目属性がレンダリングされません。
本稿では、まず、これらのリストコントロールがいったいどのようにレンダリングされるのかを理解した上で、リストコントロールのListItemインスタンスの属性もレンダリングされるようにリストコントロールクラスを拡張する方法を見ていきます。最後にちょっとしたデモがありますから、これも見てください。このデモでは、CheckBoxListの中に[None]というチェックボックスオプションを用意しており、このチェックボックスをオンにすると、クライアント側スクリプトが実行されて、リスト内の他のチェックボックスが自動的にオフになります。この機能の実装方法については、続きをお読みください。
リストコントロールのレンダリング
ASP.NET 1.xでは、RadioButtonListおよびCheckBoxListのレンダリング方法は、ListBoxおよびDropDownListのレンダリング方法とは少し異なります。RadioButtonListとCheckBoxListは、ListItemのレンダリングにSystem.Web.UI.WebControls.RepeatInfoクラスを使用します。RepeatInfoクラスは、HTMLの<table>内に項目群を垂直方向または水平方向にレンダリングします。ただし、RepeatInfoクラスは外側のHTML <table>のレンダリングだけを担当するという点に注意してください。内部要素のレンダリング処理は、外から与えられるオブジェクト(IRepeatInfoUserインターフェイスを実装しているもの)によって行われます。次の図は、RepeatInfoクラスによるデータレンダリングの流れを示しています。
ご覧のとおり、RepeatInfoクラスは、RenderRepeater()メソッドに渡されるIRepeatInfoUser実装オブジェクトにレンダリングの多くの部分を代行させます。IRepeatInfoUserインターフェイスには、HasHeader、HasFooter、RepeatedItemCountなどの諸プロパティと、実際に項目のレンダリングを行うRenderItem()メソッドが定義されています。
CheckBoxListとRadioButtonListは、このIRepeatInfoUserインターフェイスを自身で実装しています。したがって、WebコントロールがRepeatInfoクラスのRenderRepeater()メソッドに引き渡すものは、自身のインスタンスに他なりません。つまり、CheckBoxListとRadioButtonListはどちらもIRepeatInfoUserインターフェイスの必要なプロパティとメソッドを実装しているということです。CheckBoxListとRadioButtonListは、RenderRepeater()メソッドを呼び出すときに自身のコピーを引き渡します。そのため、リストコントロールの個々の項目のレンダリングの際には、リストコントロール自身のRenderItem()メソッドが呼び出され、それによって適切な項目がレンダリングされます。つまり、CheckBoxListにはCheckBoxのインスタンス、RadioButtonListにはRadioButtonのインスタンスが追加されます。
DataListもお忘れなく
ASP.NETに用意されている組み込みWebコントロールのうち、IRepeatInfoUserインターフェイスを実装しているものとしては、RadioButtonListとCheckBoxListの他にあと1つ、DataListがあります。このコントロールも、内部的にRepeatInfoクラスを使ってレンダリングを行います。前記2つがCheckBoxまたはRadioButtonをレンダリングするのに対し、DataListはDataListItemをレンダリングします。
また、上の図では、RepeatInfoクラスが「適切な<table>セルをレンダリングする」(Render appropriate <table> cell)とあります。「適切な」という言葉を使ったのは、RepeatInfoクラスを使用するWebコントロールでは、1行当たりの項目数などのフォーマットオプションを、RepeatDirection、RepeatLayout、RepeatColumnsといったプロパティで指定できるためです。このプロパティをどう設定するかで、RepeatInfoクラスが当該項目のために表内に新しい行をレンダリングするかどうかが決まります。
DropDownListとListBoxのレンダリングには、RepeatInfoクラスが使用されません。それぞれの項目は<option>要素として、RenderContents()メソッドの内部で直接レンダリングされます。具体的には、Itemsプロパティが列挙され、コレクション中のListItemごとに、<option>タグが適切なテキスト属性・値属性とともにレンダリングされます。DropDownListとListBoxとの違いは、ListBoxではレンダリングされる<select>タグにmultiple属性が加えられることです。これは、エンドユーザーがリストから複数の項目を選択できることを意味します。
リストコントロールのListItemには、なぜ属性を適用できないか
リストコントロール中の項目に属性を与えようとして、ひどくがっかりした経験をお持ちの方は多いでしょう。たとえば、「Red」、「Green」、「Blue」の3項目を含むDropDownListを作成したいとします。どうせなら各項目の背景色をそれぞれ赤、緑、青にしたいと思い、次のようなコードを書いてみました。
’Assuming items in DropDownList are:
’Red, Green, and Blue (in that order)
DropDownListID.Items(0).Attributes("style") = "background-color:red;"
DropDownListID.Items(1).Attributes("style") = "background-color:green;"
DropDownListID.Items(2).Attributes("style") = "background-color:blue;"
しかし、このページをブラウザに表示してみると、どのリスト項目の背景も白色になっているはずです。ソースを表示してみると、<option>要素にstyle属性が付加されていないことがわかります。宣言構文によって属性を指定した場合も同様で、属性は失われます。何が起こったのでしょう。
残念ながら、リストコントロールでは項目属性をレンダリングできません。これは明らかにバグであり、ニュースグループ上でもさまざまに取り上げられています。リストコントロールのソースコードをReflectorで覗いてみると、「Attributes」と見れば、それをすべてオミットするように作られていることがわかります。この理由の一部は、おそらく、リストコントロールの各インスタンスが、レンダリング前はListItemクラスで表現されていることにあるのでしょう。AttributesコレクションはSystem.Web.UI.WebControls.WebControlクラスで定義されており、自動的にビューステートに保存されます。したがって、その値はポストバックが行われても存続します。一方、ListItemクラスはWebControlから派生したクラスではなく、Attributesプロパティを持っていますが、その値はビューステートに保存されません。
項目属性もレンダリングできるようにリストコントロールを拡張する
ここには、実は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中の項目に属性を追加できます。ここで、いま述べたクライアント側動作がどのようなコードで実現されるかを、サンプルとして紹介しておきます。
HTML部分
<%@ 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チェックボックスがクリアされるようになっています。ライブデモをぜひご覧ください。
それでは、ハッピープログラミング!