このチュートリアルでは、別の Repeater 内に入れ子になっている Repeater を使用する方法を探ります。 この例では、内部 Repeater を宣言とプログラムの両方で設定する方法を示します。
イントロダクション
静的 HTML およびデータ バインド構文に加えて、テンプレートには Web コントロールとユーザー コントロールを含めることもできます。 これらの Web コントロールには、宣言型のデータ バインド構文を使用してプロパティを割り当てたり、適切なサーバー側のイベント ハンドラーでプログラムからアクセスしたりできます。
テンプレート内にコントロールを埋め込むことで、外観とユーザー エクスペリエンスをカスタマイズして改善できます。 たとえば、GridView コントロールのチュートリアルの TemplateFields の使用に関するチュートリアルでは、従業員の雇用日を表示するために TemplateField に Calendar コントロールを追加して GridView の表示をカスタマイズする方法を説明しました。「編集および挿入インターフェイスへの検証コントロールの追加」および「データ変更インターフェイスのカスタマイズ」チュートリアルでは、検証コントロール、TextBoxes、DropDownLists、およびその他の Web コントロールを追加して、編集インターフェイスと挿入インターフェイスをカスタマイズする方法について説明しました。
テンプレートには、他のデータ Web コントロールを含めることもできます。 つまり、テンプレート内に別の DataList (Repeater、GridView、DetailsView など) を含む DataList を作成できます。 このようなインターフェイスの課題は、適切なデータを内部データ Web コントロールにバインドすることです。 ObjectDataSource を使用した宣言型オプションからプログラムによるオプションまで、さまざまな方法があります。
このチュートリアルでは、別の Repeater 内に入れ子になっている Repeater を使用する方法を探ります。 外側の Repeater には、データベース内の各カテゴリの項目が含まれており、カテゴリの名前と説明が表示されます。 各カテゴリアイテムの内部リピータには、そのカテゴリに属する各製品の情報が箇条書きリストに表示されます (図 1 を参照)。 この例では、宣言型とプログラム型の両方で内部 Repeater を設定する方法を示します。
図 1: 各カテゴリとその製品が一覧表示されます (フルサイズの画像を表示する をクリックします)。
手順 1: カテゴリ一覧の作成
入れ子になったデータ Web コントロールを使用するページを構築する場合、入れ子になった内部コントロールを気にすることなく、最も外側のデータ Web コントロールを最初に設計、作成、テストすると便利です。 そのため、まず、各カテゴリの名前と説明を一覧表示するページに Repeater を追加するために必要な手順について説明します。
まず、NestedControls.aspx フォルダーのDataListRepeaterBasics ページを開き、Repeater コントロールをページに追加し、そのIDプロパティを CategoryList に設定します。 Repeater のスマート タグから、 CategoriesDataSourceという名前の新しい ObjectDataSource を作成することを選択します。
図 2: 新しい ObjectDataSource CategoriesDataSource に名前を付ける (フルサイズの画像を表示する をクリックします)
CategoriesBLL クラスの GetCategories メソッドからデータをプルするように ObjectDataSource を構成します。
図 3: CategoriesBLL クラスの GetCategories メソッドを使用するように ObjectDataSource を構成する (フルサイズの画像を表示する をクリックします)。
Repeater のテンプレート コンテンツを指定するには、ソース ビューに移動し、宣言構文を手動で入力する必要があります。
ItemTemplate要素のカテゴリ名と段落要素 (<h4>) のカテゴリの説明を表示する<p>を追加します。 さらに、各カテゴリを水平ルール (<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 のデータを取得する方法はいくつかあります。そのうちの 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: Category-Specific 製品を ProductsByCategoryList Repeater にバインドする
この時点でブラウザーからページにアクセスすると、画面は図 4 と同じように表示されます。これは、データを Repeater にバインドしていないためです。 適切な製品レコードを取得して Repeater にバインドする方法はいくつかあり、いくつかの方法は他の方法よりも効率的です。 ここでの主な課題は、指定されたカテゴリに適した製品を取得することです。
内部のRepeaterコントロールにバインドするデータは、CategoryListRepeaterItemTemplate内にあるObjectDataSourceを使用して宣言的にアクセスするか、ASP.NETページのコードビハインドからプログラムによってアクセスできます。 同様に、このデータは、内部 Repeater の DataSourceID プロパティを使用するか、宣言型データ バインド構文を使用するか、 CategoryList Repeater の ItemDataBound イベント ハンドラーで内部 Repeater を参照し、プログラムで DataSource プロパティを設定し、その DataBind() メソッドを呼び出すことによって、内部 Repeater にバインドできます。 これらの各アプローチを調べてみましょう。
ObjectDataSource コントロールとItemDataBoundEvent ハンドラーを使用して宣言によってデータにアクセスする
このチュートリアル シリーズ全体で ObjectDataSource を広範囲に使用したので、この例のデータにアクセスするための最も自然な選択肢は、ObjectDataSource を使用することです。
ProductsBLL クラスには、指定したGetProductsByCategoryID(categoryID)に属する製品に関する情報を返すcategoryID メソッドがあります。 そのため、 CategoryList Repeater の ItemTemplate に ObjectDataSource を追加し、このクラスのメソッドからデータにアクセスするように構成できます。
残念ながら、Repeater ではデザイン ビューでテンプレートを編集できないため、この ObjectDataSource コントロールの宣言構文を手動で追加する必要があります。 次の構文は、新しい ObjectDataSource (CategoryList) を追加後にItemTemplate Repeater ProductsByCategoryDataSourceを示しています。
<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 (ID) のProductsByCategoryDataSourceに設定する必要があります。 また、ObjectDataSource には、<asp:Parameter> メソッドに渡されるcategoryID値を指定するGetProductsByCategoryID(categoryID)要素があることに注意してください。 しかし、この値を指定するにはどうすればよいですか? 理想的には、次のように、データ バインド構文を使用してDefaultValue要素の<asp:Parameter> プロパティを設定できます。
<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();
}
}
このイベント ハンドラーは、まず、ヘッダー、フッター、または区切り記号項目ではなく、データ項目を処理していることを確認します。 次に、現在のCategoriesRowにバインドされた実際のRepeaterItem インスタンスを参照します。 最後に、ItemTemplateで ObjectDataSource を参照し、そのCategoryIDパラメーター値を現在のCategoryIDのRepeaterItemに割り当てます。
このイベント ハンドラーを使用すると、各ProductsByCategoryListのRepeaterItem Repeater は、RepeaterItemのカテゴリ内の製品にバインドされます。 図 5 は、結果の出力のスクリーン ショットを示しています。
図 5: 外側のリピータは、各カテゴリを一覧表示します。[Inner One] には、そのカテゴリの製品が一覧表示されます (フルサイズの画像を表示するには、ここをクリックします)
プログラムによるカテゴリ データによる製品へのアクセス
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型の値を返すので、Integer メソッドに渡す前にオブジェクトをGetProductsInCategory(categoryID)にキャストします。 ここでデータ バインド構文を使用してアクセスするCategoryIDは、CategoryIDの Repeater () のCategoryListであり、Categories テーブル内のレコードにバインドされていることに注意してください。 したがって、CategoryIDをデータベースNULL値にすることはできないので、Evalを処理しているかどうかを確認せずにDBNullメソッドを盲目的にキャストできます。
この方法では、 GetProductsInCategory(categoryID) メソッドを作成し、指定された categoryIDを指定して適切な製品セットを取得する必要があります。 これを行うには、ProductsDataTable クラスの ProductsBLL メソッドによって返されるGetProductsByCategoryID(categoryID)を返すだけです。
GetProductsInCategory(categoryID) ページの分離コード クラスにNestedControls.aspx メソッドを作成してみましょう。 これを行うには、次のコードを使用します。
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 を参照してください)。
注
ASP.NET ページの分離コード クラスで GetProductsInCategory(categoryID) メソッドを作成すると、忙しいように見える場合があります。 結局のところ、このメソッドは単に ProductsBLL クラスのインスタンスを作成し、その GetProductsByCategoryID(categoryID) メソッドの結果を返します。
DataSource='<%# ProductsBLL.GetProductsByCategoryID((int)(Eval("CategoryID"))) %>'のように、内部 Repeater のデータバインド構文から直接このメソッドを呼び出さないのはなぜですか? この構文はProductsBLL クラスの現在の実装では機能しませんが (GetProductsByCategoryID(categoryID) メソッドはインスタンス メソッドであるため)、静的ProductsBLL メソッドを含むようにGetProductsByCategoryID(categoryID)を変更するか、クラスに静的Instance() メソッドを含めてProductsBLL クラスの新しいインスタンスを返すようにすることができます。
このような変更により、ASP.NET ページの分離コード クラスの GetProductsInCategory(categoryID) メソッドが不要になりますが、分離コード クラス メソッドを使用すると、取得したデータを柔軟に操作できます。これについては、後ほど説明します。
すべての製品情報を一度に取得する
私たちが検討した2つの前に述べた手法は、ProductsBLLクラスのGetProductsByCategoryID(categoryID)メソッドを呼び出すことによって、現在のカテゴリに属する製品を取得します(最初のアプローチはObjectDataSourceを介して行われ、2つ目はコードビハインドクラスのGetProductsInCategory(categoryID)メソッドを介して行われました)。 このメソッドが呼び出されるたびに、ビジネス ロジック層はデータ アクセス層を呼び出します。この層は、指定された入力パラメーターと一致する Products テーブルの行を返す SQL ステートメントを使用してデータベースにクエリを実行 CategoryID 。
システム内の N 個のカテゴリを指定すると、このアプローチでは、データベース 1 つのデータベース クエリに対する N + 1 回の呼び出しを行ってすべてのカテゴリを取得し、 次に N 個の呼び出しで各カテゴリに固有の製品を取得します。 ただし、2 つのデータベース呼び出しで必要なすべてのデータを取得し、1 つの呼び出しですべてのカテゴリを取得し、もう 1 つを呼び出してすべての製品を取得できます。 すべての製品を取得したら、それらの製品をフィルター処理して、現在の CategoryID に一致する製品のみがそのカテゴリの内部リピータにバインドされるようにすることができます。
この機能を提供するには、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 に減ります。
この機能強化では、ページのレンダリングされたマークアップに変更を加えることはなく、他の方法よりも少ないレコードが返されることはありません。 データベースへの呼び出しの数を減らすだけです。
注
データベース アクセスの数を減らすと、パフォーマンスが確実に向上するという直感的な理由が考えられます。 ただし、これは当てはまるとは言えない場合があります。 たとえば、 CategoryID が NULLされている製品が多数ある場合、 GetProducts メソッドの呼び出しでは、表示されない製品の数が返されます。 さらに、カテゴリのサブセットのみを表示している場合は、すべての製品を返すのが無駄になる可能性があります。これは、ページングを実装している場合に当てはまる可能性があります。
これまでと同様に、2 つの手法のパフォーマンスを分析する場合、唯一の確実な対策は、アプリケーションの一般的なケース シナリオに合わせて調整された制御されたテストを実行することです。
概要
このチュートリアルでは、1 つのデータ Web コントロールを別のデータ Web コントロール内に入れ子にする方法について説明しました。具体的には、外側の Repeater に各カテゴリの項目を表示し、内部 Repeater で箇条書きリストの各カテゴリの製品を一覧表示する方法を調べました。 入れ子になったユーザー インターフェイスを構築する場合の主な課題は、正しいデータにアクセスして内部データ Web コントロールにバインドすることです。 使用できるさまざまな手法があり、そのうちの 2 つをこのチュートリアルで調べました。 最初に調べたアプローチでは、外側のデータ Web コントロールの ItemTemplate で、 DataSourceID プロパティを介して内部データ Web コントロールにバインドされた ObjectDataSource を使用しました。 2 番目の手法では、ASP.NET ページの分離コード クラスのメソッドを使用してデータにアクセスしました。 このメソッドは、データ バインド構文を使用して、内部データ Web コントロールの DataSource プロパティにバインドできます。
このチュートリアルで調べた入れ子型ユーザーインターフェースでは、Repeater 内にさらに入れ子にした Repeaterを使いますが、これらの技法は他のデータ Web コントロールにも拡張できます。 Repeater を GridView 内に、あるいは GridView を DataList 内に入れ子にすることなどができます。
プログラミングに満足!
著者について
7 冊の ASP/ASP.NET 書籍の著者であり、4GuysFromRolla.com の創設者である Scott Mitchell は、1998 年から Microsoft Web テクノロジを使用しています。 Scott は、独立したコンサルタント、トレーナー、ライターとして働いています。 彼の最新の本は サムズ・ティーチ・セルフ ASP.NET 24時間で2.0です。 彼には mitchell@4GuysFromRolla.comで連絡できます。
特別な感謝
このチュートリアル シリーズは、多くの役に立つ校閲者によってレビューされました。 このチュートリアルのリード レビュー担当者は、Zack Jones と Liz Shulok でした。 今後の MSDN の記事を確認することに関心がありますか? その場合は、mitchell@4GuysFromRolla.comにメッセージを送ってください。