表示层设计模式:在 ASP.NET 中实现 Page Controller
 

2009-01-07 来源:microsoft

 
本页内容
上下文
实现策略
测试考虑事项
结果上下文
相关模式
 

上下文

您要在 ASP.NET 中构建 Web 应用程序,您希望通过使用内置的 Page Controller(页面控制器)来利用 ASP.NET 的事件驱动特性。

实现策略

默认情况下,Page Controller 模式中所描述的概念是在 ASP.NET 中实现的。ASP.NET 页面框架实现这些概念所采取的方式使得在客户端上捕获事件、将其传输到服务器并调用适当方法这一系列操作的基本机制是自动进行的,并且对实现者来说是不可见的。页面控制器是可扩展的,因为它会在生命周期的特定点上公开各种事件(请参阅此模式后面的"页面生命周期"),因此,与应用程序具体相关的操作可以在适当的时候运行。

例如,假定用户正在与包含一个按钮服务器控件的 Web 窗体页进行交互(请参阅此模式后面的"简单页面示例")。当用户单击按钮控件时,一个事件将作为 HTTP 投递内容传送到服务器,在那里,ASP.NET 页面框架会解释投递的信息,并将引发的事件与适当的事件处理程序相关联。框架自动调用该按钮的适当事件处理程序,作为框架的正常处理的一部分。因此,您不再需要实现此功能。此外,您还可以使用内置控制器,或者,您可以用自己自定义的控制器来代替内置控制器(请参阅 Front Controller)。

页面生命周期

下面按发生顺序列出了页面生命周期中最常见的各个阶段。其中还包括引发的特定事件,以及处理请求时在各个阶段可能执行的一些典型操作:

  • ASP.NET 页面框架初始化(事件: Init 。这是生命周期的第一个步骤,该步骤将初始化 ASP.NET 运行库以便为响应请求做好准备。
  • 用户代码初始化(事件: Load 。您应该执行与应用程序具体相关的常见任务,例如,当页面控制器引发 Load 事件时打开数据库连接。您可以假设:引发 Load 事件后,服务器控件已创建并完成初始化、状态已还原并且窗体控件反映了客户端的更改。 [Reilly02]
  • 与应用程序相关的事件处理。在此阶段,您应该执行与应用程序相关的处理,以响应控制器引发的事件。 .
  • 清理(事件: Unload 。该页面已完成生成,现在可以丢弃。您应该关闭 Load 事件打开的任何数据库连接,丢弃任何不再需要的对象。在连接对象被作为垃圾回收后,Microsoft?.NET Framework 将自动关闭数据库连接。不过,您对何时进行垃圾回收没有任何控制权。因此,显式关闭数据库连接以充分利用数据库连接池是一个很好的做法。

注意:还有几个页面处理阶段没有在这里列出。不过,这些阶段不用于大多数页面处理情况。

简单页面示例

第一个示例是一个简单页面,它接受来自用户的输入,然后在屏幕上显示该输入。该示例说明了 ASP.NET 用于实现服务器控件的事件驱动模型。

1: 简单页面

当用户键入他或她的名字、然后单击"Click Here"按钮后,键入的名字将直接出现在按钮下面,如图 2 所示。

2: 显示用户输入的简单页面

在 ASP.NET 网页中,用户界面编程分为两个不同的部分:可视组件(或视图)和结合了模型和控制器的逻辑。这种划分将页面的可视部分(视图)同与页面交互的、页面后面的代码(模型和控制器)分离开来。

可视元素称为 Web 窗体页。该页面由包含静态 HTML 服务器控件或 ASP.NET 服务器控件(或同时包含这两种控件)的文件构成。在此示例中,Web 窗体页名为 SimplePage.aspx,它由以下代码组成:

<%@ Page language="c#" Codebehind="SimplePage.aspx.cs" AutoEventWireup="false" Inherits="SimplePage" %> 
<HTML> 
   <body> 
      <form id="Form1" runat="server"> 
         Name:<asp:textbox id="name" runat="server" /> 
         <p /> 
         <asp:button id="MyButton" text="Click Here" OnClick="SubmitBtn_Click" runat="server" /> 
         <p /> 
         <span id="mySpan" runat="server"></span> 
      </form> 
   </body> 
</HTML> 

Web 窗体页的逻辑由为了与窗体进行交互而创建的代码构成。编程逻辑放在一个与用户界面文件分离的文件中。此文件被称为"代码隐藏"文件,文件名是 SimplePage.aspx.cs:

using System; 
using System.Web.UI; 
using System.Web.UI.WebControls; 
using System.Web.UI.HtmlControls; 
public class SimplePage : System.Web.UI.Page 
{ 
   protected System.Web.UI.WebControls.TextBox name; 
   protected System.Web.UI.WebControls.Button MyButton; 
   protected System.Web.UI.HtmlControls.HtmlGenericControl mySpan; 
   public void SubmitBtn_Click(Object sender, EventArgs e)  
   { 
      mySpan.InnerHtml = "Hello, " + name.Text + ".";  
   } 
} 

此代码的用途是通知页面控制器:当用户单击按钮后,将向服务器发送一个请求,并执行 SubmitBtn_Click 函数。

此实现显示了连接到控制器所提供的事件是多么简单。它还说明,用这种方式编写的代码更易于理解,因为应用程序逻辑没有与管理事件调度的低级代码结合起来。

公用外观示例

下面的示例使用页面控制器的典型实现策略来提供显示动态内容的横幅,该横幅在应用程序的每一页上显示已验证的用户的电子邮件地址(该地址是从数据库检索的)。

站点内的所有页面对象所继承的基类中包含了公用实现。图 3 显示了站点中的一个网页。

3: 显示动态内容的横幅

站点中的各个页面负责呈现自己的内容,而基类则负责呈现头信息。因为各个页面是从基类继承的,所以它们都具有相同的功能。

此实现使用了称为 Template Method的设计模式。该模式在一个操作中定义了一个算法的框架,而将一些步骤交给子类完成。Template Method 允许子类重新定义算法的某些步骤,而不必更改该算法的结构。 [Gamma95]

Template Method 应用于此问题需要将公用代码从各个页面移到一个基类中。这样可以确保公用代码放在一个地方,并且很容易维护。在此示例中,基类名为 BasePage 并负责将 Page_Load 方法连接到 Load 事件。与 BasePage 相关的工作(即从数据库检索用户的电子邮件地址和设置站点名)完成后,Page_Load 函数将调用名为 PageLoadEvent 的方法。子类实现 PageLoadEvent,以执行它们自己的特定 Load 功能。图 4 显示了此解决方案的结构。

4: 代码隐藏页面实现的结构

请求网页时,ASP.NET 运行库会触发 Load 事件,该事件再调用 BasePagePage_Load 方法。BasePage 方法检索所需数据,然后对所请求的特定页面调用 PageLoadEvent,以执行任何与页面相关的所需加载。图 5 显示了页面请求序列。

5: 页面请求序列

通过以这种方式实现公用功能,页面不必设置头信息,并且还可以很容易地进行整个站点的更改。如果头信息呈现和初始化代码不包含在一个文件中,则必须对包含与头信息有关的代码的所有文件进行更改。

BasePage.cs

基类代码实现了以下功能:

  • 将 Load 事件连接到 Page_Load 方法,以便进行与请求具体相关的初始化。
  • 从请求上下文检索已验证的用户的名字,并使用 DatabaseGateway 类在数据库中查找该用户的记录。该代码将 eMail 标签分配给用户的电子邮件地址。
  • 将站点名分配给 siteName 标签。
  • 调用 PageLoadEvent 方法,可以由派生类实现该方法以进行任何与页面相关的加载。

注意:最好将 BasePage 类定义为抽象类,因为这样可以强制实现者提供 PageLoadEvent 的实现。不过,在 Microsoft Visual Studio? .NET 中,不可能将此基类定义为抽象类。相反,该类提供可由派生类覆盖的默认实现。

using System; 
using System.Web.UI; 
using System.Web.UI.WebControls; 
public class BasePage : Page 
{ 
   protected Label eMail; 
   protected Label siteName; 
   virtual protected void PageLoadEvent(object sender, System.EventArgs e) 
   {} 
   protected void Page_Load(object sender, System.EventArgs e) 
   { 
         if(!IsPostBack) 
         { 
               string name = Context.User.Identity.Name; 
               eMail.Text = DatabaseGateway.RetrieveAddress(name); 
               siteName.Text = "Micro-site"; 
               PageLoadEvent(sender, e); 
         } 
   } 
   #region Web Form Designer generated code 
   override protected void OnInit(EventArgs e) 
   { 
      // 
      // 
      // CODEGEN: 此调用是 ASP.NET Web 窗体设计器所必需的。 
         // 
         InitializeComponent(); 
         base.OnInit(e); 
   } 
   /// <summary> 
   /// 设计器支持所必需的方法 - 不要使用代码编辑器修改 
   /// 此方法的内容。 
   /// </summary> 
   private void InitializeComponent() 
   {     
         this.Load += new System.EventHandler(this.Page_Load); 
   } 
   #endregion 
} 

BasePage.inc

您不仅必须为页面后面的逻辑代码提供公用基类,而且还必须提供用来保存视图或 UI 的呈现代码的公用文件。该代码包括在每个 .aspx 页面中。此 HTML 文件不是为了用于进行独立显示。通过使用公用文件,您可以在一个地方进行更改,并将这些更改传播到包括该文件的所有网页。下面的示例代码显示了此示例的公用文件,文件名为 BasePage.inc:

<table width="100%" cellspacing="0" cellpadding="0"> 
   <tr> 
      <td align="right" bgcolor="#9c0001" cellspacing="0" cellpadding="0" width="100%" height="20"> 
         <font size="2" color="#ffffff">欢迎: 
         <asp:Label id="eMail" runat="server">username</asp:Label>&nbsp; </font> 
      </td> 
   </tr> 
   <tr> 
      <td align="right" width="100%" bgcolor="#d3c9c7" height="70"> 
         <font size="6" color="#ffffff"> 
         <asp:Label id="siteName" Runat="server">Micro-site Banner</asp:Label>&nbsp; </font> 
      </td> 
   </tr> 
</table> 

DatabaseGateway.cs

该类封装了这些页面对数据库的所有访问。这是 Table Data Gateway [Fowler03] 的一个例子,它提供了此应用程序中的页面的模型代码。

using System; 
using System.Collections; 
using System.Data; 
using System.Data.SqlClient; 
public class DatabaseGateway 
{ 
   public static string RetrieveAddress(string name) 
   { 
         String address = null; 
         String selectCmd =  
               String.Format("select * from webuser where (id = '{0}')", 
               name); 
         SqlConnection myConnection =  
               new SqlConnection("server=(local);database=webusers;Trusted_Connection=yes"); 
         SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection); 
         DataSet ds = new DataSet(); 
         myCommand.Fill(ds,"webuser"); 
         if(ds.Tables["webuser"].Rows.Count == 1) 
         { 
               DataRow row = ds.Tables["webuser"].Rows[0]; 
               address = row["address"].ToString(); 
         } 
         return address; 
   } 
} 

Page1.aspx

下面是如何在页面中使用公用功能的示例:

<%@ Page language="c#" Codebehind="Page1.aspx.cs" AutoEventWireup="false" Inherits="Page1" %> 
<HTML> 
   <HEAD> 
      <title>Page-1</title> 
   </HEAD> 
   <body> 
      <!-- #include virtual="BasePage.inc" --> 
      <form id="Page1" method="post" runat="server"> 
         <h1>Page: 
            <asp:label id="pageNumber" Runat="server">NN</asp:label></h1> 
      </form> 
   </body> 
</HTML> 

该文件中的以下指令用于加载头信息的公用 HTML:

<!-- #include virtual="BasePage.inc" --> 

Page1.aspx.cs

代码隐藏类必须从 BasePage 类继承,然后实现 PageLoadEvent 方法来进行任何与页面具体相关的加载。在此示例中,与页面具体相关的活动是将数字 1 分配给 pageNumber 标签。

using System; 
using System.Web.UI; 
using System.Web.UI.WebControls; 
public class Page1 : BasePage 
{ 
   protected System.Web.UI.WebControls.Label pageNumber; 
   protected override void PageLoadEvent(object sender, System.EventArgs e) 
   { 
      pageNumber.Text = "1"; 
   } 
} 

测试考虑事项

对 ASP.NET 运行库的依赖性使实现的测试变得很困难。不可能对从 System.Web.UI.Page 或环境中包含的其他各种类继承而来的类进行实例化。这样就不可能对应用程序的各个部分单独进行单元测试。自动测试此实现的唯一方法是,生成 HTTP 请求,然后检索 HTTP 响应,确定响应是否正确。此方法容易产生错误,因为您是将响应的文本与预期的文本进行比较。

结果上下文

内置的 ASP.NET 页面控制器功能具有以下优缺点:

优点

  • 充分利用框架功能。页面控制器功能内置在 ASP.NET 中,通过将与应用程序具体相关的动作连接到由控制器公开的事件,可以轻松地对它进行扩展。另外,通过使用代码隐藏功能,还可以很容易地将与控制器具体相关的代码与模型和视图代码分离开来。
  • 显式 URL 用户输入的 URL 引用了应用程序中的实际网页。这意味着这些网页可以作为书签,并在以后输入。URL 还倾向于使用更少的参数,以便让用户更容易输入它们。
  • 增加了模块性和重用性。"公用外观"示例说明了您可以如何对许多页面重用 BasePage,而不必修改 BasePage 类或 HTML 文件。

缺点

  • 需要更改代码。正如"公用外观"示例中说明的那样,为了共享公用功能,必须对各个网页进行修改,以便继承新定义的基类而不是 System.Web.UI.PageIntercepting Filter 模式描述了通过更改 Web.config 文件而不是网页本身来添加公用功能的机制。
  • 使用继承。"公用外观"示例通过使用继承来让多个网页共享实现。学习面向对象编程方法的大多数程序员一开始会喜欢继承。不过,使用继承来共享实现常常会导致软件很难更改。如果基类因条件逻辑而变得复杂,最好引入帮助器类或者考虑使用 Front Controller
  • 难以测试。由于页面控制器是在 ASP.NET 中实现的,因此很难单独测试。要提高可测试性,您应该将同样多的功能从 ASP.NET 专用代码中分隔到不依赖于 ASP.NET 的类中。这样,不必启动 ASP.NET 运行库就能进行测试。

相关模式

有关详细信息,请参阅以下相关模式:


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织