ASP.NETでのデータベース検索ページの作成に役立つ13のヒントはじめにデータベースの検索画面とナビゲーション画面は、アプリケーションの他の部分に比べると、最初はとても簡単そうに見えます。しかし、すべてのユーザーの要望や要求に応えようとすると思いのほか時間がかかります。 この記事では、ASP.NET 2.0、SQL Server 2005、C# 2.0を使って、検索用のWebページを作成します。検索と結果のページでは、オプションの検索条件を表示し、結果セットのカスタムページングを行います。このソリューションでは、ランキング番号を生成するSQL 2005の新しい言語機能を利用して、カスタムページングとの関連付けを行います。また、.NETジェネリックの新機能を利用して、ストアドプロシージャの結果をカスタムコレクションに出力します。ミヤギ師匠が練習をとおしてダニエルに技を教えたように、この記事の例では、ファクトリ作成パターンなど、.NETジェネリックを使ったいくつかの一般的なデザインパターンを示します。この記事では、データベースのWebページを作成する上での一般的な方法論にも触れます。 最終的な完成イメージさて、本題に入りましょう。この記事では、検索および結果/ナビゲーションの画面を作成します(図1、2、3を参照)。 これから説明する13のヒントを通じて、最終的に次の状態を実現します。
最後に、付録として、.NETジェネリックを利用し、匿名メソッドでソートを処理するコードを示します。 説明は少なく、コードは多く私は、この記事を執筆するに際して、新年の誓いを立てました(この記事を執筆している時点で、2006年がまだ数日残っています)。それは、「説明は短く、コードは長く」です。私が.NETについて学びだして数年経ちますが、非常に参考になった書籍や記事は、有意義なコードサンプルが記載されているものでした。そこで、どのヒントでも、余計なおしゃべりはできるだけ控えて、具体的なコードを紹介するようにします。 ヒント1: 要件の明確化例えばブラウザベースの電子メールWebページなど、任意の列でソートでき、Next/Previousリンクまたはページ番号のリンクでページ間をナビゲートするWebサイトを利用したことはありませんか。たまに使うのであれば、このような一般的なナビゲーション設計でも十分です。 しかし、「R」という文字で始まる特定の行や項目を表示したい場合はどうでしょうか。多分、降順で列をソートし、目的の項目に達するまでNextリンクをクリックするか、目的の項目が何ページ目にあるかを推測することになるでしょう。これはまるで、値段当てを競うテレビ番組のようです。出場者が「500」と言うと司会者が「もっと高い」と言い、出場者が「550」と言うと司会者が「もっと安い」と言います。出場者が本当の値段を言い当てるまで(またはブザーが鳴るまで)、値の増減が繰り返されます。 このことは、この記事のサンプルプロジェクトのテーマであるナビゲーションにそのまま当てはまります。よくないデザインパターンを言い表すときに「空振り」という表現を使いますが、ユーザーが余計なナビゲーション操作をしなければならないWebページも、そうしたよくないデザインの一例です。もちろん、外見的に優れたデザインのWebページはユーザーを魅了しますが、リピーター率が高いのはナビゲーション部分がきちんと整備されているWebページです。 この記事では、図1、2、3のようなWebページを作成する方法について説明します。このソリューションには、次の機能が含まれます。
例えば、「City」列を基準として文字「A」が先頭に来る順序で顧客を表示しているときに、文字「M」で始まる値を「City」列に含んでいる最初の顧客データにクイックジャンプできる。「City」列の値が「M」で始まる顧客データの数が
MAXROWSより少ない場合は、「M」で始まるすべての行と、それ以降の行がMAXROWSに達するまでページに表示される。この実装について、もう少し説明しておきます。おそらく、ここで挙げた機能のほとんどは最新バージョンのASP.NETないしサードパーティのWebグリッドに含まれているのではないかと思った人もいることでしょう。部分的にはそうなのですが、すべての要件を満たすためには、多少の努力が必要です。今回のソリューションでは、SQL 2005のストアドプロシージャ、ASP.NET 2.0のGridView、そしてC# 2.0のコードを使ってこれを作成します。図4は、このWebサイトの開発ソリューション全体を示しています。全体的なプランは次のとおりです。
図4 Webサイトの開発ソリューション ![]() ヒント2: セッション変数およびストアドプロシージャパラメータの定義表1および表2は、検索ページのセッション変数とストアドプロシージャパラメータをそれぞれ示しています。 表1 ページのセッション変数
表2 ストアドプロシージャパラメータ
結果セットページには決まった行数しか表示されないため、現在の先頭行インデックスと末尾行インデックスをロジックで保持して、前後のページへのナビゲーションに対処する必要があります。ここで、簡単なシナリオを考えてみましょう。ユーザーが最初に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 最初の実行時、ストアドプロシージャパラメータの ユーザーが次のページにナビゲートする場合、分離コードのロジックでは、ストアドプロシージャの TopボタンとBottomボタンについては、分離コードによって、 最後に、特定の文字(例えば「R」)に直接ジャンプする場合は、ストアドプロシージャの 変数の処理については、いくつか注意すべき点があります。
ヒント3: 【重要ポイント】T-SQL 2005の新しいランキング関数 リスト1は、結果セットを返す完全なストアドプロシージャです(話の結末を先に知りたいタイプの人は、まずリスト1を見てください)。次のコードは、ヒント2の 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の新しい 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 次のヒントでは、特定のページ/行インデックスの範囲の結果だけを返す、ランキングのフィルタリングについて説明します。 ヒント4: オプションの検索パラメータおよびページ/行インデックス範囲の処理 ストアドプロシージャには検索条件用のさまざまなパラメータが含まれていますが、一度に使用するパラメータの数は限られています。例えば、エンドユーザーが検索する情報として考えられるのは名前、住所、または電話番号のみです。ストアドプロシージャは、ユーザーが指定した入力条件についてのみクエリを実行します。開発者によっては、動的SQL文を作成することによってこれを処理します。T-SQLの また、このストアドプロシージャでは、 WHERE LastName LIKE ’%’ + COALESCE(@LastName,LastName)+ ’%’ AND Address LIKE ’%’ + COALESCE(@Address,Address) + ’%’ AND ... さらに、このストアドプロシージャでは、数値のページングの場合は行の範囲でフィルタを行い、クイックナビゲーションの場合は特定の文字または値で始まる現在のソート列に基づいてフィルタを行う必要があります。ここまでくるとストアドプロシージャがやや複雑になってきます。この処理を実現するには、 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 SQL 2005より前のバージョンでは、中間結果に対してさらにSQL文を記述する場合、開発者は派生テーブル、テーブル変数、一時テーブルなどを使用していました。SQL 2005では、共通テーブル式(CTE)が導入されました。CTEは、基本的には、後に続く1つのステートメントで参照できる動的なビューです。 CTEは、 WITH CustListTemp AS (SELECT CustomerID, LastName, FirstName, Address, City, State, Zip, ROW_NUMBER() OVER (ORDER BY .. ) このCTEをクエリするコードを作成することができます。CTEを参照できるのは、次に続くSQL文だけです。CTEに対するクエリは、実際にはサブクエリを実行して、行の 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の リスト2には、基本DALを継承するDAL(daCustomer)が含まれています。メソッド
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を呼び出すことができます。コードの重要なポイントを次に示します。
ヒント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イベントに対応します。これらのメソッドは、ナビゲーションの方向に応じて 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イベントを利用します。このイベントは、ユーザーが選択した列見出しの 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: グリッドの結果セット情報の表示 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(); } ... 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の // set the DataKeyNames property // to uniquely determine a selected row this.grdResults.DataKeyNames = new string[] { "CustomerID" }; 次に、 // 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の 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テーブルに存在します。 これは、問題への取り組みのプロセスを繰り返す良い練習です。この練習は、可変個数の選択を処理する再利用可能な手法も示しています。
上のタスクは、ユーザーインターフェイスで始まり、データベースで終わっているため、その順序で変更を進めようと思いがちです。しかし、バックエンドから始めて、フロントエンドに向かって作業する方が、わずかながら効率的です。設計は外側から内側に向かって行い、開発は内側から外側に向かって行います。 最初に、 SQL Server 2005より前のバージョンでは、開発者はしばしば、 SQL Server 2005の新しいXML機能が反映されているのは、XQueryと呼ばれる一般的な機能と、 リスト4には、 リスト4内のコードでは、 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に挿入します。次に、 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の カスタムコレクションに「均等な時間」を与えるために、.NETジェネリックを使った基本メソッドをデータアクセス層に追加しました。メソッド 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ジェネリックを使うことができます。先頭のパラメータとして public void ReadIntoCollection<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); 今回は、.NET DataAdapterの oSqlConn.Open(); SqlDataReader oDR = oCmd.ExecuteReader(); 次の一連のステップでは、リーダーを通じて まずこのコードでは、リーダーオブジェクトを使ってループを設定し、クラスタイプのインスタンスを作成します。パラメータが.NETジェネリックを利用してクラスパラメータを定義しているため、コードではTプレースホルダを使ってクラスのインスタンスを指定できます。このコードをデバッガで調べると、
while(oDR.Read()) {
T oItem = (T)Activator.CreateInstance(
oCollectionType);
次に、コードは、クラスのすべてのプロパティを検出するのに.NETリフレクションを多少使う必要があります。
// get all the properties of the class
PropertyInfo[] oCollectionProps = (
(Type) oItem.GetType()).GetProperties();
クラスのプロパティの配列ができたため( 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ジェネリックの 例えば、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コードに十分対抗できます。第一に、新しい 第二に、C# 2.0では匿名メソッドを使ってデリゲートの代わりにロジックを配置できます。つまり、個別のカスタムメソッドを作成するのではなく、本来ならデリゲートが生じる位置に、インラインでコードを設定できます。いくつかのコードサンプルを紹介しましょう。DataSetの代わりに、CustomerRecという名前のカスタムリストの例を使用します。これは、 コードでは、リストの // 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); }); 結局のところ、開発者は多少コードを記述する必要はありますが、新しい この記事で紹介したソースコード全体は、私のWebサイトに掲載されています。詳細については、私のブログを参照してください。 最後に記事、論文、コードなどを投稿した後に、よいアイディアを思いついたことはありませんか。私は、後になっていろいろと思いつくのが得意です。幸いなことに、そういう場合はブログが重宝します。私がこれまで発表した記事の補足ヒントや注意に関しては、私のブログを参照してください。場合によっては、お楽しみが見つかるかもしれません。 著者紹介Kevin S. Goff(Kevin S. Goff)
.NET、Visual FoxPro、SQL Server、Crystal Reportsによる独自のWebソリューション/デスクトップソリューションを提供するコンサルティンググループ「Common Ground Solutions」の創業者兼主任コンサルタント。ソフトウェアアプリケーションの開発経験は17年に及ぶ。米農務省からシステムオートメーション関連の賞をいくつか受賞。また、6桁の投資見返りを実現するソリューションを開発したことによりFortune 500企業から特別表彰を受ける。保険、会計、環境衛生、不動産、出版、広告、製造、金融、日用品、貿易振興など多様な業界に携わった経験を持ち、さまざまな形態で独自のトレーニングも行っている。
|