Share via


入れ子になったデータ Web コントロール (C#)

作成者: Scott Mitchell

PDF のダウンロード

このチュートリアルでは、別の 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 を宣言とプログラムの両方で設定する方法を示します。

Each Category, Along with its Products, are Listed

図 1: 各カテゴリとその製品が一覧表示されています (クリックすると、フルサイズの画像が表示されます)

手順 1: カテゴリの一覧を作成する

入れ子になったデータ Web コントロールを使用するページを構築する場合、内側の入れ子になったコントロールを気にすることなく、最も外側のデータ Web コントロールの設計、作成、テストを最初に行うのが有用であることがわかっています。 このため、まず、各カテゴリの名前と説明を一覧表示するページに Repeater を追加するために必要な手順について説明します。

まず、DataListRepeaterBasics フォルダーの NestedControls.aspx ページを開き、ページに Repeater を追加し、その ID 属性を CategoryList に設定します。 Repeater のスマート タグから、CategoriesDataSource という名前の新しい ObjectDataSource の作成を選択します。

Name the New ObjectDataSource CategoriesDataSource

図 2: 新しい New ObjectDataSource に CategoriesDataSource という名前を付ける (クリックするとフルサイズの画像が表示されます)

データを CategoriesBLL クラスの GetCategories メソッドからプルするように ObjectDataSource を構成します。

Configure the ObjectDataSource to Use the CategoriesBLL Class s GetCategories Method

図 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 は、これまでの進行状況をブラウザーで表示した場合を示しています。

Each Category s Name and Description is Listed, Separated by a Horizontal Rule

図 4: カテゴリごとに水平線で区切られ、各カテゴリの名前と説明が一覧表示されている (クリックするとフルサイズの画像が表示されます)

手順 2: 入れ子になった製品 Repeater を追加する

カテゴリの一覧が完成したら、次の作業として、適切なカテゴリに属する製品の情報を表示する CategoryListItemTemplate に Repeater を追加します。 この内側の Repeater のデータを取得するには、いくつかの方法があります。そのうちの 2 つについて後ほど説明します。 ここでは、CategoryList Repeater の ItemTemplate 内に製品 Repeater を作成しましょう。 具体的には、製品 Repeater によって、各製品を、製品の名前と価格を含む各リスト項目として箇条書きで表示します。

この Repeater を作成するには、内側の Repeater の宣言構文とテンプレートを CategoryListItemTemplate に手動で入力する必要があります。 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 パラメーター値を現在の RepeaterItemCategoryID に割り当てます。

このイベント ハンドラーにより、各 RepeaterItem 内の ProductsByCategoryList Repeater が、RepeaterItem のカテゴリ内の製品にバインドされます。 図 5 は、結果として生成された出力のスクリーンショットを示しています。

The Outer Repeater Lists Each Category; the Inner One Lists the Products for that Category

図 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

データベースへのアクセス回数を削減すればパフォーマンスが確実に向上すると直感的に考える人もいるかもしれません。 ただし、そうではない場合もあります。 たとえば、CategoryIDNULL の製品が多数ある場合、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行をドロップしてください。