翻译|其它|编辑:郝浩|2006-03-13 09:56:00.000|阅读 1394 次
概述:
# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>
每次访问带有 ContentRotator 控件的页时,ContentRotator 必须决定要随机显示什么内容。每个内容项有一个相关的 impressions 值,该值影响到该内容项相对于其他内容项被选中的可能性。该 impressions 参数是一个正整数值,在不指定的情况下,默认值为 1。此外,每个内容项可以有一个可选的关键字 参数。如果指定 ContentRotator 控件的 KeywordFilter 属性,则将用于显示的内容项集合限制为那些具有匹配关键字 值的内容项。
用于随机选择内容项的算法的工作方式为:将每个可应用的内容项以端对端方式进行布局,从而形成一行。每个内容项的长度是它的 impressions 值,这意味着该行的整体长度是可应用的内容项 impressions 的总和。接下来,选择一个小于总长度的随机数,而要显示的内容项是位于随机数位置的内容项。图 1 以图形方式阐释该算法。
图 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++) // on// itemsly add the content item to the list of filtered content 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 以图形方式阐释该过程。
图 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 允许以声明方式指定它的内容项,并允许静态和动态内容。
本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com