カスタム データベース駆動型サイト マップ プロバイダーを構築する (VB)

作成者: Scott Mitchell

PDF のダウンロード

ASP.NET 2.0 の既定のサイト マップ プロバイダーは、静的 XML ファイルからデータを取得します。 XML ベースのプロバイダーは、多くの中小規模の Web サイトに適していますが、大規模な Web アプリケーションでは、より動的なサイト マップが必要です。 このチュートリアルでは、ビジネス ロジック層からデータを取得するカスタム サイト マップ プロバイダーを構築します。ビジネス ロジック レイヤーは、データベースからデータを取得します。

はじめに

ASP.NET 2.0 のサイト マップ機能を使用すると、ページ開発者は、XML ファイルなどの永続的なメディアで Web アプリケーションのサイト マップを定義できます。 定義したサイト マップ データには、System.Web 名前空間SiteMap クラスを介して、または、さまざまなナビゲーション Web コントロール (SiteMapPath、Menu、TreeView コントロールなど) を使用してプログラムでアクセスできます。 サイト マップ システムではプロバイダー モデルが使用されるため、さまざまなサイト マップのシリアル化実装を作成して Web アプリケーションに組み込めます。 ASP.NET 2.0 に付属する既定のサイト マップ プロバイダーでは、サイト マップ構造が XML ファイルに保持されます。 マスター ページとサイト ナビゲーションのチュートリアルでは、この構造を含む Web.sitemap という名前のファイルを作成し、新しいチュートリアル セクションごとに XML を更新しました。

既定の XML ベースのサイト マップ プロバイダーは、サイト マップの構造が非常に静的である場合 (これらのチュートリアルなど) に適しています。 ただし、多くのシナリオでは、より動的なサイト マップが必要です。 図 1 に示すサイト マップについて考えてみましょう。各カテゴリと製品が、Web サイトの構造のセクションとして表示されています。 このサイト マップでは、ルート ノードに対応する Web ページにアクセスするとすべてのカテゴリが一覧表示される場合があります。一方、特定のカテゴリの Web ページにアクセスすると、そのカテゴリの製品が一覧表示され、特定の製品の Web ページを表示すると、その製品の詳細が表示されます。

サイト マップの構造を構成するカテゴリと製品

図 1: カテゴリと製品でサイト マップの構造が構成される (クリックするとフルサイズの画像が表示されます)

この、カテゴリベースおよび製品ベースの構造は、Web.sitemap ファイルにハードコーディングできますが、カテゴリまたは製品が追加、削除、または名前変更されるたびにファイルを更新する必要があります。 そのため、サイト マップの構造がデータベースから取得された場合、または、理想的にはアプリケーション アーキテクチャのビジネス ロジック層から取得された場合、サイト マップのメンテナンスは大幅に簡略化されます。 これにより、製品とカテゴリが追加、名前変更、または削除されると、サイト マップは自動的に更新され、これらの変更が反映されます。

ASP.NET 2.0 のサイト マップのシリアル化はプロバイダー モデルの上に構築されているため、データベースやアーキテクチャなどの代替データ ストアからデータを取得する独自のカスタム サイト マップ プロバイダーを作成できます。 このチュートリアルでは、BLL からデータを取得するカスタム プロバイダーを構築します。 では、始めましょう。

Note

このチュートリアルで作成するカスタム サイト マップ プロバイダーは、アプリケーションのアーキテクチャとデータ モデルと密接に結び付けられています。 Jeff Prosise による SQL Server でのサイト マップの格納に関する記事と、「待ち望まれていた SQL サイト マップ プロバイダー」の記事では、サイト マップ データを SQL Server に格納するための一般化されたアプローチについて述べています。

手順 1: カスタム サイト マップ プロバイダー Web ページを作成する

カスタム サイト マップ プロバイダーの作成を開始する前に、まず、このチュートリアルに必要な ASP.NET ページを追加しましょう。 まず、SiteMapProvider という名前の新しいフォルダーを追加します。 次に、次の ASP.NET ページをそのフォルダーに追加し、各ページを Site.master マスター ページに関連付けます。

  • Default.aspx
  • ProductsByCategory.aspx
  • ProductDetails.aspx

また、CustomProviders サブフォルダーを App_Code フォルダーに追加します。

サイト マップ プロバイダー関連のチュートリアルの ASP.NET ページを追加する

図 2: サイト マップ プロバイダー関連のチュートリアルの ASP.NET ページを追加する

このセクションのチュートリアルは 1 つだけなので、Default.aspx でセクションのチュートリアルを一覧表示する必要はありません。 代わりに、Default.aspx で GridView コントロールにカテゴリが表示されます。 これについては、手順 2 で取り組みます。

次に、Default.aspx ページへの参照を含むように Web.sitemap を更新します。 具体的には、<siteMapNode> をキャッシュした後に次のマークアップを追加します。

<siteMapNode 
    title="Customizing the Site Map" url="~/SiteMapProvider/Default.aspx" 
    description="Learn how to create a custom provider that retrieves the site map 
                 from the Northwind database." />

Web.sitemap を更新した後、ブラウザーでチュートリアル Web サイトの表示を確認してみましょう。 左側のメニューに、唯一のサイト マップ プロバイダー チュートリアルの項目が含まれるようになりました。

サイト マップにサイト マップ プロバイダーチュートリアルのエントリが含まれるようになりました

図 3: サイト マップ プロバイダーのチュートリアルのエントリがサイト マップに含まれるようになった

このチュートリアルの主な焦点は、カスタム サイト マップ プロバイダーを作成し、そのプロバイダーを使用するように Web アプリケーションを構成する方法を説明することです。 具体的には、図 1 に示すように、ルート ノードと各カテゴリと製品のノードを含むサイト マップを返すプロバイダーを構築します。 一般に、サイト マップ内の各ノードで URL を指定できます。 今回のサイト マップの場合、ルート ノードの URL は ~/SiteMapProvider/Default.aspx です。これは、データベース内のすべてのカテゴリを一覧表示します。 サイト マップ内の各カテゴリ ノードには、指定された categoryID 内のすべての製品を一覧表示する、~/SiteMapProvider/ProductsByCategory.aspx?CategoryID=categoryID を指す URL が含まれます。 最後に、各製品のサイト マップ ノードは、特定の製品の詳細が表示される ~/SiteMapProvider/ProductDetails.aspx?ProductID=productID を指します。

まず、Default.aspxProductsByCategory.aspxProductDetails.aspx のページを作成する必要があります。 これらのページは、それぞれ手順 2、3、4 で完成します。 このチュートリアルの要旨はサイト マップ プロバイダーであり、過去のチュートリアルではこのような複数ページのマスター/詳細レポートの作成について説明しているので、手順 2 から 4 までは急ぎ足で進めます。 複数のページにまたがるマスター/詳細レポートの作成について簡単な復習が必要な場合は、「2 つのページでマスター/詳細をフィルター処理する」のチュートリアルをご覧ください。

手順 2: カテゴリの一覧を表示する

SiteMapProvider フォルダー内の Default.aspx ページを開き、ツールボックスからデザイナーに GridView をドラッグして、その IDCategories に設定します。 GridView のスマート タグから、CategoriesDataSource という名前の新しい ObjectDataSource にバインドし、CategoriesBLL クラスの GetCategories メソッドを使用してデータを取得するように構成します。 この GridView ではカテゴリが表示され、データ変更機能は提供されないため、[UPDATE]、[INSERT]、[DELETE] タブのドロップダウン リストを [(None)] に設定します。

GetCategories メソッドを使用してカテゴリを返すように ObjectDataSource を構成する

図 4: GetCategories メソッドを使ってカテゴリを返すように ObjectDataSource を構成する (クリックするとフルサイズの画像が表示されます)

[UPDATE]、[INSERT]、[DELETE] の各タブのドロップダウン リストを [(なし)] に設定する

図 5: [UPDATE]、[INSERT]、[DELETE] の各タブのドロップダウン リストを [(None)] に設定する (クリックするとフルサイズの画像が表示されます)

データ ソースの構成ウィザードが完了すると、Visual Studio によって、CategoryIDCategoryNameDescriptionNumberOfProductsBrochurePath の BoundField が追加されます。 GridView を編集して、CategoryNameDescription の BoundField のみが含まれるようにし、CategoryName BoundField の HeaderText プロパティを Category に更新します。

次に、HyperLinkField を追加し、左端のフィールドになるように配置します。 DataNavigateUrlFields プロパティを CategoryID に、DataNavigateUrlFormatString プロパティを ~/SiteMapProvider/ProductsByCategory.aspx?CategoryID={0} にそれぞれ設定します。 Text プロパティを [View Products] に設定します。

カテゴリ GridView に HyperLinkField を追加する

図 6: Categories GridView に HyperLinkField を追加する

ObjectDataSource を作成し、GridView のフィールドをカスタマイズした後、2 つのコントロールの宣言型マークアップは次のようになります。

<asp:GridView ID="Categories" runat="server" AutoGenerateColumns="False" 
    DataKeyNames="CategoryID" DataSourceID="CategoriesDataSource" 
    EnableViewState="False">
    <Columns>
        <asp:HyperLinkField DataNavigateUrlFields="CategoryID" 
            DataNavigateUrlFormatString=
                "~/SiteMapProvider/ProductsByCategory.aspx?CategoryID={0}"
            Text="View Products" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category" 
            SortExpression="CategoryName" />
        <asp:BoundField DataField="Description" HeaderText="Description" 
            SortExpression="Description" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="CategoriesDataSource" runat="server" 
    OldValuesParameterFormatString="original_{0}" SelectMethod="GetCategories" 
    TypeName="CategoriesBLL"></asp:ObjectDataSource>

図 7 は、ブラウザーで表示した場合の Default.aspx を示しています。 カテゴリの [View Products] リンクをクリックすると、手順 3 で作成する ProductsByCategory.aspx?CategoryID=categoryID に移動します。

各カテゴリは、[製品の表示] リンクと共に一覧表示されます

図 7: 各カテゴリが [View Products] リンクと共に一覧表示される (クリックするとフルサイズの画像が表示されます)

手順 3: 選択したカテゴリの製品を一覧表示する

ProductsByCategory.aspx ページを開き、GridView を追加して、ProductsByCategory という名前を付けます。 そのスマート タグから、GridView を ProductsByCategoryDataSource という名前の新しい ObjectDataSource にバインドします。 ProductsBLL クラスの GetProductsByCategoryID(categoryID) メソッドを使用するように ObjectDataSource を構成し、[UPDATE] タブ、[INSERT] タブ、[DELETE] タブでドロップダウン リストを [(None)] に設定します。

ProductsBLL クラスの GetProductsByCategoryID(categoryID) メソッドを使用する

図 8: ProductsBLL クラスの GetProductsByCategoryID(categoryID) メソッドを使う (クリックするとフルサイズの画像が表示されます)

データ ソースの構成ウィザードの最後の手順では、categoryID のパラメーター ソースの入力を求められます。 この情報はクエリ文字列フィールド CategoryID を介して渡されるので、図 9 に示すように、ドロップダウン リストから [QueryString] を選択し、[QueryStringField] テキストボックスに [CategoryID] を入力します。 [完了] をクリックして、ウィザードを完了します。

categoryID パラメーターに CategoryID Querystring フィールドを使用する

図 9: categoryID パラメーターに CategoryID クエリ文字列フィールドを使用する (クリックするとフルサイズの画像が表示されます)

ウィザードが完了すると、Visual Studio は対応する BoundField と CheckBoxField を製品データ フィールドの GridView に追加します。 ProductNameUnitPriceSupplierName を除くすべての BoundField を削除します。 これら 3 つの BoundField の HeaderText プロパティをカスタマイズして、Product、Price、Supplier をそれぞれ読み取るようにします。 UnitPrice BoundField を通貨として書式設定します。

次に、HyperLinkField を追加し、左端の位置に移動します。 その Text プロパティを [View Details] に設定し、その DataNavigateUrlFields プロパティを ProductID に設定し、その DataNavigateUrlFormatString プロパティを ~/SiteMapProvider/ProductDetails.aspx?ProductID={0} に設定します。

ProductDetails.aspxをポイントするビューの詳細 HyperLinkField を追加する

図 10: ProductDetails.aspx を指すビューの詳細 HyperLinkField を追加する

これらのカスタマイズを行った後、GridView と ObjectDataSource の宣言型マークアップは次のようになります。

<asp:GridView ID="ProductsByCategory" runat="server" AutoGenerateColumns="False"
    DataKeyNames="ProductID" DataSourceID="ProductsByCategoryDataSource" 
    EnableViewState="False">
    <Columns>
        <asp:HyperLinkField DataNavigateUrlFields="ProductID" 
            DataNavigateUrlFormatString=
                "~/SiteMapProvider/ProductDetails.aspx?ProductID={0}"
            Text="View Details" />
        <asp:BoundField DataField="ProductName" HeaderText="Product"
            SortExpression="ProductName" />
        <asp:BoundField DataField="UnitPrice" DataFormatString="{0:c}" 
            HeaderText="Price" HtmlEncode="False" 
            SortExpression="UnitPrice" />
        <asp:BoundField DataField="SupplierName" HeaderText="Supplier" 
            ReadOnly="True" SortExpression="SupplierName" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ProductsByCategoryDataSource" runat="server" 
    OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProductsByCategoryID" TypeName="ProductsBLL">
    <SelectParameters>
        <asp:QueryStringParameter Name="categoryID" 
            QueryStringField="CategoryID" Type="Int32" />
    </SelectParameters>
</asp:ObjectDataSource>

ブラウザーで Default.aspx の表示に戻り、Beverages の [View Products] リンクをクリックします。 これにより、ProductsByCategory.aspx?CategoryID=1 に移動し、Beverages カテゴリに属する Northwind データベース内の製品の名前、価格、サプライヤーが表示されます (図 11 を参照)。 このページをさらに拡張して、ユーザーをカテゴリ一覧ページ (Default.aspx) に戻すリンクと、選択したカテゴリの名前と説明を表示する DetailsView または FormView コントロールを含めます。

飲料名、価格、サプライヤーが表示されます

図 11: 飲料名、価格、サプライヤーが表示される (クリックするとフルサイズの画像が表示されます)

手順 4: 製品の詳細を表示する

最後のページ ProductDetails.aspx に、選択した製品の詳細が表示されます。 ProductDetails.aspx を開き、DetailsView をツールボックスからデザイナーにドラッグします。 DetailsView の ID プロパティを ProductInfo に設定し、その Height プロパティ値と Width プロパティ値をクリアします。 スマート タグから DetailsView を、ProductDataSource という名前の新しい ObjectDataSource にバインドし、ObjectDataSource を構成して ProductsBLL クラスの GetProductByProductID(productID) メソッドからデータをプルします。 手順 2 および 3 で作成した前の Web ページと同様に、[UPDATE]、[INSERT]、[DELETE] タブのドロップダウン リストを [(None)] に設定します。

GetProductByProductID(productID) メソッドを使用するように ObjectDataSource を構成する

図 12: GetProductByProductID(productID) メソッドを使用するように ObjectDataSource を構成する (クリックするとフルサイズの画像が表示されます)

データ ソースの構成ウィザードの最後の手順では、productID パラメーターのソースの入力を求められます。 このデータはクエリ文字列フィールド ProductID を介して取得されるため、ドロップダウン リストを QueryString に設定し、QueryStringField テキスト ボックスを ProductID に設定します。 最後に、[完了] ボタンをクリックしてウィザードを完了します。

ProductID Querystring フィールドから値をプルするように productID パラメーターを構成する

図 13: ProductID クエリ文字列フィールドから値をプルするように productID パラメーターを構成する (クリックするとフルサイズの画像が表示されます)

データ ソースの構成ウィザードが完了すると、Visual Studio によって、製品データ フィールドの DetailsView に、対応する BoundField と CheckBoxField が作成されます。 ProductIDSupplierIDCategoryID の BoundField を削除し、必要に応じて残りのフィールドを構成します。 外観を構成すると、DetailsView と ObjectDataSource の宣言型マークアップは次のようになります。

<asp:DetailsView ID="ProductInfo" runat="server" AutoGenerateRows="False" 
    DataKeyNames="ProductID" DataSourceID="ProductDataSource" 
    EnableViewState="False">
    <Fields>
        <asp:BoundField DataField="ProductName" HeaderText="Product" 
            SortExpression="ProductName" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category" 
            ReadOnly="True" SortExpression="CategoryName" />
        <asp:BoundField DataField="SupplierName" HeaderText="Supplier" 
            ReadOnly="True" SortExpression="SupplierName" />
        <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit" 
            SortExpression="QuantityPerUnit" />
        <asp:BoundField DataField="UnitPrice" DataFormatString="{0:c}" 
            HeaderText="Price" HtmlEncode="False" 
            SortExpression="UnitPrice" />
        <asp:BoundField DataField="UnitsInStock" HeaderText="Units In Stock" 
            SortExpression="UnitsInStock" />
        <asp:BoundField DataField="UnitsOnOrder" HeaderText="Units On Order" 
            SortExpression="UnitsOnOrder" />
        <asp:BoundField DataField="ReorderLevel" HeaderText="Reorder Level" 
            SortExpression="ReorderLevel" />
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued" 
            SortExpression="Discontinued" />
    </Fields>
</asp:DetailsView>
<asp:ObjectDataSource ID="ProductDataSource" runat="server" 
    OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProductByProductID" TypeName="ProductsBLL">
    <SelectParameters>
        <asp:QueryStringParameter Name="productID" 
            QueryStringField="ProductID" Type="Int32" />
    </SelectParameters>
</asp:ObjectDataSource>

このページをテストするには、Default.aspx に戻り、Beverages カテゴリの [View Products] をクリックします。 飲料製品の一覧から、Chai Tea の [View Details] リンクをクリックします。 これにより、Chai Tea の詳細を示す ProductDetails.aspx?ProductID=1 が表示されます (図 14 を参照)。

チャイティーサプライヤー、カテゴリ、価格、およびその他の情報が表示されます

図 14: Chai Tea のサプライヤー、カテゴリ、価格、その他の情報が表示されている (クリックするとフルサイズの画像が表示されます)

手順 5: サイト マップ プロバイダーの内部動作について

サイト マップは、Web サーバーのメモリ内で、階層を形成する SiteMapNode インスタンスのコレクションとして表されます。 ルートが 1 つだけ存在し、ルート以外のすべてのノードに 1 つの親ノードが必要であり、すべてのノードに任意の数の子が含まれる場合があります。 各 SiteMapNode オブジェクトは、Web サイトの構造内のセクションを表します。これらのセクションには、一般的に対応する Web ページがあります。 その結果、SiteMapNode クラスには、SiteMapNode が表すセクションの情報を提供する、TitleUrlDescription などのプロパティがあります。 階層内の各 SiteMapNode を一意に識別する Key プロパティと、この階層の ChildNodesParentNodeNextSiblingPreviousSibling などを確立するために使用されるプロパティもあります。

図 15 は図 1 の一般的なサイト マップ構造を示していますが、実装の詳細をより詳しく示しています。

各 SiteMapNode には、タイトル、URL、キーなどのプロパティがあります

図 15: 各 SiteMapNode プロパティに、TitleUrlKey などのプロパティがある (クリックするとフルサイズの画像が表示されます)

サイト マップには、System.Web 名前空間SiteMap クラスを介してアクセスできます。 このクラスの RootNode プロパティは、サイト マップのルート SiteMapNode インスタンスを返します。CurrentNode は、Url プロパティが現在要求されているページの URL と一致する SiteMapNode を返します。 このクラスは、ASP.NET 2.0 のナビゲーション Web コントロールによって内部的に使用されます。

SiteMap クラスのプロパティにアクセスするときは、サイト マップ構造をいくつかの永続的なメディアからメモリにシリアル化する必要があります。 ただし、サイト マップのシリアル化ロジックは、SiteMap クラスにハード コーディングされません。 代わりに、ランタイムに、SiteMap クラスは、シリアル化に使用するサイト マップ "プロバイダー" を決定します。 既定では、XmlSiteMapProvider クラスが使用されます。このクラスは、適切に書式設定された XML ファイルからサイト マップの構造を読み取ります。 ただし、少しの作業で、独自のカスタム サイト マップ プロバイダーを作成できます。

すべてのサイト マップ プロバイダーは、サイト マップ プロバイダーに必要な基本的なメソッドとプロパティを含む SiteMapProvider クラスから派生する必要がありますが、実装の詳細の多くは省略されています。 2 番目のクラス StaticSiteMapProvider は、SiteMapProvider クラスを拡張し、必要な機能のより堅牢な実装を含みます。 内部的には、StaticSiteMapProvider はサイト マップの SiteMapNode インスタンスを Hashtable に格納し、内部 HashtableSiteMapNode を追加および削除する AddNode(child, parent)RemoveNode(siteMapNode),Clear() などのメソッドを提供します。 XmlSiteMapProvider は、StaticSiteMapProvider から派生しています。

StaticSiteMapProvider を拡張するカスタム サイト マップ プロバイダーを作成する場合、オーバーライドする必要がある抽象メソッドとして BuildSiteMapGetRootNodeCore の 2 つがあります。 BuildSiteMap は、その名前が示すように、永続的ストレージからサイト マップ構造を読み込み、メモリ内に構築する役割を担います。 GetRootNodeCore は、サイト マップ内のルート ノードを返します。

Web アプリケーションでサイト マップ プロバイダーを使用するには、その前にアプリケーションの構成に登録する必要があります。 既定では、XmlSiteMapProvider クラスは名前 AspNetXmlSiteMapProvider を使用して登録されます。 追加のサイト マップ プロバイダーを登録するには、次のマークアップを Web.config に追加します。

<configuration>
    <system.web>
        ...
        <siteMap defaultProvider="defaultProviderName">
          <providers>
            <add name="name" type="type" />
          </providers>
        </siteMap>
    </system.web>
</configuration>

name 値は人間が判読できる名前をプロバイダーに割り当てますが、type はサイト マップ プロバイダーの完全修飾型名を指定します。 カスタム サイト マップ プロバイダーを作成した後、手順 7 で name 値と type 値の具体的な値について説明します。

サイト マップ プロバイダー クラスは、SiteMap クラスから初めてアクセスされたときにインスタンス化され、Web アプリケーションの有効期間中はメモリ内に残ります。 複数の同時 Web サイト訪問者から呼び出される可能性があるサイト マップ プロバイダーのインスタンスは 1 つだけであるため、プロバイダーのメソッドは "スレッドセーフ" であることが不可欠です。

パフォーマンスとスケーラビリティの理由から、BuildSiteMap メソッドが呼び出されるたびに再作成するのではなく、インメモリ サイト マップ構造をキャッシュし、このキャッシュされた構造を返すことが重要です。 BuildSiteMap は、ページで使用されているナビゲーション コントロールとサイト マップ構造の深さに応じて、ユーザーとページ要求ごとに複数回呼び出すことができます。 いずれの場合も、BuildSiteMap のサイト マップ構造をキャッシュしない場合は、呼び出されるたびに、アーキテクチャから製品とカテゴリの情報を再取得する必要があります (その結果、データベースに対するクエリが発生します)。 前のキャッシュのチュートリアルで説明したように、キャッシュ データは古くなる可能性があります。 これに対処するために、時間または SQL キャッシュの依存関係ベースの有効期限を使用できます。

Note

サイト マップ プロバイダーは、必要に応じて Initialize メソッドをオーバーライドできます。 Initialize は、サイト マップ プロバイダーが最初にインスタンス化されたときに呼び出され、<add name="name" type="type" customAttribute="value" /> のように、<add> 要素内の Web.config のプロバイダーに割り当てられたカスタム属性が渡されます。 これは、ページ開発者がプロバイダーのコードを変更することなく、さまざまなサイト マップ プロバイダー関連の設定を指定できるようにする場合に便利です。 たとえば、カテゴリと製品のデータを、アーキテクチャではなく、データベースから直接読み取る場合、ページ開発者がプロバイダーのコードでハード コーディングされた値を使用するのではなく、Web.config を使用してデータベースの接続文字列を指定できるようにしたい場合があります。 手順 6 で構築するカスタム サイト マップ プロバイダーでは、この Initialize メソッドはオーバーライドされません。 Initialize メソッドの使用例については、Jeff Prosise による SQL Server でのサイト マップの格納に関する記事を参照してください。

手順 6: カスタム サイト マップ プロバイダーの作成

Northwind データベースのカテゴリと製品からサイト マップを構築するカスタム サイト マップ プロバイダーを作成するには、StaticSiteMapProvider を拡張するクラスを作成する必要があります。 手順 1 で、App_Code フォルダーに CustomProviders フォルダーを追加するように求めました。このフォルダーに NorthwindSiteMapProvider という新しいクラスを追加します。 以下のコードを NorthwindSiteMapProvider クラスに追加します。

Imports System.Web
Imports System.Web.Caching
Public Class NorthwindSiteMapProvider
    Inherits StaticSiteMapProvider
    Private ReadOnly siteMapLock As New Object()
    Private root As SiteMapNode = Nothing
    Public Const CacheDependencyKey As String = "NorthwindSiteMapProviderCacheDependency"
    Public Overrides Function BuildSiteMap() As System.Web.SiteMapNode
        ' Use a lock to make this method thread-safe
        SyncLock siteMapLock
            ' First, see if we already have constructed the
            ' rootNode. If so, return it...
            If root IsNot Nothing Then
                Return root
            End If
            ' We need to build the site map!
            ' Clear out the current site map structure
            MyBase.Clear()
            ' Get the categories and products information from the database
            Dim productsAPI As New ProductsBLL()
            Dim products As Northwind.ProductsDataTable = productsAPI.GetProducts()
            ' Create the root SiteMapNode
            root = New SiteMapNode( _
                Me, "root", "~/SiteMapProvider/Default.aspx", "All Categories")
            AddNode(root)
            ' Create SiteMapNodes for the categories and products
            For Each product As Northwind.ProductsRow In products
                ' Add a new category SiteMapNode, if needed
                Dim categoryKey, categoryName As String
                Dim createUrlForCategoryNode As Boolean = True
                If product.IsCategoryIDNull() Then
                    categoryKey = "Category:None"
                    categoryName = "None"
                    createUrlForCategoryNode = False
                Else
                    categoryKey = String.Concat("Category:", product.CategoryID)
                    categoryName = product.CategoryName
                End If
                Dim categoryNode As SiteMapNode = FindSiteMapNodeFromKey(categoryKey)
                ' Add the category SiteMapNode if it does not exist
                If categoryNode Is Nothing Then
                    Dim productsByCategoryUrl As String = String.Empty
                    If createUrlForCategoryNode Then
                        productsByCategoryUrl = _
                            "~/SiteMapProvider/ProductsByCategory.aspx?CategoryID=" & _
                            product.CategoryID
                    End If
                    categoryNode = New SiteMapNode _
                        (Me, categoryKey, productsByCategoryUrl, categoryName)
                    AddNode(categoryNode, root)
                End If
                ' Add the product SiteMapNode
                Dim productUrl As String = _
                    "~/SiteMapProvider/ProductDetails.aspx?ProductID=" & _
                    product.ProductID
                Dim productNode As New SiteMapNode _
                    (Me, String.Concat("Product:", product.ProductID), _
                    productUrl, product.ProductName)
                AddNode(productNode, categoryNode)
            Next
            ' Add a "dummy" item to the cache using a SqlCacheDependency
            ' on the Products and Categories tables
            Dim productsTableDependency As New _
                System.Web.Caching.SqlCacheDependency("NorthwindDB", "Products")
            Dim categoriesTableDependency As New _
                System.Web.Caching.SqlCacheDependency("NorthwindDB", "Categories")
            ' Create an AggregateCacheDependency
            Dim aggregateDependencies As New System.Web.Caching.AggregateCacheDependency()
            aggregateDependencies.Add(productsTableDependency, categoriesTableDependency)
            ' Add the item to the cache specifying a callback function
            HttpRuntime.Cache.Insert( _
                CacheDependencyKey, DateTime.Now, aggregateDependencies, _
                Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, _
                CacheItemPriority.Normal, AddressOf OnSiteMapChanged)
            ' Finally, return the root node
            Return root
        End SyncLock
    End Function
    Protected Overrides Function GetRootNodeCore() As System.Web.SiteMapNode
        Return BuildSiteMap()
    End Function
    Protected Sub OnSiteMapChanged _
    (key As String, value As Object, reason As CacheItemRemovedReason)
        SyncLock siteMapLock
            If String.Compare(key, CacheDependencyKey) = 0 Then
                ' Refresh the site map
                root = Nothing
            End If
        End SyncLock
    End Sub
    Public ReadOnly Property CachedDate() As Nullable(Of DateTime)
        Get
            Dim value As Object = HttpRuntime.Cache(CacheDependencyKey)
            If value Is Nothing OrElse Not TypeOf value Is Nullable(Of DateTime) Then
                Return Nothing
            Else
                Return CType(value, Nullable(Of DateTime))
            End If
        End Get
    End Property
End Class

まず、このクラスの BuildSiteMap メソッドを調べてみましょう。これは lock ステートメントで始まります。 lock ステートメントでは、一度に 1 つのスレッドのみを入力できるため、コードへのアクセスがシリアル化され、2 つの同時実行スレッドが干渉しあうことがなくなります。

クラス レベル SiteMapNode の変数 root は、サイト マップ構造をキャッシュするために使用されます。 サイト マップが初めて構築されたとき、または基になるデータが変更された後で初めて構築された場合、rootNothing になり、サイト マップ構造が構築されます。 次にこのメソッドが呼び出されたときに rootNothing にならないように、サイト マップのルート ノードには、構築プロセス中に root が割り当てられます。 したがって、rootNothing でない限り、サイト マップ構造は再作成することなく呼び出し元に返されます。

ルートが Nothing の場合は、製品とカテゴリの情報からサイト マップ構造が作成されます。 サイト マップは、SiteMapNode インスタンスを作成し、StaticSiteMapProvider クラスの AddNode メソッドの呼び出しによって階層を形成することで構築されます。 AddNode は内部のブックキーピングを実行し、さまざまな SiteMapNode インスタンスを Hashtable に格納します。 階層の構築を開始する前に、まず Clear メソッドを呼び出して、内部 Hashtable から要素をクリアします。 次に、ProductsBLL クラスの GetProducts メソッドと、結果の ProductsDataTable がローカル変数に格納されます。

サイト マップの構築は、ルート ノードを作成して、root に割り当てることから始まります。 ここと、この BuildSiteMap 全体で使用する SiteMapNode のコンストラクターのオーバーロードには、次の情報が渡されます。

  • サイト マップ プロバイダー (Me) への参照。
  • SiteMapNodeKey。 この必須値は、それぞれの SiteMapNode で一意である必要があります。
  • SiteMapNodeUrlUrl は省略可能ですが、指定した場合、各 SiteMapNodeUrl 値は一意である必要があります。
  • SiteMapNodeTitle。これは必須です。

AddNode(root) メソッドの呼び出しにより、サイト マップにルートとしてSiteMapNode rootが追加されます。 次に、ProductsDataTable 内の各 ProductRow が列挙されます。 現在の製品カテゴリに SiteMapNode が既に存在する場合は、それが参照されます。 それ以外の場合は、カテゴリに対応する新規の SiteMapNode が作成され、AddNode(categoryNode, root) メソッド呼び出しを通じて SiteMapNode``root の子として追加されます。 適切なカテゴリ SiteMapNode ノードが見つかったか作成された後、SiteMapNode は現在の製品に対して作成され、AddNode(productNode, categoryNode) を介して SiteMapNode カテゴリの子として追加されます。 カテゴリ SiteMapNodeUrl プロパティ値は ~/SiteMapProvider/ProductsByCategory.aspx?CategoryID=categoryID で、製品 SiteMapNodeUrl プロパティには ~/SiteMapNode/ProductDetails.aspx?ProductID=productID が割り当てられていることに注意してください。

Note

CategoryID がデータベースの NULL 値を持つ製品は、Title プロパティが None に設定され、Url プロパティが空の文字列に設定されているカテゴリ SiteMapNode の下にグループ化されます。 ProductBLLクラスのGetProductsByCategory(categoryID)メソッドには現在、NULL CategoryID値を持つ製品だけを返す機能がないため、Urlを空の文字列に設定することにしました。 また、ナビゲーション コントロールが Url プロパティの値を欠いている SiteMapNode をレンダリングする方法を示したいと思いました。 このチュートリアルを拡張して、None SiteMapNodeUrl プロパティが ProductsByCategory.aspxを指し示しながら、 NULL CategoryID 値を持つ製品のみを表示するようにすることをお勧めします。

サイト マップを構築した後、AggregateCacheDependency オブジェクトを介して、Categories および Products のテーブルに対する SQL キャッシュの依存関係を使用して、任意のオブジェクトがデータ キャッシュに追加されます。 前のチュートリアル「SQL キャッシュ依存関係を使用する」では、SQL キャッシュの依存関係の使用について説明しました。 しかし、カスタム サイト マップ プロバイダーでは、まだ調べていない、データ キャッシュの Insert メソッドのオーバーロードを使用します。 このオーバーロードは、オブジェクトがキャッシュから削除されたときに呼び出されるデリゲートを、最終的な入力パラメーターとして受け取ります。 具体的には、NorthwindSiteMapProvider クラスでさらに下に定義された OnSiteMapChanged メソッドを指す新しい CacheItemRemovedCallback デリゲートを渡します。

Note

サイト マップのメモリ内表現は、クラス レベルの変数 root を介してキャッシュされます。 カスタム サイト マップ プロバイダー クラスのインスタンスは 1 つだけであり、そのインスタンスは Web アプリケーション内のすべてのスレッド間で共有されるため、このクラス変数はキャッシュとして機能します。 BuildSiteMap メソッドはデータ キャッシュも使用しますが、Categories テーブルまたは Products テーブル内の基になるデータベース データが変更されたときに通知を受け取る手段としてのみ使用されます。 データ キャッシュに格納される値は、現在の日付と時刻に過ぎません。 実際のサイト マップ データはデータ キャッシュに格納されません。

BuildSiteMap メソッドは、サイト マップのルート ノードを返すことによって完了します。

残りのメソッドは非常に簡単です。 GetRootNodeCore はルート ノードを返す役割を担います。 BuildSiteMap はルートを返すので、GetRootNodeCore は単に BuildSiteMap の戻り値を返します。 OnSiteMapChanged メソッドは、キャッシュ項目が削除されたときに rootNothing に戻します。 ルートを Nothing に戻すと、次回 BuildSiteMap が呼び出されたときに、サイト マップ構造が再構築されます。 最後に、CachedDate プロパティは、データ キャッシュに格納されている日付と時刻の値 (そのような値が存在する場合) を返します。 このプロパティは、ページ開発者がサイト マップ データが最後にキャッシュされた日時を判断するために使用できます。

手順 7: NorthwindSiteMapProvider を登録する

手順 6 で作成した NorthwindSiteMapProvider サイト マップ プロバイダーを Web アプリケーションで使用するには、Web.config<siteMap> セクションに登録する必要があります。 具体的には、次のマークアップを、Web.config<system.web> 要素内に追加します。

<siteMap defaultProvider="AspNetXmlSiteMapProvider">
  <providers>
    <add name="Northwind" type="NorthwindSiteMapProvider" />
  </providers>
</siteMap>

このマークアップは、2 つのことを行います。最初に、組み込み AspNetXmlSiteMapProvider が既定のサイト マップ プロバイダーであることを示します。次に、手順 6 で作成したカスタム サイト マップ プロバイダーを、人間が判読できる名前 Northwind で登録します。

Note

アプリケーションの App_Code フォルダーにあるサイト マップ プロバイダーの場合、type 属性の値は単なるクラス名です。 または、カスタム サイト マップ プロバイダーを別のクラス ライブラリ プロジェクトに作成し、コンパイル済みのアセンブリを Web アプリケーションの /Bin ディレクトリに配置することもできます。 その場合、type 属性値は Namespace.ClassName, AssemblyName になります。

Web.config を更新した後、ブラウザーでチュートリアルの任意のページを表示します。 左側のナビゲーション インターフェイスには、Web.sitemap で定義されているセクションとチュートリアルが引き続き表示されることに注意してください。 これは、AspNetXmlSiteMapProvider を既定のプロバイダーとして残したためです。 NorthwindSiteMapProvider を使用するナビゲーション ユーザー インターフェイス要素を作成するには、Northwind サイト マップ プロバイダーを使用することを明示的に指定する必要があります。 これを行う方法については、手順 8 で説明します。

手順 8: カスタム サイト マップ プロバイダーを使用してサイト マップ情報を表示する

カスタム サイト マップ プロバイダーが作成され、Web.config に登録されたので、これで、SiteMapProvider フォルダーの Default.aspxProductsByCategory.aspxProductDetails.aspx ページにナビゲーション コントロールを追加できます。 まず Default.aspx ページを開き、ツールボックスからデザイナーに SiteMapPath をドラッグします。 SiteMapPath コントロールは、ツールボックスのナビゲーション セクションにあります。

SiteMapPath をDefault.aspxに追加する

図 16: SiteMapPath を Default.aspx に追加する (クリックするとフルサイズの画像が表示されます)

SiteMapPath コントロールは階層リンクを表示し、サイト マップ内の現在のページの位置を示します。 「マスター ページとサイト ナビゲーション」のチュートリアルで、マスター ページの上部に SiteMapPath を追加しました。

少し時間を取り、ブラウザーでこのページを表示してみてください。 図 16 で追加された SiteMapPath は、既定のサイト マップ プロバイダーを使用して、Web.sitemap からデータを取得します。 そのため、階層リンクには、右上隅の階層リンクと同様に、[Home] > [Customizing the Site Map] と表示されます。

階層リンクは、既定のサイト マップ プロバイダーを使用します

図 17: 階層リンクは既定のサイト マップ プロバイダーを使用する (クリックするとフルサイズの画像が表示されます)

図 16 で追加した SiteMapPath を使用するには、手順 6 で作成したカスタム サイト マップ プロバイダーを使用し、その SiteMapProvider プロパティを、Web.configNorthwindSiteMapProvider に割り当てた名前の Northwind に設定します。 残念ながら、デザイナーは既定のサイト マップ プロバイダーを引き続き使用しますが、このプロパティを変更した後にブラウザーからページにアクセスすると、階層リンクでカスタム サイト マップ プロバイダーが使用されるようになったことを確認できます。

階層リンクにカスタム サイト マップ プロバイダーがどのように表示されるかを示すスクリーンショット。

図 18: 階層リンクがカスタム サイト マップ プロバイダー NorthwindSiteMapProvider を使用するようになった (クリックするとフルサイズの画像が表示されます)

SiteMapPath コントロールは、ProductsByCategory.aspx ページと ProductDetails.aspx ページに、より機能的なユーザー インターフェイスを表示します。 これらのページに SiteMapPath を追加し、両方の SiteMapProvider プロパティを Northwind に設定します。 Default.aspx から、[Beverages] の [View Products] リンクをクリックし、[Chai Tea] の [View Details] リンクをクリックします。 図 19 に示すように、階層リンクには、現在のサイト マップ セクション (Chai Tea) とその先祖 [Beverages] と [All Categories] が含まれています。

階層リンクに現在のサイト マップ セクション (チャイ ティー) とその先祖 (飲料とすべてのカテゴリ) がどのように表示されるかを示すスクリーンショット。

図 19: 階層リンクがカスタム サイト マップ プロバイダー NorthwindSiteMapProvider を使用するようになった (クリックするとフルサイズの画像が表示されます)

SiteMapPath に加えて、Menu コントロールや TreeView コントロールなどの他のナビゲーション ユーザー インターフェイス要素を使用できます。 このチュートリアルのダウンロードの Default.aspxProductsByCategory.aspxProductDetails.aspx のページには、すべて Menu コントロールが含まれます (図 20 を参照)。 ASP.NET 2.0 のナビゲーション コントロールとサイト マップ システムの詳細については、「ASP.NET 2.0 クイック スタート」の「ASP.NET 2.0 の高度なサイト ナビゲーション機能」と「サイト ナビゲーション コントロールの使用」セクションをご覧ください。

メニュー コントロールは、各カテゴリと製品を一覧表示します。

図 20: 各カテゴリと製品を一覧表示する Menu コントロール (クリックするとフルサイズの画像が表示されます)

このチュートリアルで前述したように、サイト マップ構造には、SiteMap クラスを使用してプログラムでアクセスできます。 次のコードは、既定のプロバイダーのルート SiteMapNode を返します。

Dim root As SiteMapNode = SiteMap.RootNode

AspNetXmlSiteMapProvider はアプリケーションの既定のプロバイダーであるため、上記のコードでは、Web.sitemap で定義されているルート ノードが返されます。 既定以外のサイト マップ プロバイダーを参照するには、次のように SiteMap クラスの Providers プロパティを使用します。

Dim root As SiteMapNode = SiteMap.Providers("name").RootNode

ここで name は、カスタム サイト マップ プロバイダー (今回の Web アプリケーションの場合は Northwind) の名前です。

サイト マップ プロバイダーに固有のメンバーにアクセスするには、SiteMap.Providers["name"] を使用してプロバイダー インスタンスを取得し、適切な型にキャストします。 たとえば、ASP.NET ページに NorthwindSiteMapProviderCachedDate プロパティを表示するには、次のコードを使用します。

Dim customProvider As NorthwindSiteMapProvider = _
    TryCast(SiteMap.Providers("Northwind"), NorthwindSiteMapProvider)
If customProvider IsNot Nothing Then
    Dim lastCachedDate As Nullable(Of DateTime) = customProvider.CachedDate
    If lastCachedDate.HasValue Then
        SiteMapLastCachedDate.Text = _
            "Site map cached on: " & lastCachedDate.Value.ToString()
    Else
        SiteMapLastCachedDate.Text = "The site map is being reconstructed!"
    End If
End If

Note

SQL キャッシュの依存関係機能を必ずテストしてください。 Default.aspxProductsByCategory.aspxProductDetails.aspx のページにアクセスしたら、チュートリアルの編集、挿入、削除のいずれかのセクションに移動して、カテゴリまたは製品の名前を編集します。 次に、SiteMapProvider フォルダー内のいずれかのページに戻ります。 基になるデータベースへの変更がポーリング メカニズムで検出されるのに十分な時間が経過したと想定して、サイト マップを更新すると新しい製品またはカテゴリ名が表示されるはずです。

まとめ

ASP.NET 2.0 のサイト マップ機能には、SiteMap クラス、いくつかの組み込みのナビゲーション Web コントロール、サイト マップ情報が XML ファイルに保持されることが想定された既定のサイト マップ プロバイダーが含まれます。 データベース、アプリケーションのアーキテクチャ、リモート Web サービスなど、他のソースからのサイト マップ情報を使用するには、カスタム サイト マップ プロバイダーを作成する必要があります。 これには、SiteMapProvider クラスから直接または間接的に派生するクラスの作成が含まれます。

このチュートリアルでは、アプリケーション アーキテクチャから呼び出された製品とカテゴリの情報のサイト マップに基づくカスタム サイト マップ プロバイダーを作成する方法について説明しました。 プロバイダーは、StaticSiteMapProvider クラスを拡張し、データを取得し、サイト マップ階層を構築し、結果の構造をクラス レベルの変数にキャッシュする BuildSiteMap メソッドを作成する必要がありました。 基になる Categories または Products データが変更されたときにキャッシュされた構造を無効にするために、コールバック関数とともに SQL キャッシュ依存関係を使用しました。

プログラミングに満足!

もっと読む

この記事で説明したトピックの詳細については、次のリソースを参照してください。

著者について

7 冊の ASP/ASP.NET 書籍の著者であり、4GuysFromRolla.com の創設者である Scott Mitchell は、1998 年から Microsoft Web テクノロジに取り組んでいます。 Scott は、独立したコンサルタント、トレーナー、ライターとして働いています。 彼の最新の本は サムズは24時間で2.0 ASP.NET 自分自身を教えています。 にアクセスするか、ブログを使用して にアクセスmitchell@4GuysFromRolla.comできます。これは でhttp://ScottOnWriting.NET見つけることができます。

特別な感謝

このチュートリアル シリーズは、多くの役に立つ校閲者によってレビューされました。 このチュートリアルのリード レビュー担当者は、Dave Gardner、Zack Jones、Toria Murphy、Bernadette Leigh でした。 今後の MSDN の記事を確認することに関心がありますか? その場合は、 にmitchell@4GuysFromRolla.com行をドロップしてください。