ASP.NET 通用网页类库 - 第一部分






4.55/5 (32投票s)
一套用于 ASP.NET 应用程序的通用、可重用页面类。
目录
引言
这是我开发的一系列关于 ASP.NET 应用程序类库文章中的第一篇。它包含一套通用的、可重用的页面类,可以在 Web 应用程序中直接使用,以提供一致的外观、感觉和功能集。也可以从中派生出新类来扩展其功能。这些功能都是相当模块化的,也可以提取并放入您自己的类中。程序集中的类包括:
BasePage
- 本文介绍,在 第二部分 中继续介绍,它涵盖了该类的数据更改检查功能,以及在 第三部分 中介绍,它涵盖了该类允许其通过电子邮件发送渲染内容的相应功能。RenderedPage
、MenuPage
和VerticalMenuPage
- 本文介绍。这些类提供了一种方法,使 .NET 1.1 应用程序可以创建自动渲染所有通用页眉和页脚 HTML 标签的页面。PageUtils
- 这是一个实用类,用于 第四部分 中介绍的类库。它还涵盖了BasePage
类中一些与将验证消息转换为可点击链接相关的方法。
下载包含一个小型演示应用程序,使用 C# 和 VB.NET 编写,利用了这些类。要尝试演示应用程序,请在 IIS 中创建两个虚拟目录,并将它们指向两个演示文件夹。
- .NET 1.1 版本使用 \DotNet\Web\EWSWeb\EWSWebDemoCS 目录,虚拟目录名称为 EWSWebDemoCS11,以及 \DotNet\Web\EWSWeb\EWSWebDemoVB 目录,虚拟目录名称为 EWSWebDemoVB11。
- .NET 2.0 版本使用 \DotNet20\Web\EWSWeb\EWSWebDemoCS 目录,虚拟目录名称为 EWSWebDemoCS20,以及 \DotNet20\Web\EWSWeb\EWSWebDemoVB 目录,虚拟目录名称为 EWSWebDemoVB20。
每个应用程序的启动页是 Default.aspx。演示项目设置为在安装了 Visual Studio .NET 和 IIS 的开发计算机上编译和运行。如果您使用的是远程服务器,则需要设置虚拟目录,生成项目,然后将其复制到服务器位置。首次在设计视图中打开页面时,可能会收到无法查看的错误。如果出现这种情况,请重新生成项目,以便包含基类的程序集存在。
对于演示中的电子邮件部分,您需要 Web 服务器上的 SMTP 服务或对独立 SMTP 服务器的访问。错误页面演示使用存储在 Web.config 文件中的电子邮件地址,该地址目前设置为一个占位符地址。您应该修改 appSettings
部分中由 ErrorRptEMail
键指定的地址,使其有效。电子邮件页面功能还可以使用可选的配置选项来控制 SMTP 服务器的名称(EMailPage_SmtpServer
)。
嵌入式资源
该类库包含一些客户端脚本文件。为了避免单独分发和安装它们,它们被嵌入到程序集中作为资源,这些资源在运行时会被提取并返回给客户端浏览器。有关此实现方式的更多信息,请参阅包含的帮助文件和以下 **Code Project** 文章:A Resource Server Handler Class For Custom Controls [^]。
.NET 1.1 版本中的嵌入式资源要求在 Web.config 文件中添加一个 HTTP 处理程序条目。这是一个简单的过程,只需要将定义从演示程序复制并粘贴到您自己的项目的 Web.config 文件中即可。有关更多信息,请参阅提供的帮助文件和上述文章。.NET 2.0 提供了一种内置的提供嵌入式资源的方法,因此对于使用该程序集 .NET 2.0 版本的应用程序来说,此步骤不是必需的。
脚本压缩
脚本在项目生成步骤中也会被压缩,使用的 JavaScript 压缩器在文章 A JavaScript Compression Tool for Web Applications [^] 中进行了描述。这通过删除注释和多余的空格来减小脚本的大小,从而占用更少的空间。如果您不想使用脚本压缩,可以通过打开项目,在 **解决方案资源管理器** 中右键单击项目名称,选择 **属性**,展开 **通用属性** 文件夹,然后选择 **生成事件** 子项来将其从预生成步骤中删除。单击 **预生成事件命令行** 选项,然后删除您看到的命令行。将 ScriptsDev 文件夹中的脚本复制到 Scripts 文件夹以替换库中分发的现有压缩版本。如果不再使用压缩器,可以从项目中删除 ScriptsDev 文件夹。
在项目中使用的程序集
BasePage
类以及库中的其他类都包含在示例项目中。它们被编译成一个程序集,您可以在自己的项目中使用它来引用。下面的章节描述了该类每个主要功能以及用于实现这些功能的方法和属性。如果您已经在应用程序中使用自定义页面类,可以直接提取您感兴趣的部分来包含在您自己的项目中。
源代码下载中包含一个 HTML 帮助文件,其中对程序集中的类进行了更详细的文档说明。它使用 NDOC [^] 从源代码中的 XML 注释生成。帮助文件第一页标题为 **使用说明**,描述了如何在自己的项目中安装和使用该程序集。有关更多信息,请参考它。
代码是用 C# 编写的。但是,由于 .NET 是语言中立的,因此该程序集在用于其他语言(如 VB.NET)的项目中可以完全按原样使用。下面的类文档使用 C# 声明形式呈现属性和方法。下载文件包含演示应用程序的 C# 和 VB.NET 版本。上述帮助文件显示了 C# 和 VB.NET 的类声明和示例代码。
在您自己的应用程序中使用 BasePage 及其派生类
在您自己的应用程序中使用页面类非常简单。只需按照以下步骤操作:
- 如果尚未完成,请在您的项目中添加对 EWSoftware.Web 程序集的引用。
- .NET 1.1 应用程序,请将必要的设置添加到 Web.config 文件中。有关更多信息,请参阅提供的帮助文件。
- 向您的项目添加一个新的 Web 窗体。
- 打开窗体的代码隐藏模块。
- 在代码模块中添加一个
using EWSoftware.Web;
语句(VB.NET 使用Imports EWSoftware.Web
)。 - 在类声明中,将对
System.Web.UI.Page
作为基类的引用替换为对EWSoftware.Web
命名空间中的一个页面类(BasePage
、RenderedPage
、MenuPage
或VerticalMenuPage
)或派生自它们的您自己的类的引用。 - 通常,您会在
Page_Load
事件中添加代码来设置页面标题和其他属性,并将初始焦点设置到页面上的第一个控件。这应仅在初始页面加载时完成,而不是在回发时完成,因为其他事件可能已更改页面标题或将焦点设置到其他控件。例如:private void Page_Load(object sender, System.EventArgs e) { if(!Page.IsPostBack) { this.PageTitle = "My Page Title"; this.PageDescription = "My Test Page"; this.Robots = RobotOptions.Index | RobotOptions.Follow; this.SetFocusExtended(txtFirstField); ... Do other stuff for initial page load ... } }
- 返回设计视图,并以常规方式向表单添加控件。
如果您决定使用 RenderedPage
类,您还需要执行以下操作:
- 在设计视图中打开新窗体,并切换到 HTML 视图。
- 删除
<!DOCTYPE>
标签、打开的<html>
和关闭的</html>
标签、<head>
部分,以及打开的<body>
和关闭的</body>
标签。新页面中应该只有<%@ Page>
指令、打开的<form>
标签和关闭的</form>
标签。
设计视图中无样式
在使用 RenderedPage
类及其派生类时的一个问题是,当整个支持性页眉 HTML 存在于 ASPX 页面中时,您会丢失正常的样式设置。解决此问题的方法是在页面顶部暂时添加一个 <link>
标签,该标签指向应用程序样式表,以便在设计页面初始布局时使用。完成后请务必将其删除。
对于 .NET 2.0,一个更好的解决方案是使用母版页而不是 RenderedPage
。这允许您拥有与 RenderedPage
类相同的功能,但具有更大的灵活性。但是,您仍然可以从 BasePage
派生您的页面类,以获得它提供的额外功能。
BasePage 类
本文介绍了一个名为 BasePage
的类,该类派生自 System.Web.UI.Page
。它可以作为任何 ASP.NET 应用程序中页面的基类,也可以用于派生包含 Web 应用程序页面中常见附加功能的新类。BasePage
类包含以下有用功能:
- 提供属性来自定义或更改通用页眉标签(例如,页面标题、描述、关键字、样式表、robot 选项、其他页眉标签等)。对于 .NET 1.1 版本,这些属性由
RenderedPage
类使用。对于 .NET 2.0 版本,只要存在具有runat="server"
标签的head
控件,BasePage
就会使用它们。 - 提供属性,以便在
BasePage
类派生的类中更轻松地将用户控件和支持结构插入到页面的form
控件中。 - 提供方法,允许您在一次调用中启用或禁用一个或多个控件。支持允许设置 CSS 类以更好地显示禁用状态。
- 提供服务器端方法和客户端代码,允许您将焦点设置到页面上的任何控件。控件可以是页面本身上的普通控件,也可以是嵌入在其他控件中的控件,例如
DataGrid
的EditItemTemplate
中的控件,这些控件可能在页面渲染之前不存在。 - 对于数据录入表单,提供属性和客户端代码,允许您自动跟踪表单的脏状态。对于 **Internet Explorer**,客户端代码还可以提示用户在执行可能导致数据丢失的操作(如离开页面、关闭浏览器等)之前保存更改。这在本文开头提到的另一篇文章中有介绍。
- 提供
AuthType
属性,允许您获取应用程序生效的身份验证方法(匿名、基本、NTLM 或 Kerberos)。 - 覆盖
OnError
方法,将有关错误原因的更多上下文信息保存到应用程序缓存中,以便可以将其传递给自定义错误页面。
启用和禁用表单控件
启用和禁用控件只需将其 Enabled
属性设置为 true
或 false
即可。但是,我发现 Web 页面上禁用控件的默认视觉样式有时很难与启用控件区分开来。因此,我添加了一个属性来允许指定一个更好的显示禁用状态的备用样式。对于我自己的应用程序,我使用 CSS 样式来设置银色背景,从而更好地指示禁用状态,这与 Windows Forms 应用程序类似。下面的方法在禁用控件时使用此属性。为了节省打字,提供了方法的重载,允许您指定一个包含两个或多个控件的列表,以一次性启用或禁用它们。还提供了一个方法,允许您一次性启用或禁用页面上的所有控件。当页面包含许多控件或像面板那样包含嵌套控件时,这可以节省大量时间。
public string DisabledCssClass
此属性用于获取或设置禁用控件的 CSS 类。CSS 类名称应出现在与应用程序关联的样式表中。如果未设置或设置为 null,该属性将使用
BasePage.DisabledCssName
常量定义的样式名称。目前已将其设置为样式名称 **Disabled**。
public void SetEnabledState(WebControl ctl, bool enabled)
此方法用于启用或禁用单个控件。如果禁用,并且控件是
TextBox
、DropDownList
或ListBox
(或它们的派生类),则会将样式类设置为DisabledCssClass
属性指定的样式。启用控件时,此方法会调用以下重载,并将空字符串作为正常样式。public void SetEnabledState(WebControl ctl, bool enabled, string normalClass)
此方法与上面相同,但它允许您为
TextBox
、DropDownList
和ListBox
指定正常样式类名称。如果您已显式指定了启用状态的样式,并在启用控件时需要恢复它,则可以使用它。而不是像前面的方法那样用空字符串清除样式,您可以使用此版本将其替换为指定的样式。public void SetEnabledState(WebControl ctl, bool enabled, string normalClass) { if(ctl == null) throw new ArgumentNullException("ctl", "The control cannot be null"); ctl.Enabled = enabled; if(ctl is System.Web.UI.WebControls.TextBox || ctl is System.Web.UI.WebControls.DropDownList || ctl is System.Web.UI.WebControls.ListBox) if(enabled) ctl.CssClass = normalClass; else ctl.CssClass = this.DisabledCssClass; }
public void SetEnabledState(bool enabled, params WebControl[] ctlList)
此方法用于一次性启用或禁用多个控件。只需向其传递要设置的状态和要启用或禁用的控件列表。禁用
TextBox
、DropDownList
或ListBox
控件(或它们的派生类)时,它会将样式类设置为DisabledCssClass
属性指定的样式。启用此类控件时,它会清除样式类。代码与单个控件方法相同,只是它被包装在foreach
循环中,该循环遍历传入的控件数组。public void SetEnabledState(string normalClass, bool enabled, params WebControl[] ctlList)
此方法与上面相同,但它允许您为
TextBox
、DropDownList
和ListBox
指定正常样式类名称。如果您已显式指定了启用状态的样式,并在启用控件时需要恢复它,则可以使用它。public void SetEnabledAll(bool enabled, System.Web.UI.Control ctlPageForm)
这可以用于禁用或启用 Web 页面、表单、面板或选项卡控件上的所有编辑控件。如果遇到其他容器控件(如
Panel
)以在其中启用或禁用控件,该方法将递归地调用自身。请注意,按钮和链接不会被此方法禁用,因为您很可能希望它们保持启用状态以执行退出页面等操作。如果您确实希望禁用某些按钮,则必须单独调用上述方法。由于所有控件都可能被禁用,因此缺乏区分它们和启用控件的明显禁用样式不是问题,因此此方法不会以任何方式更改它们的样式。public void SetEnabledAll(bool enabled, Control ctlPageForm) { Control form = null; string controlType; // If null, default to the current page if(ctlPageForm == null) ctlPageForm = this.PageForm; // Yes, I could add a reference to the MS IE Web // Controls, but I don't want this library to have a // dependency on it so we'll just check for IE Web // Controls by type name string instead. controlType = ctlPageForm.ToString(); // If passed a form, panel, multi-page, or page view, // use it directly. If passed a page, see if it // contains a form. If so, use that form. If not, use // the page. if(ctlPageForm is System.Web.UI.HtmlControls.HtmlForm || ctlPageForm is System.Web.UI.WebControls.ContentPlaceHolder || ctlPageForm is System.Web.UI.WebControls.Panel || ctlPageForm is System.Web.UI.WebControls.MultiView || ctlPageForm is System.Web.UI.WebControls.View || controlType.IndexOf("MultiPage") != -1 || controlType.IndexOf("PageView") != -1) { form = ctlPageForm; } else if(ctlPageForm is System.Web.UI.Page && ctlPageForm != this.PageForm) form = BasePage.FindPageForm((Page)ctlPageForm); // Ignore anything unexpected if(form == null) return; // Disable each edit control on the page foreach(Control ctl in form.Controls) if(ctl is System.Web.UI.WebControls.TextBox || ctl is System.Web.UI.WebControls.DropDownList || ctl is System.Web.UI.WebControls.ListBox || ctl is System.Web.UI.WebControls.CheckBox || ctl is System.Web.UI.WebControls.CheckBoxList || ctl is System.Web.UI.WebControls.RadioButton || ctl is System.Web.UI.WebControls.RadioButtonList) ((WebControl)ctl).Enabled = enabled; else { // As above, done this way to avoid a dependency controlType = ctl.ToString(); if(ctl is System.Web.UI.WebControls.ContentPlaceHolder || ctl is System.Web.UI.WebControls.Panel || ctl is System.Web.UI.WebControls.MultiView || ctl is System.Web.UI.WebControls.View || controlType.IndexOf("MultiPage") != -1 || controlType.IndexOf("PageView") != -1) this.SetEnabledAll(enabled, ctl); // Recursive } }
如上所示,此方法可以识别 Microsoft Internet Explorer Web Controls
MultiPage
和PageView
,并且还将启用或禁用其中包含的控件。请注意,由于支持它的实现方式,它不依赖于该程序集。它不检查类型并创建对程序集的依赖,而是选择通过名称使用字符串来检查它们。这使得程序集独立于 IE Web Controls 程序集,并且不会强制开发者在不使用它时也将其包含进来。它还可以通过类似的方式扩展以检查其他类名和控件。唯一的潜在缺点是它通过硬编码类名作为文本字符串。但是,我认为为了使程序集没有依赖性并仍然提供非常有用的服务,这是值得的。有关 Internet Explorer Web Controls 的更多信息,请参阅 ASP.NET [^] 的 **Source Projects** 部分。
设置控件焦点
在 ASP.NET 2.0 之前,我在新闻组上看到了几个关于如何将焦点设置到 Web Form 控件的解释请求,因为 .NET 1.1 Web 控件缺乏任何 Focus
方法。缺乏此类方法是有道理的,因为设置焦点是客户端功能而不是服务器端功能。因此,设置焦点 falls to the page class itself,因为它必须生成客户端脚本来完成此操作。为了解决这个问题,BasePage
提供了两种方法来处理此任务。然而,仅发出一条简单的 JavaScript 代码来调用控件的 focus()
方法是不够的。要设置焦点的控件可能嵌入在另一个控件中,例如 DataGrid
,并且可能在服务器端请求设置焦点时不存在,并且可能不会获得设计时分配的预期控件 ID。因此,该库包含一个客户端脚本模块,该模块在设置控件焦点方面具有一些扩展能力。稍后将对此进行描述。以下是可用于设置控件焦点的两个类方法。在 ASP.NET 2.0 中,所有控件都有一个 Focus
方法。此外,Page
类包含两个 SetFocus
方法,与 BasePage
中的两个方法类似。为避免冲突,BasePage
中的两个方法被命名为 SetFocusExtended
。SetFocusExtended
方法可用于 .NET 1.1 来设置控件焦点,也可用于 .NET 2.0 版本,以防您需要它们提供的额外功能。
/// <summary>
/// This sets the control that should have the focus when the
/// page has finished loading by control reference.
/// </summary>
public void SetFocusExtended(WebControl ctl)
{
if(ctl != null)
{
focusedControl = ctl.ClientID;
findControl = false;
}
else
focusedControl = null;
}
/// <summary>
/// This sets the control that should have the focus when the
/// page has finished loading by control ID.
/// </summary>
public void SetFocus(string clientID)
{
focusedControl = clientId;
findControl = true;
}
对于窗体控件的子控件且未嵌入其他控件(如数据网格)的控件(即,它们是窗体本身上显示的普通控件),请使用第一个版本。该方法获取控件的客户端 ID 并将其存储在私有 focusedCtl
变量中。私有 findCtrl
变量设置为 false
,用于告知客户端代码通过精确匹配 ID 值来查找控件。这将在下面进行解释。
第二个版本将要设置焦点的控件 ID 作为 string
传递,对于设置嵌入在其他控件中的控件(例如数据网格的编辑项模板中的控件或在运行时动态创建并添加到窗体中的控件)很有用。对于嵌入式控件,控件或其客户端 ID 可能在您要设置焦点时不存在,因此这允许使用设计时控件 ID 来设置。与之前一样,focusedCtrl
变量设置为指定的控件 ID。但这次,findCtrl
变量设置为 true
,用于告知客户端代码搜索 ID 以指定 ID 值结尾的控件。容器(如 DataGrid
)会更改其中控件的 ID,以使它们都保持唯一。因此,客户端代码必须通过搜索以指定值结尾的 ID 来定位它。例如,如果您将一个 TextBox
放在 EditItemTemplate
中并给它一个 ID txtName
,实际渲染的控件 ID 可能看起来像 dgGrid:_ctl5:txtName
。客户端代码将搜索窗体上的所有控件,查找以 txtName
结尾的控件,并将其设置为焦点。
要清除焦点,请将 null
(VB.NET 中为 Nothing
)传递给任一方法。由于重载,您在执行此操作时需要使用类型转换,让编译器选择一个版本。哪个版本都可以。例如:
// Clear the focus
this.SetFocus((string)null);
OnPreRender()
方法被重写,如果调用了上述任一方法,则注册包含客户端焦点代码的脚本模块。它会生成一行启动脚本,该脚本调用代码模块中的函数,并将上述两个变量的值作为参数传递,然后将脚本注册到页面。您还会看到,如果页面有任何验证器,则会渲染设置焦点代码。这用于支持 ConvertValMsgsToLinks()
方法,该方法用于将 ValidationSummary
控件中显示的验证消息转换为可点击的链接,这些链接可用于将用户带到生成验证错误的字段。该方法及其相关代码在本文系列的另一篇文章中详细介绍,该文章涵盖了 PageUtils
类。请参阅本文开头的目录以获取链接。
function BP_funSetFocus(strID, bFindCtrl)
{
var nPgIdx, nIdx, nPos, ctl, ctlParent, htmlCol;
// Do we need to find the control by partial ID?
if(bFindCtrl == false)
{
ctl = document.getElementById(strID);
// Search for the control if it was found by the
// NAME attribute rather than by ID (i.e. the ID
// matched a NAME attribute on a META tag).
if(ctl != null && typeof(ctl) != "undefined" &&
(typeof(ctl.id) != "string" || ctl.id != strID))
bFindCtrl = true;
}
if(bFindCtrl == true)
{
// True name is unknown. Find the control ending
// with the specified name (i.e. it's embedded in
// a data grid).
htmlColl = document.getElementsByTagName("*");
for(nIdx = 0; nIdx < htmlColl.length; nIdx++)
{
ctl = htmlColl[nIdx];
if(typeof(ctl.id) != "undefined")
{
nPos = ctl.id.indexOf(strID);
if(nPos != -1 && ctl.id.substr(nPos) == strID)
break;
}
else
ctl = null;
}
}
// If not found, exit
if(ctl == null || typeof(ctl) == "undefined")
return false;
客户端 JavaScript 函数 BP_funSetFocus()
接收要设置焦点的控件 ID 和一个布尔标志,该标志指示是否应按部分名称查找控件。如果 find 标志为 false
,它会调用 document.getElementByID()
来获取对具有指定 ID 的控件的引用。如果为 true
,它将搜索页面上的所有控件元素,查找 ID 以指定值结尾的控件。如果找不到具有指定精确或部分 ID 的控件,该函数将退出,什么也不会发生。如果找到控件,并且在 Internet Explorer 上运行,则将执行以下代码段:
// NOTE: This section is IE-specific.
// See if there is a parent element. If so, work back up the chain
// to see if the control is embedded in an PageView IE Web Control.
// If so, select that page before giving focus to the control. If
// not, it may not work as the control may not be visible.
if(typeof(ctl.parentElement) != "undefined")
{
ctlParent = ctl.parentElement;
while(ctlParent != null && ctlParent.tagName != "PageView")
ctlParent = ctlParent.parentElement;
// If found, set the page as the active one in the containing
// MultiPage control.
if(ctlParent != null && ctlParent.tagName == "PageView")
{
nPgIdx = ctlParent.PageIndex;
ctlParent = ctlParent.parentElement;
if(ctlParent != null && ctlParent.tagName == "MultiPage")
{
ctlParent.selectedIndex = nPgIdx;
// We also have to set the index of any TabStrip
// associated with the MultiPage.
htmlColl = document.getElementsByTagName("TabStrip");
for(nIdx = 0; nIdx < htmlColl.length; nIdx++)
if(htmlColl[nIdx].targetID == ctlParent.id)
{
htmlColl[nIdx].selectedIndex = nPgIdx;
break;
}
}
}
}
// End IE-specific section
如上所述,代码会检查父元素。如果存在,它会沿着链向上查找,以确定控件是否嵌入在 PageView
Internet Explorer Web 控件中。如果是,它将确保首先选择正确的页面视图和选项卡,然后再将焦点设置到控件。如果未执行此操作,当当前选定的页面视图不是包含要设置焦点的控件的视图时,代码将生成错误。我只使用 Internet Explorer Web Controls 来为我的应用程序提供选项卡页面支持,因此它们是我唯一知道的。如果您使用不同的选项卡和页面视图控件,您可能可以修改上面的部分来检测它们并提供类似的支持。有关 Internet Explorer Web Controls 的更多信息,请参阅 ASP.NET [^] 的 **Source Projects** 部分。
// Focus the control. If it's a table, we may have been asked to
// set focus to a radio button or checkbox list. If so, select
// the control in the first cell of the table.
if(ctl.tagName == "TABLE")
{
ctl = ctl.cells(0);
ctl = ctl.firstChild;
}
ctl.focus();
// If it is a textbox-type control, select the text in the control
if(ctl.type == "text" || ctl.tagName == "TEXTAREA")
ctl.select();
return false;
最后一部分实际上是将焦点设置到在第一步中找到的控件。单选按钮和复选框列表控件可以在表格内生成其元素。当被要求在服务器端将焦点设置到此类控件时,您最终会得到包含单选按钮或复选框的表格的引用。如果尝试将焦点设置到表格控件,它仅在 Internet Explorer 中有效,并且只会滚动页面使表格在屏幕上可见,而您不会看到第一个复选框或单选按钮周围的焦点矩形。在 Netscape 中,尝试将焦点设置到表格元素根本不起作用。因此,首先进行检查以确定找到的控件的标签名是否为 HTML 表格。如果是,则实际获得焦点的控件设置为表格的第一个单元格的第一个子元素。这样做允许焦点设置到实际的单选按钮或复选框控件,这在 Internet Explorer 和 Netscape 中都有效。
一旦控件获得焦点,将进行最后一次检查,以确定控件是否为文本框或文本区域控件。如果是,则会选中其内容,以模仿进入控件时的选项卡行为。正如您所见,正确地在所有情况下将焦点设置到控件比最初看起来要复杂得多。
检测身份验证类型
向类添加了 AuthType
属性,以便用户可以了解应用程序正在使用的身份验证方法,例如匿名、基本、NTLM 或 Kerberos。
public string AuthType
{
get
{
// This prevents an exception being reported in
// design view.
if(this.Context == null)
return null;
// Figure out the authentication type
string authType =
Request.ServerVariables["AUTH_TYPE"];
if(authType == "Negotiate")
{
// Typically, NTLM will yield a header that
// is 300 bytes or less while Kerberos is
// more like 5000 bytes. If blank, the best
// we can do is return "Negotiate".
string authorization =
Request.ServerVariables["HTTP_AUTHORIZATION"];
if(authorization != null)
if(authorization.Length > 1000)
authType = "Kerberos";
else
authType = "NTLM";
}
else // If length != 0, it's probably Basic
if(authType.Length == 0)
authType = "Anonymous";
return authType;
}
}
在工作中,我们为内部网应用程序实现了集成的 Windows 身份验证(使用 Kerberos)。此属性在确定事物是否按预期工作方面非常有用。为了区分 NTLM 和 Kerberos 身份验证,它依赖于 HTTP_AUTHORIZATION
服务器变量的长度。如注释所示,NTLM 标头比 Kerberos 标头短得多。请注意,HTTP_AUTHORIZATION
变量仅在请求应用程序的第一个页面时可用。在所有后续页面请求中,它都不存在,因此最好返回提供的 AUTH_TYPE
值“Negotiate”。
该类还包含一个 CurrentUser
属性,可用于获取已验证用户的 ID。它返回 User.Identity.Name
属性的值,如果存在域限定符,则将其删除。例如,如果它是 MYDOMAIN\EWOODRUFF,则此属性返回 EWOODRUFF。这可以节省您检查和删除它的时间,如果您不需要的话。
增强错误信息
当您的应用程序出现错误时,尽可能多地获取信息总是有益的,以帮助您复制问题并找到错误的根源。ASP.NET 显示的默认错误页面提供了有关错误原因的基本信息。它还允许您覆盖错误处理功能并指定自定义错误页面。有许多可用选项,例如将信息写入事件日志、将其写入服务器上的文本文件、将其通过电子邮件发送给开发人员等。执行写入事件日志之类的操作需要服务器上的特殊权限。因此,我决定将 BasePage
类的错误处理行为保持相当通用。有关如何处理错误信息的决定推迟到自定义错误页面。它可以根据应用程序或环境进行修改,以根据需要记录或显示信息。您可能还会发现,一旦编写好,自定义错误页面就可以在不更改的情况下从一个应用程序复制到另一个应用程序,从而在您所有应用程序中提供相同的错误处理方法。
通常,在到达自定义错误页面时,详细错误信息是不可用的。为了克服此限制,该类会覆盖 OnError
方法,然后将信息打包并存储在应用程序缓存中。然后,自定义错误页面可以检索该信息并根据需要完成其报告错误的任务。
protected override void OnError(System.EventArgs e)
{
string remoteAddr;
Hashtable htErrorContext = new Hashtable(5);
SortedList slServerVars = new SortedList(9);
// Extract a subset of the server variables
slServerVars["SCRIPT_NAME"] =
Request.ServerVariables["SCRIPT_NAME"];
slServerVars["HTTP_HOST"] =
Request.ServerVariables["HTTP_HOST"];
slServerVars["HTTP_USER_AGENT"] =
Request.ServerVariables["HTTP_USER_AGENT"];
slServerVars["AUTH_TYPE"] = this.AuthType;
slServerVars["AUTH_USER"] =
Request.ServerVariables["AUTH_USER"];
slServerVars["LOGON_USER"] =
Request.ServerVariables["LOGON_USER"];
slServerVars["SERVER_NAME"] =
Request.ServerVariables["SERVER_NAME"];
slServerVars["LOCAL_ADDR"] =
Request.ServerVariables["LOCAL_ADDR"];
remoteAddr = Request.ServerVariables["REMOTE_ADDR"];
slServerVars["REMOTE_ADDR"] = remoteAddr;
// Save the context information
htErrorContext["LastError"] =
Server.GetLastError().ToString();
htErrorContext["ServerVars"] = slServerVars;
htErrorContext["QueryString"] = Request.QueryString;
htErrorContext["Form"] = Request.Form;
htErrorContext["Page"] = Request.Path;
// Store it in the cache with a short time limit. The
// remote address is used as a key. We can't use the
// session ID or store the info in the session as it's
// not always the same session on the error page.
Cache.Insert(remoteAddr, htErrorContext,
null, DateTime.MaxValue, TimeSpan.FromMinutes(5));
base.OnError(e);
}
代码创建一个排序列表来包含几个有用的服务器变量,例如正在生效的身份验证方法、用户信息、服务器信息和脚本信息。我只保存了过去发现有用的那些,而不是全部存储。如有必要,您可能希望在列表中添加更多内容。列表与最后错误信息、查询字符串、表单变量和页面名称一起存储在一个哈希表中。为了将信息传递给自定义错误页面,哈希表使用远程地址作为键存储在应用程序缓存中。对象应用了五分钟的时间限制,这样它就不会在缓存中停留过长时间而浪费资源。自定义错误页面在检索到对象后也可以将其从缓存中删除。此方法对大多数应用程序都有效。除非您预计用户数量非常庞大,并且发生了每个人都遇到的意外错误,否则它不会给服务器带来太大负担。
如上所述,错误信息不是存储在会话中,也不使用会话 ID 作为键。原因是根据我的经验,有时当到达错误页面时,会话 ID 会完全不同,因此我们无法检索信息。我通常在应用程序的第一个加载页面发生错误时注意到这一点。通过使用应用程序缓存并使用远程地址作为键,我们可以确保错误信息在到达错误页面时始终可供错误页面访问。
要使 ASP.NET 调用您的自定义错误页面,您需要修改 Web.config 文件中的 customErrors
标记,以便 defaultRedirect
属性指向您的错误页面,并将 mode
属性设置为 On
或 RemoteOnly
。
<system.web>
<!-- CUSTOM ERROR MESSAGES
Set customErrors mode="On" or "RemoteOnly" to
enable custom error messages, "Off" to disable.
Add <error> tags for each of the errors
you want to handle.
-->
<customErrors mode="RemoteOnly"
defaultRedirect="ErrorPageInternal.aspx" />
</system.web>
下载文件中的演示项目包含一个示例,该示例显示了在发生错误后从应用程序缓存中检索到的信息。
RenderedPage 类
这里和几个其他网站上都有关于创建 ASP.NET Web Forms 的基页或模板类的原因和各种方法的文章。因此,我在这里不再赘述。相反,请参阅以下 Code Project 文章以获取更多关于为什么以及如何使用这些方法的背景信息。RenderedPage
类包含我的特定实现,其各种功能将在本文的其余部分进行介绍。
ASP.NET 2.0 包含了一个新的母版页功能,允许您更灵活地实现页面模板。因此,如果您正在使用 ASP.NET 2.0,我建议使用母版页而不是 RenderedPage
。但是,您仍然可以从 BasePage
派生您的页面类,以获得本文和其他系列文章中所述的功能,并将它们与母版页结合使用。
请注意,BasePage
类的 .NET 2.0 版本确实支持使用 PageDescription
、PageKeywords
、PageTitle
和 Robots
属性。为了使用它们,只需确保您的页面或母版页包含一个带有 runat="server"
属性的 head
标签。渲染时,该类将为您添加适当的标签到页眉控件。这允许您在不进行任何额外编码的情况下逐页修改它们。
生成通用的页眉和页脚标签
RenderedPage
类将渲染所有通用页眉和页脚标签,包括 DOCTYPE
标签、html
的开始和结束标签、head
的开始和结束标签、一些通用 meta
标签(如描述、关键字和 robot 说明)、一个用于样式表的 link
标签(可以通过 PageStyleSheet
属性定义)、一个包含通过 PageTitle
属性设置的页面标题的 title
标签,以及 body
标签的开始和结束标签。body
标签可以修改,以包含由 PageBodyStyle
属性定义的 class
属性,以更改页面正文的样式。有关每个渲染方法的详细说明,请参阅提供的帮助文件。
覆盖了 Render
方法以控制渲染过程。除非必要,否则您不应覆盖此方法来更改页面内容的渲染。相反,请根据需要覆盖以下 virtual
方法。OnInit
也可以被覆盖,以通过 PageForm
属性插入控件。MenuPage
类包含一个这方面的示例。
protected virtual void RenderHeader(HtmlTextWriter writer)
此方法首先被调用,用于渲染从
DOCTYPE
标签到body
标签开始的通用页眉标签。代码很简单,使用StringBuilder
生成要渲染的 HTML。在添加head
标签的闭合标签之前,StringBuilder
被传递给以下方法。当此方法返回到Render
方法时,ASPX 文件中定义的页面实际内容将在body
标签开始后立即渲染。protected virtual void RenderAdditionalHeaderTags(StringBuilder header)
在派生类中覆盖此方法,以在
head
部分添加其他标签(例如,其他meta
标签,用于标题、关键字等)。基类版本什么也不做。此方法生成的附加标签插入在title
标签之后,紧接着在head
标签之前。protected virtual void RenderFooter(HtmlTextWriter writer)
此方法可以被派生类覆盖,以便在
body
标签闭合之前,在正文末尾添加其他通用标签。如果被覆盖,请在输出附加标签后调用基类方法,除非您完全替换了此方法通常生成的所有闭合标签的渲染过程。
在运行时向表单添加其他控件
向在运行时动态创建的表单添加其他控件非常简单。PageForm
属性用于获取对页面上定义的表单的引用。然后,您可以使用 Form
对象的 Controls
属性将其中的任何其他控件插入。这通常在重写的 OnInit
方法中完成。MenuPage
类使用此方法插入支持性 HTML 和用户控件文件,以创建带有菜单的页面。
MenuPage 和 VerticalMenuPage 类
MenuPage
类派生自 RenderedPage
,并提供了一个在页面顶部横向或在页面左侧纵向显示的简单菜单的布局。菜单以用户控件的形式存储,该用户控件在运行时由类加载并放置在正确的位置。可以使用 MenuControlFile
属性更改用户控件。如果未指定,则默认查找名为 MenuCtrl.ascx 的控件。请注意,此类并非旨在与一些更复杂、功能齐全的菜单自定义控件竞争。它只是我用来在 ASP.NET 1.0 中快速启动和运行我的应用程序的一个简单类。
大部分工作在重写的 OnInit
方法中完成。根据 verticalMenu
字段的设置,它使用 BasePage.PageForm
属性在 form
控件内的实际页面内容周围插入 table
控件的 HTML。
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
// Insert the table and menu control at the start of
// the page. The layout depends on whether or not
// the menu is going to be rendered horizontally at
// the top or vertically down the left side of the
// page.
if(verticalMenu)
{
this.PageForm.Controls.AddAt(0, new LiteralControl(
"<table height='100%' cellpadding='0' " +
"width='100%'>\n<tr valign='top'>\n" +
" <td width='15%'>\n"));
}
else
{
this.PageForm.Controls.AddAt(0, new LiteralControl(
"<table cellpadding='0' width='100%'>\n" +
"<tr>\n<td>\n"));
}
if(menuCtrl != null)
this.PageForm.Controls.AddAt(1,
LoadControl(menuCtrl));
else
this.PageForm.Controls.AddAt(1, new LiteralControl(
"MenuControlFile property not set in derived " +
"OnInit!"));
if(verticalMenu)
{
this.PageForm.Controls.AddAt(2, new
LiteralControl("</td><td> <" +
"/td>\n<td>\n"));
// Page content goes in between and this wraps it up
this.PageForm.Controls.Add(
new LiteralControl("</td>\n</tr>\n" +
"</table>\n"));
}
else // For horizontal, page is rendered below menu
this.PageForm.Controls.AddAt(2,
new LiteralControl("</td>\n</tr>\n" +
"</table>\n"));
}
VerticalMenuPage
类更简单,只包含一个构造函数,将 verticalMenu
字段设置为 true
,以便菜单默认垂直渲染。如前所述,它们不是最复杂的菜单类,但通过从任一类派生 ASPX 页面并创建菜单用户控件,您可以在非常短的时间内启动和运行一个简单的带有菜单的应用程序。
结论
我在所有 ASP.NET 应用程序中都使用 BasePage
类及其派生类,以赋予它们一致的外观、感觉和功能集。希望您会发现这个类以及库中的其他类(或其中的部分)像我一样有用。
修订历史
- 04/02/2006
此版本中的非破坏性更改
- 使用了最新版本的 JavaScript 压缩器,以进一步减小嵌入式脚本的大小。
- .NET 2.0 演示已重新设计,使用母版页而不是
RenderedPage
和MenuPage
。 - 对于 .NET 2.0 版本,嵌入脚本资源的方法已更改为使用 .NET 2.0 方法,因此不再需要为 EWSoftware.Web.aspx 资源页面添加
httpHandlers
部分。因此,可以从您的 Web.config 文件中删除此部分。
破坏性更改
- 通过将渲染代码拆分到其自己的派生类(
RenderedPage
)中,对BasePage
类进行了重大修改。这使得在保留BasePage
类的非渲染相关功能(数据更改检查等)的同时,更容易迁移到 .NET 2.0 和使用母版页。 - 已删除
EMailPage
类。电子邮件功能已合并到BasePage
类中。这是为了将渲染代码移至其自己的派生类所必需的。 - 已修改了多个属性和方法名称,以符合 .NET 命名约定(关于大小写)(
BasePage.BypassPromptIds
、BasePage.DisabledCssClass
、BasePage.DisabledCssName
、BasePage.MsgLinkCssClass
、BasePage.MsgLinkCssName
、BasePage.SkipDataCheckIds
、EMailPageEventArgs.SmtpServer
、MenuPage.MenuCtrlFileName
、PageUtils.HtmlEncode
、RenderedPage.PageBodyCssClass
、RenderedPage.CssFileName
)。 BasePage.SetFocus
方法已重命名为BasePage.SetFocusExtended
。在 .NET 2.0 中,所有 Web 控件现在都有一个Focus
方法。此外,标准Page
类有两个SetFocus
方法,与旧的BasePage
版本有些相似。SetFocusExtended
方法可用于 .NET 1.1 来设置控件焦点,也可用于 .NET 2.0 版本,以防您需要它们提供的额外功能。
- 11/26/2004
此版本中的更改
- 从
MenuPage
类生成的 HTML 中删除了硬编码的类名和单元格内边距。菜单用户控件应控制样式和内边距。 - 修复了 Michael Freidgeim 报告的
BP_funSetFocus()
中的一个 bug,使其能正确地按 ID 查找控件,即使 ID 恰好匹配META
标签上的NAME
属性。 - 向
BasePage
添加了RobotOptions
枚举类型和Robots
属性,以允许在页面页眉中包含Robots
meta 标签。 - 向
BasePage
添加了PageDescription
和PageKeywords
属性,以允许在页面页眉中包含Description
和Keywords
meta 标签。
- 从
- 12/01/2003
- 初始发布。