开始 该应用程序的名称为SportStore ,它遵循在线商店 所采取的经典方式。包含以下功能。
产品分类。客户可以通过分类和页面进行浏览。
商品购物车。客户可以添加或删除商品。
结算页面。客户输入其邮寄地址的细节。
管理区。包含管理产品分类所需的CRUD功能,并且只有管理员才能操作。
创建VS解决方案和项目。 创建一个解决方案 ,且包含有3个项目 :域模型项目 、MVC应用程序项目 、单元测试项目 。
打开VS,创建空解决方案 ,名称为SportsStore 。然后添加3个项目。
项目名称
VS项目模板
目的
SportsStore.Domain
类库
存放域实体与逻辑,通过EF创建的存储库来建立持久化
SportsStore.WebUI
ASP.NET MVC Web应用程序(空模板)
存放控制器与视图,充当SportsStore应用程序的UI
SportsStore.UnitTests
测试项目
存放用于上述两个项目的单元测试
创建完成3个项目后,做以下几件事。
删除SportsStore.Domain项目中的Class1.cs文件。
把SportsStore.WebUI项目,设置为启动项目 。
打开SportsStore.WebUI项目的属性页,勾选特定页 选项,但文本框不需要输入值。这样,启动调试后,将直接请求应用程序的根URL。
安装工具包 打开Nuget命令行窗口。输入以下命令。
Install-Package Ninject -version 3.0.1.10 -projectname SportsStore.WebUI
Install-Package Ninject.Web.Common -version 3.0.0.7 -projectname SportsStore.WebUI
Install-Package Ninject.MVC3 -version 3.0.0.6 -projectname SportsStore.WebUI
Install-Package Ninject -version 3.0.1.10 -projectname SportsStore.UnitTests
Install-Package Ninject.Web.Common -version 3.0.0.7 -projectname SportsStore.UnitTests
Install-Package Ninject.MVC3 -version 3.0.0.6 -projectname SportsStore.UnitTests
Install-Package Moq -version 4.1.1309.1617 -projectname SportsStore.WebUI
Install-Package Moq -version 4.1.1309.1617 -projectname SportsStore.UnitTests
Install-Package Microsoft.Aspnet.Mvc -version 5.0.0 -projectname SportsStore.Domain
Install-Package Microsoft.Aspnet.Mvc -version 5.0.0 -projectname SportsStore.UnitTests
添加项目之间的引用
项目名
解决方案依赖项
程序集引用
SportsStore.Domain
None
System.ComponentModel.DataAnnotations
SportsStore.WebUI
SportsStore.Domain
None
SportsStore.UnitTests
SportsStore.Domain SportsStore.WebUI
System.Web Microsoft.CSharp
设置DI容器 在SportsStore.WebUI 项目添加Infrastructure文件夹 ,在其中添加NinjectDependencyResolver.cs 文件。
public class NinjectDependencyResolver : IDependencyResolver { private IKernel kernel; public NinjectDependencyResolver (IKernel kernelParamm ) { kernel = kernelParamm; AddBindings(); } public object GetService (Type serviceType ) { return kernel.TryGet(serviceType); } public IEnumerable<object > GetServices (Type serviceType ) { return kernel.GetAll(serviceType); } private void AddBindings ( ) { } }
修改App_Start文件夹中的NinjectWebCommon.cs 文件,集成Ninject。
private static void RegisterServices (IKernel kernel ) { System.Web.Mvc.DependencyResolver.SetResolver(new Infrastructure.NinjectDependencyResolver(kernel)); }
从域模型开始 在SportsStore.Domain项目 中,添加Entities文件夹 ,添加Product.cs 文件。
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 ; } }
创建抽象存储库 存储库模式 用于从持久化数据库中存储和接收数据。
在SportsStore.Domain项目 中,创建Abstract文件夹 ,添加IProductsRepository.cs 接口。
public interface IProductRepository { IEnumerable<Product> Products { get ; } }
IQueryable<T>接口继承了IEnumerable<T>接口。一个类可以依靠IProductRepository接口获取Product对象 ,而不必知道这些对象从哪儿来,也不必知道接口如何实现,这就是存储库模式 的本质。
创建模仿存储库 创建IProductRepository接口 的模仿实现 ,直到使用数据库时,再替换这些存储库模仿实现。
在SportsStore.WebUI项目的NinjectDependencyResolver类 的AddBindings方法 定义模仿实现,并绑定 到IProductRepository接口 。
private void AddBindings ( ) { 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 } }); kernel.Bind<IProductRepository>().ToConstant(mock.Object); }
ToConstant方法 设置了Ninject的作用域 。每次接收到IProductRepository接口实现的请求时,返回的都是同样的模仿对象 。
显示产品列表 添加控制器 在SportsStore.WebUI项目 中,添加ProductController 控制器。其中构造器中声明了一个对IProductRepository接口的依赖项 ,并添加List动作方法 。
public class ProductController : Controller { private IProductRepository repository; public ProductController (IProductRepository productRepository ) { repository = productRepository; } public ViewResult List ( ) { return View(repository.Products); } }
添加布局、视图起始文件及视图 右键List动作方法 ,添加List视图 ,选择空模板 ,选择Product 模型类,选择使用布局页 。
修改_Layout.cshtml 文件代码如下。
<!DOCTYPE html> <html > <head > <meta charset ="utf-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > @ViewBag.Title</title > </head > <body > @RenderBody() </body > </html >
修改List视图 代码如下。
@using SportsStore.Domain.Entities @model IEnumerable<Product > @{ ViewBag.Title = "Products"; } @foreach (var p in Model) { <div > <h3 > @p.Name</h3 > @p.Description <h4 > @p.Price.ToString("c")</h4 > </div > }
注意,ToString(“c”)方法 ,将Price属性转换成一个字符串,根据服务器的语言 ,渲染成货币形式 。也可以修改服务器的语言设置,只要把<globalization culture=”fr-FR” uiCulture=”fr-FR” /> 添加到web.config 的<system.web>结点 即可。
设置默认路由 应该将应用程序根URL 的请求发送给ProductController 的List动作方法 。修改RouteConfig.cs 文件,将Home改成Product,将Index改成List。
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 } ); }
运行应用程序 页面正常显示。
准备数据库 数据库使用SQLServer ,并用Entity Framework 来访问数据库。
创建数据库 打开SSMS,创建数据库SportsStore ,创建表Products ,并插入数据。
use SportsStorecreate table Products( [ProductID] int not null primary key identity , [Name ] nvarchar (100 ) not null , [Description] nvarchar (500 ) not null , [Category ] nvarchar (50 ) not null , [Price] decimal (16 , 2 ) not null ) insert into Products(Name , Description, Category , Price)values ('Kakak' , 'A boat for one person' , 'Watersports' , 275 ),('Lifejacket' , 'Protective and fashionable' , 'Watersports' , 48.95 ), ('Soccer Ball' , 'FIFA-approved size and weight' , 'Soccer' , 19.50 ), ('Corner Flags' , 'Give your playing field a professional touch' , 'Soccer' , 34.95 ), ('Stadium' , 'Flat-packed 35,000-seat stadium' , 'Soccer' , 79500.00 ), ('Thinking Cap' , 'Improve your brain efficiency' , 'Chess' , 16.00 ), ('Unsteady Chair' , 'Secretly give your opponent a disadvantage' , 'Chess' , 29.95 ), ('Human Chess Board' , 'A fun game for the family' , 'Chess' , 75.00 ), ('Bling-Bling King' , 'Gold-plated, diamond-studded King' , 'Chess' , 1200 )
创建Entity Framework上下文 安装EF 打开Nuget命令行,输入命令安装EF。
Install-Package EntityFramework -projectname SportsStore.Domain
Install-Package EntityFramework -projectname SportsStore.WebUI
创建上下文类 创建上下文类 ,以便模型与数据库关联起来。在SportsStore.Domain项目 中创建Concrete文件夹 ,并添加EFDbContext.cs 文件。
public class EFDbContext : DbContext { public DbSet<Product> Products { get ; set ; } }
注意 :
为了利用code-first特性 ,需要创建一个派生于System.Data.Entity.DbContext的类 ,该类会为数据库中的每个表自动定义一个属性 。
属性名称为数据表名 。而DbSet 结果的类型参数为模型类型 。
配置EF连接数据库 在SportsStore.WebUI项目 中,修改web.config 文件。该连接字符串的名称 与这个上下文类的名称 相同。
<connectionStrings > <add name ="EFDbContext" connectionString ="Data Source=.;Initial Catalog=SportsStore;Integrated Security=True" providerName ="System.Data.SqlClient" /> </connectionStrings >
创建Product存储库 在Concrete文件夹 下,添加EFProductRepository.cs 文件。
public class EFProductRepository : IProductRepository { private EFDbContext context = new EFDbContext(); public IEnumerable<Product> Products { get { return context.Products; } } }
注意 :存储库类实现了IProductRepository接口 ,并使用一个EFDbContext实例 ,以便用EF 接收数据库的数据。
修改NinjectDependencyResolver.cs 文件中的绑定,用实际存储库 绑定替换 之前的模仿存储库 。
private void AddBindings ( ) { kernel.Bind<IProductRepository>().To<EFProductRepository>(); }
运行项目,可以看到应用程序从数据库 ,而不是从模仿存储库 得到了产品数据。
添加分页 分页功能 在ProductController 中的List 动作方法添加一个分页参数 ,实现分页功能。
public class ProductController : Controller { public int PageSize = 4 ; public ViewResult List (int page = 1 ) { return View( repository.Products .OrderBy(p => p.ProductID) .Skip((page - 1 ) * PageSize) .Take(PageSize)); } }
单元测试:分页 在单元测试项目的UnitTest1.cs 文件创建单元测试。
[TestClass ] public class UnitTest1 { [TestMethod ] public void Can_Paginate ( ) { Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products).Returns(new Product[] { new Product { ProductID = 1 , Name = "P1" }, new Product { ProductID = 2 , Name = "P2" }, new Product { ProductID = 3 , Name = "P3" }, new Product { ProductID = 4 , Name = "P4" }, new Product { ProductID = 5 , Name = "P5" } }); ProductController controller = new ProductController(mock.Object); controller.PageSize = 3 ; IEnumerable<Product> result = (IEnumerable<Product>)controller.List(2 ).Model; Product[] prodArray = result.ToArray(); Assert.IsTrue(prodArray.Length == 2 ); Assert.AreEqual(prodArray[0 ].Name, "P4" ); Assert.AreEqual(prodArray[1 ].Name, "P5" ); } }
注意 :获取控制器返回的数据很容易。在结果上调用Model属性 ,就可以得到在List方法 中生成的IEnumerable<Product>序列 。
显示页面链接 运行项目,通过修改https://localhost:44328/?page=1 链接中的page参数 来切换分页。
这样很不方便。因此,需要在每个产品列表的底部渲染某种页面链接,让客户能够在页面之间进行导航。
添加视图模型 在SportsStore.WebUI项目中的Models文件夹 ,添加PagingInfo.cs 文件。
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); } } }
注意 :视图模型 不属于域模型,它只是一种便于在控制器与视图之间传递数据 的类。因此,把PagingInfo类 放在MVC框架项目的Models文件夹 。
添加HTML辅助器方法 在SportsStore.WebUI项目中,创建HtmlHelpers文件夹 ,并添加PagingHelpers.cs 文件。
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" ); tag.MergeAttribute("href" , pageUrl(i)); tag.InnerHtml = i.ToString(); if (i == pagingInfo.CurrentPage) { tag.AddCssClass("selected" ); tag.AddCssClass("btn-primary" ); } tag.AddCssClass("btn btn-default" ); result.Append(tag.ToString()); } return MvcHtmlString.Create(result.ToString()); } }
注意 :PageLinks扩展方法 是用PagingInfo 对象提供的信息生成 一组页面链接的HTML标记 。
单元测试:创建页面链接 在单元测试中,新增测试方法,测试PageLinks辅助器方法 。
[TestMethod ] public void Can_Generate_Page_Links ( ) { HtmlHelper myHelper = null ; PagingInfo pagingInfo = new PagingInfo { CurrentPage = 2 , TotalItems = 28 , ItemsPerPage = 10 }; Func<int , string > pageUrlDelegate = i => "Page" + i; MvcHtmlString result = myHelper.PageLinks(pagingInfo, pageUrlDelegate); Assert.AreEqual(@"<a class=""btn btn-default"" href=""Page1"">1</a>" + @"<a class=""btn btn-default btn-primary selected"" href=""Page2"">2</a>" + @"<a class=""btn btn-default"" href=""Page3"">3</a>" , result.ToString()); }
添加视图的模型数据 在SportsStore.WebUI项目的Models文件夹 中,添加ProductsListViewModel.cs 文件。
public class ProductsListViewModel { public IEnumerable<Product> Products { get ; set ; } public PagingInfo PagingInfo { get ; set ; } }
修改ProductController 中的List动作方法 ,将ProductsListViewModel 类传给视图。
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); }
单元测试:页面模型视图数据 为了确保控制器向视图发送正确的分页数据,在单元测试添加新的测试方法。
[TestMethod ] public void Can_Send_Pagination_View_Model ( ) { Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products).Returns(new Product[] { new Product { ProductID = 1 , Name = "P1" }, new Product { ProductID = 2 , Name = "P2" }, new Product { ProductID = 3 , Name = "P3" }, new Product { ProductID = 4 , Name = "P4" }, new Product { ProductID = 5 , Name = "P5" } }); ProductController controller = new ProductController(mock.Object); controller.PageSize = 3 ; ProductsListViewModel result = (ProductsListViewModel)controller.List(2 ).Model; PagingInfo pageInfo = result.PagingInfo; Assert.AreEqual(pageInfo.CurrentPage, 2 ); Assert.AreEqual(pageInfo.ItemsPerPage, 3 ); Assert.AreEqual(pageInfo.TotalItems, 5 ); Assert.AreEqual(pageInfo.TotalPages, 2 ); }
PS :由于修改了List动作方法传给视图的模型,因此对应的测试方法Can_Paginate 也需要做出相应修改。并且更新List视图 中的@model 。
显示页面链接 在List视图 中调用HTML辅助器方法 。
@using SportsStore.WebUI.HtmlHelpers @model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Products"; } @foreach (var p in Model.Products) { <div > <h3 > @p.Name</h3 > @p.Description <h4 > @p.Price.ToString("c")</h4 > </div > } <div > @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x })) </div >
注意 :只有当包含扩展方法的命名空间在范围内时,其中的扩展方法才是可用的。
一种方法是在视图文件 中使用@using语句 ,如List视图代码中的第一行。
另一种方法是在Views/Web.config配置文件 中,添加HTML辅助器方法的命名空间 ,这样就可以去掉视图中的@using。
<system.web.webPages.razor > <host factoryType ="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.7.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.Routing" /> <add namespace ="SportsStore.WebUI" /> <add namespace ="SportsStore.WebUI.HtmlHelpers" /> </namespaces > </pages > </system.web.webPages.razor >
运行应用程序,可以看到,产品列表下方有分页链接 。
改进URL 目前的链接使用的是查询字符串 形式,如:http://localhost/?page=2。 可以根据可组合URL 模式创建一种对用户有意义的URL形式。如:http://localhost/Page2。
修改RouteConfig.cs 文件的RegisterRoutes方法 ,添加一条新的路由。
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 } ); } }
运行应用程序,并导航到一个页面,可以看到新的URL方案已生效。
设置内容样式 安装Bootstrap包 输入Nuget命令:Install-Package -version 3.0.0 bootstrap -projectname SportsStore.WebUI
在布局中运用Bootstrap样式 修改_Layout.cshtml 文件,添加Bootstrap样式。
<!DOCTYPE html> <html > <head > <meta charset ="utf-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <link href ="~/Content/bootstrap.css" rel ="stylesheet" /> <link href ="~/Content/bootstrap-theme.css" rel ="stylesheet" /> <title > @ViewBag.Title</title > </head > <body > <div class ="navbar navbar-inverse" role ="navigation" > <a class ="navbar-brand" href ="#" > SPORTS STORE</a > </div > <div class ="row panel" > <div id ="categories" class ="col-xs-3" > Put something useful here later </div > <div class ="col-xs-8" > @RenderBody() </div > </div > </body > </html >
修改List.cshtml 文件。
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Products"; } @foreach (var p in Model.Products) { <div class ="well" > <h3 > <strong > @p.Name</strong > <span class ="pull-right label label-primary" > @p.Price.ToString("c")</span > </h3 > <span class ="lead" > @p.Description</span > </div > } <div class ="btn-group pull-right" > @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x })) </div >
创建分部视图 分部视图 是嵌入到另一个视图中的一个内容片段 ,而不是一个模板。它是一种自包含的文件,且可以跨视图重用,有助于减少重复 。
右键SportsStore.WebUI项目的/Views/Shared文件夹 ,点击添加视图 。选择空模板 ,模型类选择Product ,并勾选创建为分部视图 ,命名为ProductSummary 。
在ProductSummary 分部视图中添加标记。
@model SportsStore.Domain.Entities.Product <div class ="well" > <h3 > <strong > @Model.Name</strong > <span class ="pull-right label label-primary" > @Model.Price.ToString("c")</span > </h3 > <span class ="lead" > @Model.Description</span > </div >
修改List 视图,使用ProductSummary 分部视图去掉foreach循环中的重复标记。
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Products"; } @foreach (var p in Model.Products) { @Html.Partial("ProductSummary", p) } <div class ="btn-group pull-right" > @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x })) </div >