トップページへ PASSJ ブログへ
トップページへ

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


前回は、ADO.NETを使って、データベースにレコードを追加する方法を説明しました。
今回は、レコードを参照して、ユーザーに表示する処理を実装していきます。
表示すべきレコードの特定
画像の処理
単一レコード表示を実装する
画像処理を実装する
まとめ

■表示すべきレコードの特定
データベースに書き込まれているレコードを表示する場合、表形式で一覧を表示する方法と、単一レコードのみを表示する方法がとれます。
多くのWebアプリケーションでは、表形式で一覧表示するWebページと、単一レコードのみを表示するWebページの計2つのページで構成します。
表形式で一覧表示するWebページに対して[詳細]などのリンクやボタンを設け、それがクリックされたときに単一レコードを表示するWebページが表示される、より詳細な情報を表示するユーザーインタフェースをとるのが一般的です(
図2-1


図2-1 表形式の表示から単一レコードの表示にリンクを張る

図2-1 表形式の表示から単一レコードの表示にリンクを張る

今回構築するWebアプリケーションは、図2-1に示したように、表形式で一覧表示するWebページと単一レコードを表示するWebページの両者を作ることにします。
表形式で一覧表示するWebページは、少し複雑なので次回に説明することにし、今回は、単一レコードを表示するWebページに絞って説明します。
以下では、単一レコードを表示するWebページをproductdetail.aspxという名前のWebフォームとして構成することにします。



●URLクエリーと拡張パス情報
さて単一レコードを表示する場合、どのレコードの情報を表示するのかを決める必要があります。とくに、図2-1のように、表形式の表示からリンクを張る場合には、リンク内に表示すべきレコードを指定する値を埋め込んでおかないと、単一レコードを表示するWebページで、どのレコードを表示してよいのかがわかりません。
今回構築しているWebアプリケーションでは、商品ID(Productsテーブルのid列の値。連載第1回目の表1-1を参照)によって、レコードの特定ができます。ですから、URL中に商品IDの値を埋め込んでおけば、どのレコードを表示すべきかがわかるようになります。
URLに値を埋め込む方法としては、URLクエリーを使う方法と、拡張パス情報を使う方法の2通りがあります。

(1)URLクエリー
URLの後ろに、“?項目名=&項目名=…”のようにして値を付け加える方法です。たとえば、次のようなURLを使います。
http://サーバー名/仮想ディレクトリ名/productdetail.aspx?id=商品ID
[TIPS]
“?”以降のURLクエリーでは、 “+”“&”“=”“%”などの文字は、特別な意味をもっており、項目名や値として使えません。これらの文字を含む場合には、URLエンコードと呼ばれるエンコードをします。URLエンコードとは、英数字以外や特別な意味をもつ記号を“%XX(XXは文字の16進数表記)”という表記に置き換えるエンコード方法です。ASP.NETでは、Server.URLEncodeメソッドを使うとURLエンコードできます。

URLクエリーの値は、ASP.NETにおいて、Request.QueryStringプロパティで取得できます。上記のURLのように、商品IDを指定するのに“id”という項目名を採用するのであれば、次のようにして、URLクエリーとして指定された商品IDを取得できます。
int productID;
productID = int.Parse(Request.QuertString["id"]);
[TIPS]
URLクエリーを取得する場合、QueryStringプロパティを使わず、Request["項目名"]と表記してもかまいません。しかし、Request["項目名"]と表記した場合には、Cookies、Form、QueryString、ServerVariablesの各プロパティを調べるため、速度が遅くなります。また、URLクエリー以外の情報――クッキー情報やフォームの情報、サーバーの環境変数など――で、URLクエリーと同じ項目名が設定されていると誤動作の原因になります。URLクエリーのみを取得したいときには、明示的にQueryStringプロパティを使うことを推奨します。

(2)拡張パス情報

Webフォーム名の後ろにスラッシュ(“/”)で続けたすべての文字列を、「拡張パス情報」と言います。たとえば、次のようなURLを使います。
http://サーバー名/仮想ディレクトリ名/productdetail.aspx/商品ID
拡張パス情報は、ASP.NETにおいて、Request. PathInfoプロパティで取得できます。たとえば、次のようにすると、変数pathinfoの値は、“/商品ID”という値となります。
string pathinfo;
pathinfo = Request.PathInfo;
拡張パス情報は文字列であり、Webフォーム名の直後のスラッシュ(“/”)も含みます。これらの文字列を分割して必要な情報だけを取り出すのは開発者の仕事です。具体的には、のちに示すように、正規表現のパターンマッチを使って整数部分だけを取り出すなどの処理をすることになるでしょう。


URLに何か情報を含ませたい場合には、URLクエリーと拡張パス情報のどちらを使ってもかまいません。
一般には、比較的簡単に扱えるURLクエリーが多用されます。なぜなら、URLクエリーの場合には、QueryStringプロパティを使えば、簡単に値の取り出しができるのに対し、拡張パス情報の場合には、拡張パス情報から必要な情報だけを取り出す処理が必要になるからです。

[TIPS]
URLクエリーが多用されるのは、歴史的な理由として、(ASP.NETではなく)ASPの頃には、IISの拡張パス情報の処理に不具合があり、正しく取得できなかったという点も挙げられます。

どちらの方法をとってもよいのですが、商品の詳細を表示するWebページであるproductdetail.aspxについては、次のようなURLクエリーを使って、商品IDを指定することにします。
http://サーバー名/仮想ディレクトリ名/productdetail.aspx?id=商品ID
■画像の処理
さて、今回のWebアプリケーションには、商品情報として、商品画像も含まれています。商品画像を表示するには、どうすればよいのでしょうか。

●画像は<IMG>要素で構成される
HTMLでは、画像は<IMG>要素で表現されます。Webフォームの場合にも、例外ではありません。
そこで画像を表示したいのであれば、画像を返すWebフォームを別途作っておきます。今回は、そのようなWebフォームをproductimage.aspxというファイル名で用意するとします。
そして、画像として<IMG>要素をWebフォーム中に埋め込みます。このとき、productimage.aspxに対しては、どの商品の画像を表示するのかを指定する必要があります。つまり何らかの形で、productimage.aspxに対して、商品IDを与える必要があります。
先に説明したURLクエリーを使うのであれば、次のような<IMG>要素を埋め込むことになるでしょう。

<IMG SRC="productimage.aspx?id=商品ID">
productimage.aspx側では、Request.QueryString["id"]の値を参照して商品IDを特定し、データベースから該当する商品の画像データを読み込んで、それをバイナリとして出力するという方式をとることになります。

図2-2 Webフォームへの画像の埋め込み

図2-2 Webフォームへの画像の埋め込み<

このように、Webアプリケーションに画像を埋め込みたいならば、(1)<IMG>要素を組み込んだWebフォーム、(2)画像を返すWebフォーム、の2つから構成することになります。単一のWebフォームでは実現できません。

●拡張パス情報のメリット
ところで、画像を送信する場合には、いくつかの理由から、URLクエリーよりも、拡張パス情報のほうが好ましい点もあります。
それは、ユーザーがWebブラウザにて、画像を保存する場合です。たとえば、次のような拡張パス情報を考えます。
<IMG SRC="productimage.aspx/1.jpg">
このときWebブラウザは、このファイルのファイル名が(productimage.aspxではなく)“1.jpg”であると認識します。すなわちユーザーがWebブラウザで画像を保存すれば、その画像ファイル名は、“1.jpg”となるのです。
 だからと言って、拡張パス情報にこだわる必要もないのですが、今回は、拡張パス情報の使い方の例を紹介するため、画像を送信するWebフォームに対しては、URLクエリーではなく、次のように拡張パス情報を使って実装することにします。

<IMG SRC="productimage.aspx/商品ID.jpg">
このURLを使うと、productimage.aspxでは、Request.PathInfoプロパティで、“/商品ID.jpg”という文字列が得られます。そこでproductimage.aspxでは、正規表現を使って、この文字列から“/”と“.jpg”に挟まれた“商品ID”の部分を抽出すれば、商品IDの特定ができます。

[TIPS]
URLScan(IIS Lockdown Tool)を利用している場合には、拡張パス情報にピリオドが入ったURLは、デフォルトで除外の対象となり、正しくアクセスできません。
今回のサンプルをURLScanを使っている環境で動作させる場合には、AllowDotInPathの値を1に変更する必要があります。

●サムネイルの送信
さて図2-1を見るとわかりますが、次回作成する予定の商品一覧ページには、画像のサムネイル(縮小画像)が含まれています。ユーザーに一覧で表示する場合には、画像のサムネイルもあったほうがよいでしょう。
そこでproductimage.aspxにおいて、次のように、拡張パス情報が“商品IDs.jpg”というように、商品IDの後ろに“s”が与えられていたならば、画像を縮小したものを送信するという細工を加えることにします。
<IMG SRC="productimage.aspx/商品IDs.jpg">
つまり、

(1)<IMG SRC="productimage.aspx/1.jpg">
と指定されてきたとき
 商品IDが1である商品の画像を送信する
(2)<IMG SRC="productimage.aspx/1s.jpg">
と指定されてきたとき
 商品IDが1である商品の画像を縮小したものを送信する

という仕組みをもたせることにします。
■単一レコード表示を実装する
では実際に、単一レコードを表示するWebフォームを構築していきます。  まずは、図2-3リスト2-1に示すWebフォームを用意します。
productdetail.aspxファイルは、次のように、URLクエリーに商品IDが渡されたとき、該当商品の情報を表示するというものです

http://サーバー名/仮想ディレクトリ名/productdetail.aspx?id=商品ID
図2-3 productdetail.aspxファイル

図2-3 productdetail.aspxファイル

リスト2-1 productdetail.aspxファイル

図2-3
を見るとわかるように、productdetail.aspxは、商品名、価格、在庫状況、解説、登録日、最終更新日、そして画像を含むWebフォームです。
このWebフォームが呼び出されたとき、該当商品のレコードの値を取得して表示するには、Loadイベントで処理すればよく、その処理内容は、
リスト2-2のようになります。

リスト2-2 productdetail.aspx.csファイルのLoadイベントの処理

●URLクエリーから商品IDを取得する
productdetail.aspxファイルでは、図2-3に示したように、Panel_Errorパネルをエラー表示用に、Panel_Successパネルを結果表示用に利用しています。まずは、次のようにして、両パネルとも非表示にします。このパネルは、以降の処理によって、表示/非表示を適時切り替えることにします。
// パネルをとりあえず両方非表示にする
Panel_Success.Visible = false;
Panel_Error.Visible = false;
ErrorMessage.Text = "";
そして次に、URLクエリーとして受け取った商品IDを取得します。その処理は、次のようになります。
// 商品IDを取得する
int productid;
try
{
    productid = int.Parse(Request.QueryString["ID"]);
}
catch (ArgumentNullException)
{
    // 商品IDが指定されていない
    Panel_Error.Visible = true;
    ErrorMessage.Text = "商品番号が指定されていません";
    return;
}
catch (FormatException)
{
    // 書式が不正
    Panel_Error.Visible = true;
    ErrorMessage.Text = "商品番号が不正です";
    return;
}
catch (OverflowException)
{
    // オーバーフロー
    Panel_Error.Visible = true;
    ErrorMessage.Text = "商品番号が不正です";
    return;
}
catch (Exception ex)
{
    // その他のエラー
    Panel_Error.Visible = true;
    ErrorMessage.Text = "不明なエラーです";
    Response.AppendToLog(ex.Message);
    return;
}
ここでは、int.Parseメソッドを使って、得たURLクエリーの値をint型に変換しています。もし変換できない場合には、URLクエリーとして商品IDが指定されていないか、商品IDに数字以外の値が設定されているので、エラーメッセージを適当なLabelコントロールに設定して、そのまま処理を終えるようにしています。

●データベースにSELECT文を送信する
データベースに接続して、値を取り出すには、データベースに接続するための接続文字列を設定したSqlConnectionオブジェクトと、サーバーに送信するクエリーを設定したSqlCommandオブジェクトを用意します。
この処理は、第1回目に説明したINSERT文を送出するときと同じで、次のようになります。


[TIPS]
ADO.NETを使ってレコードを取得するには、以下に説明する方法以外に、DataSetオブジェクトを用いる方法もあります。DataSetオブジェクトについては、次回以降で採り上げます。
// データベースに接続して該当のレコードを得る
SqlConnection sqlconn =
    new SqlConnection(ConfigurationSettings.AppSettings["DSNSTRING"]);
// SELECT文を送信するSqlCommandオブジェクトを用意する
SqlCommand sqlcmd = new SqlCommand(
    "SELECT id, productname, price, stock, comment, detail, DATALENGTH(imgdata) As imglength, createdate, lastupdate FROM Products" +
    " WHERE id=@id", sqlconn);

// パラメータを設定する
sqlcmd.Parameters.Add("@id", SqlDbType.Int).Value = productid;
ここでは、次のSELECT文を準備しています。
SELECT id, productname, price, stock, comment, detail, DATALENGTH(imgdata) As imglength, createdate, lastupdate FROM Products WHERE id=@id
“@id”はパラメータであり、“Parameters.Add”を使って、URLクエリーから取得した商品IDを埋め込んでいます。
ところで、このSELECT文で指定している、“DATALENGTH(imgdata) As imglength”の部分は、補足が必要かも知れません。
imgdata列は、BLOB型(image型)の列として定義してあります。ここには、アップロードされた画像イメージがバイナリとして格納されています(連載第1回目を参照)。DATALENGTH関数は、Transact-SQLの関数で、BLOB型のバイト数を返す(ただし値がNULL値の場合にはnullを返す)機能をもちます。
連載第1回目で説明したように、今回のWebアプリケーションでは、商品を登録するときに画像をアップロードしないことを許しています。商品の登録処理では、画像をアップロードしてこなかった場合には、imgdata列にNULL値を設定するようにしています。
さて今回、商品の詳細を表示する場合には、<IMG>要素を用いて画像を埋め込むわけですが、画像がアップロードされていないのであれば、画像そのものが存在しないので、その場合には、<IMG>要素を埋め込まず、「画像はありません」といったメッセージを表示すべきです。
つまりimgdata列がNULL値や長さが0であるかどうかによって、(1)<IMG>要素を埋め込む、(2)「画像はありません」と表示する、といういずれかの判断が必要となります。
そこで、このように“DATALENGTH(imgdata) As imglength”と入れて、データベースに格納されている画像のバイト数を、読み取る情報として含めるようにしたわけです。

●データベースからの値の取り出し
サーバーに送信したいSELECT文を設定したSqlCommandオブジェクトを用意したならば、実際にサーバーに向けて送信します。
SELECT文のように、クエリーがレコードを返す場合には、ExecuteReaderメソッドを呼び出します。ExecuteReaderメソッドを呼び出すと、該当クエリーが実行され、結果レコードを1つずつ読み取れるSqlDataReaderオブジェクトが得られます。


[TIPS]
実際には、例外処理などが必要ですが、以下の解説では、プログラムを見やすくするため、例外処理を省いています。例外処理の実装については、リスト2-2を参照してください。
SqlDataReader sqlreader;

// 接続を開く
sqlconn.Open();
// SELECT文を実行する
sqlreader = sqlcmd.ExecuteReader();
SqlDataReaderオブジェクトのReadメソッドを呼び出すと、ひとつずつレコードを進みながら読み込むことができます。
Readメソッドは、レコードが存在するならばtrue、もうレコードが存在しないならばfalseを返します。そのため、全レコードを取得する処理をするのであれば、一般に、次のようなwhileループで構成することになります。

while (sqlreader.Read())
// 1レコード分、読み込む処理
// 後述するGetStringメソッド、GetInt32メソッドなどを使って読み込んでいく
}
処理が終わったならば、SqlDataReaderオブジェクトとSqlConnectionオブジェクトの両方を閉じます。
sqlreader.Close();
sqlconn.Close();
リスト2-2では、SELECT文のWHERE句に商品IDを指定しているため、該当レコードが複数存在することはなく、ひとつ存在するか、もしくは存在しないかのいずれかとなります。そこでwhileループではなく、次のように1レコードだけを読み込む処理をしています。
// レコードの値を取得する
if (sqlreader.Read())
{
    // レコードが存在する
    // ラベルなどに、実際の値を設定する
    ProductNameLabel.Text = Server.HtmlEncode(sqlreader.GetString(1));
    CreatedateLabel.Text = Server.HtmlEncode(string.Format("登録日 {0:yyyy/MM/dd}", sqlreader.GetDateTime(7)));
    LastupdateLabel.Text = Server.HtmlEncode(string.Format("最終更新日 {0:yyyy/MM/dd}", sqlreader.GetDateTime(8)));
    switch (sqlreader.GetInt32(3))
    {
        case 0:
            StockLabel.Text = "在庫なし";
            break;
        case 1:
            StockLabel.Text = "在庫あり";
            break;
        case 2:
            StockLabel.Text = "在庫僅少";
            break;
        case 3:
            StockLabel.Text = "在庫要問い合わせ";
            break;
        default:
            StockLabel.Text = "在庫不明";
            break;
    }
    PriceLabel.Text = Server.HtmlEncode(string.Format("価格{0:c}", sqlreader.GetDecimal(2)));
    DetailLabel.Text = Server.HtmlEncode(sqlreader.GetString(5)).Replace("\n", "<BR>");

    // 画像の処理
    if (sqlreader.IsDBNull(6) || sqlreader.GetInt32(6) == 0)
    {
        // 画像なし
        NoImageLabel.Text = "画像がありません";
        ProductImage.Visible = false;
    }
    else
    {
        // 画像あり
        NoImageLabel.Visible = false;
        ProductImage.ImageUrl = string.Format("productimage.aspx/{0}.jpg", productid);
    }
    // 成功した旨のパネルを表示
    Panel_Success.Visible = true;
}
else
{
    // レコードが存在しない
    ErrorMessage.Text = "該当の商品がありません";
    Panel_Error.Visible = true;
}
 
実際にレコードの各列の値を取得するには、GetStringメソッド、GetInt32メソッドなどを使います。どのメソッドを使うのかは、列の型によって異なります表2-1

表2-1 SqlDataReaderオブジェクトから列の値を取得するメソッド

メソッド名 該当する型
GetBoolean bool
GetByte byte
GetBytes byte[]
GetChar char
GetChars char[]
GetDateTime DateTime
GetDecimal Decimal
GetDouble double
GetFloat float
GetGUID guid
GetInt16 short
GetInt32 int
GetInt64 long
GetString string
GetValue object
GetValues object[]

表2-1に示した各メソッドの引数には、列の番号を0から始まる序数として与えます。ここでは、次のようなSELECT文を実行しているので、id列は0、productname列は1、stock列は2、…というように順に対応することになります
SELECT id, productname, price, stock, comment, detail,
DATALENGTH(imgdata) As imglength, createdate, lastupdate FROM Products
WHERE id=@id
また、NULL値であるかどうかを調べるには、IsDBNullメソッドを使います。ここでは、IsDBNullメソッドを使ってimglength列(DATALENGTH(imgdata)の値)を取得し、NULL値であるならば<IMG>要素を表示しない、NULL値でないならば<IMG>要素のSRC属性に相当するImageUrlプロパティに対して、のちに作成する画像イメージを送信するproductimage.aspxファイルのURLを指定しています。
if (sqlreader.IsDBNull(6) || sqlreader.GetInt32(6) == 0)
{
    // 画像なし
    NoImageLabel.Text = "画像がありません";
    ProductImage.Visible = false;
}
else
{
    // 画像あり
    NoImageLabel.Visible = false;
    ProductImage.ImageUrl = string.Format("productimage.aspx/{0}.jpg", productid);
}
この処理により、たとえば商品IDが1である商品を処理中であり、画像が存在するならば、<IMG SRC="productimage.aspx/1.jpg">という要素が埋め込まれることになります。

●HTMLエンコード処理の必要性
ところで、HTMLでは、“<”や“>”などの記号は、特殊な意味に用いられるので注意しなければなりません。HTML出力する場合には、“<”を“&lt;”、“>”を“&gt;”などの表記に置換する必要があります。これらの置換方法を「HTMLエンコード」と呼びます。
HTMLエンコードするには、Server.HtmlEncodeメソッドを呼び出します。
ProductNameLabel.Text = Server.HtmlEncode(objReader.GetString(1));
もしServer.HtmlEncodeメソッドを呼び出さずに、クライアントにそのまま出力してしまうと、データに“<”や“>”などの文字が含まれていたとき、それらがHTML要素として認識され、出力が化けてしまうことになるので、十分に注意してください。

●改行の置換
ところでHTMLでは、改行は<BR>要素で示されます。そのため、元のデータに改行が含まれる場合、そのまま出力したのでは、改行として表示されません。つまり、改行を“<BR>”に置換する処理が必要です。
改行の置換は、Server.HtmlEncodeメソッドでは処理されないので、開発者が自ら置換処理します。
今回のWebアプリケーションでは、商品の解説文を示すdetail列は、改行の入力を許しています。そのため出力する場合には、次のように、改行を
“<BR>”に置換する処理をします。
DetailLabel.Text =
Server.HtmlEncode(objReader.GetString(5)).Replace("\n", "<BR>");
}
[TIPS]
HTMLにおいて、空白は「&nbsp;」で示されますが、HtmlEncodeメソッドでは、この置換は行なわれません。もし空白を「&nbsp;」に置換したいならば、上に示した改行に対する処理と同様な方法で、空白を「&nbsp;」に置換すると良いでしょう。
■画像処理を実装する
以上で、HTML部分の出力はできました。次に、データベースから画像を取得して、その画像データをクライアントに送信する処理を実装します。

●バイナリデータを返すWebフォーム
画像はHTML出力ではなく、バイナリデータです。よって、Webフォームには、何もコントロールを設置する必要はなく、次のように@Pageディレクティブだけがあれば十分です。
<%@ Page language="c#" Codebehind="productimage.aspx.cs" AutoEventWireup="false" Inherits="ProductAppCS.productimage" %>
実際にVisual Studio .NETで開発すると、それ以外にも、<HTML>要素や<HEAD>要素、<BODY>要素などが自動的に作られますが、それらは無視してかまいません。自動生成された要素を削除してもかまいませんが、今回は、Webフォーム上の要素を一切出力しないように作り込んでいく(たとえどのようなコントロールが置かれていようとそれを無視する)ので、そのままでかまいません。
画像を読み取って出力するには、Loadイベントでデータベースに接続して、レコード内の画像データを読み込み、それを出力するだけです。Loadイベントの処理は、
リスト2-3のようになります。

リスト2-3 productimage.aspx.csファイルのLoadイベントの処理

●拡張パス情報を取得する
productimage.aspxでは、拡張パス情報を使って商品IDを特定することにしています。
【通常画像】
productimage.aspx/商品ID.jpg
【サムネイル】
productimage.aspx/商品IDs.jpg
そこでまず、拡張パス情報から商品IDを取り出す処理が必要です。
リスト2-3では正規表現を用いて、次のように処理しています。
// 拡張パス情報を得て、商品IDを抽出する
string pathinfo = Request.PathInfo;
bool thumbflag;

// 正規表現を使って拡張パス情報を抽出する
Match m = Regex.Match(pathinfo, "^/(\\d{1,10})(s?).jpg$");
if (!m.Success)
{
    // 書式が一致しない
    // 404エラーを返す
    Response.StatusCode = (int)HttpStatusCode.NotFound;
    Response.End();
    return;
}

int productid;
try
{
    productid = int.Parse(m.Groups[1].Value);
}
catch (FormatException)
{
    // 整数に変換できない書式
    // 404エラーを返す
    Response.StatusCode = (int)HttpStatusCode.NotFound;
    Response.End();
    return;
}
catch (OverflowException)
{
    // オーバーフロー
    // 404エラーを返す
    Response.StatusCode = (int)HttpStatusCode.NotFound;
    Response.End();
    return;
}
catch (Exception ex)
{
    // その他のエラー
    // 500エラーを返す
    Response.StatusCode = (int)HttpStatusCode.InternalServerError;
    Response.End();
    Response.AppendToLog(ex.Message);
    return;
}

if (m.Groups[2].Value == "s")
{
    // 「XXs.jpg」である。つまりサムネイル画像が指定されている
    thumbflag = true;
}
else
{
    // 「XX.jpg」である。サムネイル画像ではない
    thumbflag = false;
}
この処理では、正規表現を使って商品ID部分を取得するのと同時に、商品IDの後ろに“s”が付いているかどうかも調べ、“s”が付いている場合には、サムネイル画像が要求されていると判断する処理もしています。
ちなみに、正規表現のクラスは、
System.Text.RegularExpressions名前空間にあります。そのため、次のようにして、この名前空間をインポートする必要があります。
using System.Text.RegularExpressions;
●HTTPエラーを処理する
ところで、拡張パス情報として正しい商品IDが与えられれば問題ありませんが、商品IDが与えられなかったり、数字として識別できない商品IDの値が渡されてきたりすることも考えられます。そのような場合には、何らかのエラーを返す処理が必要です。
HTML形式でクライアントに出力する場合には、適当なLabelコントロールを用いて、「正しくありません」などと表示すればよいわけですが、バイナリ形式の場合には、そうもいきません。
そこでエラーを返すために、HTTPのステータスコードを利用します。具体的には、先に示したリストにあるように、Response.Statusプロパティに、返したいHTTPのステータスコードを設定します。
// 404エラーを返す
Response.StatusCode = (int)HttpStatusCode.NotFound;
NET Frameworkでは、HTTPのステータスコードがHttpStatusCode列挙体に示されています。この列挙体は、System.Net名前空間にあるので、次のようにインポートしておきます。
 
using System.Net;
HTTPのステータスコードは、クライアントに対して、いくつかのエラーを報告する数値です。いくつかの種類がありますが、リスト2-3で使っているものは、次の2つです。

・HttpStatusCode.NotFound
 要求するURLに対するコンテンツが存在しないことを示す。
・HttpStatusCode.InternalServerError
 内部エラーが発生したことを示す。

[TIPS]Response.StatusCodeプロパティは整数値であり、HttpStatusCode列挙体に示されている以外の任意のステータスコードを設定することもできます。

データベースからBLOB型の値を取得する
さて、商品IDが得られたならば、データベースに接続して、該当商品のレコードを取り出します。そのためには、先に説明したproductdetail.aspxファイルの処理と同じく、SqlCommandオブジェクトを使ってSELECT文を送信します。その処理は、次のようになります。
// 該当する商品画像をデータベースから取得する
SqlConnection sqlconn =
    new SqlConnection(ConfigurationSettings.AppSettings["DSNSTRING"]);
SqlCommand sqlcmd = new SqlCommand("SELECT imgdata FROM Products WHERE id=@id", sqlconn);

// パラメータの設定
sqlcmd.Parameters.Add("@id", SqlDbType.Int).Value = productid;

// 実行する
SqlDataReader sqlreader;
sqlconn.Open();
sqlreader = sqlcmd.ExecuteReader();
ここでSqlDataReaderオブジェクトのReadメソッドを呼び出せば、該当のレコードが取り出せます。
if (sqlreader.Read())
{
    // 画像を読み取る処理
}
else
{
    // レコードが存在しないので、見つからないという意味のHTTPステータスコードを返す
    Response.StatusCode = (int)HttpStatusCode.NotFound;
}
実際に画像を取り出すには、SqlDataReaderオブジェクトのGetValueメソッドを使います。GetValueメソッドを使うと、任意の値をobject型として取得できるので、BLOB型の場合には、それをバイト配列にキャストして取得します。
byte [] b = (byte[])sqlreader.GetValue(0);
●画像を送信する
次に画像をバイナリデータとしてクライアントに出力します。
まずは、バイナリデータのコンテンツタイプ(MIMEタイプ)を設定します。連載第1回目に作成した商品データの登録処理では、画像データをJPEG形式データとしてアップロードさせているので、送信するする形式は、JPEG形式となります。
そこでまず、次のようにResponse.ContentTypeプロパティに“image/pjpeg”を設定します。“image/pjpeg”は、JPEG形式を示すコンテンツタイプです。

[TIPS]JPEG形式を示すコンテンツタイプとして、“image/pjpeg”ではなく、“image/jpeg”を使ってもかまいません。コンテンツタイプの詳細については、RFC2046やhttp://www.iana.org/assignments/media-types/を参照してください。
Response.ContentType = "image/pjpeg";
コンテンツタイプを“image/pjpeg”に設定することによって、クライアントが、このデータを受け取ったときに、JPEG形式の画像であると認識します。この設定を忘れると、クライアントはASP.NETのデフォルトの形式であるHTML形式であると認識してしまう――バイナリが無理矢理HTML形式として解釈されクライアントに文字として表示されてしまう――ので注意してください。
続いて実際の画像データを送信します。画像データを送信するには、次のようにResponse.BinaryWriteメソッドを呼び出します。引数には、先にデータベースから取得しておいた画像データのバイト配列を渡します。
Response.BinaryWrite(b);
そして最後に、Response.Endメソッドを呼び出して、クライアントとの接続を閉じます。
Response.End();
ASP.NETにおいて、バイナリデータを送信する場合には、Response.Endメソッドの呼び出しが重要です。
Webフォームに配置したコントロールなどの出力は、Loadイベントの処理後にクライアントに送信されます。そのため、Loadイベント内でResponse.Endメソッドを呼び出してクライアントへの出力を閉じておけば、たとえWebフォームに何らかのコントロールを配置してしまっても、それらは(HTMLとして)クライアントに送信されません。
もしLoadイベントでResponse.Endメソッドを呼び出さないと、そのあと、Webフォームの内容が(たとえ改行のみであっても)さらに送信されてしまい、画像の末尾にゴミが付いてしまう可能性があります。

●画像を縮小して送信する
次に、画像のサムネイルを作成する方法を説明します。画像のサムネイルを作るには、いちどBitmapクラスを使って、サーバー上で縮小した画像を再作成します。
まずは、次のようにしてBitmapオブジェクトを実体化し、先にデータベースから読み込んでおいたバイナリデータを読み込みます。
Bitmap img = new Bitmap(new System.IO.MemoryStream(b));
これでサーバー上に、元の画像のビットマップが出来上がります。
続いて縮小します。縮小率は、どのように設定してもよいのですが、
リスト2-3では、アプリケーション構成ファイルに、縮小後のドット数をあらかじめ指定しておくという方式をとっています。
たとえば、縦横64ドット以内に収めるのであれば、アプリケーション構成ファイルに、あらかじめ次のように記述しておくものとします。
<appSettings>
    <add key="ThumbSize" value="64"/>
</appSettings>
リスト2-3では、この設定内容を読み込み、次のようにして、縮小後の大きさを、まず計算しています。
// 縮小する大きさを算出する
int smallwidth, smallheight, thumbsize;
thumbsize = int.Parse(ConfigurationSettings.AppSettings["ThumbSize"]);

// 元画像の幅、高さの大きいほうに合わせる
if (img.Width < img.Height)
{
    // 高さのほうが大きい
    smallheight = thumbsize;
    smallwidth = img.Width * thumbsize / img.Height;
}
else
{
    // 幅のほうが大きい
    smallwidth = thumbsize;
    smallheight = img.Height * thumbsize / img.Width;
}
実際に縮小するには、GetThumbnailImageメソッドを呼び出します。すると指定の大きさに拡大・縮小されたImageオブジェクトが作られます。
System.Drawing.Image thumbimg =
img.GetThumbnailImage(smallwidth, smallheight, null, IntPtr.Zero);
[TIPS]
GetThumbnailImageメソッドでは、縮小だけでなく拡大することもできます。

[TIPS]
上のリストで、“System.Drawing.Image”とフルネームで記述しているのは、ASP.NETの場合、System.Web.UI.WebControls名前空間のImageクラス(Imageコントロール)と名前が重複するためです。

縮小したならば、Responseオブジェクトに対して書き込みます。先の送信例と同じく、BinaryWriteメソッドを使ってもかまいませんが、ImageオブジェクトはSaveメソッドを備えており、画像をストリームに対して書き込むことができるので、次のように、Response.OutputStreamプロパティで得られる、クライアントへの出力を指し示すStreamオブジェクトに書き込むのが簡単です。
thumbimg.Save(Response.OutputStream, System.Drawing.Imaging.ImageFormat.Jpeg);
BitmapオブジェクトやImageオブジェクトは、リソースを消費するので、使い終わったら、Disponseメソッドを呼び出してリソースを解放します。
thumbimg.Dispose();
img.Dispose();
ここでは、画像を縮小したものを返しているだけですが、BitmapクラスやImageクラスを使えば、画像データに対して、拡大・縮小・回転などの処理ができるだけでなく、色の置換や重ね合わせ、さらには、動的に画像を作成する(たとえば数値をグラフ化するなど)ことも可能です。

[TIPS]
ここではデータベースに保存した画像を参照されるたびに縮小加工してクライアントに返しているので、効率が良いとは言えません。効率を高めるには、動的に縮小画像を作成するのではなく、あらかじめサーバー側に縮小した画像を用意するほうが好ましいと言えます。たとえば、商品を登録する際、画像がアップロードされた時点で、サーバー側に縮小された画像を保存しておき、参照されるたびに縮小処理をしないようにします。

■まとめ
今回は、データベースから単一のレコードを取得して、それをクライアントに送信する方法と、バイナリデータとして画像イメージを送信する方法について説明しました。
次回は、DataGridコントロールを使い、複数のレコードを読み込み、それらを表形式で表示する方法を説明します。


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


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

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