建置自訂的資料庫驅動網站導覽提供者 (C#)

作者:Scott Mitchell

下載 PDF

ASP.NET 2.0 中的預設網站地圖提供者會從靜態 XML 檔案擷取其數據。 雖然基於 XML 的提供者適用於許多中小型網站,但較大的 Web 應用程式需要更具動態性的網站地圖。 在本教學課程中,我們將建置自定義網站地圖提供者,從商業規則層擷取其數據,進而從資料庫擷取數據。

簡介

ASP.NET 2.0 s 網站地圖功能可讓頁面開發人員在一些持續性媒體中定義 Web 應用程式的網站地圖,例如在 XML 檔案中。 定義之後,即可透過SiteMap命名空間中的 System.Web 類別,或透過各種導覽 Web 控件,例如 SiteMapPath、Menu 和 TreeView 控件,以程式設計方式存取網站地圖數據。 網站地圖系統會使用提供者模型,以便建立不同的網站地圖串行化實作,並插入 Web 應用程式。 隨附於 ASP.NET 2.0 的默認網站地圖提供者會保存 XML 檔案中的網站地圖結構。 回到主版頁面和網站導覽教學課程中,我們建立了名為 Web.sitemap 的檔案,其中包含此結構,並且已使用每個新的教學課程區段來更新其 XML。

如果網站地圖的結構相對固定,預設的 XML 格式網站地圖提供者運作良好,例如這些教學。 不過,在許多情況下,需要更動態的網站地圖。 請考慮圖 1 中顯示的網站地圖,其中每個類別和產品都會顯示為網站結構中的區段。 透過此網站地圖,流覽對應至根節點的網頁可能會列出所有類別,而流覽特定類別的網頁會列出該類別的產品,並檢視特定產品的網頁會顯示該產品的詳細數據。

構成網站地圖結構的類別和產品

圖 1:構成網站地圖結構的類別和產品 (按兩下以檢視完整大小的影像)

雖然這個類別和產品型結構可以硬式編碼到檔案中 Web.sitemap ,但每次新增、移除或重新命名類別或產品時,都必須更新檔案。 因此,如果網站地圖維護結構是從資料庫擷取,或者最好是從應用程式架構的商業規則層擷取,則會大幅簡化網站地圖維護。 如此一來,當新增、重新命名或刪除產品與類別時,網站地圖會自動更新以反映這些變更。

由於 ASP.NET 2.0 s 網站地圖串行化建置在提供者模型之上,因此我們可以建立自己的自定義網站地圖提供者,從替代數據存放區擷取其數據,例如資料庫或架構。 在本教學課程中,我們將建置自定義提供者,以從 BLL 擷取其數據。 現在就開始吧!

注意

本教學課程中建立的自定義網站地圖提供者與應用程式的架構和數據模型緊密結合。 Jeff Prosise的Storing Site Maps in SQL Server和The SQL Site Map Provider You’ve Been Waiting For文章探討了在SQL Server中儲存網站地圖資料的通用方法。

步驟 1:建立自定義網站地圖提供者網頁

開始建立自定義網站地圖提供者之前,讓我們先新增本教學課程所需的 ASP.NET 頁面。 首先新增一個名為 SiteMapProvider 的新資料夾。 接下來,將以下 ASP.NET 頁面新增至該資料夾,確保將每個頁面與 Site.master 母版頁相關聯:

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

同時將 CustomProviders 子資料夾新增至 App_Code 資料夾。

新增網站地圖提供者相關教學課程的 ASP.NET 頁面

圖 2:新增網站地圖提供者相關教學課程的 ASP.NET 頁面

由於本節只有一個教學課程,因此我們不需要 Default.aspx 列出區段的教學課程。 相反地, Default.aspx 會在 GridView 控件中顯示類別。 我們將在步驟 2 中解決此問題。

接下來,更新 Web.sitemap 以包括對 Default.aspx 頁面的參考。 具體來講,在快取 <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 後,花點時間透過瀏覽器查看教學網站。 左側功能表現在包含唯一網站地圖提供者教程的項目。

網站地圖現在包括網站地圖提供者教程的條目

圖 3:網站地圖現在包含網站地圖提供者教學課程的條目

本教學課程的主要重點是說明如何建立自定義網站地圖提供者,以及設定 Web 應用程式以使用該提供者。 具體來說,我們將建置一個提供者,來傳回網站地圖,其中包含根節點以及每個類別和產品的節點,如圖 1 所示。 一般而言,網站地圖中的每個節點可以指定URL。 針對我們的網站地圖,根節點的 URL 將會是 ~/SiteMapProvider/Default.aspx,這會列出資料庫中的所有類別。 網站地圖中的每個類別節點都會有指向 的 URL ~/SiteMapProvider/ProductsByCategory.aspx?CategoryID=categoryID,這會列出指定 categoryID 中的所有產品。 最後,每個產品網站地圖節點都會指向 ~/SiteMapProvider/ProductDetails.aspx?ProductID=productID,這會顯示特定產品的詳細數據。

若要開始,我們需要建立 Default.aspx、 ProductsByCategory.aspx和 ProductDetails.aspx 頁面。 這些頁面分別在步驟 2、3 和 4 中完成。 由於本教學課程的推力位於網站地圖提供者上,而且因為過去的教學課程涵蓋如何建立這類多頁主版/詳細數據報告,因此我們會匆忙完成步驟 2 到 4。 如果您需要複習如何建立跨越多個頁面的主/從報表,請參閱跨兩頁的主/從篩選教學課程。

步驟 2:顯示類別清單

請在 SiteMapProvider 資料夾中開啟 Default.aspx 頁面,然後從 [工具箱] 將 GridView 拖曳至設計工具,並將其 ID 設定為 Categories。 從 GridView 的智慧標記,將它系結至名為 CategoriesDataSource 的新 ObjectDataSource,並加以設定,讓它使用 CategoriesBLL 類別 s GetCategories 方法來擷取其數據。 由於此 GridView 只會顯示類別,且未提供資料修改功能,因此請將 UPDATE、INSERT 和 DELETE 索引卷標的下拉式清單設定為 [無]。

使用 GetCategories 方法將 ObjectDataSource 設定為傳回類別

圖 4:使用 GetCategories 方法設定 ObjectDataSource 以傳回類別 (按兩下以檢視完整大小的影像)

將 UPDATE、INSERT 和 DELETE 標籤中的下拉清單設為(無)

圖 5:將 UPDATE、INSERT 和 DELETE 索引標籤標的下拉式清單設定為 [無] (按兩下以檢視完整大小的影像)

完成 [設定數據源精靈] 之後,Visual Studio 會新增 CategoryID、CategoryName、Description、NumberOfProducts 和 BrochurePath 的 BoundField。 編輯 GridView,使其只包含 CategoryName 和 Description BoundFields,並將 BoundField s CategoryName 屬性更新HeaderText為 Category 。

接下來,新增 HyperLinkField 並將它定位為最左邊的字段。 將 DataNavigateUrlFields 屬性設定為 CategoryID,並將 DataNavigateUrlFormatString 屬性設定為 ~/SiteMapProvider/ProductsByCategory.aspx?CategoryID={0}。 將 Text 屬性設定為 [檢視產品]。

將 HyperLinkField 新增至 Categories GridView

圖 6:將 HyperLinkField 新增至 Categories GridView

建立 ObjectDataSource 並自定義 GridView 字段之後,兩個控件宣告式標記看起來會如下所示:

<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 在瀏覽器中檢視的情況。 點擊類別的 [查看產品] 連結會帶您前往 ProductsByCategory.aspx?CategoryID=categoryID,我們將在步驟 3 中建置。

每個類別都會與檢視產品連結一起列出

圖 7:每個類別都會與檢視產品連結一起列出(按兩下以檢視完整大小的影像)

步驟 3:列出選取類別的產品

ProductsByCategory.aspx開啟頁面並新增 GridView,並將它命名為 ProductsByCategory。 從其智能標記中,將 GridView 系結至名為 ProductsByCategoryDataSource的新 ObjectDataSource。 將 ObjectDataSource 設定為使用 ProductsBLL 類別 s GetProductsByCategoryID(categoryID) 方法,並將 UPDATE、INSERT 和 DELETE 索引標籤中的下拉式清單設定為 [無]。

使用 ProductsBLL 類別 s GetProductsByCategoryID(categoryID) 方法

圖 8:使用 ProductsBLL 類別 s GetProductsByCategoryID(categoryID) 方法(按兩下以檢視完整大小的影像)

設定數據源精靈中的最後一個步驟會提示輸入 categoryID 的參數來源。 由於這項資訊會透過 querystring 字段 CategoryID傳遞,請從下拉式清單中選取 [QueryString],然後在 [QueryStringField] 文本框中輸入 CategoryID,如圖 9 所示。 按一下 [完成] 以完成精靈。

使用查詢字串欄位 CategoryID 作為 categoryID 參數

圖 9:使用 CategoryID 欄位 作為 categoryID 參數值 (點擊以查看完整大小的影像)

完成精靈之後,Visual Studio 會將對應的 BoundFields 和 CheckBoxField 新增至 Product 數據欄位的 GridView。 刪除 ProductName、UnitPrice 和 SupplierName BoundField 之外的所有內容。 自定義這三個 BoundFields HeaderText 屬性,分別讀取 Product、Price 和 Supplier。 將 UnitPrice BoundField 格式化為貨幣。

接下來,新增 HyperLinkField 並將它移至最左邊的位置。 將屬性 Text 設定為 [檢視詳細資料],將其 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>

返回瀏覽器檢視,然後在飲料的 [檢視產品] 連結上點擊。 這會帶您前往 ProductsByCategory.aspx?CategoryID=1,顯示屬於飲料類別之 Northwind 資料庫中產品的名稱、價格和供應商(請參閱圖 11)。 您可以進一步增強此頁面,以包含連結,以將使用者傳回類別清單頁面 (Default.aspx) 和顯示所選類別名稱和描述的 DetailsView 或 FormView 控制件。

[飲料名稱]、[價格] 和 [供貨商] 會顯示

圖 11:顯示飲料名稱、價格和供應商(按兩下以檢視全尺寸影像)

步驟 4:顯示產品詳細數據

最後一頁 ProductDetails.aspx會顯示選取的產品詳細數據。 開啟 ProductDetails.aspx 並將DetailsView 從工具箱拖曳至設計工具。 將 DetailsView 的 ID 屬性設定為 ProductInfo ,並清除其 Height 和 Width 屬性值。 從其智慧標記,將 DetailsView 系結至名為 ProductDataSource的新 ObjectDataSource,設定 ObjectDataSource 從 ProductsBLL 類別 s GetProductByProductID(productID) 方法提取其數據。 如同在步驟 2 和 3 中建立的前一個網頁,請將 UPDATE、INSERT 和 DELETE 索引卷標的下拉式清單設定為 [無]。

設定 ObjectDataSource 以使用 GetProductByProductID(productID) 方法

圖 12:將 ObjectDataSource 設定為使用 GetProductByProductID(productID) 方法 (按兩下以檢視完整大小的影像)

[設定數據源精靈] 的最後一個步驟會提示 productID 參數的來源。 由於此數據是透過 querystring 欄位 ProductID,請將下拉式清單設定為 QueryString,並將 QueryStringField 文字框設定為 ProductID。 最後,按兩下 [完成] 按鈕以完成精靈。

設定 productID 參數,從 ProductID 查詢字串字段提取其值

圖 13:設定 productID 參數,以從 Querystring 欄位提取其值(點擊以查看完整尺寸圖像)

完成 [設定數據源精靈] 之後,Visual Studio 會在產品數據欄位的 DetailsView 中建立對應的 BoundFields 和 CheckBoxField。 移除ProductID、SupplierID和CategoryID這些BoundFields,並依需求設定其他欄位。 在少數審美設定之後,我的 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 然後按兩下 [檢視飲料] 類別的產品。 從飲料產品清單中,按下柴茶的 [檢視詳細數據] 連結。 這會帶您前往 ProductDetails.aspx?ProductID=1,其中顯示柴茶的詳細數據(請參閱圖 14)。

柴茶供應商、類別、價格及其他信息隨即顯示

圖 14:柴茶供貨商、類別、價格及其他資訊隨即顯示(按兩下以檢視全尺寸影像)

步驟 5:了解網站地圖提供者的內部工作

網站地圖會在網頁伺服器記憶體中表示為構成階層的 SiteMapNode 實例集合。 必須只有一個根,所有非根節點都必須只有一個父節點,而且所有節點可能都有任意數目的子節點。 每個 SiteMapNode 物件都代表網站結構中的區段;這些區段通常有對應的網頁。 因此,類別SiteMapNode具有、Title、 和 Url等Description屬性,可提供 所表示區段SiteMapNode的資訊。 也有一個屬性可唯一識別SiteMapNode階層中的每個SiteMapNode,以及用來建立此階層ChildNodes、ParentNode、NextSibling、PreviousSibling等等的屬性。

圖 15 顯示圖 1 中的網站地圖結構,但以更精細的方式說明了實作細節。

每個 SiteMapNode 都有標題、URL、金鑰等屬性

圖 15:每個都有 SiteMapNode 屬性,例如 Title、 Url、 Key等 (按兩下以檢視完整大小的影像)

網站地圖可透過 SiteMap 命名空間中的 System.Web 類別存取。 這個類別的RootNode屬性會傳回網站地圖的根SiteMapNode實例;CurrentNode傳回SiteMapNode,其Url屬性符合目前請求頁面的URL。 ASP.NET 2.0 s 導覽 Web 控件會在內部使用此類別。

當類別 SiteMap 的屬性被存取時,它必須將網站地圖結構從某些永續性媒體序列化到記憶體中。 不過,網站地圖序列化邏輯沒有硬編碼到 SiteMap 類別中。 相反地,在運行時,類別 SiteMap 決定使用哪個網站地圖 提供者 來進行串行化。 根據預設,會使用 類別XmlSiteMapProvider,它會從格式正確的 XML 檔案讀取網站地圖結構。 不過,透過稍作修改,我們可以建立自己的客製化網站地圖提供者。

所有網站地圖提供者都必須衍生自 SiteMapProvider 類別,其中包含網站地圖提供者所需的基本方法和屬性,但省略許多實作詳細數據。 第二個類別StaticSiteMapProvider繼承了SiteMapProvider類別,並提供了更健全的必要功能實作。 在內部,StaticSiteMapProvider將網站地圖的SiteMapNode實例儲存在Hashtable中,並提供AddNode(child, parent)、RemoveNode(siteMapNode),和Clear()等方法,還將SiteMapNode新增和移除到內部Hashtable。 XmlSiteMapProvider 衍生自 StaticSiteMapProvider。

建立延伸 StaticSiteMapProvider的自訂網站地圖提供者時,必須覆寫兩個抽象方法: BuildSiteMap 和 GetRootNodeCore。 BuildSiteMap,如其名稱所示,負責從永久儲存裝置載入網站地圖結構,並在記憶體中構建。 GetRootNodeCore 會傳回網站地圖中的根節點。

在 Web 應用程式可以使用網站地圖提供者之前,必須先在應用程式組態中註冊該提供者。 根據預設,類別 XmlSiteMapProvider 會使用名稱 AspNetXmlSiteMapProvider註冊。 若要註冊其他網站地圖提供者,請將下列標記新增至 Web.config:

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

名稱值會將人類可讀取的名稱指派給提供者,而類型則指定網站地圖提供者的完整類型名稱。 在建立自訂網站地圖提供者之後,我們將在步驟 7 中探索名稱和類型值的具體數值。

網站地圖提供者類別在第一次從 SiteMap 類別存取時會被實例化,並在 web 應用程式的存留期內保留在記憶體中。 由於網站地圖提供者只有一個實例,可能會被多個並行的網站訪客呼叫,因此提供者的方法必須是 執行緒安全的。

基於效能和擴充性考量,必須將網站地圖結構快取於記憶體中,並傳回此快取的結構,而非每次呼叫 BuildSiteMap 方法時重新建立。 BuildSiteMap 的呼叫次數可能因每位使用者的頁面請求有所不同,這取決於頁面上使用的導覽控件和網站地圖結構的深度。 無論如何,如果我們不快取網站地圖結構,則每次呼叫BuildSiteMap時,我們便需從架構系統重新擷取產品和類別資訊(這會導致查詢資料庫)。 如先前快取教學課程中所討論,快取的數據可能會過時。 為了解決這個問題,我們可以使用基於時間或 SQL 快取相依性的到期機制。

注意

網站地圖提供者可以選擇性地覆寫 Initialize 方法。 Initialize 首次實體化網站地圖提供者時,會被呼叫,並將傳遞在Web.config的<add>元素中指派給提供者的任何自定義屬性,例如:<add name="name" type="type" customAttribute="value" />。 如果您想要允許頁面開發人員指定各種網站地圖提供者相關設定,而不需要修改提供者的程式代碼,它很有用。 例如,如果我們直接從資料庫讀取類別和產品數據,而不是透過架構,我們可能會想要讓頁面開發人員指定資料庫 連接字串Web.config,而不是使用提供者程序代碼中的硬式編碼值。 我們將在步驟 6 中建置的自訂網站地圖提供程式不會覆寫此方法 Initialize 。 如需使用 Initialize 方法的範例,請參閱 Jeff Prosise s Storing Site Maps in SQL Server 一文。

步驟 6:建立自訂網站地圖提供者

若要建立自定義網站地圖提供者,以從 Northwind 資料庫中的類別和產品建置網站地圖,我們需要建立擴充 的 StaticSiteMapProvider類別。 在步驟 1 中,我要求您在 CustomProviders 資料夾中新增App_Code資料夾 - 將新的類別新增至名為 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 一次只允許一個執行緒進入,藉此串行化其程式碼的存取,並防止兩個並行執行緒互相干擾。

類別層級 SiteMapNode 變數 root 可用來快取網站地圖結構。 當網站地圖第一次被建構,或在基礎數據被修改後的第一次,root 將會 null,並建構網站地圖結構。 網站地圖的根節點會在建構程式期間指派給 root ,因此下次呼叫此方法時, root 將不會是 null。 因此,只要 root 不是 null 網站地圖結構就會傳回給呼叫者,而不需要重新建立它。

如果 root 為 null,則會從產品和類別資訊建立網站地圖結構。 網站地圖是藉由建立 SiteMapNode 實例,然後透過對類別StaticSiteMapProvider 的 AddNode 方法的呼叫來形成階層來建置。 AddNode 會執行內部記帳,將各種 SiteMapNode 實例儲存在 中 Hashtable。 在開始建構階層之前,我們會先呼叫 Clear 方法,清除內部 Hashtable 中的元素。 接下來, ProductsBLL 類別 s GetProducts 方法和產生的 ProductsDataTable 會儲存在局部變數中。

網站地圖的建構從建立根節點並將其指派給 root開始。 在此和整個過程中SiteMapNode所使用的 s 建構函式多載BuildSiteMap會傳遞下列資訊:

  • 網站地圖提供者的參考(this)。
  • s SiteMapNodeKey。 此必要值對於每個SiteMapNode都必須是唯一的。
  • SiteMapNodesUrl。 Url 是選擇性的,但如果提供,則每個 SiteMapNode 值都必須是唯一的 Url 。
  • 需要SiteMapNode的Title。

方法呼叫會將SiteMapNoderoot作為根節點新增到網站地圖中。 接下來,會列舉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。

注意

具有資料庫NULL值的CategoryID這些產品會被分組在一個類別SiteMapNode下,此類別的Title屬性被設置為 None,並且Url屬性設置為空字串。 我決定將 設為Url空字串,因為 ProductBLL 類別的 GetProductsByCategory(categoryID) 方法目前缺少只傳回具有NULLCategoryID值之產品的功能。 此外,我想要示範導航控制如何顯示缺少 Url 屬性值的 SiteMapNode。 我鼓勵您擴充本教學課程,讓 None SiteMapNodeUrl 屬性指向 ProductsByCategory.aspx,但只會顯示具有NULLCategoryID值的產品。

建構網站地圖之後,透過 AggregateCacheDependency 物件,以 Categories 和 Products 表的 SQL 快取相依性,將任意物件新增至數據快取。 我們在上一個教學課程中探索了使用 SQL 快取相依性, 使用 SQL 快取相依性。 不過,自定義網站地圖提供者會使用我們尚未探索的數據快取方法 Insert 重載。 此多載的最後一個輸入參數是當物件從快取中移除時所呼叫的委派。 具體而言,我們會傳入新的 CacheItemRemovedCallback 委派 ,指向 OnSiteMapChanged 類別中 NorthwindSiteMapProvider 進一步定義的方法。

注意

網站地圖的在記憶體中表示會透過類別層級變數 root 進行快取。 由於自定義網站地圖提供者類別只有一個實例,而且因為該實例會在 Web 應用程式中的所有線程之間共用,因此這個類別變數會做為快取。 BuildSiteMap方法也會使用數據快取,但只是作為一種手段,用於在Categories或Products數據表中的基礎資料庫數據變更時接收通知。 請注意,放入數據快取中的值只是目前的日期和時間。 實際的網站地圖數據 不會 放入數據快取中。

方法 BuildSiteMap 會藉由傳回網站地圖的根節點來完成。

其餘的方法相當簡單。 GetRootNodeCore 負責傳回根節點。 由於BuildSiteMap傳回根目錄,GetRootNodeCore直接傳回BuildSiteMap的值。 方法會在 OnSiteMapChanged 移除快取項目時設定 root 回 null 。 將根設定回 null,當下次呼叫 BuildSiteMap 時,網站地圖結構將會被重建。 最後,如果存在這類值,屬性 CachedDate 會傳回儲存在數據快取中的日期和時間值。 頁面開發人員可以使用此屬性來判斷上次快取網站地圖數據的時間。

步驟 7:註冊NorthwindSiteMapProvider

為了讓我們的 Web 應用程式使用在步驟 6 中建立的 NorthwindSiteMapProvider 網站地圖提供者,我們需要將其註冊在 Web.config 的 <siteMap> 區段中。 具體來說,將下列標記新增到Web.config的<system.web>元素中:

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

此標記會執行兩件事:第一,表示內建的 AspNetXmlSiteMapProvider 作為預設的網站地圖提供者,第二,將步驟 6 中建立的自定義網站地圖提供者以人性化的名稱 Northwind 註冊。

注意

對於位於應用程式 s App_Code 資料夾中的網站地圖提供者,屬性的值 type 只是類別名稱。 或者,自定義網站地圖提供者可能已在個別的類別庫專案中建立,而編譯的元件會放在 Web 應用程式 s /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 控制件位於 [工具箱] 的 [導覽] 區段中。

將 SiteMapPath 新增至 Default.aspx

圖 16:將 SiteMapPath 新增至 Default.aspx (按兩下以檢視完整大小的影像)

SiteMapPath 控制項會顯示導覽列,指出網站地圖中目前頁面的所在位置。 我們早在主版頁面與網站導覽教程中,就將 SiteMapPath 新增至主版頁面的頂端。

請花點時間透過瀏覽器查看此頁面。 圖 16 中新增的 SiteMapPath 會使用預設的網站地圖提供者,從 Web.sitemap提取其數據。 因此,階層鏈接會顯示首頁 > 自定義網站地圖,就像右上角的階層連結一樣。

導覽列使用預設的網站地圖提供者

圖 17:麵包屑導航使用預設網站地圖提供者(點擊以查看完整大小影像)

若要在圖 16 中新增 SiteMapPath,請使用我們在步驟 6 中建立的自定義網站地圖提供者,將其 SiteMapProvider 屬性 設定為 Northwind,也就是我們指派給 NorthwindSiteMapProvider 中的 Web.config名稱。 不幸的是,設計師會繼續使用預設的網站地圖提供者,但是如果您在進行此屬性變更之後瀏覽頁面,您會看到階層連結現在使用自定義網站地圖提供者。

顯示如何在階層式導覽中呈現自訂網站地圖提供者的螢幕擷取畫面。

圖 18:階層連結現在使用自訂網站地圖提供者 NorthwindSiteMapProvider (按一下以檢視完整大小的影像)

SiteMapPath 控制件會在 ProductsByCategory.aspx 和 ProductDetails.aspx 頁面中顯示功能更實用的使用者介面。 將 SiteMapPath 新增至這些頁面,將 兩者中的 屬性設定 SiteMapProvider 為 Northwind。 從 Default.aspx 單擊 [檢視飲料的產品] 鏈接,然後在 Chai Tea 的 [檢視詳細數據] 連結上。 如圖 19 所示,階層連結包括目前的網站地圖區段(柴茶)及其祖先:飲料和所有類別。

顯示麵包屑如何顯示網站地圖當前區段(柴茶)及其上級(飲料和所有類別)的螢幕快照。

圖 19:麵包屑現在使用自訂網站地圖提供者 NorthwindSiteMapProvider (按一下以檢視完整大小的影像)

除了 SiteMapPath 之外,還可以使用其他導覽使用者介面元素,例如 Menu 和 TreeView 控制件。 本教學課程下載中的 Default.aspx、ProductsByCategory.aspx 和 ProductDetails.aspx 頁面都包含選單控制項(請參閱圖 20)。 請參閱 ASP.NET 2.0 的複雜網站導覽功能和 ASP.NET 2.0 快速入門中的使用網站導覽控件一節,以深入瞭解 ASP.NET 2.0 中的流覽控件和網站地圖系統。

功能表控制項會列出每個類別和產品

圖 20:功能表控制項會列出每個類別和產品 (單擊以檢視完整大小的影像)

如本教學課程稍早所述,網站地圖結構可以透過 SiteMap 類別以程式設計方式存取。 下列程式代碼會傳回預設提供者的根 SiteMapNode 目錄:

SiteMapNode root = SiteMap.RootNode;

AspNetXmlSiteMapProvider由於是應用程式的預設提供者,因此上述程式代碼會傳回 中Web.sitemap定義的根節點。 若要參考預設以外的網站地圖提供者,請使用 SiteMap 類別的 Providers 屬性 ,如下所示:

SiteMapNode root = SiteMap.Providers["name"].RootNode;

其中 name 是自訂網站地圖提供者的名稱(Northwind,適用於我們的 Web 應用程式)。

若要存取網站地圖提供者特定的成員,請使用 SiteMap.Providers["name"] 來擷取提供者實例,然後將它轉換成適當的類型。 例如,若要在 ASP.NET 頁面中顯示 NorthwindSiteMapProvider s 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!";
}

注意

請務必測試 SQL 快取相依性功能。 流覽 Default.aspx、 ProductsByCategory.aspx和 ProductDetails.aspx 頁面之後,請移至 [編輯]、[插入] 和 [刪除] 區段中的其中一個教學課程,並編輯類別或產品的名稱。 然後返回資料夾中的其中一個頁面 SiteMapProvider 。 假設輪詢機制已經過足夠的時間來記下基礎資料庫的變更,則網站地圖應該更新以顯示新的產品或類別名稱。

摘要

ASP.NET 2.0 s 網站地圖功能包括類別 SiteMap 、許多內建導覽 Web 控制件,以及預期網站地圖資訊保存到 XML 檔案的預設網站地圖提供者。 若要使用來自其他來源的網站地圖資訊,例如從資料庫、應用程式架構或遠端 Web 服務,我們需要建立自定義網站地圖提供者。 這牽涉到建立從 SiteMapProvider 類別直接或間接衍生出來的類別。

在本教學課程中,我們已瞭解如何建立自定義網站地圖提供者,以從應用程式架構中擷取的產品和類別資訊為基礎的網站地圖。 我們的提供者擴充 類別 StaticSiteMapProvider ,並需要建立 BuildSiteMap 方法來擷取數據、建構網站地圖階層,並在類別層級變數中快取產生的結構。 我們搭配回呼函式使用 SQL 快取相依性,在修改基礎 Categories 或 Products 數據時使快取結構失效。

快樂程式!

深入閱讀

有關本教學課程中討論的主題的更多資訊,請參閱以下資源:

關於作者

斯科特·米切爾,七本 ASP/ASP.NET 書籍和 4GuysFromRolla.com 創始人的作者,自1998年以來一直與Microsoft Web 技術合作。 Scott 擔任獨立顧問、講師和作家。 他的新書是 Sams Teach Yourself ASP.NET 2.0 in 24 Hours。 可以透過 mitchell@4GuysFromRolla.com 聯絡他。

特別感謝

本教學課程系列已經過許多熱心的檢閱者檢閱。 本教學課程的主要檢閱者是戴夫·加德納、紮克·鐘斯、特蕾莎·墨菲和伯納黛特·利。 有興趣檢閱我即將推出的 MSDN 文章嗎? 如果是,請在 mitchell@4GuysFromRolla.com給我留言。