問題2
ヘッダーのCheckBoxをオンにすると、グリッド上のすべてのチェックボックスがオンになるのですから、当初私は、ヘッダーのCheckBoxは一定のアクション(具体的に言うと、グリッド上のすべての下位チェックボックスをオンにするアクション)を単独で実行するユーザーインターフェイス項目であると考えていました。しかし実際には、ヘッダーのCheckBoxは、グリッド上のチェックボックスの状態を反映しているに過ぎません。つまり、ヘッダーのCheckBoxがオンになっていれば、グリッド上のすべての他のチェックボックスもオンになっていることを意味します。ヘッダーチェックボックスがオフになっていれば、グリッド上のチェックボックスの1個以上がオフになっています。
このような事情を勘案すれば、グリッド上の他のチェックボックスがすべて手動でオンにされたときにヘッダーチェックボックスが自動的にオンになるような仕組みが必要なことは明らかです。またその反対に、その他のすべてのチェックボックスがオンにされた後でその1個が手動でオフにされた場合は、ヘッダーチェックボックスが自動的にオフになる必要もあります。この仕組みを実現するため、次のように修正することにしました。
ChangeHeaderAsNeeded()
というJavaScript関数を追加し、この関数の中で、グリッド上の他のCheckBoxがすべてオンの場合はヘッダーのCheckBoxをオンにし、オフのCheckBoxが1個でもある場合はヘッダーのCheckBoxをオフにする。- 下位の各CheckBoxのクライアントサイドonclickイベントが
ChangeHeaderAsNeeded()
関数を呼び出すように関連付ける。
まずはChangeHeaderAsNeeded()
関数を見ていきましょう。この関数では、CheckBoxIDs配列の最初の要素がヘッダーのCheckBoxのIDであり、それ以外の要素が下位CheckBoxのIDであることを前提としています。ループを使って下位CheckBoxを1つずつチェックして、オンかどうかを確認します。オンではないCheckBoxが1つでもあれば、ヘッダーのCheckBoxをオフにして関数を終了します。オフの下位CheckBoxが見つからないままループが終わった場合は、ヘッダーのCheckBoxをオンにします。
function ChangeHeaderAsNeeded() { // Whenever a checkbox in the GridView is toggled, we need to // check the Header checkbox if ALL of the GridView checkboxes are // checked, and uncheck it otherwise if (CheckBoxIDs != null) { // check to see if all other checkboxes are checked for (var i = 1; i < CheckBoxIDs.length; i++) { var cb = document.getElementById(CheckBoxIDs[i]); if (!cb.checked) { // Whoops, there is an unchecked checkbox, make sure // that the header checkbox is unchecked ChangeCheckBoxState(CheckBoxIDs[0], false); return; } } // If we reach here, ALL GridView checkboxes are checked ChangeCheckBoxState(CheckBoxIDs[0], true); } }
次に、下位CheckBoxを設定して、クライアントサイドonclickイベントの発生時にChangeHeaderAsNeeded()
関数が呼び出されるようにします。具体的には、以下のクライアントサイドコードをPage_Load
イベントハンドラ内のFor Each
ループに追加します。
Protected Sub Page_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Me.Load ... Code omitted for brevity ... For Each gvr As GridViewRow In FileList.Rows 'Get a programmatic reference to the CheckBox control Dim cb As CheckBox = _ CType(gvr.FindControl("RowLevelCheckBox"), CheckBox) 'If the checkbox is unchecked, ensure 'that the Header CheckBox is unchecked cb.Attributes("onclick") = "ChangeHeaderAsNeeded();" 'Add the CheckBox's ID to the client-side CheckBoxIDs array ClientScript.RegisterArrayDeclaration( _ "CheckBoxIDs", String.Concat("'", cb.ClientID, "'")) Next End Sub
これで終わりです。この変更により、すべてのチェックボックスを手動でオンにするとヘッダーのCheckBoxがオンになります。同様に、すべての下位チェックボックスがオンのときに1個のチェックボックスをオフにすると、ヘッダーのCheckBoxがオフになります。
ヘッダーのCheckBoxのIDが常に最初にあると前提しても安全か?
ChangeHeaderAsNeeded()
関数では、ヘッダーのCheckBoxのIDが常にCheckBoxIDs配列の最初のIDであることを前提としています。でも、この前提は常に正しいのでしょうか? コードでは、下位CheckBoxのIDを追加する前にヘッダーのCheckBoxのIDを追加するので、そう信じる根拠はあります。ヘッダーのCheckBoxのIDは、CheckBoxIDs配列の最初の要素になるはずです。
この前提が正しいのは、ASP.NET 2.0を使用しているときです。ASP.NET 2.0の内部のRegisterArrayDeclaration(arrayName, arrayValue)
メソッドでは、ListDictionaryオブジェクトに配列要素を格納してから、それらの要素をクライアントサイドの配列にレンダリングするからです。ListDictionaryはFIFO(First In, First Out)で動作するので、要素はサーバーサイドコードで追加した順序でクライアントサイド配列に格納されます。
ただし、ASP.NET 1.xを使用していると、RegisterArrayDeclaration(arrayName, arrayValue)
の実装でHybridDictionaryが使われます。HybridDictionaryでは、最初にListDictionaryを使って項目が保存されますが、要素が一定の数を超えると(具体的には9番目の要素から)Hashtableが使われます。Hashtableの要素は、ハッシュ値に基づく順序で返されるため、必ずしもプログラムで追加した順序と同じではありません。この問題を回避するには、RegisterArrayDeclaration
メソッドのロジックを独自の方法(素のJavaScriptそのものを吐き出すなど)で置き換えるか、Peter BlumのフリーのRegisterScriptsライブラリを使います。HybridDictionaryの順序の問題と使用可能な迂回策については、記事『Specialized Collections』を参照してください。
まとめ
この記事では、クライアントサイドのテクニックを使ってGridViewのヘッダーに[Check/Uncheck All]チェックボックスを追加して、すべての下位チェックボックスを一度にオンまたはオフにする機能を用意する手順を説明しました。[Check All]ボタンと[Uncheck All]ボタンに加えてヘッダーの[Check/Uncheck All]チェックボックスを用意することで、グリッドのすべてのチェックボックスをオンまたはオフにする複数の方法をユーザーに提供できます。クライアントサイドスクリプトを使ってチェックボックスのオン、オフを処理するので、このインターフェイスは応答速度が速くなります。
それでは、ハッピープログラミング!