65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET MVC - 第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.40/5 (27投票s)

Mar 23, 2008

CDDL

13分钟阅读

viewsIcon

233710

downloadIcon

9027

对 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 应用程序。

必备组件

模型-视图-控制器模式

首先,当然要讨论的是 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 的计划。

mvc1.png

此项目向导首先询问您是否要创建单元测试项目。如前所述,MVC 模式的一个好处是逻辑的分离,便于测试。尽管单元测试将在未来的文章中介绍,但我们将与主 Web 应用程序项目一起创建测试项目。

mvc2.png

正如我们下面所看到的,项目创建向导将创建 Web 应用程序的基本外壳,包括一个主页和一个名为 AboutIndex 的视图,以及 HomeController。测试项目也已创建并添加到解决方案中,其中包含 HomeController 的单元测试。

mvc3.png

音乐目录应用程序

我们将使用这个基本框架来构建一个简单的演示应用程序,该应用程序显示艺术家、与艺术家关联的专辑以及与专辑关联的歌曲。在本系列的后续文章中,我们还将创建用于编辑和插入数据的表单。本文提供的数据库脚本包含了我们将要使用到的所有数据。

应用 MVC 模式

模型

由于没有数据,该应用程序没有多大用处,我们将从创建一个 MusicCatalog 模型开始。为简化开发,我们将使用 LINQ to SQL。

添加新项 -> 数据 -> LINQ to SQL 类。我们将类命名为 Music

mvc4.png

单击“添加”按钮后,将显示一个设计器,其中包含两个面板。右面板允许您将存储过程从数据库拖放到此表面,以在生成的类中创建方法。目前我们将跳过此步骤,专注于左面板。此面板允许您将数据库表从服务器资源管理器拖到表面,并从中创建类及访问方法。对于此演示,请打开服务器资源管理器并导航到 MusicCatalog 数据库。将三个表 ArtistAlbumSong 拖到设计器表面。

简要了解 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 方法与虚拟方法的一个区别是,如果未实现方法,编译器会忽略它,如下所示。

mvc5.png

然而,当方法在 partial 类中实现时,它将被使用并编译到程序集中。

mvc6.png

这种技术的一种用途是提供简单的轻量级消息传递能力。它也适用于在代码中提供钩子,其他开发人员可以使用这些钩子来提供附加功能(例如审核),而无需驱动额外的类。

添加访问器方法

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 类。它们展示了使用 SingleWhere LINQ 扩展方法来返回匹配给定 ID 的艺术家以及给定艺术家 ID 的专辑列表。由于本文不是关于 LINQ 的,我们将继续前进,将细节留给其他文章。

控制器

本应用中的所有控制器都位于名称正确的 Controllers 文件夹下。在 ASP.NET MVC 应用程序中,用户不请求页面或资源,而是请求操作。控制器中的每个公共方法都是一个可请求的操作。

项目模板提供的默认实现包含一个控制器 HomeController,以及两个操作 IndexAbout。创建新的控制器类时,它必须带有 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 后缀。

mvc7.png

创建的类派生自 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 中的操作相匹配。接下来,右键单击刚刚创建的文件夹,选择 添加 -> 新建项

mvc8.png

如您所见,有四个与 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 页面相同的​​方法和事件,例如 LoadDataBind 事件。在我们的例子中,我们将覆盖 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 而不是通用对象,如第一个图像所示。

mvc9.png

mvc10.png

创建菜单

为了使其有所用,此应用程序需要一种方式来列出数据库中的艺术家,以便用户可以选择它们。一个简单的字母菜单应该能满足我们的需求。

mvc11.png

我们通过简单地从 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 链接的辅助方法:ActionLinkRouteLink。请记住,在 MVC 应用程序中,所有内容都通过控制器进行路由,控制器根据需要执行操作,因此才有 ActionLink 方法。我们创建的不是指向 URI 的 HTML 链接,而是告诉控制器请求哪个操作的链接。

在上面的示例中,第一个参数是链接上显示的文本。下一个参数是要请求的操作的名称。第三个参数,正如我们所看到的,是一个 RouteValueDictionary 对象,它使用对象初始化程序来为 controllerletter 属性赋值。这会生成一个与路由格式 {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 属性中为每个指定的​​值,在此情况下分别为 HomeIndex。上面注册的第一个路由仅在请求中指定了控制器时才匹配,但如果未包含操作或 ID,则将使用这些值的默认值。

未完待续...

ASP.NET MVC 是一项非常丰富且详细的技术,无法在一篇文章中全部涵盖。希望本文能说明基本概念,并可用于评估该技术的巨大潜力。本系列的后续文章将深入探讨 URL 路由、将数据发布到数据库或处理页面输入以及单元测试。

参考文献

注意:本系列文章基于早期版本,Preview 2 版本发布后许多内容已发生变化。

Partial 方法

历史

  • 第一部分发布:2008 年 3 月 23 日。
© . All rights reserved.