构建更好的 ASP.NET 1.1 BasePage 框架






4.52/5 (25投票s)
这是一段关于如何构建更好的基页的旅程。其结果将是一个可重用的框架,您可以使用它创建任意数量的基页(在许多不同的网站上),并且仍然能够让您的团队中的设计师满意。
第一部分 - 简介
这是一段关于如何构建更好的基页的旅程。其结果将是一个可重用的框架,您可以使用它创建任意数量的基页(在许多不同的网站上),并且仍然能够让您的团队中的设计师满意。有很多东西需要解释,所以我将它分成几个部分,首先介绍我过去看到的所有方法的优缺点。
每当我开始一个新的 ASP.NET 项目时,都会被问到如何创建一个可以在网站所有页面上重复使用的外观。以下是我迄今为止看到的几种方法
让设计师创建一个 HTML 模板并在所有页面中重复使用它
- 优点
- 无需为每个页面重新创建外观。
- 你可以在设计器中看到外观(大问题):P
- 缺点
- 你必须将 HTML 剪切并粘贴到所有页面中。
- 你必须维护网站上的所有页面才能对网站进行最简单的更改(如果你问我,这没什么优势)。
评论:这是我看到刚开始 ASP.NET 开发的人所采用的方法。维护这样的网站至少可以说是一场噩梦。只要祈祷你的客户不会要求改变主要外观,否则你将不得不加班才能完成……
创建用户控件并在网站的所有页面中重复使用它们
- 优点
- 只需将 UI 拖放到新页面中即可完成!
- 缺点
- 难以维护网站,因为如果需要在基布局中删除/添加用户控件,则必须更改网站中的每个页面。
- 你无法在设计器中看到完整的外观(再次——大问题)。
评论:这稍微好一些,因为它利用用户控件将外观集中到独立的组件中,但当需要添加/删除主要外观模板时,仍然可能出现问题且耗时。
创建一个基页并将外观硬编码到基页的 OnInit() 调用中
- 优点
- 现在我们有进展了!我们不再需要从每个单独的页面维护整个网站的主要外观——我们也不需要担心添加到主要 UI 的元素的变化,因为我们可以从一个地方更改所有这些。
- 缺点
- 从设计师的角度来看,非常难以维护(此方法没有可用的设计师支持),试着告诉您的网页设计团队,看看会发生什么。
- 如果需要对网站的外观进行最微小的 HTML 更改,则需要重新编译网站。
- 您不应该在
OnInit()
中执行此操作,因为它不能保证您的控件在 ASP.NET 管道执行的所有阶段都就位,生成基页的代码应该在CreateChildControls()
调用中。您可以通过调用EnsureChildControls()
强制调用CreateChildControls()
。
评论:我们曾在两个不同的客户网站上使用此方法,效果良好。然而,当我撰写本文时向客户介绍此方法时,他们犹豫了,并想知道如何使用可视化设计器以及如何在不编译和重新部署网站 DLL 的情况下进行更改。下一个方法是该问题的答案。
为您的主要外观的所有部分创建用户控件,并在生成基页时动态加载它们
- 优点
- 设计师可以更改每个用户控件的 HTML,而无需重新编译网站 DLL(除非他们添加服务器控件)。
- 您可以在不更改任何基页代码(在大多数情况下)的情况下更改网站的外观。
- 缺点
- 您仍然需要处理将控件布局硬编码到基页中的问题(不酷)。
评论:尽管此方法在一个客户网站上为我们提供了很好的服务,但我们也不必担心他们会改变主要布局(暂时)。如果发生这种情况,我将建议迁移到下一个方法,这也是我们将在本系列相关文章中实现的方法。
在一个单独的库中构建一个 Basepage 基框架,并在您的网站中引用它——从这个 Base 页派生并创建 1-n 个 Basepage,您可以在您的网站中使用——使每个 Base page 在 Web.Config 文件中可配置
- 优点
- 设计师不仅可以从单独的用户控件(我称之为基控件或 Base 控件)更改您网站的布局和外观,还可以从 UI 布局中更改,这只是带有占位符的另一个用户控件。
- 所有关于如何生成 HTML 头部、链接哪些样式/脚本以及如何设置 body 标签的信息都可以从 web.config 中更改。
- 缺点
- 仍然——设计器中无法查看基页元素的完整支持(您只需等待 ASP.NET 2.0 中的母版页)。
我在这里提到 ASP.NET 2.0 是因为它的即将到来将使本文中的大多数 ASP.NET 1.1 技术过时。
评论 – 我们过去在几个客户那里使用过这种方法,效果一直很好。我不喜欢它的其中一点是缺乏集中配置,并且无法固化我们网站中的状态对象。如果有一个框架可以完成所有这些事情就好了,这正是我们将在本文中要做的事情。
那么为什么要写这篇文章呢?很简单,尽管我看到很多公司将他们的系统升级到 Windows 2003 Server、Windows XP 并开始使用 .NET 框架,但我确实知道有一些地方仍然在使用 NT 4 Server(尽管客户端机器运行的是 Windows 2000 Professional,对他们来说运行良好)。所以,是的,当 VS.NET 2005 发布时,这种方法将失效。但对于我们这些仍然在使用旧技术的人来说,这将大大节省时间。
本节最后,我将描述开始实现您自己的 BasePage 框架的步骤。
- 创建一个类库项目,将其命名为“BasePageFramework”。
- 添加
System.Web
引用。 - 创建一个
SuperBasePage
类并继承自System.Web.UI.Page
。 - 创建一个类并命名为
BasePageConfigurationHandler
。实现System.Configuration.ICustomConfigurationHandler
接口。
添加一个 XML 文件,以便我们有一个地方放置我们的配置内容(这不需要成为项目的一部分,它只是一个方便的地方来保存我们的基页的配置架构)。
将以下 XML 添加到架构中……
<configSections>
<section name="base.configuration"
type="BasePageFramework.Configuration.BasePageSectionHandler,
BasePageFramework"/>
</configSections>
<base.configuration>
<page name="AcmeBase" defaultTitle="Acme Widgets">
<body>
<attribute name="topmargin" value="0"/>\
<attribute name="leftmargin" value="0"/>
<attribute name="rightmargin" value="0"/>
<attribute name="bottommargin" value="0"/>
<attribute name="bgcolor" value="#FFFF00"/>
</body>
<metatags>
<meta name="AcmeMeta" Content="dogs,cats,
fish,gerbils,pets,friends,mantees"/>
</metatags>
<links>
<css file="acmestyles.css" path="~/Content/styles"/>
<script file="acmescriptlib.js" path="~/Content/scripts"/>
</links>
</page>
<page name="AcmePrintFriendly" defaultTitle="Acme Widgets - Print">
<body>
<attribute name="topmargin" value="0"/>
<attribute name="leftmargin" value="0"/>
<attribute name="rightmargin" value="0"/>
<attribute name="bottommargin" value="0"/>
<attribute name="bgcolor" value="#FFFFFF"/>
</body>
<metatags>
<meta name="AcmeMeta" Content="dogs,cats,fish,
gerbils,pets,friends,mantees"/>
</metatags>
<links>
<css file="acmestyles.css" path="~/Content/styles"/>
<script file="acmescriptlib.js" path="~/Content/scripts"/>
</links>
</page>
</base.configuration>
第二部分 - 实现配置处理程序
我将从第一部分我们离开的地方继续,所以我会跳过设置 Base Page Framework 项目的步骤。请参考第一部分查看这些步骤……
现在我喜欢将网站页面中所有一致的东西(例如脚本链接、CSS 文件、body 属性、默认名称等)集中到一个地方。我见过其他基页硬编码这些东西,这总是让我不寒而栗。(每个人都应该这样,因为应该避免硬编码此类事物。)
恕我直言,将这些内容放在一个可以在不重新编译项目的情况下更改的位置是很有意义的。我最喜欢的地方是 Web.Config 文件。但由于这是一个独立的框架程序集,最好不要使用 <appsettings>
部分中可用的默认 <add key="" value=""/>
设置,因为我个人认为这对于设置的用途不太直观。此外,最好不要用将用于一个或多个站点的配置信息来混淆我们应用程序的 appsettings
。为了解决这个问题,我们为 Base Page Framework 创建了一个自定义配置节。这就是本节的主题。
对于我们的基页框架,我们需要做的第一件事是创建一个自定义配置处理程序来解析 web.config 中的自定义配置设置。(请参阅第一部分了解架构)。我们通过实现 System.Configuration
命名空间中的 IConfigurationSectionHandler
接口来做到这一点。这非常容易,如果您知道如何解析 XML,您就已经完成了 4/5 的工作。
IConfigurationSectionHandler
接口有一个名为 Create()
的方法。以下是 Create
的原型以及每个参数的预期用途
public object Create(object parent, object configContext, XmlNode section);
Returns
object
:这个对象可以是您想要的任何东西。我喜欢创建一个单独的配置类来保存所有设置并返回它,以便以后可以缓存它。这个例子将做同样的事情。
参数
object parent
父配置节。
object configContext
一个
HttpConfigurationContext
对象。这只有当我们从 ASP.NET 调用它时才会传递给我们。正如您可能猜到的,如果您通过 app.config 文件在 Windows 应用程序中执行此操作,则此参数将为null
。XmlNode section
这是我们的自定义配置部分。我们将解析它以从配置文件中获取配置设置。
我们开始吧……
我喜欢在任何项目中将我的项目进行逻辑分离,所以为我们的配置内容创建一个文件夹。称之为“configuration”。向此文件夹添加一个类并将其命名为 BasePageConfigurationSectionHandler
。这个类将是我们的配置处理程序,因此实现 IConfigurationSectionHandler
接口。这是代码
public class BasePageSectionHandler :
System.Configuration.IConfigurationSectionHandler
{
#region IConfigurationSectionHandler Members
public object Create(object parent, object configContext, XmlNode section)
{
System.Xml.XmlNodeList pages = section.SelectNodes("page");
return new BasePagesConfiguration(pages);
}
#endregion
}
在此实现中,我们将返回我们即将创建的下一个类,即 BasePagesConfiguration
类。如果您查看我们的配置架构,您会看到可以有 1:n 个页面元素。这是因为您的网站中可以有多个基页。因此,如果您猜到 BasePagesConfiguration
是一个 collectionbase
对象,那么您就猜对了。接下来我们需要实现这个对象。这是代码
public class BasePagesConfiguration : System.Collections.ReadOnlyCollectionBase
{
public BasePagesConfiguration(System.Xml.XmlNodeList pages)
{
foreach(System.Xml.XmlNode page in pages)
this.InnerList.Add(new BasePageConfiguration(page));
}
public BasePageConfiguration this[string pageName]
{
get
{
foreach(BasePageConfiguration pge in this.InnerList)
{
if(pge.Name == pageName)
return pge;
}
throw new System.InvalidOperationException("The base page ID "
+ pageName + " could not be found in" +
" the current configuration");
}
}
}
此对象的索引器通过键值返回单个 BasePageConfiguration
对象。我不太关心集合返回单个配置对象的速度,所以我使用一个简单的循环。我们不关心性能的原因是,我们的基页将内部缓存其配置对象,因此每次请求时都不必一直访问配置文件。
最后我们得到了配置对象。我们将恰当地称之为 BasePageConfiguration
。在您的项目中创建 BasePageConfiguration
类。这是代码。我将假设您知道如何解析 XML 文档,所以我不会详细介绍它的工作原理。我只解释此对象的每个字段的用途。
public class BasePageConfiguration
{
private string _pageName = string.Empty;
private NameValueCollection _bodyAttributes = new NameValueCollection();
private string _defaultTitle = string.Empty;
private NameValueCollection _scriptFiles = new NameValueCollection();
private NameValueCollection _cssFiles = new NameValueCollection();
private NameValueCollection _metaTags = new NameValueCollection();
public BasePageConfiguration(XmlNode xml)
{
//parse the xml passed to us and set the configuration state
//first get the name of this page
System.Xml.XmlAttribute pageName =
(XmlAttribute)xml.Attributes.GetNamedItem("name");
System.Xml.XmlAttribute defaultTitle =
(XmlAttribute)xml.Attributes.GetNamedItem("defaultTitle");
if(defaultTitle != null)
_defaultTitle = defaultTitle.Value;
else
_defaultTitle = string.Empty;
if(pageName != null)
_pageName = pageName.Value;
else
throw new System.Configuration.ConfigurationException("The name" +
" attribute is required on all basepage configuration entries.");
//get the body tag attributes
System.Xml.XmlNodeList bodyAttrib = xml.SelectNodes("body/attribute");
foreach(XmlNode attr in bodyAttrib)
{
XmlAttribute name =
(XmlAttribute)attr.Attributes.GetNamedItem("name");
XmlAttribute val =
(XmlAttribute)attr.Attributes.GetNamedItem("value");
if(name != null && val != null)
{
_bodyAttributes.Add(name.Value,val.Value);
}
}
//get the site settings
System.Xml.XmlNode title = xml.SelectSingleNode("site/title");
if(title != null)
{
XmlAttribute val =
(XmlAttribute)title.Attributes.GetNamedItem("value");
if(val != null)
_defaultTitle = val.Value;
}
//get the main page links
System.Xml.XmlNodeList cssFiles = xml.SelectNodes("links/css");
foreach(XmlNode css in cssFiles)
{
XmlAttribute file =
(XmlAttribute)css.Attributes.GetNamedItem("file");
XmlAttribute path =
(XmlAttribute)css.Attributes.GetNamedItem("path");
if(file != null && path != null)
_cssFiles.Add(file.Value,path.Value);
}
System.Xml.XmlNodeList scriptFiles = xml.SelectNodes("links/script");
foreach(XmlNode script in scriptFiles)
{
XmlAttribute file =
(XmlAttribute)script.Attributes.GetNamedItem("file");
XmlAttribute path =
(XmlAttribute)script.Attributes.GetNamedItem("path");
if(file != null && path != null)
_scriptFiles.Add(file.Value,path.Value);
}
// get the place holder settings so we can see what user
// controls to load into our base page placeholders
System.Xml.XmlNodeList metaTags = xml.SelectNodes("metatags/meta");
foreach(XmlNode tag in metaTags)
{
XmlAttribute name =
(XmlAttribute)tag.Attributes.GetNamedItem("name");
XmlAttribute content =
(XmlAttribute)tag.Attributes.GetNamedItem("Content");
this._metaTags.Add(name.Value,content.Value);
}
}
//do not allow outside access to the individual
//values of the configuration
public NameValueCollection BodyAttributes{get{return _bodyAttributes;}}
public HorizontalAlign SiteAlignment{get{return _alignment;}}
public string DefaultTitle{get{return _defaultTitle;}}
public NameValueCollection Scripts{get{return _scriptFiles;}}
public NameValueCollection StyleSheets{get{return _cssFiles;}}
public string Name{get{return _pageName;}}
public NameValueCollection MetaTags{get{return _metaTags;}}
}
_pageName
这是我们基页的 ID。系统中的每个基页都必须有一个唯一的名称,以便在配置文件中找到其配置(稍后当我们实现实际的基页 Base 时会详细介绍)。
private NameValueCollection _bodyAttributes
这些是将放入我们每个页面的单独 body 属性标签。它们在基页 Base 的
OnAddParsedSubObject()
调用中被注入到 HTML 中(非常感谢 Michael Earls 在这方面给了我启发)。private string _defaultTitle
这是将用于我们网站所有页面的默认标题。如果 HTML 设计师没有在
<Title>
标签中输入任何内容,我们将用这个值替换空白值。private NameValueCollection _scriptFiles
这是一个将放置在
<head>
标签内的脚本链接列表。private NameValueCollection _cssFiles
这是一个将放置在
<head>
标签内的样式表链接列表。private NameValueCollection _metaTags
这是一个将出现在我们网站所有页面中的默认元标签列表。
最后,如您所见,配置是一个只读对象。这很合乎逻辑,因为它只会在配置处理程序创建对象时从配置文件中获取其值。
使用此处理程序很容易。要创建配置处理程序的实例,您需要在 ConfigurationSettings
对象中调用 GetConfig()
。
Configuration.BasePagesConfiguration baseConfiguration =
(Configuration.BasePagesConfiguration)
System.Configuration.ConfigurationSettings.GetConfig(
"base.configuration");
返回:根据 configSections
标签中您的部分返回相应的配置对象。请参见从第一部分创建的架构中截取的以下代码片段。
<configSections>
<section name="base.configuration"
type="BasePageFramework.Configuration.BasePageSectionHandler,
BasePageFramework"/>
</configSections>
此处返回的对象是您决定从 IConfigurationSectionHandler.Create
实现返回的任何对象。在这种情况下,它是我们的 ReadOnlyCollectionBase
"BasePagesConfiguration
" 对象,其中包含我们网站的各个 BasePageConfiguration
对象。
此配置节的名称属性是您 .config 文件中的配置节。类型是实现 IConfiguraitonSectionHandler
接口的完全限定对象名,以及此对象所在的程序集名称,用逗号分隔。如果您引用强命名程序集,则需要将程序集名称的所有四个部分放入 type 属性中,如下所示(取自 System.dll)
<section name="MyCustomSection"
type="System.Configuration.NameValueSectionHandler,system,
Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089,custom=null" />
如您所见,您将强名称的其余四个部分放在 DLL 名称后的 type 标签中(用逗号分隔)。
在第 3 部分中,我们将开始实现 SuperBasePage
本身。在第 4 部分的结尾,我将向您展示如何添加一些额外功能,这些功能封装了标准站点状态管理。这些对象是实现 ISmartQueryString
、ISmartSession
和 ISmartApplication
接口的类。
第三部分 - 实现 SuperBasePage
我将从我们创建 BasePageConfiguration
类的地方继续。在这一部分中,我们将实现 SuperBasePage
,它将用于您网站的所有派生 Base 页。在您的网站中拥有多个 Base 页很有用,因为您可能希望为打印友好页面、弹出窗口、对话框等提供布局……
这一部分有点长,我会尽量保持简短,考虑到这里发生的事情。我还会在下一部分发布一些 UML 类图和序列图,这样就会有一些不错的文档可以与之搭配。
SuperBasePage
实际上是两个独立的类和一个实现自定义接口的用户控件,它们共同构成一个模板。它们声明如下
public interface IBaseTemplate
{
PlaceHolder ContentArea
{get;}
}
public class SuperBasePage : WebUI.Page
{
private BaseServerForm _baseForm = null;
//new SuperBasePage("BASE_FORM");
//the form tag where we place all our webserver controls
private Configuration.BasePageConfiguration _config = null;
…
这三项如果正确使用,不仅能让我们快速创建可以从 web.config 配置的基页,还能让我们将 HTML 保留在网站的各个页面中。我见过其他框架/基页概念,在使用前需要删除 HTML
、HEAD
、TITLE
、BODY
和 FORM
标签。我们在这里不会这样做。保持这部分完整很重要,原因有二
- 设计师能更好地理解它,这样您就不必担心他们是否会错误地替换这些标签,更重要的是,
- 将这些标签保留在每个页面中,使我们能够覆盖 web.config 中放置的内容(例如跟踪指令等... 我们的框架遵循相同的模式)。
首先,我们需要查看 System.Web
命名空间中可用的现有对象。我们现在感兴趣的是 System.Web.UI.Page
和 System.Web.UI.HtmlControls.HtmlForm
。我们将首先关注后两者。
当设置了服务器标签时,HtmlForm
成为无处不在的 ASP.NET Web 窗体的核心。没有它,您无法在页面上实例化服务器控件。这里存在我们的问题,我们如何让必须在每个页面上定义的表单表现相同,通过显示相同的外部内容和每个页面的独特内容,更重要的是,我们如何让页面中的内容实际嵌套到此表单的内容区域中?我过去开发的基页中看到的方法是“移动”每个页面的内容到基页类中指定的“内容区域”。我们在这里仍然这样做,但以一种更谨慎的方式。而且,正如我之前提到的,大多数示例都强制您在使用页面之前从页面中删除 HTML 头部和尾部以及 Form
标签(尝试向网页设计师解释这一点,看看他们会怎么做)。
一定有更好的方法,所以我们不将派生页面的内容移动到基页面的内容区域,而是对 HtmlForm
进行一些巧妙的转换,将其更改为具有以下功能的表单……
- 拥有一个定义所有页面布局的模板引用。
- 此表单需要知道如何以这样一种方式构造它:当您向它传递一个现有表单时,它可以取代旧表单,但有一个重要的区别——它将具有将我们各个页面的内容放置到其所属位置的功能。
因此,本质上,我们的基页不是劫持每个页面的内容,而是接收一个 HtmlForm
并使其以这样一种方式工作,使我们能够根据表单创建时将初始化的给定模板,将内容设置在我们想要的任何位置。您会注意到,我们的 BaseServerForm
不过是一个带有模板引用的普通 HtmlForm
。以下是我们的基服务器表单的代码……
public class BaseServerForm : HtmlUI.HtmlForm
{
private WebCtlUI.PlaceHolder _uiTemplatePlcHldr =
new WebCtlUI.PlaceHolder();
internal BaseServerForm(HtmlUI.HtmlForm oldFrm,IBaseTemplate uiTemplate)
{
foreach(string key in oldFrm.Attributes.Keys)
this.Attributes.Add(key,oldFrm.Attributes[key]);
System.Type frmTp = oldFrm.GetType();
// move the controls first - so they are located
// in the main form before we
// rip apart the htmlform via reflection
while(oldFrm.Controls.Count > 0)
uiTemplate.ContentArea.Controls.Add(
(System.Web.UI.Control)oldFrm.Controls[0]);
//copy the old form's values into our new form...
foreach(FieldInfo fields in
frmTp.GetFields(BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.Instance))
this.GetType().GetField(fields.Name,
BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.Instance).SetValue(this,
fields.GetValue(oldFrm));
this._uiTemplatePlcHldr.Controls.Add(
(System.Web.UI.Control)uiTemplate);
this.Controls.Add((System.Web.UI.Control)
this._uiTemplatePlcHldr);
}
public IBaseTemplate BaseTemplate
{
get{return (IBaseTemplate)_uiTemplatePlcHldr.Controls[0];}
}
}
这个类的构造函数接收两个参数:一个 HtmlForm
对象和一个对名为 IBaseTemplate
的引用。以下是您在代码中实现 IBaseTemplate
的方式……
public class AcmeBaseTemplate : System.Web.UI.UserControl,
BasePageFramework.IBaseTemplate
{
protected System.Web.UI.WebControls.PlaceHolder _content;
protected System.Web.UI.WebControls.PlaceHolder _leftNav;
public PlaceHolder LeftNavigation
{get{return _leftNav;}}
#region IBaseTemplate Members
public PlaceHolder ContentArea
{
get
{
return _content;
}
}
#endregion
这个模板接口是我绕过另一个框架的限制的方法,该框架几乎做了同样的事情。如果我们将来可能想改变布局,为什么要定义一个用于我们完整基础布局的接口呢?我认为有意义的是定义一个类,它公开我们基础页面的内容区域,这样我们就可以随意更改它们包含的内容。此外,如果能创建具有访问我们页面所有区域的不同布局,那也会很不错。这还将允许我们在特定条件下在运行时动态更改布局。最后,如果我们的设计师无需编写代码,而是通过使用设计器就能改变基础页面的外观,那也真的很好。这就是使用用户控件(实现我们的 IBaseTemplate
接口)派上用场的地方。
然而,有一个问题,我们无法确定模板开发人员会将放置内容区域称为什么。我们确实需要知道这一点,这样我们才能以一种良好、无缝和谨慎的方式将页面控件重新定位到该区域。这就是 IBaseTemplate
接口发挥作用的地方。正如您在上面的示例中看到的,我们在基页中定义了一个占位符,用于放置派生页面的内容,并通过此接口唯一的属性“ContentArea
”(巧合地返回我们内容的占位符)返回它。现在,我们的基页将与用户控件签订一个合同,其中规定“我将负责定位控件的位置,这样您就不必这样做了”。
您将在基页定义中稍后看到,为什么我们需要通过属性将用户控件中的其他占位符暴露给外部世界,就像我在上面的示例中所做的那样。
BaseServerForm
类除了封装页面内容到模板内容区域的移动(并扮演服务器表单的角色)之外,并没有做太多其他事情,所以除了加载 UI 模板,它基本上只是一个定义了布局的服务器表单。我还应该指出,我们不希望除了 Base 页之外的任何东西创建 BaseServerForm
,因此我们通过将其声明为 internal
来向外部世界隐藏构造函数。这是因为 Base 页是唯一可能需要创建 BaseServerForm
的对象。但是,我们确实希望开发人员能够引用该表单,以防他们在请求处理之前决定以编程方式更改某些内容。我不得不诉诸于使用反射来拆解旧 HtmlForm
的私有值并将其复制到我们的 BaseServerForm
中。我们本可以使用与 HtmlForm
的“has-a”关系,但这看起来有点笨重,因此我将其设计为“is-a”关系,以使其与我们的页面的关系无缝。另请注意,在进行反射以获取任何 internal
值之前,将旧 HtmlForm
的控件树和属性移动到我们的新 BaseServerForm
是至关重要的。我们可以很好地访问控件树,无需使用反射来获取它。我通常不喜欢使用反射,除非绝对必要。我喜欢将反射比作胶带,它是一个有着崇高目的的工具……但当“创造性地”使用时,你可能会自食恶果(如果你能原谅这个旧 C 语言的陈词滥调)。
现在我们已经定义了 BaseServerForm
类,我们需要定义 SuperBasePage
,所有网站的基页都将派生自它。这是代码……
public class SuperBasePage : WebUI.Page
{
#region Constants
//large string builder initial buffer siz
private const int LG_STR_BUFFER_SZ = 512;
//medium string builder inital buffer size
private const int MD_STR_BUFFER_SZ = 256;
//small (default) string builder buffer size
private const int SM_STR_BUFFER_SZ = 128;
private const string OPEN_TITLE_TAG = "";
private const string OPEN_HEAD_TAG = "";
private const string CLOSE_HEAD_TAG = "";
private const string OPEN_HTML_TAG = "";
private const int DEFAULT_LINK_WIDTH = 20;
//this is used to estimage the number of characters
//to reserve space for in our string builders
//in an attempt to maximize efficency
#endregion
#region SuperBasePage Members
//our form sits in this placeholder
private WebCtl.PlaceHolder _serverFormPlcHldr =
new WebCtl.PlaceHolder();
//the title of the page (displayed in the caption bar)
private string _title = string.Empty;
private BaseServerForm _baseForm = null;
//new SuperBasePage("BASE_FORM");
//the form tag where we place all our webserver controls
private Configuration.BasePageConfiguration _config = null;
private string _basePageName = string.Empty;
private ISmartQueryString _smartQueryString = null;
private ISmartSession _smartSession = null;
private ISmartApplication _smartApp = null;
#endregion
#region Properties
public string Title
{
get{return this._title;}
set{this._title = value;}
}
public BaseServerForm BaseForm
{get{return this._baseForm;}}
public IBaseTemplate MainUITemplate
{get{return this._baseForm.BaseTemplate;}}
public ISmartSession SmartSession
{
get{
if(this._smartSession == null)
throw new InvalidOperationException("You must" +
" initialize the smart session state" +
" before you can reference it");
else
return this._smartSession;
}
}
public ISmartApplication SmartApplication
{
get{
if(this._smartApp == null)
throw new InvalidOperationException("You must" +
" initialize the smart application state" +
" before you can reference it");
else
return this._smartApp;
}
}
public ISmartQueryString SmartQueryString
{
get
{
if(this._smartQueryString == null)
throw new InvalidOperationException("You must" +
" initialize the smart query string" +
" before you can reference it");
else
return this._smartQueryString;
}
}
#endregion
protected override void CreateChildControls()
{
InitSmartQueryString(ref this._smartQueryString);
if(this._smartQueryString != null)
this._smartQueryString.SetQueryStringInfo(this.Page);
InitSmartSession(ref this._smartSession);
if(this._smartSession != null)
this._smartSession.SetSessionState(this.Page.Session);
//allows us to set the base page state from
//the child page before any rendering of the base page occurs
BasePreRender(this._baseForm);
//call the virutal method to allow the base page
//to set the appropriate usercontrols into the base template
LoadTemplatePanels();
}
protected override void OnInit(EventArgs e)
{
this.EnsureChildControls();
base.OnInit (e);
}
protected override void AddParsedSubObject(Object obj)
{
//put all the configuration extras into our html header here
//make sure you modify the body tag as well since it is also
//editable from our custom configuration section
if(obj is HtmlCtl.HtmlForm)
{
BasePageFramework.IBaseTemplate tmplt = null;
this.LoadBaseUITemplate(ref tmplt);
if(tmplt == null)
throw new InvalidOperationException("Unable to" +
" load the base UI Template");
this._baseForm = new BaseServerForm((HtmlCtl.HtmlForm)obj,
tmplt);
//replace the object reference with
//our own "base server form"
obj = this._baseForm;
}
if(obj is Web.UI.LiteralControl)
{
Web.UI.LiteralControl htmlHeader = (Web.UI.LiteralControl)obj;
//we only need to do this processing when we are in the header
if(htmlHeader.Text.IndexOf(OPEN_HTML_TAG)>=0)
{
//we are going to need the stuff from
//the configuration now so load it up...
ConfigLoadingArgs cfgParam = new ConfigLoadingArgs();
InitializeConfiguration(ref cfgParam);
this._config = (Configuration.BasePageConfiguration)
Cache["BASE_CONFIGURATION_" + cfgParam.BasePageID];
if(this._config == null)
InitFromConfiguration(cfgParam.BasePageID);
if(this._config != null)
{
System.Text.StringBuilder htmlBuilder = new
System.Text.StringBuilder(htmlHeader.Text.Length*2);
htmlBuilder.Append(htmlHeader.Text);
//check to see if the current literal control
//being passed to us is actually
//the HTML header - if it is then we need
//to add any values from the custom web.config settings
//if the current page has any of the same
//values in the body tag or any inline scripts -
//then we need to allow the page to override
//the web.config settings -
//this is in following with the
//same pattern for everything in ASP.NET
//(machine.config-web.config-page...)
//see if the title is filled in if not
//insert the default title
//if the title tag is missing then add one now
int openingTitleTagOffset =
htmlHeader.Text.ToUpper().IndexOf(
OPEN_TITLE_TAG,0,htmlHeader.Text.Length);
int endTitleTagOffset =
htmlHeader.Text.ToUpper().IndexOf(
CLOSE_TITLE_TAG,0,htmlHeader.Text.Length);
endTitleTagOffset -= CLOSE_TITLE_TAG.Length-1;
int titleLen =
htmlHeader.Text.Substring(openingTitleTagOffset,
endTitleTagOffset -openingTitleTagOffset).Length;
//get the length of our title
if(openingTitleTagOffset >= 0)
{
//check the length of our title
//if the title is missing then add the default one
if(titleLen == 0)
htmlBuilder.Insert(openingTitleTagOffset+
OPEN_TITLE_TAG.Length,
this._config.DefaultTitle);
}
else
{
int openHeadTagOffset =
htmlHeader.Text.ToUpper().IndexOf(OPEN_HEAD_TAG,
0,htmlHeader.Text.Length);
//if the head tag is missing then add it
//otherwise just insert the default title if needed
//create another string Builder to handle
//our missing header/title tag
System.Text.StringBuilder subHtml =
new System.Text.StringBuilder((OPEN_HEAD_TAG.Length
+ CLOSE_HEAD_TAG.Length
+ OPEN_TITLE_TAG.Length
+ CLOSE_TITLE_TAG.Length)
+_config.DefaultTitle.Length);
if(openHeadTagOffset == -1)
{
subHtml.Append(OPEN_HEAD_TAG);
subHtml.Append(OPEN_TITLE_TAG);
subHtml.Append(_config.DefaultTitle);
subHtml.Append(CLOSE_TITLE_TAG);
subHtml.Append(CLOSE_HEAD_TAG);
//insert the output right
//after the opening HTML tag
htmlBuilder.Insert(OPEN_HTML_TAG.Length,
subHtml.ToString());
}
else
{
subHtml.Append(OPEN_TITLE_TAG);
subHtml.Append(_config.DefaultTitle);
subHtml.Append(CLOSE_TITLE_TAG);
//insert the output right after
//the opening Head Tag
htmlBuilder.Insert(openHeadTagOffset,
subHtml.ToString());
}
}
//insert our configuration metatags and
//scripts/links after the closing title tag...
//int closingTitleTagOffset =
htmlBuilder.ToString().ToUpper().IndexOf(CLOSE_HEAD_TAG,
0,htmlBuilder.ToString().Length);
htmlBuilder.Replace(CLOSE_HEAD_TAG,
GetWebConfigLinks(htmlBuilder.ToString()));
RenderBodyTagLinks(htmlBuilder);
//replace the text in the literal
//control with our new header
htmlHeader.Text = htmlBuilder.ToString();
}
}
}
this.Controls.Add((System.Web.UI.Control)obj);
}
#region HTML Header Parsing Code
private void RenderBodyTagLinks(System.Text.StringBuilder htmlBuilder)
{
//rip out the body tag and replace it with our own
//that has the attributes from the web config file
//but allows the individual pages to override
//these settings at the page level
string bodyTag = htmlBuilder.ToString();
int bodyTagOffset = bodyTag.ToUpper().IndexOf("<BODY");
bodyTag = bodyTag.Substring(bodyTagOffset,
bodyTag.Length - bodyTagOffset);
//create the attributes that will be dumped into the body tag
//make sure it is not already in the body before entering it
//if it is in the body then throw it out so the individual page
//can override the web.config's settings
System.Text.StringBuilder bodyTagBuilder =
new System.Text.StringBuilder(SM_STR_BUFFER_SZ);
bodyTagBuilder.Append("
foreach(string attribute in _config.BodyAttributes.Keys)
if(bodyTag.IndexOf(attribute) == -1)
{
bodyTagBuilder.Append(" ");
bodyTagBuilder.Append(attribute);
bodyTagBuilder.Append("=\"");
bodyTagBuilder.Append(
_config.BodyAttributes.Get(attribute));
bodyTagBuilder.Append("\" ");
}
//append the global body tag attributes to the
//end of the current attribute list in our body tag
string newAttribList = bodyTag.Substring(5,bodyTag.Length-6);
//remove the opening body tag and closing bracket
//get the current attributes out of the old body tag
//and add them to our bodytagbuilder
bodyTagBuilder.Append(" ");
bodyTagBuilder.Append(newAttribList);
int bodytagoffset = htmlBuilder.ToString().IndexOf(bodyTag);
//remove the old body tag from our string builder
htmlBuilder.Remove(bodytagoffset,htmlBuilder.Length-bodytagoffset);
string output = htmlBuilder.ToString();
htmlBuilder.Append(bodyTagBuilder.ToString());
}
//takes the existing script links and returns a new string
//containing the existing links merged with the ones in the web.config
private string GetWebConfigLinks(string oldHtml)
{
System.Text.StringBuilder subHtml =
new System.Text.StringBuilder((_config.Scripts.Count*
DEFAULT_LINK_WIDTH)+CLOSE_HEAD_TAG.Length);
this.GetMetaTags(subHtml,oldHtml);
this.GetStyleSheetLinks(subHtml);
this.GetScriptLinks(subHtml);
subHtml.Append("\r\n\t");
subHtml.Append(CLOSE_HEAD_TAG);
return subHtml.ToString();
}
private void GetMetaTags(System.Text.StringBuilder htmlBuilder,
string oldHtml)
{
foreach(string metaTag in _config.MetaTags.Keys)
{
if(oldHtml.IndexOf(metaTag) == -1)
{
htmlBuilder.Append("\r\n\t\t");
htmlBuilder.Append(" " +
" htmlBuilder.Append("NAME=\"");
htmlBuilder.Append(metaTag);
htmlBuilder.Append("\"");
htmlBuilder.Append(" CONTENT=\"");
htmlBuilder.Append(_config.MetaTags.Get(metaTag));
htmlBuilder.Append("\"/>");
}
}
}
private void GetStyleSheetLinks(System.Text.StringBuilder scriptLinks)
{
foreach(string key in this._config.StyleSheets.Keys)
{
scriptLinks.Append("\r\n\t\t " +
" scriptLinks.Append("type=\"text/css\" ");
scriptLinks.Append("rel=\"stylesheet\" ");
scriptLinks.Append("href=\"");
scriptLinks.Append(ParseRootPath(
this._config.StyleSheets.Get(key)));
scriptLinks.Append("/");
scriptLinks.Append(key);
scriptLinks.Append("\"");
scriptLinks.Append("/>");
scriptLinks.Append(Environment.NewLine);
}
}
private void GetScriptLinks(System.Text.StringBuilder scriptLinks)
{
foreach(string key in this._config.Scripts.Keys)
{
scriptLinks.Append("\r\n\t\t ");
}
}
#endregion
#region Utility Methods
private string ParseRootPath(string path)
{
if(path.IndexOf('~') == 0)
{
if(System.Web.HttpContext.Current.Request.ApplicationPath == "/")
{
path = path.Replace("~","");
}
else
path = path.Replace("~",
System.Web.HttpContext.Current.Request.ApplicationPath);
return path;
}
else
return path;
}
private void InitFromConfiguration(string pageName)
{
this._config =
(Configuration.BasePageConfiguration)Cache["BASE_CONFIGURATION_"
+ pageName];
if(this._config == null)
{
Configuration.BasePagesConfiguration baseConfiguration =
(Configuration.BasePagesConfiguration)
System.Configuration.ConfigurationSettings.GetConfig(
"base.configuration");
if(baseConfiguration == null)
return;
//the base configuration was not set
//in the current web.config - exit now...
//load the current page's configuration and stick it
//into cache so it does not have to be reloaded
//on subsequent requests
this._config = baseConfiguration[pageName];
Cache.Add("BASE_CONFIGURATION_" + pageName,
this._config,null,DateTime.MaxValue,
TimeSpan.FromDays(100),
System.Web.Caching.CacheItemPriority.NotRemovable,null);
}
}
#endregion
#region Virutal Methods
protected virtual void InitializeConfiguration(ref
ConfigLoadingArgs configParams)
{}
///
/// override this method in our child pages to allow us
/// to set base page state before any rendering takes place
/// in the base page
///
protected virtual void BasePreRender(BaseServerForm baseForm)
{}
///
/// override this method to allow the
/// base page to load the base UI template
/// the base UI template is of type BaseUITemplate
/// BaseUITemplate is of type Usercontrol
///
protected virtual void LoadBaseUITemplate(ref
BasePageFramework.IBaseTemplate tmplt)
{}
///
/// override in the base page so you can load
/// user controls at the derived base page
/// where you want them to be loaded - this is done
/// so the derived base page can subscribe to events
/// fired by these controls
///
protected virtual void LoadTemplatePanels()
{}
///
/// override this method to initialize the smart
/// querystring with your derived smart querystring instance
///
/// derived instance of a smart query string base
/// class that Implements the ISmartQueryString interface
protected virtual void InitSmartQueryString(ref
ISmartQueryString isqs)
{}
///
/// override this method to initialized the smart session
/// state with your derived smart session instance
///
/// derirved smart session object that implements
/// the ISmartSession Interface
protected virtual void InitSmartSession(ref ISmartSession ises)
{}
///
/// override this method to initialize the smart
/// application state with your derived smart application instance
///
/// derived smart application object that
/// implements the ISmartApplication interface
protected virtual void InitSmartApplication(ref
ISmartApplication iapp)
{}
#endregion
}
以下是您如何实现一个派生自 SuperBasePage
的页面
public class AcmeBase : BasePageFramework.SuperBasePage
{
protected override void LoadBaseUITemplate(ref
BasePageFramework.IBaseTemplate tmplt)
{
tmplt = (IBaseTemplate)
LoadControl("~/BaseTemplateControl.ascx");
}
protected override void InitializeConfiguration(ref
BasePageFramework.Configuration.ConfigLoadingArgs configParams)
{
configParams.BasePageID = "AcmeBase";
}
protected override void InitSmartApplication(ref
BasePageFramework.SmartState.ISmartApplication iapp)
{
iapp = (ISmartApplication)new AcmeAppObject();
}
protected override void InitSmartQueryString(ref
BasePageFramework.SmartState.ISmartQueryString isqs)
{
isqs = (ISmartQueryString)new AcmeSmartQueryString();
}
protected override void InitSmartSession(ref
BasePageFramework.SmartState.ISmartSession ises)
{
ises = (ISmartSession)new AcmeSmartSession();
}
}
这是在虚拟 LoadBaseUITemplate
调用期间加载的派生基模板实现
public class AcmeBaseTemplateMain : System.Web.UI.UserControl,
BasePageFramework.IBaseTemplate
{
private PlaceHolder _header = new PlaceHolder();
private PlaceHolder _leftNavigation = new PlaceHolder();
private PlaceHolder _contentArea = new PlaceHolder();
private void Page_Load(object sender, System.EventArgs e)
{
// Put user code to initialize the page here
}
#region IBaseTemplate Members
public PlaceHolder ContentArea
{
get
{
return _contentArea;
//The IBaseTemplate Interface's ContentArea method
//is used by the SuperBasePage to handle
//content placement from the derived page
}
}
public PlaceHolder Header
{
get{return _header;}
//PLACEHOLDER THAT CONTAINS OUR HEADER'S CONTENT
}
public PlaceHolder LeftNav
{
get{return _leftNavigation;}
//PLACEHOLDER THAT CONTAINS OUR LEFT NAV BAR
}
#endregion
}
您可以看到,我正在结合使用重写 AddParsedSubObject()
和 AddChildControls()
虚方法。这是因为我们只需要使用 AddParsedSubObject()
方法做一件事。在 HTML 头部和 HTML 表单被添加到我们的页面之前拦截它们。这个想法来自 Micheal Earls(一位来自亚特兰大的 Magenicon 同事)。在此处查看他的博客 这里。我见过在服务器控件中重写此方法用于子控件标签,但从未在这样的页面类中。我认为这是该方法的一种非常巧妙的用法,它解决了不必从内容区域周围删除外部 HTML 的问题。现在外部 HTML 保持不变,这也为我们提供了在页面级别覆盖基页配置的机会,从而模仿了 ASP.NET 中当前存在的 web.config 设置/页面指令模式。
对于不熟悉此方法的人来说,它会在处理程序遇到子元素时触发。在 System.Web.Page
类的情况下,它总共会触发三次。
- 当它遇到标签时触发一次。
- 当它遇到标签时触发一次。
- 当它遇到标签时再触发一次。
正如您可能已经猜到的,我们真正关心的只有前两个,当它为 HTML 标签触发时,我们获取自定义配置设置并将其放置在头部。处理程序的这部分需要非常快速,所以我挣扎了一段时间,思考什么是最好的方法。我过去使用过 HtmlWriter
,但我认为对于这个来说开销太大了,因为我们已经在渲染调用中使用了它,而且我被迫解析所有 HTML 才能使该方法有效(所以显然,这里不是一个选项)。
经过一些实验,我决定使用 StringBuilder
,因为它们比普通字符串连接更快(事实上,StringBuilder
的性能与 C 语言中旧的 memcpy()
调用相当)。为了真正利用 StringBuilder
的性能,您需要提前估算所需的空间量。如果不是,当 StringBuilder
必须内部调整大小以腾出更多空间时,它会移动大量的字符内存。我结合使用默认 HTML 内容以及 web.config 中的内容来计算 StringBuilder
缓冲区的良好初始大小。这之所以如此重要,是因为当 StringBuilder
在 append()
期间达到其限制时,它会创建一个两倍于旧缓冲区大小的新缓冲区,并将所有字符数据复制到新缓冲区(然后销毁旧缓冲区)。所有这些字符数据的移动抵消了使用 StringBuilder
带来的性能提升(尽管它仍然可能比普通的字符串连接运算符更快)。无论如何,恕我直言,谨慎总比后悔好。
我们也在 CreateChildControls
重写中进行了一些初始化。我们这样做是为了确保这些方法在请求处理时只调用一次。这是通过从我们的 init()
调用 EnsureChildControls()
来完成的。此调用“确保”我们的子控件被创建(在这种情况下,我们正在初始化 SmartQueryString
和 SmartSessionState
)。由于它们对于使用基页并不关键,并且我试图使其简短,我将在最后一部分详细介绍这两个类。
SuperBasePage
中的虚方法将在我们的派生 SuperBasePage
类中被重写。使用这些虚方法,我们可以定义当请求时我们的基页如何以及加载什么。
以下是每个方法的作用……
protected virtual void InitializeConfiguration(ref ConfigLoadingArgs configParams);
重写此方法,以便我们可以告诉基页使用哪个页面配置。(还记得我们的配置节中有不止一个页面元素吗?这就是我们指定要使用哪个节的地方。)在此示例中,我们想要从配置中获取的基页名称属性是“
AcmeBase
”。我们正在通过引用传递configParams
参数,以便我们可以在重写中(在派生基页中)设置它。这是一个如何实现它的例子。
protected override void InitializeConfiguration(ref BasePageFramework.Configuration.ConfigLoadingArgs configParams) { configParams.BasePageID = "AcmeBase"; }
这反过来会告诉我们的 Super Base 页面从以下页面配置节加载设置……
<page name="AcmeBase" defaultTitle="Acme Widgets"> <body> <attribute name="topmargin" value="0"/> <attribute name="leftmargin" value="0"/> <attribute name="rightmargin" value="0"/> <attribute name="bottommargin" value="0"/> <attribute name="bgcolor" value="#FFFF00"/> </body> <metatags> <meta name="AcmeMeta" Content="dogs,cats,fish, gerbils,pets,friends,mantees"/> </metatags> <links> <css file="acmestyles.css" path="~/Content/styles"/> <script file="acmescriptlib.js" path="~/Content/scripts"> </links> </page>
参数通过引用传递,以便我们可以从派生基页类设置基页名称。请注意,如果配置元素不存在,我将抛出异常(如您在
SuperBasePage
代码中看到的)。您还可以看到我正在缓存设置。我这样做有两个原因- 如果我想为多个页面使用相同的配置——配置将只在内存中创建一次,并与所有使用它的页面共享。
- 我们不希望在每个请求上都解析配置,因为这会导致严重的性能损失。
这是通过其虚方法从
SuperBasePage
传递给我们的派生页面的ConfigLoadingArgs
类public class ConfigLoadingArgs { private string _basePageID = string.Empty; internal ConfigLoadingArgs() {} public string BasePageID { get{return _basePageID;} set{_basePageID = value;} } }
protected virtual void BasePreRender(BaseServerForm baseForm)
此方法在我们将任何模板加载到基 UI 之前在
CreateChildControls
调用中被调用。我自己从未需要使用过它,但我认为对于某人来说,在加载任何控件之前访问此页面的BaseServerForm
可能是必要的。这就是您将要执行此操作的地方。protected virtual void LoadBaseUITemplate(ref BasePageFramework.IBaseTemplate tmplt)
在这里,我们引入了我们的主要用户控件(实现
IBaseTemplate
接口),它将为我们网站中的所有页面提供布局。在内部,当BaseServerForm
加载并初始化时,它将使用IBaseTemplate
接口将内容移动到适当的区域。以下是我在我的派生 Base 页面类中使用它的方式——这里的美妙之处在于,我们不需要将每个控件加载到主模板的所有区域,您可以拥有一些静态的,一些动态加载的(在
LoadTemplatePanels
调用中,您将看到我们如何动态加载控件,我们通常在希望派生 Base 页面订阅给定控件的事件时这样做)。protected override void LoadBaseUITemplate(ref BasePageFramework.IBaseTemplate tmplt) { tmplt = (IBaseTemplate)LoadControl("~/BaseTemplateControl.ascx"); }
protected virtual void LoadTemplatePanels()
此重写允许我们在加载和初始化基模板 UI 和表单后,将控件动态加载到基模板的区域中。您可能需要这样做是为了在派生基页类中附加事件并订阅它们(例如导航回发、注销请求等)。还记得我之前告诉过您,我将告诉您为什么要在
BaseTemplate
用户控件中公开所有占位符吗?您可以从以下代码片段中看到原因。我可以在派生基页中访问这些占位符。但是有一个问题,您需要在此重写中,将已初始化的IBaseTemplate
向下转型为您的MainTemplate
用户控件类型,然后才能访问占位符属性。以下是我使用它的方式(我包含了一个事件示例,以便您可以看到我的基页在事件触发时想要做什么)……
protected override void LoadTemplatePanels() { this.BaseForm.BaseTemplate.Header.Controls.Add( LoadControl("~/BaseControls/AcmeHeader.ascx")); _leftNavCtl = (LeftNav)LoadControl("~/BaseControls/LeftNav.ascx"); this.BaseForm.BaseTemplate.LeftNav.Controls.Add(_leftNavCtl ); _leftNavCtl.OnNavEvent += new NavEventHandler(OnNavigationEvent); } private void OnNavigationEvent(object sender,NavEventArg e) { this.SmartQueryString.SmartRedirect(e.NavURL); }
这些只是示例。我计划在未来发布一个包含所有操作示例的完整项目。
最后两个涉及 SmartQueryString
和 SmartSession
接口。智能处理程序接口使我们能够强类型化 QueryString
、Session
和 Application
对象。我将在下一篇文章中更详细地介绍这些方法的用途……
protected virtual void InitSmartQueryString(ref ISmartQueryString isqs)
protected virtual void InitSmartSession(ref ISmartSession ises)
protected virtual void InitSmartApplication(ref ISmartApplication iapp)
基页中的其他方法……
private string ParseRootPath(string path)
此方法确保我们始终知道在网站中,我们的控件/资源等相对于派生页面的位置。它使用 ASP.NET 对波浪号“~”的标准识别作为应用程序根的分隔符。这很重要,因为我们可能正在虚拟目录中运行我们的应用程序(如果从网站根目录运行,我们可以在链接前面使用标准斜杠“/”来指定网站的根)。不要忘记,此页面可以在您网站的任何子目录中的所有页面中使用,这也意味着由于您的页面位于不同的目录/子目录中,您需要此功能,以便您的页面始终可以找到用户控件和其他网站资源等。
private void InitFromConfiguration(string pageName)
我们在此处获取自定义配置设置。请记住,框架的功能之一是单个页面可以覆盖您在 web.config 中设置的设置。这样,如果您想要相同的布局(但不同的背景颜色、脚本、元标签等),您无需硬编码即可完成,只需像任何网页设计师一样,将其放在超文本中即可。如果您不覆盖它,则页面将使用 web.config 中的设置。这遵循 ASP.NET 中所有指令使用的相同“机器/web/页面”模式。
嗯,这有很多要说的(也供你阅读)。我尽量保持简短,因为这只是一个博客帖子。所以,如果你有任何问题,尽管问,我很乐意回答。在本文的最后一部分,我将讨论智能处理程序对象(SmartSession
、SmartQueryString
、SmartApplication
)以及如何创建自己的基处理程序以不同方式持久化值。为了解释它们的用途,它们允许我们从基页内部管理查询字符串等(就像响应和请求对象已经做的那样——这些是强类型且易于使用的)。它们让我的生活变得无比轻松。
第四部分 - 实现智能会话对象
在最后一部分(暂时如此,直到有人给我一些更好的想法或者我在半夜突然想到自己的想法),我将描述如何实现智能状态对象,我们可以通过母版页从中获取数据。我真正不喜欢做的一件事是必须硬编码字符串才能在通用集合对象中查找内容,然后依赖该对象是特定类型才能实际使用它。当我独立完成项目时,这不是一个大问题,但当开发人员增加到几个人以上时,跟踪键变得很困难(当然,除非您打开跟踪并逐步完成整个应用程序,以便您可以监视状态对象的添加和删除)。
在处理查询字符串数据时,情况甚至更糟。您可以随意添加和删除查询字符串中的内容,而且不必保持一致,因此当您重定向到 Web 应用程序中的另一个页面时,您会假定下一个页面总是能够访问您需要的查询字符串元素,更糟糕的是,它们所代表的值不会在上下文中发生变化。我对此的解决方案是将这些对象封装在我自己的智能会话类中。我们曾经在一个电子商务项目中同步查询字符串遇到了一个大问题,当范围转移并且客户表示他们想要另一个功能(以及另一个查询字符串,因为该网站必须通过查询字符串进行所有状态维护)时,我们不得不遍历购买和支付流程中的每个页面等等,以确保在需要时添加此元素(更糟糕的是——他们在网站的每次重定向中都使用字符串连接运算符来完成此操作)。呸!!!
所以我花了一些时间明智地思考如何更好地解决这个问题,解决方案是 ISmartQueryString
、ISmartSession
和 ISmartApplication
接口。以下是接口示例及其允许我们做的事情……
public interface ISmartSession
{
void SetSessionState(System.Web.SessionState.HttpSessionState ses);
void FlushSessionState();
void AbandonSession();
void PersistToSession();
}
public interface ISmartQueryString
{
void SetQueryStringInfo(System.Web.UI.Page webPage);
void SmartRedirect(string url);
}
public interface ISmartApplication
{
void FlushAppState();
void PersistToAppState();
void SetApplicationState(System.Web.HttpApplicationState state);
}
ISmartQueryStirng
:允许我们将查询字符串封装在代码中的一个位置——我们总是能确保元素的存在(并设置默认值),因此我们无需在每个页面中编写代码来检查这一点。我们将在 SmartQueryStringBase
类中实现此接口(稍后会详细介绍)。
以下是需要在 ISmartQueryString
中实现的方法
void SetQueryStringInfo(System.Web.UI.Page webPage);
在这里,我们将 HTTP 处理程序(在 ASP.NET 中是页面对象)传递给我们的智能查询字符串基类。我们需要传递整个对象,因为我们将使用请求对象和响应对象中的几个项目,您稍后会看到。
void SmartRedirect(string url);
这是让我们的查询字符串在系统中保持活跃的神奇调用。是的,您的开发人员可以直接使用
Response
调用并破坏它,但如果您在代码中找到它,这很容易补救。(尝试对Response
使用Find
。您会立即找到它!)您可以做的另一件事是使用 FXCop 来防止它发生,如果您需要自动化限制。然后,您可以对误导的开发人员做任何您想做的事情,并让他们与团队的其他成员保持一致。
现在,一个框架如果没有开箱即用的此接口实现就不完整,所以我提供了一个。在此接口中,我使用反射从我的 SmartQueryString
对象中获取值(我将它称为 AcmeSmartQueryString()
,因为没有更好的名称)。您可以在以下代码中看到反射如何获取我们的智能查询字符串的属性值,并将该值移动到我们的查询字符串。我还在它前面加上一个前缀:“sqs
”,表示“Smart Query String”。这告诉我们的智能查询字符串类存在一个查询字符串,并且当请求期间初始化 MasterPage 时,它可以开始将其解析回我们的 AcmeSmartQueryString()
对象。这是代码……
[Serializable()]
public abstract class SmartQueryStringBase : ISmartQueryString
{
[field: NonSerialized()]
private System.Web.UI.Page _page = null;
public void SetQueryStringInfo(System.Web.UI.Page webPage)
{
_page = webPage;
ExtractQueryStringInfo();
}
public void SmartRedirect(string url)
{
//build a querystring from the internal elements
//of this class and redirect the user
string qs = GenerateQueryString();
if(qs != string.Empty)
_page.Response.Redirect(ParseRootPath(url) + "?sqs=true&" + qs);
else
_page.Response.Redirect(ParseRootPath(url));
}
//use reflection to take our objects field values
//and format it into a querystring
private string GenerateQueryString()
{
PropertyInfo [] props = this.GetType().GetProperties();
StringBuilder sb = new StringBuilder();
foreach(PropertyInfo prop in props)
{
BasePageFramework.Attributes.SmartQueryStringKeyAttribute [] attb =
(BasePageFramework.Attributes.SmartQueryStringKeyAttribute [])
prop.GetCustomAttributes(typeof(
BasePageFramework.Attributes.SmartQueryStringKeyAttribute),
true);
//use the name if the attribute was omitted from the property
if(attb.Length == 0)
sb.Append(prop.Name + "=" + prop.GetValue(this,
null).ToString() + "&");
else
sb.Append(attb[0].KeyName + "=" +
prop.GetValue(this,null).ToString() + "&");
}
return sb.ToString();
}
public override string ToString()
{
return "?sqs=true&" + GenerateQueryString();
}
private string ParseRootPath(string path)
{
if(path.IndexOf('~') == 0)
{
if(System.Web.HttpContext.Current.Request.ApplicationPath == "/")
{
path = path.Replace("~","");
}
else
path = path.Replace("~",
System.Web.HttpContext.Current.Request.ApplicationPath);
return path;
}
else
return path;
}
private void ExtractQueryStringInfo()
{
string sqsElement = _page.Request.QueryString["sqs"];
if(sqsElement != null && sqsElement != string.Empty)
{
PropertyInfo [] props = this.GetType().GetProperties();
foreach(PropertyInfo prop in props)
{
BasePageFramework.Attributes.SmartQueryStringKeyAttribute [] attb
= (BasePageFramework.Attributes.SmartQueryStringKeyAttribute [])
prop.GetCustomAttributes(typeof(
BasePageFramework.Attributes.SmartQueryStringKeyAttribute),true);
if(attb.Length == 0)
prop.SetValue(this,GetFieldValue(prop.Name),null);
else
prop.SetValue(this,GetFieldValue(attb[0].KeyName),null);
}
}
}
private object GetFieldValue(string elementName)
{
string retval = _page.Request.QueryString.Get(elementName);
if(retval != null)
return retval;
else
return string.Empty;
}
}
最后,如果您仔细看,您可以看到 SmartRedirect
的作用。它很棒,因为您只需为智能重定向提供您要去的 URL,智能查询字符串就会为您完成其余的工作!相当不错!
现在,如果你仔细观察,你还会看到我正在使用一个属性来生成我们查询字符串的键名。这就是 SmartQueryStringKeyAttribute()
。我们将把这个属性应用到我们的 AcmeSmartQueryString
对象的属性上,这样我们就可以在查询字符串的输出中保持键值简短而美观。
以下是我们用于创建键值输出的属性类……
public class SmartQueryStringKeyAttribute : System.Attribute
{
private string _queryStringKey = string.Empty;
public SmartQueryStringKeyAttribute(string queryStringKey)
{
_queryStringKey = queryStringKey;
}
public string KeyName
{
get{return _queryStringKey;}
}
}
最后——这是 AcmeSmartQueryString
类(我们的派生智能查询字符串对象,它保存了应用程序的智能查询字符串值)
public class AcmeSmartQueryString :
BasePageFramework.SmartState.SmartQueryStringBase
{
private string _pgNum = string.Empty;
private string _tabIdx = string.Empty;
private string _userName = string.Empty;
public AcmeSmartQueryString()
{}
"pn")>
public string PageNumber
{
get{return _pgNum;}
set{_pgNum = value;}
}
"ti")>
public string TabIndex
{
get{return _tabIdx;}
set{_tabIdx = value;}
}
"un")>
public string UserName
{
get{return _userName;}
set{_userName = value;}
}
}
您可以看到这个类不与其基成员交互,这就是使用接口的美妙之处。您无需担心如何将查询字符串与此对象之间进行转换,因为所有这些都在幕后发生。您现在可以根据需要自由地与此对象进行交互,就像它是代码中的另一个实例一样。我省略了一个属性,这样您就可以看到在下一个页面请求填充时接收端会发生什么。您将看到属性的完整名称。它只有状态,仅此而已!这就是它的美妙之处,使用这个框架的开发人员无需了解幕后发生了什么,他们只需与这个实例交互即可……请记住,尽管智能查询字符串将属性映射到我们的智能查询字符串,但您只能使用字符串(因此得名)。我正在研究如何允许您向查询字符串添加其他类型(枚举、其他值类型等)。我将把这个问题作为未来帖子的主题。
这是我们的 Acme 母版页,它不仅初始化其他母版页组件,还加载了我们的 AcmeSmartQueryString
public class AcmeBasePage : BasePageFramework.SuperBasePage
{
private AcmeControls.LeftNav _leftNav = null;
protected override void LoadBaseUITemplate(ref
BasePageFramework.IBaseTemplate tmplt)
{
tmplt = (BasePageFramework.IBaseTemplate)
LoadControl("~/Base/AcmeBaseTemplate.ascx");
}
protected override void LoadTemplatePanels()
{
_leftNav = (AcmeControls.LeftNav)
LoadControl("~/AcmeControls/LeftNav.ascx");
((AcmeBaseTemplate)
this.MainUITemplate).LeftNavigation.Controls.Add(_leftNav);
_leftNav.OnNavClick +=
new AcmeWidgets.AcmeControls.LeftNavEvent(_leftNav_OnNavClick);
}
protected override void InitializeConfiguration(ref
BasePageFramework.Configuration.ConfigLoadingArgs configParams)
{
configParams.BasePageID = "AcmeBase";
}
protected override void InitSmartQueryString(ref
BasePageFramework.SmartState.ISmartQueryString isqs)
{
isqs = (BasePageFramework.SmartState.ISmartQueryString)
new AcmeSmartState.AcmeSmartQueryString();
}
protected override void InitSmartSession(ref
BasePageFramework.SmartState.ISmartSession ises)
{
ises = (BasePageFramework.SmartState.ISmartSession)
new AcmeSmartState.AcmeSmartSession();
}
private void _leftNav_OnNavClick(object sender,
AcmeWidgets.AcmeControls.LeftNavEventArg e)
{
switch(e.PageNumber)
{
case "1":
this.SmartQueryString.SmartRedirect("Default.aspx");
break;
case "2":
this.SmartQueryString.SmartRedirect("PageTwo.aspx");
break;
case "3":
this.SmartQueryString.SmartRedirect("PageThree.aspx");
break;
case "4":
this.SmartQueryString.SmartRedirect("PageFour.aspx");
break;
default:
this.SmartQueryString.SmartRedirect("Default.aspx");
break;
}
}
}
您可以在上面页面中的事件处理程序中看到,我们正在使用当前实例的 SmartQueryString
(在请求时在我们的基页中初始化)进行重定向并保持我们的字符串元素完整。这不仅使管理查询字符串变得更容易,而且还将查询字符串中发生的所有事情集中在一个地方,从而使维护变得无限容易。
题外话,我知道你们中的一些人会说……但我讨厌使用反射!!为什么不使用格式化程序或类型转换器呢?!为了回答这个问题,您可以自己编写。我为这些对象创建了接口,这样您就可以创建自己的 SmartQueryStringBase
类(以及其他智能状态对象)——母版页基类不关心——它只需要保留对这些接口的引用,所以如果您必须这样做,请自己编写。
对于其他的智能状态对象,事情就不那么自动化了,因为我们没有“智能重定向”来将数据传递给我们的代码,以便它能够持久化。因此,我们需要告诉其他的 SmartState 对象,每当我们在其中更改某些内容时,它们就自行持久化。这些接口相当简单,除了我们必须以某种方式设置状态。这就是“SetApplicationState
”和“SetSessionState
”发挥作用的地方。将这些调用传递给适当的状态对象,然后就可以开始了!
这是我们的开箱即用的 SmartSessionBase
类
[Serializable]
public class SmartSessionBase : ISmartSession
{
// Fields
[field: NonSerialized()]
private HttpSessionState _session = null;
private void ExtractSessionStateRefs()
{
foreach(PropertyInfo inf in this.GetType().GetProperties())
inf.SetValue(this,this._session[inf.Name],null);
}
void ISmartSession.AbandonSession()
{
this._session.Abandon();
}
void ISmartSession.FlushSessionState()
{
this._session.Clear();
}
void ISmartSession.PersistToSession()
{
foreach(PropertyInfo inf in this.GetType().GetProperties())
{
this._session[inf.Name] = inf.GetValue(this,null);
}
}
void ISmartSession.SetSessionState(HttpSessionState ses)
{
this._session = ses;
this.ExtractSessionStateRefs();
}
}
这是 AcmeSmartSessionBase
的实现(我包含了 Person
对象,以便您可以看到我们在会话状态中保留的类
public class AcmeSmartSession :
BasePageFramework.SmartState.SmartSessionBase
{
private Person _person = null;
public Person SelectedPerson
{
get{return _person;}
set{_person = value;}
}
}
[Serializable()]
public class Person
{
string _name = string.Empty;
int _age = 0;
int _weight = 0;
public string Name
{
get{return _name;}
set{_name = value;}
}
public int Age
{
get{return _age;}
set{_age = value;}
}
public int Weight
{
get{return _weight;}
set{_weight = value;}
}
}
使用应用程序状态类似,只是我们在修改实例之前将其锁定。以下是我们的默认 SmartApplicationStateBase
的代码(除了线程同步内容,与 SmartSessionBase
没有太大区别)
public class SmartApplicationBase : ISmartApplication
{
[field: NonSerialized()]
private HttpApplicationState _appState = null;
#region ISmartApplication Members
public void FlushAppState()
{
CheckInit();
_appState.Clear();
}
public void PersistToAppState()
{
CheckInit();
_appState.Lock();
//update the derived smartapplication object
//to the application state...
foreach(PropertyInfo inf in this.GetType().GetProperties())
this._appState[inf.Name] = inf.GetValue(this,null);
_appState.UnLock();
}
public void SetApplicationState(System.Web.HttpApplicationState state)
{
_appState = state;
//extract the values out of application state
//so our smart application object
//has everything it needs to be referenced in the page
_appState.Lock();
foreach(PropertyInfo inf in this.GetType().GetProperties())
inf.SetValue(this,this._appState[inf.Name],null);
_appState.UnLock();
}
private void CheckInit()
{
if(_appState == null)
throw new InvalidOperationException("The SmartApplicationState" +
" must be initialized before it is accessed");
}
#endregion
}
最后,这里是如何使用这些对象的示例(请记住,我们的母版页已经为我们设置了这些对象,因此当我们需要它们时,它们都已准备就绪)
private void Button1_Click(object sender, System.EventArgs e)
{
//create an object
Classes.AcmeClass cls = new Classes.AcmeClass("Chase",30);
//set it into our smartsession instance
((Master.AcmeSmartSession)this.SmartSession).Person = cls;
//update our current smart session to the session state
this.SmartSession.PersistToSession();
AcmeSmartQueryString sqs =
(AcmeSmartQueryString)this.SmartQueryString;
sqs.Page = 2.ToString();
sqs.SmartRedirect("TestOne.aspx");
}
这就是你需要在它的时候去获取它的方式……
AcmeSmartSession sss = (AcmeSmartSession)this.SmartSession;
if(sss.Person != null)
sss.Person.Name = "Bob";
我不会发布任何关于如何使用应用程序状态的内容,因为它与 SmartSession
的工作方式完全相同。这就是智能状态对象的概括——我目前正在研究通过添加接口来检查新/脏/干净对象,以使持久化调用更高效。我将在这些增强功能可用时发布到此框架。