#9:ナビゲーションを最新に保つ
ASP.NETのプロバイダモデルは、動的なコンテンツをサイトマップに追加したいときにその真価を発揮します。これまでの8つのソリューションで紹介した「Web.sitemap」アプローチのメリットをそのまま活かして、プロバイダモデルを拡張し、データベースドリブンのsiteMapNodesをサイトマップの任意のセクションに簡単に挿入することができます。また、データベースコンテンツをキャッシュに保管し、データベース変更があったときだけ更新することも簡単です。最後のソリューションでは、これらの2つの課題への取り組み方を示します。
カスタムサイトマッププロバイダを構築する
カスタムサイトマッププロバイダは非常に柔軟性があり保守のしやすいソリューションを提供しますが、他にも注目すべきアプローチが2つあります。第一に、「Web.sitemap」はurl属性でquerystringパラメータをサポートしているので、単一のページを複数のsiteMapNodesで使用することができます。問題は、掲示板への投稿など、変更頻度の高いページでの「Web.sitemap」の維持がWebマスターの悩みの種になることです。
第二に、siteMapNodeはsiteMapFile属性をサポートしており、この属性を通じて別個の.sitemapファイルから自分のコンテンツを取得することができます。この手法を利用するには、データベーストリガの内側から.sitemapファイルを作成/上書きします。ただし、このアプローチでは、データベースとWebサーバーが同一のマシン上になければならないので、アプリケーションのスケーラビリティが制限されます。この手法をより適切に使うためには、手動で管理される「Web.sitemap」を使用して、コードジェネレータによって管理される.sitemapファイルからデータをプルする必要があります(Blue Inkはこの手法を使っています。詳しくは、著者紹介をご覧ください)。
とにかく、最善のアプローチはカスタムサイトマッププロバイダを作成することだと言えるでしょう。このプロセスでは、SiteMapProviderを継承する新しいクラスを作成し、それを「Web.config」ファイルに認識させ、「Web.sitemap」から参照する必要があります。「Web.sitemap」から新しいプロバイダを参照するには、siteMapNodeの最後の属性providerを使います。
<siteMapNode provider="CategoriesProvider" />
provider属性を使うことで、siteMapNodeは「Web.config」で指定されたプロバイダから自分のコンテンツを取得することができます。例に示されている値CategoriesProviderは、Northwind.CategoriesSiteMapProviderクラスを参照します。
<siteMap defaultProvider="XmlSiteMapProvider" enabled="true"> <providers> <clear /> <add name="XmlSiteMapProvider" description="Default SiteMap provider" type="System.Web.XmlSiteMapProvider" siteMapFile="Web.sitemap" securityTrimmingEnabled="true" /> <add name="CategoriesProvider" description="Displays Categories From Northwind" connectionStringName="Northwind" type="Northwind.CategoriesSiteMapProvider" /> </providers> </siteMap>
既定のサイトマッププロバイダは1つでなければなりませんが、ASP.NETは複数のサイトマッププロバイダをサポートしているので互いに参照することができます。このアプローチにより、サイトマップのセクションをコンパートメント化することも可能になります。「Web.sitemap」のエントリと「Web.config」のエントリを構成し終えたら、カスタムプロバイダを作成しなければなりません。
前述の通り、各サイトマッププロバイダは最終的にはSiteMapProviderクラスから継承しなければなりません。ただし、SiteMapProviderの部分実装として、サイトマップ情報をメモリに読み込む処理を簡易化するStaticSiteMapProviderという中継クラスが用意されています。StaticSiteMapProviderは、基本クラスSiteMapProviderに、AddNode()、RemoveNode()、Clear()、BuildSiteMap()などのメソッドを追加したものです。
カスタムプロバイダでオーバーライドする必要性が高い関数は、BuildSiteMap()と、SiteMapProviderから継承したInitialize()です。ASP.NETはアプリケーションの実行期間中にInitialize()を1回だけ呼び出すので、これは「Web.config」から構成設定を取得するのにちょうど適した場所です。反対に、BuildSiteMap()は頻繁に呼び出され、ルートのsiteMapNodeを返さなければなりません。カスタムサイトマッププロバイダでは、パフォーマンスを向上させるために、可能なときにはキャッシュ内にあるサイトマップのコピーを返すようにBuildSiteMap()を実装する必要があります。この処理の一般的な流れを疑似コードの形で紹介しておきます。
private SiteMapNode mnodeRoot = null; public override SiteMapNode BuildSiteMap() { if (site map has already been built) return mnodeRoot; Clear(); SiteMapNode nodeRoot = GetRootNode(); AddNode(nodeRoot); open a connection to Northwind select all categories foreach (category) { SiteMapNode nodeCategory = CreateCategoryNode(); AddNode(nodeCategory, nodeRoot); } }
残念ながら、前述のアプローチは2つの大きな問題の影響を受けます。1つ目の問題は、サイトマップ情報は手動で(例えば、Webサイトの保守セクションでmnodeRootをnullに設定することで)無効化しない限り変更されないことです。サイトマップ情報は、表示されるデータが変更されたら自動的に更新されるのが理想です。ここでは、SqlCacheDependencyオブジェクトを使ってこの問題を解決する方法を簡単に説明します。
2つ目の問題はもっと細かいことですが、極めて重要です。ASP.NET 2.0はSiteMapProvider(および実際にはすべてのプロバイダ)の実装にシングルトン(Singleton)パターンを使っています。つまり、ASP.NET 2.0はすべてのページ要求間で(すなわち複数のスレッド間で)カスタムサイトマッププロバイダの1つのインスタンスを共有しているわけです。カスタムサイトマッププロバイダのアプローチのメリットの1つは、メンバ変数に永続データを格納できることですが、サイトマッププロバイダの状態を変更するあらゆるコードでスレッドセーフを考慮しなければならないという短所もあります。従って、複数のスレッドが同時にサイトマップを作成しようとすることを念頭に置いて、前述のアプローチを変更しなければなりません。
private readonly object mobjLock = new object(); public override SiteMapNode BuildSiteMap() { if (site map has already been built) return mnodeRoot; lock(mobjLock) { Clear(); SiteMapNode nodeRoot = GetRootNode(); AddNode(nodeRoot); loop through data { call AddNode() } } }
このコードでは、2つのスレッドがサイトマッププロバイダの状態の更新を同時に試みることはありません。コードがlock()を呼び出すのは状態が更新されるときだけです。事前に呼び出すと、不必要にパフォーマンスが低下します。
