トップページへ PASSJ ブログへ
トップページへ
分科会
特集!
コミュニケーション
資格
セミナー・コンファレンス
インフォメーション

テクニカルライター 大澤 文孝


前回は、単一商品を表示する部分までを実装しました。
今回は、全商品を一覧表示する部分を実装していきます。
DataGridコントロールで表形式として出力
DataSetオブジェクトの仕組み
DataGridコントロールにおける表示の設定
まとめ

■DataGridコントロールで表形式として出力
データベースの内容を表形式で表示するには、DataGridコントロールを使い、データバインディングするのが簡単です。 DataGridコントロールには、SqlDataReaderオブジェクト(もしくはその他のデータベースにおけるDataReaderオブジェクト)やDataSetオブジェクト、そしてArrayListオブジェクトなどの配列をデータバインディングすることができます。

[TIPS]
より複雑なレイアウトで構成したい場合には、Repeaterコントロールを使うこともできます。Repeaterコントロールは、任意の要素を繰り返して出力するためのコントロールです。たとえば、表形式ではないけれどもデータの数だけ繰り返し表示したい場面で用います。

今回は、DataGridコントロールを使って、図3-1に示すレイアウトのproductview.aspxファイルを作っていくことにします。

図3-1 表形式でProductsテーブル内を表示するproductview.aspxファイルの実行結果

図3-1 表形式でProductsテーブル内を表示するproductview.aspxファイルの実行結果

●すべてをGUIで手軽に構成
Visual Studio .NETでデータベースアプリケーションを構築する場合、データバインディングの設定のほとんどをGUI環境から操作できます。とくにDataGridコントロールを使って表形式で出力する場合には、コードを逐一記述していくよりも、GUI環境で操作するほうが簡単です。
そこでまず、Visual Studio .NET上でいくつかのコントロールを配置して、手早くテーブルの内容を表示する方法から説明します。
その操作手順は、次のようになります。


[TIPS]
のちに述べるように、DataSetオブジェクトを使った方法は、効率的であるとは言えず、パフォーマンスが悪いものとなります。とくにテーブル内のレコード数が多い場合には、注意が必要です。
実際の構築方法としては、とりあえずは、DataSetオブジェクトを使って構成しておいても、のちにレイアウトなどが決まってきた段階で、SqlDataReaderオブジェクトを使う方法に切り替えることをお勧めします。
SqlDataReaderオブジェクトに切り替えていく方法については、次回以降に説明します。

【手順1―DataGridコントロールの配置】
まずは、図3-2に示すようにDataGridコントロールを配置します。ここでは、このDataGridコントロールの名前(IDプロパティの値)をProductGridという名前にするとします。

図3-2 DataGridコントロールの配置

図3-2 DataGridコントロールの配置

【手順2―SqlConnectionコントロールとSqlDataAdapterコントロールの配置】
次にSqlConnectionコントロールとSqlDataAdapterコントロールを配置します。
これらのコントロールは、[ツールボックス]の[データ]の部分からひとつずつ配置することもできます。しかし[サーバーエクスプローラ]から、SQL Serverを辿り、データベースのテーブルをドラッグ&ドロップすると、必要なプロパティが設定されたSqlConnectionコントロールとSqlDataAdapterコントロールができるので、そうしたほうが簡単です(図3-3)。


[TIPS]
Visual Studio .NET上でのデータベース接続は、開発者(Visual Studio .NETを実行しているユーザー)のアクセス権で実行されます。
それに対して、ASP.NETで実行される場合には、ASPNETユーザーで実行されます。 そのためSQL Serverのセキュリティ設定が正しくないと、「開発環境ではうまくいくけれども実行するとうまくいかない」とか「実行するときはうまく動くけれども開発環境ではうまくいかない」という場面が出てくるかも知れません。その場合には、SQL Server Enterprise Managerなどで、開発者とASPNETユーザーの双方に、適切なアクセス権が付いているかどうかを再確認してください。
なお、Windows 2003 Serverからは、ASP.NETの実行ユーザーがASPNETユーザーではなく、NT AUTHORITY\NETWORK SERVICEユーザーとなっています。そのため、Windows 2003 ServerでASP.NETを用い、データーベースアクセスする場合には、データベースに対して、NETWORK SERVICEユーザーに適切な権限を与える必要があります。その詳細は、「Visual Studio .NET 2003ガイドツアー&評価ガイド」や、GotDotNetの「ASP.NET + IIS 6のセキュリティモデル」などを参照してください。

図3-3 サーバーエクスプローラからProductsテーブルをドラッグ&ドロップする

図3-3 サーバーエクスプローラからProductsテーブルをドラッグ&ドロップする

自動配置されたSqlConnectionコントロールやSqlDataAdapterコントロールには、それぞれ「sqlConnection1」や「sqlDataAdapter1」などの適当な名前が付けられます。名前を変更してもかまいませんが、ここではとりあえず、そのままにすることにしておきます。

【手順3―DataSetコントロールの作成】
次に、配置されたSqlDataAdapterコントロールの[プロパティ]ウィンドウにある[データセットの生成]をクリックします。すると、データセットの作成のダイアログボックスが表示されるので、作成したいDataSetコントロールの名前を指定し、[OK]ボタンを押します(図3-4)。
図3-4では、[新規作成]の部分で、「ProductDataSet」という名前を付けているので、これにより、ProductDataSetという名前のDataSetオブジェクトが作成されます。


[TIPS]
DataSetコントロールを作成すると、テーブル構成を記したスキーマ情報が、拡張子.xsdをもつファイルとして保存されます。このファイルは、DataSetコントロールを削除しても残ったままです。 そのため、DataSetコントロールを削除したのち、再度作成すると、スキーマ情報を含む拡張子.xsdをもつファイルが見つかるため、新しいテーブルではなく、既存のテーブルと見なされることがあります。
とくにテーブルを構成し直したときには、拡張子.xsdのファイルを削除しないと、DataSetコントロールが、古いテーブル構造を見続けてしまう可能性があるので注意してください。

図3-4 DataSetコントロールを作成する

図3-4 DataSetコントロールを作成する

【手順4―DataGridコントロールへのデータバインディング】
最後に、DataGridコントロールと、いま作成したDataSetコントロールとをデータバインディングします。
そのためには、DataGridコントロールのDataSourceプロパティにDataSetコントロールの名前を、DataMemberプロパティにテーブル名をそれぞれ設定します(図3-5)。


図3-5 DataGridコントロールにデータバインディングする

図3-5 DataGridコントロールにデータバインディングする

●データベースから読み取り、表示するためのコードを記述する


private void Page_Load(object sender, System.EventArgs  e)
{
    // ページを初期化するユーザー コードをここに挿入します。
   // データベースからデータを取得する
   sqlDataAdapter1.Fill(ProductDataSet);
   // データバインディングする
   ProductGrid.DataBind();
}


[TIPS]
このときデータベースとの接続には、サーバーエクスプローラからドラッグ&ドロップしたときのデータベース接続文字列が採用されます。
前回まで説明したように、今回のアプリケーションでは、データベース接続文字列をアプリケーション構成ファイル(Web.config)に記述する方法をとっています。
アプリケーション構成ファイルに記述されたデータベース接続文字列を用いるには、Fillメソッドを呼び出す前に、SqlConnectionコントロールのConnectionStringプロパティを変更しておきます。そのコードは、本稿末からダウンロードできるソースファイルに記述しているので、そちらを参照してください。

詳しくは、のちに説明しますが、Fillメソッドを呼び出すと、SqlDataAdapterコントロールを通じてDataSetコントロール内にデータベースの内容を取得できます。
そして、DataBindメソッドを呼び出すと、DataGridコントロールに対して、データバインディングが開始されます。
このようにコードを記述したのち実行すると、図3-6のように、Productsテーブルの内容が表形式で表示されます。


図3-6 実行結果

図3-6 実行結果

図3-6を見るとわかるように、テーブルの全列が表示されており、まだ冒頭の図3-1に示したレイアウトにはなっていません。
しかしこれはレイアウト上の問題だけであり、基本的には、ここで示した手順のようにするだけで、容易にデータベース内のデータを表示することができるということがお分かり頂けたと思います。


■DataSetオブジェクトの仕組み
レイアウトについては、すぐあとで調整することにして、まずは、ここまでGUI環境で構成してきた各種コントロールが、どのような仕組みで動いているのかを説明します。

前回まででは、SqlCommandオブジェクトを使ってデータベースに対する操作をしてきました。それに対して、今回用いる、SqlDataAdapterコントロールとDataSetコントロールを使う方法は、ADO.NETに用意されている、データベース操作をするための、もうひとつの方法です。

なおこれらのコントロールは、GUI環境で実装してきたためコントロールとなっているだけで、実質的には、SqlDataAdapterオブジェクトとDataSetオブジェクトです。そこで以下、「コントロール」という名称ではなく「オブジェクト」という名称で説明を続けます。
さて、SqlDataAdapterオブジェクトやDataSetオブジェクトを使ってデータベースにアクセスする場合には、データベースに接続しっぱなしでデータ操作するのではなく、データベースに接続してレコードを一気にメモリ上に取り出し、以降は、メモリ上でデータ操作するという手法をとります。  このように、SqlDataAdapterオブジェクトやDataSetオブジェクトを使ったデータベース操作は、データベースに接続しない状態でデータを操作することから、「非接続データベースアクセス」と呼びます。


●SqlDataAdapterオブジェクトがデータベースとの橋渡しをする
非接続データベースアクセスにおいて重要な機能を担っているのが、SqlDataAdapterオブジェクトです。
SqlDataAdapterオブジェクトで、重要なプロパティは、次の4つです。
  • SelectCommandプロパティ
    データベースからレコードを取得する際に発行するクエリが設定されたSqlCommandオブジェクトを指す。
  • InsertCommandプロパティ
    データベースに対して新たにレコードを追加する際に発行するクエリが設定されたSqlCommandオブジェクトを指す。
  • UpdateCommandプロパティ
    データベースに対して既存のレコードを更新する際に発行するクエリが設定されたSqlCommandオブジェクトを指す。
  • DeleteCommandプロパティ
    データベースに対して既存のレコードを削除する際に発行するクエリが設定されたSqlCommandオブジェクトを指す。
いままでの手順のように、Visual Studio .NET上で[サーバーエクスプローラ]からテーブルをドラッグしてSqlConnectionコントロールとSqlDataAdapterコントロールを生成した場合には、上記4つのプロパティが、自動的に設定されます。

●Fillメソッドを使ってデータベースからDataSetオブジェクトに転送する
たとえば、SelectCommandプロパティは、図3-7に示すように、次のSELECT文を保持するSqlCommandオブジェクトが設定されています。


SELECT id, productname, price, stock, comment, detail, imgdata, createdate, lastupdate FROM Products

図3-7 自動設定されたSelectCommandプロパティ

図3-7 自動設定されたSelectCommandプロパティ

先に示したLoadイベントの処理では、引数にDataSetオブジェクトを与えて、SqlDataAdapterオブジェクトのFillメソッドを呼び出していました。


// データベースからデータを取得する
sqlDataAdapter1.Fill(ProductDataSet);

このようにFillメソッドを呼び出すと、SqlDataAdapterオブジェクトは、SelectCommandプロパティに設定されているSqlCommandオブジェクトの内容をデータベースに対して発行します。すなわち、次のSELECT文が実行されることになります。


SELECT id, productname, price, stock, comment, detail, imgdata, createdate, lastupdate FROM Products

そしてこのSELECT文の実行の結果得られたレコードが、DataSetオブジェクトの内部に格納されます(図3-8)。

図3-8 Fillメソッドでデータベースからデータを読み込む

図3-8 Fillメソッドでデータベースからデータを読み込む

DataSetオブジェクトは、テーブルに相当するDataTableオブジェクトを含み、そのDataTableオブジェクト内に各レコードに相当するDataRowオブジェクトを含む、図3-9に示す構成をとります。

図3-9 DataSetオブジェクトの構造

図3-9 DataSetオブジェクトの構造

図3-9を見るとわかるように、DataSetオブジェクトとは、メモリ上に構成されたデータベースの複製であると考えることができます。
たとえば、図3-9の構成において、Productsテーブルの先頭列のproductnameフィールドの値は、次の書式で取得できます。



ProductDataSet.Tables["Products"].Rows[0]["productname"]

重要なことは、DataSetオブジェクトは、メモリ上のデータであり、データベース上のデータではないということです。実際、データベースに接続されるのは、Fillメソッドを呼び出した瞬間だけで、その後、すぐに接続は閉ざされます。

[TIPS]
ただし、Fillメソッドを呼び出す前に、SqlConnectionオブジェクトのOpenメソッドを明示的に呼び出して、データベースと接続していた場合には、Fillメソッドを呼び出したあとも、接続は維持されます。

●Updateメソッドを使ってDataSetオブジェクトからデータベースに書き戻す
DataSetオブジェクトの内容は、変更することもできます。たとえば次のようにすると、Productsテーブルの先頭列のproductname列の値を「新しい製品」に変更することができます。


productDataSet.Tables["Products"].Rows[0]["productname"].Value = "新しい製品";

しかし、値の変更はDataSetオブジェクト内部に対するものでしかなく、データベースには反映されません。
反映させるためには、SqlDataAdapterオブジェクトのUpdateメソッドを呼び出します。



sqlDataAdapter1.Update(productDataSet);

Updateメソッドを呼び出すと、DataSetオブジェクト内のレコードのうち、更新されたものがデータベースに書き戻されます。
このとき使われるのが、SqlDataAdapterオブジェクトのInsertCommand、UpdateCommend、DeleteCommandの各プロパティです。

DataSetオブジェクトにおいてレコードに相当するDataRowオブジェクトは、レコードが更新されたかどうかのフラグを保持しています。このフラグは、RowStateプロパティで示されます。
RowStateプロパティは、表3-1に示すDataRowState列挙体のうちのひとつをとります。


表3-1 RowStateプロパティの取りうる値
意味
DataRowState.Added 追加されたレコード
DataRowState.Deleted 削除されたレコード
DataRowState.Detached 追加されたレコードだが、まだDataTableオブジェクトに追加されていない状態。DataRowオブジェクトをDaraTableオブジェクトのRowsコレクションに追加する前の段階であり、Rowsコレクションに追加した時点で、DataRowState.Addedに変化する
DataRowState.Modified 変更されたレコード
DataRowState.Unchanged 変更されていないレコード

SqlDataAdapterオブジェクトのFillメソッドを呼び出してDataSetオブジェクトにレコードを取り込んだとき、すべてのDataRowオブジェクトのRowStateプロパティが、変更されていないレコードであることを示すDataRowState.Unchangedに設定されます。

その後、DataRowオブジェクトに変更を加えると、その変更状況に応じて、表3-1に示したいずれかの値に変化します。たとえば、新しい値を設定すると、DataRowState.Modifiedに変化します。

SqlDataAdapterオブジェクトのUpdateメソッドを呼び出すと、このRowStateプロパティの値によって、InsertCommand、UpdateCommand、DeleteCommandの各プロパティに設定しておいたSqlCommandオブジェクトに設定されているクエリが、データベースに対して実行されます(表3-2図3-10)。


表3-2 RowStateプロパティの値と実行されるSqlCommandオブジェクトとの関係
RowStateプロパティの値 実行されるSqlCommandオブジェクト
DataRowState.Added InsertCommandプロパティに設定してあるもの
DataRowState.Modified UpdateCommandプロパティに設定してあるもの
DataRowState.Deleted DeleteCommandプロパティに設定してあるもの

図3-10 Updateメソッドを呼び出したときの処理の流れ

図3-10 Updateメソッドを呼び出したときの処理の流れ

図3-10に示したように、Updateメソッドを呼び出したときにデータベースに再接続され、DataSetオブジェクト内のレコード(DataRowオブジェクト)のうち変更が加えられたものが、データベースに書き戻されます。
このとき、書き戻したDataRowオブジェクトのDataRowStateプロパティは、DataRowState.Unmodifiedに設定されます。そのため、Updateメソッドを複数回呼び出しても、同じレコードが何度も書き戻されてしまう心配はありません。


[TIPS]
Updateメソッドを呼び出してレコードを書き戻そうとしたとき、書き戻しの対象となっているレコードが、すでに第三者によってデータベースから削除されてしまっていたり、別の値に書き換えられてしまっていたりする可能性もあります。その場合には、Updateメソッドを呼び出したときに、競合エラーの例外が発生します。競合の例外に対応するのは開発者の責任です。

[サーバーエクスプローラ]からテーブルをドラッグしてSqlConnectionオブジェクトとSqlDataAdapterオブジェクトを作成した場合には、InsertCommand、UpdateCommand、DeleteCommandの各プロパティも、適切なものが設定されます。

[TIPS]
ただし、自動作成するには、テーブルに対して主キーが設定されていなければならないなど、いくつかの制限があります。今回は、主キーが構成された単一テーブルの構成なので問題となりませんが、複数のテーブルからなる複雑な構成の場合には、手作業でInsertCommand、UpdateCommand、DeleteCommandの各プロパティを設定しなければならない場面もあります。

たとえば、自動生成されたSqlDataAdapterオブジェクトのUpdateCommandプロパティを確認すると、次のように、UPDATE文とSELECT文の組み合わせが設定されていることがわかります。


UPDATE Products
SET productname = @productname, price = @price, stock = @stock,
      comment = @comment, detail = @detail, imgdata = @imgdata,
      createdate = @createdate, lastupdate = @lastupdate
WHERE  (id = @Original_id) AND (comment = @Original_comment) AND
         (createdate = @Original_createdate) AND (detail = @Original_detail) AND
         (lastupdate = @Original_lastupdate) AND (price = @Original_price) AND
         (productname = @Original_productname) AND (stock = @Original_stock);

SELECT id, productname, price, stock, comment, detail, imgdata,
          createdate, lastupdate
FROM Products
WHERE (id = @id)


これを見るとわかるように、設定されているクエリは、パラメータクエリとなっています。
詳細は省きますが、パラメータは、「@Original_列名」の部分にはレコードの元の値が、「@列名」の部分にはレコードの新しい値が、それぞれ格納されるように構成されています。
つまり、SqlDataAdapterオブジェクトのUpdateメソッドを呼び出したとき、DataRowオブジェクトの「元の値」と「更新後の値」がパラメータ部分に埋め込まれて実行されます。その結果、適切なUPDATE文が実行されることによって、データベース上のデータが新しい値に書き換わります。

ちなみに、UPDATE文の後ろにSELECT文があるのは、データベースで更新したのちのレコードを、再度DataSetオブジェクト内に読み取り直すための処理です。
実際には、UPDATE文の後ろにSELECT文を置いて、読み取り直す処理は必要ありません。つまりUPDATE文の後ろのSELECT文は不要です。なぜなら、データベースに書き込む新しい値は、すでにDataSetオブジェクトに保存してあるものであり、再度読み取り直す必要はないためです。
UPDATE文の後ろにSELECT文を置いて、読み取り直す処理が必要となるのは、データベース上で、何らかの新しい値が格納されるときです。たとえば、IDENTITY列を使っている場合には、データベース上で割り当てられた新しいIDENTITY値をDataSetオブジェクト側にもってくるために、InsertCommandプロパティに設定するSqlCommandオブジェクトにおいては、そのような処理が必要となることがあります。

ところでUpdateメソッドは、図3-10に示したように、DataSetオブジェクトからデータベースへの一方向の転送しかしないという点に注意してください。Updateメソッドは、DataSetオブジェクトとデータベースとの同期をするものではありません。たとえば、データベース上で新たに加えられたレコードは、Updateメソッドを呼び出しても、DataSetオブジェクトに転送されることはありません。

●DataSetオブジェクトによる処理のまとめ
少し話が複雑になってきたので、ここでDataSetオブジェクトによる処理をまとめておきます。
DataSetオブジェクトを使ってデータベース処理する場合の手順は、次のようになります。
  1. Fillメソッドを呼び出す
    SqlDataAdapterオブジェクトのFillメソッドを呼び出します。するとSelectCommandプロパティに設定してあるSqlCommandオブジェクトのクエリが実行され、その結果がDataSetオブジェクトに格納されます。
  2. 必要に応じてデータを処理する
    (1)で得たDataSetオブジェクトに対して、必要な処理をします。たとえばDataRowオブジェクトの内容を変更するなどの処理です。
  3. Updateメソッドを呼び出す
    DataSetオブジェクトに対して処理をし終えたら、それをデータベースに反映させるためにUpdateメソッドを呼び出します。
    このときInsertCommand、UpdateCommand、DeleteCommandの各プロパティに設定してあるSqlCommandオブジェクトのクエリが、処理内容に応じて実行され、データベースの内容が書き換わります。
もちろん、データベースを読み取るだけで、書き戻さないのであれば、(2)や(3)の処理は必要ありません。また、書き戻さない場合には、SqlDataAdapterオブジェクトのSelectCommandプロパティだけを設定すればよく、InsertCommand、UpdateCommand、DeleteCommandの各プロパティを設定する必要はありません。
ここで注目したいのは、(2)の処理は、データベースと接続していない状態で行なうという点です。そのため(2)の処理は、かなり時間がかかろうとも、データベースに負荷をかけることはありません。
そればかりか、DataSetオブジェクトの内容をファイルに保存したり、ネットワークを経由して別のコンピュータに移動したりしてもかまいません。

[TIPS]
DataSetオブジェクトには、XML形式の文字列として取り出したり、XML形式でファイルに読み書きしたりするメソッドが用意されています。

●DataSetオブジェクトの利点と問題点
DataSetオブジェクトを使う利点は、自由な順序で任意のレコードを読み書きできるという点が挙げられます。
前回は、データベースからレコードを読み取るためにSqlDataReaderオブジェクトを使いました。SqlDataReaderオブジェクトは、1レコードずつ順に読み取ることしかできないので、任意のレコードを任意の順序で読み書きすることはできません。それに対してDataSetオブジェクトでは、図3-9に示したように、いちどメモリに読み込まれてコレクションとして構成されるため、どのレコードでも好きな順序で読み取ることができます。

またDataTableオブジェクトには、DataViewオブジェクトを被せることができます。DataViewオブジェクトを使うと、元のDataTableオブジェクトに含まれるデータをソートしたり、フィルタリングしたりすることが容易に行なえます。
反面、デメリットとして挙げられるのが、効率の悪さです。

DataSetオブジェクトを使う場合には、SqlDataAdapterオブジェクトのFillメソッドを使って、データベースからレコードを取り込みます。そのため、たとえば、テーブルに10万レコード存在するならば、10万個のDataRowオブジェクトを含むDataSetオブジェクトができてしまいます。これはメモリの消費を考えると、かなり辛いと言えます。

そのため、レコード数が多いテーブルを読み取るのならば、SqlDataAdapterオブジェクトのSelectCommandプロパティに設定するSqlCommandオブジェクトのクエリにおいて、WHERE句やTOP句を使って、取得するレコード数を制限する工夫が不可欠です。
極論を言えば、Webアプリケーションを構成する場合に、DataSetオブジェクトを使う必要性は、さほどないと言えます。
DataSetオブジェクトが実用的なのは、「複数のレコードを取得して、書き換えて、複数のレコードをデータベースに書き戻す」という処理をする場合です。
しかしWebアプリケーションは、ユーザーインタフェース的に言って、ユーザーに複数のレコードを変更させる場面は、ほとんどありません。

今回の例のように、表形式でレコードを表示する場合には、DataSetオブジェクトを使うと、確かに便利です。しかしDataGridコントロールには、DataSetオブジェクトではなく、SqlDataReaderオブジェクトをデータバインディングすることもできるので、必ずしもDataSetオブジェクトを使わなければならないわけではありません。

ADO.NETでデータベースからレコードを読み取る場合、前回説明したSqlDataReaderオブジェクトを使う方法と、今回説明したDataSetオブジェクトを使う方法がありますが、これらは、用途に応じて、適時使い分けるように心懸けてください。
Visual Studio .NETでは、サーバーエクスプローラからドラッグすると、自動的にSqlConnectionオブジェクトやSqlDataAdapterオブジェクトが作られ、開発者の負担が減るため、DataSetオブジェクトを使った非接続データベースアクセスをつい使いがちです。しかし、それは良い傾向とは言えません。

とくに単一レコードしか取得する必要がない場合には、SqlDataReaderオブジェクトを使うようにしてください。また複数レコードを取得する場合でも、SqlDataReaderオブジェクトで済むならば、極力SqlDataReaderオブジェクトを使って構成するようにします。 ここでは、とりあえず、いままで構成してきたDataSetオブジェクトを使う方法でしばらく話を進めます。
しかし連載の次回以降では、パフォーマンスを考慮して、SqlDataReaderオブジェクトを使う方法について言及する予定です。


■DataGridコントロールにおける表示の設定
さてDataSetオブジェクトの仕組みがわかったところで、DataGridのレイアウトの構成を見ていくことにします。
すでに示したように、現在の状態では、図3-6のように、全列が表形式で表示されています。
以下、これを修正し、冒頭の図3-1に示したように、サムネイル画像などを含むレイアウトに変更していきます。


●列の設定
DataGridコントロールは、デフォルトでは、データバインディングしたとき、バインディングされたデータの全列を表示するという構成になっています。
まずはこれを変更し、必要な列だけを表示するようにします。そのためには、DataGridコントロールのプロパティページで、[列]の部分を編集します。
デフォルトでは[実行時に自動的に列を作成する]にチェックが付いているので、このチェックをはずし、[列一覧]の部分から表示したい列だけを[選択された列]のほうに移します。
列には、「データフィールド」「ボタン列」「ハイパーリンク列」「テンプレート列」の4種類があります。 今回構成するレイアウトでは「画像」「商品名」「在庫」「価格」「概要」「詳細」の列を構成します(図3-11)。


図3-11 列の構成

図3-11 列の構成

「商品名」「在庫」「価格」「概要」については、Productsテーブル内のproductname列、stock列、price列、comment列をそれぞれ表示すればよいので、「データフィールド」から該当列を採用します。
プロパティ画面では、ヘッダに表示される文字列や書式を指定できます。たとえば、価格の列は、図3-12のように構成します。


図3-12 価格の列の設定

図3-12 価格の列の設定

図3-12では、「ヘッダーテキスト」部分に「価格」という文字列を設定しているので、列のヘッダが「価格」となります。また、「データフォーマット式」で、「{0:c}」を指定しているので、先頭に円記号が付いた小数点以下なしの金額形式として表示されます。
他の「商品名」「在庫」「概要」についても同様に設定します。


●テンプレート列として構成する
画像部分は、前回作成したサムネイル画像を返すproductimage.aspxをimg要素として埋め込みます。前回作成したproductimage.aspxファイルでは、拡張パス情報として、「/商品IDs.jpg」を指定すると、その商品IDのサムネイルを返すようにしてあります(たとえば「productimage.aspx/1s.jpg」は、商品IDとして1をもつ商品のサムネイル画像)。
img要素のようなカスタマイズした列は、GUIで構成することはできません。そこで、とりあえずは、テンプレート列として構成しておき、あとでHTMLファイルを直接編集します(図3-13)。


図3-13 画像に対する列の構成

図3-13 画像に対する列の構成

●ハイパーリンク列として構成する
詳細部分は、前回作成した単一商品の詳細を返すproductdetail.aspxファイルへのリンクとして構成します。
前回作成したproductdetail.aspxファイルは、URLクエリとして、「? id=商品ID」を指定すると、その商品IDをもつ詳細を表示するようにしてあります(たとえば「productdetail.aspx?id=1」は、商品IDとして1をもつ商品の詳細)。
このようなリンクを作るには、ハイパーリンク列として構成します。 ハイパーリンク列は、[テキスト][テキストフィールド][テキスト書式文字列][URL][URLフィールド][URL書式文字列]の各項目で詳細を設定します(図3-14)。


図3-14 ハイパーリンク列の構成

図3-14 ハイパーリンク列の構成

[テキスト]と[URL]の欄は、データバインディングされた値に関わらず、つねに固定した値を設定したいときに用いる設定項目です。図3-14では、[テキスト]部分に「詳細」という文字列を指定しています。そのため、ユーザーに表示されるテキストは、つねに「詳細」という文字列となります。
列の値によってテキストやリンクを変更したい場合には、[テキストフィールド][テキスト書式文字列][URLフィールド][URL書式文字列]の各欄で指定します。
この場合の設定方法は、[テキストフィールド]や[URLフィールド]に、値を取得する列名を指定し、[テキスト書式文字列]や[URL書式文字列]で、その列の値を埋め込む書式を指定します。
図3-14では、[URL]にid列を指定し、[URL書式文字列]に「productdetail.aspx?id={0}」を指定しています。そのため、たとえばid列の値が1であった場合には、「productdetail.aspx?id=1」というリンクが構成されます。


[TIPS]
「{0}」の部分には、書式文字列(たとえば「{0:c}」など)を指定することもできます。

●列のレイアウトを設定する
以上で、各列に関する設定は終わりです。あとは必要に応じて、左寄せ、センタリング、右寄せ、背景色や文字色、フォント、列幅などの見栄えを調整します。これらの設定は、[書式]の部分から設定できます(図3-15)。
書式の設定は、見栄えだけで、動作にとくに影響を与えるものではないので、ここでは、書式に関する設定の詳細については割愛します。


図3-15 書式の設定

図3-15 書式の設定

まだ画像部分を構成していませんが、この時点での実行結果は、図3-16のようになります。
図3-16を見るとわかるように、いままで指定してきた列だけが表示されており、また[詳細]のリンクも、正しく構成できていることが確認できます。


図3-16 ここまでの実行結果

図3-16 ここまでの実行結果

●テンプレート列のデータバインディングを構成する
では、サムネイル画像を埋め込むために、テンプレート列として構成した部分をHTMLで直接編集していきます。
いままでの行程で構成したDataGridコントロールをHTMLとして見ると、次のようになっていることがわかります。


[TIPS]
下記のリストでは、書式に関するタグは省略しています。書式を設定すると、HeaderStyleタグやItemStyleタグなどが含まれます。


<asp:datagrid id=ProductGrid runat="server" >
  DataSource="<%# ProductDataSet %>"
  DataMember="Products" AutoGenerateColumns="False">
<Columns>
  <asp:TemplateColumn HeaderText="画像">
  </asp:TemplateColumn>

  <asp:BoundColumn DataField="productname" >
        SortExpression="productname" HeaderText="商品名">
  </asp:BoundColumn>

  <asp:BoundColumn DataField="stock">
        SortExpression="stock" HeaderText="在庫">
  </asp:BoundColumn>

  <asp:BoundColumn DataField="price">
        SortExpression="price" HeaderText="価格" DataFormatString="{0:c}">
  </asp:BoundColumn>

  <asp:BoundColumn DataField="comment">
        SortExpression="comment" HeaderText="概要">
  </asp:BoundColumn>

  <asp:HyperLinkColumn Text="詳細"
        DataNavigateUrlField="id"
        DataNavigateUrlFormatString="productdetail.aspx?id={0}"
        HeaderText="詳細">
  </asp:HyperLinkColumn>
</Columns>
</asp:datagrid>


上記の自動生成されたHTMLを見るとわかるように、テンプレート列として構成された列は、<asp:TemplateColumn>タグで表現されています。
表示する列の定義は、この<asp:TemplateColumn>と</asp:TemplateColumn>に定義されたなかに、<ItemTemplate>を記述することによって行ないます。
つまり、次のように定義します。


<asp:TemplateColumn HeaderText="画像">
<ItemTemplate>
    ここに表示したい内容を記述する
</ItemTemplate>
</asp:TemplateColumn>


[TIPS]
<ItemTemplate>以外に、<HeaderTemplate>(ヘッダ定義)、<FooterTemplate>(フッタ定義)、EditItemTemplate(DataGridコントロールが編集状態になったときの編集列の定義)を定義することもできます。

<ItemTemplate>と</ItemTemplate>で挟まれた部分には、任意の要素を配置することができます。
ASP.NETでは、データバインディングされた動的な値を埋め込むときに、次の書式を使います。


<%# 値 %>

値の部分には、メソッドやプロパティ、式など、値を返すものすべてを記述できます。
データバインディングされている場合、データバインディングの元となるオブジェクトは、「Container.DataItem」で示されます。そしてその値は、DataBinder.Evalメソッドを使って書式化できます。
たとえば、データバインディングされたレコードのid列の値を埋め込みたいのであれば、次の書式をとります。


<%# DataBinder.Eval(Container.DataItem, "id", "{0}") %>

DataBinder.Evalメソッドは、実行時に、第1引数に与えられたオブジェクトと第2引数に与えられた文字列を結合した式を評価し、その値を書式化する機能をもちます。
つまり上記のDataBinderメソッドの呼び出しは、「Container.DataItem["id"]」の値を動的に求めて、それを埋め込むという意味になります。


[TIPS]
第3引数を省略すると、書式化せず、該当オブジェクトをobject型として、そのまま返します。

[TIPS]
「DataBinder.Eval(Container.DataItem, "id")」と記述するのではなく、「DataBinder(Container, "DataItem.id")」と記述してもかまいません。DataBinder.Evalメソッドは、第1引数と第2引数を実行時に結合して評価するので、どちらでも同じです。

[TIPS]
DataBinder.Evalメソッドを使うのは、型が決まるのが実行時であるためです。VB.NETの場合には、型に厳格でないので、DataBinder.Evalメソッドを使わずに、「 <%# Container.DataItem("id") %> 」と記述することもできます。 しかしC#は、型に厳格であるため、「<%# Container.DataItem["id"] %>」と記述することはできません。なぜなら、DataItemプロパティが指す型が、実行時(データバインディングされたとき)に決定されるからです。

そこで実際に、画像のURLを埋め込む部分を記述すると、次のようになります。

<asp:TemplateColumn HeaderText="画像">
<ItemTemplate>
<img >
    src='<%# DataBinder.Eval(Container.DataItem, "id",
                "productimage.aspx/{0}s.jpg") %>'>
</ItemTemplate>
</asp:TemplateColumn>


ここでは、img要素のsrcアトリビュートのなかに、「 <%# %> 」の表記を使ったデータバインディングの書式を利用しています。一見、書式がおかしいようにも見受けられますが、このように、アトリビュートの値として「 <%# %> 」を使ったデータバインディングを記述しても、問題ありません。

[TIPS]
ここでは、srcアトリビュートに関して、シングルクォートで括っている(「src='<%# 値 %>'」)点に注目してください。ダブルクォートで括ると、内部のダブルクォートと重複するため、デザインビューから開けなくなります。

ところで、今回のアプリケーションでは、画像をアップロードしないことも許しているので、画像がアップロードされていない商品のサムネイルは存在しません。しかしこのままだと、商品のサムネイルが存在しない場合でも、img要素が埋め込まれるので、ユーザーは、壊れた画像(Internet Explorerの場合には、画像部分に「×」マークが表示される)を見ることになってしまいます。
そこで、画像が存在しない場合には、img要素を埋め込まないようにしたほうがよいでしょう。
そのためには、Imageサーバーコントロールを使って、次のようにすれば対応できます。


<asp:TemplateColumn HeaderText="画像">
<ItemTemplate>
<asp:Image runat="server">
    ImageUrl='<%# DataBinder.Eval(Container.DataItem, "id",
                                "productimage.aspx/{0}s.jpg") %>'
    Visible = '<%# !(Convert.IsDBNull(
                                  DataBinder.Eval(Container, "DataItem.imgdata"))) %>'/>
</ItemTemplate>
</asp:TemplateColumn>


ここでは、ImageサーバーコントロールのVisibleプロパティに、


<%# !(Convert.IsDBNull(DataBinder.Eval(Container, "DataItem.imgdata"))) %>

という値を指定しています。
この値は、imgdata列がNULL値であるときtrue、NULLでないときにfalseを返します。そのため、imgdata列がNULL値であるかないかによって、Visibleプロパティが変化し、 要素を表示するかしないかを切り替える処理を実現できます。


●メソッドから返された値を埋め込む
ところで、図3-16を見るとわかるように、在庫の列が数字になっています。在庫は、数字ではなく、連載第1回の表1-2に示したように、「あり」「なし」「僅少」のような文字として表示したいので、この部分を修正します。
先に説明したように、<%# %>の書式では、任意のメソッドやプロパティを呼び出すことができます。
そこで、在庫の数値から、それに対応する文字列を返すStockNameという名前のメソッドを次のように用意しておきます。


[TIPS]
ここではメソッドとして用意しましたが、もちろん、プロパティとして用意してもかまいません。


public string StockName(object stockvalue)
{
    // 商品の在庫を文字列として返す
    string []stockstring = {"なし", "あり", "僅少", "要問い合わせ"};
    int stock = (int)stockvalue;
    if ((stock >= 0) && (stock < stockstring.Length))
    {
      return stockstrting[stock];
    }
    else
    {
      return "不明";
    }
}


そして、在庫の表示部分を<%# %>を使ったデータバインディングに変更します。
いままでの行程で進めてきた場合、在庫の表示部分は、次のようになっています。


<asp:BoundColumn DataField="stock" SortExpression="stock" HeaderText="在庫">
</asp:BoundColumn>



このように<asp:BoundColumn>で表現されたままだと、<%# %>でのデータバインディングが利用できません。そこで、テンプレートを使った書式に変更します。
テンプレートを使った書式にするには、DataGridコントロールの列のプロパティ画面で、「この列をテンプレート列に変換する」というリンクをクリックします(図3-17)。


図3-17 テンプレート列に変換する

図3-17 テンプレート列に変換する

すると、次のように、TemplateColumnを利用した書式に変化します。


<asp:TemplateColumn HeaderText="在庫">
<ItemTemplate>
    <asp:Label runat="server"
        Text='<%# DataBinder.Eval(Container, "DataItem.stock") %>'>
    </asp:Label>
</ItemTemplate>
<EditItemTemplate>
    <asp:TextBox runat="server"
        Text='<%# DataBinder.Eval(Container, "DataItem.stock") %>'>
    </asp:TextBox>
</EditItemTemplate>
</asp:TemplateColumn>


<EditItemTemplate>は、DataGridコントロール上でユーザーに編集させるときに利用するものですから無視してかまいません。
変換されたテンプレート列を見ると、<asp:Label>コントロールを使ってデータバインディングするようになっています。この部分を、先に作成したStockNameメソッドの呼び出しに変更します。



<asp:TemplateColumn HeaderText="在庫">
<ItemTemplate>
 <asp:Label runat="server"
        Text='<%# StockName(DataBinder.Eval(Container, "DataItem.stock")) %>'>
    </asp:Label>
</ItemTemplate>
</asp:TemplateColumn>



すると、StockNameメソッドの引数に「DataBinder.Eval(Container, "DataItem.stock")」の値――すなわち、該当レコードのstock列の値――が渡され、StockNameメソッドが「あり」「なし」などの在庫文字列を返し、それが埋め込まれるようになります。

[TIPS]
ここでは、自動生成された書式をそのまま使い、「DataBinder.Eval(Container."DataItem.stock")」と記述しています。この部分は、「DataBinder.Eval(Container.DataItem, "stock")」のように、DataItemを第1引数に移してもかまいません。

ここで注意したいのは、DataBinder.Evalメソッドは、第3引数を省略した場合には、該当オブジェクトをobject型として返すという点です。そのため先に示したStockNameメソッドの引数は、次のようにobject型として定義しています。

public string StockName(object stockvalue)


この部分をint型などで受け取ろうとすると、型が違うので、実行時エラーが発生します。

●HTMLエンコード処理
最後に重要な点として、HTMLエンコード処理について触れておきます。
結論から言うと、データバインディングでは、一切のHTMLエンコード処理をしません。つまり、HTMLエンコードするのは、開発者の責任です。
HTMLエンコードするには、いくつかの方法があります。たとえば、DataSetオブジェクトを生成するときにHTMLエンコード化するのもひとつの方法です。
もっとも簡単な方法(ただし効率はそんなに良くない)は、すべてを<TemplateColumn>で構成されたテンプレート列に変更し、<%# %>の部分で、Server.HtmlEncodeメソッドの呼び出しを追加することです。


[TIPS]
効率が良くない理由は、DataBinder.Evalメソッドは、実行時に型のチェックをするためです。

たとえば、商品名に関しては、次のようにします。

【変更前】

<asp:BoundColumn DataField="productname"
    SortExpression="productname" HeaderText="商品名">
</asp:BoundColumn>



【テンプレート化直後(図3-17のようにして変更した直後)】

<asp:TemplateColumn HeaderText="商品名">
<ItemTemplate>
    <asp:Label runat="server"
      Text='<%# DataBinder.Eval(Container, "DataItem.productname") %>'>
</asp:Label>
</ItemTemplate>
<EditItemTemplate>
    <asp:TextBox runat="server"
      Text='<%# DataBinder.Eval(Container, "DataItem.productname") %>'>
</asp:TextBox>
</EditItemTemplate>
</asp:TemplateColumn>



【Server.HtmlEncodeの呼び出しを記述し、HTMLエンコードするように変更したもの】

<asp:TemplateColumn HeaderText="商品名">
<ItemTemplate>
    <asp:Label runat="server"
      Text='<%# Server.HtmlEncode(
        DataBinder.Eval(Container, "DataItem.productname", "{0}"))
%>'>
</asp:Label>
</ItemTemplate>
<EditItemTemplate>
    <asp:TextBox runat="server"
      Text='<%# Server.HtmlEncode(
        DataBinder.Eval(Container, "DataItem.productname"), "{0}")
%>'>
</asp:TextBox>
</EditItemTemplate>
</asp:TemplateColumn>



この例に示したように、<%# %>で埋め込む際に、Server.HtmlEncodeメソッドの呼び出しを入れることによって、出力時にHTMLエンコードするようにします。

[TIPS]
DataBinder.Eval(Container, "DataItem.productname", "{0}")のように、第3引数に{0}を指定しているのは、string型に変換したいためです。DataBinder.Evalメソッドは第3引数を省略するとobject型として値を返します。Server.HtmlEncodeメソッドは、引数にobject型をとることができないので、エラーとなります。

[TIPS]
連載第2回目で説明したように、Server.HTMLEncodeメソッドは、改行を<BR>タグに置換することはありません。改行を正しく反映させたいならば、改行を<BR>タグに置換するメソッドを用意し、<%# %>内で呼び出すようにするとよいでしょう。

コラム カスタムコントロールを使ってHTMLエンコードする
HTMLエンコードする方法として、カスタムサーバーコントロールを作り、HTMLエンコード機能をカスタムサーバーコントロール側に実装する方法をとることもできます。
HTMLエンコード機能を付けたカスタムサーバーコントロールの例を、リスト3-1に示します。
リスト3-1に示したHtmlLabelコントロールは、Labelコントロール(System.Web.UI.WebControls.Label)から継承したカスタムコントロールで、HtmlTextプロパティというプロパティを追加したものです。
HtmlTextプロパティでは、HtmlEncodeメソッドやHtmlDecodeメソッドを呼び出して、HTMLエンコード/HTMLデコードした値を設定および取得するようにしています。


リスト3-1 HtmlLabelコントロール

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;

namespace MyControlLib // MyControlLib名前空間
{
    /// <summary>
    /// HtmlLabel の概要の説明です。
    /// </summary>
    [DefaultProperty("Text"),
      ToolboxData("<{0}:HtmlLabel runat=server></{0}:HtmlLabel>")]
      public class HtmlLabel : System.Web.UI.WebControls.Label
        // Labelコントロールから継承
    {
      public string HtmlText
        {
        get
        {
          // Textプロパティの値をHTMLデコードして返す
          return System.Web.HttpUtility.HtmlDecode(Text);
        }

        set
        {
          // Textプロパティの値をHTMLエンコードして設定する
          Text = System.Web.HttpUtility.HtmlEncode(value);
        }
      }
    }
}



リスト3-1のHtmlLabelコントロールは、MyControlLib名前空間に作っています(もちろん名前空間は任意の名前を指定できます)。これをビルドして、たとえば、MyControlLib.dllファイルを作ります。
Webフォームから、このHtmlLabelコントロールを利用する場合には、Webフォーム内に、次のRegisterディレクティブを記述します。



<%@ Register tagprefix="myctl" Namespace="MyControlLib" Assembly="MyControlLib" %>


[TIPS]
Webフォーム(のプロジェクト)において、MyControlLib.dllに対する参照設定が必要です。また実行時には、MyControlLib.dllファイルが、Webフォームを含む仮想ディレクトリのbinサブディレクトリに存在しなければなりません(Visual Studio .NET 2003上で参照設定した場合には、ビルドすると、binサブディレクトリに自動的にコピーされます)。

Namespaceアトリビュートはコントロールの名前空間の名前、Assemblyアトリビュートはコントロールが含まれるアセンブリの名前です。そして、tagprefixアトリビュートがサーバーコントロールのタグ名として使う接頭辞となります。
Registerディレクティブを記述すると、Webフォームから、“<接頭辞:クラス名>”という表記で、カスタムサーバーコントロールを筒買うことができるようになります。
ここでは、tagprefixアトリビュートに“myctl”という名前を指定しているので(もちろん接頭辞は任意の名前を指定できます)、リスト3-1のHtmlLabelコントロールは、Webフォームから、次の書式で利用できます。



<myctl:HtmlLabel runat="server" HtmlText="適当な文字列" id = "html1"/>


上記のようにWebフォームに配置したときには、次のように、HtmlTextプロパティを使って、文字列を設定できます。


html1.HtmlText = "<テスト>";


リスト3-1に示したように、HtmlTextプロパティでは、HTMLEncodeメソッドを使ってHTMLエンコードしています。そのため、HTML出力される際には、


<span id="html1">&lt;テスト&gt;</span>


のように、HTMLエンコードされて出力されます。

[TIPS]
<span>タグとして出力されるのは、HtmlLabelコントロールが、Labelコントロールから継承しているからです。

ここでは、LabelコントロールにHTMLエンコード機能を追加したカスタムサーバーコントロールを作る例を示しましたが、他のコントロールも、同様の方法で継承したコントロールを作り、HTMLエンコード機能を追加することができます。
HTMLエンコードする箇所が多数ある場合には、このように、HTMLエンコード機能を実装したカスタムサーバーコントロールを利用することも検討すると良いでしょう。


■まとめ
今回は、SqlDataAdapterオブジェクトとDataSetオブジェクトを使ったデータベースアクセスの概要と、DataGridコントロールを使った表形式の出力について説明しました。
実際、Visual Studio .NETでは、SqlDataAdapterオブジェクトとDataSetオブジェクトを使ったデータベースアクセスは、比較的容易に実装可能で、記述すべきコードも少なくて済みます。
しかし最後に説明したように、HTMLエンコードされないなどの問題があるため、自動生成されたHTMLを開発者が加工するのは必須です。すべてがGUI操作でできるわけではありません。
次回は、DataGridコントロールによる並べ替えやページ操作などを中心に説明する予定です。

今回のリストをすべてダウンロード(LZH形式)


「ステップアップ!ADO.NET」 目次
第1回 ASP.NETの基礎とデータベースへの書き込み
第2回 データベースからの読み込み 〜その1:単一レコードの表示〜
第4回 データベースからの読み込み 〜その3:ページ操作、並べ替え、そして検索〜
第5回 データベースの更新 〜レコードの更新と削除、そして競合問題を考える〜

★この連載に関する意見交換は、下記の掲示板・MLをご利用ください★
・PASSJ掲示板 『開発の現場を語ろう』
・Webテクノロジー分科会メーリングリスト pml-web@sqlpassj.org


PASSJメールニュース 著作権ついて プライバシーポリシー リンクポリシー お問い合わせ
(C) 2005 Professional Association for SQL Server Japan. All rights reserved.