ASP.NET MVC - 第一部分
对 ASP.NET Extensions 3.5 中的 ASP.NET MVC 应用程序的探讨。
引言
创建 ASP.NET Web 应用程序有多种方式;然而,它们都面临一个共同的难题,即难以将业务逻辑与表示层分离。分离这些层可以实现更模块化的开发、代码重用和更轻松的测试。一种支持这些特性的模式是模型-视图-控制器,即 MVC。ASP.NET 团队已经认识到这种模式的好处,并正致力于将其整合到 ASP.NET 中。本文将重点介绍如何从 ASP.NET 3.5 Extensions 的 Preview 2 版本构建一个 ASP.NET MVC Web 应用程序。
必备组件
- ASP.NET 3.5 Extensions, Preview 2, http://www.asp.net/downloads/3.5-extensions/。
- MusicCatalog 数据库,包含在下载中。
模型-视图-控制器模式
首先,当然要讨论的是 MVC 模式是什么。MVC 是一种将应用程序划分为不同职责区域的模式:模型、视图和控制器。
- 模型 (Model):模型负责维护应用程序的状态,通常通过数据库来实现。
- 视图 (View):这些组件仅用于显示数据;除了格式化数据以便显示之外,它们不提供任何功能。
- 控制器 (Controller):控制器是 MVC 应用程序中的核心通信机制。它们将视图中的操作传达给模型,并将模型的结果返回给视图。
MVC 模式的一个主要观点是模型和视图之间没有直接通信。这使得模型和控制器代码可以在不同类型的应用程序中得到重用。Web 应用程序的逻辑可以通过更改视图组件轻松地应用于 Windows 应用程序。控制器组件还可以作为 Web 服务公开,用于 SOA,而不会影响视图。当然,在不影响控制器的情况下,也可以更改模型;例如,如果数据库本身或架构发生更改。分离功能的另一个好处是便于测试。由于视图只关心显示,所有需要测试的逻辑都在控制器中。可以轻松地集成单元测试来测试这些功能。
ASP.NET Extensions
ASP.NET 3.5 Extensions Preview 是一组对 ASP.NET 和 ADO.NET 的增强功能,预计将包含在未来的版本中,但现在可以作为预览版使用。这些增强功能包括 ASP.NET Silverlight 控件、ADO.NET 实体框架、ASP.NET Dynamic Data 和 ASP.NET MVC。后者当然是我们这里关注的重点。有关其他内容的更多信息,请参阅本文末尾的参考资料。
ASP.NET MVC 项目的所有功能都包含在三个程序集中
- System.Web.Mvc
- System.Web.Routing
- System.Web.Abstractions
创建 ASP.NET MVC 项目
安装扩展后,您应该在 文件 -> 新建项目 -> Visual C# -> Web 下看到一种新的项目类型。此项目仅在 C# 中可用。目前没有支持 VB 的计划。
此项目向导首先询问您是否要创建单元测试项目。如前所述,MVC 模式的一个好处是逻辑的分离,便于测试。尽管单元测试将在未来的文章中介绍,但我们将与主 Web 应用程序项目一起创建测试项目。
正如我们下面所看到的,项目创建向导将创建 Web 应用程序的基本外壳,包括一个主页和一个名为 About
和 Index
的视图,以及 HomeController
。测试项目也已创建并添加到解决方案中,其中包含 HomeController
的单元测试。
音乐目录应用程序
我们将使用这个基本框架来构建一个简单的演示应用程序,该应用程序显示艺术家、与艺术家关联的专辑以及与专辑关联的歌曲。在本系列的后续文章中,我们还将创建用于编辑和插入数据的表单。本文提供的数据库脚本包含了我们将要使用到的所有数据。
应用 MVC 模式
模型
由于没有数据,该应用程序没有多大用处,我们将从创建一个 MusicCatalog 模型开始。为简化开发,我们将使用 LINQ to SQL。
添加新项 -> 数据 -> LINQ to SQL 类。我们将类命名为 Music
。
单击“添加”按钮后,将显示一个设计器,其中包含两个面板。右面板允许您将存储过程从数据库拖放到此表面,以在生成的类中创建方法。目前我们将跳过此步骤,专注于左面板。此面板允许您将数据库表从服务器资源管理器拖到表面,并从中创建类及访问方法。对于此演示,请打开服务器资源管理器并导航到 MusicCatalog 数据库。将三个表 Artist、Album 和 Song 拖到设计器表面。
简要了解 LINQToSQL 类
可以看到,在将表拖到设计表面后,数据库中建立的外键关系也体现在了设计器和生成的类中。如果我们打开 Music.designer.cs 文件,可以看到已创建的类。
[System.Data.Linq.Mapping.DatabaseAttribute(Name="MusicCatalog")]
public partial class MusicDataContext : System.Data.Linq.DataContext
在此注意,DataContext
已自动附加到我们提供的名称 Music
。这是 LINQ to SQL 类命名约定。我们还可以看到 LINQ to SQL 使用 DatabaseAttribute
来标识此类表示的数据库。
部分方法
生成的类还包含已声明为 Partial 的方法的定义。
#region Extensibility Method Definitions
partial void OnCreated();
partial void InsertArtist(Artist instance);
partial void UpdateArtist(Artist instance);
partial void DeleteArtist(Artist instance);
partial void InsertAlbum(Album instance);
partial void UpdateAlbum(Album instance);
partial void DeleteAlbum(Album instance);
partial void InsertSong(Song instance);
partial void UpdateSong(Song instance);
partial void DeleteSong(Song instance);
#endregion
这是 .NET 3.0 中添加的一项新功能,类似于 .NET 2.0 中引入的 partial 类。Partial 类允许将代码分离到多个类中,并编译成一个单元。例如,ASP.NET 代码隐藏文件就是这样分离的。Partial 方法允许设计器实现、定义和存根化可能由使用该类的开发人员实现的方法。Partial 方法与虚拟方法的一个区别是,如果未实现方法,编译器会忽略它,如下所示。
然而,当方法在 partial 类中实现时,它将被使用并编译到程序集中。
这种技术的一种用途是提供简单的轻量级消息传递能力。它也适用于在代码中提供钩子,其他开发人员可以使用这些钩子来提供附加功能(例如审核),而无需驱动额外的类。
添加访问器方法
DataContext
类本身并没有多大用处,因为它并没有提供访问所需数据的方法,例如获取与给定艺术家关联的专辑列表。所以我们现在添加一些方法。这些可以通过数据库中的存储过程提供,并拖到设计器表面自动生成。然而,通过自己实现它们,我们可以了解一些 LINQ 方法和技术。
public Artist GetArtistById(int id)
{
return Artists.Single(a => a.id == id);
}
public List<Album> GetAlbumsForArtist(int id)
{
return Albums.Where(a => a.artist_id == id).ToList();
}
这两个方法只是示例;有关完整实现,请参阅 MusicDataContext
类。它们展示了使用 Single
和 Where
LINQ 扩展方法来返回匹配给定 ID 的艺术家以及给定艺术家 ID 的专辑列表。由于本文不是关于 LINQ 的,我们将继续前进,将细节留给其他文章。
控制器
本应用中的所有控制器都位于名称正确的 Controllers 文件夹下。在 ASP.NET MVC 应用程序中,用户不请求页面或资源,而是请求操作。控制器中的每个公共方法都是一个可请求的操作。
项目模板提供的默认实现包含一个控制器 HomeController
,以及两个操作 Index
和 About
。创建新的控制器类时,它必须带有 Controller 后缀。MVC 使用反射根据名称(如我们将看到的)定位控制器。此要求的原因尚不清楚。
public class HomeController : Controller
{
public void Index()
{
RenderView("Index");
}
public void About()
{
RenderView("About");
}
}
我们将在稍后介绍 RenderView
方法,所以现在暂时忽略它。
MusicController
要为 MusicCatalog 应用程序创建控制器,请在 MVC 项目中右键单击 Controllers 文件夹,然后选择 添加 -> 新建项 或类,并在“添加新项”对话框中选择 MVC Controller Class。注意描述中提到该类必须使用前面提到的 Controller 后缀。
创建的类派生自 System.Web.Mvc.Controller
,并已包含一个名为 Index
的方法。这是任何控制器的默认操作。
public class MusicController : Controller
{
public void Index()
{
// Add action logic here
}
}
我们将添加先前创建的模型 MusicDataContext
的实例,以及用于检索和显示艺术家、专辑和歌曲的必要操作方法。
public class MusicController : Controller
{
private MusicDataContext m_Music = new MusicDataContext();
public void Index()
{
// Add action logic here
}
public void Artists(string letter)
{
RenderView("Artists", DataContext.GetArtists(letter));
}
public void Albums(int id)
{
RenderView("Albums", DataContext.GetAlbumsForArtist(id));
}
public void Songs(int id)
{
RenderView("Songs", DataContext.GetSongsForAlbum(id));
}
#region Properties
private MusicDataContext DataContext
{
get { return m_Music; }
}
#endregion
}
渲染视图
控制器通过调用 RenderView
来启动向用户显示页面的过程,该方法有几个重载。在我们使用的重载版本中,第一个参数是要渲染的视图页面的名称。请注意,我们不需要指定完整路径,只需指定名称即可。MVC 框架将在 views 文件夹下与控制器名称匹配的文件夹中查找此页面。RenderView
方法的第二个参数是一个将传递给视图的对象。此对象被分配给 ViewData
成员,该成员是 IDictionary<string, object>
的一个实现。您可以直接分配 ViewData
并使用 RenderView
的另一个重载,如下所示。
public void Artists(string letter)
{
ViewData["Artists"] = DataContext.GetArtists(letter);
RenderView("Artists");
}
当然,由于 ViewData
是一个 IDictionary<string, object>
,我们可以分配多个值传递给 View
。
public void Index()
{
ViewData["TheAnswer"] = 42;
ViewData["Data"] = DataContext.GetArtists("A");
RenderView("Artists");
//RenderView("Artists", DataContext.GetArtists("A"));
}
如果使用上面注释掉的第二个 RenderView
方法,它将用从 DataContext.GetArtists("A")
返回的 List<Artists>
覆盖 ViewData
的任何先前赋值。
View
现在我们有了模型和控制器,是时候转向视图了。我们将首先添加 Artist 视图来显示数据库中的所有音乐艺术家。
首先,在 Views 下添加一个名为 Music 的新文件夹。此文件夹的名称与它关联的控制器名称匹配。请注意 Home 文件夹及其视图与项目模板创建的 HomeController
中的操作相匹配。接下来,右键单击刚刚创建的文件夹,选择 添加 -> 新建项。
如您所见,有四个与 MVC 项目相关的项;Master Page 和 User Control 应该很明显。MVC View Page 和 MVC View Content Page 之间的区别在于后者用于 MasterPage,而前者是独立页面。我们将选择 MVC View Content Page 并将其命名为 Artists.aspx。当被要求使用 MasterPage 时,请在 Shared 文件夹下选择 Site.Master
。
MVC View 页面的一个需要注意的地方是,它们不是 ASP.NET 页面。虽然它们为了方便起见带有 aspx 扩展名,但它们没有表单,如下面的 MVC View Page 示例所示。与传统 ASP.NET 应用程序中的表单不同,所有交互都通过控制器处理。尽管它们不是 ASP.NET 表单,但仍然可以使用服务器控件,正如我们稍后将看到的。
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title></title>
</head>
<body>
<div>
</div>
</body>
</html>
ViewPage 后置代码
打开 Music.aspx.cs,您会看到该类是一个 Partial 类,就像 ASP.NET 页面一样,但它派生自 ViewPage
而不是 Page
。
public partial class Artists : ViewPage
在此类中,您可以使用与普通 ASP.NET 页面相同的方法和事件,例如 Load
或 DataBind
事件。在我们的例子中,我们将覆盖 Load
事件。
protected override void OnLoad(EventArgs e)
{
AddMenu();
ArtistList.DataSource = (List<Artist>)ViewData["Artists"];
ArtistList.DataBind();
}
我们首先在页面顶部添加一个简单的字母菜单用于导航;稍后将详细介绍。接下来,我们将从控制器传递过来的 ViewData
绑定到 ListView
控件。请记住,ViewData
也是控制器类的一个成员。在类之间保持相同的名称是一种便利。由于 ViewData
是一个 IDictionary<string, object>
,我们需要将其从 object 强制转换为通用的 List<Artists>
才能使用。这种松散耦合很好,但如果我们还需要更强类型的数据怎么办?ViewPage
还有一个泛型构造函数,允许您指定 ViewData
的类型,从而无需进行任何类型转换以及潜在的装箱/拆箱操作所带来的性能损失。我们还可以获得 Intellisense 支持和编译时错误检查。
public partial class Artists : ViewPage< List<Artist> >
现在 ViewData
可以用作 List
而不是通用对象,如第一个图像所示。
创建菜单
为了使其有所用,此应用程序需要一种方式来列出数据库中的艺术家,以便用户可以选择它们。一个简单的字母菜单应该能满足我们的需求。
我们通过简单地从 A 到 Z 迭代来构建菜单,为每个字母创建一个 HTML 链接并将其添加到容器中。这里没什么特别的,除了创建链接。
private void AddMenu()
{
// Build alphabetic menu
for(char c = 'A'; c <= 'Z'; c++)
{
string link = Html.ActionLink(c.ToString(), "Artists",
new RouteValueDictionary( new { controller = "Music",
letter = c.ToString() }) );
Alphabet.Controls.Add(new LiteralControl(link));
// Add seperator
if(c != 'Z')
Alphabet.Controls.Add(new LiteralControl(" | "));
}
}
ViewPage
有一个 HTML
属性,该属性公开了一个 HtmlHelper
类。顾名思义,此类提供了用于创建 HTML 链接的辅助方法:ActionLink
和 RouteLink
。请记住,在 MVC 应用程序中,所有内容都通过控制器进行路由,控制器根据需要执行操作,因此才有 ActionLink
方法。我们创建的不是指向 URI 的 HTML 链接,而是告诉控制器请求哪个操作的链接。
在上面的示例中,第一个参数是链接上显示的文本。下一个参数是要请求的操作的名称。第三个参数,正如我们所看到的,是一个 RouteValueDictionary
对象,它使用对象初始化程序来为 controller
和 letter
属性赋值。这会生成一个与路由格式 {controller}/{action}/{letter}
匹配的 HTML anchor 标签,其外观如下:<a href="/Music/Artists/A">A</a>。
URL 路由
现在我们已经奠定了应用程序的基本架构,并且希望对主要组件有所了解,问题是,应用程序如何知道何时显示特定页面?答案是 URL 路由。
路由是通往控制器中操作的路径,或者在传统 Web 应用程序中是 URL。ASP.NET MVC 应用程序为此目的向应用程序范围添加了一个 RouteTable
对象。RouteTable
定义在 System.Web.Routing
命名空间中,包含一个名为 Routes
的属性,该属性是一个 RouteCollection
。
ASP.NET MVC 项目模板将此实现添加到 Gloabal.asax.cs 文件中。
public class GlobalApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.Add(new Route("{controller}/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { action = "Index", id = "" }),
});
routes.Add(new Route("Default.aspx", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home",
action = "Index", id = "" }),
});
}
protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}
}
URL 路由使用模式匹配将请求定向到相应的控制器和操作,并且路由按照它们注册的顺序进行评估。就像异常处理一样,您应该从最具体到最一般的顺序注册路由。
添加到集合中的第一个路由是用于使用格式 {controller}/{action}/{id}
的请求,例如 Home/About/1
。第二个路由是一个默认的捕获所有路由。通常在 Web 应用程序中,没有资源的请求(只有应用程序名称),例如 http://www.mymvcapp,将被路由到默认页面,对于 ASP.NET 应用程序通常是 default.aspx。
在这两个路由中,您都可以看到也正在为 Defaults
属性赋值。此属性的类型为 RouteValueDictionary
,它是一个 IDictionary<string, object>
,顾名思义,它存储了一系列用于路由的默认值。在第二个路由中,由于未为控制器或操作提供值,因此将使用 Defaults
属性中为每个指定的值,在此情况下分别为 Home
和 Index
。上面注册的第一个路由仅在请求中指定了控制器时才匹配,但如果未包含操作或 ID,则将使用这些值的默认值。
未完待续...
ASP.NET MVC 是一项非常丰富且详细的技术,无法在一篇文章中全部涵盖。希望本文能说明基本概念,并可用于评估该技术的巨大潜力。本系列的后续文章将深入探讨 URL 路由、将数据发布到数据库或处理页面输入以及单元测试。
参考文献
- http://www.asp.net/downloads/3.5-extensions/
- http://weblogs.asp.net/scottgu/archive/2007/10/14/asp-net-mvc-framework.aspx
注意:本系列文章基于早期版本,Preview 2 版本发布后许多内容已发生变化。
Partial 方法
- http://blogs.msdn.com/wesdyer/archive/2007/05/23/in-case-you-haven-t-heard.aspx
- http://community.bartdesmet.net/blogs/bart/archive/2007/07/28/c-3-0-partial-methods-what-why-and-how.aspx
历史
- 第一部分发布:2008 年 3 月 23 日。