在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
我从事专业开发迄今为止已有 15 年,在此之前,我利用业余时间从事开发至少也有 10 年了。与我这一代的大多数人一样,我是从 8 位计算机起步,然后转用 PC 平台的。随着计算机的复杂性日益增加,我编写的应用程序涵盖了从小型游戏到个人数据管理再到控制外部硬件的各项功能。
不过,在我职业生涯的前半段,我编写的所有软件都有一个共同点:即,都是运行在用户桌面上的本地应用程序。我最早是在 90 年代初期听说万维网这件新生事物。那时我发现,通过构建 Web 应用程序,可以让我输入我的考勤卡信息而不必再费时费力从工作场所赶回办公室。
一言以蔽之,我感觉很是困惑。我当时满脑子是面向桌面的理念,很难接纳这种无状态的 Web。要添加很多让人头疼的调试、我没有 UNIX 服务器的超级用户访问权限,再加上这个奇怪的角括号,这些因素使年轻时的我止步不前,又重返桌面开发渡过了几年时光。
我远离了 Web 开发领域,虽然这领域显然很重要,但我并没有真正理解其编程模型。然后,Microsoft® .NET Framework 和 ASP.NET 发行了。尽管它与桌面应用程序编程有许多相似之处,但终于有了可以让我从事 Web 应用程序编程的框架。我可以构建窗口(页面),将控件与事件挂钩,而设计器使我不必处理那些讨厌的角括号。最妙的是,ASP.NET 会通过查看状态自动为我处理 Web 的无状态性质!我又重新找回了程序员的快乐 ... 至少在一段时间内是如此。
随着经验的增加,我的设计内容也随之丰富。我早已掌握了几种最佳实践,并将其应用到桌面应用程序编程。其中的两种就是:
这些是适用于任何技术的基本原则。分离关注点是一项可帮助您处理复杂问题的基本原则。在同一个对象内混合多种责任(如计算剩余的工时、设置数据格式并绘图)会给维护带来很大的负担。而自动测试对于获得生产质量的代码同时仍保持条理性至关重要,尤其是当您更新现有项目时更是如此。
ASP.NET Web 窗体使入门变得非常简单,但另一方面,要将我的设计理念应用到 Web 应用程序却并非易事。Web 窗体坚持以 UI 为侧重点;其基本单位为页面。首先设计 UI 并拖曳控件。只需将应用程序逻辑融入页面的事件处理程序(与为 Windows® 应用程序启用的 Visual Basic® 非常相似)就万事大吉,这一点非常吸引人。
但进一步的页面单元测试常常有很大困难。您必须先启动所有 ASP.NET,然后才能在“页面”对象的生命周期内运行该对象。尽管可以通过发送 HTTP请求到服务器或自动化浏览器来测试 Web 应用程序,但这类测试非常脆弱(更换一个控制 ID 测试就会中断)、难以设置(您必须以完全相同的方式在每位开发人员的计算机上设置该服务器)并且运行缓慢。
当我开始构建更复杂的 Web 应用程序时,Web 窗体提供的抽象概念(如控件、视图状态和页面生命周期)就开始添乱而不是帮忙了。我需要花越来越多的时间来配置数据绑定(并编写大量的事件处理程序对其进行正确配置)。我不得不想办法缩减视图状态的大小以便更快加载我的页面。Web 窗体要求每个 URL 均存在物理文件,这对于动态站点(例如 wiki)非常困难。而成功编写一个自定义的 WebControl 是一个非常复杂的过程,需要全面了解页面生命周期和 Visual Studio® 设计器。
自从在 Microsoft 工作开始,我就一直与其他人分享关于各种 .NET 难题的体验并希望可以解决一些难题。最近,作为开发人员参加有关模式与实践的 Web 客户端软件工厂项目 (codeplex.com/websf) 时,我遇到了一个这样的机会。特别是,模式与实践交付的内容之一就是自动单元测试。在 Web 客户端软件工厂中,我们建议使用 Model View Presenter (MVP) 模式构建可测试的 Web 窗体。
简而言之,MVP 并非将您的逻辑放入页面中,而是让您构建自己的页面,页面 (View) 只需调用单独的对象,即 Presenter。Presenter 对象随即执行响应视图上活动必需的任何逻辑,通常通过使用其它对象 (Model) 访问数据库、执行业务逻辑等。一旦这些步骤完成后,Presenter 会更新视图。这种方法提供了可测试性,因为表示器从 ASP.NET 管道中隔离出来;它与视图通过界面进行通信并可脱离页面独立进行测试。
MVP 的这种功能实现有点笨;您需要单独的视图界面,并且您必须在源代码文件中编写许多事件转发函数。但如果您想要在 Web 窗体应用程序中得到可测试的 UI,这差不多是最佳途径。任何改进均需要在基础平台中做出更改。
模型视图控制器模式
幸运的是,ASP.NET 团队听取了象我这样的开发人员的意见,并且已经着手开发一种新的 Web 应用程序框架,该框架与您所熟知并喜爱的 Web 窗体处于同一层级,但采用一组完全不同的设计目标:
由于此新框架基于模型视图控制器 (MVC) 模式,因此其名称为 ASP.NET MVC。MVC 模式最初在 70 年代发明,是 Smalltalk 技术的一部分。正如我将在本文中所展示的,它实际上非常适合 Web 的性质。MVC 将您的 UI 分为三种不同的对象:用于接收和处理输入的控制器;包含您域逻辑的模型;以及用于生成输出的视图。在 Web 环境中,输入为 HTTP 请求,而请求流程与图 1 类似。
Figure 1 MVC 模式请求流程
这实际上与 Web 窗体中的过程完全不同。在 Web 窗体模型中,输入进入页面(视图),然后视图负责处理输入并生成输出。而 MVC 中这些责任是分开的。
因此,您可能立即会产生以下一种想法:“嘿,这太好了。我应该如何使用它?”或“为什么我要编写这些对象,以前只需要编写一个对象?”这两个问题都问得很好,最好通过示例来进行解释。因此,我将使用 MVC Framework 编写一个小型 Web 应用程序以说明其优点。
创建控制器
要继续进行,您将需要安装 Visual Studio 2008 并获得 MVC Framework 的副本。在撰写本文时,ASP.NET 扩展的 2007 年 12 月社区技术预览 (CTP) 中已提供了这些内容 (asp.net/downloads/3.5-extensions)。您可能想要获取扩展 CTP 和 MVC 工具包,其中包括一些非常有用的帮助程序对象。一旦下载并安装 CTP 后,您将在“新建项目”对话框中获得名为“ASP.NET MVC Web 应用程序”的新项目类型。
选择“MVC Web 应用程序”项目后,会为您提供一个与常用网站或应用程序稍有不同的解决方案。该解决方案模板会创建一个带有一些新目录的 Web 应用程序(如图 2 中所示)。特别是 Controllers 目录包含各种控制器类,而 Views 目录(及其所有子目录)包含了各种视图。
Figure 2 MVC 项目结构
我将会编写一个非常简单的控制器,返回 URL 中传递的名称。右键单击 Controllers 文件夹并选择“添加项目”以显示常用的“添加项目”对话框以及一些新增加的内容,包括 MVC 控制器类和几个 MVC 视图组件。在此例中,我将添加一个非常富有想象力、名为 HelloController 的类:
using System;
using System.Web; using System.Web.Mvc; namespace HelloFromMVC.Controllers { public class HelloController : Controller { [ControllerAction] public void Index() { } } } 控制器类比页面简单得多。实际上,唯一真正必需做的就是从 System.Web.Mvc.Controller 中衍生并将 [ControllerAction] 属性置于您的操作方法中。操作是调用以响应特定 URL 请求的一种方法。操作负责执行所需的一切处理,然后呈现一个视图。我将通过编写一个将名称传递到视图的简单操作着手,如下所示:
[ControllerAction]
public void HiThere(string id) { ViewData["Name"] = id; RenderView("HiThere"); } 操作方法会通过 ID 参数从 URL 接收该名称(稍后会介绍方法),将其存储在 ViewData 集合中,然后呈现名为 HiThere 的视图。 在讨论如何调用此方法,或该视图的显示内容之前,我希望说一说可测试性。还记得我之前关于测试 Web 窗体页面类有多难的评论吗?控制器的测试简单得多。实际上,控制器可以直接实例化,而调用操作方法无需任何附加的基础结构。您不需要 HTTP 上下文,也不需要服务器,只要测试工具即可。作为示例,我在图 3 中为此类包括了 Visual Studio Team System (VSTS) 测试单元。
Figure 3 Controller Unit Test
namespace HelloFromMVC.Tests
{ [TestClass] public class HelloControllerFixture { [TestMethod] public void HiThereShouldRenderCorrectView() { TestableHelloController controller = new TestableHelloController(); controller.HiThere("Chris"); Assert.AreEqual("Chris", controller.Name); Assert.AreEqual("HiThere", controller.ViewName); } } class TestableHelloController : HelloController { public string Name; public string ViewName; protected override void RenderView( string viewName, string master, object data) { this.ViewName = viewName; this.Name = (string)ViewData["Name"]; } } } 下面将进行几项操作。实际的测试相当简单:实例化该控制器,使用预期的数据调用该方法,然后检查呈现的视图是否正确。我通过创建测试专用的子类覆盖 RenderView 方法进行检查。这可以缩短实际创建 HTML 的时间。我只关心是否将正确的数据发送到视图以及是否呈现了正确的视图。我不关心此测试视图本身的底层详细信息。 创建视图
当然,最终我必须生成一些 HTML,因此,让我们创建该 HiThere 视图。要进行此操作,首先,我将在解决方案中的 Views 文件夹下创建名为 Hello 的新文件夹。默认情况下,控制器将在 Views\<控制器前缀> 文件夹(控制器前缀为控制器类的名称去掉 "Controller" 字样)中查找视图。因此,对于 HelloController 呈现的视图,它会在 Views\Hello 中查找。解决方案的查找结果如图 4 所示。
Figure 4 将视图添加到项目中
视图的 HTML 如下所示:
<html>
<head runat="server"> <title>Hi There!</title> </head> <body> <div> <h1>Hello, <%= ViewData["Name"] %></h1> </div> </body> </html> 应注意以下几件事。没有 runat="server" 标记。没有 form 标记。没有控件声明。实际上,这看起来更象传统的 ASP 而不是 ASP.NET。请注意,MVC 视图仅负责生成输出,因此其不需要任何 Web 窗体页面所需的事件处理或复杂控件。 MVC Framework 借用了 .aspx 文件格式作为一种有用的文本模板语言。如果需要,甚至可以使用源代码,但默认情况下,源代码文件如下所示:
using System;
using System.Web; using System.Web.Mvc; namespace HelloFromMVC.Views.Hello { public partial class HiThere : ViewPage { } } 没有页面初始化或加载方法,没有事件处理程序,除了基类声明以外没有任何内容,基类声明为 ViewPage 而不是 Page。这就是 MVC 视图所需的一切。运行该应用程序,导航至 http://localhost:<端口>/Hello/HiThere/Chris,您将看到如图 5 所示的内容。 Figure 5 成功的 MVC 视图
如果您看到的并非如图 5 所示,而是难以理解的意外情况,请不要惊慌。如果您将 HiThere.aspx 文件设置为 Visual Studio 中的活动文档,则当按 F5 后,Visual Studio 将尝试直接访问 .aspx 文件。由于 MVC 视图要求控制器在显示前运行,因此尝试直接导航至该页面将不起作用。只需将该 URL 编辑为与图 5 中所示的内容相匹配,即可正常工作。
MVC Framework 如何知道调用我的操作方法?该 URL 甚至没有文件扩展名。答案是 URL 路由。如果您仔细查看 global.asax.cs 文件,则会看到如图 6 所示的代码段。全局 RouteTable 会存储 Route 对象的集合。每个 Route 说明一个 URL 窗体以及对其进行何种操作。默认情况下,会向该表中添加两个路由。第一个是该方法的内容。它说明每个 URL 在服务器名后均由三部分组成,第一部分应为控制器名,第二部分为操作名称,而第三部分为 ID 参数。
public class Global : System.Web.HttpApplication
{ protected void Application_Start(object sender, EventArgs e) { // Change Url= to Url="[controller].mvc/[action]/[id]" // to enable automatic support on IIS6 RouteTable.Routes.Add(new Route { Url = "[controller]/[action]/[id]", Defaults = new { action = "Index", id = (string)null }, RouteHandler = typeof(MvcRouteHandler) }); RouteTable.Routes.Add(new Route { Url = "Default.aspx", Defaults = new { controller = "Home", action = "Index", id = (string)null }, RouteHandler = typeof(MvcRouteHandler) }); } }
Url = "[controller]/[action]/[id]"
此默认路由是能让我的 HiThere 方法得以调用的路由。请记住此 URL:http://localhost/Hello/HiThere/Chris?此路由将 Hello 与控制器、HiThere 与操作以及 Chris 与 ID 一一对应。MVC Framework 随即创建 HelloController 实例,调用 HiThere 方法,然后将 Chris 作为 ID 参数的值传递。 此默认路由为您提供了许多功能,但您也可以添加自己的路由。例如,我想要一个真正友好的站点,好友们只需输入他们的姓名即可获得个性化的问候。如果我在路由表的顶部添加以下路由
RouteTable.Routes.Add(new Route
{ Url = "[id]", Defaults = new { controller = "Hello", action = "HiThere" }, RouteHandler = typeof(MvcRouteHandler) }); 随后,我只需访问 ,我的操作仍处于调用状态,而我将会看到熟悉的友好问候。 系统如何知道调用哪个控制器和操作?答案是 Defaults 参数。它利用新的 C# 3.0 匿名类型语法来创建一个伪词典。Route 上的 Defaults 对象可包含任意附加的信息,对于 MVC,它还可以包含一些众所周知的条目:即控制器和操作。如果 URL 中没有指定控制器或操作,则其将使用 Defaults 中的名称。这就是为什么即使我在 URL 中忽略它们,但仍可以将我的请求映射到正确的控制器和操作。
还有一件事需要注意:还记得我说过“添加到表格的顶部”吗?如果您将其置于底部,将会出现错误。路由根据先到先得的原则进行工作。当处理 URL 时,路由系统会自上至下浏览表格,并且使用第一个匹配的路由。在本例中,默认路由 "[controller]/[action]/[id]" 匹配,因为它们是操作和 ID 的默认值。这样,它会继续查找 ChrisController,但我没有控制器,因此会出现错误。
稍大的示例
现在,我已经说明了 MVC Framework 的基础知识,将为您展示一个更大的示例,实现比仅显示字符串更多的功能。wiki 是一种可以在浏览器中进行编辑的网站。可以轻松地添加或编辑页面。我使用 MVC Framework 编写了一个小型的示例 wiki。“编辑此页面”屏幕如图 7 所示。
Figure 7 编辑主页
您可以检查本文的代码下载以查看如何实现底层 wiki 逻辑。现在我想重点说明 MVC Framework 如何使 Web 上的 wiki 获取变得简单。让我们先设计 URL 结构。我想要以下各项:
让我们从 wiki 页面的基本显示开始。我为它创建了一个名为 WikiPageController 的新类。接下来,我会添加一个名为 ShowPage 的操作。启动的 WikiPageController 如图 8 所示。ShowPage 方法相当简单。WikiSpace 和 WikiPage 类分别表示一组 wiki 页面和特定的页面(及其修订)。此操作只需加载模型并调用 RenderView。但此处的 "new WikiPageViewData" 行是什么意思?
public class WikiPageController : Controller
{ ISpaceRepository repository; public ISpaceRepository Repository { get { if (repository == null) { repository = new FileBasedSpaceRepository( Request.MapPath("~/WikiPages")); } return repository; } set { repository = value; } } [ControllerAction] public void ShowPage(string pageName, int? version) { WikiSpace space = new WikiSpace(Repository); WikiPage page = space.GetPage(pageName); RenderView("showpage", new WikiPageViewData { Name = pageName, Page = page, Version = version ?? 0 }); } } 我前面的示例说明了一种将数据从控制器传递到视图的方法:即 ViewData 词典。词典非常方便,但也很危险。它们几乎包含一切内容,您不能获取内容的任何 IntelliSense®,并且由于 ViewData 词典属于 Dictionary<string, object> 类型,它将消耗内容,您必须计算所有一切。 当您了解在视图中将需要什么数据后,就可以传递强类型化的 ViewData 对象。在我的示例中,我创建了一个简单的对象 (WikiPageViewData),如图 9 中所示。此对象将 wiki 页面信息带到视图,同时还携带了一些实用工具方法,执行获取 wiki 标记的 HTML 版本这类任务。
public class WikiPageViewData {
public string Name { get; set; } public WikiPage Page { get; set; } public int Version { get; set; } public WikiPageViewData() { Version = 0; } public string NewVersionUrl { get { return string.Format("/CreateNewVersion/{0}", Name); } } public string Body { get { return Page.Versions[Version].Body; } } public string HtmlBody { get { return Page.Versions[Version].BodyAsHtml(); } } public string Creator { get { return Page.Versions[Version].Creator; } } public string Tags { get { return string.Join(",", Page.Versions[Version].Tags); } } } 现在,我已经定义了视图数据,那么,我如何使用它呢?在 ShowPage.aspx.cs 中,您将看到以下内容:
namespace MiniWiki.Views.WikiPage {
public partial class ShowPage : ViewPage<WikiPageViewData> { } } 请注意,我将基类类型定义为 ViewPage<WikiPageViewData>。这意味着页面的 ViewData 属性为 WikiPageViewData 类型,而不是象以前示例中的“Dictionary”。 .aspx 文件中的实际标记非常简单:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
AutoEventWireup="true" CodeBehind="ShowPage.aspx.cs" Inherits="MiniWiki.Views.WikiPage.ShowPage" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContentPlaceHolder" runat="server"> <h1><%= ViewData.Name %></h1> <div id="content" class="wikiContent"> <%= ViewData.HtmlBody %> </div> </asp:Content> 请注意,当引用 ViewData 时,我没有使用索引操作符 []。由于我现在有强类型化的 ViewData,我可以直接访问该属性。不需要进行任何计算,而 Visual Studio 会提供 IntelliSense。
目光敏锐的读者将会注意到此文件中的 <asp:Content> 标记。没错,“母版页”确实可以与 MVC 视图配合使用。并且“母版页”还可以成为视图。让我们看看“母版页”的源代码:
namespace MiniWiki.Views.Layouts
{ public partial class Site : System.Web.Mvc.ViewMasterPage<WikiPageViewData> { } } 相关标记如图 10 中所示。现在,“母版页”将获得与视图完全相同的 ViewData 对象。我已经将“母版页”的基类声明为 ViewMasterPage<WikiPageViewData>,因此,我拥有了正确类型的 ViewData。我会在那里设置各种 DIV 标记以对页面进行布局,填写版本列表,然后以常用内容占位符收尾。 <%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="MiniWiki.Views.Layouts.Site" %> <%@ Import Namespace="MiniWiki.Controllers" %> <%@ Import Namespace="MiniWiki.DomainModel" %> <%@ Import Namespace="System.Web.Mvc" %> <html > <head runat="server"> <title><%= ViewData.Name %></title> <link href="http://http://www.cnblogs.com/Content/Site.css" rel="stylesheet" type="text/css" /> </head> <body> <div id="inner"> <div id="top"> <div id="header"> <h1><%= ViewData.Name %></h1> </div> <div id="menu"> <ul> <li><a href="http://Home">Home</a></li> <li> <%= Html.ActionLink("Edit this page", new { controller = "WikiPage", action = "EditPage", pageName = ViewData.Name })%> |
请发表评论