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

ASP.NETでのデータベース検索ページの作成に役立つ13のヒント

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

はじめに

 データベースの検索画面とナビゲーション画面は、アプリケーションの他の部分に比べると、最初はとても簡単そうに見えます。しかし、すべてのユーザーの要望や要求に応えようとすると思いのほか時間がかかります。

 この記事では、ASP.NET 2.0、SQL Server 2005、C# 2.0を使って、検索用のWebページを作成します。検索と結果のページでは、オプションの検索条件を表示し、結果セットのカスタムページングを行います。このソリューションでは、ランキング番号を生成するSQL 2005の新しい言語機能を利用して、カスタムページングとの関連付けを行います。また、.NETジェネリックの新機能を利用して、ストアドプロシージャの結果をカスタムコレクションに出力します。ミヤギ師匠が練習をとおしてダニエルに技を教えたように、この記事の例では、ファクトリ作成パターンなど、.NETジェネリックを使ったいくつかの一般的なデザインパターンを示します。この記事では、データベースのWebページを作成する上での一般的な方法論にも触れます。

最終的な完成イメージ

 さて、本題に入りましょう。この記事では、検索および結果/ナビゲーションの画面を作成します(図1、2、3を参照)。

図1 検索結果の1ページ目。ユーザーはさまざまな条件で検索を実行でき、複数ページにまたがる結果を見ることができる。
図1 検索結果の1ページ目。ユーザーはさまざまな条件で検索を実行でき、複数ページにまたがる結果を見ることができる。
図2 LastName列を基準にして既定のソート/ページングを行い、文字「K」にジャンプした結果。
図2 LastName列を基準にして既定のソート/ページングを行い、文字「K」にジャンプした結果。
図3 Address列を基準にしてソート/ページングを行い、5ページ目にジャンプした結果。
図3 Address列を基準にしてソート/ページングを行い、5ページ目にジャンプした結果。

 これから説明する13のヒントを通じて、最終的に次の状態を実現します。

  • 検索画面で求められる要件を明確化し、それぞれの要件についての解決方法を決める。
  • すべてのセッション変数とストアドプロシージャのパラメータを定義する。
  • オプションの検索パラメータをすべて処理するストアドプロシージャコードを記述する。
  • SQL 2005の新しいランキング関数を使って、ランキングとページングを統合するストアドプロシージャコードを記述する。
  • SQL 2005の共通テーブル式を実装して、クエリと連携する。
  • C#でデータアクセス層(DAL)を作成する。
  • 基本のASP.NET 2.0 Webページを作成する。
  • クイック検索ナビゲーションおよびページングを処理するASP.NET 2.0コードを作成する。
  • 列のソートを処理するページを作成する。
  • グリッド結果セットおよび結果情報を設計する。
  • ページの列を別のページへのリンクとして設定する。
  • ユーザーがリストに対して可変個数の選択を行うことができるように、アプリケーションを変更する。
  • カスタムオブジェクトに出力するようにDALを変更する。

 最後に、付録として、.NETジェネリックを利用し、匿名メソッドでソートを処理するコードを示します。

説明は少なく、コードは多く

 私は、この記事を執筆するに際して、新年の誓いを立てました(この記事を執筆している時点で、2006年がまだ数日残っています)。それは、「説明は短く、コードは長く」です。私が.NETについて学びだして数年経ちますが、非常に参考になった書籍や記事は、有意義なコードサンプルが記載されているものでした。そこで、どのヒントでも、余計なおしゃべりはできるだけ控えて、具体的なコードを紹介するようにします。

ヒント1: 要件の明確化

 例えばブラウザベースの電子メールWebページなど、任意の列でソートでき、Next/Previousリンクまたはページ番号のリンクでページ間をナビゲートするWebサイトを利用したことはありませんか。たまに使うのであれば、このような一般的なナビゲーション設計でも十分です。

 しかし、「R」という文字で始まる特定の行や項目を表示したい場合はどうでしょうか。多分、降順で列をソートし、目的の項目に達するまでNextリンクをクリックするか、目的の項目が何ページ目にあるかを推測することになるでしょう。これはまるで、値段当てを競うテレビ番組のようです。出場者が「500」と言うと司会者が「もっと高い」と言い、出場者が「550」と言うと司会者が「もっと安い」と言います。出場者が本当の値段を言い当てるまで(またはブザーが鳴るまで)、値の増減が繰り返されます。

 このことは、この記事のサンプルプロジェクトのテーマであるナビゲーションにそのまま当てはまります。よくないデザインパターンを言い表すときに「空振り」という表現を使いますが、ユーザーが余計なナビゲーション操作をしなければならないWebページも、そうしたよくないデザインの一例です。もちろん、外見的に優れたデザインのWebページはユーザーを魅了しますが、リピーター率が高いのはナビゲーション部分がきちんと整備されているWebページです。

 この記事では、図1、2、3のようなWebページを作成する方法について説明します。このソリューションには、次の機能が含まれます。

  • 名前、住所、市、州、郵便番号の検索パラメータに基づいて、50万行から成る顧客テーブルで検索を実行できる。部分テキスト検索も可能(例えば、「City」列で「HAMILTON」を検索すると、「123 HAMILTON BLVD」がヒットする)。
  • 結果はページンググリッドに表示され、一度に表示される最大行数(MAXROWS)を設定できる。
  • Next/Previous/Top/Bottomボタンでページをナビゲートできる。
  • 列見出しをクリックして列をソートできる。
  • 現在の列のソート基準に基づいて、特定の結果セットにクイックジャンプできる。
  • 例えば、「City」列を基準として文字「A」が先頭に来る順序で顧客を表示しているときに、文字「M」で始まる値を「City」列に含んでいる最初の顧客データにクイックジャンプできる。「City」列の値が「M」で始まる顧客データの数がMAXROWSより少ない場合は、「M」で始まるすべての行と、それ以降の行がMAXROWSに達するまでページに表示される。
  • クイックジャンプ用のコントロールはプルダウン形式であり、選択項目はアルファベットの各文字または1桁の数字から成る。

 この実装について、もう少し説明しておきます。おそらく、ここで挙げた機能のほとんどは最新バージョンのASP.NETないしサードパーティのWebグリッドに含まれているのではないかと思った人もいることでしょう。部分的にはそうなのですが、すべての要件を満たすためには、多少の努力が必要です。今回のソリューションでは、SQL 2005のストアドプロシージャ、ASP.NET 2.0のGridView、そしてC# 2.0のコードを使ってこれを作成します。図4は、このWebサイトの開発ソリューション全体を示しています。全体的なプランは次のとおりです。

  1. 検索ページのすべての入力条件と変数を明確化し、ストアドプロシージャのパラメータを確定する。
  2. T-SQL 2005の新しいランキング関数を利用してページングパラメータと関連付けるストアドプロシージャを作成し、テストする。
  3. Webページとストアドプロシージャとのやり取りに使用するデータアクセスクラスをC#で作成する。
  4. すべてのWebページ動作(グリッド結果のフォーマット、ページナビゲーション、ページコントロール、変数など)を処理するWebページを設計し、C#の分離コード(コードビハインド)を作成する。
  5. DataSetまたはカスタムコレクションを返す基本のデータアクセス層を設定する。
図4 Webサイトの開発ソリューション
図4 Webサイトの開発ソリューション

ヒント2: セッション変数およびストアドプロシージャパラメータの定義

 表1および表2は、検索ページのセッション変数とストアドプロシージャパラメータをそれぞれ示しています。

表1 ページのセッション変数
変数説明
CurrentFirstRowページングされた結果セットの先頭の行番号
CurrentLastRowページングされた結果セットの末尾の行番号
SortCol現在のソート列
StartRowIndexページングする場合に取得する先頭行
表2 ストアドプロシージャパラメータ
変数説明
FirstNameFirstNameの検索テキスト
LastNameLastNameの検索テキスト
AddressAddressの検索テキスト
CityCityの検索テキスト
StateStateの検索テキスト
ZipZipの検索テキスト
StartRowIndex取得する先頭行
MaxRows取得する最大行数
AlphaCharナビゲート先の英字
SortColソートの基準となる列

 結果セットページには決まった行数しか表示されないため、現在の先頭行インデックスと末尾行インデックスをロジックで保持して、前後のページへのナビゲーションに対処する必要があります。ここで、簡単なシナリオを考えてみましょう。ユーザーが最初に15行の結果セットをもたらすような条件を「City」列で選択したとします。このとき、検索ページに一度に表示できるのは4行だけとします。

1  ANDERSON
2  ARTHUR
3  BARTON
4  BOUTON
5  DEIDRICK
6  DOBSON
7  HAMILTON
8  JERICHO
9  MONTGOMERY
10 RIDDLEY
11 STEVENS
12 TILLY
13 WILCOX
14 WILLIAMS
15 ZEUSS

 最初の実行時、ストアドプロシージャパラメータのStartRowIndexおよびAlphaCharはそれぞれ0と空白であり、ストアドプロシージャは最初の4行を返します。ページの分離コードでは、CurrentFirstRowおよびCurrentLastRowの値を1と4にそれぞれ設定します。

 ユーザーが次のページにナビゲートする場合、分離コードのロジックでは、ストアドプロシージャのStartRowIndexパラメータを5(CurrentLastRow + 1)に設定したうえで、ストアドプロシージャを再び呼び出します。ストアドプロシージャは、「DEIDRICK」〜「JERICHO」の行を返し、Webページの分離コードはCurrentFirstRowおよびCurrentLastRowをそれぞれ5と8に設定します。1つ前のページに戻る場合は、分離コードのロジックにより、StartRowインデックスが、セッション変数CurrentFirstRowの値からセッション変数MaxRowsの値を引いた値に設定されます。

 TopボタンとBottomボタンについては、分離コードによって、StartRowIndexを0または-1(ストアドプロシージャは-1を処理します)に設定します。

 最後に、特定の文字(例えば「R」)に直接ジャンプする場合は、ストアドプロシージャのAlphaCharがRに設定され、「RIDDLEY」〜「WILCOX」の行が返されます。分離コードにより、セッション変数のCurrentFirstRowおよびCurrentLastRowは、それぞれ10と13に設定されます。

 変数の処理については、いくつか注意すべき点があります。

  • ユーザーが特定の文字(または数字)にジャンプする場合、StartRowIndexパラメータは使用されません。
  • 逆に、ユーザーが通常のナビゲーションボタン(Top、Next、Previous、End)を使用する場合、AlphaCharパラメータは使用されません。

ヒント3: 【重要ポイント】T-SQL 2005の新しいランキング関数

 リスト1は、結果セットを返す完全なストアドプロシージャです(話の結末を先に知りたいタイプの人は、まずリスト1を見てください)。次のコードは、ヒント2のSQLパラメータを使って、SQL Management Studio(図5を参照)で対話的にストアドプロシージャをテストする場合のサンプルのSQLファイルを示しています。

DECLARE @LastName varchar(50),
   @FirstName varchar(50),
   @Address varchar(50),
   @City varchar(50),
   @State varchar(2),
   @Zip  varchar(50),
   @StartRowIndex int,
   @MaxRows int,
   @Alphachar varchar(1) ,
   @SortCol varchar(20)
SET @STATE  = ’NY’
SET @MaxRows = 100
SET @startRowIndex = 0
SET @SortCol  = ’ADDRESS’
SET @AlphaChar  = ’’
EXEC [dbo].[LookupEmployees]   @LastName, @FirstName , @Address ,
   @city, @state, @zip, @startRowIndex , @MaxRows ,  @alphachar,
   @SortCol

 この記事の以降の3つのヒントでは、ストアドプロシージャの各部を個別に説明します。

 ヒント2のサンプルデータでは、検索条件(名前や住所など)に一致するレコードごとに、シーケンシャルな行番号が対応していました。今回のストアドプロシージャでは、T-SQL 2005の新しいROW_NUMBER関数を使って、条件に基づいてこの行番号を割り当てます。ROW_NUMBERを使うことで開発者はシーケンシャルなランキング番号を割り当てることができ、ORDER BY文でコーディングする場合と同じようにランキングを並べることができます。

SELECT CustomerID, LastName, FirstName, Address,
   City, State, Zip, ROW_NUMBER() OVER (ORDER BY
CASE  @SortCol
   WHEN ’LASTNAME’ THEN LastName+Firstname
   WHEN ’ADDRESS’ THEN Address
   WHEN ’CITY’ THEN City+LastName+Firstname
   WHEN ’STATE’ THEN State+LastName+Firstname
   WHEN ’ZIP’  THEN Zip+LastName+Firstname
   ELSE LastName + Firstname
END)
AS RowNum
FROM Customers

 次のヒントでは、特定のページ/行インデックスの範囲の結果だけを返す、ランキングのフィルタリングについて説明します。

図5 ストアドプロシージャのテスト
図5 ストアドプロシージャのテスト

ヒント4: オプションの検索パラメータおよびページ/行インデックス範囲の処理

 ストアドプロシージャには検索条件用のさまざまなパラメータが含まれていますが、一度に使用するパラメータの数は限られています。例えば、エンドユーザーが検索する情報として考えられるのは名前、住所、または電話番号のみです。ストアドプロシージャは、ユーザーが指定した入力条件についてのみクエリを実行します。開発者によっては、動的SQL文を作成することによってこれを処理します。T-SQLのCOALESCE関数を使用する方法もあります。この関数の場合、NULL入力パラメータのチェックが行われます。

 また、このストアドプロシージャでは、LIKE文を使って部分テキスト検索を実装します。

WHERE LastName LIKE
   ’%’ + COALESCE(@LastName,LastName)+ ’%’  AND
   Address LIKE
   ’%’ + COALESCE(@Address,Address) + ’%’ AND ...

 さらに、このストアドプロシージャでは、数値のページングの場合は行の範囲でフィルタを行い、クイックナビゲーションの場合は特定の文字または値で始まる現在のソート列に基づいてフィルタを行う必要があります。ここまでくるとストアドプロシージャがやや複雑になってきます。この処理を実現するには、WHERE句にインラインのCASE文を記述します。

WHERE
CASE WHEN @lPaging = 1 AND @SortCol= ’LASTNAME’
   AND SUBSTRING(LastName,1,1) >=
   RTRIM(@AlphaChar) THEN 1
WHEN @lPaging = 1 AND  @SortCol= ’ADDRESS’
   AND SUBSTRING(Address,1,1) >=
   RTRIM(@AlphaChar) THEN 1
WHEN @lPaging = 0 AND RowNum BETWEEN
   ( CASE @StartRowIndex WHEN -1 THEN
   (RecCount )  -  @MaxRows ELSE
   @StartRowIndex  END )  AND
   (CASE @StartRowIndex WHEN -1 THEN
   ( RecCount ) - @MaxRows ELSE
   @StartRowIndex  END ) + @maxRows
THEN 1

ヒント5: クエリの連携

 ヒント3では、ランキング番号を生成する関数を取り上げました。そして、ヒント4では、2種類のT-SQL WHERE句を取り上げました。1つは最初のクエリに含まれますが、もう1つは、行インデックス範囲または1文字の英数字に基づいて元の結果セットをさらにフィルタするものです。最初の結果セットをさらにフィルタするという概念は、次のヒントへと続きます。

 SQL 2005より前のバージョンでは、中間結果に対してさらにSQL文を記述する場合、開発者は派生テーブル、テーブル変数、一時テーブルなどを使用していました。SQL 2005では、共通テーブル式(CTE)が導入されました。CTEは、基本的には、後に続く1つのステートメントで参照できる動的なビューです。

 CTEは、WITH文で簡単に作成できます。次の例では、CustListTempという名前のCTEを作成しています。

WITH CustListTemp AS
   (SELECT  CustomerID, LastName, FirstName,
    Address, City, State, Zip,
    ROW_NUMBER() OVER (ORDER BY….. )

 このCTEをクエリするコードを作成することができます。CTEを参照できるのは、次に続くSQL文だけです。CTEに対するクエリは、実際にはサブクエリを実行して、行のCOUNTを決定していることに注意してください。ユーザーが最後のMAXROW行数を表示する場合、クエリ内の後のWHERE句では、CTEから条件付きでCOUNTを調べます。

SELECT TOP (@MaxRows)  CustomerID, LastName,
   FirstName, Address, City, State, Zip,
   RowNum  FROM
   (SELECT CustListTemp.*,  (SELECT COUNT(*) from
    CustListTemp) AS RecCount FROM CustListTemp) CustList WHERE...

ヒント6: データアクセス層の作成

 ストアドプロシージャが完成したら、次に、ストアドプロシージャと連携するデータアクセスコンポーネントを構成します。

 2006年9月/10月号の『CoDe Magazine』に掲載された「Baker’s Dozen」の記事では、TableAdapterまたはDataSetのMerge関数を使わずに、.NETジェネリックを利用して、型指定されたDataSet(または標準のDataSet)を直接読み込む基本データアクセスクラスを取り上げました。開発者は、型指定されたDataSetのインスタンスに加えて、SQLパラメータのリストとストアドプロシージャの名前を渡すことができます。基本データアクセスクラスは、型指定されたDataSetを自動的に読み込みます(この記事のサンプルコードには、このメソッドが含まれています)。

 リスト2には、基本DALを継承するDAL(daCustomer)が含まれています。メソッドGetCustomersは、次の処理を実行します。

  1. 表2にリストされているパラメータを受け取ります。
  2. SQLパラメータのリストを作成します。
  3. 各検索条件パラメータの長さをチェックし、文字列が空の場合はNULL値を渡します。
  4. 型指定されたDataSetのインスタンスを作成します(例では、単純な型指定のDataSetを使用しています)。
  5. 基本メソッドReadIntoTypedDsを呼び出します。
List<SqlParameter> oSQLParms = new
   List<SqlParameter>();

// set any parameters to NULL, if they are blank
oSQLParms.Add(new SqlParameter("@LastName",
   LastName.Length > 0 ? LastName : null));
dsCustomer odsCustomer = new dsCustomer();
this.ReadIntoTypedDs(odsCustomer,
   "[dbo].[LookupEmployees]", oSQLParms);
return odsCustomer;

ヒント7: 基本のASP.NET 2.0 Webページの作成

 リスト3は、Webページの完全な分離ソースコードです(Default.aspx.cs)。このページには、ヒント6のデータアクセスクラスへの参照が含まれているため、開発者はDALを呼び出すことができます。コードの重要なポイントを次に示します。

  • 1文字の英数字による「クイックナビ」用のドロップダウンリスト(cboAlphaIndex)は、英数字値の配列から読み込まれます(詳細については、ヒント8を参照)。
  • Retrieveボタン(btnRetrieve)のClickイベントが、ページメソッドのGetDataを呼び出します。次にこれが、DALのGetCustomersメソッドを呼び出します。
  • ページがGetDataを呼び出すときはいつでも、SetInfoという名前のメソッドも呼び出します(このメソッドについてはヒント10で説明します)。このメソッドは結果セット内の行数を表示し、さらにセッション変数のCurrentFirstRowおよびCurrentLastRowを更新します。
  • 4つのナビゲーションコマンドボタン(btnFirstbtnPrevbtnNextbtnLast)は、4つのナビゲーションメソッドを呼び出します。これについては、ヒント8で説明します。
  • GridView(grdResults)には、ユーザーが列見出しをクリックするたびに起動するイベント(grdResults_Sorting)があります。詳細については、ヒント9で説明します。
  • GridViewには、ユーザーがGridView内の行を選択すると起動するイベント(grdResults_SelectedIndexChanged)もあります。ヒント11では、このイベントの処理方法と、GridViewのDataKeyNamesプロパティの定義済みの値(CustomerIDに設定)を使って行の値を決定する方法について説明します。

ヒント8: クイック検索ナビゲーションとページングの処理

 ページのLoadイベントによって、ユーザーがクイックナビゲーション用に選択できる1文字値のドロップダウンリストが読み込まれます。

string[] alphabet = new string[] { " ", "A", "B",
   "C",..., "0", "1", "2", "3"...;
for (int i = 0; i < alphabet.Length; i++)
   this.cboAlphaIndex.Items.Add(alphabet[i].Trim());

 コードには、ナビゲーション用の4つのメソッドもあります。これは、4つのナビゲーションボタンのClickイベントに対応します。これらのメソッドは、ナビゲーションの方向に応じてStartRowIndexを設定します。

private void NavBegin()
{
   // set the startrowindex to zero, and make sure
   // we’re not specifying a letter
   Session["startRowIndex"] = 0;
   // set alpha index pulldown back to nothing
   this.cboAlphaIndex.SelectedIndex = 0;
   this.GetData();
}

private void NavPrevious()
{
   // set the startrowindex to the row number for the
   // first record in the current page, minus 1, and
   // minus maxrows
   // so if we’re looking at rows 200-249, and we go
   // back one page, the new start row index would be
   // 200-1-50, or 149....and we’d get back 149-199
   Session["startRowIndex"] = (int)Session["CurrentFirstRow"] -
      (int)Session["MaxRows"];
   this.cboAlphaIndex.SelectedIndex = 0;
   this.GetData();
}

private void NavNext()
{
   // startrow index becomes the value of the last
   // row  [the stored proc does a ’greater than’]
   Session["startRowIndex"] = (int)Session["CurrentLastRow"] + 1;
   this.cboAlphaIndex.SelectedIndex = 0;
   this.GetData();
}

private void NavEnd()
{
   // -1 is the ’magic number’, it tells the stored
   // proc to just grab everything from
   // rowcount-maxrows, to rowcount
   Session["startRowIndex"] = -1;
   this.cboAlphaIndex.SelectedIndex = 0;
   this.GetData();
}

ヒント9: 列の並び替えの処理

 列を並び替えるには、Sortingイベントを利用します。このイベントは、ユーザーが選択した列見出しのSortExpressionを公開します。SortExpressionをプログラムで明示的に設定しない場合、SortExpressionはデータ列の名前になります。

protected void grdResults_Sorting
    (object sender, GridViewSortEventArgs e)
{
   Session["SortCol"] = e.SortExpression.ToString().Trim();
   this.lblAlphaNav.Text = e.SortExpression.ToString().Trim() +
      " starting with...";
   this.GetData();
}

ヒント10: グリッドの結果セット情報の表示

 GetDataを呼び出して結果セットを返したら、結果セットをSetInfoに渡します。このメソッドでは、最初に、集計行を取り出します。これは、次に示すように、フィルタ条件を満たす行の総数に相当します。

using daCustomer;
private void SetInfo(dsCustomer odsCustomer)
{
   DataRow[] aRows = odsCustomer.dtCustomer.Select
      ("customerid = -1");
   int nTotalCount = 0;
   if( aRows.Length> 0) {
      dsCustomer.dtCustomerRow oRow =
         (dsCustomer.dtCustomerRow)aRows[0];
      nTotalCount = oRow.RowNum;
      oRow.Delete();
      odsCustomer.dtCustomer.AcceptChanges();
   }
...

 SetInfoは、次に、実際に表示する行数を決定します。すべてではありませんが、ほとんどの場合、これはMaxRowsと同じになります。値が1以上の場合、メソッドは結果セットの先頭と末尾の行のRowNum列を読み取り、その値を、セッション変数のCurrentFirstRowおよびCurrentLastRowに割り当てます。

int nResultCount = odsCustomer.dtCustomer.Rows.Count;
if (nResultCount > 0) {
   Session["CurrentFirstRow"] =
      odsCustomer.dtCustomer[0].RowNum;
   Session["CurrentLastRow"] = odsCustomer.dtCustomer
      [nResultCount - 1].RowNum;
}
this.grdResults.Caption =
   "Number of matching records: " +
   nTotalCount.ToString().Trim() +
   "...click on any column heading to sort";

ヒント11: 別のページにリンクする列の設定

 ASP.NETの初心者がよく悩んでしまう問題の1つは、GridView内の各行に対してリンクを設定するにはどうすればいいかということです。また、図1、2、3に示されているような選択アイコンを各行に追加する方法も問題になります。

 基本の3つの手順に従うだけで、これを実装できます。最初に、GridViewにバインドされている列のリストから、キーフィールドを特定する必要があります。これを行うには、GridViewのDataKeyNamesプロパティを設定します。キーフィールド(またはフィールドの組み合わせ)は、各行の一意な値を表している必要があります。サンプルプロジェクトではCustomerIDを一意なIDとして使っているため、開発者はGridViewの一意なIDを次のように定義できます。

// set the DataKeyNames property
// to uniquely determine a selected row
this.grdResults.DataKeyNames =
   new string[] { "CustomerID" };

 次に、ButtonFieldオブジェクトを作成し、プロパティを設定し、オブジェクトをGridViewに追加することによってアイコン列を追加します。

// Insert a button field to the GridView
// so that the user can select a row by clicking
// on the button icon
ButtonField obt = new ButtonField();
obt.CommandName = "Select";
obt.ButtonType = ButtonType.Image;
obt.ImageUrl = "openfolder.ico";
this.grdResults.Columns.Clear();
this.grdResults.Columns.Add(obt);

 最後に、ユーザーがアイコンをクリックすると起動するGridViewのSelectedIndexChangedイベントを利用します。イベントの中で、SelectedDataKeyプロパティの値をキーの有効な型に変換します。この例の場合は、現在の顧客IDの値を取得し、そのIDを使って、クエリ文字列が含まれる別のWebページへの呼び出しを設定します。

protected void grdResults_SelectedIndexChanged
   (object sender, EventArgs e)
{
   //  this reads from the DataKeyNames property
   int nCustomerID = (int)this.grdResults.
      SelectedDataKey.Values[0];
   Response.Redirect("CustomerPage.aspx?CUSTID=" +
      nCustomerID.ToString().Trim());
}

ヒント12: 可変個数の選択の許可

 最善の努力を払ったとしても、ユーザーは新たな要望を抱えて舞い戻ってきます。人数情報のフィルタに加えて、ルックアップテーブルから顧客の会計情報(支払日、延滞30日など)をフィルタすることも要求されます。エンドユーザーは、1つのステータス、複数のステータス、またはすべてのステータスを選択する可能性があります。ステータスコードは、Customerテーブルに存在します。

 これは、問題への取り組みのプロセスを繰り返す良い練習です。この練習は、可変個数の選択を処理する再利用可能な手法も示しています。

  • Webページを変更して、複数の選択に対応できるデータバインドコントロールを含めます。開発者によっては上級のサードパーティコントロールを選ぶかもしれませんが、この例では単純なASP.NET 2.0 CheckedListBox Webコントロールを使います。
  • 選択した値のCheckedListBoxコントロールをXML文字列に読み取るコードを作成します。
  • データアクセスクラスを変更して、このXML文字列をパラメータとしてストアドプロシージャに渡します。
  • 最後に、ストアドプロシージャを変更してXMLパラメータを処理します。

 上のタスクは、ユーザーインターフェイスで始まり、データベースで終わっているため、その順序で変更を進めようと思いがちです。しかし、バックエンドから始めて、フロントエンドに向かって作業する方が、わずかながら効率的です。設計は外側から内側に向かって行い、開発は内側から外側に向かって行います。

 最初に、XMLパラメータをストアドプロシージャに追加します。XML文字列は再び、ユーザーが選択するステータスの列を格納します。SQL Server 2005は新しいXMLデータ型をサポートするため、このプロセスは非常に簡単に行われます。Customerテーブルへのジョインでは、ストアドプロシージャはXML文字列をテーブル変数に変換します。

 SQL Server 2005より前のバージョンでは、開発者はしばしば、sp_xml_preparedocumentシステムストアドプロシージャとOPENXML関数を使って、XML選択をSQLテーブルに変換していました。この方法は今でも有効ですが、2つの問題を抱えています。第一に、メモリ負荷が大きくなります。ストアドプロシージャsp_xml_preparedocumentは、COM XMLドキュメントオブジェクトモデルへのメモリポインタを返すため、大規模なXMLドキュメント上でOPENXMLを使うとサーバに負担がかかります。第二に、sp_xml_preparedocumentをT-SQLユーザー定義関数の内部で使用することはできません。そのため、このタスク用の汎用的で再利用可能なSQL機能を開発することは困難です。

 SQL Server 2005の新しいXML機能が反映されているのは、XQueryと呼ばれる一般的な機能と、nodes()という特別なメソッドです。これらを使うと、XML文字列から特定のデータをテーブルに切り出すことができます。この機能は使用するリソースが少なくて済み、1行のコードでタスクを実現できます。

 リスト4には、XMLtoTableと呼ばれるT-SQL 2005テーブル値UDFが含まれています。このUDFは、XML文字列をパラメータとして受け取り、IDpk列内のXML文字列に格納されている整数キーを含むテーブル変数を返します。XML文字列内の選択列の名前を可変にするには、UDFを変更して、列の名前をパラメータとして渡します。

 リスト4内のコードでは、nodes()メソッドを使って、XML文字列のIDpkノードを先頭にクエリを実行し、結果をテーブル/(列)形式でエイリアスに格納します。そこから、SELECT文はvalue関数を使ってIDpkの値を整数の結果セットに読み込みます。

INSERT INTO @tPKList
SELECT Tbl.col.value(’.’,’int’) as IntPK
FROM   @XMLString.nodes(’//IDpk’ ) Tbl(col)

 このUDFを、Customersテーブルに対するメインクエリに組み込むことができます。

-- uses the new parameter XMLString
-- that contains the list of statuses
SELECT <column list>
FROM Customers
   JOIN [dbo].[XMLtoTable](@XMLString) StatusList
ON StatusList.IntPK = customers.statusfk

 データベースの処理は終わりました。この後の処理はずっと簡単です。リスト2内のデータアクセスクラスを変更して、新しいパラメータを含めます。

// code also adds XMLStatuses as a parameter
// to GetCustomers in Listing 4
oSQLParms.Add(
new SqlParameter("@XMLString", XMLStatuses));

 あと一息です。最後のステップでは、CheckedListBoxコントロールを割り当て、それを読み取ることによって、Webページを処理します。

 Webページには、コントロールを割り当てるコードが必要です。デモ上の目的で、コードはDataTableを手動で読み込みます。実際には、顧客ステータスコードのバックエンドデータベースを読み取るデータクラスから得られます。

DataTable dtStatus = new DataTable();
dtStatus.Columns.Add("StatusPK", typeof(Int32));
dtStatus.Columns.Add("Descript", typeof(String));
dtStatus.Rows.Add(1, "Up to Date");
dtStatus.Rows.Add(2, "Overdue 30 days");
dtStatus.Rows.Add(3, "Overdue 60 days");
dtStatus.Rows.Add(4, "Overdue 90 days");
dtStatus.Rows.Add(5, "Overdue 120 days");
dtStatus.Rows.Add(6, "Account suspended");
// Set the data binding, and the text/value fields
this.chkStatusList.DataSource = dtStatus;
this.chkStatusList.DataTextField = "descript";
this.chkStatusList.DataValueField = "statuspk";
this.chkStatusList.DataBind();

 最後に、ユーザーが選択したアイテムのCheckedListBoxを読み取り、XML文字列を返すコードを作成する必要があります。次のコードでは、一時DataSetを作成し、コントロール内のアイテムのコレクションを通じて読み取りを行い、選択されたアイテムを一時DataSetに挿入します。次に、GetXMLメソッドを使って、選択アイテムが含まれる一時DataSetのXML表現を返します。

private string GetStatuses()
{
   DataTable dtSelected = new DataTable();
   dtSelected.Columns.Add("IDpk", typeof(Int32));
   foreach (ListItem oItem in this.chkStatusList.Items)
      if (oItem.Selected == true)
         dtSelected.Rows.Add(Convert.ToInt32(oItem.Value));
   DataSet ds = new DataSet();
   ds.Tables.Add(dtSelected);
   return ds.GetXml();
}

ヒント13: カスタムオブジェクトに出力するようにDALを変更

 ヒント6で述べたように、2006年9月/10月号の『CoDe Magazine』に掲載された「Baker’s Dozen」の記事では、TableAdapterまたはDataSetのMerge関数を使わずに、.NETジェネリックを利用して、型指定されたDataSetをストアドプロシージャから直接読み込むコードを紹介しました。

 カスタムコレクションに「均等な時間」を与えるために、.NETジェネリックを使った基本メソッドをデータアクセス層に追加しました。メソッドReadIntoCollection(リスト5を参照)はストアドプロシージャを実行し、結果を直接カスタムコレクションに出力します。例えば、前のヒントの例で、型指定されたDataSetではなく、カスタムコレクションを使うものとします。

List<SqlParameter> oSQLParms = new List<SqlParameter>();
oSQLParms.Add(new SqlParameter("@LastName", LastName));
List<CustomerClass> oCustomers = new List<CustomerClass>();
// pass an instance of the list, SP name/parms,
// and a type reference to the class
this.ReadIntoCollection(oCustomers, "[dbo].[LookupEmployees]",
   oSQLParms, typeof(CustomerClass));

 このメソッドの内部では何が起きているのでしょうか。

 はじめに、メソッド内のパラメータを見てみましょう。呼び出し元の関数が、CustomerClassアイテムリストのインスタンスを基本メソッドに渡す際、基本メソッドは、この特定のクラスについて何も知りません。ここでは、パラメータ定義で.NETジェネリックを使うことができます。先頭のパラメータとしてList oCollectionを指定することで、任意の種類の有効なListを渡すことができます。2番目のパラメータと3番目のパラメータ(ストアドプロシージャ名およびSQLパラメータのリスト)は、ヒント6と同じです。最後のパラメータは、クラス自体、つまりCustomerClassへの型参照です。リストを読み込むときにクラスのプロパティを反復するため、基本メソッドにはこのパラメータが必要です。

public void ReadIntoCollection<T>List<T> oCollection,
   string cStoredProc, List<SqlParameter> oParmList,
   Type oCollectionType)

 次に、基本メソッドは接続を開き、ストアドプロシージャのコマンドオブジェクトを定義します。そして、SQLパラメータを再び確立します。これは、DataSetメソッドと非常によく似ています。

SqlConnection oSqlConn = this.GetConnection();
SqlCommand oCmd = new SqlCommand(cStoredProc, oSqlConn);
oCmd.CommandType = CommandType.StoredProcedure;
foreach (SqlParameter oParm in oParmList)
   oCmd.Parameters.Add(oParm);

 今回は、.NET DataAdapterのFillメソッドを使うのではなく、接続を開き、DataReaderを実行します。リーダーを反復してカスタムリストを読み込むことが目的です。

oSqlConn.Open();
SqlDataReader oDR = oCmd.ExecuteReader();

 次の一連のステップでは、リーダーを通じてTypeパラメータを使ってクラスのインスタンスを作成し、クラスのすべてのプロパティを決定します。リーダーからクラスインスタンスへそのプロパティ名の実際の値を読み取り、リストを作成します。ずいぶんと複雑そうです。まるで昔の、「大きな象を食べるにはどうすればいい?」「一口ずつ食べていけばいい」という話を思い出します。

 まずこのコードでは、リーダーオブジェクトを使ってループを設定し、クラスタイプのインスタンスを作成します。パラメータが.NETジェネリックを利用してクラスパラメータを定義しているため、コードではTプレースホルダを使ってクラスのインスタンスを指定できます。このコードをデバッガで調べると、oItemがCustomerClass型のクラスであることが分かります。

while(oDR.Read())  {
   T oItem = (T)Activator.CreateInstance(
      oCollectionType);

 次に、コードは、クラスのすべてのプロパティを検出するのに.NETリフレクションを多少使う必要があります。oItemのすべてのプロパティを、GetPropertiesを使ってPropertyInfo型の配列に読み込みます。配列oCollectionPropsを呼び出します。

// get all the properties of the class
PropertyInfo[] oCollectionProps = (
   (Type) oItem.GetType()).GetProperties();

 クラスのプロパティの配列ができたため(oCollectionProps)、その配列を反復処理し、プロパティの名前を取得し、リフレクションメソッドSetValueを使って、DataReaderからoItem内の特定のプロパティの値を設定できます。

for (int n=0; n<oCollectionProps.Length; n++)
{
   string cPropName = CollectionProps[n].Name;
   oCollectionProps[n].SetValue
      (oItem, oDR[cPropName], null);
}
// Add the item to the collection
oCollection.Add(oItem);
// Now get the next row in the DataReader

 よく理解できない場合は、まずジェネリックを使わないコードを考えてみてから、ジェネリックを使用するコードと比較してみましょう。

   while(oDR.Read())  {
      CustomerClass oCustomer = new CustomerClass();
      // no need to loop through properties, we
      // know what they are
      oCustomerClass.FirstName = oDR["FirstName"];
      oCustomerClass.LastName = oDR["LastNName"];
   }
   oCollection.Add(oItem);
}

完成ソースコード

リスト1 完成版のストアドプロシージャ
IF EXISTS (SELECT * FROM sys.objects WHERE object_id =
OBJECT_ID(N’[dbo].[LookupEmployees]’) AND type in (N’P’, N’PC’))
DROP PROCEDURE [dbo].[LookupEmployees2]
go
CREATE PROCEDURE [dbo].[LookupEmployees2]
    @LastName varchar(50)=null, @FirstName varchar(50)=null,
    @Address varchar(50)=null,  @City varchar(50)=null,
    @State varchar(2)=null,     @Zip varchar(50)=null,
    @StartRowIndex int, @MaxRows int,
    @AlphaChar varchar(1)=null,   @SortCol varchar(20)=null
AS
BEGIN
SET NOCOUNT ON
DECLARE @lPaging bit
IF  @AlphaChar is null
SET @lPaging = 0
else
SET @lPaging = 1
WITH CustListTemp AS
   (SELECT CustomerID, LastName, FirstName, Address, City, State,
           Zip, ROW_NUMBER() OVER (ORDER BY
CASE @SortCol
WHEN ’LASTNAME’ THEN LastName + Firstname
WHEN ’ADDRESS’ THEN Address
WHEN ’CITY’  THEN City + LastName + Firstname
WHEN ’STATE’ THEN STATE + LastName + Firstname
WHEN ’ZIP’  THEN ZIP + LastName + Firstname
ELSE LastName + Firstname END) AS RowNum
FROM Customers
WHERE LastName LIKE ’%’+COALESCE(@LastName,LastName)+’%’ AND
   FirstName LIKE ’%’+COALESCE(@FirstName,FirstName)+’%’ AND
   Address LIKE ’%’+COALESCE(@Address,Address)+’%’ AND
   City LIKE ’%’+COALESCE(@City,City)+’%’ AND
   State LIKE ’%’+COALESCE(@State,State)+’%’ AND
   Zip LIKE ’%’+COALESCE(@Zip,Zip)+’%’   )
SELECT TOP (@MaxRows) CustomerID, LastName, FirstName, Address,
   City, State, Zip, RowNum
FROM
   ( SELECT CustListTemp.*,
      (SELECT COUNT(*) from CustListTemp) AS RecCount
       FROM CustListTemp )CustList
       WHERE
       CASE
         WHEN @lPaging = 1 AND @SortCol= ’LASTNAME’  AND
            SUBSTRING(LastName,1,1) >= RTRIM(@AlphaChar) THEN 1
         WHEN @lPaging = 1 AND @SortCol= ’ADDRESS’ AND
            SUBSTRING(Address,1,1) >= RTRIM(@AlphaChar) THEN 1
         WHEN @lPaging = 1 AND  @SortCol= ’CITY’  AND
            SUBSTRING(City,1,1) >= RTRIM(@AlphaChar) THEN 1
         WHEN @lPaging = 1 AND  @SortCol= ’STATE’ AND
            SUBSTRING(State,1,1) >= RTRIM(@AlphaChar) THEN 1
         WHEN @lPaging = 1 AND  @SortCol= ’ZIP’  AND
            SUBSTRING(Zip,1,1) >= RTRIM(@AlphaChar) THEN 1
         WHEN @lPaging = 0 AND
            RowNum BETWEEN
               ( CASE @StartRowIndex WHEN -1 THEN
               ( RecCount ) - @MaxRows ELSE  @StartRowIndex END )
            AND
               ( CASE @StartRowIndex WHEN -1 then
               ( RecCount )- @MaxRows ELSE @StartRowIndex END) +
                   @MaxRows
            THEN 1 ELSE 0 END = 1
         END
GO
リスト2 カスタムDAL
using System;
using System.Collections.Generic;
using System.Text;
using System.Data.SqlClient;
using System.Data;
namespace daCustomer
{
   public class daCustomer : SimpleDataAccess.SimpleDataAccess
   {
      public dsCustomer GetCustomers(string FirstName,
         string LastName, string Address, string City,
         string State, string Zip, int StartRowIndex,
         int MaxRows, string AlphaChar, string SortCol)
      {
         if (AlphaChar == "")
            AlphaChar = null;
         List<SqlParameter> oSQLParms = new
            List<SqlParameter>();
         oSQLParms.Add(new SqlParameter("@LastName",
            LastName.Length > 0 ? LastName : null));
         oSQLParms.Add(new SqlParameter("@FirstName",
            FirstName.Length > 0 ? FirstName : null));
         oSQLParms.Add(new SqlParameter("@Address",
            Address.Length > 0 ? Address : null));
         oSQLParms.Add(new SqlParameter("@City",
            City.Length > 0 ? City : null));
         oSQLParms.Add(new SqlParameter("@State",
            State.Length > 0 ? State : null));
         oSQLParms.Add(new SqlParameter("@Zip",
            Zip.Length > 0 ? Zip : null));
         oSQLParms.Add(new SqlParameter("@startRowIndex",
            StartRowIndex));
         oSQLParms.Add(new SqlParameter("@MaxRows",  MaxRows));
         oSQLParms.Add(new SqlParameter("@alphachar", AlphaChar));
         oSQLParms.Add(new SqlParameter("@SortCol",SortCol ));
         dsCustomer odsCustomer = new dsCustomer();
         this.RetrieveDataIntoTypedDs(odsCustomer,
            "[dbo].[LookupEmployees]", oSQLParms);
         return odsCustomer;
      }
   }
}
リスト3 ページの分離コード
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class _Default : System.Web.UI.Page
{
   protected void Page_Load(object sender, EventArgs e)
   {
      if (!this.IsPostBack)
      {
         Session["CriteriaSet"] = false;
         string[] alphabet = new string[] { " ", "A", "B", "C",
            "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P",
            "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2",
            "3", "4", "5", "6", "7", "8", "9" };
         for (int i = 0; i < alphabet.Length; i++)
            this.cboAlphaIndex.Items.Add(alphabet[i].Trim());
         this.InitializeVars();
      }
   }
   protected void btnRetrieve_Click(object sender, EventArgs e)
   {
      this.GetData();
   }
   private void SetInfo(daCustomer.dsCustomer odsCustomer)
   {
      int nResultCount = odsCustomer.dtCustomer.Rows.Count;
      if (nResultCount > 0)
      {
         Session["CurrentFirstRow"] = odsCustomer.dtCustomer[0].RowNum;
         Session["CurrentLastRow"] = odsCustomer.dtCustomer[
            nResultCount - 1].RowNum;
      }
      this.grdResults.Caption = "Number of matching records: "
         + nResultCount.ToString().Trim();
   }
   private void GetData()
   {
      string FirstName = this.txtFirstName.Text.ToString().Trim();
      string LastName = this.txtLastName.Text.ToString().Trim();
      string Address = this.txtAddress.Text.ToString().Trim();
      string City = this.txtCity.Text.ToString().Trim();
      string State = this.txtState.Text.ToString().Trim();
      string Zip = this.txtZip.Text.ToString().Trim();
      string AlphaChar = this.cboAlphaIndex.Text.ToString().Trim();
      int StartRowIndex = Convert.ToInt32(Session["StartRowIndex"]);
      int MaxRows = Convert.ToInt32(Session["MaxRows"]);
      string SortCol = Convert.ToString(Session["SortCol"]);
      daCustomer.daCustomer odaCustomer = new
         daCustomer.daCustomer();
      daCustomer.dsCustomer odsCustomer =
         odaCustomer.GetCustomers(FirstName, LastName,
         Address, City, State, Zip, StartRowIndex, MaxRows,
         AlphaChar, SortCol);
      this.SetInfo(odsCustomer);
      this.grdResults.DataSource = odsCustomer;
      this.grdResults.DataBind();
   }
   private void InitializeVars()
   {
      Session["startRowIndex"] = 0;
      Session["AlphaChar"] = null;
      Session["CurrentFirstRow"] = 0;
      Session["CurrentLastRow"] = 0;
      Session["MaxRows"] = 15;
      Session["SortCol"] = "LASTNAME";
   }
   protected void btnFirst_Click(object sender,
      ImageClickEventArgs e)
   {
      this.NavBegin();
      this.GetData();
   }
   protected void btnPrev_Click(object sender, ImageClickEventArgs e)
   {
      this.NavPrevious();
      this.GetData();
   }
   protected void btnNext_Click(object sender, ImageClickEventArgs e)
   {
      this.NavNext();
      this.GetData();
   }
   protected void btnLast_Click(object sender, ImageClickEventArgs e)
   {
      this.NavEnd();
      this.GetData();
   }
   private void NavBegin()
   {
      // set the startrowindex to zero, and make sure we’re
      // not specifying a letter
      Session["startRowIndex"] = 0;
      Session["AlphaChar"] = null;
      this.cboAlphaIndex.SelectedIndex = 0;
   }
   private void NavPrevious()
   {
      // set the startrowindex to the row number for the first
      // record in the current page, minus 1, and minus maxrows
      // so if we’re looking at rows 200-249, and we go back one
      // page, the new start row index would be 200-1-50, or
      // 149....and we’d get back 149-199
      Session["startRowIndex"] = (int)Session["CurrentFirstRow"] -
         (int)Session["MaxRows"];
      Session["AlphaChar"] = null;
      this.cboAlphaIndex.SelectedIndex = 0;
   }
   private void NavNext()
   {
      // startrow index becomes the value of the last row  [the
      // stored proc does a ’greater than’]
      Session["startRowIndex"] = (int)Session["CurrentLastRow"] + 1;
      Session["AlphaChar"] = null;
      this.cboAlphaIndex.SelectedIndex = 0;
   }
   private void NavEnd()
   {
      // -1 is the ’magic number’, it tells the stored proc to
      // just grab everything from rowcount-maxrows, to rowcount
      Session["startRowIndex"] = -1;
      Session["AlphaChar"] = null;
      this.cboAlphaIndex.SelectedIndex = 0;
   }
   protected void cboAlphaIndex_SelectedIndexChanged(
      object sender, EventArgs e)
   {
      this.GetData();
   }
   protected void grdResults_Sorting(object sender,
      GridViewSortEventArgs e)
   {
      Session["SortCol"] = e.SortExpression.ToString().Trim();
      this.GetData();
   }
   protected void grdResults_SelectedIndexChanged
                                (object sender, EventArgs e)
   {
      int nCustomerID = (int)this.grdResults.SelectedDataKey.Values[0];
      Response.Redirect("CustomerPage.aspx?CUSTID=" +
         nCustomerID.ToString().Trim());
   }
}
リスト4 XML文字列をテーブル変数に変換するT-SQL 2005 UDF
-- Table-valued UDF to convert an XML string
-- to a table-valued UDF
-- Useful if you have an XML string of user-selections,
-- and want to convert them to a Table variable that
-- you can use in subsequent JOIN statements
-- Uses the new XML data type
CREATE FUNCTION [dbo].[XMLtoTable]
   (@XMLString XML )
RETURNS
   @tPKList TABLE  ( IntPK int )   -- returns table variable
AS
BEGIN
INSERT INTO @tPKList
SELECT Tbl.col.value(’.’,’int’) as IntPK
FROM   @XMLString.nodes(’//IDpk’ ) Tbl(col)
-- use nodes() method to shred XML data into relational
-- data.  Incoming XML must have a numeric column
-- with the name IDpk
RETURN
END
リスト5 結果セットをカスタムオブジェクトに出力するジェネリックDALメソッド
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Reflection;
namespace SimpleDataAccess
{
   public class SimpleDataAccess
   {
      public void RetrieveIntoCollection<T>(
         List<T> oCollection , string cStoredProc,
         List<SqlParameter> oParmList, Type oCollectionType)
      {
         SqlConnection oSqlConn = this.GetConnection();
         SqlCommand oCmd = new SqlCommand(cStoredProc, oSqlConn);
         oCmd.CommandType = CommandType.StoredProcedure;
         foreach (SqlParameter oParm in oParmList)
            oCmd.Parameters.Add(oParm);
         oSqlConn.Open();
         SqlDataReader oDR = oCmd.ExecuteReader();
         while(oDR.Read())  {
            T oItem = (T)Activator.CreateInstance(oCollectionType);
            // get all the properties of the class
            PropertyInfo[] oCollectionProps =
               ((Type) oItem.GetType()).GetProperties();
            for (int n=0; n<oCollectionProps.Length; n++)  {
               string cPropName = oCollectionProps[n].Name;
               oCollectionProps[n].SetValue(oItem, oDR[cPropName], null);
            }
            oCollection.Add(oItem);
         }
         oSqlConn.Close();
      }
      public SqlConnection GetConnection()
      {
         SqlConnectionStringBuilder oStringBuilder = new
            SqlConnectionStringBuilder();
         oStringBuilder.UserID = "sa";
         oStringBuilder.Password = "";
         oStringBuilder.InitialCatalog = "NewCustomer";
         oStringBuilder.DataSource = "KCI890";
         return new SqlConnection(oStringBuilder.ConnectionString);
      }
   }
}

付録: コード集: 匿名メソッドによるカスタムリストのソート

 私はどちらかと言えばDataSet派ですが、.NETジェネリックのListクラスの有用さ、特にC# 2.0の新しい匿名メソッドと組み合わせた場合の威力は認めざるを得ません。この付録では、カスタムリストコレクションのソートとフィルタを行ういくつかのコードを紹介します。

 例えば、LocationID、Customer ID、およびAmount Dueの各フィールドから成るレコードのリストがあるものとします。Amount Dueが10000より大きいという条件で、Locations 1とLocations 2についてフィルタを実行します。また、その結果を、Location内でAmount Dueの降順にソートします。ADO.NETを使うと、次に示すように、DataViewでこれを実現できます。

DataView dv = new DataView(dt);
dv.RowFilter =
   "LocationID in (1,2) AND AmountDue > 10000";
dv.Sort = "LocationID, AmountDue DESC";

 「DataSet対カスタムコレクション」という議論の中で、DataSetの支持者は、カスタムコレクションで同じ機能を実現するには複雑なコードを記述しなければならないと主張します(Visual Studio 2005より前の時点では、私も確かにこのような主張をしていました)。

 しかし、Visual Studio 2005が提供する2つの新しい機能を組み合わせれば、上記のADO.NETコードに十分対抗できます。第一に、新しいListクラスには、ソートとフィルタのメソッドが用意されています(SortメソッドとFindAllメソッドを使用)。ソート/フィルタのカスタムメソッドを作成し、メソッドの名前を、Sort/FindAllメソッドのデリゲートパラメータとして指定します。

 第二に、C# 2.0では匿名メソッドを使ってデリゲートの代わりにロジックを配置できます。つまり、個別のカスタムメソッドを作成するのではなく、本来ならデリゲートが生じる位置に、インラインでコードを設定できます。いくつかのコードサンプルを紹介しましょう。DataSetの代わりに、CustomerRecという名前のカスタムリストの例を使用します。これは、LocationIDおよびAmountDueというプロパティを持ちます。

 コードでは、リストのFindAllメソッド内に匿名メソッドを挿入し、LocationIDが1に等しい顧客のフィルタリストを作成します。次に、フィルタリストをAmountDueの降順にソートします。

 Sortメソッドのデリゲートパラメータが2つのパラメータを受け取ることに注目してください。これは、ソート比較を構成する各オブジェクトインスタンスにそれぞれ対応します。匿名メソッドは、リスト内の各アイテムに対して実行されます。実行のたびに、コードは2つの入力値を比較し、.NETのCompareToメソッドを使って2つの値のうち大きい方の値を返します。昇順でソートを呼び出した場合は、最初のパラメータと2番目のパラメータを比較しますが、この例では降順で呼び出しているので、パラメータの使い方が反転します。

// anonymous method to filter on Location = 1
List<CustomerRec> oFilteredCustomers =
   oCustomerRecs.FindAll(
   (delegate(CustomerRec oRec)  {
   return (oRec.LocationID == 1 );})
   );

// anonymous method to sort on amount due DESC
// by reversing the incoming parameters
oFilteredCustomers.Sort(
   delegate(CustomerRec oRec1, CustomerRec oRec2)
   { return oRec2.AmountDue.CompareTo
      (oRec1.AmountDue); });

 ORとANDの組み合わせなど、もっと複雑なインラインコードを含めることができます。次のコードサンプルでは、Location 1または2、かつAmount Dueが10000より大きいという条件でデータをフィルタするADO.NETサンプルのロジックを再現します。

// anonymous method to filter on
// either Location 1 or 2, AND amount due GT 10000
List<CustomerRec> oFilteredCustomers =
   oCustomerRecs.FindAll((delegate(CustomerRec oRec)  {
   return (
      (oRec.LocationID == 1 || oRec.LocationID == 2)
      && oRec.AmountDue > 10000);
   }));

 最後のコードサンプルは、Location内でAmountの降順でフィルタリストをソートする匿名メソッドを示しています。デリゲートは、各入力比較に対応する2つのパラメータを受け取ります。Locationが等しい場合は、2番目のパラメータのAmount Dueを1番目のパラメータに対して比較します。Locationが等しくない場合は、1番目のパラメータのLocationIDを2番目のパラメータに対して比較します。

// Now sort on amount due DESC, within Location
// To do so, check the two incoming locations 1st
// If they are equal, reverse the order of two
// incoming parameters, and compare the amount due
// [just like above]
// If they AREN’T equal, compare the two locations
oFilteredCustomers.Sort(
   delegate(CustomerRec oRec1, CustomerRec oRec2)
   { return oRec1.LocationID == oRec2.LocationID ?
       oRec2.AmountDue.CompareTo(oRec1.AmountDue):
       oRec1.LocationID.CompareTo(oRec2.LocationID);
   });

 結局のところ、開発者は多少コードを記述する必要はありますが、新しいListクラスを使うことで、高度なソートとフィルタの機能を実装できるようになりました。加えて、匿名メソッドを実装できるようになったことで、ADO.NET構文を超えるカスタムフィルタを作成することが可能になりました(ADO.NETではカスタムフィルタメソッドのフックはサポートされていません)。

 この記事で紹介したソースコード全体は、私のWebサイトに掲載されています。詳細については、私のブログを参照してください。

最後に

 記事、論文、コードなどを投稿した後に、よいアイディアを思いついたことはありませんか。私は、後になっていろいろと思いつくのが得意です。幸いなことに、そういう場合はブログが重宝します。私がこれまで発表した記事の補足ヒントや注意に関しては、私のブログを参照してください。場合によっては、お楽しみが見つかるかもしれません。

著者紹介

Kevin S. Goff(Kevin S. Goff)
.NET、Visual FoxPro、SQL Server、Crystal Reportsによる独自のWebソリューション/デスクトップソリューションを提供するコンサルティンググループ「Common Ground Solutions」の創業者兼主任コンサルタント。ソフトウェアアプリケーションの開発経験は17年に及ぶ。米農務省からシステムオートメーション関連の賞をいくつか受賞。また、6桁の投資見返りを実現するソリューションを開発したことによりFortune 500企業から特別表彰を受ける。保険、会計、環境衛生、不動産、出版、広告、製造、金融、日用品、貿易振興など多様な業界に携わった経験を持ち、さまざまな形態で独自のトレーニングも行っている。
最新トップニュース
  • サイジニア株式会社は2008年10月8日、ANA セールス株式会社が運営する「ANA SKY WEB TOUR」のユーザー向けの旅行商品推奨システムとして、コミュニティディスカバリーエンジン「デクワス」を納入、稼働を開始したと発表した。
  • 国内日立電線が100ME「Apresia」2製品を発表(Webテクノロジー 10月8日 17:50)
    日立電線は2008年10月8日、イーサネットスイッチ「Apresia シリーズ」の100メガビットイーサネットスイッチ2製品を開発し、11月28日から販売を開始すると発表した。
  • ヤフー株式会社は2008年10月8日、同社の提供する有料会員サービス「Yahoo! プレミアム」にて、12月1日より会員費を月額294円(総額)から月額346円(総額)へ改定する、と発表した。
  • 国内DNP、多機能 IC カードの新タイプを開発(Webテクノロジー 10月8日 17:20)
    大日本印刷株式会社(DNP)は2008年10月8日、Java Card 版 FeliCa デュアルインターフェイスカードの新タイプを開発し、金融機関向けに10月中旬より販売を開始すると発表した。
  • 株式会社サンゼロミニッツは2008年10月8日、同社が運営するタウン情報検索サイト「30min.」のランチマップ機能が、米国時間10月7日に公開された Firefox3 のアドオン「Geode」に対応した、と発表した。
Graphic Design Forum
【Graphic Design Forum】
活気に満ちた誕生日をどうぞ (10月8日)
データメーション
【データメーション】
Google 版酒気検知機能が間もなく登場(10月8日)
ベンチャー専門家の目利きブログ「なぜこの企業は伸びるのか?」
【ベンチャー専門家の目利きブログ「なぜこの企業は伸びるのか?」】
「ITを活用し、消費生活における意思決定の支援、悩み・迷いを解決する!」/株式会社ALBERT(10月8日)
エンジニアの独り言
【エンジニアの独り言】
得体の知れない情報(?)との向き合い方(9月17日)
最新テクノロジーの意外な処方箋
【最新テクノロジーの意外な処方箋】
昆虫と退屈なことについて(9月16日)
気になるトレンド用語
気になるトレンド用語
はてなブックマークが変わる!そもそもブラウザのお気に入りと何が違うの?(10月8日)
e-Japan 先端テクノロジー解説
e-Japan 先端テクノロジー解説
行政サービスのマルチチャネル化について(10月8日)
ウチのサイトを SEO
ウチのサイトを SEO
ちゃんと title つけていますか?(10月8日)
百式のネットビジネス研究
百式のネットビジネス研究
Blog 記事の編集を読者に任せることができる「gooseGrade」(10月8日)
「IT の耳」
「IT の耳」
【書評】ニコ動から RMT まで〜『人はなぜ形のないものを買うのか―仮想世界のビジネスモデル』(10月7日)
DevX
DevX
アジャイルソフトウェアプロジェクトを管理する(10月7日)
エンジニア転職ノウハウ開発室
エンジニア転職ノウハウ開発室
SEって、デジタル製品は判官びいきで選ぶよね?(10月7日)
アイレップの SEM フロンティア
アイレップの SEM フロンティア
フル CSS でサイトを構築する SEO のメリット(10月7日)
モバイルSEO@フラクタリスト
モバイルSEO@フラクタリスト
応用的な SEO 施策(3)(10月6日)
サーチからはじまるインタラクティブエージェンシー
サーチからはじまるインタラクティブエージェンシー
DB マーケティングと Web マーケティング 〜ビールとオムツの伝説から〜(10月6日)
海外のインターネットコムアメリカ韓国ドイツトルコ
Copyright 2008 Jupitermedia Corporation All Rights Reserved.http://www.internet.com/