japan.internet.comThe Internet & IT Network
RSS
  • ニュース
  • コラム
  • リサーチ
  • ヘッドライン
  • 特集
  • ブログ
  • プレスリリース
  • 専門チャンネル
  • イベント
  • ランキング
  • ニュースメール
2009年7月4日
文字サイズ文字サイズ小文字サイズ中文字サイズ大
デベロッパー2005年11月8日 10:00

Webカスタムコントロールの作成

海外海外internet.com発の記事
  • このエントリーを含むはてなブックマーク
  • この記事をクリップ!
  • Buzzurlにブックマーク
  • Yahoo!ブックマークに登録
  • newsing it!
  • この記事をokyuuへインポート

目次

前提条件

  • C#、Visual Studio .Net、およびASP.NETアプリケーション作成に詳しいこと
  • ADO.NETおよびデータバインディングの基本を理解していること
  • HTMLおよびカスケーディングスタイルシート(CSS)の基本を理解していること

はじめに

 ASP.NETページで利用できるようなサーバーコントロールを設計したいと思ったことはありませんか? 私があるページを手がけていたとき、CheckBoxListのようなコントロールが必要になりました。一連のチェックボックスをグループにまとめ、それぞれのグループにカテゴリ見出しを付ける必要がありました。このコントロールに配置すべきアイテムとアイテムカテゴリはデータベースに格納されていました。しかし、標準のCheckBoxListコントロールでは、チェックボックスをカテゴリ別に表示することができません。

 もちろん、CheckBoxListコントロールをうまく使いながらこうした問題の解決を試みる方法は数多くあります。簡単に思いつく方法は2つあります。1つは、カテゴリまたはグループごとに個別のCheckBoxListコントロールをASPXページに追加し、カテゴリ見出しはハードコーディングするという方法です。もう1つは、分離コードファイルのPage_Loadメソッドを使用して、実行時にCheckBoxListコントロールをプログラム的にページに追加するという方法です。

 私のプロジェクトでは、チェックボックスとして表示されたアイテムのなかからユーザーが任意の数のアイテムを選択できるフォームを作成する必要がありました。ここでは仮に、カテゴリは自動車メーカー、アイテムは車種を表すものとして話を進めます。アイテムとカテゴリはデータベースに格納されていたので、新たなメーカーや車種の追加のほか、既存エントリの変更や削除も行うことができました。

 「カテゴリ名とCheckBoxListコントロールをWebフォーム上にハードコーディングする」という選択肢はすぐに除外しました。メーカーの追加、削除、名前変更が起こるたびにASPXページとC#の分離コードファイルの更新を行うのは非現実的だったからです。

 また、実行時にコントロールをフォームに追加するという選択肢も賢明とは言えません。プログラミングによってWebコントロールをプレースホルダや他のコンテナコントロールに追加するのはとても容易です。しかし問題は、ユーザーの指定した値を読み取るためには、ポストバックの状態やチェックボックスの表示と非表示に関わらず、ページを読み込むたびにコントロールの「リビルド」(プログラムによるコントロールの再作成)を行わなければならないという点です。これではサーバーに相当な負荷がかかってしまいます。

 私が本当に欲しかったのは、フォームにドラッグアンドドロップしたり、いくつかのプロパティを設定したり、データバインディングコードを追加したりでき、デザイン時にきちんと形ができているコントロールでした。標準のWebコントロールにはぴったり来るものがなさそうだったので、自分の手でカスタムサーバーコントロールを作ることにし、これを「CategorizedCheckBoxList」と名付けました。コントロールを一から作り上げると思うと気力が萎えてしまうかもしれませんが、実際にはそれほど難しいものではないことがわかりました。

コントロールの背景知識

 Webカスタムコントロールは、いくつかの点でユーザーコントロールよりも優れています。まず、ユーザーコントロールは、それを利用するプロジェクトごとにASCXやクラスファイルをコピーしなければならないので、再利用に適していません。また、Visual Studio .NETでデザイナサポートが用意されていないという点でも不利です。ユーザーコントロールをWebフォームに配置してみれば、私の言いたいことがわかるはずです。ユーザーコントロールは汎用的なグレーボックスで表示され、ユーザーコントロールのプロパティ値を示すヒントはIDEから一切提供されません。ユーザーコントロールの主な利点と言えば、作成が容易なので迅速なアプリケーション開発ができるということくらいです。

 一方、Webカスタムコントロールには大きな魅力があります。サーバーコントロールにはASCXファイルが存在しないので、非常に簡単に再利用できます。DLLへのプロジェクト参照を設定しさえすれば、すぐに動作させることができます。WebカスタムコントロールはVisual Studio .NETツールボックスに追加でき、こうすることによって再利用は極めて容易になります。さらに、グローバルアセンブリキャッシュ(GAC)にWebカスタムコントロールをインストールすることも可能です。なお、GACの詳細については、本稿の範囲を超えるのでこれ以上は触れません。ともかく、私がWebカスタムコントロールを気に入っている最大の理由は、申し分のないカスタマイズが可能で、デザイン時サポートがあるということです。

 Webカスタムコントロールを作成するにあたっての最大の障壁は、コントロール用のHTMLを記述しなければならないという点でしょう。Visual Studio .NETにはWebカスタムコントロールのためのビジュアルデザイナがないので、ツールボックスのアイテムを自作のコントロールにドラッグアンドドロップすることができません。しかし、C#プログラムを習得できるくらいなら、HTMLを覚えるのも苦ではないでしょう。

プロジェクト再利用のための計画

 これから作成するコントロールを簡単に再利用できるようにするために、新しいWebコントロールライブラリのプロジェクトを用意することにしましょう。

 Visual Studio .NETによって、「WebCustomeControl1.cs」というクラスが作成されます。この名前を「CategorizedCheckBoxList.cs」に変更します(なお、クラスファイル内の記述もWebCustomControl1CategorizedCheckBoxListに置き換えるようにしてください)。

データバインドプロパティ

 今回作成するコントロールにはデータソースをバインドする必要があります。問題を簡単にするために、このコントロールではADO.NETのDataTableをデータソースとして使用することにしました。DataTable型を選んだのは、コントロールにはデータやデータの取得方法に関する情報をあまり持たせたくなかったからです。今回のコントロールが必要とするデータは、DataTable、カテゴリ名として使われるフィールド名、チェックボックスの値、それに各チェックボックスのラベルとなるテキストだけです。それぞれに関するデータバインドプロパティを次に示します。

  • DataTable
  • System.Data.DataTable型。データソースとして利用されるテーブルを表します。このテーブルには、カテゴリ名の列、チェックボックス値の列、および各チェックボックスのラベルの列を必ず含めなければなりません。今回のコントロールは、カテゴリを第一キー、ラベルを第二キーとしてこのテーブルを自動的にソートします。
  • DataCategoryColumn
  • 文字列型。DataTableのテーブル内で、アイテムの所属カテゴリ名を格納している列の名前を表します。
  • DataValueColumn
  • 文字列型。DataTableのテーブル内で、チェックボックスの値を格納している列の名前を表します。DataValueColumnDataTextColumnに同じ列名を指定できます。DataValueColumnには任意の型のデータを格納できますが、文字データを使用する場合、カンマを含めることはできません。
  • DataTextColumn
  • 文字列型。DataTableのテーブル内で、チェックボックスの隣に表示されるラベルを格納している列の名前を表します。

 また、ページを読み込むときにチェックマークを入れるべきチェックボックスを指定する手段も必要になります。この機能を用意しておくと、フォームのデフォルト値を設定したり、以前に保存しておいた値をユーザーが編集するようなフォームを実現したりできます。そのためのプロパティを、ArrayListオブジェクトを使って用意することにしました。

  • Selections
  • 文字列値を格納するArrayListオブジェクト。選択状態(チェックマークがオンの状態)としてマークすべき値のリストです。このプロパティを利用して、選択状態の値を指定し、ポストバック後に選択状態の値のリストを取得することができます。

CSSとその他の視覚的プロパティ

 CSSクラスセレクタのための追加プロパティを公開することで、CategorizedCheckBoxListコントロールの視覚的スタイルをASPXページから制御することができます。

  • TableCssClass
  • 文字列型。一連のチェックボックスを配置するテーブルのスタイルを指定するCSSクラスです。
  • RowCssClass
  • 文字列型。チェックボックスを含む各テーブル列のスタイルを指定するCSSクラスです。
  • CategoryCssClass
  • 文字列型。カテゴリ見出しを含むセルのスタイルを指定するCSSクラスです。
  • CheckBoxCssClass
  • 文字列型。チェックボックスを含むセルのスタイルを指定するCSSクラスです。
  • TextCssClass
  • 文字列型。チェックボックのラベルを含むテーブルセルのスタイルを指定するCSSクラスです。

 さらに次のようなプロパティを追加しました。

  • TableWidth
  • 文字列型。テーブルの幅をピクセル数またはパーセント(%記号を付ける)で指定します。
  • CellPadding
  • 整数型。テーブルセルの内側の余白を指定します。
  • CellSpacing
  • 整数型。テーブルセルの間隔を指定します。
  • Columns
  • 整数型。チェックボックスの表示に用いる列数を指定します。
  • SharedTable
  • ブール型。Trueの場合は、すべてのチェックボックスを1つの同じテーブルに表示します。Falseの場合は、一連のチェックボックスをカテゴリごとに独立したテーブルに表示します。このプロパティを用意したのは、チェックボックスのラベルの長さがカテゴリごとに大きく異なる場合に、ページの見た目が悪くなってしまうからです。

Renderメソッド

 Webカスタムコントロールを扱うときには、Renderメソッドが大活躍します。簡単に言うと、Renderメソッドは、Webブラウザ上にコントロールを表示するためのHTMLを出力します。ちょうどクラシックASPのResponse.Writeを使うのと同じです。ただし、Renderメソッドでは、ホストASPXページから自動的に渡されるHtmlTextWriterオブジェクトを利用する点が異なります。

 細かい処理はすべてVisual Studio .Netが面倒を見てくれるので、このWebカスタムコントロールをページ上で使用するための特別なコーディングをする必要はありません。ページにWebカスタムコントロールを配置するだけで、互いにどのようにやりとりをすればよいかをページとコントロールが理解してくれるのです。

 では、今回のWebカスタムコントロールのRenderメソッドを詳しく見てみましょう。

/// <summary>
/// Writes out the HTML needed to render this control.
/// </summary>
/// <param name="output">The HTML text writer the we will utilize.
/// This is passed to the control automatically,
/// by the host ASPX page.</param>
protected override void Render(HtmlTextWriter output)
{
    try
    {
        // No need for ViewState
        this.EnableViewState = false;
        // Make sure that the htmlFieldName is set.
        GetHtmlFieldName();
        // Should the control be visible?
        if(this.Visible == true)
        {
             // Yes. Render the html.
             BuildCategorizedCheckBoxList(output);
        }
    }
    catch(Exception ex)
    {
        // Something bad happened. 
        // Let’s tell the user what that was.
        output.Write("Error building CategorizedCheckBoxList:<br>");
        output.Write(ex.Message);
    }
}

 ご覧のとおり、最初にコントロールのViewStateを無効にします。これにより、Webブラウザに送られるHTMLの負荷が軽減されます。次に、ReadPostBackというメソッドを呼び出します。このメソッドにより、フォームが発行された場合に、どのチェックボックスが選択されていたかを把握します。最後に、コントロールを可視化する場合にはBuildCategorizedCheckBoxListメソッドを呼び出して、コントロールを表示するためのHTMLを出力させます。

 ReadPostBackメソッドは、チェックボックスフィールドに割り当てている名前を取得し、Request.Formコレクションに同じフィールド値がないか探します。コントロールを含むWebフォームがポストバックされると、ASP.NETはフォーム上のすべてのフィールドを含むNameValueCollectionの形でそのFormオブジェクトを公開します。フィールドがない場合はそのFormオブジェクトはnullになり、フィールドがある場合はif文内のコードが実行されます。

/// <summary>
/// Retrieves a list of the checkbox values that were
/// selected (checked), if the form was submitted.
/// This is kind of a poor-man’s view state implementation.
/// But unlike view state,
/// it doesn’t add anything to the page weight.
/// </summary>
protected void ReadPostBack()
{
    // See what field name we are assigning to the checkboxes
    GetHtmlFieldName();
    // Were any checkboxes checked?
    if(HttpContext.Current.Request.Form[htmlFieldName] != null)
    {
        // Since we assigned the same field name to all of
        // the checkboxes, ASP.NET will give us
        // a comma-delimited list of the selections.
        // First, conver the list to a string array.
        string [] Input = HttpContext.Current
            .Request.Form[htmlFieldName].Split(’,’);
        // Then, iterate through the array and add
        // each value to our ArrayList.
        for(int i = 0; i < Input.Length; i++)
        {
            selections.Add(Input[i]);
        }
    }
}

 詳しくは後述しますが、チェックボックスフィールドのHTMLを作成すると、各フィールドに同じ名前が割り当てられていることがわかるでしょう。ASP.NETがRequest.Formコレクションにフィールドを格納するときには、複数の値を持つフィールドをカンマ区切りリストにまとめます。あとはこのカンマ区切りリストを文字列の配列に変換し、それぞれの値を選択状態変数(つまりArrayList)に追加するだけです。

 ReadPostBackメソッドは、パブリックプロパティであるSelectionにアクセスするときにも呼び出されます。この呼び出しが必要なのは、ページ読み込み時に発生するイベントのシーケンス上の理由からです。これにより、ホストページ側は、どのチェックボックスが選択されていたかをコントロールの描画に先立って把握できるのです。

 次に示すのは、GetHtmlFieldNameメソッドです。

/// <summary>
/// Returns the unique field name
/// that we will assogn to the checkboxes, later.
/// </summary>
protected void GetHtmlFieldName()
{
    // Pickup the ID assigned to the control
    // in the consuming ASPX page
    htmlFieldName = this.ID;
}

 GetHtmlFieldNameメソッドは、利用側のASPXページによってコントロールに割り当てられたIDの値をhtmlFieldNameというプロテクト変数に割り当てます。なぜこのIDをわざわざ取り上げるのか不思議に思われるかもしれません。ここでは、チェックボックスフィールドの作成時に使う名前を自分の都合に合わせてハードコーディングすることを避けたかったので、このIDを利用しています。

 フィールド名が確実に一意であれば、複数のCategorizedCheckBoxListコントロールを同じWebフォーム上に配置し、どのチェックボックスが選択されているのかを各コントロールに適切に判断させることもできます。Visual Studio .NETは、プログラマが1つのページ上の複数のコントロールに同一IDを割り当てないようにしてくれます(あるいは少なくともその手助けをしてくれます)。ですから、このIDプロパティは私たちの目的にかなったものだと言えるでしょう。

 BuildCategorizedCheckBoxListは、少々複雑なメソッドです。このメソッドは、出力ストリームに直接書き込みを行うためにRenderメソッドからHtmlTextWriterオブジェクトを取得します。このオブジェクトは、コントロール用HTMLを出力するために必要となる面倒な作業をすべて行ってくれます。それでは、このメソッドの処理内容をいくつかのセクションに分けて見ていきましょう。

 まず、DataTableにデータが入っているか否かの確認から始めます。データが入っていなければ、キーワードreturnによってこのメソッドから抜け出します。

/// <summary>
/// Outputs the HTML for this control.
/// </summary>
/// <param name="output"></param>
protected void BuildCategorizedCheckBoxList(HtmlTextWriter output)
{
    // Do we have any data?
    if(dataTable == null || dataTable.Rows.Count < 1)
    {
        // There is no data, so there’s nothing to render.
        return;
    }

 次に、チェックボックスのカテゴリ、値、ラベルを示す列を探します。これらの列が見つかれば、ローカル変数にインデックスを割り当てます。この値は後で使用します。テーブルの列を名前で参照するのは、インデックス番号で参照するよりもずっと時間がかかるので、このコードではインデックスで参照することにしました。

 初期化時に、これらの列のインデックス値をわざと-1に設定します。すべてそうしておく必要があります。というのは、DataRawにおいて列のインデックス番号が-1の列の値を取得しようすると、.NETフレームワークが例外を発行してくれるからです。

// First retrieve the column indexes of
// the specified columns. Later, we’ll get
// the values that we need using these indexes.
// This is faster than referencing a column by name.
int CategoryColumnIndex = -1;
int TextColumnIndex = -1;
int ValueColumnIndex = -1;
for(int i = 0; i < dataTable.Columns.Count; i++)
{
    if(dataTextColumn == dataTable.Columns[i].ColumnName)
    {
        TextColumnIndex = i;
    }

    if(dataValueColumn == dataTable.Columns[i].ColumnName)
    {
        ValueColumnIndex = i;
    }
    if(dataCategoryColumn == dataTable.Columns[i].ColumnName)
    {
        CategoryColumnIndex = i;
    }
}

 次に、すべてのカテゴリとそのチェックボックスを同じテーブル上に表示するかどうかを判断します。同じテーブルに表示する場合は、次のようにテーブルの開始タグを書き出します。

/**********************************/
/* Build the html to display of the items */
/**********************************/
// If the consuming page wants one single, shared table,
// write the opening tag, now.
if(this.sharedTable == true)
{
    output.Write(GetTableTag());
}

 GetTableTagは、テーブルの開始タグ用HTMLを生成するヘルパーメソッドです。より正確に言えば、このメソッドは、tableTagというプライベート変数の値がもしnullであれば値を割り当てたうえで、その値を返すものです。話を簡単にするため、GetTableTagの詳細についてここでは詳しく触れません。ただ、文字列は不変のデータ型なので、あとで何度も文字列に追加していくようなときは必ずStringBuilderオブジェクトを使うということを覚えておけばいいでしょう。

 話を元に戻しましょう。続いて、DataTableに含まれるカテゴリのリストを取得する必要があります。そこでまず、このDataTableからDataViewを生成し、カテゴリ列に基づいてソートを行います。LastCategoryというローカル変数を用意し、これを使ってDataViewの各列を参照しながら新たなカテゴリ名が現れていないか繰り返しチェックしていきます。新しいカテゴリが現れるたびに、そのカテゴリをCategoriesというArrayListに追加します。

// Create a string for the "previous" category
string LastCategory = string.Empty;
// Sort the data by category
DataView Category = dataTable.DefaultView;
Category.Sort = this.dataCategoryColumn;
// Assemble a distinct list of the categories
// found in the data
ArrayList Categories = new ArrayList();
for(int i = 0; i < Category.Count; i++)
{
    if(LastCategory != Category[i][CategoryColumnIndex].ToString())
    {
        Categories.Add(Category[i][CategoryColumnIndex].ToString());
        LastCategory = Category[i][CategoryColumnIndex].ToString();
    }
}

 これでようやくカテゴリのリストができあがったので、このリストに対してループ処理を行い、各カテゴリ(および対応するチェックボックス)のHTMLを出力します。これを行うために、DataViewを作成し、そのビューの列数を対象カテゴリの列数以内に制限するRowFilterを設定します。

// Loop through the categories
for(int i = 0; i < Categories.Count; i++)
{
    // Get the rows for this category only
    DataView CategoryItems = new DataView(dataTable);
    CategoryItems.RowFilter = String.Format("{0}=’{1}’",
        this.dataCategoryColumn,
        Categories[i].ToString().Replace("’","’’"));
    CategoryItems.Sort = this.dataTextColumn;

 カテゴリごとに別々のHTMLテーブルを作成する場合は、この時点でテーブルの開始タグを出力します。

// If the consuming page wants a separate table
// for each category, write the opening tabel tag
// for the current category, now.
if(this.sharedTable == false)
{
     output.Write(GetTableTag());
}

 今度は、現在処理しているカテゴリ用のHTMLを出力します。ヘルパーメソッドであるOutputCategoryRowを使用して、必要なHTMLを出力します。

// Add the category heading to the html
OutputCategoryRow(output, (string)Categories[i]);

 この段階になると、話が少し複雑になってきます。現在処理しているカテゴリに属するアイテム数や、チェックボックスの表示に使用する列数はわかっています。しかし、すべてのアイテムの表示に必要な列数が明確になっていません。

 そこで、カテゴリ内の全アイテム数を表示列数で割ることによって、全アイテムの表示に必要な列数を求めます。余りが出た場合には、求めた列数に1を加えます。

// Calculate the total number of rows based on
// the item count and the number of columns
totalItems = CategoryItems.Count;
totalRows = totalItems / columns;
// If there was anything left-over as a result of
// the division, we need to add another row
if(totalItems % columns > 0)
{
    totalRows++;
}

 次に、列を処理するループに入ります。現在処理中のアイテムのインデックス番号を保持するためのカウンタを用意します。このカウンタの値は0から始まります。各アイテムのHTMLを書き出すたびに、最後のアイテムでないかチェックします(CurrentItemIndexの値が「全アイテム数-1」に等しければ、最後のアイテムの列を作成し終えたことになります)。

 現在のカテゴリにおける最後のアイテムのHTML出力が終わったら、CurrentItemIndexを-1に設定します。CurrentItemIndexの値が-1なのに表示すべき列がまだ残っている場合には、チェックボックスとその隣のテキストの両方に対して空のテーブルセル用HTMLを出力します。

// Create an integer to hold the index number
// for the current item
int CurrentItemIndex = 0;
// Now loop through the rows
for(int Row = 0; Row < totalRows; Row++)
{
    // Determine the starting index for this row.
    // This is the same calculation that we would perform
    // to handle paging in a grid.
    int Start = (Row * columns);
    // Create an integer to hold the index number
    // for the current item
    int CurrentItemIndex = Start;
    // Start the row
    output.Write("");
    output.Write("<tr class="");
    output.Write(this.rowCssClass);
    output.Write("">");
    output.Write("
");
    // Column loop
    for(int Col = 0; Col < columns; Col++)
    {
        // Make sure that we haven’t hit a blank entry.
        if(CurrentItemIndex == -1)
        {
            // Add an empty cell (two, actually)
            AddEmptyCells(output);
        }
        else
        {
            // Now add the checkbox and text
            OutputCheckBox(output,
                htmlFieldName,
                CategoryItems[CurrentItemIndex]
                    [TextColumnIndex].ToString(),
                CategoryItems[CurrentItemIndex]
                    [ValueColumnIndex].ToString(),
                IsChecked(CategoryItems[CurrentItemIndex]
                    [ValueColumnIndex].ToString())
                );
            // If we have more data left,
            // increment the current index counter.
            if(CurrentItemIndex < (totalItems - 1)
                && CurrentItemIndex != -1)
            {
                // increment the current index
                CurrentItemIndex++;
            }
            else
            {
                // We’re at the end of the items
                // in the data table. Set the value of
                // the current index to -1,
                // which our rendering code
                // ignores (creates empty table cells)
                CurrentItemIndex = -1;
            }
        }
        // Add a line break
        output.Write("
");
    }

 この時点で列内のすべての列の処理が終わっているので、列の終わりに必要な処理を行います。

    // End the row
    output.Write("");
    output.Write("</tr>
");
}

 このループは、すべての列に対する処理が終了するまで繰り返されます。最後に、カテゴリごとに別々のテーブルを使う場合には、現在のカテゴリテーブルを次のようにして終了させます。

    // Table tag
    if(this.sharedTable == false)
    {
        output.Write("</table>
");
    }
}

 このループは、すべてのカテゴリの処理が終了するまで繰り返されます。共有テーブルを使用する場合は、ループの最後でテーブルの終了タグを出力します。

    // Finish the table
    if(this.sharedTable == true)
    {
        output.Write("</table>
");
    }

    /**********************************/
}

 これまで説明していなかったヘルパーメソッドのOutputCheckboxIsCheckedについて、ここで述べておきましょう。OutputCheckboxは、チェックボックスフィールドを含むテーブルセルと、その隣のラベルを含むテーブルセルのためのHTMLを出力します。ここでは、ラベルが2番目の行に重ならないようにするために、チェックボックスとそのラベルを別々のセルに配置しました。これにより、すべての要素をきれいに配置することができます。

 IsCheckedメソッドは、チェックボックスに「チェック」を付けるか付けないかを決めるために用いられます。ここで使っているSelectionsという変数はArrayListオブジェクトなので、その実装は容易です。

/// <summary>
/// Looks for a match between the current value and
/// the list of selected values.
/// </summary>
/// <param name="currentValue">The value that we want to look for.
/// </param>
/// <returns>True if the current value is contained in
/// the selected list. Otherwise, false.</returns>
protected bool IsChecked(string currentValue)
{
    // If we have selections, continue
    if(selections != null && selections.Count > 0)
    {
        // Can we find the current value?
        if(selections.IndexOf(currentValue) > -1)
        {
            // Yes, so this item should be marked
            // as selected (checked)
            return true;
        }
        else
        {
            // No, so this item should not be selected
            return false;
        }
    }
    else
    {
        // Nothing at all was selected, so return false
        return false;
    }
}

コントロールによるHTML出力

 ヘルパーメソッドであるGetTableTagOutputCategoryRow、およびOutputCheckBoxを詳しく調べてみれば、TABLEタグ、TRおよびTDタグ、それにチェックボックスフィールドタグにCSSの「クラス」属性が含まれていることにおそらく気がつくでしょう。コントロールのパブリックプロパティにおいて指定されたCSSクラスをこうしたタグのために利用します。そうすれば、CategorizedCheckBoxListコントロールの外観と印象をASPXページによって管理できます。

 また、TABLE、TR、およびTDの各タグのHTMLに改行( )とタブ()が追加されていることにも気が付いたかもしれません。こうした記号の追加は、どうしても必要なものではありませんし、かえってWebブラウザがダウンロードするHTMLのサイズを増やすことになります。しかし、結果として得られるHTMLは非常に読みやすいものになります(このページのソースをご覧になればわかります)。ただし、製品版アプリケーションでこのコントロールを実装する場合には、こうした余白文字は削除しておくのがよいでしょう。

デザイン時サポート

 さて、ここまでは順調です。しかし依然として、Visual Studio .NETのページデザイナでは、このコントロールは汎用的なグレーボックスとして表示されたままです。さらに、プロパティウィンドウには、このコントロールのプロパティが1つも表示されません。このコントロールにデザイン時サポートを加えるためには、まずクラスおよびプロパティの宣言部にコード属性を追加する必要があります。

 Visual Studioのデザイン時サポートを加える際には、パブリックプロパティにコード属性を設定します。

[Category("Appearance"),
DefaultValue(""),
Description("The CSS Class name for the table row" 
    + "containing each item.")]
public string RowCssClass
{

 プロパティ宣言部の上に記述した大カッコ内のコードは、このプロパティをプロパティウィンドウにどのように表示するかをVisual Studio .NETに指示しています。

一般的な属性
Categoryプロパティが現れるカテゴリを定めます。次の共通オプションがあります。
●・Appearance
Behavior
Data
Descriptionプログラマがプロパティの設定を行う際の参考としてプロパティボックスの最下部に表示される説明です。
DefaultValueデフォルト値を表します。
Browsableプロパティをプロパティボックスに表示するかどうかを指定します。次のいずれかの値をとります。
true
false

 ご覧のとおり、プロパティウィンドウのサポートは簡単に実現できます。

 しかし、コントロール自身のデザイン時サポート、つまりブラウザ上でのコントロールの描画をより正確に示すためのデザイン時サポートを追加するには、もう少し複雑な作業が必要です。そのためには、System.Web.UI.Design.ControlDesigner型から派生させた新しいクラスを作成し、CategorizedCheckBoxListクラスにDesigner属性を追加しなければなりません。Designer属性は、このコントロールのデザイン時HTMLを出力するときに特別なクラスを使用するようVisual Studioに指示します。

 ControlDesigner基底クラスには、GetDesignTimeHtmlというメソッドが用意されています。このメソッドは、Visual Studio .NETによって使用されるHTMLを作成し、デザインビューのページ上にコントロールを描画します。このメソッドをオーバーライドすることによって、独自のHTMLを書き出し、コントロールの外観を自在に制御することができます。

 ただし、Visual Studio .NET IDE内でのCategorizedCheckBoxListの外観と実際のWebアプリケーション上での外観を完全に一致させようとは考えないでください。そうすると、必要以上にコードが複雑になってしまうからです。しかし、何百行もコードを書かなくても、コントロールの基本的なルックアンドフィールを再現することは可能です。Microsoftお気に入りの言葉に従い、「Do more with less(最小のコストで最大の効果を得よ)」という路線で考えてみましょう。

 ControlDesignerのコードを次に示します。

/// <summary>
/// Provides a moderate level of fidelity
/// for the CategorizedCheckBoxList control
/// in the VS.net IDE.
/// </summary>
internal class CategorizedCheckBoxListControlDesigner :
    System.Web.UI.Design.ControlDesigner
{
    /// <summary>
    /// Provides easy access the properties set in the IDE.
    /// </summary>
    protected CategorizedCheckBoxList ccbl;
    /// <summary>
    /// Initializes the designer
    /// </summary>
    /// <param name="component"></param>
    public override void Initialize(IComponent component)
    {
        // Make sure that this designer is attached to
        // a CategorizedCheckBoxList
        if(component is CategorizedCheckBoxList)
        {
            base.Initialize (component);
            ccbl = (CategorizedCheckBoxList)component;
        }
    }

 このクラスでinternalというアクセス修飾子を使用していることに注意してください。このCategorizedCheckBoxLisControlDesignerクラスは大幅にカスタマイズされているので、CategorizedCheckBoxListクラスでのみ使用されると考えてよいでしょう。ControlDesignerinternalとして宣言すると、同じ.NETアセンブリ内のファイルからしか利用できなくなります。

 最初に、Visual Studioで設定されたプロパティにアクセスするためのCategorizedCheckBoxList変数を生成します。次に、Initializeメソッドをオーバーライドします。このデザイナを使用するコンポーネントがCategorizedCheckBoxListである場合は、そのコンポーネントをロードして、CategorizedCheckBoxListプロテクト変数を初期化します。

 GetDesignTimeHtmlメソッドを実行すると、CategorizedCheckBoxListをサンプルデータと一緒に表示するためのHTMLが次のように生成されます。なお、太字のコードはCategorizedCheckBoxListから取得されるプロパティを示しています。

/// <summary>
/// Writes the HTML used be VS.net to display
/// the control at design-time.
/// </summary>
/// <returns></returns>
public override string GetDesignTimeHtml()
{
    try
    {
        // Start building the HTML
        StringBuilder Sb = new StringBuilder();

        // Table
        Sb.Append("<table");
        Sb.Append(" cellspacing="");
        Sb.Append(ccbl.CellSpacing.ToString());
        Sb.Append(""");
        Sb.Append(" cellpadding="");
        Sb.Append(ccbl.CellPadding.ToString());
        Sb.Append(""");
        Sb.Append(" border="0">");
        // Category Row
        Sb.Append("<tr><td colspan="4"");
        Sb.Append(" class="");
        Sb.Append(ccbl.CategoryCssClass);
        Sb.Append(""");
        Sb.Append(">Fruit</td></tr>");
        // Item Row
        Sb.Append("<tr valign="top">");
        Sb.Append("<td><input type="checkbox" "
            + "name="1" value="1" class="");
        Sb.Append(ccbl.CheckBoxCssClass);
        Sb.Append(""></td>");
        Sb.Append("<td class="");
        Sb.Append(ccbl.TextCssClass);
        Sb.Append("">Apples</td>");
        Sb.Append("<td><input type="checkbox" "
            + "name="1" value="2" class="");
        Sb.Append(ccbl.CheckBoxCssClass);
        Sb.Append(""></td>");
        Sb.Append("<td class="");
        Sb.Append(ccbl.TextCssClass);
        Sb.Append("">Oranges</td>");
        Sb.Append("</tr>");
        // Item Row
        Sb.Append("<tr valign="top">");
        Sb.Append("<td><input type="checkbox" "
            + "name="1" value="3" class="");
        Sb.Append(ccbl.CheckBoxCssClass);
        Sb.Append(""></td>");
        Sb.Append("<td class="");
        Sb.Append(ccbl.TextCssClass);
        Sb.Append("">Tangerines</td>");
        Sb.Append("<td></td>");
        Sb.Append("<td class="");
        Sb.Append(ccbl.TextCssClass);
        Sb.Append(""></td>");
        Sb.Append("</tr>");
        // Category Row
        Sb.Append("<tr><td colspan="4"");
        Sb.Append(" class="");
        Sb.Append(ccbl.CategoryCssClass);
        Sb.Append(""");
        Sb.Append(">Vegetables</td></tr>");
        // Item Row
        Sb.Append("<tr valign="top">");
        Sb.Append("<td><input type="checkbox" "
            + "name="2" value="1" class="");
        Sb.Append(ccbl.CheckBoxCssClass);
        Sb.Append(""></td>");
        Sb.Append("<td class="");
        Sb.Append(ccbl.TextCssClass);
        Sb.Append("">Broccoli</td>");
        Sb.Append("<td><input type="checkbox" "
            + "name="2" value="2" class="");
        Sb.Append(ccbl.CheckBoxCssClass);
        Sb.Append(""></td>");
        Sb.Append("<td class="");
        Sb.Append(ccbl.TextCssClass);
        Sb.Append("">Green Beans</td>");
        Sb.Append("</tr>");
        // Item Row
        Sb.Append("<tr valign="top">");
        Sb.Append("<td><input type="checkbox" "
            + "name="2" value="3" class="");
        Sb.Append(ccbl.CheckBoxCssClass);
        Sb.Append(""></td>");
        Sb.Append("<td class="");
        Sb.Append(ccbl.TextCssClass);
        Sb.Append("">Potatoes</td>");
        Sb.Append("<td><input type="checkbox" "
            + "name="2" value="4" class="");
        Sb.Append(ccbl.CheckBoxCssClass);
        Sb.Append(""></td>");
        Sb.Append("<td class="");
        Sb.Append(ccbl.TextCssClass);
        Sb.Append("">Tomatoes</td>");
        Sb.Append("</tr>");
        // End the table
        Sb.Append("</table>");
        return Sb.ToString();
    }
    catch(Exception ex)
    {
        // Display the error in VS.net, in Design view
        return String
            .Concat("<h3>Error</h3>Stack Trace:<br>", ex.StackTrace);
    }
}

 ここまでくれば、後はこのControlDesignerCategorizedCheckBoxListコントロールから利用できるように指定するだけです。そのために、もう一度コード属性を使います。

/// <summary>
/// The CategorizedCheckBoxList is like a CheckBoxList,
/// but with the ability to categorize the display of items.
/// </summary>
[Designer(
"UsefulControls.CategorizedCheckBoxListControlDesigner"),
DefaultProperty("DataTable"),
ToolboxData("<{0}:ccbl runat=server></{0}:ccbl>")]
public class CategorizedCheckBoxList :
    System.Web.UI.WebControls.WebControl
{
    /// <summary>
    /// Initializes a new instance of
    /// the CategorizedCheckBoxList class.
    /// </summary>
    public CategorizedCheckBoxList()
    {
        // Init the class
    }

 以上でCategorizedCheckBoxListコントロールのコーディングは終わりです。ここから先は、このコントロールの使い方を見ていきましょう。

Webカスタムコントロールの利用

 カスタムコントロールの使い方としては、ツールボックスからドラッグアンドドロップしてページに配置するのが最も簡単な方法です。作成したコントロールをツールボックスに追加するには、まずツールボックス上で右クリックして[Add/Remove Items]を選択します。

 するとダイアログボックスが表示されるので、そこでローカルPCの.NET Framework Componentsフォルダを選択し、Web Control LibraryプロジェクトのDLLを指定します。ただし、DLLの場所は、プロジェクトをデバッグ構成でビルドした場合はプロジェクトフォルダの下の「binDebug」フォルダに、リリース構成でビルドした場合は「binRelease」になっています。

 DLLを追加すると、CategorizedCheckBoxListというコンポーネントが表示されるので選択します。

 [OK]をクリックするとツールボックス上にCategorizedCheckBoxListが表示されるはずです。

 ASPXページでCategorizedCheckBoxListコントロールを利用するために、このコントロールをツールボックスからVisual Studio .NETのデザインビュー内のページにドラッグアンドドロップします。コントロールをページに配置すると、表示される列数やCSSプロパティを設定できるようになります。

 このコントロールのデータソースとなるDataTableは、分離コードで指定することをお勧めします。これは、用意できていないうちにCategorizedCheckBoxListからアクセスされるのを防ぐためです。一方、チェックボックスのカテゴリ、値、およびラベルを示す列の名前は、分離コードファイルとASPXページのどちらで指定してもかまいません。

 それでは、この記事のダウンロードサンプルに収録されている「Default.aspx」ページの内容を見ていきましょう。Page_Loadメソッドは次のようになっています。

private void Page_Load(object sender, System.EventArgs e)
{
    // If the page has not posted-back,
    // or if it has but the "Show List" checkbox is checked,
    // get the data for our CategorizedCheckBoxList.
    if(!IsPostBack || chkShowList.Checked == true)
    {
        // Get the data
        GetMdbData();
    }
    else
    {
        // Hide the CategorizedCheckBoxList
        CategorizedCheckBoxList1.Visible = false;
        // Hide the "Show List" checkbox
        chkShowList.Visible = false;
        // Hide the submit button, too
        btnTestValues.Visible = false;
    }
}

 ここでは、コントロールが表示される場合にのみコントロール用データの取得を行っています(CheckBoxListコントロールのインスタンスを動的に生成する場合は、コントロールの表示と非表示に関係なく、データ取得を毎回行う必要があります)。

 次に、GetMdbDataメソッドを呼び出し、Accessデータベースからデータを選択します。GetMdbDataメソッドのコードは次のとおりです。

protected void GetMdbData()
{
    // Create a connetion
    OleDbConnection Conn = new OleDbConnection();
    Conn.ConnectionString = String.Concat(
        "Provider=Microsoft.Jet.OleDb.4.0;data source=",
        Server.MapPath("SampleData.mdb"));
    // Build a data adapter that selects all of the columns
    // and rows in a saved query called qryCarModelCarMaker,
    // in the Access database
    OleDbDataAdapter Adp = new OleDbDataAdapter(
        "SELECT * FROM qryCarModelCarMaker", Conn);

    // Create an instance of our Cars typed dataset
    Cars TypedSampleData = new Cars();
    // Use the adapter to fill the CarTable
    Adp.Fill(TypedSampleData, "CarTable");
    // Specify the data properties for
    // our CategorizedCheckBoxList control
    CategorizedCheckBoxList1.DataTable = TypedSampleData.CarTable;
    CategorizedCheckBoxList1.DataTextColumn = "Model";
    CategorizedCheckBoxList1.DataValueColumn = "CarModelPK";
    CategorizedCheckBoxList1.DataCategoryColumn = "Make";
    // Clean-up
    Conn.Dispose();
    Adp.Dispose();
}

 ここではダイナミックデータベースクエリの結果を格納した型付きのDataSetを使用していますが、CategorizedCheckBoxListコントロールは、DataTableと、および必要なデータを格納している列の名前だけを受け付けます。サンプルプロジェクトの他のページでは、型付きのデータとそうでないデータの両方をXMLファイルから読み込んでいます。

 いくつかのアイテムを選択したうえで[Test Checkbox Values]ボタンをクリックすると、選択したチェックボックスの値が表示されます。このサンプルで表示されるのは、選択された車種のプライマリキーの値です。では、選択したアイテムの値の取得方法を見てみましょう。

private void btnTestValues_Click(object sender, System.EventArgs e)
{
    // Were any checkboxes checked?
    if(CategorizedCheckBoxList1.Selections.Count > 0)
    {
        // Yes. Let’s use a string builder to tell
        // the user what we find.
        StringBuilder Sb = new StringBuilder();
        Sb.Append("The following values were selected:");
        
        // Use an HTML un-ordered list to display the values
        Sb.Append("<ul>");

        // Loop through the selections
        foreach(string check in CategorizedCheckBoxList1.Selections)
        {
             // Add this item to our HTML list
             Sb.Append("<li>");
             Sb.Append(check);
             Sb.Append("</li>");
        }
        // End the list
        Sb.Append("</ul>");
        // Set the text of our label
        Label1.Text = Sb.ToString();
    }
    else
    {
        // Use our label to tell the user
        // that nothing was picked.
        Label1.Text = "No checkboxes were selected.";
    }
}

 このメソッドを実行すると、選択したチェックボックスの値のリストが次のように表示されます。

 以上でCategorizedCheckBoxListコントロールの基本的な説明はほぼ終わりですが、デフォルト選択の指定方法の説明をまだしていませんでした。このコントロールでは、ArrayListに基づくSelectionsというパブリックプロパティを参照することで、事前選択オプションを簡単に指定できます。

// If the page has not posted-back,
// or if it has but the "Show List" checkbox is checked,
// get the data for our CategorizedCheckBoxList.
if(!IsPostBack || chkShowList.Checked == true)
{
    // Get the data
    GetMdbData();
    // Select all of the Audi models
    CategorizedCheckBoxList1.Selections.Add("37");
    CategorizedCheckBoxList1.Selections.Add("38");
    CategorizedCheckBoxList1.Selections.Add("39");
    CategorizedCheckBoxList1.Selections.Add("40");
    CategorizedCheckBoxList1.Selections.Add("41");
}

 なお、このArrayListには必ず文字列値を指定するということを忘れないでください。

まとめ

 .NET Frameworkには、さまざまなWebコントロールを含んだツールボックスが用意されています。カスタムコントロールを自分で開発しようとする前に、必ず標準のコントロールの利用を検討してみるべきです。標準のWebコントロールは、ここで紹介した例よりもかなり複雑なものです。上位レベルおよび下位レベルのブラウザのサポートや、豊富なイベントモデルが用意されています。しかも、こうしたWebコントロールについては、ASP.NETのリリースごとにMicrosoftによるアップグレードが期待ができます。

 しかし、あなたの要求を満たすコントロールが見つからないときには、どんどん自作してください。ユーザーコントロールに比べれば複雑ですが、Webカスタムコントロールは手に負えないほど難解なものではありませんし、数多くのメリットをもたらしてくれます。あらゆるコントロールの描画に利用されるHTMLについても同じことが言えます。想い描いたアイデアをHTMLで表現する方法が理解できれば、Webカスタムコントロールを構築することができます。

著者紹介

Conrad Jalali(Conrad Jalali)
ユーザビリティに重点を置く設計会社、Useful Studiosの共同創立者。ここ5年間はActive Server Pages(ASP)に取り組んでおり、C#とSQL Server 2000を使用したASP.NET開発を専門とする。Sarah Lawrence Collegeを卒業し、教養課程の学士号を取得。Microsoft認定プロフェッショナル(MCP)の資格を所有。妻Elizabethと飼い犬Sparkyと共にワシントンDCに居住。メールの宛先はconrad@smallcog.com
このエントリーを含むはてなブックマーク この記事をクリップ!
BuzzurlにブックマークBuzzurlにブックマーク Yahoo!ブックマークに登録
この記事をokyuuへインポート
最新トップニュース
データメーション
【データメーション】
中国が「Green Dam」フィルタ規制を撤回(7月1日)
Graphic Design Forum
【Graphic Design Forum】
Chris Dickman(6月25日)
プライバシー ジャパン・インターネットコム版
【プライバシー ジャパン・インターネットコム版】
グーグル・ストリートビューの問題について総務省の見解(6月23日)
エンジニアの独り言
【エンジニアの独り言】
システムを「使う」時代のエンジニアに求められるもの(6月2日)
最新ハイテク講座
最新ハイテク講座
電気は家庭でつくる時代へ!燃料電池「エネファーム」(7月3日)
アクセス解析で見るWebマーケティング
アクセス解析で見るWebマーケティング
決定力を探るアクセス解析(7月3日)
百式のネットビジネス研究
百式のネットビジネス研究
ファーストフードを高級っぽく盛り付けて紹介している「Fancy Fast Food」(7月3日)
週刊-サイト別アクセス状況データ
週刊-サイト別アクセス状況データ
ビデオリサーチインタラクティブ調査(月間インターネットオーディエンスデータ)(7月2日)
成約率、反応率を上げる Web 文章術
成約率、反応率を上げる Web 文章術
言葉がダイレクトにキャッシュを生む(7月2日)
不況時代の Web ビジネス最適化講座
不況時代の Web ビジネス最適化講座
アクセス解析エキスパートここだけの話、Web コンシェルジュの“勉強法”こっそり教えます(7月2日)
「Webからの脅威」―その傾向と最新対策
「Webからの脅威」―その傾向と最新対策
不正プログラムの分類(7月1日)
DevX
DevX
JavaScriptとDOMによる動的なWebページの作成(6月30日)
エンジニア転職ノウハウ開発室
エンジニア転職ノウハウ開発室
今のままで大丈夫?3匹の子ブタ的キャリア危険度診断(6月30日)
アイレップの SEM フロンティア
アイレップの SEM フロンティア
Web サイトは「無駄な穴のたくさん開いたじょうご」〜サイト成果向上の基本的な考え方(6月30日)
Copyright 2009 Japan Internet.com K.K. All Rights Reserved.http://www.internet.com/