上下文
您要在 Microsoft? ASP.NET 中构建 Web 应用程序,并且需要基于应用程序的复杂性分离程序的不同方面,以减少代码的重复,并限制更改的传播。
实现策略
为了解释如何在 ASP.NET 中实现 Model-View-Controller 模式,并说明在软件中分离模型、视图和控制器角色的好处,下面的示例将一个没有分离所有三个角色的单页面解决方案重构为分离这三个角色的解决方案。示例应用程序是一个带有下拉列表的网页(如图 1 所示),该页面显示了存储在数据库中的记录。

图 1:网页示例
用户从下拉列表选择特定唱片,再单击"提交"按钮。然后,应用程序从数据库中检索此唱片中的所有曲目信息,并在一个表中显示结果。此模式中描述的所有三个解决方案均实现了完全相同的功能。
单个 ASP.NET 页
可以通过许多方法在 ASP.NET 中实现该页。最简单、最直接的方法是,将所有内容放在一个名为"Solution.aspx"的文件中,如下面的代码示例所示:
<%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <html> <head> <title>start</title> <script language="c#" runat="server"> void Page_Load(object sender, System.EventArgs e) { String selectCmd = "select * from Recording"; SqlConnection myConnection = new SqlConnection( "server=(local);database=recordings;Trusted_Connection=yes"); SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection); DataSet ds = new DataSet(); myCommand.Fill(ds, "Recording"); recordingSelect.DataSource = ds; recordingSelect.DataTextField = "title"; recordingSelect.DataValueField = "id"; recordingSelect.DataBind(); } void SubmitBtn_Click(Object sender, EventArgs e) { String selectCmd = String.Format( "select * from Track where recordingId = {0} order by id", (string)recordingSelect.SelectedItem.Value); SqlConnection myConnection = new SqlConnection( "server=(local);database=recordings;Trusted_Connection=yes"); SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection); DataSet ds = new DataSet(); myCommand.Fill(ds, "Track"); MyDataGrid.DataSource = ds; MyDataGrid.DataBind(); } </script> </head> <body> <form id="start" method="post" runat="server"> <h3>录音</h3> 选择录音:<br /> <asp:dropdownlist id="recordingSelect" runat="server" /> <asp:button runat="server" text="提交" OnClick="SubmitBtn_Click" /> <p/> <asp:datagrid id="MyDataGrid" runat="server" width="700" backcolor="#ccccff" bordercolor="black" showfooter="false" cellpadding="3" cellspacing="0" font-name="Verdana" font-size="8pt" headerstyle-backcolor="#aaaadd" enableviewstate="false" /> </form> </body> </html>
此文件实现了该模式中的所有三个角色,但并没有将它们分离到不同的文件或类中。视图角色是由与 HTML 具体相关的页面生成代码来表示的。此页使用绑定数据控件的实现来显示从数据库返回的 DataSet 对象。模型角色在 Page_Load 和 SubmitBtn_Click 函数中实现。控制器角色不是直接表示的,而是隐含在 ASP.NET 中;请参阅"Page Controller"Page Controller"。当用户发出请求时更新该页。Model-View-Controller 将它描述为被动控制器。ASP.NET 实现控制器角色,但程序员负责将动作连接到控制器将响应的事件。在本示例中,控制器在该页加载之前调用 Page_Load 函数。当用户单击"提交"按钮后,控制器调用 SubmitBtn_Click 函数。
此网页非常简单,并且是独立完整的。该实现十分有用,当应用程序很小并且不经常更改时,它是一个很出色的起点。不过,如果在开发过程中出现以下情况中的一种或多种,则应该考虑更改此方法。
• | 希望提高并行性,并减少发生错误的可能性。您可能希望由不同的人编写视图代码和模型代码,以提高并行性,并限制发生错误的可能性。例如,如果所有代码都在一个网页上,开发人员可能在更改 DataGrid 的格式时不小心更改了某些访问数据库的源代码。直到再次查看该页时,您才会发现此错误,因为再次查看该页时才会对其进行编译。 |
• | 您希望在多个网页上重用数据库访问代码。在当前的实现中,无法在不重复代码的情况下在其他网页中重用任何代码。重复的代码很难维护,因为如果数据库代码发生了更改,您必须修改访问数据库的所有网页。 |
为了解决某些这样的问题,ASP.NET 的实现引入了代码隐藏功能。
代码隐藏重构
利用 Microsoft Visual Studio? .NET 开发系统的代码隐藏功能,可以很容易地将表示(视图)代码与 Model-Controller 代码分离开来。每个 ASP.NET 页都有一种机制,这种机制允许在单独的类中实现从网页调用的方法。该机制是通过 Visual Studio .NET 提供的,它有许多优点,例如 Microsoft IntelliSense? 技术。当您使用代码隐藏功能来实现网页时,可以使用 IntelliSense 来显示网页后面的代码中所使用的对象的可用方法列表。IntelliSense 不适用于 .aspx 页。
以下是同一个示例,但这次使用代码隐藏功能来实现 ASP.NET。
视图
现在,表示代码在名为 Solution.aspx 的单独文件中:
<%@ Page language="c#" Codebehind="Solution.aspx.cs" AutoEventWireup="false" Inherits="Solution" %> <html> <head> <title>解决方案</title> </head> <body> <form id="Solution" method="post" runat="server"> <h3>录音</h3> 选择录音:<br/> <asp:dropdownlist id="recordingSelect" runat="server" /> <asp:button id="submit" runat="server" text="Submit" enableviewstate="False" /> <p/> <asp:datagrid id="MyDataGrid" runat="server" width="700" backcolor="#ccccff" bordercolor="black" showfooter="false" cellpadding="3" cellspacing="0" font-name="Verdana" font-size="8pt" headerstyle-backcolor="#aaaadd" enableviewstate="false" /> </form> </body> </html>
此代码的大部分与第一个实现中使用的代码类似。主要区别在第一行:
<%@ Page language="c#" Codebehind="Solution.aspx.cs" AutoEventWireup="false" Inherits="Solution" %>
该行告诉 ASP.NET 环境:代码隐藏类实现了该页中所引用的方法。因为该页没有任何访问数据库的代码,所以,即使数据库访问代码发生更改,也不需要修改该页。熟悉用户界面设计的人可以在数据库访问代码不会出现任何错误的情况下修改此代码。
Model-Controller
该解决方案的第二部分是以下代码隐藏网页:
using System; using System.Data; using System.Data.SqlClient; public class Solution : System.Web.UI.Page { protected System.Web.UI.WebControls.Button submit; protected System.Web.UI.WebControls.DataGrid MyDataGrid; protected System.Web.UI.WebControls.DropDownList recordingSelect; private void Page_Load(object sender, System.EventArgs e) { if(!IsPostBack) { String selectCmd = "select * from Recording"; SqlConnection myConnection = new SqlConnection( "server=(local);database=recordings;Trusted_Connection=yes"); SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection); DataSet ds = new DataSet(); myCommand.Fill(ds, "Recording"); recordingSelect.DataSource = ds; recordingSelect.DataTextField = "title"; recordingSelect.DataValueField = "id"; recordingSelect.DataBind(); } } void SubmitBtn_Click(Object sender, EventArgs e) { String selectCmd = String.Format( "select * from Track where recordingId = {0} order by id", (string)recordingSelect.SelectedItem.Value); SqlConnection myConnection = new SqlConnection( "server=(local);database=recordings;Trusted_Connection=yes"); SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection); DataSet ds = new DataSet(); myCommand.Fill(ds, "Track"); MyDataGrid.DataSource = ds; MyDataGrid.DataBind(); } #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.submit.Click += new System.EventHandler(this.SubmitBtn_Click); this.Load += new System.EventHandler(this.Page_Load); } #endregion }
此代码已从单个 ASP.NET 页移到独立的文件中。需要进行几处语法更改,以便将两个实体链接在一起。类中定义的成员变量的名称与 Solution.aspx 文件中引用的名称一样。必须显式定义的另一方面是,控制器如何链接对必须执行的操作进行响应的事件。在此示例中,InitializeComponent 方法链接了两个事件。第一个是 Load 事件,它被链接到 Page_Load 函数。第二个是 Click 事件,它在单击"提交"按钮后触发 SubmitBtn_Click 函数的运行。
代码隐藏功能是一种很好的机制,它将视图角色与模型角色和控制器角色分开。当您需要让其他网页重用代码隐藏类中的代码时,该功能可能变得不太适合。从技术角度看,重用代码隐藏网页中的代码是可能的,但不建议您这样做,因为这样会增加共享代码隐藏类的所有网页的相互关联度。
Model-View-Controller 重构
为了解决最后一个问题,需要将模型代码与控制器进一步分离。视图代码与前面实现中的代码相同。
模型
下面的代码示例描述了该模型,并且只与数据库相关;它不包含任何与视图相关的代码(依赖于 ASP.NET 的代码):
using System; using System.Collections; using System.Data; using System.Data.SqlClient; public class DatabaseGateway { public static DataSet GetRecordings() { String selectCmd = "select * from Recording"; SqlConnection myConnection = new SqlConnection( "server=(local);database=recordings;Trusted_Connection=yes"); SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection); DataSet ds = new DataSet(); myCommand.Fill(ds, "Recording"); return ds; } public static DataSet GetTracks(string recordingId) { String selectCmd = String.Format( "select * from Track where recordingId = {0} order by id", recordingId); SqlConnection myConnection = new SqlConnection( "server=(local);database=recordings;Trusted_Connection=yes"); SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection); DataSet ds = new DataSet(); myCommand.Fill(ds, "Track"); return ds; }
现在,这是唯一依赖于数据库的文件。该类是 Table Data Gateway 模式的一个很好示例。Table Data Gateway 包含用于访问单个表或视图的所有 SQL 代码:选择、插入、更新和删除。其他代码调用其方法,以便与数据库进行交互。 [Fowler03]
控制器
该重构使用代码隐藏功能来改写模型代码,以适应网页上存在的数据控件,并将控制器转发的事件映射到具体的操作方法。因为此处的模型返回 DataSet 对象,它的工作非常简单。该代码与视图代码一样,不依赖于从数据库检索数据的方式。
using System; using System.Data; using System.Collections; using System.Web.UI.WebControls; public class Solution : System.Web.UI.Page { protected System.Web.UI.WebControls.Button submit; protected System.Web.UI.WebControls.DataGrid MyDataGrid; protected System.Web.UI.WebControls.DropDownList recordingSelect; private void Page_Load(object sender, System.EventArgs e) { if(!IsPostBack) { DataSet ds = DatabaseGateway.GetRecordings(); recordingSelect.DataSource = ds; recordingSelect.DataTextField = "title"; recordingSelect.DataValueField = "id"; recordingSelect.DataBind(); } } void SubmitBtn_Click(Object sender, EventArgs e) { DataSet ds = DatabaseGateway.GetTracks( (string)recordingSelect.SelectedItem.Value); MyDataGrid.DataSource = ds; MyDataGrid.DataBind(); } #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.submit.Click += new System.EventHandler(this.SubmitBtn_Click); this.Load += new System.EventHandler(this.Page_Load); } #endregion }
测试
通过将模型与 ASP.NET 环境分隔开,可以使模型代码的测试更加容易。为了在 ASP.NET 环境内测试此代码,必须测试该过程的输出。这意味着需要读取 HTML 并确定它是否正确,这个过程比较冗长并且容易产生错误。通过将模型分离出来以便在没有 ASP.NET 的情况下运行,可以避免这个冗长的过程,并单独测试代码。以下是 NUnit (http://nunit.org) 中模型代码的示例单元测试:
using System; using NUnit.Framework; using System.Collections; using System.Data; using System.Data.SqlClient; [TestFixture] public class GatewayFixture { [Test] public void Tracks1234Query() { DataSet ds = DatabaseGateway.GetTracks("1234"); Assertion.AssertEquals(10, ds.Tables["Track"].Rows.Count); } [Test] public void Tracks2345Query() { DataSet ds = DatabaseGateway.GetTracks("2345"); Assertion.AssertEquals(3, ds.Tables["Track"].Rows.Count); } [Test] public void Recordings() { DataSet ds = DatabaseGateway.GetRecordings(); Assertion.AssertEquals(4, ds.Tables["Recording"].Rows.Count); DataTable recording = ds.Tables["Recording"]; Assertion.AssertEquals(4, recording.Rows.Count); DataRow firstRow = recording.Rows[0]; string title = (string)firstRow["title"]; Assertion.AssertEquals("Up", title.Trim()); } }
结果上下文
在 ASP.NET 中实现 MVC 具有以下优缺点:
优点
• | 降低了依赖性。利用 ASP.NET 页,程序员可以在一个网页内实现方法。正如"单个 ASP.NET 页"所显示的那样,这对于原型和小型短期应用程序非常有用。随着页面复杂性不断提高,或者对网页之间共享代码的需要不断增加,分离代码的各部分就变得更加有用。 |
• | 减少代码重复。DatabaseGateway 类中的 GetRecordings 和 GetTracks 方法现在可以由其他网页使用。这样就无需将方法复制到多个视图中。 |
• | 分离职责和问题。修改 ASP.NET 页所使用的技巧不同于编写数据库访问代码所使用的技巧。如前所述,通过分离视图和模型,各个领域的专业人员可以并行工作。 |
• | 优化的可能性。如前所述,将职责分成特定的类可以提高进行优化的可能性。在前面描述的示例中,每次发出请求时,就会从数据库加载数据。因此,在某些情况下可以对数据进行缓存,这样可以提高应用程序的总体性能。但是,如果不分离代码,缓存数据就会很难实现,或者不可能。 |
• | 可测试性。通过将模型与视图分离,您可以在 ASP.NET 环境以外测试模型。 |
缺点
增加了代码和复杂性。前面显示的示例增加了更多的文件和代码,因此,当必须对所有三个角色进行更改时,就会增加代码的维护成本。在某些情况下,与在多个文件中进行更改相比,在一个文件中进行更改更为容易。因此,您必须对分离代码的理由和额外付出的代价进行权衡。如果是很小的应用程序,可能不值得付出这样的代价。
相关模式
有关详细信息,请参阅以下相关模式:
• | Table Data Gateway。 此模式是一个充当数据库表的网关的对象。在该模式中,将由一个实例处理表中的所有角色。 [Fowler03] |
• | Bound Data Control。此模式是绑定到数据源的用户界面组件,它可以在屏幕或网页上自行显示。 |