本页内容
![]() | 代码隐藏 |
![]() | 编译 |
![]() | 程序集生成 |
![]() | 小结 |
当我撰写本专栏的时候,Microsoft® .NET Framework 2.0 和 Visual Studio®2005 的候选版本已经“出炉”了,当您阅读到本文时,它们都已经投入使用。这一切似乎等待了很长时间。
我还记得在 2003 年的 8 月坐在 Microsoft 公司的一个房间内倾听 Scott Guthrie 和其他人(包括我的同事 Rob Howard,他也是专栏作家)介绍 ASP.NET 2.0 的大量新功能。他们演示了一个又一个功能,这些功能令我们非常吃惊,因为它们极大地简化了 Web 开发,而且是以可插入和可扩展的方式实现的,因此在开发过程中能够以任何所需级别进行更改。
后续测试版本中进行了大量更改,多数是以修改、错误修复和控件附加的形式进行的。但是,有一个功能(代码隐藏模型)自从第一个预览版以来已经进行了大量更改,这主要是为了响应客户的反馈。现在即将发布之时,我想利用这个机会描述一下这个新的代码隐藏模型、它的基本原理,以及 Web 开发人员将如何使用它。我也会介绍该模型的一些潜在的副作用以及如何在设计中解决它们。请注意,ASP.NET 2.0 运行时完全支持 1.x 模型,因此针对 1.x 编写的应用程序可以在无需修改的情况下直接运行。
代码隐藏
虽然该代码隐藏模型在 2.0 中是不同的,但是它的语法已经进行了少量更改。实际上,该更改十分细微,如果您不仔细查看,甚至都无法注意到它。图 1 显示新的代码隐藏语法。
该模型与以前的 1.x 模型有两个区别 — 在 @ Page 指令中引入了 CodeFile 属性,以及将代码隐藏类声明为部分类。当开始生成该页时,您将注意到另一个区别 — 服务器端控件不再需要在代码隐藏类中显式声明,但是您仍然能够以编程方式完整地访问它们。例如,图 2 中的窗体有若干个在代码隐藏文件中以编程方式使用的服务器端控件,但是您可以注意到,代码隐藏类中缺少任何显式控件声明。
其中的原因与应用于代码隐藏类的部分关键字有关。除了使用呈现该页的方法将 .aspx 文件转换为一个类定义(正如它已经做的一样),ASP.NET 现在也为包含受保护控件成员变量声明的代码隐藏类生成一个同辈部分类。然后,您的类与该生成的类定义一起编译,并用作针对 .aspx 文件生成的类的基类。结果是,您基本上以经常使用的方式编写代码隐藏类,但是您不再需要声明(或让服务器为您声明)服务器端控件的成员变量声明。这一直是 1.x 中一个不太稳定的关系,因为如果您无意间修改了一个控件声明,使得它不再与该窗体上所声明控件的 ID 匹配,就会突然停止工作。现在,成员变量以隐式方式声明并始终是正确的。图 3 显示所涉及类集的一个示例。
请注意,该部分类模型仅当在 @ Page 指令中使用 CodeFile 关键字时使用。如果使用不带 CodeFile(或者带有 src 属性)的 Inherits 关键字,ASP.NET 会使用 1.x 代码隐藏类型并简单地将类设置为 .aspx 文件的唯一基类。此外,如果您根本没有代码隐藏,则类生成与它在 1.x 中的操作将完全相同。由于 ASP.NET 2.0 向后与 1.x 兼容,因此现在有大量代码隐藏选项供您使用。
Visual Studio 2005 将使用任何 Web 窗体新的部分类隐藏模型,而且如果您使用转换向导,它也将很好地转换 Visual Studio .NET 2003 项目以便使用新模型。因为 ASP.NET 2.0 的一些新功能依赖于它的原因,所以如果可能,最好将所有文件转换为新代码隐藏模型(如果使用 Visual Studio,那么转换几乎是唯一的选择,因为 Visual Studio 2005 不会打开未转换的 1.x 项目)。例如,对 Profile 属性包的强类型访问添加到 2.0 中代码隐藏类的同辈部分类中,但是如果您使用 1.x 代码隐藏模型,则该强类型访问器直接添加到 .aspx 生成的类定义中,而且对于代码隐藏类不可用。这也适用于强类型的母版页和以前的页访问。
编译
此时,您可能想知道,为什么 ASP.NET 小组非要使用这个新代码隐藏模型来使用继承。ASP.NET 除了将来自 .aspx 文件的方法呈现为部分类(然后这些类与简化的代码隐藏类合并)之外,还可以轻松生成所有控件变量声明。这就是 Windows 窗体在 .NET Framework 2.0 中的工作方式。设计器生成的所有代码被放置在同辈部分类(然后该类与您的应用程序逻辑合并)中,事件处理程序被放置在窗体驱动的单个类中,从而在无需借助于继承的情况下,在计算机生成的代码和开发人员代码之间创建一个完全的分离。
嗯,ASP.NET 2.0 中代码隐藏的原始实现也执行此操作 — 代码隐藏类只是一个与分析的 .aspx 文件类定义合并的部分类。它简单有效,但遗憾的是它不够灵活。该模型的问题在于,预编译的二进制程序集中的代码隐藏文件不再能够与完整的 .aspx 文件一起部署,因为它们现在必须同时编译(使用部分类的一个限制是,一个类的所有部分必须在单个编译中合并,而且类定义无法跨越程序集)。对于许多开发人员而言,该限制是无法接受的,因为他们已经习惯于将二进制代码隐藏程序集与完整的 .aspx 文件一起部署,后者随后会进行适当的更新而不必重新编译。实际上,这就是默认情况下 Visual Studio .NET 2003 中使用的模型,而且在实践中非常流行。
由于重新引入了继承模型并将部分类移到基类中,.aspx 文件现在可以从代码隐藏类中进行独立部署和编译。为此,您需要某种方式在编译或部署过程中生成同辈的部分类,后者包含控件变量声明,因为在过去这一直是针对请求进行的。走近 ASP.NET 编译器。
在 ASP.NET 2.0 中,ASP.NET 编译器 (aspnet_compiler.exe) 最初作为完全预编译整个站点的一种方式引入,从而使得只部署二进制程序集成为可能(甚至也对 .aspx 和 .ascx 文件进行预编译)。这是非常吸引人的,因为它消除了发出请求时的任何按需编译,从而消除了目前在一些站点上可以看到的第一个部署后点击。它也使得对已部署站点进行修改更加困难(因为您无法打开 .aspx 文件并更改内容),当部署只想通过标准部署过程更改的应用程序时,这是很吸引人的。ASP.NET 2.0 的发布版本提供的编译器支持仅支持二进制的部署模型,但是它也进行了增强以支持可更新的部署模型,其中站点中的所有源代码预编译为二进制程序集,但是所有 .aspx 和 .ascx 文件都基本保持完整,以便可以在服务器上进行更改(针对 .aspx 和 .ascx 文件的更改,涉及移除的 CodeFile 属性以及进行修改以包括程序集名的 Inherits 属性)。由于在代码隐藏模型中重新引入了继承,因此该模型是可能的。这样,包含控件声明的同辈部分类可以独立于实际的 .aspx 文件类定义生成和编译。
![](https://i-blog.csdnimg.cn/blog_migrate/68e22a33065e2b496bacfbb97f05a73e.png)
图 4 使用 aspnet_compiler.exe 进行二进制部署
图 4 显示使用二进制部署选项对 aspnet_compiler.exe 实用工具的调用,以及针对部署目录的结果输出。请注意,该部署目录中的 .aspx 文件只是没有内容的标记文件。它们之所以位于那里,是为了确保 IIS 应用程序中 .aspx 扩展的“Check that file exists”选项进行设置后,带有终结点名称的文件可用。PrecompiledApp.config 文件用于跟踪应用程序的部署方式,以及 ASP.NET 是否需要在请求时编译任何文件。要生成“可更新的”站点,需要将一个 -u 添加到命令行,得到的 .aspx 文件将包含它们的原始内容(而不是空的标记文件)。请注意,该功能也可以通过 Visual Studio 2005 的 Build | Publish Web Site 菜单项以图形方式访问,如图 5 所示。该命令行工具和 Visual Studio 都依赖于 System.Web.Compilation 命名空间的 ClientBuildManager 类提供该功能。
![](https://i-blog.csdnimg.cn/blog_migrate/97f689b453f932ea836bc401b2dfa39b.png)
图 5 Visual Studio 2005 中的 Build | Publish Web Site 工具
使用手边的 aspnet_compiler 实用工具,您无需担心应用程序的大体部署方式就可使其运行,因为任何站点都能以下面三种方式之一进行部署 — 全源、全二进制或可更新(二进制文件中的源代码和源代码中的 .aspx 文件)— 无需对开发中使用的页面属性或代码文件进行任何更改。这在以前的 ASP.NET 版本中是不可能的,因为您必须在开发时决定是否使用 src 属性来引用代码隐藏文件,或者预编译它们并将程序集部署到 /bin 目录。完整的二进制部署甚至不是一个选项。
程序集生成
既然编译为程序集可以在三种情况下发生(由开发人员显式进行,使用 aspnet_compiler.exe,或者在请求处理中进行),因此了解文件到程序集的映射变得更为重要。实际上,根据编写页面的方式,您实际上可以得到一个应用程序,在作为全源或全二进制部署时,该应用程序可以正常工作,但在使用可更新的切换进行部署时,却编译失败。
模型 ASP.NET 通常使用 App_Code 目录内容的单独程序集以及 global.asax 文件(如果存在),然后将每个目录中的所有 .aspx 页编译为单独的程序集。(如果同一目录中的页面是以不同语言制作的,或者它们通过 @ Reference 指令彼此依赖,则它们也可以形成单独的程序集。)用户控件和母版页通常也独立于 .aspx 页进行编译。例如,如果要在一个项目中包含 Visual Basic® 和 C# 源代码,也可以配置 App_Code 目录来创建多个程序集。在程序集创建的细节中有一些细微差别,这取决于您所选的部署模式。图 6 描述特定 Web 站点的组件,该 Web 站点基于您要使用的部署模式编译为单独的程序集。(请注意,我要忽略资源、主题和浏览器目录,因为它们不包含代码,虽然它们也编译为单独的程序集。正如前面提到的,目标程序集也因语言的不同和引用依赖项而异。)
程序集生成的另一个技巧是,使用 aspnet_compiler 的 -fixednames 选项请求将每个 .aspx 文件编译为单独的程序集,该程序集的名称跨编译器的不同调用保持一致。如果您想更新单个页面而不修改部署站点上的其他程序集,这是很有用的。它也可以为任何大型站点生成大量程序集,因此您一定要在使用该选项之前测试您的部署。
如果您觉得这比较复杂,我可以告诉您它的优点,即您无需花费大量时间考虑将哪些文件映射为单独的程序集。.aspx 文件一直在最后进行编译,并一直包括对生成的所有其他程序集的引用,因此,无论您选择哪种部署模型,它通常都会正常工作。
在部署中,可能实际影响您在页面中制作代码的方式的一个重要区别是,当使用可更新部署时编译中的分离。当部署可更新站点时,代码隐藏文件在部署之前编译为单独的程序集。从 .aspx 文件生成的类不进行编译,除非作出对目录中文件的实际请求。这与二进制部署(其中所有文件在部署之前编译)以及源部署(其中所有文件在请求时编译)形成了鲜明对比。以下这一简单的示例解释这是如何引出问题的,请考虑图 7 中带有嵌入属性的用户控件(.ascx 文件),以及一个使用该控件并从其代码隐藏类设置该属性的相关页面。
图 7 中的页面将以源或二进制部署模式编译并运行,但是当作为可更新站点部署时将无法编译,原因是该用户控件 Color 属性的定义在部署时不可用(该限制也存在于 1.x 模型中)。要避免此类问题发生,通常您可以将所有代码放在代码隐藏文件中,或者干脆不使用代码隐藏文件,将代码直接放在 .aspx 和 .ascx 文件中。
有关文件到程序集映射的另一个注意事项是,使用内部关键字防止外部程序集访问类中的方法,这可能只在某些部署方案中奏效而在其他方案中却不然,这是因为存在不同的程序集映射选项。除非您提前计划要使用哪个部署选项,否则最好避免在页面中使用内部方法并继续使用类型范围的保护关键字:公共、受保护和私有。
小结
对于 ASP.NET 开发人员而言,ASP.NET 2.0 中的新代码隐藏模型既熟悉又陌生。之所以说熟悉是因为,它仍然使用继承将代码隐藏类与其 .aspx 生成的类定义相关联,而之所以说陌生是因为,诸如部分类这样的元素和控件成员变量声明的隐式生成都是基本的转换。实际上,您可能不会注意到用法上的许多差别,但是无论您何时进行非一般的操作(例如,创建一个通用基 Page 类,或者将代码隐藏与内联代码模型混合),了解本文描述的类关系和程序集映射都是很重要的。
附录:
Default.aspx
<%@ Page Language="C#" AutoEventWireup="true"
CodeFile="Default.aspx.cs" Inherits="MsdnMag.Default" %>
Default.aspx.cs
namespace MsdnMag
{
public partial class Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
}
}
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="MsdnMag.Default" %> <!DOCTYPE html PUBLIC "..." "..."> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Untitled Page</title> </head>
<body> <form id="form1" runat="server"> <div> Enter your name: <asp:TextBox ID="_nameTextBox" runat="server" /><br /> <asp:Button ID="_enterButton" runat="server" Text="Enter" OnClick="_enterButton_Click"/> <br /> <asp:Label ID="_messageLabel" runat="server" /> </div> </form> </body> </html>Default.aspx.cs
namespace MsdnMag {
public partial class Default : System.Web.UI.Page {
protected void _enterButton_Click(object sender, EventArgs e) {
_messageLabel.Text = "Hello there " + _nameTextBox.Text + "!";
}
}
}
Class for ASPX file generated by ASP.NET
namespace ASP { public class default_aspx : MsdnMag.Default { ... } }Sibling partial class generated by ASP.NET
namespace MsdnMag { public partial class Default : IRequiresSessionState { protected TextBox _nameTextBox; protected Button _enterButton; protected Label _messageLabel; private HtmlForm form1; ... } }Codebehind partial class that you write
namespace MsdnMag {
public partial class Default : Page {
void _enterButton_Click(object sender, EventArgs e) {
_messageLabel.Text = "Hello there " + _nameTextBox.Text + "!";
}
}
}
Deployment Mode | |||
---|---|---|---|
All Source | All Binary | Updatable (mixed) | |
What compiles into a unique assembly | App_Code directory global.asax .ascx and associated codebehind file (separate assembly for each user control) .master and associated codebehind file (separate assembly for each master page) All .aspx files and their codebehind files in a given directory (separate assembly per directory) | App_Code directory global.asax .ascx and .master files and their associated codebehind files All .aspx files and their codebehind files in a given directory (separate assembly per directory) | App_Code directory (D) global.asax (R) .ascx and .master files (R) codebehind files for .ascx and .master files (D) All .aspx files in a given directory (separate assembly per directory) (R) All codebehind files associated with .aspx files in a given directory (separate assembly per directory) (D) |
When it's compiled | Request time | Deployment time | (R) = Compiled at request time (D) = Compiled at deployment time |
MyUserControl.ascx
<%@ Control Language="C#"%>
<script runat="server">
public string Color {
get { return (string)ViewState["color"] ?? "white"; }
set { ViewState["color"] = value; }
}
protected override void OnLoad(EventArgs e) {
_mainPanel.BackColor = System.Drawing.Color.FromName(Color);
base.OnLoad(e); }
</script>
<asp:Panel runat="server" ID="_mainPanel">
<h2>Some text</h2>
Other text
</asp:Panel>
Default.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %> <%@ Register Src="MyUserControl.ascx" TagName="MyUserControl" TagPrefix="uc1" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server"> <div> <uc1:MyUserControl id="MyUserControl1" runat="server"> </uc1:MyUserControl></div> </form> </body> </html>Default.aspx.cs
using System;
using System.Web.UI;
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
MyUserControl1.Color = "purple";
}
}