在 ASP.NET 2.0 中构建和使用动态站点地图






3.87/5 (13投票s)
您需要直接从数据集构建动态站点地图,因为您的网站没有静态内容。
引言
站点地图和面包屑(SiteMapPath)非常棒。对于动态网站来说,它们至关重要,因为搜索引擎历来对动态网站中常见的查询字符串驱动内容并不敏感。站点地图可以帮助搜索引擎找到这些内容并对其进行适当的索引,为用户提供面包屑导航,并让用户在一个地方看到您提供给他们的所有内容。
ASP.NET 2.0 的站点地图内置功能非常出色。许多先前与之相关的复杂性都已得到解决;然而,在为我们的系统实现一个站点地图时,我们遇到了一些微软并未解决的挑战。
如果您有以下任何与我相同的动机,本文中的代码和方法将对您有所帮助:
- 您正在使用分层数据集来生成用于动态内容的树,该树将用于填充站点地图。
- 您希望以一种快速且代码行数最少的方式,直接从数据集中生成 ASP.NET 站点地图数据源所需的 XML。
- 您的内容管理系统足够庞大,不支持在每次 Web 请求时都生成此数据集(请记住,它会为您的所有页面提供面包屑导航),或者将这个庞大的对象(代表您系统中的所有内容)填充到服务器缓存中——因此您想要一个每天(或您定义的频率)更新的静态 XML 站点地图文件。
- 您希望备份您的 XML 站点地图文件以进行维护或仅出于安全考虑。
- 您的动态 CMS 中有多个内容部分,它们是不同的树——这意味着您的页面是上下文敏感的,并且在同一个 Web 应用程序中会从单独的站点地图中提取内容。
- 您想了解如何在 .NET 2.0 中实现内存中的 XSLT。
- 您想了解如何在 .NET 2.0 中使用嵌入式资源文件。
- 您只是想看看能够将分层数据集转换为 `*.sitemap` 文件的 XSLT。
- 您想了解如何处理站点地图中重复的 URL(这是 ASP.NET 内置 SiteMapProvider 所禁止的)。
步骤 1:确定您的命名约定
首先,您必须定义 XML 文件的命名约定。由于这是一个自动化过程,您希望屏蔽客户端,让他们不必考虑其内部工作原理。在我的例子中,我需要三个组件来唯一标识我的站点地图:
- `ApplicationID`(相关 Web 应用程序的 ID)
- `CultureName`(当前内容的区域性名称,例如 en-US 或 en-GB)
- `SitemapType`(唯一标识内容组以进行分离的内容树)
我的 Sitemap 类中有以下字段:
#region Private Field Declarations
private Guid applicationId;
private string cultureName;
private DataSet dataSource;
private byte maxTiers;
private SitemapType type;
private string Id;
private string fileName;
//Can be modified for your data structure -
// recommend repeating the construct for Tables.Count - 1
private object[,] heirarchicalRelations = new object[,] { { new string[] { "ID" },
new string[] { "ParentID" } }, { new string[] { "ID" },
new string[] { "ParentID" } }, { new string[] { "ID" },
new string[] { "ParentID" } }, { new string[] { "ID" },
new string[] { "ParentID" } } };
#endregion
枚举
/// <summary>
/// This distinguishing property allows you to define multiple content areas within one
/// web application. Then you can call out the appropriate SiteMapProvider for different
/// path contexts in a single web application.
/// </summary>
public enum SitemapType
{
/// <summary>
/// A sitemap that lists all of the informational content in the system
/// </summary>
Content = 0,
/// <summary>
/// A sitemap that lists all of the properties and areas in the system
/// </summary>
Property = 1,
}
构造函数会构建 ID 和文件名(文件名中不需要应用程序 ID,因为文件存储在各自应用程序的目录中)。我们在软件中使用驼峰式命名约定来命名私有字段,并使用帕斯卡式命名约定来命名公共属性。我省略了公共属性,但您可以看到下面的内容,它们与私有字段相对应。`ToString("G")` 格式化为枚举的名称。
/// <summary>
/// Generates a Sitemap from its Type, application ID, and requested culture
/// </summary>
/// <param name="appId">The unique ID for the application that the sitemap belongs to</param>
/// <param name="cName">Defines the culture that the client would like
/// to view the sitemap in. The application must support that culture.
/// The standard abbreviation string must be used.</param>
/// <param name="sType">The sitemap type that should be generated</param>
public CMSSitemap(Guid appId, string cName, SitemapType sType)
{
this.ApplicationID = appId;
this.CultureName = cName;
this.Type = sType;
StringBuilder pathBuilder = new StringBuilder(this.Type.ToString("G"));
pathBuilder.Append("_");
pathBuilder.Append(this.CultureName);
pathBuilder.Append(".sitemap");
this.FileName = pathBuilder.ToString();
StringBuilder IdBuilder =
new StringBuilder(this.ApplicationID.ToString());
IdBuilder.Append("_");
IdBuilder.Append(this.Type.ToString("G"));
IdBuilder.Append("_");
IdBuilder.Append(this.CultureName);
this.ID = IdBuilder.ToString();
BSData helper = new BSData();
SqlParameter p_applicationId = new SqlParameter("@ApplicationID",
SqlDbType.UniqueIdentifier, 16,
ParameterDirection.Input, false, ((Byte)(18)),
((Byte)(0)), "", DataRowVersion.Current, appId);
SqlParameter p_cultureName = new SqlParameter("@CultureName",
SqlDbType.VarChar, 10, ParameterDirection.Input,
false, ((Byte)(18)), ((Byte)(0)), "",
DataRowVersion.Current, cName);
SqlParameter p_sitemapType = new SqlParameter("@SitemapType",
SqlDbType.TinyInt, 3, ParameterDirection.Input, false,
((Byte)(18)), ((Byte)(0)), "",
DataRowVersion.Current, sType);
SqlParameter[] param =
new SqlParameter[3] { p_applicationId, p_cultureName, p_sitemapType };
this.DataSource = helper.ReadOnlyHeirarchicalQuery(
helper.BWEnterpriseReader,
"dbo.[proc_getSitemapData]",
this.heirarchicalRelations, param);
this.modifyDuplicateUrls();
this.MaxTiers = (byte)this.DataSource.Tables.Count;
}
步骤 2:处理您的分层数据
关于我们的数据层(您很可能拥有自己的方法,因此填充数据集由您决定)。我们专门使用存储过程作为数据层的基础。它们通过 ADO.NET 辅助方法调用,其中数据库连接被隔离和封装。它们在构建数据集后将数据集过滤回业务层。上面的方法“`ReadOnlyHeirarchicalQuery`”将构建一个包含 `DataRelations` 的分层数据集。
私有字段 `heiarchicalRelations` 目前存在争议。为了允许客户端调用该方法并定义多个父子列,我使用了一个 `object[,]`(您可以拥有多个列的 PK 和 FK)。它的缺点是要求调用者知道有多少层级会返回以形成 `DataRelations`。如果我将数据集限制为仅具有同名的父 ID 和子 ID 列,我就可以获得一个更受限制但更优雅的解决方案,因为我可以根据从 SQL 返回的 `DataTable` 的数量动态生成 `DataRelations`。在这个新场景中,调用者不需要知道必须构建多少 `DataRelations`。在发布之前,我会这样做,因为我们的层级目前正在波动。
我知道您可以管理自己的分层数据集及其方法,但我提供了这些信息作为我工作的概念证明。
来自微软会议的有用事实 - 我上面没有给出存储过程的真实名称,但请注意我用 `proc_` 前缀。您不应使用 `sp_` 前缀您的存储过程,因为这样做会减慢您的整个数据层,因为 SQL 将搜索所有系统存储过程,然后才能找到您的存储过程。
供您参考,我在构造函数中调用的方法如下所示:
/// <summary>
/// Allows for execution of multiple-select statements with one sproc,
/// and heirarchical datasets with auto-creation of relations
/// </summary>
/// <param name="comPathString">Connection string to the DB</param>
/// <param name="sprocName">Name of sproc to execute</param>
/// <param name="dataRelations">Each object must be a string[],
/// with column 0 of the multi-dimensional object[] being the string[]
/// of Parent Column Names, and column 1 being the string[] of Child Column Names.
/// This construct is required to support multi-column PK and FKs</param>
/// <param name="paramList">Parameter array for the stored procedure</param>
/// <returns>Heirarchical DataSet</returns>
internal DataSet ReadOnlyHeirarchicalQuery(string comPathString,
string sprocName, object[,] dataRelations, SqlParameter[] paramList)
{
SqlConnection comPath = new SqlConnection(comPathString);
SqlCommand executeSproc = new SqlCommand(sprocName,comPath);
executeSproc.CommandType = CommandType.StoredProcedure;
foreach (SqlParameter p in paramList)
{
executeSproc.Parameters.Add(p);
}
DataSet finalResultSet = new DataSet("finalResultsDS");
try
{
comPath.Open();
SqlDataReader readSprocResults = executeSproc.ExecuteReader();
do
{
finalResultSet.Tables.Add();
foreach(DataRow r in readSprocResults.GetSchemaTable().Rows)
{
finalResultSet.Tables[finalResultSet.Tables.Count-1].Columns.Add(
r[0].ToString(),Type.GetType(r[12].ToString()));
}
while (readSprocResults.Read())
{
addRow = finalResultSet.Tables[finalResultSet.Tables.Count-1].NewRow();
foreach (DataColumn c in
finalResultSet.Tables[finalResultSet.Tables.Count-1].Columns)
{
addRow[c.Ordinal] = readSprocResults[c.Ordinal];
}
finalResultSet.Tables[finalResultSet.Tables.Count-1].Rows.Add(addRow);
}
}
while (readSprocResults.NextResult());
comPath.Close();
for (int i=0;i<=dataRelations.GetUpperBound(0);i++)
{
string[] parents = (string[])dataRelations[i,0];
string[] children = (string[])dataRelations[i,1];
DataColumn[] pc = new DataColumn[parents.Length];
DataColumn[] cc = new DataColumn[parents.Length];
for (int j=0;j<parents.Length;j++)
{
pc[j] = finalResultSet.Tables[i].Columns[parents[j]];
}
for (int j=0;j<children.Length;j++)
{
cc[j] = finalResultSet.Tables[i+1].Columns[children[j]];
}
DataRelation tempDR = new DataRelation("", pc, cc);
tempDR.Nested = true;
finalResultSet.Relations.Add(tempDR);
}
}
catch (Exception e)
{
comPath.Close();
throw new ApplicationException(e.Message);
}
return finalResultSet;
}
如果您正在处理自己的数据集,这里唯一需要注意的是——数据集的名称是 `finalResultsDS`。请记住这一点,用于 XSLT。
步骤 3:处理重复的 URL
初始化类数据之前的最后一步是 `this.modifyDuplicateUrls();`。
我的站点层级存在一个问题——业务用户有时希望能够将内容分类到多个地方。这也会填充菜单系统。您可以通过另一种方式处理——在生成数据集时,只为内容选择一个路径。在我的例子中,这不是一个选项。用户希望看到内容显示在他们定义的层级中。
为什么这会成为一个问题?想想 .NET 框架中的 SiteMap 类必须做什么来生成面包屑导航——`HttpRequest` 定义了一个要命中的 URL,现在您的 `SiteMapProvider` 必须弄清楚,“嗯,这个 URL 在层级中的哪个位置对应?” 如果有不止一个选项,它怎么知道您指的是哪个路径?然后,当您的路径不相互排斥时,它如何生成面包屑导航?没错。它做不到。您需要编写自己的 `SiteMapProvider`,其中包含在那种情况下做出决策的逻辑。我没时间做这个。我只是想保留业务用户定义的 SiteMap 数据层级的完整性,同时仍使用内置的提供程序。我不想放弃微软内置提供程序所提供的优化和精妙代码。我只是觉得没有必要投入那么多时间。
我通过一个查询字符串参数来实现我的目标。第一次在层级中看到原始 URL 时,我会保留它。对于重复的 URL,我添加一个虚拟查询字符串参数来区分它们。我的菜单系统层级也一样,这样面包屑导航就能与菜单匹配。但是,如果用户从外部链接到我们的某个页面,我的 CMS 仍然可以通过提供一个后备方案来解决他们想要的内容。这超出了本文的范围,开始涉及我的自定义 CMS,它是一个非常强大、多语言的系统。您需要知道的是,这种方法将允许您欺骗默认的 `SiteMapProvider`,使其按照我的要求运行。以下方法可以做到这一点:
/// <summary>
/// SiteMap datasources cannot have duplicate Urls with the default provider.
/// This finds duplicate urls in your heirarchy
/// and tricks the provider into treating them correctly
/// </summary>
private void modifyDuplicateUrls()
{
StringCollection urls = new StringCollection();
string rowUrl = String.Empty;
uint duplicateCounter = 0;
string urlModifier = String.Empty;
foreach (DataTable dt in this.DataSource.Tables)
{
foreach (DataRow dr in dt.Rows)
{
rowUrl = (string)dr["Url"];
if (urls.Contains(rowUrl))
{
duplicateCounter++;
if (rowUrl.Contains("?"))
{
urlModifier = "&instance=" + duplicateCounter.ToString();
}
else
{
urlModifier = "?instance=" + duplicateCounter.ToString();
}
dr["Url"] = rowUrl + urlModifier;
}
else
{
urls.Add(rowUrl);
}
}
}
}
步骤 4:编写您的 XSLT 并将其嵌入您的类库
您需要一个 XSLT 来将您的 `*.sitemap` 文件直接从数据集转换为正确的格式。我在下面展示了一个示例 `DataSet.GetXML()` 输出、用于转换它的 XSLT 以及生成的 `*.sitemap` 文件。为了简洁起见,我只包含了输入和输出的截断片段。如果您的数据集和数据表的命名约定不同,您需要相应地修改 XSLT。这些文件包含在源文件下载中。
`DataSet.GetXML()` 生成的输入 XML
<FINALRESULTSDS>
<TABLE1>
<ID>3efae161-e807-4419-98d5-162f69cca7da</ID>
<DESCRIPTION>Find and reserve corporate housing and serviced
apartments anywhere in the world. - Find and Reserve</DESCRIPTION>
<URL>~/Apps/AdvancedPropertySearch.aspx?CM=2441358F-1E9D-4694-8E82-9E4FB4E6AD16</URL>
<ITEMORDER>0</ITEMORDER>
<TABLE2>
<ID>8115e4ef-153b-4b11-85bd-5e0a10d16c59</ID>
<PARENTID>3efae161-e807-4419-98d5-162f69cca7da</PARENTID>
<DESCRIPTION>Reservation Request</DESCRIPTION>
<URL>~/Apps/ReservationRequest.aspx</URL>
<ITEMORDER>0</ITEMORDER>
</TABLE2>
</TABLE1>
<TABLE1>
<ID>4d60e90d-8ede-4e52-88c9-65b9416ff02a</ID>
<DESCRIPTION>Temporary housing for extended stays by BridgeStreet
Worldwide corporate housing and serviced
apartments. - Accommodations Solutions</DESCRIPTION>
<URL>~/Apps/CMSTemplate.aspx?CM=4B042082-E8CB-4A34-A589-936F3642F7A1</URL>
<ITEMORDER>1</ITEMORDER>
<TABLE2>
<ID>6e49589e-3743-4ded-a718-41a615a5c4f0</ID>
<PARENTID>4d60e90d-8ede-4e52-88c9-65b9416ff02a</PARENTID>
<DESCRIPTION>Temporary housing for extended stays and business travel
by BridgeStreet Worldwide corporate housing
and serviced apartments. - Business Travelers</DESCRIPTION>
<URL>~/Apps/CMSTemplate.aspx?CM=64DA181A-7A9E-4811-AC0D-7A0C89827F28</URL>
<ITEMORDER>0</ITEMORDER>
</TABLE2>
<TABLE2>
<ID>ce5614cb-c8b4-449c-a494-8c91b3bb3328</ID>
<PARENTID>4d60e90d-8ede-4e52-88c9-65b9416ff02a</PARENTID>
<DESCRIPTION>Extended stay accommodations for military personnel
and government travelers through BridgeStreet Worldwide corporate
housing and serviced apartments. - Government Travelers</DESCRIPTION>
<URL>~/Apps/CMSTemplate.aspx?CM=6EF08802-4023-4F81-8C1E-1C3D9B4CEA86</URL>
<ITEMORDER>1</ITEMORDER>
</TABLE2>
XSLT。您可以看到它支持六级层级。如果需要,您可以添加更多。或者,我敢肯定递归有更好的方法来实现这一点,但我的强项不是 XSLT,所以如果有人能展示一种更优雅的方法来实现这一点,而不限制嵌套级别,请在评论中提供。同时,这可以胜任工作,而且做得很好。
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:apply-templates />
</xsl:template>
<xsl:template match="/finalResultsDS">
<xsl:element name="siteMap"
namespace="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0">
<!-- First Table in Heirarchy -->
<xsl:for-each select="./*[starts-with(local-name(), 'Table')]">
<xsl:element name="siteMapNode"
namespace="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0">
<xsl:call-template name="transformElementsToAttributes" />
<!-- Second Table in Heirarchy -->
<xsl:for-each select="./*[starts-with(local-name(), 'Table')]">
<xsl:element name="siteMapNode"
namespace="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0">
<xsl:call-template name="transformElementsToAttributes" />
<!-- Third Table in Heirarchy -->
<xsl:for-each select="./*[starts-with(local-name(), 'Table')]">
<xsl:element name="siteMapNode"
namespace="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0">
<xsl:call-template name="transformElementsToAttributes" />
<!-- Fourth Table in Heirarchy -->
<xsl:for-each select="./*[starts-with(local-name(), 'Table')]">
<xsl:element name="siteMapNode"
namespace="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0">
<xsl:call-template name="transformElementsToAttributes" />
<!-- Fifth Table in Heirarchy -->
<xsl:for-each select="./*[starts-with(local-name(), 'Table')]">
<xsl:element name="siteMapNode"
namespace="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0">
<xsl:call-template name="transformElementsToAttributes" />
<!-- Sixth Table in Heirarchy -->
<xsl:for-each select="./*[starts-with(local-name(), 'Table')]">
<xsl:element name="siteMapNode"
namespace="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0">
<xsl:call-template name="transformElementsToAttributes" />
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:template>
<xsl:template name="transformElementsToAttributes">
<xsl:attribute name="url">
<xsl:value-of select="Url"/>
</xsl:attribute>
<xsl:attribute name="title">
<xsl:value-of select="Title"/>
</xsl:attribute>
<xsl:attribute name="description">
<xsl:value-of select="Description"/>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
哇哦,下面是我有趣类生成的法语站点地图。这是最终产品。
<?xml version="1.0" encoding="utf-8"?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0">
<siteMapNode url="/Default.aspx" title="Accueil" description="Accueil">
<siteMapNode url="~/Apps/AdvancedPropertySearch.aspx?CM=00CB5839-329F-4692-9A60-2D4A389DD6DB"
title="Rechercher et R‚server" description="Solutions d'h‚
bergement en appartements meubl‚s avec services h“teliers
pour tous types de s‚jour : courts s‚jours, s‚jours professionnels,
d‚menagement, relocation - Rechercher et R‚server">
<siteMapNode url="~/Apps/ReservationRequest.aspx"
title="Demande de R‚servation" description="Demande de R‚servation" />
</siteMapNode>
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=1EBB6235-4174-437C-B23F-31B9E58E4FC0"
title="Solutions de Logement"
description="Solutions d'h‚bergement en appartements meubl‚s avec services
h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement, relocation - Solutions de Logement">
.
.
.
.
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=40ECF480-0ECA-4121-AB28-E1CC886E419E"
title="Achat / Vente"
description="Propositions d'appartements … vendre sur Paris - Achat / Vente" />
</siteMapNode>
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=3093CA21-5CB4-4EDF-9C46-F0E6C8ED922F"
title="Pourquoi des R‚sidences H“teliŠres"
description="Solutions d'h‚bergement en appartements meubl‚s avec services
h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement,
relocation - Pourquoi des R‚sidences H“teliŠres ?">
<siteMapNode url="~/Apps/FAQ.aspx" title="FAQ" description="FAQ" />
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=0F626C88-6ABC-4B7F-8F71-5055184F7030"
title="Am‚nagements & Services"
description="Solutions d'h‚bergement
en appartements meubl‚s avec services h“teliers pour tous
types de s‚jour : courts s‚jours, s‚jours professionnels,
d‚menagement, relocation - Am‚nagements & Services" />
.
.
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=8A2B0A11-74AD-4551-96EF-C13428E3E227"
title="R“le Dans le Secteur" description="Solutions d'h‚bergement en appartements
meubl‚s avec services h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement, relocation - R“le Dans le Secteur" />
</siteMapNode>
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=25D55721-C606-4C42-8F98-7EDBABABA819"
title="Qui Sommes-nous"
description="Solutions d'h‚bergement en appartements
meubl‚s avec services h“teliers pour tous types de s‚jour : courts s‚
jours, s‚jours professionnels, d‚menagement, relocation - Qui Sommes-nous">
.
.
.
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=3237E7F6-F732-4F24-9E65-C761B0AE8800"
title="CarriŠres"
description="Solutions d'h‚bergement en appartements meubl‚s avec services
h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement, relocation - CarriŠres" />
</siteMapNode>
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=885399BD-44A4-4720-8C75-F6521ED3A2D4"
title="Partenariat Mondial"
description="Solutions d'h‚bergement en appartements meubl‚s avec
services h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement, relocation - Partenariat Mondial">
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=2E1B9DAA-A2AC-48CD-8716-2EF113E71973"
title="BridgeNet Login"
description="Solutions d'h‚bergement en appartements meubl‚s avec
services h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement,
relocation - Bienvenue … Nos Partenaires Mondiaux" />
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=1B27D0CC-53C5-4A78-A5E0-BD4AA8A51C94"
title="BridgeStreet Global Alliance"
description="Solutions d'h‚bergement en appartements meubl‚s avec
services h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement,
relocation - BridgeStreet Global Alliance" />
</siteMapNode>
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=ADAF636D-0481-4ECC-B918-4BBF1D11FD4E"
title="Des Solutions Faciles Pour L’h‚bergement D’affaires"
description="Solutions d'h‚bergement en appartements meubl‚s avec services
h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement,
relocation - Des Solutions Faciles Pour L’h‚bergement D’affaires">
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=5295303A-1EEA-4FF5-A913-2842540F0762"
title="Programmes Tech"
description="Solutions d'h‚bergement en appartements meubl‚s avec services
h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement, relocation - Programmes Tech" />
</siteMapNode>
<siteMapNode url="~/Apps/Announcements.aspx?CM=50C83AAB-76F1-4BB9-88BA-720A116286BA"
title="Actualit‚ de L’entreprise"
description="Temporary housing for extended stays by BridgeStreet
Worldwide corporate housing and serviced
apartments. - Corporate News - September 7, 2006">
.
.
.
.
<siteMapNode
url="~/Apps/ClientNewsletter.aspx?CM=40753C0C-C80A-4FA9-B926-DC077C30EAD9"
title="Q1 2006 Client Newsletter"
description="Q1 2006 Client Newsletter - Client Newsletter - Q1 2006" />
</siteMapNode>
<siteMapNode url="~/Apps/Announcements.aspx?CM=D596B11F-2B3C-A4E6-6126-05FAD803B16D"
title="BridgeStreet Worldwide's Global Partners Expand Company's West Coast Presence"
description="BridgeStreet Worldwide's Global Partners Expand Company's West
Coast Presence - Company Enters Pacific Northwest
and San Francisco Markets - Corporate News - November 16, 2005" />
<siteMapNode url="~/Apps/Announcements.aspx?CM=D596B17D-2B3C-A4E6-6098-06F29E5EF2F3"
title="BridgeStreet Worldwide Receives 2005 Technology ROI Award"
description="BridgeStreet Worldwide Receives 2005 Technology ROI Award - Winners
Use Technology Solutions to Achieve Positive Business
and Financial Results - Corporate News - August 17, 2005" />
.
.
.
.
<siteMapNode url="~/Apps/Announcements.aspx?CM=D596B390-2B3C-A4E6-6400-FA0765B40496"
title="BridgeStreet Is First to Place Full Corporate Housing Inventory on GDS"
description="BridgeStreet Is First to Place Full Corporate Housing Inventory
on GDS - Online Access Opens Up New Commission Opportunities
for Travel Agents - Corporate News - June 7, 2002" />
<siteMapNode url="~/Apps/Announcements.aspx?CM=D596B13F-2B3C-A4E6-6D47-FCB993A8BD49"
title="BridgeStreet Worldwide Chicago Office Receives Record-Setting Fourth
Consecutive “CAMME” Award from Chicagoland Apartment Association"
description="BridgeStreet Worldwide Chicago Office Receives Record-Setting
Fourth Consecutive “CAMME” Award from Chicagoland
Apartment Association - Company Also Wins “Associate
Member Brochure—Print Form” Category - Corporate News - October 20, 2005" />
</siteMapNode>
<siteMapNode
url="~/Apps/ContactUs.aspx?CM=4F38AB79-0BFE-4DB5-9C19-1D056C287808&instance=3"
title="Nous Contacter"
description="Solutions d'h‚bergement en appartements meubl‚s avec
services h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement, relocation - Nous Contacter" />
</siteMapNode>
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=690CC898-F538-4772-A127-CCBE5FB2742B"
title="Mon Compte"
description="Solutions d'h‚bergement en appartements meubl‚s avec
services h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement, relocation - Mon Compte">
.
.
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=0612F0E6-B3E1-43A1-8EBC-7CB36EC70D06"
title="tudes de Cas"
description="Solutions d'h‚bergement en appartements meubl‚s avec
services h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement, relocation - tudes de Cas" />
</siteMapNode>
<siteMapNode url="~/Apps/ContactUs.aspx?CM=4F38AB79-0BFE-4DB5-9C19-1D056C287808"
title="Nous Contacter"
description="Solutions d'h‚bergement en appartements meubl‚s avec
services h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement, relocation - Nous Contacter" />
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=077BF590-4317-431A-8E25-14134B86C6E4"
title="Politique de Confidentialit‚"
description="Solutions d'h‚bergement en appartements meubl‚s avec
services h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement,
relocation - Politique de Confidentialit‚" />
</siteMapNode>
<siteMapNode url="~/Apps/WebSpecials.aspx?CM=00D1CBCB-48B2-4033-86F4-258EA017997F"
title="Informations Sp‚ciales"
description="Solutions d'h‚bergement en appartements meubl‚s avec services
h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement,
relocation - Informations Sp‚ciales">
<siteMapNode url="~/Apps/WebSpecials.aspx?CM=E255629F-6C1A-4DA3-BAD7-03E3525B4EBE"
title="Pittsburgh, Pennsylvania"
description="Temporary housing for extended stays by BridgeStreet
Worldwide corporate housing and serviced apartments. - Area Special" />
.
.
.
.
<siteMapNode url="~/Apps/WebSpecials.aspx?CM=B4B978F7-DEC2-47AA-BEAA-F64BB70096F5"
title="St. Louis, Missouri"
description="Temporary housing for extended stays by BridgeStreet
Worldwide corporate housing and serviced apartments. - Area Special" />
</siteMapNode>
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=A432E443-F244-47AD-B074-D401F72B9A64"
title="Flash Info"
description="Solutions d'h‚bergement en appartements meubl‚s avec services
h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement,
relocation - Flash Info - Travail dur... Sommeil facile.">
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=3EC5E979-3916-43B0-B935-40895D8CBDA1"
title="Saint Germain" description="Solutions d'h‚bergement en appartements meubl‚s avec
services h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement, relocation - Flash Info - Saint Germain" />
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=C4B7E9ED-0E8C-494E-9BDF-5BACDE299E47"
title="Bridgestreet l'Opera"
description="Solutions d'h‚bergement en appartements meubl‚s avec services
h“teliers pour tous types de s‚jour : courts s‚jours,
s‚jours professionnels, d‚menagement,
relocation - Flash Info - Bridgestreet l'Opera" />
.
.
.
.
<siteMapNode url="~/Apps/CMSTemplate.aspx?CM=0E7F3F43-4416-491D-9A0C-D968D7E101B6"
title="Notting Hill"
description="Solutions d'h�bergement en appartements meubl�s avec services
h�teliers pour tous types de s�jour : courts s�jours,
s�jours professionnels, d�menagement,
relocation - Flash Info - Notting Hill" />
</siteMapNode>
</siteMapNode>
</siteMap>
现在,您所要做的就是将您的 XSLT 嵌入到您的 DLL 中。在您的类库项目中添加一个新的 XSLT 文件。只需将上面的 XSLT 代码(或下载的源代码)复制过来。一旦它在您的项目中,您就可以将其添加为资源文件,以便从程序集中作为 `byte[]` 提取(这样就没有额外的磁盘读取了,不是吗?)。
在类库属性页上,选择“资源”选项卡。将您的文件添加到资源中。选择新添加的 XSLT 文件。设置属性如下(`FileType` 必须为 `Binary`):
在解决方案资源管理器中找到该文件,选择您需要嵌入的文件,并将生成操作更改为“嵌入的资源”。
现在,当您的类库构建时,您可以在内存中访问该文件,而无需像 .NET 1.1 那样使用 `System.Reflection`。
步骤 5:备份和编写方法
现在您需要编写备份现有文件、执行转换并写入 `*.sitemap` 文件的代码。
首先是备份方法。我只需要每天备份,所以我的文件名是根据这个来命名的。如果您想要不同的方案,可以更改 `ToString("")` 日期格式信息。
/// <summary>
/// Backs up the current Sitemap file to a user defined directory
/// </summary>
/// <param name="pathToCurrent">The relative path to the directory
/// where the current files are stored</param>
/// <param name="pathToBackup">The relative path
/// to the directory where the sitemap backup files are stored</param>
public void BackupCurrentFile(string pathToCurrent, string pathToBackup)
{
string fullCurrentPath = HttpContext.Current.Server.MapPath(pathToCurrent);
string fullBackupPath = HttpContext.Current.Server.MapPath(pathToBackup);
if (Path.HasExtension(fullCurrentPath) || Path.HasExtension(fullBackupPath))
{
throw new ArgumentException("You cannot specify the fileName " +
"of your sitemap, please provide only the directories " +
"where you'd like to store your sitemap files.",
"pathToCurrent or pathToBackup");
}
else
{
if (!fullCurrentPath.EndsWith("\\"))
{
fullCurrentPath = fullCurrentPath + "\\";
}
if (!fullBackupPath.EndsWith("\\"))
{
fullBackupPath = fullBackupPath + "\\";
}
fullCurrentPath = fullCurrentPath + this.FileName;
string backupFileName =
Path.GetFileNameWithoutExtension(fullBackupPath + this.FileName);
backupFileName = backupFileName + "_" +
DateTime.Now.ToString("yyyy-MM-dd") + ".sitemap";
fullBackupPath = fullBackupPath + backupFileName;
FileInfo siteMapFileInfo = new FileInfo(fullCurrentPath);
FileInfo backupFileInfo = new FileInfo(fullBackupPath);
if (backupFileInfo.Exists)
{
File.SetAttributes(fullBackupPath, FileAttributes.Normal);
}
siteMapFileInfo.CopyTo(fullBackupPath, true);
}
}
现在,您期待的时刻到了,转换和文件写入。您可以看到 XSLT 是作为 `byte[]` 从内存中提取出来的。这是一种非常快速的技术,代码量很少。请注意,`SiteMapTransformer` 是嵌入式资源的名称(XSLT 文件)。另请注意,`DataSet.GetXML()` 方法直接从数据集中生成前面的输入架构。
/// <summary>
/// Writes the Sitemap to a standard ASP.NET sitemap xml file
/// </summary>
/// <param name="fileName">The relative path name where you want to write to</param>
/// <param name="pathToCurrent">The relative path to the directory
/// where the current sitemap files are stored</param>
/// <param name="pathToBackup">The relative path to the directory
/// where the sitemap backup files are stored</param>
public void WriteFile(string pathToCurrent, string pathToBackup)
{
string fullPath = HttpContext.Current.Server.MapPath(pathToCurrent);
if (Path.HasExtension(fullPath))
{
throw new ArgumentException("You cannot specify the fileName " +
"of your sitemap, please provide only the directory " +
"you'd like to store your sitemap files.",
"pathToCurrent");
}
else
{
if (!fullPath.EndsWith("\\"))
{
fullPath = fullPath + "\\";
}
FileInfo siteMapFileInfo = new FileInfo(fullPath + this.FileName);
//Backup the current file
if (siteMapFileInfo.Exists)
{
this.BackupCurrentFile(pathToCurrent, pathToBackup);
}
XslCompiledTransform xslt = new XslCompiledTransform();
MemoryStream transformStream = new MemoryStream(Resources.SiteMapTransformer);
MemoryStream siteMapXmlStream = new MemoryStream();
//Generate Xml from the heirarchical dataset
XmlReader dataSetXmlReader =
XmlReader.Create(new StringReader(this.DataSource.GetXml()));
//Load the Xslt file from the assembly manifest (now in a stream)
xslt.Load(XmlReader.Create(transformStream));
//Perform the transformation
xslt.Transform(dataSetXmlReader, XmlWriter.Create(siteMapXmlStream));
//Write the stream to file, the current file is overwritten if it exists
if (siteMapFileInfo.Exists)
{
File.SetAttributes(siteMapFileInfo.FullName, FileAttributes.Normal);
}
File.WriteAllBytes(siteMapFileInfo.FullName, siteMapXmlStream.ToArray());
}
}
步骤 6:使用它
我们必须在 `web.config` 中为不同类型的站点地图定义不同的站点地图提供程序。以下是 `Web.Config` 的要求:
<siteMap defaultProvider="Content_en-US" enabled="true">
<providers>
<clear/>
<add name="Content_en-US" description="The SiteMap Provider for en-US Content"
type="System.Web.XmlSiteMapProvider, System.Web, Version=2.0.3600.0,
Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
siteMapFile="/Website/SiteMaps/Content_en-US.sitemap" />
<add name="Content_en-GB"
description="The SiteMap Provider for en-GB Content"
type="System.Web.XmlSiteMapProvider, System.Web, Version=2.0.3600.0,
Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
siteMapFile="/Website/SiteMaps/Content_en-GB.sitemap" />
<add name="Content_fr-FR"
description="The SiteMap Provider for fr-FR Content"
type="System.Web.XmlSiteMapProvider, System.Web,
Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
siteMapFile="/Website/SiteMaps/Content_fr-FR.sitemap" />
<add name="Properties_en-US"
description="The SiteMap Provider for en-US Properties"
type="System.Web.XmlSiteMapProvider, System.Web,
Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
siteMapFile="/Website/SiteMaps/Properties_en-US.sitemap" />
<add name="Properties_en-GB"
description="The SiteMap Provider for en-GB Properties"
type="System.Web.XmlSiteMapProvider, System.Web,
Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
siteMapFile="/Website/SiteMaps/Content_en-GB.sitemap" />
<add name="Properties_fr-FR"
description="The SiteMap Provider for fr-FR Properties"
type="System.Web.XmlSiteMapProvider, System.Web,
Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
siteMapFile="/Website/SiteMaps/Properties_fr-FR.sitemap" />
</providers>
</siteMap>
现在您的前端代码非常简单:
<asp:Label ID="SC_Title" runat="server" SkinID="MainContentHeader"
meta:resourcekey="SC_TitleResource1"></asp:Label><br />
<br />
<asp:Label ID="SC_ContentSitemapLabel" SkinID="MainContentSubHeader"
runat="server" meta:resourcekey="SC_ContentSitemapLabelResource1"></asp:Label><br /><br />
<asp:TreeView SkinID="SiteMapTree" ID="SC_ContentTree"
DataSourceID="SC_SiteMapSource" runat="server" meta:resourcekey="SC_ContentTreeResource1">
</asp:TreeView><br /><br />
<asp:Label ID="SC_PropertySitemapLabel" SkinID="MainContentSubHeader"
runat="server" meta:resourcekey="SC_PropertySitemapLabelResource1"></asp:Label><br /><br />
<asp:SiteMapDataSource ID="SC_SiteMapSource" runat="server" />
在 `page_load` 上的后台代码示例(`ExpandDepth` 是初始视图要展开的层级数):
string currentCultureName = Session["CurrentCulture"] as string;
this.SC_SiteMapSource.SiteMapProvider = "Content_" + currentCultureName;
this.SC_ContentTree.ExpandDepth = 1;
现在您有了站点地图,但面包屑导航呢?在这里:
<asp:SiteMapPath SkinID="SiteMapPath" ID="SC_SiteMapPath" runat="server">
</asp:SiteMapPath>
在母版页上
public void SetSiteMapProvider(string siteMapProviderName)
{
this.SC_SiteMapPath.SiteMapProvider = siteMapProviderName;
}
在 Web 窗体上
this.Master.SetSiteMapProvider("Content_" + currentCulture);
您明白了。使用 `*.sitemap` 文件非常简单。
步骤 7:计划您的站点地图文件生成
这就是“一刀切”不适用的地方。您现在拥有能够完成这项工作的类和方法。我推荐三种计划站点地图文件写入和备份的方法:Windows 服务、在计划任务服务器上运行的 WinForms 应用程序,或者在 .NET 3.0 中,WWF(工作流基金会,不是摔跤)中有一些很酷的东西。在 3.0 的 MS RTM 之前,我会坚持使用 Windows 服务。这超出了本文的范围,但完成这项工作的代码也非常简单。您只需获取感兴趣的网站的 `HttpContext` 的句柄,然后像这样使用该类:
CMSSitemap sitemap = new CMSSitemap(new Guid("YOURAPPGUID"),
"fr-FR", SitemapType.Content);
sitemap.WriteFile("/Website/SiteMaps", "/Website/SiteMapBackup");
您现在可能正在大声疾呼的理想解决方案是,在您确定内容已被删除或插入到您的动态 CMS 时触发此代码块。如果您能做到这一点,**就这样做**!请记住,如果您有一个如此庞大的 CMS,并且您的主要目标之一是对内容进行搜索引擎索引,那么您希望站点地图能够反映最新内容,以便于浏览和搜索引擎索引。如果您使用 ASP.NET 的 `TreeView` 控件,可以放心地认为 Lynx 可以看到它。Lynx 无法看到 Infragistics TreeMenu 控件中的链接。无论您为表示层使用什么控件,如果您关心搜索引擎索引,请确保文本浏览器(如 Lynx)可以看到链接。
另外请注意,关于搜索引擎是否会索引查询字符串 URL,仍然有一些模糊之处。我在 Google 网站管理员帐户中看到 Google 试图索引我的。您可能需要确保这一点并为此付出一些努力。如果您需要更进一步,请尝试搜索引擎安全 (SES) URL。这位博主在他为 HttpModule 所做的工作上做得非常出色:http://weblogs.asp.net/skillet/archive/2004/05/04/125523.aspx。
最后评论
您的实现取得了很大成就。对于动态系统来说,拥有站点地图并非易事。ASP.NET 2.0 减轻了“视图”代码的难度。您不希望在每次 Web 请求时都遍历您的 CMS 数据库,但您希望利用内置的 .NET 2.0 站点地图控件和提供程序。您的站点地图 XML 做了很多工作:它们将内容分支到不同的站点地图类型,这些类型可以是上下文敏感的、支持多种语言的,还能为您擦鞋。XSLT 在几行代码中就能将您的数据集直接转换为 `*.sitemap` XML 格式。将其作为 `byte[]` 嵌入类库并在 `MemoryStream` 中使用非常酷。
您在文章顶部看到了面包屑导航。这里是站点地图树的视图:
您可以看到我们的 CMS 定义了我们希望隔离的内容类型以提高可用性。拥有多种站点地图类型允许我们利用这一出色的架构功能。
您可以在 http://www.bridgestreet.com/Apps/Sitemap.aspx 上看到这一点以及更多内容。非关系型、扁平的 UI 数据可以通过卫星程序集实现多语言。运行菜单和内容的关联数据作为自定义对象缓存到服务器内存中,这使得站点运行得非常快,因为任何页面上的元素都不是静态的,但数据库的访问次数却非常少。我的另一篇地理相关文章也在此网站上实现。您可以在网站上看到它的威力。
我希望您能够运用这些概念,并在您自己的站点地图工作中将其转化为您的优势。