生成 ContentRotator ASP.NET 服务器控件
发布日期: 2006-3-7 | 更新日期: 2006-3-7
Scott Mitchell
4GuysFromRolla.com
适用于:
ASP.NET 1.1
摘要:介绍创建自定义、编译的 ASP.NET 服务器控件涉及的步骤,这种控件对所有的特定内容进行随机滚动,就像内置的 AdRotator 控件随机滚动一系列预定义的标语广告一样。在介绍 ContentRotator 控件的核心内容时,本文涉及到自定义 ASP.NET 控件开发的几个方面。
下载 ContentRotator.msi。
![*](http://img.microsoft.com/library/gallery/templates/MNP2.Common/images/3squares.gif)
本页内容
简介
早在九十年代末期,似乎没有什么事情是不可能发生的。World Wide Web 及其对商业的影响急剧增加 — 孩子们退学建立 Web 站点而一夜暴富,企业也斥资数百万美元在黄金电视时段播放广告,吸引人们上网为宠物购买食物。没错,这就是新经济时代,相比于在令人乏味的建筑物中销售单调的旧产品而言,将数百万的 Web 网民吸引到 Web 站点来的确更有经济价值。
在这个浮华的时代,Microsoft 引入了 传统 ASP 的 AdRotator COM 组件,它使崭露头角的 Web 企业家能够将标语广告轻松添加到他们的站点中。显示标语广告的第一步是具有一个列出可以显示的标语的 advertisements 文件。该文本文件包含有关每个标语的四类信息:
• | 指向该标语图像的 URL |
• | 该图像链接到的 URL |
• | 该图像的替换文本 |
• | 该标语相对于文件中其他标语的显示频率 |
使用该文本文件时,ASP 开发人员必需调用 AdRotator 的 GetAdvertisement(advertisementFile) 方法,从而传入到 advertisements 文件的路径,而 AdRotator 将返回 HTML 标记,用于显示从指定文件随机选择的标语广告。我想,这时候已经财源滚滚了。
2002 年交付 ASP.NET 时,新经济时代已经成为过去。NASDAQ 指数在 2000 年曾经攀升至 5,000 点左右,而现在已跌到 2,000 点以下。尽管 .com 彻底垮台,但 Microsoft 依旧对在线广告持乐观态度,缘由是他们已修整了 传统 ASP 的 AdRotator 控件并进行了发布 — 作为一个标准 Web 控件与 ASP.NET 一起提供。AdRotator 的 ASP.NET 版本较之传统的 ASP 版本有以下几个优势:
• | advertisements 文件现已是 XML 格式化的,从而使页面开发人员可以更轻松地创建和编辑该文件的内容。 |
• | 每个广告可以包含一个指示其所属类别的关键字。除了 AdRotator 的新 KeywordFilter 属性之外,这可用来限制针对具体 AdRotator 控件实例显示的标语集。 |
• | 除了标准的广告属性集之外,页面开发人员可以将他们自己的附加设置添加到 advertisements 文件中。然后,添加的这些设置可由页面开发人员通过 AdRotator 的 AdCreated 事件以编程方式访问。 |
虽然 AdRotator 能更容易地随机显示来自预定义列表的标语,但是它存在很多缺点,在我看来,其中有两个主要的缺点:
1. | AdRotator 的内容只可以通过 XML 格式化的 advertisements 文件进行指定。这虽然不是一个主要限制,但是如果能够通过 AdRotator 的标记以声明方式指定内容,或者通过源代码以编程方式进行指定,则是比较理想的,这非常类似于针对 DropDownList Web 控件以声明方式或编程方式指定 ListItem。 |
2. | AdRotator 在它可以生成的标记方面受到一些限制。顾名思义,它旨在显示广告,这些广告是文本或链接到某个 URL 的图像。只需进行少量工作,就可以将 AdRotator 创建为显示任何 类型的内容,而不仅仅是标语或文本广告。 |
第二个缺点特别让我感到烦恼,因为只需再进行少量工作就可以使 AdRotator 成为一个非常通用的内容滚动器,而不仅仅设计为只显示广告。在本文中,我们将通过创建一个全功能的 ContentRotator 服务器控件来克服该缺点以及其他缺点。
考虑内容滚动
在深入研究任何编码项目之前,重要的是用充足的时间回答以下三个问题。
1. | 有满足我需要的现有控件吗?如果您的工作和我类似,就是说每天有很多会议和电子邮件,还要花费更多时间进行其他繁忙的工作,则编写代码通常是一天中最有趣的活动。如果对问题产生了兴趣并通过代码找到该问题的解决方案,那么没有什么比这让令人兴奋了。 但是,“有趣”与“具有经济价值”可完全是两码事。创建具体控件可能是有趣的,但如果已经存在能够提供所需功能的控件,那么花费时间生成、测试和调整此类控件就毫无经济意义可言。 当我想创建新的服务器控件时,我首先会进入 ASP.NET Control Gallery,看看我是不是在做无用功。快速查看 Control Gallery,显示用于 Content Rotators 的一个完整类别。但是,这些内容滚动器(例如,新闻或股票自动接收器)的大多数都在单个页面滚动所有的内容。 我发现有一个控件可以滚动完整的诸如 AdRotator 控件(Duncan Mackenzie 的ContentRotator 用户控件,详述于文章 Rotating Is Fun)的任意静态 HTML 内容。虽然 Duncan 的用户控件提供了 AdRotator 的基本功能且支持任意 HTML 内容,但我决定不使用他的解决方案,因为它并不提供我需要的功能。(例如,Duncan 的控件不允许为内容标记关键字。) 在搜索过程中,我没有找到满足需要的内容滚动器。因此我决定自己创建。(哈哈!老板,会议请先等一下吧,我现在要马上完成一个编程项目!) |
2. | 我的控件需要提供什么功能?如果您决定创建自己的控件,请不要立即深入到最有趣的部分 — 编码;相反,您需要在开始编码之前清楚地了解该控件需要具有哪些功能。确定控件要求的一个好方法是设计常规用例,这些用例描述最终用户(在本例中是页面开发人员)将如何使用您的控件。在生成服务器控件时,我首先要自问的几个问题是: • | 页面开发人员将如何使用该控件? | • | 他们需要该控件做什么? | • | 语法应该如何? |
在经过一番深思熟虑之后,我设计了以下四个用例: Harry(一个很有前途的 ASP.NET 开发人员)想增强其公司的 Intranet,以便主页可以随机显示一个公司雇员的简历和照片。由于 Harry 的公司只有十多个雇员,因此他目前喜欢在一个文件中硬编码简历和照片,但希望日后可以对其进行升级,以便随着公司的发展从数据库检索这些项。他的目标是让 ContentRotator 从该文件随机选择一个雇员,每次访问时在主页上显示有关该雇员的信息。 Jisun 是 BuyPetFoodOnline.com 的一位开发人员,这个网站是新启动的,关于它的风险投资能否取得回报都寄希望于 John Q 身上。大家对在线购买 Alpo 很感兴趣。BuyPetFoodOnline.com 上销售的所有宠物食物品牌都在一个 Microsoft SQL Server 数据库中维护。在该站点的主页上,Jisun 想显示最畅销的 10 种宠物食物品牌列表,或者各类狗食列表或猫食列表。此外,她希望每当狗食或猫食的内容组合时,最畅销的十个销售品牌内容平均显示两次。 Todd 运行一个有关健身的 Web 站点,目前已有数千名会员在该站点上注册,注册信息包括姓名、出生日期、体重以及其他和健康相关的数据。在每页的底部,Jim 想针对访问者的个人信息显示随机健康统计信息。例如,他想显示以下消息:[[username]]、您已经活了 [[numberOfDaysSinceBirth]] 天、您的平均脉搏是 [[averageHeartRateTimesDaysAlive]],其中每一个占位符均用特定于登录用户的值进行填充。 成长中的 ASP.NET 开发人员 Darren 是一个 XML 的新手,他很担心在 XML 格式化的内容文件中指定内容项时会犯错误。Darren 非常熟悉 DropDownList Web 控件,经常使用 DropDownList 的描述性语法指定 ListItem。Darren 也希望能够以相同的方式为 ContentRotator 指定内容项。同样,他将能够以编程方式操作 ContentRotator 的内容项,使用的语法类似于以编程方式使用 DropDownList 的 ListItem 所需的语法。 ContentRotator 的功能就是从这些用例中产生的,这些用例在 ContentRotator 的实现中提供指导。 |
3. | 有代码重用的可能性吗?面向对象编程的一大好处是,可以轻松合并和扩展现有功能。在创建新服务器控件时,很可能已经存在提供类似功能的 ASP.NET 服务器控件。只扩展该现有服务器控件而不重新生成服务器控件可能吗?在现有控件基础上进行构建将节省大量的编码和测试时间。 在开始创建用于 ContentRotator 的代码时,我实验了使 ContentRotator 扩展现有 ASP.NET AdRotator 控件的可能性。AdRotator 类包含了从广告文件读取项并随机选择一个适当项所需的方法和属性。我能够重用该类并重写发送标语或文本广告的特定方法,从而将其更改为返回更多一般内容的方法吗? 我考虑了该方法,但由于以下原因决定不使用它。首先,AdRotator 的许多方法都没有标记为 virtual,这意味着它们不能重写。特别是,由于解析 advertisements 文件的方法不是虚拟的,因此我的派生类将必须使用 AdRotator 的现有 XML 格式。这不一定会限制 ContentRotator 的功能(由于 AdRotator 可以添加任意 XML 元素),但它仍然会产生一些限制,因为它将使用 advertisement 文件的 和 元素。此外,AdRotator 需要一个 元素,它对于一般内容滚动器来说是一个无法接受的要求。 |
在经过谨慎考虑之后,我最终准备开始编写代码 — 这是一个有趣的环节。在下文中,我将介绍 ContentRotator 控件的一些更有趣的代码,这些代码不仅可以揭示该特定控件的内部工作,还提供一个示例,用于在创建的服务器控件中实现相似的功能。
指定内容项
ContentRotator 控件提供三种用于指定内容项的方式:
1. | 通过 XML 格式的一个单独内容文件。 |
2. | 通过 ContentRotator 的声明性语法。 |
3. | 通过服务器端编程方法。 |
第一个选项使用一个外部文件提供对内容项更好的重用,因为单个内容文件可以通过单个 Web 站点中不同页上的许多内容滚动器使用。但是,有时您可能想快速创建简单的 ContentRotator 控件,而不必烦恼于创建单独的内容文件。在这些情况下,您可以使用第二个选项,并提供内容项来迭代通过该控件的声明性语法。最后一个选项允许您以编程方式指定内容项。如果需要动态选择可能的内容项,或者这些内容项存在于一个数据库或其他某个非静态存储中,则该选项是有用的。
通过内容文件指定内容
当在 XML 格式化内容文件中指定内容项时,必须针对具体的 XML 架构提供内容项。特别是,内容文件必须以 元素开头,该元素包含每个内容项的 元素。每个 项有三个可选属性:
• | impressions — 指定内容项的重要性,用来确定项显示的概率。 |
• | keyword — 指定内容项的关键字。ContentRotator 控件包含一个 KeywordFilter 属性,如果设置该属性,则将所显示的内容项限制为具有匹配的关键字 参数的内容项。 |
• | contentPath — 内容项可以包含静态 HTML 标记或代码驱动的动态内容。如果您想使用动态内容,可以通过该属性指定到 User Control 的路径。如果设置所选内容项的 contentPath 属性,则该内容通过指定的 User Control 生成。 |
元素也可以包含提供要显示的静态标记的文本。如果未提供 contentPath 属性,则显示该静态标记。
以下示例显示一个带有四个内容项的、正确进行格式化的内容文件。第一个内容项缺少任何可选属性,只由要显示的文本内容组成。第二个内容项提供 impressions 和 keyword 属性,而第三个内容项只设置 keyword 属性。请注意,如果您想在内容项的文本部分显示 HTML 标记,则需要像在第二个示例中一样转义 XML 标记,方法是使用 < 和 > 而不是 < 和 >,或者需要将整个内容包装在一个 部分中。第四个(最后一个)内容项引用一个 User Control (RichContent.ascx),该控件通过 contentPath 属性指定。此外,impressions 属性设置为 5。
<?xml version="1.0" encoding="utf-8" ?>
<contents>
<content>
Things are just average... neither positive nor negative...
</content>
<content impressions="3" keyword="positiveComments">
<b>You will soon see a workplace promotion.</b>
</content>
<content keyword="positiveComments">
<![CDATA[
Happiness is <i>just around the corner!</i>
]]>
</content>
<content contentPath="~/RichContent.ascx" impressions="5" />
</contents>
该内容文件需要保存在 Web 服务器的文件系统中。要显示特定文件的内容,只需将 ContentRotator 添加到 ASP.NET 页,并将它的 ContentFile 属性设置为内容文件的虚拟路径。
注要记住,XML 是区分大小写的,因此 XML 元素的大小写是重要的。如果您没有使用正确的大小写(例如,使用 而非 ),则不会从内容文件检索这些内容项,从而得到一个不发出任何内容项的 ContentRotator 控件。
声明性地指定内容项
许多 ASP.NET Web 内置控件允许其大多数属性用 Web 控件的声明性语法来指定。例如,您可以指定通过 声明性语法创建 DataGrid 的特定 DataGridColumn。ContentRotator 提供类似的声明性语法来指定它的内容项。对于 ContentRotator 要随机显示的每个项,请在 标记中添加一个 元素。每个 元素可以包含以下属性:
• | Content |
• | Impressions |
• | Keyword |
• | ContentPath |
这些属性分别映射到 元素的文本部分,以及 XML 内容文件架构的 impressions、keyword 和 contentPath 属性。(您可以选择将 Content 属性指定为该内部标记的文本内容,如下面的前两个 实例所示。)下面显示用于 ContentRotator 的声明性语法,其中带有前面使用的四个内容项。
<skm:ContentRotator id="ContentRotator1" runat="server">
<skm:ContentItem>Things are just average... neither positive nor
negative...</skm:ContentItem>
<skm:ContentItem Impressions="3" Keyword="positiveComments"><b>You will soon see a workplace promotion
</b></skm:ContentItem>
<skm:ContentItem Content="Happiness is <i>just around the
corner!</i>" Keyword="positiveComments"></skm:ContentItem>
<skm:ContentItem ContentPath="~/RichContent.ascx" Impressions="5"></skm:ContentItem>
</skm:ContentRotator>
请注意,使用该声明性语法时,您无需使用 Content 属性来转义 HTML 字符。
以编程方式指定内容
为了协助内容和内容项的概念,ContentRotator 包含两个类:
• | ContentItem—抽象地表示带有诸如 Content、ContentPath、Keyword、Impressions 等属性的内容项。 |
• | ContentItemCollection—强类型化的 ContentItem 实例集合。 |
ContentRotator 控件包含类型 ContentItemCollection 的一个 Items 属性。您可以通过将 ContentItem 实例添加到 Items Items 属性,以编程方式指定,当随机选择项时 ContentRotator 应该考虑的内容项。和其他 ASP.NET 服务器控件一样,添加到 Items 属性的内容存储在该控件的视图状态中,因此,您只需在第一次页访问时添加这些项,而不是在随后的回调中进行添加。下面的代码片段显示了如何以编程方式添加前面两个示例中使用的内容项集合:
private void Page_Load(object sender, System.EventArgs e)
{
if (!Page.IsPostBack)
{
// only need to load content items on first page visit –
they are persisted across
// postbacks in the ViewState...
ContentRotator1.Items.Add(new ContentItem("Things are just
average... neither positive nor negative..."));
ContentRotator1.Items.Add(new ContentItem(" You will
soon see a workplace promotion ", string.Empty, "positiveComments", 3));
ContentItem thirdCI = new ContentItem();
thirdCI.Content = "Happiness is just around the
corner!";
thirdCI.Keyword = "positiveComments";
ContentRotator1.Items.Add(thirdCI);
// Add dynamic content
ContentRotator1.Items.Add(new ContentItem(string.Empty,
"~/RichContent1.ascx", string.Empty, 5));
}
}
如您所见,ContentItem 类有大量构造函数重载,它们可以将创建新 ContentItem 实例并设置其属性的代码减至一行。
如果您不想阅读代码详细信息,而直接开始在 ASP.NET 应用程序中使用 ContentRotator,则您可以跳过下文并从本文顶部的链接下载控件。该下载包括 ContentRotator 控件(以 C# 编写)的完整源代码,以及一个示例 ASP.NET Web 应用程序(也以 C# 编写),该程序显示针对上面所讨论用例的解决方案。此外,该下载还包括一个外观漂亮、经编译的帮助文件,用来帮助决定使用 ContentRotator 控件的页面开发人员。
确定要显示的内容项
每次访问带有 ContentRotator 控件的页时,ContentRotator 必须决定要随机显示什么内容。每个内容项有一个相关的 impressions 值,该值影响到该内容项相对于其他内容项被选中的可能性。该 impressions 参数是一个正整数值,在不指定的情况下,默认值为 1。此外,每个内容项可以有一个可选的关键字 参数。如果指定 ContentRotator 控件的 KeywordFilter 属性,则将用于显示的内容项集合限制为那些具有匹配关键字 值的内容项。
用于随机选择内容项的算法的工作方式为:将每个可应用的内容项以端对端方式进行布局,从而形成一行。每个内容项的长度是它的 impressions 值,这意味着该行的整体长度是可应用的内容项 impressions 的总和。接下来,选择一个小于总长度的随机数,而要显示的内容项是位于随机数位置的内容项。图 1 以图形方式阐释该算法。
![](http://www.microsoft.com/china/msdn/library/webservices/asp.net/art/ContentRotator1S.gif)
图 1.
为了应用此算法,ContentRotator 控件首先需要检索要考虑的内容项列表。想必您还记得,该列表可能作为 XML 文件驻留在磁盘上,它可以用 ContentRotator 的声明性语法指定,或者以编程方式提供。让我们看看如何使用这三种技术访问该内容项列表。
从内容文件读取内容数据
ContentRotator 有一个 ContentItemCollection 类型的 Items 属性,它包含由 ContentRotator 控件考虑的可应用内容项集。(相信您还记得,该可应用内容项集依赖于是否设置了该控件的 KeywordFilter 属性,如果设置了该属性,还取决于内容项的关键字参数。)Items 属性通过调用 GetFileData(virtualFilePath) 方法,在 Load 事件中进行填充。该方法返回一个 ContentItemCollection 实例,它包含由 virtualFilePath 参数指定的内容文件中的所有 内容项。
在每次访问页面时打开、读取并解析整个内容文件将是无效和不必要的,特别是在考虑到该文件可能很少进行更改的情况下。要提高性能,需要使用一个文件依赖项缓存该内容文件中的项。这意味着该内容文件中的项将驻留在用于提高性能的缓存中,但是当基础内容文件修改时,该缓存项将自动失效。以下 GetFileData(filePath) 方法的代码阐释该缓存行为:
// See if the item exists in the cache
string cacheKey = string.Concat("ContentRotateCacheKey:",
physicalFilePath);
ContentItemCollection cachedContent = (ContentItemCollection)
HttpContext.Current.Cache[cacheKey];
if (cachedContent == null)
{
// it's *not* in the cache, must manually get the file data and cache it
cachedContent = LoadFile(physicalFilePath);
if (cachedContent == null)
return null;
else
// Add the content to the cache
HttpContext.Current.Cache.Insert(cacheKey, cachedContent,
new CacheDependency(physicalFilePath));
}
// return the cached content
return cachedContent;
变量 physicalFilePath 包含到该内容文件的物理路径,并用于形成缓存键。这确保了每个不同的内容文件都将有其自己的缓存项。接下来,访问 Cache 对象,从而检索名为 cacheKey 的缓存项的值。如果该项是 null(由于该缓存中没有插入此类项,或者缓存项已失效),则来自内容文件的内容和基于内容文件的缓存依赖项一起加载并插入到该缓存中。
LoadFile(physicalFilePath) 方法使用 XPathNodeIterator 迭代通过内容文件,从而清除多种属性和文本内容,并为该内容文件中的每一项生成一个 ContentItem 实例。每个 ContentItem 实例添加到 ContentItemCollection,它在该方法的结果中返回。以下代码显示了迭代通过内容文件中每一项的过程;fStream 是该内容文件的一个打开的文件流。
// Use an XPathNavigator to iterate through the XML elements in the ContentFile
reader = new XmlTextReader(fStream);
XPathDocument xpDoc = new XPathDocument(reader);
XPathNavigator xpNav = xpDoc.CreateNavigator();
XPathNodeIterator xmlItems = xpNav.Select("/contents/content");
XPathExpression contentExpr = xpNav.Compile("string(text())");
XPathExpression contentPathExpr = xpNav.Compile("string(@contentPath)");
XPathExpression keywordExpr = xpNav.Compile("string(@keyword)");
XPathExpression impressionsExpr = xpNav.Compile("string(@impressions)");
if (xmlItems == null)
throw new FormatException("ContentFile in invalid format.");
else
{
while (xmlItems.MoveNext())
{
string content = (string) xmlItems.Current.Evaluate(contentExpr);
string contentPath = (string) xmlItems.Current.Evaluate(contentPathExpr);
string keyword = (string) xmlItems.Current.Evaluate(keywordExpr);
string impressionsStr = (string) xmlItems.Current.Evaluate(impressionsExpr);
int impressions = 1; // default impressions value is 1
if (impressionsStr != null && impressionsStr.Length > 0)
impressions = Convert.ToInt32(impressionsStr,
CultureInfo.InvariantCulture);
contentItems.Add(new ContentItem(content.Trim(),
contentPath, keyword, impressions));
}
}
XPathNodeIterator 逐行通过每个 元素,从而应用一个 XPathExpression 来挑选每个属性和文本内容。当迭代每个 项之后,创建 ContentItem 实例,并为它的属性分配来自 XPathExpression 的值。每个 ContentItem 实例添加到 ContentItemCollection 实例,该实例稍后从 LoadFile(physicalFilePath) 方法返回。
以编程方式读取内容数据
为了以编程方式将项添加到任何类型的 Web 控件,您需要执行以下三个步骤:
1. | 创建一个表示要添加项的类。 |
2. | 创建一个表示项集合的类。 |
3. | 为该 Web 控件添加一个属性,其类型为在步骤 2 中定义的类。该属性保留用于该控件的项集。 |
要实际查看这些步骤,请考虑内置的 ASP.NET DropDownList Web 控件,该控件包含组成 DropDownList 的可用选择的项集合。每一项都由 ListItem 类的实例表示(步骤 1)。ListItemCollection 类提供强类型的 ListItem 实例集合(步骤 2),而且 DropDownList 类具有类型 ListItemCollection 的一个 Items 属性(步骤 3)。利用 ASP.NET 页的源代码片段,DropDownList 可以通过以下语法使 ListItem 实例以编程方式添加到其中:
DropDownList1.Items.Add(new ListItem(text, value));
为了使用 ContentRotator 实现类似的功能,我创建了 ContentItem 类来表示一个特定的内容项。如前所述,该类具有特定于内容项的属性,例如,Content 和 Impressions,等等。接下来,我创建了一个 ContentItemCollection 类,它提供 ContentItem 实例的强类型集合。最后,我在 ContentRotator 类中创建了一个 ContentitemCollection 类型的 Items 属性。
当创建了这些类和属性之后,页面开发人员能够以编程方式将内容添加到 ContentRotator 中,其语法与 DropDownList 的语法不同:
ContentRotator1.Items.Add(new ContentItem(content));
ContentRotator1.Items.Add(new ContentItem(content, contentPath,
keyword, impressions));
...
从该控件的声明性语法读取内容
除了能够以编程方式指定项之外,很多 Web 控件也可以使页面开发人员能够通过该控件的声明性语法指定项集。例如,通过 DropDownList,能够以声明方式说明 ListItem,如下所示:
<asp:DropDownList runat="server" ...>
<asp:ListItem Value="value1">text1</asp:ListItem>
<asp:ListItem Value="value2" Text="text2"></asp:ListItem>
...
<asp:ListItem Value="valueN">textN</asp:ListItem>
</asp:DropDownList>
将该功能添加到 ContentRotator 非常简单,这是因为我们已经定义了 ContentItem 类,而且还具有用于 ContentRotator 控件的 Items 属性。我们只需使用两个属性,以便指示用声明性语法指定的项映射到该 ContentRotator 的 Items 属性。
首先,在类级别添加 ParseChildren() 属性,从而指示应该解析该子标记,而且它映射到 Items 属性:
[ParseChildren(true, "Items")]
public class ContentRotator : Control
{
...
}
最后,将 PersistenceMode() 属性添加到 Items 属性声明,从而指示该 Items 属性将作为内部默认属性保留。
[PersistenceMode(PersistenceMode.InnerDefaultProperty]
public ContentItemCollection Items
{
get { ... }
}
这就是所有的步骤。通过这两个属性,页面开发人员能够用该控件的声明性语法指定内容项,如下所示:
<skm:ContentRotator runat="server" ...>
<skm:ContentItem Content="content" ContentPath="contentPath"
Keyword="keyword" Impressions="impressions"></skm:ContentItem>
...
</skm:ContentRotator>
需要注意的是,对于我们当前的代码,ContentItem 实例的所有属性必须作为 元素的属性指定。理想情况下,页面开发人员将能够指定静态内容,方法是利用 Content 属性或 和 标记之间的文本内容。为此,我们需要将一小段代码添加到 ContentItem 类,从而指示该类应该解析内部文本内容。
特别是,ContentItem 类需要实现 IParserAccessor 接口,该接口定义一个简单的方法 AddParsedSubObject(object)。AddParsedSubObject(object) 方法以声明性语法传入 元素的内容。如果内部内容是纯文本,则传入 LiteralControl 实例。在本例中,我们想将 LiteralControl 的 Text 属性分配给 ContentItem 实例的 Content 属性。这是通过以下代码实现的:
public class ContentItem : IParserAccessor
{
...
public void AddParsedSubObject(object obj)
{
if (obj is LiteralControl)
Content = ((LiteralControl) obj).Text;
else
throw new HttpException(...);
}
#endregion
}
选择一个随机内容项
当在 Load 事件中检索 ContentRotator 的 Items 属性之后,调用 SelectContentFromItems() 方法。该方法返回从 Items 集合随机选择的 ContentItem 实例。如果设置了 ContentRotator 的 KeywordFilter 属性,它将从集合中移除不可用的项。然后,它为剩余的项确定 impressions 参数的总数,并在 0 和 impressions 总数减去 1 所得的数之间随机选择一个数。基于该随机数选择并返回适当的内容项。
以下代码显示如何移除不可用项以及如何计算 impressions 的总数(在设置了 KeywordFilter 属性的情况下)。接下来,选择一个随机 impressions,并返回适当的 ContentItem 实例。
// Determine the sum of the Impressions
int totalWeight = 0, i = 0;
string controlsKeywordFilter = this.KeywordFilter;
ContentItemCollection filteredArray = new ContentItemCollection();
for (i = 0; i < Items.Count; i++)
// only add the content item to the list of filtered content
// items if the KeywordFilter property hasn't been set or,
// if it has, if the KeywordFilter matches the content item's
// keyword attribute.
if (controlsKeywordFilter.Length == 0 ||
CultureInfo.InvariantCulture.CompareInfo.Compare(Items[i].Keyword, controlsKeywordFilter,
CompareOptions.IgnoreCase) == 0)
{
totalWeight += Items[i].Impressions; // increment the totalWeight
filteredArray.Add(Items[i]);
}
// Randomly choose a number between 0 and totalWeight - 1
int randomWeight = random.Next(totalWeight);
totalWeight = 0;
// Now grab the appropriate ContentItem based on randomWeight
i = 0;
while (i < filteredArray.Count && (totalWeight +
filteredArray[i].Impressions) <= randomWeight)
totalWeight += filteredArray[i++].Impressions;
return filteredArray[i];
提供动态内容
确定了显示哪个内容项就成功了一半。我们也需要能够实际显示所选的内容项。这是在 SelectContentFromItems() 方法指定显示什么内容项之后,在 Load 事件中实现的。如果该项包含静态内容(即,如果它没有设置它的 ContentPath 属性),则会创建一个新 LiteralControl 实例,并将它添加到 ContentRotator 的控件层次结构,将其 Text 属性设置为要显示的静态内容的值。
但是,如果要显示的内容是动态的(即,设置了它的 ContentPath 属性,从而指定将提供动态内容的 User Control),则调用 Page 的 LoadControl(path) 方法加载特定 User Control 的控件层次结构。该控件层次结构的根通过 LoadControl() 方法返回,而且该控件添加到 ContentRotator 的控件层次结构。图 2 以图形方式阐释该过程。
![](http://www.microsoft.com/china/msdn/library/webservices/asp.net/art/ContentRotator2S.gif)
图 2.
默认情况下,ContentRotator 将随机选择要在每次 访问页时显示的内容项,包括针对相同页的回调。如果您需要跨回调来显示相同的随机检索内容项,则只需简单地将 ContentRotator 的 NewContentOnPostbacks 设置为 false。如果您要使用需要回调的动态内容(例如,一个提示用户进行某些输入,并利用回调显示关于该输入的详细信息用户控件),然后您需要将 NewContentOnPostbacks 设置为 false。如果您使 NewContentOnPostbacks 保留其默认值 true,则当该用户从随机选择的 User Control 回调时,ContentRotator 可能选择一个不同的内容项,而这样会丢失回调数据。
实际上,无论加载的内容是静态的还是动态的,如果 NewContentOnPostbacks 属性设置为 true,ContentRotator 的控件层次结构都会禁用它的视图状态。这是因为回调时,ContentRotator 可能选择与上次访问页面时不同的内容项。如果本例中 ContentRotator 不禁用视图状态,则如果已经随机选择了一个不同的内容项,将在加载视图状态阶段引发一个异常。但是,如果 NewContentOnPostbacks 属性设置为 false,则维持 ContentRotator 控件层次结构的视图状态。
注有关使用 LoadControl() 方法以编程方式加载用户控件的更多信息,请一定要阅读我以前撰写的文章 An Extensive Examination of User Controls。关于视图状态和相关问题的信息位于 Understanding ASP.NET View State。
自定义所选的内容项
ContentRotator 的功能要求之一是,允许页面开发人员在内容项中指定动态占位符。例如,页面开发人员应该能够创建带有内容文本“The current time is [[time]]”的内容项,并用当前系统时间动态取代 [[time]] 占位符。我得出两个方法来解决这个问题:
1. | 提供一个预定义的占位符集,用于进行动态替换。 |
2. | 允许页面开发人员决定使用什么占位符以及用什么值替换它们。 |
第一个选项是这两个中较容易实现的一个:我可以在 ContentRotator 控件的 Load 事件中进行检查,该事件可以使用相应的动态值系统地替换所选内容项中的所有预定义占位符。虽然该方法易于实现,但我想它不会提供页面开发人员所需的自定义级别。
因此,我将定义占位符并填充动态值的工作完全留给页面开发人员。页面开发人员可以制作他需要的任何占位符,从而在该内容文件中或通过声明性语法将它们添加到这些内容项的文本内容。我只需为页面开发人员提供映射到所选内容项的机制。为此,我使用 ContentRotator 公开一个 ContentCreated 事件。每次访问页面时,只要选择要显示的内容项,就会激发该事件。
页面开发人员可以创建一个用于该事件的事件处理程序。在该事件处理程序中,页面开发人员将接收所选的用于显示的 ContentItem 实例。此时,可以搜索该内容项的文本来获取要用其动态值替换的占位符。下面的示例阐明这一行为。ContentRotator 有三个通过声明性语法定义的内容项,其中两个带有动态占位符。
<skm:ContentRotator id="ContentRotator1" runat="server">
<skm:ContentItem Content="The current time is
[[time]]."></skm:ContentItem>
<skm:ContentItem Content="Hello, World! Nothing dynamic
here!"></skm:ContentItem>
<skm:ContentItem Content="Welcome to page
[[url]]."></skm:ContentItem>
</skm:ContentRotator>
在 ASP.NET 页的代码隐藏类中,创建事件处理程序并将其绑定到 ContentRotator 的 ContentCreated 事件。该事件处理程序的第二个参数是类型 ContentCreatedEventArgs,该参数在其 ContentItem 属性中包含所选的 ContentItem 实例。如以下代码所示,该事件处理程序使用当前系统时间替换 [[time]] 的任何实例,并使用当前页的 URL 替换 [[url]] 的任何实例。
private void ContentRotator1_ContentCreated(object sender,
skmContentRotator.ContentCreatedEventArgs e)
{
// Replace [[time]] with DateTime.Now.TimeOfDay
e.ContentItem.Content = e.ContentItem.Content.Replace("[[time]]",
DateTime.Now.TimeOfDay.ToString());
// Replace [[url]] with Request.RawUrl
e.ContentItem.Content = e.ContentItem.Content.Replace("[[url]]",
Request.RawUrl);
}
注占位符(在本例中是 [[time]] 和 [[url]])由页面开发人员决定。即页面开发人员可以使用他选择作为占位符的任何名称和标记。例如,我们可以使用 *currentTime*(而不是 [[time]])作为占位符,从而将 [[time]] 的所有实例替换为内容文件和事件处理程序中的 *currentTime*。
小结
随着 .com 泡沫的衰退,诸如 Microsoft 的 Ad Rotator 一类的控件正逐渐过时。但是,Ad Rotator 的概念是很好的,而且在很多方案中,一般内容滚动控件证明是无价的。本文展示的 ContentRotator 控件应该适合这些情况中的大多数。ContentRotator 提供的语义和语法与 Ad Rotator 控件的相似,包括用于内容项的一个 impressions 值,关键字筛选,以及一个允许页面开发人员映射所选内容项的事件。此外,ContentRotator 允许以声明方式指定它的内容项,并允许静态和动态内容。
祝大家编程愉快!
特别鸣谢。 . .
在我将文章提交给 MSDN 编辑之前,很多志愿者帮助校对了本文并针对本文的内容、语法和说明提供了反馈。在本文的校对过程中作出过主要贡献的人主要有 Julius Estrada、Hilton Giesenow、Milan Negovan、Carl Lambrecht 和 Jeffrey Palermo。校对人员这个团体正在不断扩增加,如果您想成为其中的一员,请通过 mitchell@4guysfromrolla.com 与我联系。
关于作者
Scott Mitchell(出版过有关 Web 开发的六本书籍,并且是 4GuysFromRolla.com 的创立者)在过去七年中一直研究 Microsoft Web 技术。Scott 是一名独立的顾问、讲师和作家。您可以通过 mitchell@4guysfromrolla.com 或他的网络日记 http://ScottOnWriting.NET 与他联系。
转到原英文页面