カスタム データベース駆動型サイト マップ プロバイダーを構築する (C#)
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
フォルダーに追加します。
図 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.aspx
、ProductsByCategory.aspx
、ProductDetails.aspx
のページを作成する必要があります。 これらのページは、それぞれ手順 2、3、4 で完成します。 このチュートリアルの要旨はサイト マップ プロバイダーであり、過去のチュートリアルではこのような複数ページのマスター/詳細レポートの作成について説明しているので、手順 2 から 4 までは急ぎ足で進めます。 複数のページにまたがるマスター/詳細レポートの作成について簡単な復習が必要な場合は、「2 つのページでマスター/詳細をフィルター処理する」のチュートリアルをご覧ください。
手順 2: カテゴリの一覧を表示する
SiteMapProvider
フォルダー内の Default.aspx
ページを開き、ツールボックスからデザイナーに GridView をドラッグして、その ID
を Categories
に設定します。 GridView のスマート タグから、CategoriesDataSource
という名前の新しい ObjectDataSource にバインドし、CategoriesBLL
クラスの GetCategories
メソッドを使用してデータを取得するように構成します。 この GridView ではカテゴリが表示され、データ変更機能は提供されないため、[UPDATE]、[INSERT]、[DELETE] タブのドロップダウン リストを [(None)] に設定します。
図 4: GetCategories
メソッドを使ってカテゴリを返すように ObjectDataSource を構成する (クリックするとフルサイズの画像が表示されます)
図 5: [UPDATE]、[INSERT]、[DELETE] の各タブのドロップダウン リストを [(None)] に設定する (クリックするとフルサイズの画像が表示されます)
データ ソースの構成ウィザードが完了すると、Visual Studio によって、CategoryID
、CategoryName
、Description
、NumberOfProducts
、BrochurePath
の BoundField が追加されます。 GridView を編集して、CategoryName
と Description
の BoundField のみが含まれるようにし、CategoryName
BoundField の HeaderText
プロパティを Category に更新します。
次に、HyperLinkField を追加し、左端のフィールドになるように配置します。 DataNavigateUrlFields
プロパティを CategoryID
に、DataNavigateUrlFormatString
プロパティを ~/SiteMapProvider/ProductsByCategory.aspx?CategoryID={0}
にそれぞれ設定します。 Text
プロパティを [View Products] に設定します。
図 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)] に設定します。
図 8: ProductsBLL
クラスの GetProductsByCategoryID(categoryID)
メソッドを使う (クリックするとフルサイズの画像が表示されます)
データ ソースの構成ウィザードの最後の手順では、categoryID のパラメーター ソースの入力を求められます。 この情報はクエリ文字列フィールド CategoryID
を介して渡されるので、図 9 に示すように、ドロップダウン リストから [QueryString] を選択し、[QueryStringField] テキストボックスに [CategoryID] を入力します。 [完了] をクリックして、ウィザードを完了します。
図 9: categoryID パラメーターに CategoryID
クエリ文字列フィールドを使用する (クリックするとフルサイズの画像が表示されます)
ウィザードが完了すると、Visual Studio は対応する BoundField と CheckBoxField を製品データ フィールドの GridView に追加します。 ProductName
、UnitPrice
、SupplierName
を除くすべての BoundField を削除します。 これら 3 つの BoundField の HeaderText
プロパティをカスタマイズして、Product、Price、Supplier をそれぞれ読み取るようにします。 UnitPrice
BoundField を通貨として書式設定します。
次に、HyperLinkField を追加し、左端の位置に移動します。 その Text
プロパティを [View Details] に設定し、その DataNavigateUrlFields
プロパティを ProductID
に設定し、その DataNavigateUrlFormatString
プロパティを ~/SiteMapProvider/ProductDetails.aspx?ProductID={0}
に設定します。
図 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)] に設定します。
図 12: GetProductByProductID(productID)
メソッドを使用するように ObjectDataSource を構成する (クリックするとフルサイズの画像が表示されます)
データ ソースの構成ウィザードの最後の手順では、productID パラメーターのソースの入力を求められます。 このデータはクエリ文字列フィールド ProductID
を介して取得されるため、ドロップダウン リストを QueryString に設定し、QueryStringField テキスト ボックスを ProductID に設定します。 最後に、[完了] ボタンをクリックしてウィザードを完了します。
図 13: ProductID
クエリ文字列フィールドから値をプルするように productID パラメーターを構成する (クリックするとフルサイズの画像が表示されます)
データ ソースの構成ウィザードが完了すると、Visual Studio によって、製品データ フィールドの DetailsView に、対応する BoundField と CheckBoxField が作成されます。 ProductID
、SupplierID
、CategoryID
の 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
が表すセクションの情報を提供する、Title
、Url
、Description
などのプロパティがあります。 階層内の各 SiteMapNode
を一意に識別する Key
プロパティと、この階層の ChildNodes
、ParentNode
、NextSibling
、PreviousSibling
などを確立するために使用されるプロパティもあります。
図 15 は図 1 の一般的なサイト マップ構造を示していますが、実装の詳細をより詳しく示しています。
図 15: 各 SiteMapNode
プロパティに、Title
、Url
、Key
などのプロパティがある (クリックするとフルサイズの画像が表示されます)
サイト マップには、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
に格納し、内部 Hashtable
に SiteMapNode
を追加および削除する AddNode(child, parent)
、RemoveNode(siteMapNode),
、Clear()
などのメソッドを提供します。 XmlSiteMapProvider
は、StaticSiteMapProvider
から派生しています。
StaticSiteMapProvider
を拡張するカスタム サイト マップ プロバイダーを作成する場合、オーバーライドする必要がある抽象メソッドとして BuildSiteMap
と GetRootNodeCore
の 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
クラスに追加します。
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;
using System.Web.Caching;
public class NorthwindSiteMapProvider : StaticSiteMapProvider
{
private readonly object siteMapLock = new object();
private SiteMapNode root = null;
public const string CacheDependencyKey =
"NorthwindSiteMapProviderCacheDependency";
public override SiteMapNode BuildSiteMap()
{
// Use a lock to make this method thread-safe
lock (siteMapLock)
{
// First, see if we already have constructed the
// rootNode. If so, return it...
if (root != null)
return root;
// We need to build the site map!
// Clear out the current site map structure
base.Clear();
// Get the categories and products information from the database
ProductsBLL productsAPI = new ProductsBLL();
Northwind.ProductsDataTable products = productsAPI.GetProducts();
// Create the root SiteMapNode
root = new SiteMapNode(
this, "root", "~/SiteMapProvider/Default.aspx", "All Categories");
AddNode(root);
// Create SiteMapNodes for the categories and products
foreach (Northwind.ProductsRow product in products)
{
// Add a new category SiteMapNode, if needed
string categoryKey, categoryName;
bool createUrlForCategoryNode = true;
if (product.IsCategoryIDNull())
{
categoryKey = "Category:None";
categoryName = "None";
createUrlForCategoryNode = false;
}
else
{
categoryKey = string.Concat("Category:", product.CategoryID);
categoryName = product.CategoryName;
}
SiteMapNode categoryNode = FindSiteMapNodeFromKey(categoryKey);
// Add the category SiteMapNode if it does not exist
if (categoryNode == null)
{
string productsByCategoryUrl = string.Empty;
if (createUrlForCategoryNode)
productsByCategoryUrl =
"~/SiteMapProvider/ProductsByCategory.aspx?CategoryID="
+ product.CategoryID;
categoryNode = new SiteMapNode(
this, categoryKey, productsByCategoryUrl, categoryName);
AddNode(categoryNode, root);
}
// Add the product SiteMapNode
string productUrl =
"~/SiteMapProvider/ProductDetails.aspx?ProductID="
+ product.ProductID;
SiteMapNode productNode = new SiteMapNode(
this, string.Concat("Product:", product.ProductID),
productUrl, product.ProductName);
AddNode(productNode, categoryNode);
}
// Add a "dummy" item to the cache using a SqlCacheDependency
// on the Products and Categories tables
System.Web.Caching.SqlCacheDependency productsTableDependency =
new System.Web.Caching.SqlCacheDependency("NorthwindDB", "Products");
System.Web.Caching.SqlCacheDependency categoriesTableDependency =
new System.Web.Caching.SqlCacheDependency("NorthwindDB", "Categories");
// Create an AggregateCacheDependency
System.Web.Caching.AggregateCacheDependency aggregateDependencies =
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,
new CacheItemRemovedCallback(OnSiteMapChanged));
// Finally, return the root node
return root;
}
}
protected override SiteMapNode GetRootNodeCore()
{
return BuildSiteMap();
}
protected void OnSiteMapChanged(string key, object value, CacheItemRemovedReason reason)
{
lock (siteMapLock)
{
if (string.Compare(key, CacheDependencyKey) == 0)
{
// Refresh the site map
root = null;
}
}
}
public DateTime? CachedDate
{
get
{
return HttpRuntime.Cache[CacheDependencyKey] as DateTime?;
}
}
}
まず、このクラスの BuildSiteMap
メソッドを調べてみましょう。これは lock
ステートメントで始まります。 lock
ステートメントでは、一度に 1 つのスレッドのみを入力できるため、コードへのアクセスがシリアル化され、2 つの同時実行スレッドが干渉しあうことがなくなります。
クラス レベル SiteMapNode
の変数 root
は、サイト マップ構造をキャッシュするために使用されます。 サイト マップが初めて構築されたとき、または基になるデータが変更された後で初めて構築された場合、root
は null
になり、サイト マップ構造が構築されます。 次にこのメソッドが呼び出されたときに root
が null
にならないように、サイト マップのルート ノードには、構築プロセス中に root
が割り当てられます。 したがって、root
が null
でない限り、サイト マップ構造は再作成することなく呼び出し元に返されます。
ルートが null
の場合は、製品とカテゴリの情報からサイト マップ構造が作成されます。 サイト マップは、SiteMapNode
インスタンスを作成し、StaticSiteMapProvider
クラスの AddNode
メソッドの呼び出しによって階層を形成することで構築されます。 AddNode
は内部のブックキーピングを実行し、さまざまな SiteMapNode
インスタンスを Hashtable
に格納します。 階層の構築を開始する前に、まず Clear
メソッドを呼び出して、内部 Hashtable
から要素をクリアします。 次に、ProductsBLL
クラスの GetProducts
メソッドと、結果の ProductsDataTable
がローカル変数に格納されます。
サイト マップの構築は、ルート ノードを作成して、root
に割り当てることから始まります。 ここと、この BuildSiteMap
全体で使用する SiteMapNode
のコンストラクターのオーバーロードには、次の情報が渡されます。
- サイト マップ プロバイダー (
this
) への参照。 SiteMapNode
のKey
。 この必須値は、それぞれのSiteMapNode
で一意である必要があります。SiteMapNode
のUrl
。Url
は省略可能ですが、指定した場合、各SiteMapNode
のUrl
値は一意である必要があります。SiteMapNode
のTitle
。これは必須です。
AddNode(root)
メソッドの呼び出しにより、サイト マップにルートとしてSiteMapNode
root
が追加されます。 次に、ProductsDataTable
内の各 ProductRow
が列挙されます。 現在の製品カテゴリに SiteMapNode
が既に存在する場合は、それが参照されます。 それ以外の場合は、カテゴリに対応する新規の SiteMapNode
が作成され、AddNode(categoryNode, root)
メソッド呼び出しを通じて SiteMapNode``root
の子として追加されます。 適切なカテゴリ SiteMapNode
ノードが見つかったか作成された後、SiteMapNode
は現在の製品に対して作成され、AddNode(productNode, categoryNode)
を介して SiteMapNode
カテゴリの子として追加されます。 カテゴリ SiteMapNode
の Url
プロパティ値は ~/SiteMapProvider/ProductsByCategory.aspx?CategoryID=categoryID
で、製品 SiteMapNode
の Url
プロパティには ~/SiteMapNode/ProductDetails.aspx?ProductID=productID
が割り当てられていることに注意してください。
Note
CategoryID
がデータベースの NULL
値を持つ製品は、Title
プロパティが None に設定され、Url
プロパティが空の文字列に設定されているカテゴリ SiteMapNode
の下にグループ化されます。 ProductBLL
クラスのGetProductsByCategory(categoryID)
メソッドには現在、NULL
CategoryID
値を持つ製品だけを返す機能がないため、Url
を空の文字列に設定することにしました。 また、ナビゲーション コントロールが Url
プロパティの値を欠いている SiteMapNode
をレンダリングする方法を示したいと思いました。 このチュートリアルを拡張して、None SiteMapNode
の Url
プロパティが 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
メソッドは、キャッシュ項目が削除されたときに root
を null
に戻します。 ルートを null
に戻すと、次回 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.aspx
、ProductsByCategory.aspx
、ProductDetails.aspx
ページにナビゲーション コントロールを追加できます。 まず Default.aspx
ページを開き、ツールボックスからデザイナーに SiteMapPath
をドラッグします。 SiteMapPath コントロールは、ツールボックスのナビゲーション セクションにあります。
図 16: SiteMapPath を Default.aspx
に追加する (クリックするとフルサイズの画像が表示されます)
SiteMapPath コントロールは階層リンクを表示し、サイト マップ内の現在のページの位置を示します。 「マスター ページとサイト ナビゲーション」のチュートリアルで、マスター ページの上部に SiteMapPath を追加しました。
少し時間を取り、ブラウザーでこのページを表示してみてください。 図 16 で追加された SiteMapPath は、既定のサイト マップ プロバイダーを使用して、Web.sitemap
からデータを取得します。 そのため、階層リンクには、右上隅の階層リンクと同様に、[Home] > [Customizing the Site Map] と表示されます。
図 17: 階層リンクは既定のサイト マップ プロバイダーを使用する (クリックするとフルサイズの画像が表示されます)
図 16 で追加した SiteMapPath を使用するには、手順 6 で作成したカスタム サイト マップ プロバイダーを使用し、その SiteMapProvider
プロパティを、Web.config
で NorthwindSiteMapProvider
に割り当てた名前の 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.aspx
、ProductsByCategory.aspx
、ProductDetails.aspx
のページには、すべて Menu コントロールが含まれます (図 20 を参照)。 ASP.NET 2.0 のナビゲーション コントロールとサイト マップ システムの詳細については、「ASP.NET 2.0 クイック スタート」の「ASP.NET 2.0 の高度なサイト ナビゲーション機能」と「サイト ナビゲーション コントロールの使用」セクションをご覧ください。
図 20: 各カテゴリと製品を一覧表示する Menu コントロール (クリックするとフルサイズの画像が表示されます)
このチュートリアルで前述したように、サイト マップ構造には、SiteMap
クラスを使用してプログラムでアクセスできます。 次のコードは、既定のプロバイダーのルート SiteMapNode
を返します。
SiteMapNode root = SiteMap.RootNode;
AspNetXmlSiteMapProvider
はアプリケーションの既定のプロバイダーであるため、上記のコードでは、Web.sitemap
で定義されているルート ノードが返されます。 既定以外のサイト マップ プロバイダーを参照するには、次のように SiteMap
クラスの Providers
プロパティを使用します。
SiteMapNode root = SiteMap.Providers["name"].RootNode;
ここで name は、カスタム サイト マップ プロバイダー (今回の Web アプリケーションの場合は Northwind) の名前です。
サイト マップ プロバイダーに固有のメンバーにアクセスするには、SiteMap.Providers["name"]
を使用してプロバイダー インスタンスを取得し、適切な型にキャストします。 たとえば、ASP.NET ページに NorthwindSiteMapProvider
の CachedDate
プロパティを表示するには、次のコードを使用します。
NorthwindSiteMapProvider customProvider =
SiteMap.Providers["Northwind"] as NorthwindSiteMapProvider;
if (customProvider != null)
{
DateTime? lastCachedDate = customProvider.CachedDate;
if (lastCachedDate != null)
LabelID.Text = "Site map cached on: " + lastCachedDate.Value.ToString();
else
LabelID.Text = "The site map is being reconstructed!";
}
Note
SQL キャッシュの依存関係機能を必ずテストしてください。 Default.aspx
、ProductsByCategory.aspx
、ProductDetails.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行をドロップしてください。