はじめに
私にはJakeという息子が生まれたばかりで、早く写真を見せろという要求が親類縁者から引きも切らず寄せられてきます。Web開発者の端くれとして、当然、赤ん坊のためのWebサイトを作り、写真をすべてそこに載せることを考えました。さらに、Jakeの崇拝者がそれぞれ好きな写真に投票し、毎月末、見事1位に輝いた写真の光り輝くプリントが各自の郵便受けに送り届けられるというアイデアはどうでしょうか。この計画を実行に移すには、写真1枚1枚の評価を継続的に受け付けることが必要で、サイト上でそれをやってくれるユーティリティを開発しなければなりません。まず、そのユーティリティにどのような機能が必要かを考えました。
計画
どのようなプログラミングに取りかかるときも、私はまず問題を把握し、解決手順の概要を思い描くことに多少の時間を費やします。このユーティリティに望まれる機能は何でしょうか。次のように書き出してみました。
- サイトユーザーに各写真を評価してもらう。
- 写真の評価は1~5の5段階とする。
- 各写真の評価結果をグラフィカルに表示する。
- フィードバックをもらう(ピンぼけ防止装置をあげようか、という祖母からの言葉を期待して)。
設計
次に写真評価フォームを設計しました。図1のフォームがそれです。このフォームの背後にあるコードは次のとおりです。
<form runat="server"> <asp:Linkbutton text="Rate this muggle photo" id="Rate_This_Photo_Button" OnClick="Show_Rate_Article" runat="server" /> <div id="Rate_Form" runat="server" visible="false"> <table width="178" align="center" cellpadding="5" cellspacing="5"> <tr> <td valign="top"> <table> <tr> <td valign="top">Rating:<br></td> <td> <asp:RadioButtonList cellpadding="0" cellspacing="0" id="MyRadioButtonList" runat="server"> <asp:ListItem value="1">*</asp:ListItem> <asp:ListItem value="2">* *</asp:ListItem> <asp:ListItem value="3">* * *</asp:ListItem> <asp:ListItem value="4">* * * *</asp:ListItem> <asp:ListItem value="5">* * * * *</asp:ListItem> </asp:RadioButtonList> <asp:RequiredFieldValidator id="Reqd_Radios" ControlToValidate="MyRadioButtonList" ErrorMessage="Please rate the muggle." Runat="server" /> </td> </tr> </table> Comments:<br> <asp:Textbox TextMode="multiline" rows="5" cols="15" runat="server" id="Comments" /><br> <asp:Linkbutton text="Submit" OnClick="Submit_Rating" runat="server" /> </td> </tr> </table> </div> </form>
このフォームは、RadioButtonList
コントロール(MyRadioButtonList
)と、複数行のテキストボックス、そして下記のSubmit_Rating
イベントハンドラ(サブルーチン)をトリガーするリンクボタンから成ります。他に、ラジオボタンのリストから必ず1個を選択してもらいたいので、RequiredFieldValidator
というコントロールを加えました(RequiredFieldValidator
など、妥当性検査のためのASP.NETコントロールについては、『Form Validation with ASP.NET - It Doesn't Get Any Easier』を参照してください)。これが必要なのは、私のデータベーステーブルでは評価フィールドにnull
を認めていないためです。
さて、フォームに入力されたすべての値をデータベースに挿入しなければなりません。それには、sp_Muggle_PhotoRatingInsert
というストアドプロシージャを使うことにしました。最初にデータベース接続とSQLCommand
オブジェクトをセットアップします。データベースとの接続には、「web.config」ファイルのappSettings
部分に格納されているDSN(データソース名)を使用します(データベースにレコードを追加するコードのサンプルを見たい方は『ASP.NET version of "Database Add"』を、「web.config」ファイルの詳細については『Format of ASP.NET Configuration Files』を参照してください)。リンクボタンがクリックされると、次のイベントハンドラが呼び出されます。
Sub Submit_Rating(sender As Object, e As EventArgs) Dim myConnection As New SqlConnection _ (ConfigurationSettings.AppSettings("MyDSN")) 'The SqlCommand receives two parameters. 'The first is the name of my stored procedure. 'And the second is the name of my database connection from above. Dim myCommand As New SqlCommand _ ("sp_Muggle_PhotoRatingInsert", myConnection) myCommand.CommandType = CommandType.StoredProcedure
次に、ストアドプロシージャへの入力パラメータを追加しなければなりません。これには、SqlParameter
型の変数を宣言し、ストアドプロシージャに予期させる入力パラメータの名前(たとえば、@Photo_ID
)と、変数の型(ここでは、SqlDbType.Int
)およびサイズを指定します。このあと、パラメータ値を望みの値に設定し、最後にSqlCommand
に追加します。具体的にはParameters.Add
メソッドを使って新パラメータの名前を引き渡します。次のコードを見てください。
Dim parameterPhoto_ID As New SqlParameter _ ("@Photo_ID", SqlDbType.Int, 4) 'Here Photo_ID is a public variable set by the calling page parameterPhoto_ID.Value = Photo_ID myCommand.Parameters.Add(parameterPhoto_ID)
RadioButtonList
コントロールで便利な点の1つは、SelectedItem.Value
プロパティを使用して、選択された項目の値を直接取り出せることです。ストアドプロシージャのパラメータをセットアップし、選択されたラジオボタンをそのパラメータの値として設定するには、次のコードを使います。
Dim parameterRating As New SqlParameter _ ("@Rating", SqlDbType.Float, 8) parameterRating.Value = MyRadioButtonList.SelectedItem.Value myCommand.Parameters.Add(parameterRating)
ストアドプロシージャに与えるその他のパラメータについては、特に複雑なことはありません。SqlParameter
型の変数を宣言し、上記のとおり、必要なパラメータをそれに引き渡したのち、そのパラメータ値を設定します。そして、パラメータをSqlCommand
オブジェクトに追加すれば終わりです。私のストアドプロシージャには、@PhotoID
と@Rating
両パラメータに加えて、@Comments
と@Remote_Address
というパラメータがあります。前者は投票者のコメント用(あれば)、後者は投票者のIPアドレス用です。
Dim parameterComments As New SqlParameter _ ("@Comments", SqlDbType.VarChar, 255) parameterComments.Value _ = Comments.Text 'The contents of the multiline textbox myCommand.Parameters.Add(parameterComments) Dim parameterRemote_Address As New SqlParameter _ ("@Remote_Address", SqlDbType.VarChar, 50) parameterRemote_Address.Value _ = Request.Servervariables("REMOTE_HOST") myCommand.Parameters.Add(parameterRemote_Address)
パラメータが追加できました。このストアドプロシージャを実行するには、データベース接続を開き、SqlCommand
オブジェクトを実行して、接続を閉じなければなりません。次に示す数行のコードでそれを行います(これがイベントハンドラの終わりであることに注意してください)。
myConnection.Open() 'I use the ExecuteNonQuery method because the stored procedure does 'not return any rows from the database. myCommand.ExecuteNonQuery() 'Close the database connection myConnection.Close() End Sub
さて、これまでに何が達成されたでしょうか。フォームができました。ユーザーはこのフォームで個々の写真を評価(1~5)し、何か思うところがあれば、それをフィードバックできます。しかし、評価結果の表示方法にはまだ手をつけていません。せっかくですから、写真の最新評価をグラフィカルに表示することにしましょう。
各写真の最新評価を正確に反映した画像を動的に作成するには、どうすればいいでしょうか。幸いなことに、ASP.NETでは画像をその場で作成できます。詳しい方法については、『Drawing Serpinski's Triangle with ASP.NET』や、『Create Snazzy Web Charts and Graphics On the Fly with the .NET Framework』といった記事を参照してください。次の節では、ユーザーの投票後すぐに、その写真の最新評価を動的に作成する方法を考えます。
画像
画像の表示には、<img>
タグのsrc
属性を設定します。画像を生成してブラウザに直接返す.aspxファイルを用意しておき、その.aspxファイル名をsrc
属性に指定するとよいでしょう。他に、画像を別途作成してサーバに保管し、そのファイル名を<img>
タグのsrc
属性に指定する方法もありますが、この方法だと、Webサーバに大量の一時画像ファイルが溜まって、ときどき削除する必要が生じます(画像の動的作成については、『ASP.NET: Tips, Tutorials, and Code』のこのサンプル章を参照してください)。
表示する画像を指定するために、サーバ側<img>
タグを作成します。こうすることで、そのタグのSrc
プロパティをプログラム的に指定できます。
<!-- create a server-side image tag --> <img id="RatingBar" runat="server" /> 'In the server-side code, you can set the src property like so: RatingBar.Src = "photo_rating_image.aspx?Photo_ID=" & Photo_ID
上のコードでは、Photo_ID
をファイル名とクエリ文字列に結合し、それをRatingBar
画像コントロールのsrc
属性に設定しています。
「photo_rating_image.aspx」ファイルのコードを見てみましょう。まず、このファイルで使用する名前空間をインポートし、サーバ側スクリプトブロックを開いて、Page_Load
イベントハンドラを開始します。
<%@ Import Namespace = "System.Data" %>
<%@ Import Namespace = "System.Data.SqlClient" %>
<%@ Import Namespace = "System.Drawing" %>
<%@ Import Namespace = "System.Drawing.Imaging" %>
<Script runat="server">
Sub Page_Load( Sender As Object, e As EventArgs)
「photo_rating_image.aspx」ファイルは、クエリ文字列経由でPhoto_ID
を受け取ります。この情報は、指定された写真の現在評価を取り出して、そのグラフィカル表現を作成するのに必要です。今回のPage_Load
イベントハンドラでは、次のようにしてクエリ文字列を入手します。
'Start by getting our Photo_ID from the querystring Dim Photo_ID As Integer = Request.Querystring("Photo_ID")
前の節では、ユーザーの投票をデータベーステーブルに書き込むためにデータベース接続を確立しました。今度は全体評価を表示することが目的ですから、それとは別のデータベース接続を確立して、累積的評価を取り出さなければなりません。やり方は以前と同様ですが、今回はsp_Muggle_PhotoRatingOutput
ストアドプロシージャを呼び出します。
'Set up a connection to my database as well as a 'SqlCommand Object for my stored procedure Dim myConnection As New SqlConnection _ (ConfigurationSettings.AppSettings("MyDSN")) Dim myCommand As New SqlCommand _ ("sp_Muggle_PhotoRatingOutput", myConnection) myCommand.CommandType = CommandType.StoredProcedure 'Add the Photo_ID as a parameter to the stored procedure Dim parameterPhoto_ID As New SqlParameter _ ("@Photo_ID", SqlDbType.Int, 4) parameterPhoto_ID.Value = Photo_ID myCommand.Parameters.Add(parameterPhoto_ID)
このストアドプロシージャが前の例と異なる点は、出力パラメータ経由で値を返すことです。SqlCommmand
のパラメータは、デフォルトでは入力パラメータとなりますから、パラメータの方向が出力であることを指定する必要があります。指定方法は、出力パラメータに値を代入できないことを除けば、以前と変わりません。さらに、parameterRating
パラメータが出力パラメータであることをSqlCommand
オブジェクトに知らせるための1行が必要です。次のようにします。
'Add the Rating as a parameter to the stored procedure 'and specify its direction as output Dim parameterRating As New SqlParameter _ ("@Rating", SqlDbType.Float, 8) parameterRating.Direction = ParameterDirection.Output myCommand.Parameters.Add(parameterRating)
次に示す数行のコードで、SqlCommand
を実行して、出力パラメータの値を取り出すことができます。
'Open the connection, execute the stored procedure 'and close the connection myConnection.Open() myCommand.ExecuteNonQuery() myConnection.Close() Dim Rating_Output as Double Rating_Output = parameterRating.Value
Rating_Output
はDouble
型の変数で、ここに@PhotoID
で指定された写真の平均評価(1.0~5.0)が含まれます。評価のグラフィカル表現を表示するためには、値を整数に変換します。まず、画像の寸法を決めなければなりませんが、サイトのレイアウトから、画像の幅を168ピクセル、高さを15ピクセルにすることにします。これを定数としてセットアップします。
'Set up the constants for our image Dim MaxImageLength As integer Dim MaxImageHeight As integer MaxImageHeight = 15 ' pixels MaxImageLength = 168 ' pixels
画像の最大長をMaxImageLength
(=168)とし、評価のグラフィカル表現がそれを超えないようにしたいとすれば、(Rating_Output / 5) * MaxImageLength
という式が重要な意味を持ちます。
Dim Rating as Integer Rating = Cint(((Rating_Output) / 5) * MaxImageLength)
これで、写真の評価値が得られます。次の節では、評価値のグラフィカル表現をJPEGとして動的に作成する方法を考えてみます。
ASP.NETでの描画
前の節では、個々の写真の評価結果をどう取り出してくるかを見ました。今度は、ASP.NET Webページにおける画像の動的描画の方法を考えます。画像表示には、単一GIFから動的に(おそらくGIFの幅を操作するなどして)生成する方法や、さまざまな評価結果を事前に1組の静的画像として用意しておく方法なども考えられますが、ここでは.NET FrameworkのSystem.Drawing
クラスを使い、適切な画像をその場で(オンザフライで)生成することにしました。効率だけを考えれば単一GIFを使用したほうがいいのでしょうが、今回は、ASP.NETでの画像のオンザフライ描画を試す意図もあってこの方法を選択しました。
画像を動的に描画するには、Bitmap
クラスのインスタンスを作成し、それにMaxImageLength
とMaxHeight
という2つの定数を渡します。
Dim objBitmap as Bitmap = new Bitmap(MaxImageLength, MaxImageHeight)
次に、いま作成したBitmap
クラスに基づいてGraphics
クラスのインスタンスを作成します。
Dim objGraphics as Graphics = Graphics.FromImage(objBitmap)
Bitmap
クラスとGraphics
クラスの詳細については、『ASP.NET: Tips, Tutorials, and Code』のサンプル章を参照してください。
画像は、銀色の背景に緑色で描くことにして、その2色のブラシをセットアップします。ただ、緑色は、カラーパレットにあるものをそのまま使用するのでなく、サイトの色にぴったりマッチする緑色にしたいので、Color
オブジェクトのFromArgb
メソッドを使用します。このメソッドを使用するには、カラーチャネルごとに数値でRGBカラーを指定しなければなりません。私のサイトに使用している緑色は、Photoshop Color PickerによるとR88、G128、B86です。したがって、次の緑色ブラシを指定しました。
Dim objBrushGreen as SolidBrush _ = new SolidBrush( Color.FromArgb( 88, 128, 86 ))
銀色には、System.Drawing
のColor
メンバにある名前付き定数を使用します。したがって、銀色ブラシは次のようになります。
Dim objBrushGray as SolidBrush = new SolidBrush(Color.Silver)
まず、Graphics
のFillRectangle
メソッドを使い、長方形の全体を塗りつぶします。このメソッドのパラメータは、ブラシ、起点のXピクセル、起点のYピクセル、描く幅、描く高さの5つです。画像全体を銀色で塗りつぶしますから(こうしておくと、評価を表す緑色の線を書き込んでも、それ以外の部分は銀色のまま残ります)、次のようになります。
objGraphics.FillRectangle _ (objBrushGray, 0, 0, MaxImageLength, MaxImageHeight)
得られた銀色の長方形の中に評価のグラフィック表現を描きます。起点のy位置を2
とし、高さをMaxImageHeight - 4
とします。こうすると、下の銀色が見えて、画像の上端と下端に幅2ピクセルの境界線がシミュレートされます。変数Rating
が1~168の整数に設定されていることを思い出してください。これをFillRectangle
メソッドの長さパラメータとして使います。
objGraphics.FillRectangle _ (objBrushGreen, 0, 2, Rating , MaxImageHeight - 4)
画像の6地点に、2ピクセル幅の銀色の線(縞)を高さいっぱいに描きます。これが評価尺度です。最初と最後の縞は、2ピクセル幅の境界線になります。
objGraphics.FillRectangle(objBrushGray, 0, 0, 2 , MaxImageHeight) objGraphics.FillRectangle(objBrushGray, 32, 0, 2 , MaxImageHeight) objGraphics.FillRectangle(objBrushGray, 65, 0, 2 , MaxImageHeight) objGraphics.FillRectangle(objBrushGray, 99, 0, 2 , MaxImageHeight) objGraphics.FillRectangle(objBrushGray, 132, 0, 2 , MaxImageHeight) objGraphics.FillRectangle(objBrushGray, 166, 0, 2 , MaxImageHeight)
これで描画は終わり、あとは画像をブラウザに送るだけです。それには、ページの応答のコンテンツタイプを指定し(JPEG画像を作成するのでimage/jpeg
と設定)、描画された画像のバイナリコンテンツをResponse
オブジェクトのOuputStream
に書き込みます。さらに、後始末としてオブジェクトを破棄します。
'Set the response.contentytpe to image 'and send the image to the browser Response.ContentType = "image/jpeg" objBitmap.Save(Response.Outputstream, ImageFormat.JPEG) objBitmap.Dispose() objGraphics.Dispose()
これで、ユーザーに写真を評価してもらうページと、最新の評価結果を画像として動的に生成するページができました。あと必要なものは、写真が表示される各ページに評価画像をはめ込むためのユーザーコントロールです。次の節では、このユーザーコントロールの作り方を考えます。
ユーザーコントロールの作成
写真を表示するページには、「photo_rating.ascx」というユーザーコントロールを配置します。このコントロールはPhoto_ID
というパブリック変数を含んでおり、この変数は呼び出し側のページによって設定されます(ユーザーコントロールの作成と使用については、ぜひ『Building ASP.NET User Controls』をお読みください)。「photo_rating.ascx」ユーザーコントロールは、まず2つのImport
ディレクティブを宣言し、続いてPhoto_ID
パブリック変数を宣言します。
<%@ Import Namespace = "System.Data" %>
<%@ Import Namespace = "System.Data.SqlClient" %>
<Script runat="server">
'Photo_ID is public so the page that includes
'this user control can set its value
Public Photo_ID As Integer
このコントロールのPage_Load
イベントハンドラには、ただの1行しか含まれていません。この行でGet_Rating
サブルーチンを呼び出します。
Sub Page_Load( Sender As Object, e As EventArgs) Get_Rating() End Sub
Get_Rating()
にも上のデータベースコードと同様の働きがあり、私は写真評価のテキスト表現(「4.11 out of 5」「5点満点中4.11点」など)を得るのにこれを使っています。その内容は、「photo_rating_image.aspx」のコード内容とほぼ同じです。つまり、同じデータを得るのに2つのデータベース要求を出しているわけですから、パフォーマンスを意識するなら、「photo_rating_image.aspx」ファイルを多少変更し、Photo_ID
によらずクエリ文字列経由で評価を取り出すようにすれば、データベース検索が一度で済みます。
このサブルーチンは、さらに<img>
タグのsrc
属性に「photo_rating_image.aspx」ファイルの出力を指定し、クエリ文字列経由でPhoto_ID
を渡します。Get_Rating()
および関連<img>
/<div>
タグのコードは次のとおりです。
Sub Get_Rating() Try Dim myConnection As New SqlConnection _ (ConfigurationSettings.AppSettings("MyDSN")) Dim myCommand As New SqlCommand _ ("sp_Muggle_PhotoRatingOutput", myConnection) myCommand.CommandType = CommandType.StoredProcedure Dim parameterPhoto_ID As New SqlParameter _ ("@Photo_ID", SqlDbType.Int, 4) parameterPhoto_ID.Value = Photo_ID myCommand.Parameters.Add(parameterPhoto_ID) Dim parameterRating As New SqlParameter _ ("@Rating", SqlDbType.Float, 8) parameterRating.Direction = ParameterDirection.Output myCommand.Parameters.Add(parameterRating) myConnection.Open() myCommand.ExecuteNonQuery() myConnection.Close() Dim Rating as Double Rating = Left(parameterRating.Value, 4) RatingBar.Src _ = "/ssi/photo_rating_image.aspx?Photo_ID=" & Photo_ID MySpan.InnerHtml = Rating.ToString() & " out of 5" MyDiv.Visible = True Catch exc As Exception MyDiv.Visible = False End Try End Sub <div id="MyDiv" align="left" style="margin-left:5px" runat="server" > Average user rating<br> <img id="RatingBar" runat="server" /> <span id="MySpan" runat="server" /> </div>
Try ... Catch
ブロックは、データベース呼び出しでエラーが生じたときに、それを捕捉するためのコードです。特に、Rating = Left(parameterRating.Value, 4)
という行に注目してください。ある写真の評価がデータベース中にないと、エラーが生じます。したがって、このブロックのCatch
部分では、MyDiv
という<div>
タグのVisible
属性をfalse
に設定しています。つまり、未評価の写真については、評価結果が表示されません。
あの醜いフォームがすべてのページで表示されることは避けたいので、Show_Rate_Article
イベントハンドラをトリガーするリンクを含めました。このハンドラは、フォームのVisible
属性をtrue
に、リンクのVisible
属性をfalse
に設定します。
Sub Show_Rate_Article(sender As Object, e As EventArgs) Rate_Form.Visible=True Rate_This_Photo_Button.Visible=False End Sub <asp:Linkbutton text="Rate this muggle photo" id="Rate_This_Photo_Button" OnClick="Show_Rate_Article" runat="server" />
最後に
将来に向かって改善すべき点は、同じユーザーが同じ写真に何度も投票するのを防ぐことです。現状では、ユーザーが以前見た写真に戻ってきて、再度投票することが可能になっています。防止にはクッキーを利用できるでしょう。Remote_Address
フィールドを保存しておいて、「IPごとに1票ずつ」とすることもできますが、これだと、たとえばAOLユーザー全員で1票しか行使できないことになり、制限が厳しすぎます。
最終的に、計画段階で思い描いたとおりのユーティリティができあがりました。これで、私のサイトを訪れるユーザーは写真を評価し、コメントを残し、個々の写真が現在までにどう評価されているかをグラフィカル表現で見ることができます。最終的なコードと、テーブルおよびストアドプロシージャを作成するためのデータベースコマンドは、ダウンロードサンプルに収録されています。また、http://www.themuggle.com/view.aspでライブデモを参照できます(評価を添えた写真の一例が、http://www.themuggle.com/viewdetails.aspx?ID=437にあります)。
では皆さん、ハッピープログラミング!