让您的(自定义)Web 控件在 Page HEAD 中发声






4.40/5 (5投票s)
2004 年 11 月 3 日
11分钟阅读

50981

337
解决了Web控件需要在页面HEAD元素中添加内容的问题,例如依赖外部样式表、JavaScript或XML文件。
引言
Web控件是一个好东西,无论是Web控件、用户控件还是自定义Web控件;从现在开始,我将它们统称为Web控件。但最终,一切都归结为外观和功能。
本文将讨论那些你发现“事情不寻常”(比喻意义上)的时候。不寻常?这与ASP.NET有什么关系?嗯,这就是下一节的主题。
在我们继续之前,即使我将本文归类为需要初学者水平的知识,我也假设您对接口、HTML和基本的ASP.NET并不陌生。
背景
在我以前的生活中(作为一个网站开发者),我做了很多PHP的Web控件开发。不,PHP本身没有Web控件——这也是我最初被.NET吸引的地方——但你可以制作等同于控件的功能。当我开始开发.NET Web控件时——尽管我并没有开发很多——我突然发现自己面临一个明显没有消失的问题。这个问题是,当Web控件的外观或行为依赖于其他东西时,你该怎么办?
从现在开始,当我提到页面的HEAD
部分/元素时,我指的是在格式正确的HTML文档中,仅指<HEAD>
元素的内容。因此,它不是一个使所有页面顶部看起来都一样的模板标题。
那么,这个“其他东西”是什么?就我而言,当我的Web控件的行为依赖于某些JavaScript代码时,或者当它的外观依赖于某些样式(来自级联样式表)时,这种困境总是会出现。
控件不需要JavaScript(或其他支持的脚本语言)就能如预期般工作,但有时需要。大多数情况下,你可以内联放置脚本,但其他时候你也需要渲染同一个控件而又不重复代码。在这种情况下,通常的做法是将这种“卫星代码”放在一个单独的.js文件中,并在link
头部指定它。
同样,控件有时需要很多“化妆品”,即样式。你可以将其放在标签的style
属性中,这样工作就完成了。当样式非常复杂,以至于需要一个CSS类时,情况就出现了,在这种情况下,你只需使用标签的 class
属性。在前一种情况下,没有其他需要担心的事情,但在后一种情况下,你**需要**CSS类被定义为页面HEAD
中的文本块,以STYLE
元素的形式,或者通过LINK
元素引用外部级联样式表文件。无论哪种方式,这都放在页面的HEAD
部分,控件本身无法访问。当页面中有多个相同的Web控件实例时,情况也是如此。
如果你们问我,我会说应该有一种编程方法可以让控件指定这些东西,就像有Response.AddHeaders
一样,只是你永远不知道什么时候会有人(呸!)写一个没有 HEAD
部分的HTML页面,尽管工具通常会自动添加这个。
另一方面,如果你的控件只需要注册内联脚本,这些脚本会渲染在页面BODY
元素的某个位置,那么你最好使用Page
类的任何一个RegisterClientScriptBlock, RegisterStartupScript, RegisterOnSubmitStatement
或它们的组合。探索这些留给读者自己去好奇,因为那不是问题领域的一部分。
话虽如此,现在是时候谈谈我如何解决这个问题了。这不是解决问题的唯一方法,甚至可能不是最好的方法,但到目前为止,在我所有的搜索中,我还没有找到任何能够解决这个问题的东西,于是这个想法就变成了C#代码。
我的解决方案
我在这里描述的解决方案依赖于以下几点:
- 在页面的
HEAD
部分放置一个WebLiteral
控件。 - 在代码隐藏中定义
Literal
控件。 - 查询子控件,检查是否有任何控件需要在
HEAD
部分“发声”,即需要一些JavaScript或样式表文件引用。 - 让有此特殊需求的控件实现
IPageHeaderSubscriber
接口。
我确信会有人想出这个主题的另一种变体——这些论坛的好处之一——或者甚至更好,让这样的东西成为框架的一部分。就我自己而言,我很高兴我不再需要面对同样反复出现的问题了,现在我只需要这样做——加上第1项和第2项的小麻烦——工作就完成了。
理解代码
在这个部分,我不会描述每一小段代码,但我相信阅读我的代码不会是一次不愉快的经历。工艺不仅在于编写能正常工作的代码,还在于正确地记录它,因为几个月后,你将记不起很多事情(这是事实)。至少可以说,我坚信文档和编码标准。但好吧!让我们开始看看一些代码片段吧!.
这真的很简单!整个过程包含两件事,如下图所示。
- “有需求”控件应实现的
IPageHeaderSubscriber
。 - 执行工作的
PageHeaderSubscriber
类。
该接口定义了一组属性,实现该接口的控件类必须实现这些属性。它不描述能力,而是控件的需求,或者更委婉地说,控件的请求。这些属性由PageHeaderSubscriber
实例在处理子控件期间“逐一”查询,如果结果为true
,则继续调用相关方法以获取控件请求放置在页面 HEAD
中的信息。
/****************************************************************
* same control
****************************************************************/
/// The key is used to avoid duplicates, for example when multiple
/// instances of the same control appear on the same page.
string PageSubscriberKey { get; }
/****************************************************************
* Properties - These are queried IF the key above is not
* the same as any other control keys.
****************************************************************/
/// Gets whether the control depends on some file such
/// as a configuration file that if changed should cause
/// the web server to invalidate the current instances of
/// the page in the cache. <see cref="GetFileDependencies"/>
bool HasFileDependency { get; }
/// Returns true if the control needs to define some
/// Styles in the head section. <see cref="GetStyle"/>
bool NeedsStyle { get; }
/// Returns true if the control needs an external
/// Cascading Style Sheet file. <see cref="RegisterStyleSheets"/>
bool NeedsStyleSheet { get; }
/// Returns true if the control needs an external
/// (java)script defined in the Head section. <see cref="RegisterScriptFiles"/>
bool NeedsScript { get; }
PageHeaderSubscriber
类是为你做所有工作的类。它包含一个简单的构造函数,在构造函数中,你需要指定一个对当前页面实例(将在其中渲染控件)的引用,以及一个对将作为我们HEAD
部分占位符的Literal
控件实例的引用(稍后将详细介绍)。
/// Constructor. This overload explicitely indicates which
/// literal will hold the information we gather.
/// <param name="page">Page object we will handle </param>
/// <param name="headLiteral">Literal control place holder </param>
public PageHeaderSubscriber(System.Web.UI.Page page,
System.Web.UI.WebControls.Literal headLiteral)
{ ... }
然后,它还有一个简单的方法,接受代表这些页面的ASP.NET Web窗体的名称。这个名称是一个字符串,与你在“.aspx”页面ID
属性的 <Form>
标签中使用的名称完全相同。为什么名称需要给定而不是推导出来,前面已经在本文中解释过了。
public void Process(string formID) { ... }
如前所述,此方法应在Page_Load
期间调用以执行处理。此时,它已经知道form
的ID
/Name
以及构造函数中提供的Page
对象实例。它的第一个任务是查找与此表单ID对应的HtmlForm
实例。它通过调用页面的FindControl()
方法来实现这一点。
一旦找到表单控件的实例,它就会获取子控件列表。子控件列表是通过使用Controls
属性获取的,该属性返回一个ControlsCollection
值。然后,我们使用迭代器遍历每个子控件,并检查子控件是否实现了IPageHeaderSubscriber
;如果未实现,则跳过;否则,我们调用接口属性和方法,如前所述。需要注意的一点是,我们检查控件返回的键。此键有助于消除重复项,还可以帮助区分同一控件的审美上不同的版本,例如使用不同样式表的两个实例。
if (en.Current is IPageHeaderSubscriber)
{
... some code omitted ...
// Query each of the things we might need
IPageHeaderSubscriber ctrl = (IPageHeaderSubscriber)en.Current;
if (!keys.Contains(ctrl.PageSubscriberKey))
{
if (ctrl.HasFileDependency)
depFiles.AddRange(ctrl.RegisterFileDependencies());
if (ctrl.NeedsStyle)
pgStyles.Add(ctrl.RegisterStyleBlock());
if (ctrl.NeedsStyleSheet)
pgStyleSheets.AddRange(ctrl.RegisterStyleSheets());
if (ctrl.NeedsScript)
pgScripts.Add(ctrl.RegisterScriptFiles());
keys.Add(ctrl.PageSubscriberKey);
}
当子控件实现IPageHaderSubscriber
时,我们遍历每个接口属性。上面的代码片段显示了我们为每个符合条件的子控件进行的**数据收集**部分。然后,实际处理会按照以下代码片段所示执行。
if (this.pageHeaderLiteral != null)
{
this.pageHeaderLiteral.Text = "";
string headExtras = "";
// Files that are required. Handled directly by the Page object
if (depFiles.Count > 0)
DependentFileCollector(depFiles);
// These appear within <head><style>...<style><head>
if (pgStyles.Count > 0)
{
headExtras += ContentCollector(pgStyles, "style");
}
// These within a <link type='text/css' .. />
if (pgStyleSheets.Count > 0)
{
headExtras += StyleSheetCollector(pgStyleSheets);
}
// And these within <script ..> elements
if (pgScripts.Count > 0)
headExtras += ScriptCollector(pgScripts);
this.pageHeaderLiteral.Text = headExtras;
}
上面代码的开头显示了我们之前提到的Literal
控件是如何发挥作用的。这是在构造函数中提供给我们的实例。好奇的读者可能会问,为什么我在收集数据之前才检查实例是否为null
。答案也很简单,有一个额外的构造函数,适用于懒惰的程序员,它省略了这个Literal
控件的实例。这意味着,在这种情况下,我们也将在处理阶段搜索此控件。此Literal
控件预计具有 name
/ID
PageHeaderSubscriber
,因此如果您未在构造函数中提供它,我们会查找它,但控件必须存在,否则将不会进行进一步的处理。
上面的代码非常直接且自明,除了文件依赖项的添加。文件依赖项的处理阶段实际上使用MapPath()
方法将提供的虚拟/相对路径转换为物理路径,因为这正是Page.AddFileDependencies()
方法所期望的。这是唯一一个不出现在HEAD
部分的项目,因为它由Page
对象处理。
处理的最后一部分只是将所有收集和转换的数据粘贴成我们可以放入(由你)放置在ASPX文档HEAD
部分的Literal
控件中的内容。
使用代码
不用说,你只会将它用于需要此功能的ASP.NET页面,否则你无需理会它。所以,如果你想让你的ASPX页面支持“有要求的子控件”,你会这样做。
- 让控件实现接口(如果需要)。
- 在ASPX页面的
HEAD
部分放置一个Literal
ASP控件。 - 在页面的代码(或其代码隐藏)中声明
Literal
控件。 - 实例化处理程序并调用处理方法。
第一项已经处理完毕。第二项涉及将控件放置在下面的.aspx文件片段中所示的位置。这里只显示了相关部分,唯一例外的是我稍后会提到的SideBoxSimple
自定义Web控件。从这个片段中,你只需要记住在Literal
和Form
元素的ID
属性中使用的值。
<%@ Register TagPrefix="cc1" Namespace="Coralys.WebControls.SideBoxSimple"
Assembly="Coralys.WebControls.SideBoxSimple" %>
<HTML>
<HEAD>
<asp:Literal ID="PageHeaderSubscriber"
Runat="server"></asp:Literal>
<HEAD>
<BODY>
<form ID="Form1" method="post" runat="server">
... Rest of the ASPX (BODY, etc. here) ...
</form>
</BODY>
</HTML>
第三项及后续项如下所示,我们在这里声明了添加的Literal
(请注意其类型)。Literal
控件的变量名可以是任何名称,但代码片段中显示的ID
则不是。这个ID
(而且我觉得我必须强调这一点)是固定的。
public class WebForm1 : System.Web.UI.Page
{
protected System.Web.UI.WebControls.Literal PageHeaderSubscriber;
protected Coralys.WebControls.SideBoxSimple.SideBoxSimple SideBoxSimple1;
private void Page_Load(object sender, System.EventArgs e)
{
// Put user code to initialize the page here
Coralys.Web.PageHeaderSubscriber phsub =
new Coralys.Web.PageHeaderSubscriber(this.Page,
this.PageHeaderSubscriber);
phsub.Process("Form1");
}
}
很简单!Literal
控件已声明,我还显示了一个自定义Web控件的声明,该控件恰好实现了IPageHeaderSubscriber
。这个自定义控件将是另一篇文章的主题。
然后,在Page_Load
事件中,我做的第一件事是实例化我们的处理程序,为它提供执行后续工作所需的参数。然后,我们立即调用Process()
方法,正如荷兰语中所说的“Klaar Is Kees”(搞定)。没什么复杂的,对吧?.
关注点
当然!虽然我们不能保证任何子控件都在Page_Init
事件中创建,但我很惊讶地发现在Page_Load
事件中,无法以编程方式找出实现网页的ASP.NET窗体的名称(ID
, UniqueID
)。在调试时,您实际上可以看到它“已知”,可以通过查看 Quick Watch 窗口来找到,但有时所见并非所得,以编程方式(在 Command Window 中)查询相关属性会产生 null 值。这就是为什么窗体名称必须作为参数给出的原因。
此解决方案绝非完美,但鉴于我目前的时间限制,它能够满足我的需求。这个包不会递归地处理孙子控件,它只限制在第一层子控件。它也不提供一种像处理样式那样在HEAD部分生成内联(Java)脚本的方式,但这很容易实现。我个人避免依赖JavaScript,因此,使它成为一个“万能”解决方案并不是我的优先事项。
正如我的一位前同事指出的那样,Whidbey 版本将有一个 HtmlHead
控件,可能可以解决这个问题,但直到 Whidbey 成为主流之前,本文的解决方案(或其变体)还将继续发挥作用,您想要的是**今日**的解决方案,对吧?.
历史
- 2004-11-04 DEGT v1.0.1 将接口方法重命名为 Register*(),以符合Page类的语法。在Background部分添加了关于在
HEAD
元素外注册脚本块的信息。使用键属性避免重复。 - 2004-11-03 DEGT v1.0 初始版本。