【MVC 4】5.SportsSore —— 一个真实的应用程序
作者:[美]Adam Freeman 来源:《精通ASP.NET MVC 4》
前面建立的都是简单的MVC程序,现在到了吧所有事情综合在一起,以建立一个简单但真实的电子商务应用程序的时候了。
在此打算建立的应用程序 — SportsStore (体育用品商店),将遵循随处可见的在线商店所采取的经典方式。将创建一个客户可以通过分类和页面进行浏览的在线产品分类,一个客户可以添加和删除商品的购物车,和一个客户能够输入其右击地址细节的结算页面。另外,还将创建一个包含创建、读取、更新和删除功能的管理区,以便对产品分类进行管理并对该区域进行保护,以使只有登录的管理员才能进行修改。
此书打算建立的这个应用程序不只是一个肤浅的演示,而是要创建一个坚固且真实的、符合当前最实用要求的应用程序。由于要建立必要的底层结构,一开始的进度会有点慢。的确,若使用 WebForm,则可以更快地建立最初的功能,只要拖放一些与数据库直接绑定的空间即可。但在 MVC 应用程序中所付出的这些初期工作,会带来可维护、可扩展以及结构良好的代码,且这些代码对单元测试具有卓越支持。一旦恰当地建好了这种基本的底层架构,后面的事情就会快起来了。
1.开始
1.1 创建 Visual Studio 解决方案和项目
本文打算创建一个含有三个项目的Visual Studio 解决方案,一个项目包含域模型,一个是MVC 应用程序,第三个则包含单元测试。首先用"空白解决方案"模板创建一个名为"SportsStore"的新的Visual Studio 解决方案。
Visual Studio 解决方案是一个含有一个或多个项目的容器。示例应用程序需要三个项目,如下图所示:
1.2 添加引用
参考前面的博文 【MVC 4】3.MVC 基本工具(创建示例项目、使用 Ninject) 和 【MVC 4】4.MVC 基本工具(Visual Studio 的单元测试、使用Moq) 对库和项目做好正确的引用。所需的项目依赖性如下图所示:
1.3 设置DI容器
之前的文章 【MVC 4】3.MVC 基本工具(创建示例项目、使用 Ninject) 展示过如何使用 Ninject 创建一个自定义依赖性解析器,以便 MVC 框架用它创建整个应用程序实例化对象。这里打算采用不同的方法,即创建一个自定义的控制器工厂。用户可以在其中添加自定义代码,以改变MVC框架的(默认)行为,或者像这里所做的一样,将DI 限制到应用程序的一部分。常用的模式是用依赖性解析器来处理 DI,而用自定义控制器工厂来改变查找控制器类的方式,但在此例中,只打算使用控制器工厂。
在 SportsStore.WebUI 项目中创建一个名称"Infreastructure"的文件夹,然后创建一个名为"NinjectContrillerFactory (Ninject 控制器工厂)"的类。
using Ninject; using System; using System.Web.Mvc; using System.Web.Routing; namespace SportsStore.WebUI.Infrastructure { public class NinjectControllerFactory : DefaultControllerFactory { private IKernel ninjectKernel; public NinjectControllerFactory() { ninjectKernel = new StandardKernel(); AddBindings(); } protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType); } private void AddBindings() { //put bindings here } } }
虽然没有添加任何绑定,但在需要时,可以使用AddBindings 方法。必须告诉 MVC 希望使用此 NinjectControllerFactory 类来创建控制器对象,其办法是在 SportsStore.WebUI 项目中 Global.asax.cs 文件的Application_Start 方法中添加一些代码,如下粗体部分所示:
using SportsStore.WebUI.Infrastructure;using System.Web.Http; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; namespace SportsStore.WebUI { // 注意: 有关启用 IIS6 或 IIS7 经典模式的说明, // 请访问 http://go.microsoft.com/?LinkId=9394801 public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory()); } } }
1.4 运行程序
运行程序,会看到一个错误页面,这是因为所请求的 URL 是与 Ninject 尚未进行绑定的控制器相关联的:
如果进行到这一步,说明 Visual Studio 2012 和ASP.NET MVC 开发环境的准备工作进行的十分顺利。
2.从域模型开始
MVC 应用程序中有太多的事情都是围绕域模型而展开的,因此,域模型是开始工作的最佳位置。
由于这是一个电子商务应用程序,因此需要的最明显的域实体是产品(Product)。在SportsSore.Domain 项目中创建一个名为 "Entities" 的新文件夹,然后在其中创建一个名为“Product”的类。
namespace SportsStore.Domain.Entities { public class Product { public int ProductID { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } public string Category { get; set; } } }
上述清单遵循了在一起独立的 Visual Studio 项目中定义域模型的约定,即类必须标记为 public,虽然不一定要遵循这一约定,但这么做有助于保持模型与控制器分离。
2.1 创建一个抽象的存储库
现在需要某种方式来获取数据库中的Product 实体。正如前面博文所解释的,人们希望持久化逻辑与域模型实体是分离的——此事通过使用存储库模式来实现。此刻,不必担心会如何实现持久化,不过,将从定义它的接口来开始这一过程。
在 SportsStore.Domain 项目中创建一个名为 Abstract 的顶层新文件夹,并创建一个名为 IProductRepository 的新接口,代码如下:
using SportsStore.Domain.Entities; using System.Linq; namespace SportsStore.Domain.Abstract { public interface IProductRepository { IQueryable<Product> Products { get; } } }
该接口使用了 IQueryable<T> 接口,以便能够获得一系列 Product 对象,而不必说明数据如何存储、存储在哪儿,以及如何接收数据。使用这一 IProductRepository 接口的类,可以获取 Product 对象而不必知道它们来自哪儿或如何递交它们,这是存储库模式的本质。在添加特性的整个开发过程中,将重新审视这一接口。
2.2 创建模仿存储库
现在,已经定义了一个抽象接口,可以实现持久化机制,并将其挂接到一个数据库。本文打算在后面部分再做这件事情。为了能够开始编写应用程序的其他部分,本文打算创建一个 IProductRepository 接口的模仿实现。本文打算在 SportsStore.WebUI 项目的 NinjectControllerFactory 类的 AddBindings 方法中做这件事,代码如下:
using Moq; using Ninject; using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System; using System.Linq; using System.Collections.Generic; using System.Web.Mvc; using System.Web.Routing; namespace SportsStore.WebUI.Infrastructure { public class NinjectControllerFactory : DefaultControllerFactory { private IKernel ninjectKernel; public NinjectControllerFactory() { ninjectKernel = new StandardKernel(); AddBindings(); } protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType); } private void AddBindings() { //put bindings here Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m=>m.Products).Returns(new List<Product>{ new Product{Name="Football",Price=25}, new Product{Name="Surf board",Price=179}, new Product{Name="Running shoes",Price=95} }.AsQueryable()); ninjectKernel.Bind<IProductRepository>().ToConstant(mock.Object); } } }
此处必须对该问卷添加一些命名空间,但用来创建模仿存储库实现的过程使用的是 【MVC 4】4.MVC 基本工具(Visual Studio 的单元测试、使用Moq) 所介绍的同样的 Moq 技术。AsQueryable方法是一个 LINQ 扩展方法,它将 IEnumerable<T> 转换成 IQueryable<T>,此处需要它来匹配接口签名。
人们希望,Ninject 无论何时接收到一个 IProductRepository 接口实现的请求,都返回同样的模仿对象,这便是使用 ToConstant 方法的原因
... ninjectKernel.Bind<IProductRepository>().ToConstant(mock.Object); ...
Ninject 会一直以该模仿对象来满足对 IProductRepository 接口的请求,而不是每次都创建一个新的实现对象实例。
3.显示产品列表
本小结将创建一个控制器和一个动作方法,它能够显示存储库中的产品细节。此刻,将只只针对模仿存储库中的数据。
3.1 添加控制器
新建控制器"ProductController",模板为"空 MVC 控制器",修改代码如下
using SportsStore.Domain.Abstract; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace SportsStore.WebUI.Controllers { public class ProductController : Controller { private IProductRepository repository; public ProductController(IProductRepository productRepository) { this.repository = productRepository; } public ViewResult List() { return View(repository.Products); } } }
像这样调用 View 方法(未指定视图名称),是告诉框架为该动作方法渲染一个默认视图。通过将 Product 对象的列表传递给这个 View 方法,这是在给框架提供数据,以便用这些数据填充强类型视图中的 Model 对象。
3.2 添加视图
现在需要为 List 动作方法添加默认视图。添加对应的视图文件 List.cshtml ,并渲染视图文件如下:
@model IEnumerable<SportsStore.Domain.Entities.Product> @{ ViewBag.Title = "Products"; } @foreach (var p in Model) { <div class="item"> <h3>@p.Name</h3> @p.Description <h4>@p.Price.ToString("c")</h4> </div> }
3.3 设置默认路由
现在要做的全部工作是告诉 MVC 框架,抵达网站根的请求应该被映射到 ProductController 类的List 动作方法上。这可以通过编辑 Global.asax.cs 的 RegisterRoutes 方法实现,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace SportsStore.WebUI { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Product", action = "List", id = UrlParameter.Optional } ); } } }
3.4 运行应用程序
至此,所有基础工作均已就绪。此刻已经有一个含有一个动作方法的控制器,该动作方法在默认 URL 被请求时被调用,它依赖于存储库接口的一个模仿实现,该存储库接口生成了一些简单的测试数据。这些测试数据被传递给与动作方法关联在一起的视图,而视图对每个产品创建一个简单的细节列表。运行该应用程序,效果图如下:
这是ASP.NET MVC 框架典型的开发模式。
4.准备数据库
前面已经可以显示含有产品细节的简单视图,但其显示的知识模仿的 IProductRepository 所返回的测试数据。在可以显示真实数据的存储库之前,还需要建立一个数据库,并用一些数据填充它。
本文打算以 SQL Server 作为数据库,并用 Entity Framework(实体框架 - EF)来访问数据库,EF 是 .NET 的ORM(对象关系映射)框架。ORM 框架让开发人员可以用规则的C# 对象来使用关系数据库的表、列和行。 LINQ 可以与不同的数据源一起工作,其中之一就是 Entity Framework 。
4.1 创建数据库
补充到 Visual Studio 2012 和 SQL Server 2012 中的一个很好的特性是 LocalDB。它是特别为开发者而设计的一个免管理的SQL Server 核心功能实现。使用该特性使我们在建立项目、以及后面将数据库部署到完整版的 SQL Server 期间,可以跳过数据库的设置过程。
第一个步骤是在 Visual Studio 中创建数据库连接。从"View(视图)"菜单中打开"Database Explorer(数据库资源管理器)"窗口,点击”Connect to Database(连接到数据库)“按钮。
根据提示,登录数据库,并新建数据库 SportsStore
4.2 定义数据库方案
新建的数据库只需要一个数据表,用以存储 Product 数据。右击数据库对应的"Tables(表)"条目,新增数据表。
这将会显示创建新表的设计器。使用 T-SQL 窗口,输入对应的SQL语句创建数据表 Products 。
点击左上角的"更新"按钮,会看到该语句的效果摘要。
点击"更新数据库",以执行该 SQL 语句,并在数据库中创建 Products 表。
4.3 向数据库添加数据
本文打算对该数据库手工添加一些数据。
在“数据库资源管理器”窗口中,展开 SportsStore 数据库的“表”条目,右击 Products 表,选择“显示表数据”,然后输入下图所示数据。可以用 Tab 键逐行移动光标。在一行的最后按 Tab 键,将移到下一行并更新数据库中的数据。
4.4 创建实体框架上下文
Entity Framework 的最新版包含了一个叫做“Code-first(代码先行)”的很好的特性。其思想是可以先定义模型中的类,然后再通过这些类生成数据库。
这很适合绿地(Green-field)开发项目,但这些项目并不多见。因此,本文打算演示下 Code-First 的一种变异,以此把模型类与现有的数据库关联在一起。
第一步,是将Entity Framework (此处是6.1版本)添加到 SportsStore.Domain 项目中。通过 管理NuGet 包,安装最新的 Entity Framework 包。
下一个步骤是创建一个将前面建立的简单模型与数据库关联起来的上下文类(Context Class)。
创建一个新文件夹 "Concrete",并在其中添加一个名为"EFDbContext"的新类。代码如下:
using SportsStore.Domain.Entities; using System.Data.Entity; namespace SportsStore.Domain.Concrete { public class EFDbContext:DbContext { public DbSet<Product> Products { get; set; } } }
为了利用 Code-First 特性,需要创建一个派生于 System.Data.Entity.DbContext 的类。这个类会为用户要使用的数据库中的每个表自动地定义一个属性。
该属性指定了表名,并把 DbSet 结果的类型参数指定为实体框架用来表示表行的模型。在这个例子中,该属性名是 Products(数据库中的表名)。即,希望用 Product 模型类型来表示 Products 表的各个行。
需要告诉 Entity Framework 如何连接到数据库,为了完成这一工作,只需要在 SportsStore.WebUI 项目的 Web.config 文件中以上下文类同样的名字添加一条数据库连接字符串即可,如下所示
<connectionStrings> <add name="EFDbContext" connectionString="Data Source=(localdb)\v11.0;Initial Catalog=SportsStore;Integrated Security=True"
providerName="System.Data.SqlClient" /> </connectionStrings>
4.5 创建 Product 存储库
现在,本文已经做好了真正实现 IProductRepository 类所需要的各种准备。 在 SportsStore.Domain 项目的 Concrete 文件夹中添加一个类,取名"EFProductRepository",代码如下:
using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System.Linq; namespace SportsStore.Domain.Concrete { public class EFProductRepository:IProductRepository { private EFDbContext context = new EFDbContext(); public IQueryable<Product> Products { get { return context.Products; } } } }
这就是存储库类,它实现了 IProductRepository 接口,并使用了一个 EFDbContext 实例,以便用 Entity Framework 接收数据库的数据。在对该存储库添加特性时,便会看到此处是如何使用 Entity Framework 的。
最后一步是吧 Ninject 对模仿存储库的绑定替换为对实际存储库的绑定。编辑 SportsStore.WebUI 项目中的 NinjectControllerFactory 类,使 AddBindings 方法如下所示:
using Moq; using Ninject; using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System; using System.Linq; using System.Collections.Generic; using System.Web.Mvc; using System.Web.Routing; using SportsStore.Domain.Concrete; namespace SportsStore.WebUI.Infrastructure { public class NinjectControllerFactory : DefaultControllerFactory { private IKernel ninjectKernel; public NinjectControllerFactory() { ninjectKernel = new StandardKernel(); AddBindings(); } protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType); } private void AddBindings() { //put bindings here ninjectKernel.Bind<IProductRepository>().To<EFProductRepository>(); } } }
新的绑定以粗体显示,它告诉我 Ninject ,用户希望创建 EFProductRepository 类的实例来对 IProductRepository 接口的请求进行服务。再次运行应用程序,效果如下:
注意:记得更新 SportsStore.WebUI 项目中 Entity Framework的版本,与 SportsStore.Domain 项目中的版本保持一致。不然报错。
5.添加分页
从上图可以看出,数据库中的所有产品都显示在一个单一的页面上。本小结将添加对分页的支持,以便在一个页面上显示一定数目的产品,用户可以逐页查看整个产品分类。要实现这一点,可以在 Product 控制器中的 List 方法上添加一个参数,如下所示:
using SportsStore.Domain.Abstract; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace SportsStore.WebUI.Controllers { public class ProductController : Controller { private IProductRepository repository; //指明用户希望每页显示4个产品 public int PageSize = 4; public ProductController(IProductRepository productRepository) { this.repository = productRepository; } public ViewResult List(int page=1) { //从存储库获取 Product 对象, //按主键顺序排序,略过起始页之前出现的产品数, //然后取出由 PaeSize 字段指定的产品个数 return View(repository.Products.OrderBy(p=>p.ProductID).Skip((page-1)*PageSize).Take(PageSize)); } } }
5.1 显示页面链接
如果运行这个应用程序,将看到只有四个条目显示在页面上。如果想查看另一页,可以把查询字符串参数加到 URL 的末尾,如下所示:
http://localhost:64245/?page=2
需要修改 URL 的端口号,使之与正在运行的 ASP.NET 开发服务器端口号匹配。运用这种查询字符串,可以对整个产品分类进行导航。
而为了方便客户。需要在每个产品列表的底部渲染一些页面的链接,以使客户可以在不同的页面之间导航。为了达到这一目的,本文打算实现一个可重用的 HTML 辅助器方法,它类似于之前 【MVC 4】1.第一个 MVC 应用程序 中使用的 Html.TextBoxFor 和 Html.BeginForm 方法。该辅助器方法将为所需要的导航链接生成 HTNL 标记。
(1) 添加视图模型
为了支持 HTML 辅助器方法,本文打算把可用页面数、当前页、已经存储库中产品总数等方面的信息传递给视图。做这种事最容易的办法是创建一个视图模型,在 SportsStore.WebUI 文件夹 Models 中新建类文件 PagingInfo.cs ,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace SportsStore.WebUI.Models { public class PagingInfo { public int TotalItems { get; set; } public int ItemsPerPage { get; set; } public int CurrentPage { get; set; } public int TotalPages { get { return (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage); } } } }
视图模型并不是域模型的一部分,它只是一种便于在控制器与视图之间传递数据的类。为了强调这一点,将这个类放在 SportsStore.WebUI 项目中,以使它与域模型的类分离开(将视图模型放在 MVC 框架项目的 Models 文件夹,而不是放在类库项目中,这种做法足以说明视图模型不是域模型,明确了概念,也使应用程序的结构更清晰)。
(2)添加 HTML 辅助器方法
现在有了这个视图模型,便可以实现这个 HTML 辅助器方法了,该方法称为“PageLinks”。在 SportsStore.WebUI 项目中创建一个新文件夹“HtmlHelpers”,并添加一个新的静态类“PagingHelpers(分页辅助器)”。类文件的内容如下所示:
using SportsStore.WebUI.Models; using System; using System.Text; using System.Web.Mvc; namespace SportsStore.WebUI.HtmlHelpers { public static class PagingHelpers { public static MvcHtmlString PageLinks(this HtmlHelper html, PagingInfo pagingInfo, Func<int, string> pageUrl) { StringBuilder result = new StringBuilder(); for (int i = 1; i <= pagingInfo.TotalPages; i++) { TagBuilder tag = new TagBuilder("a"); // 构造一个<a>标签 tag.MergeAttribute("href", pageUrl(i)); tag.InnerHtml = i.ToString(); if (i == pagingInfo.CurrentPage) tag.AddCssClass("selected"); result.Append(tag.ToString()); } return MvcHtmlString.Create(result.ToString()); } } }
这个 PageLinks 扩展方法使用 PagingInfo 对象中提供的信息生成一组页面链接的 HTML 。Func 参数提供了在其中传递委托的能力,该委托用于生成查看其它页面的链接。
只有包含扩展方法的命名空间在范围内时,其中的扩展方法才是可用的。在一个代码文件中,这是用 using 语句来完成的;但对于一个 Razor 视图,必须把一个配置条目添加到 Web.config 文件中,或在这个视图上添加一条 @using 语句 。容易混淆的是,在一个 Razor 的 MVC 项目中有两个 Web.config 文件:主配置文件位于应用程序的根目录,而视图专用的配置文件位于 Views 文件夹。需要修改的是 Views/Web.config 文件,如下所示:
<system.web.webPages.razor> <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" /> <pages pageBaseType="System.Web.Mvc.WebViewPage"> <namespaces> <add namespace="System.Web.Mvc" /> <add namespace="System.Web.Mvc.Ajax" /> <add namespace="System.Web.Mvc.Html" /> <add namespace="System.Web.Optimization"/> <add namespace="System.Web.Routing" /> <add namespace="SportsStore.WebUI.HtmlHelpers"/> </namespaces> </pages> </system.web.webPages.razor>
在一个 Razor 视图中需要引用的每一个命名空间,都需要以这种方式进行声明,或在视图中用 @using 语句进行声明。
(3)添加视图模型视图
目前还没做好使用 HTML 辅助器方法的准备,还需要把这个 PagingInfo 视图模型类的一个实例提供给视图。可以用 View Bag (视图包)特性来做这件事,但是一个更好的办法是把控制器发送给视图所有数据封装成一个单一的视图模型类。为此,需要把一个新的名为“ProductsListViewModel” 的类添加到 SportsStore.WebUI 的 Models 文件夹。
using SportsStore.Domain.Entities; using System.Collections.Generic; namespace SportsStore.WebUI.Models { public class ProductsListViewModel { public IEnumerable<Product> Products { get; set; } public PagingInfo PagingInfo { get; set; } } }
现在,可以更新 ProductController 类中的 List 方法,以便使用这个 ProductsListViewModel 类,给视图提供在页面上显示的产品细节和分页细节,修改后代码如下:
using SportsStore.Domain.Abstract; using SportsStore.WebUI.Models; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace SportsStore.WebUI.Controllers { public class ProductController : Controller { private IProductRepository repository; public int PageSize = 4; public ProductController(IProductRepository productRepository) { this.repository = productRepository; } public ViewResult List(int page = 1) { ProductsListViewModel model = new ProductsListViewModel { Products = repository.Products.OrderBy(p => p.ProductID).Skip((page - 1) * PageSize).Take(PageSize), PagingInfo = new PagingInfo { CurrentPage = page, ItemsPerPage = PageSize, TotalItems = repository.Products.Count() } }; return View(model); } } }
这些修改将一个 ProductsListViewModel 对象作为模型数据传递给了视图。
此时,视图期望的是一个 Product 对象的序列,因此需要更新 List.cshtml ,以处理这个新视图模型类型,修改后代码如下:
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Products"; } @foreach (var p in Model.Products) { <div class="item"> <h3>@p.Name</h3> @p.Description <h4>@p.Price.ToString("c")</h4> </div> }
这个例子修改了 @model 指示符,以告诉 Razor,现在正在使用一个不同的数据类型。也需要更新 foreach 循环,以使数据源是模型数据的 Products 属性。
(4)显示页面链接
现在已经做到好了再 List 视图上添加页面链接的所有准备。前面已经创建了含有分页信息的视图模型,更新了控制器以使这个信息能够传递给视图,并修改了 @model 指示符以匹配新的视图模型类。剩下的事是在视图中调用这个 HTML 辅助器方法,修改视图文件如下:
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Products"; } @foreach (var p in Model.Products) { <div class="item"> <h3>@p.Name</h3> @p.Description <h4>@p.Price.ToString("c")</h4> </div> } <div class="pager"> @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x })); </div>
运行该应用程序,可以看到已经添加了页面链接,如下图所示。这个链接的样式仍然是很基本。重要的是这个链接能把客户从一个页面带到另一个页面,并浏览正在销售的产品。
5.2 改进 URL
页面链接虽然可以其作用,但它们使用的仍然是查询字符串,以便将分页信息服务器,代码如下:
http://localhost/?page=2
一个更好的方法是专门创建一种遵循可组合 URL 模式的方案。“可组合 URL ”是一种对用户有意义的方式,其形式如下:
http://localhost/Page2
幸运的是,MVC 很容易修改 URL 方案,因为它使用了 ASP.NET 的路由特性。所要做的知识吧一条新路由添加到 Global.asax.cs 中的 RegisterRoutes 方法,如下所示:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace SportsStore.WebUI { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: null, url: "Page{page}", defaults: new { controller = "Product", action = "List" } );
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Product", action = "List", id = UrlParameter.Optional } ); } } }
重要的是吧这条路由加在 Default 路由之前。路由是按它们列出的顺序进行处理的,这里需要这条新路由优先于已经存在的那条。
这是唯一需要对产品分页的 URL 方案进行修改的地方。MVC 框架与路由功能是密切集成的。因此这样的修改将自动反映在 Url.Action 方法的处理结果中。如果运行这个应用程序,并导航到一个页面,将会看到这个新的 URL 方案在其作用。
6.设置内容样式
前面已经建立了大量的基础结构,而且应用程序也开始真正地集合在一起了,但并未把注意力放到其外观上。即使这本书不是一本关于 Web 设计或 CSS 的书,但SportsStore 应用程序设计也会因为太糟糕的格式而破坏它的技术强度。本节将一些常规的事情。
本文打算实现一个带有头部的经典式两列布局。
6.1 定义布局中的公用内容
【MVC 4】2.使用 Razor 中曾解释了 Razor 布局是如何工作和运用的。 当为 Product 控制器创建 List.cshtml 视图时,曾要求用户选中 “使用一个布局” 复选框,但该文本框保留为空。这便使用了默认布局 _Layout.cshtml,可以在 SportsStore.WebUI 项目的 Views/Shared 文件夹中找到它。打开这个文件并修改如下:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <link href="~/Content/Site.css" type="text/css" rel="stylesheet" /> </head> <body> <div id="header"> <div class="title">SPORTS STORE</div> </div> <div id="categories"> We will put something useful here later </div> <div id="content"> @RenderBody() </div> </body> </html>
6.2 添加CSS样式
修改引用的 CSS 样式文件 Site.css 如下:
... body { font-family: Cambria, Georgia, "Times New Roman"; margin: 0; } div#header div.title, div.item h3, div.item h4, div.pager a { font: bold 1em "Arial Narrow", "Franklin Gothic Medium", Arial; } div#header { background-color: #444; border-bottom: 2px solid #111; color: white; } div#header div.title { font-size: 2em; padding: .6em; } div#content { border-left: 2px solid gray; margin-left: 9em; padding: 1em; } div#categories { float: left; width: 8em; padding: .3em; } div.item { border-top: 1px dotted gray; padding-top: .7em; margin-bottom: .7em; } div.item:first-child { border-top: none; padding-top: 0; } div.item h3 { font-size: 1.3em; margin: 0 0 .25em 0; } div.item h4 { font-size: 1.1em; margin: .4em 0 0 0; } div.pager { text-align: right; border-top: 2px solid silver; padding: .5em 0 0 0; margin-top: 1em; } div.pager a { font-size: 1.1em; color: #666; text-decoration: none; padding: 0 .4em 0 .4em; } div.pager a:hover { background-color: silver; } div.pager a.selected { background-color: #353535; color: white; } ...
如果运行程序,会看到其外观已经得到改善,效果图如下:
6.3 创建分部视图
本文的最后一个技巧是重构应用程序,以简化 List.cshtml 视图。本节打算创建一个分部视图(Partial View),这种分部视图是嵌入在另一个视图中的一个内容片断。分部视图是自包含文件,且可以跨视图重用,这有助于减少重复,尤其是需要在应用程序的几个地方渲染同样的数据时。
为了添加分部视图,右击 SportsStore.WebUI 项目中的 /Views/Shared 文件夹,添加新的视图文件 ProductSummary.cshtml 。
修改视图文件如下:
@model SportsStore.Domain.Entities.Product <div class="item"> <h3>@Model.Name</h3> @Model.Description <h4>@Model.Price.ToString("c")</h4> </div>
接着更新 Views/Product/List.cshtml ,以使它能够使用这个分部视图。
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Products"; } @foreach (var p in Model.Products) { Html.RenderPartial("ProductSummary",p); } <div class="pager"> @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x })) </div>
这个例子已经去掉了之前的 List.cshtml 视图中的 foreach 循环中的标记,并把它改成了这个新的分部视图中。用 Html.RenderPartial 辅助器方法来调用这个分部视图,参数是视图的名称和视图模型对象。
提示:RenderPartial 方法并不像大多数其他辅助器方法那样返回 HTML 标记。相反,它把内容直接写入到响应流,因此必须用一个分号,像一个完整的 C# 程序行一样来调用它。这比缓存已渲染的分部视图的 HTML 更有效一些,因为它将被写到响应流。如果喜欢用一种更一致的语法,可以使用 Html.partial 方法,它完成与 RenderPartial 方法同样的功能,但返回的是一个 HTML 片段,并且能够像 @Html.Partial("ProductSummary",p) 一样使用它。
运行应用程序,效果不变。