代码改变世界

Page Controller及其在ASP.NET中的实现

2007-07-30 00:52  Anders Cui  阅读(1283)  评论(0编辑  收藏  举报

1.
问题  

您已经决定使用 Model-View-Controller (MVC) 模式来将动态 Web 应用程序的用户界面组件与业务逻辑分隔开来。要构建的应用程序将以动态方式构造网页,但网页间导航多为静态导航。

如何以最佳方式为适度复杂Web应用程序构建控制器,从而既能避免代码重复,又能实现重用性和灵活性?

2. 分析

2.1 影响因素

以下因素影响这种情况中的系统,在考虑上述问题解决方案时必须协调这些因素:

MVC 模式通常主要关注模型与视图之间的分隔,而对于控制器的关注较少。在许多胖客户端方案中,控制器和视图之间的分隔相对次要,因此通常会被忽略。但在瘦客户端应用程序中,视图和控制器本来就是分隔的,这是因为显示是在客户端浏览器中进行的,而控制器是服务器端应用程序的一部分。因此有必要对控制器进行更为仔细的研究。

在动态Web应用程序中,多用户操作可以导致不同的控制器逻辑,然后显示相同页面。例如,在基于 Web 的简单电子邮件应用程序中,发送邮件和从收件箱中删除邮件这两个操作都可能将用户返回(刷新后的)收件箱页面。虽然这两种活动完成之后显示相同页面,但应用程序必须根据上一页面以及用户所单击的按钮来执行不同的操作。

显示大多数动态网页的代码都包括非常相似的步骤:验证用户身份、从查询字符串或表单域中提取页面参数、收集会话信息、从数据源检索数据、生成页面动态部分以及添加适用的页眉和页脚。这可能导致大量的代码重复。

脚本化服务器页面(如ASP.NET)可能很容易创建,但在应用程序不断增大时可能带来一些缺点。脚本化页面不能较好地分隔控制器和视图,因而降低了重用的可能性。例如,如果多个操作将导致相同页面,在多个控制器之间重用显示代码则会比较困难,这是因为显示代码与控制器代码混合在一起。对散布于业务逻辑和显示逻辑之间的脚本化服务器页面也更加难以进行测试和调试。最后,开发脚本化服务器页面要求同时精通开发业务逻辑和制作美观高效的 HTML 页面,而很少有人兼备这两项技能。基于上述考虑,因此有必要最大程度地减少脚本化服务器页面代码,而在实际类中开发业务逻辑

正如 MVC 模式中的相关叙述,测试用户界面代码往往耗时而单调。如果可以分隔用户界面专用代码和实际业务逻辑,测试业务逻辑则会更为简单,且可重复性更强。对于显示部分和应用程序控制器部分都是如此。

通用外观和导航结构往往可以提高 Web 应用程序的可用性和品牌认知度。但通用外观可能会导致显示代码重复,特别是在脚本化服务器页面中嵌入代码时。因此,需要一种机制以提高页面间显示逻辑的重用性

3. 解决方案

使用 Page Controller 模式接受来自页面请求的输入、调用请求对模型执行的操作以及确定应用于结果页面的正确视图分隔调度逻辑和所有视图相关代码。如果合适,创建用于所有页面控制器的公用基类,以避免代码重复并提高一致性和可测试性。下图显示了页面控制器与模型和视图的关系。

 

页面控制器可接收页面请求、提取所有相关数据、调用对模型的所有更新以及向视图转发请求。而视图又将根据该模型检索要显示的数据。定义独立页面控制器将分隔模型与 Web 请求细节(例如会话管理,或使用查询字符串或隐藏表单域向页面传递参数)。按照这种基本形式,为 Web 应用程序中的每个链接创建控制器。控制器因而将变得非常简单,因为每次仅须考虑一个操作。

为每个网页(或操作)创建独立控制器可能会导致大量代码重复。因此应该创建 BaseController 类以合并验证参数(请参阅下图)等公用函数。每个独立页面控制器都可以从 BaseController 继承此公用功能。除了从公用基类继承之外,还可以定义一组Helper类,控制器可以调用这些类来执行公用功能。

 

如果多数页面相似,并且可以将公用功能放入一个基类,则此方法非常有效。页面变化越多,必须插入继承树的级别也就越多。比如,所有页面都分析参数,但只有显示列表的页面才从数据库检索数据,而需要输入数据的页面则会更新模型而不检索数据。现在可以引入两个新基类,即 ListController DataEntryController,这两个类都是继承 BaseController 而得到的。然后列表页可以从 ListController 继承,而数据输入页则可以从 DataEntryController 继承。虽然这种方法在这个简单示例中非常有效,但在处理实际业务应用时,继承树可能很深且非常复杂。您可能希望向基类中添加条件逻辑,以适应某些变体,但如此操作将违反封装原则,基类也会因此在更改系统时造成较大麻烦。因此在应用程序变得更为复杂时,应当考虑使用Helper类或者Front Controller模式

因为很多时候都需要对 Web 应用程序使用页面控制器,因此多数Web应用程序框架都默认实现页面控制器。大多数框架以服务器页面的形式包含了页面控制器(例如 ASPJSP PHP)。服务器页面实际上组合了视图和控制器的功能,但没有提供显示代码与控制器代码之间的相应分隔。遗憾的是,对于有些框架,混合视图相关代码与控制器相关代码很轻松,但要正确分隔控制器逻辑却很困难。因此,Page Controller 方式在很多开发人员中口碑不佳。现在很多开发人员都将 Page Controller 与较差设计联系在一起,而将 Front Controller 与较好设计联系在一起。实际上,这种感觉是由于具体的实现在不完善的情况下造成的;Page Controller Front Controller 都是可行性极佳的体系结构选择。

因此,最好将控制器逻辑单独放入可以从服务器页面调用的独立类。ASP.NET 页面框架提供了可以实现这种分隔的完善机制,这种机制称为"代码隐藏类"

ASP.NET中的Page类正是上述分析的良好实现

3.1 变型

大多数情况下,页面控制器取决于基于 HTTP Web 请求的具体细节。因此,页面控制器代码通常包含对 HTTP 头、查询字符串、表单域、多部分表单请求等的引用。因此在 Web 应用程序框架之外测试控制器代码非常困难。唯一方法是通过模拟 HTTP 请求和分析结果来测试控制器。这种类型的测试既费时且易出错。因此,要提高可测试性,可以将依赖 Web 的代码和不依赖 Web 的代码分别放入两个单独类中

 

在此示例中,AspNetController 封装了在应用程序框架 (ASP.NET) 上的所有依赖项。例如,它可以提取来自 Web 请求的所有传入参数,并使用独立于 Web 界面的方式(例如,使用集合)将其传递至 BaseController。此方法不仅可提高可测试性,而且允许通过其他用户界面重用该控制器代码,例如胖客户端界面或自定义脚本语言。

此方法的缺点在于增加了开销。现在新增了一个类,并且在处理每个请求前必须首先对其进行转换。因此,应尽可能控制器受环境影响的部分,并权衡选择降低依赖性与提高开发效率及执行效率。

4. ASP.NET中的实现

4.1 实现策略

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

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

4.2 简单示例

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


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


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

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

<%@ Page Language="C#" AutoEventWireup="true" Codebehind="Default.aspx.cs" Inherits="PageController._Default" %>

<!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">
        Name:
<asp:TextBox ID="name" runat="server" />
        
</>
        
<asp:Button ID="MyButton" Text="Click Here" OnClick="SubmitBtn_Click" runat="server" />
        
</>
        
<span id="mySpan" runat="server"></span>
    
</form>
</body>
</html>

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

using System;
using System.Web;
using System.Web.UI;

namespace PageController
{
    
public partial class _Default : System.Web.UI.Page
    {
        
protected void SubmitBtn_Click(object sender, EventArgs e)
        {
            mySpan.InnerHtml 
= "Hello, " + name.Text + ".";
        }
    }
}

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

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

4.3 公用外观示例

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

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


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

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

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

 

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

 

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

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

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

注意该类提供可由派生类覆盖的默认实现

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace PageController
{
    
public partial class BasePage : Page
    {
        
protected Label eMail;
        
protected Label siteName;

        
override protected void OnInit(EventArgs e)
        {
            
// 
            this.Load += new System.EventHandler(this.Page_Load);

            
base.OnInit(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 
= "my cool site";

                PageLoadEvent(sender, e);
            }
        }

        
// this method can be overridden by sub class.
        virtual protected void PageLoadEvent(object sender, System.EventArgs e)
        {
        }

    }
}

您不仅必须为页面后面的逻辑代码提供公用基类,而且还必须提供用来保存视图或 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">Welcome:
         
<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>

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

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Page1.aspx.cs" Inherits="PageController.Page1" %>

<!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>
    
<!-- #include virtual="BasePage.inc" -->
    
<form id="form1" runat="server">
        
<h1>Page:<asp:label id="pageNumber" Runat="server">NN</asp:label></h1>
    
</form>
</body>
</html> 

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

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace PageController
{
    
public partial class Page1 : BasePage
    {
        
protected override void PageLoadEvent(object sender, System.EventArgs e)
        {
            pageNumber.Text 
= "1";
        }
    }
}

5. 结论

内置的 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 运行库就能进行测试。

本文整理自《使用 Microsoft .NET 的企业解决方案模式》一书中的相关内容。