入れ子になったデータ Web コントロール (C#)
このチュートリアルでは、別の Repeater 内で入れ子になった Repeater を使用する方法について説明します。 例では、内側の Repeater を宣言とプログラムの両方で設定する方法を示します。
はじめに
テンプレートには、静的 HTML とデータバインド構文に加えて、Web コントロールとユーザー コントロールを含めることもできます。 これらの Web コントロールには、宣言型のデータバインド構文を使用してプロパティを割り当てるか、またはプログラムを使用して、サーバー側の適切なイベント ハンドラー内でアクセスすることもできます。
コントロールをテンプレートに埋め込むと、外観やユーザー エクスペリエンスをカスタマイズして改善することができます。 たとえば、チュートリアル「GridView コントロールで TemplateFields を使用する 」では、従業員の入社日を表示するために、TemplateField にカレンダー コントロールを追加して GridView の表示をカスタマイズする方法について説明しました。また、チュートリアル「編集および挿入インターフェイスに検証コントロールを追加する」および「データ変更インターフェイスをカスタマイズする」では、検証コントロール、TextBoxes、DropDownLists、その他の Web コントロールを追加して、編集および挿入インターフェイスをカスタマイズする方法について説明しました。
テンプレートには、その他のデータ Web コントロールを含めることもできます。 つまり、テンプレート内に別の DataList (または Repeater、GridView、DetailsView など) を含む DataList を含めることができます。 このようなインターフェイスの課題は、適切なデータを内側のデータ Web コントロールにバインドすることです。 ObjectDataSource を使用した宣言型オプションからプログラムのオプションまで、いくつかの異なる方法を使用できます。
このチュートリアルでは、別の Repeater 内で入れ子になった Repeater を使用する方法について説明します。 外側の Repeater には、データベース内の各カテゴリの項目が含まれ、カテゴリの名前と説明が表示されます。 各カテゴリ項目の内側の Repeater は、そのカテゴリに属する各製品の説明を箇条書きで表示します (図 1 を参照)。 例では、内側の Repeater を宣言とプログラムの両方で設定する方法を示します。
図 1: 各カテゴリとその製品が一覧表示されています (クリックすると、フルサイズの画像が表示されます)
手順 1: カテゴリの一覧を作成する
入れ子になったデータ Web コントロールを使用するページを構築する場合、内側の入れ子になったコントロールを気にすることなく、最も外側のデータ Web コントロールの設計、作成、テストを最初に行うのが有用であることがわかっています。 このため、まず、各カテゴリの名前と説明を一覧表示するページに Repeater を追加するために必要な手順について説明します。
まず、DataListRepeaterBasics
フォルダーの NestedControls.aspx
ページを開き、ページに Repeater を追加し、その ID
属性を CategoryList
に設定します。 Repeater のスマート タグから、CategoriesDataSource
という名前の新しい ObjectDataSource の作成を選択します。
図 2: 新しい New ObjectDataSource に CategoriesDataSource
という名前を付ける (クリックするとフルサイズの画像が表示されます)
データを CategoriesBLL
クラスの GetCategories
メソッドからプルするように ObjectDataSource を構成します。
図 3: CategoriesBLL
クラスの GetCategories
メソッドを使用するように ObjectDataSource を構成する (クリックするとフルサイズの画像が表示されます)
Repeater のテンプレート コンテンツを指定するには、ソース ビューに移動し、宣言構文を手動で入力します。 <h4>
要素内のカテゴリの名前と、段落要素 (<p>
) 内の説明を表示する ItemTemplate
を追加します。 さらに、各カテゴリを水平線 (<hr>
) で区切りましょう。 これらの変更を加えると、ページに、次のような Repeater と ObjectDataSource の宣言構文が含まれます。
<asp:Repeater ID="CategoryList" DataSourceID="CategoriesDataSource"
EnableViewState="False" runat="server">
<ItemTemplate>
<h4><%# Eval("CategoryName") %></h4>
<p><%# Eval("Description") %></p>
</ItemTemplate>
<SeparatorTemplate>
<hr />
</SeparatorTemplate>
</asp:Repeater>
<asp:ObjectDataSource ID="CategoriesDataSource" runat="server"
OldValuesParameterFormatString="original_{0}"
SelectMethod="GetCategories" TypeName="CategoriesBLL">
</asp:ObjectDataSource>
図 4 は、これまでの進行状況をブラウザーで表示した場合を示しています。
図 4: カテゴリごとに水平線で区切られ、各カテゴリの名前と説明が一覧表示されている (クリックするとフルサイズの画像が表示されます)
手順 2: 入れ子になった製品 Repeater を追加する
カテゴリの一覧が完成したら、次の作業として、適切なカテゴリに属する製品の情報を表示する CategoryList
の ItemTemplate
に Repeater を追加します。 この内側の Repeater のデータを取得するには、いくつかの方法があります。そのうちの 2 つについて後ほど説明します。 ここでは、CategoryList
Repeater の ItemTemplate
内に製品 Repeater を作成しましょう。 具体的には、製品 Repeater によって、各製品を、製品の名前と価格を含む各リスト項目として箇条書きで表示します。
この Repeater を作成するには、内側の Repeater の宣言構文とテンプレートを CategoryList
の ItemTemplate
に手動で入力する必要があります。 CategoryList
Repeater の ItemTemplate
に次のマークアップを追加します。
<asp:Repeater ID="ProductsByCategoryList" EnableViewState="False"
runat="server">
<HeaderTemplate>
<ul>
</HeaderTemplate>
<ItemTemplate>
<li><strong><%# Eval("ProductName") %></strong>
(<%# Eval("UnitPrice", "{0:C}") %>)</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
手順 3: カテゴリ固有の製品を ProductsByCategoryList Repeater にバインドする
この時点で、ブラウザーでページにアクセスすると、画面は図 4 のようになります。これは、データを Repeater にまだバインドしていないためです。 適切な製品レコードを取得して Repeater にバインドする方法はいくつかありますが、その中には他の方法よりも効率的なものもあります。 ここでの主な課題は、指定されたカテゴリに適した製品を取得することです。
内側の Repeater コントロールにバインドするデータには、CategoryList
Repeater の ItemTemplate
で ObjectDataSource を使用して宣言によってアクセスするか、プログラムで、ASP.NET ページの分離コード ページからアクセスすることができます。 同様に、このデータは、宣言によって (内側の Repeater の DataSourceID
プロパティまたは宣言型のデータバインド構文を使用)、またはプログラムで CategoryList
Repeater の ItemDataBound
イベント ハンドラーを参照し、プログラムでその DataSource
プロパティを設定し、その DataBind()
メソッドを呼び出すことによって、内側の Repeater にバインドできます。 では、これらの各アプローチを詳しく調べてみましょう。
ObjectDataSource コントロールと ItemDataBound
イベント ハンドラーを使用して宣言によってデータにアクセスする
このチュートリアル シリーズ全体で広範にわたって ObjectDataSource を使用しているため、この例のデータにアクセスするために ObjectDataSource を使い続けるのは最も自然な選択です。 ProductsBLL
クラスには GetProductsByCategoryID(categoryID)
メソッドがあります。これは、指定した categoryID
に属する製品に関する情報を返します。 そのため、ObjectDataSource を CategoryList
Repeater の ItemTemplate
に追加し、このクラスのメソッドからデータにアクセスするように構成できます。
残念ながら、Repeater ではそのテンプレートをデザイン ビューで編集できないため、この ObjectDataSource コントロールの宣言構文を手動で追加する必要があります。 次の構文は、この新しい ObjectDataSource (ProductsByCategoryDataSource
) を追加した後の CategoryList
Repeater の ItemTemplate
を示しています。
<h4><%# Eval("CategoryName") %></h4>
<p><%# Eval("Description") %></p>
<asp:Repeater ID="ProductsByCategoryList" EnableViewState="False"
DataSourceID="ProductsByCategoryDataSource" runat="server">
<HeaderTemplate>
<ul>
</HeaderTemplate>
<ItemTemplate>
<li><strong><%# Eval("ProductName") %></strong> -
sold as <%# Eval("QuantityPerUnit") %> at
<%# Eval("UnitPrice", "{0:C}") %></li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
<asp:ObjectDataSource ID="ProductsByCategoryDataSource" runat="server"
SelectMethod="GetProductsByCategoryID" TypeName="ProductsBLL">
<SelectParameters>
<asp:Parameter Name="CategoryID" Type="Int32" />
</SelectParameters>
</asp:ObjectDataSource>
ObjectDataSource によるアプローチを使用する場合、ProductsByCategoryList
Repeater の DataSourceID
プロパティを ObjectDataSource (ProductsByCategoryDataSource
) の ID
に設定する必要があります。 また、ObjectDataSource には、<asp:Parameter>
要素もあることに注意してください。これは、GetProductsByCategoryID(categoryID)
メソッドに渡される categoryID
値を指定します。 しかし、この値を指定するにはどうすればよいでしょうか? 次のように、データバインド構文を使用して <asp:Parameter>
要素の DefaultValue
プロパティを設定できるだけで済めば理想的です。
<asp:Parameter Name="CategoryID" Type="Int32"
DefaultValue='<%# Eval("CategoryID")' />
残念ながら、データバインド構文は、DataBinding
イベントがあるコントロールでのみ有効です。 Parameter
クラスにはこのようなイベントがないため、上記の構文は無効であり、ランタイム エラーが発生します。
この値を設定するには、CategoryList
Repeater の ItemDataBound
イベントのイベント ハンドラーを作成する必要があります。 ItemDataBound
イベントは、Repeater にバインドされた項目ごとに 1 回発生することを思い出してください。 したがって、このイベントが外側の Repeater に対して発生するたびに、現在の CategoryID
の値を ProductsByCategoryDataSource
ObjectDataSource の CategoryID
パラメーターに割り当てることができます。
次のコードを使用して、CategoryList
Repeater の ItemDataBound
イベントのイベント ハンドラーを作成します。
protected void CategoryList_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
if (e.Item.ItemType == ListItemType.AlternatingItem ||
e.Item.ItemType == ListItemType.Item)
{
// Reference the CategoriesRow object being bound to this RepeaterItem
Northwind.CategoriesRow category =
(Northwind.CategoriesRow)((System.Data.DataRowView)e.Item.DataItem).Row;
// Reference the ProductsByCategoryDataSource ObjectDataSource
ObjectDataSource ProductsByCategoryDataSource =
(ObjectDataSource)e.Item.FindControl("ProductsByCategoryDataSource");
// Set the CategoryID Parameter value
ProductsByCategoryDataSource.SelectParameters["CategoryID"].DefaultValue =
category.CategoryID.ToString();
}
}
このイベント ハンドラーでは、まず、ヘッダー、フッター、または区切り項目ではなくデータ項目を処理していることを確認します。 次に、現在の RepeaterItem
にバインドされたばかりの実際の CategoriesRow
インスタンスを参照します。 最後に、ItemTemplate
内の ObjectDataSource を参照し、その CategoryID
パラメーター値を現在の RepeaterItem
の CategoryID
に割り当てます。
このイベント ハンドラーにより、各 RepeaterItem
内の ProductsByCategoryList
Repeater が、RepeaterItem
のカテゴリ内の製品にバインドされます。 図 5 は、結果として生成された出力のスクリーンショットを示しています。
図 5: 外側の Repeater は各カテゴリを一覧表示し、内側の Repeater はそのカテゴリの製品を一覧表示します (クリックするとフルサイズの画像が表示されます)
プログラムでカテゴリ別の製品データにアクセスする
ObjectDataSource を使用して現在のカテゴリの製品を取得する代わりに、ASP.NET ページの分離コード クラス (または、App_Code
フォルダーまたは別のクラス ライブラリ プロジェクト内) に、CategoryID
で渡されるときに適切な製品セットを返すメソッドを作成できます。 ASP.NET ページの分離コード クラスにそのようなメソッドがあり、そのメソッドの名前が GetProductsInCategory(categoryID)
という名前であるとします。 このメソッドを配置すると、次の宣言構文を使用して、現在のカテゴリの製品を内側の Repeater にバインドできます。
<asp:Repeater runat="server" ID="ProductsByCategoryList" EnableViewState="False"
DataSource='<%# GetProductsInCategory((int)(Eval("CategoryID"))) %>'>
...
</asp:Repeater>
Repeater の DataSource
プロパティは、データバインド構文を使用して、そのデータが GetProductsInCategory(categoryID)
メソッドから取得されたことを示します。 Eval("CategoryID")
は型 Object
の値を返すため、それを GetProductsInCategory(categoryID)
メソッドに渡す前に、オブジェクトを Integer
にキャストします。 ここでデータバインド構文を使用してアクセスされる CategoryID
は、"外側" の Repeater (CategoryList
) の CategoryID
であり、Categories
テーブル内のレコードにバインドされていることに注意してください。 したがって、CategoryID
をデータベース NULL
値にはできないことがわかっているため、DBNull
を処理しているかどうかを確認せずに Eval
メソッドを無条件にキャストできます。
このアプローチでは、GetProductsInCategory(categoryID)
メソッドを作成し、指定された categoryID
が与えられている適切な製品セットを取得する必要があります。 これを行うには、ProductsBLL
クラスの GetProductsByCategoryID(categoryID)
メソッドによって返された ProductsDataTable
を返すだけです。 では、NestedControls.aspx
ページの分離コード クラスに GetProductsInCategory(categoryID)
メソッドを作成しましょう。 このためには、次のコードを使用します。
protected Northwind.ProductsDataTable GetProductsInCategory(int categoryID)
{
// Create an instance of the ProductsBLL class
ProductsBLL productAPI = new ProductsBLL();
// Return the products in the category
return productAPI.GetProductsByCategoryID(categoryID);
}
このメソッドは、ProductsBLL
メソッドのインスタンスを作成し、GetProductsByCategoryID(categoryID)
メソッドの結果を返すだけです。 このメソッドは Public
または Protected
とマークされている必要があることに注意してください。メソッドが Private
とマークされている場合、ASP.NET ページの宣言マークアップからはアクセスできません。
この新しい手法を使用するためにこれらの変更を行った後、少し時間を取って、ブラウザーでページを確認します。 出力は、ObjectDataSource と ItemDataBound
イベント ハンドラーによるアプローチを使用した場合の出力と同じです (図 5 に戻り、スクリーンショットを確認してください)。
Note
ASP.NET ページの分離コード クラスに GetProductsInCategory(categoryID)
メソッドを作成するのは、面倒な作業のように思えるかもしれません。 結局のところ、このメソッドは単に ProductsBLL
クラスのインスタンスを作成し、その GetProductsByCategoryID(categoryID)
メソッドの結果を返すだけです。 DataSource='<%# ProductsBLL.GetProductsByCategoryID((int)(Eval("CategoryID"))) %>'
のように、このメソッドを内側の Repeater のデータバインド構文から直接呼び出さないのはなぜでしょうか? この構文は、ProductsBLL
クラスの現在の実装では機能しませんが (GetProductsByCategoryID(categoryID)
メソッドはインスタンス メソッドであるため)、ProductsBLL
を変更して静的な GetProductsByCategoryID(categoryID)
メソッドを含めるか、クラスに、ProductsBLL
クラスの新しいインスタンスを返す静的な Instance()
メソッドを含めることができます。
このような変更を行うと、ASP.NET ページの分離コード クラスの GetProductsInCategory(categoryID)
メソッドが不要になりますが、分離コード クラス メソッドを使用すると、取得したデータをより柔軟に操作できます。これについては、後ほど説明します。
一度にすべての製品情報を取得する
ここで調べた 2 つのわかりやすい手法では、ProductsBLL
クラスの GetProductsByCategoryID(categoryID)
メソッドを呼び出して、現在のカテゴリの製品を取得します (最初のアプローチでは ObjectDataSource を介して取得し、2 番目のアプローチでは分離コード クラスの GetProductsInCategory(categoryID)
メソッドを介して取得しました)。 このメソッドが呼び出されるたびに、ビジネス ロジック レイヤーによってデータ アクセス レイヤーが呼び出されます。データ アクセス レイヤーは SQL ステートメントでデータベースにクエリを実行し、Products
テーブルから、CategoryID
フィールドが指定された入力パラメーターと一致する行を返します。
システムに N 個のカテゴリがあるとすると、このアプローチでは、N + 1 回データベース呼び出しを行います (データベース クエリを 1 回実行してすべてのカテゴリを取得した後、N 回の呼び出しを行って各カテゴリに固有の製品を取得します)。 ただし、必要なすべてのデータを 2 回のデータベース呼び出しだけで取得できます (1 回の呼び出しですべてのカテゴリを取得し、もう 1 回の呼び出しですべての製品を取得します)。 すべての製品を取得した後、それらの製品をフィルター処理して、現在の CategoryID
に一致する製品のみがそのカテゴリの内側の Repeater にバインドされるようにすることができます。
この機能を提供するには、ASP.NET ページの分離コード クラスの GetProductsInCategory(categoryID)
メソッドを多少変更するだけで済みます。 ProductsBLL
クラスの GetProductsByCategoryID(categoryID)
メソッドの結果を無条件に返す代わりに、まず、"すべて" の製品にアクセスし (まだアクセスしていない場合)、渡された CategoryID
に基づいてフィルター処理された製品のビューのみを返すことができます。
private Northwind.ProductsDataTable allProducts = null;
protected Northwind.ProductsDataTable GetProductsInCategory(int categoryID)
{
// First, see if we've yet to have accessed all of the product information
if (allProducts == null)
{
ProductsBLL productAPI = new ProductsBLL();
allProducts = productAPI.GetProducts();
}
// Return the filtered view
allProducts.DefaultView.RowFilter = "CategoryID = " + categoryID;
return allProducts;
}
ページレベル変数 allProducts
が追加されていることに注意してください。 この変数は、すべての製品に関する情報を保持し、GetProductsInCategory(categoryID)
メソッドが最初に呼び出されたときに設定されます。 allProducts
オブジェクトが作成され、設定されたことを確認した後、CategoryID
が指定された CategoryID
と一致する行にのみアクセスできるように、メソッドによって DataTable の結果がフィルター処理されます。 このアプローチにより、データベースにアクセスする回数が N + 1 回から 2 回に減少します。
この機能強化により、ページのレンダリングされたマークアップに変更は加えられません。また、他のアプローチよりも返されるレコードが少なくなります。 データベースへの呼び出し回数が減少するだけです。
Note
データベースへのアクセス回数を削減すればパフォーマンスが確実に向上すると直感的に考える人もいるかもしれません。 ただし、そうではない場合もあります。 たとえば、CategoryID
が NULL
の製品が多数ある場合、GetProducts
メソッドを呼び出すと、表示されない多数の製品が返されます。 さらに、カテゴリのサブセットのみを表示する場合 (ページングを実装している場合に当てはまる可能性があります)、すべての製品を返しても無駄になる可能性があります。
通例どおり、2 つの手法のパフォーマンス分析を行う場合、唯一の確実な対策は、アプリケーションの一般的なケース シナリオに合わせて調整されたテストを実行することです。
まとめ
このチュートリアルでは、1 つのデータ Web コントロールを別のデータ Web コントロール内にネストする方法を説明しました。具体的には、外側の Repeater で各カテゴリのアイテムを表示し、内側の Repeater で各カテゴリの製品を箇条書きで一覧表示する方法を調べました。 入れ子になったユーザー インターフェイスを構築する際の主な課題は、適切なデータにアクセスして内側のデータ Web コントロールにバインドすることにあります。 さまざまな手法を使用できますが、このチュートリアルでは、そのうちの 2 つを調べました。 最初に調べたアプローチでは、外側のデータ Web コントロールの ItemTemplate
で ObjectDataSource を使用し、それを、DataSourceID
プロパティを介して内側のデータ Web コントロールにバインドしました。 2 番目の手法では、ASP.NET ページの分離コード クラスでメソッドを使用してデータにアクセスしました。 その後、このメソッドは、データバインド構文を使用して、内側のデータ Web コントロールの DataSource
プロパティにバインドできます。
このチュートリアルで調べた入れ子になったユーザー インターフェイスでは、Repeater 内に入れ子になった Repeater を使用しましたが、これらの手法を他のデータ Web コントロールに拡張できます。 たとえば、GridView 内に Repeater を入れ子にしたり、DataList 内に GridView を入れ子にしたりすることができます。
プログラミングに満足!
著者について
Scott Mitchell は、7 冊の ASP/ASP.NET 書籍の著者であり、4GuysFromRolla.com の創設者です。1998 年から Microsoft の Web テクノロジに携わっています。 Scott は、独立したコンサルタント、トレーナー、ライターとして働いています。 彼の最新の本は サムズは24時間で2.0 ASP.NET 自分自身を教えています。 にアクセスするか、ブログを使用して にアクセスmitchell@4GuysFromRolla.comできます。これは でhttp://ScottOnWriting.NET見つけることができます。
特別な感謝
このチュートリアル シリーズは、多くの役に立つ校閲者によってレビューされました。 このチュートリアルのリード レビュー担当者は、Zack Jones と Liz Shulok でした。 今後の MSDN の記事を確認することに関心がありますか? その場合は、 にmitchell@4GuysFromRolla.com行をドロップしてください。