摘要:
这篇文章将介绍一个ASP.NET应用程序SportsStore的开发过程。
开始
创建解决方案
创建工程
在New ASP.NET Project - SportsStore窗口中,选择Empty模板和MVC folders。其他的模板将自动给你创建一些文件夹和文件,这里我选择Empty,从干净的工程里开始,演示如何将模板的东西加进来。
创建后,将SportsStore工程改名为SportsStore.UI。
创建另一个Class Library工程:SportsStore.Domain。
创建后的工程结构:
安装Package,注意版本冲突:
Ninject:
Ninject.Web.Common:
Ninject.MVC3:
安装完Package后的SportsStore.WebUI引用:
在工程SportsStore.WebUI中添加SportsStore.Domain的引用:
添加引用后:
设置DI容器
在SportsStore.WebUI工程中添加文件夹Infrastructure,在文件夹中添加代码文件NinjectDependencyResolver.cs。用来实例化DI容器定义的对象。
在我的博客文章:Ninject之旅之十三:Ninject在ASP.NET MVC程序上的应用(附程序下载)介绍了如何在ASP.NET项目中运用Ninject框架。
1 using Ninject; 2 using System; 3 using System.Collections.Generic; 4 using System.Web.Mvc; 5 6 namespace SportsStore.WebUI.Infrastructure 7 { 8 public class NinjectDependencyResolver : IDependencyResolver 9 { 10 private IKernel kernel; 11 public NinjectDependencyResolver(IKernel kernelParam) 12 { 13 kernel = kernelParam; 14 AddBindings(); 15 } 16 public object GetService(Type serviceType) 17 { 18 return kernel.TryGet(serviceType); 19 } 20 public IEnumerable<object> GetServices(Type serviceType) 21 { 22 return kernel.GetAll(serviceType); 23 } 24 private void AddBindings() 25 { 26 // put bindings here 27 } 28 } 29 }
修改代码NinjectWebCommon.cs内的方法:RegisterServices。(安装Ninject Package时自动在文件夹App_Start中创建了这个代码文件),建立ASP.NET MVC应用程序和Ninject框架之间的桥梁。
1 /// <summary> 2 /// Load your modules or register your services here! 3 /// </summary> 4 /// <param name="kernel">The kernel.</param> 5 private static void RegisterServices(IKernel kernel) 6 { 7 System.Web.Mvc.DependencyResolver.SetResolver(new SportsStore.WebUI.Infrastructure.NinjectDependencyResolver(kernel)); 8 }
加粗行的代码就是用于建立ASP.NET MVC应用程序和Ninject框架之间的桥梁。
至此,一个简单的ASP.NET MVC应用程序的框架部分完成了。
创建Domain Model
在工程SportsStore.Domain工程内创建文件夹Entities,在文件夹中创建代码文件Product.cs。
代码:
1 namespace SportsStore.Domain.Entities 2 { 3 public class Product 4 { 5 public int ProductID { get; set; } 6 public string Name { get; set; } 7 public string Description { get; set; } 8 public decimal Price { get; set; } 9 public string Category { get; set; } 10 } 11 }
Product类是一个简单实体类,注意类的访问属性改成了public。
创建抽象业务逻辑层
在SportsStore.Domain工程里,添加文件夹Abstract,用于放置抽象访问层的接口代码。
代码:
1 using SportsStore.Domain.Entities; 2 using System.Collections.Generic; 3 4 namespace SportsStore.Domain.Abstract 5 { 6 public interface IProductRepository 7 { 8 IEnumerable<Product> Products { get; } 9 } 10 }
接口IProductRepository目前只定义了一个接口属性Products,用于枚举所有的产品集合。
定义接口,有利于使用Mock工具定义单元测试。
创建数据库
使用SQL Server数据库,创建数据库SportsStore。在数据库里创建表Products。
在Produts表里添加一些测试数据:
创建数据访问层
本文将使用EntityFramework框架作为数据访问层底层框架,首先为工程SportsStore.WebUI和工程SportsStore.Domain安装EntityFramework框架。
以及
创建数据访问层基础代码
在SportsStore.Domain工程里添加文件夹Concrete,在文件夹中添加代码文件EFDbContext.cs。
EFDbContext.cs代码:
1 using SportsStore.Domain.Entities; 2 using System.Data.Entity; 3 4 namespace SportsStore.Domain.Concrete 5 { 6 public class EFDbContext : DbContext 7 { 8 public DbSet<Product> Products { get; set; } 9 } 10 }
EFDbContext类继承DbContext类,作为数据访问层容器。每一个表定义一个DbSet的泛型属性。
public DbSet<Product> Products { get; set; } 表示将Products属性映射到数据库的Products表,他的实体类是Product。
添加连接字符串:
修改SportsStore.WebUI工程根目录下的文件Web.config,添加connectionStrings节点:
1 <connectionStrings> 2 <add name="EFDbContext" providerName="System.Data.SqlClient" connectionString="Data Source=localhost;Initial Catalog=SportsStore;Integrated Security=True" /> 3 </connectionStrings>
注意两点:
1. 根目录下的Web.config文件。
2. add name属性需要跟数据访问层的类类名相同,这里是EFDbContext。
3. connectionStrings节点必须在configSections节点的下方。或者说configSections必须是Web.config文件的第一个节点。
创建数据访问层代码
在Concrete文件夹中创建代码文件EFProductRepository.cs。
1 using SportsStore.Domain.Abstract; 2 using SportsStore.Domain.Entities; 3 using System.Collections.Generic; 4 5 namespace SportsStore.Domain.Concrete 6 { 7 public class EFProductRepository : IProductRepository 8 { 9 private EFDbContext context = new EFDbContext(); 10 public IEnumerable<Product> Products 11 { 12 get 13 { 14 try 15 { 16 return context.Products; 17 } 18 catch (System.Exception e) 19 { 20 return null; 21 } 22 } 23 } 24 } 25 }
- EFProductRepository类继承接口IProductRepository。
- EFProductRepository类里包含了一个EFDbContext对象context,使用new关键字实例化context对象。
- Products属性中,调用context.Products属性从数据库返回实体类Product的集合。
要使用这个repository类,我需要使用Ninject容器添加对这个类EFProductRepository的绑定。在类NinjectDependencyResolver中修改方法AddBindings,添加EFProductRepository绑定到接口EFProductRepository。
1 using Ninject; 2 using SportsStore.Domain.Abstract; 3 using SportsStore.Domain.Concrete; 4 using System; 5 using System.Collections.Generic; 6 using System.Web.Mvc; 7 8 namespace SportsStore.WebUI.Infrastructure 9 { 10 public class NinjectDependencyResolver : IDependencyResolver 11 { 12 private IKernel kernel; 13 public NinjectDependencyResolver(IKernel kernelParam) 14 { 15 kernel = kernelParam; 16 AddBindings(); 17 } 18 public object GetService(Type serviceType) 19 { 20 return kernel.TryGet(serviceType); 21 } 22 public IEnumerable<object> GetServices(Type serviceType) 23 { 24 return kernel.GetAll(serviceType); 25 } 26 private void AddBindings() 27 { 28 kernel.Bind<IProductRepository>().To<EFProductRepository>(); 29 } 30 } 31 }
添加ProductController
1 using SportsStore.Domain.Abstract; 2 using System.Web.Mvc; 3 4 namespace SportsStore.WebUI.Controllers 5 { 6 public class ProductController : Controller 7 { 8 private IProductRepository repository; 9 10 public ProductController(IProductRepository productRepository) 11 { 12 this.repository = productRepository; 13 } 14 15 public ViewResult List() 16 { 17 return View(repository.Products); 18 } 19 } 20 }
- ProductController类中,定义一个IProductRepository接口对象,使用DI的构造函数方式实例化这个对象。
- List视图方法调用接口的Products属性和View函数,返回ViewResult视图。
添加布局视图
在SportsStore.WebUI工程的Views文件夹里,添加文件夹Shared。在文件夹内创建文件_Layout.cshtml。
1 <!DOCTYPE html> 2 3 <html> 4 <head> 5 <meta name="viewport" content="width=device-width" /> 6 <title>@ViewBag.Title</title> 7 </head> 8 <body> 9 <div> 10 @RenderBody() 11 </div> 12 </body> 13 </html>
在Views文件夹的根目录下,添加_ViewStart.cshtml文件。
1 @{ 2 Layout = "~/Views/Shared/_Layout.cshtml"; 3 }
添加List视图:
1 @using SportsStore.Domain.Entities 2 @model IEnumerable<Product> 3 @{ 4 ViewBag.Title = "Products"; 5 } 6 7 @foreach (var p in Model) { 8 <div> 9 <h3>@p.Name</h3> 10 @p.Description 11 <h4>@p.Price.ToString("c")</h4> 12 </div> 13 }
@model IEnumerable<Product>指定该视图的数据模型是IEnumerable<Product>类型。
Model是一个Product集合,通过foreach访问这个集合。
p.Price.ToString("c")是按当前应用程序的文化信息格式化显示产品金额。可以修改Web.config文件的system.web节点下的globalization信息修改格式化信息。例如下面将修改成英镑格式:
<globalization culture="en-GB" uiCulture="en-GB" />
运行程序,访问url路径 /Product/List,得到运行结果:
设置默认路由
上面运行结果,如果删除/Product/List,将得到404页面。
这时候,我们需要修改默认路由,网站访问默认访问路径/Product/List。
在文件夹App_Start下,找到代码文件RouteConfig.cs,修改方法RegisterRoutes。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 using System.Web.Mvc; 6 using System.Web.Routing; 7 8 namespace SportsStore 9 { 10 public class RouteConfig 11 { 12 public static void RegisterRoutes(RouteCollection routes) 13 { 14 routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 15 16 routes.MapRoute( 17 name: "Default", 18 url: "{controller}/{action}/{id}", 19 defaults: new { controller = "Product", action = "List", id = UrlParameter.Optional } 20 ); 21 } 22 } 23 }
将
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
修改成
defaults: new { controller = "Product", action = "List", id = UrlParameter.Optional }
修改默认路由后,默认的访问路径就变成了Product/List。
再次运行程序,得到运行结果:
添加分页
现在在一个页面中显示了所有产品,但是在应用程序中如果产品数量比较多,一般要使用分页技术对列表进行分页。
首先需要添加视图模型类PageInfo。
为了支持HTML helper,我将向视图传递这些信息:页面数量、当前页数、总页数和产品数量。最简单的方法就是创建一个视图模型。
在工程SportsStore.WebUI里找到文件夹Models,在文件夹内添加代码文件PagingInfo.cs。
1 using System; 2 3 namespace SportsStore.WebUI.Models 4 { 5 public class PagingInfo 6 { 7 public int TotalItems { get; set; } 8 public int ItemsPerPage { get; set; } 9 public int CurrentPage { get; set; } 10 public int TotalPages 11 { 12 get 13 { 14 return (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage); 15 } 16 } 17 } 18 }
这也是一个简单类,只包含了模型属性,不包含方法。
有了分页模型类,还需要定义一个包含产品列表和分页信息对象的模型类ProductsListViewModel。
1 using SportsStore.Domain.Entities; 2 using System.Collections.Generic; 3 4 namespace SportsStore.WebUI.Models 5 { 6 public class ProductsListViewModel 7 { 8 public IEnumerable<Product> Products { get; set; } 9 public PagingInfo PagingInfo { get; set; }10 } 11 }
创建后的文件:
有了视图模型类,现在需要修改ProductController,使用这两个模型类。
1 using SportsStore.Domain.Abstract; 2 using SportsStore.WebUI.Models; 3 using System.Web.Mvc; 4 using System.Linq; 5 6 namespace SportsStore.WebUI.Controllers 7 { 8 public class ProductController : Controller 9 { 10 private IProductRepository repository; 11 12 public int PageSize = 4; 13 14 public ProductController(IProductRepository productRepository) 15 { 16 this.repository = productRepository; 17 } 18 19 public ViewResult List(int page = 1) 20 { 21 ProductsListViewModel model = new ProductsListViewModel 22 { 23 Products = repository.Products.OrderBy(p => p.ProductID).Skip((page - 1) * PageSize).Take(PageSize), 24 PagingInfo = new PagingInfo 25 { 26 CurrentPage = page, 27 ItemsPerPage = PageSize, 28 TotalItems = repository.Products.Count() 29 } 30 }; 31 return View(model); 32 } 33 } 34 }
- using SportsStore.WebUI.Models;:使用视图模型类,需要添加引用
- public int PageSize = 4;:定义分页记录数为4。
- List(int page = 1):List方法默认参数是1,如果访问url:Product/List,将默认返回第一页产品。
- using System.Linq;:分页调用了Linq的扩展方法,所以需要添加对Linq的引用。
- ProductsListViewModel model = new ProductsListViewModel:生成视图模型对象,包含Products对象和PagingInfo对象。
- Products = repository.Products.OrderBy(p => p.ProductID).Skip((page - 1) * PageSize).Take(PageSize):获得本页中的Product列表。
- return View(model);:将新的视图模型返回至视图。
下面需要创建帮助类,生成分页HTML元素。
创建文件夹HtmlHelpers。
在文件夹内创建代码文件PagingHelpers.cs。
1 using SportsStore.WebUI.Models; 2 using System; 3 using System.Text; 4 using System.Web.Mvc; 5 6 namespace SportsStore.WebUI.HtmlHelpers 7 { 8 public static class PagingHelpers 9 { 10 public static MvcHtmlString PageLinks(this HtmlHelper html, PagingInfo pagingInfo, Func<int, string> pageUrl) 11 { 12 StringBuilder result = new StringBuilder(); 13 for (int i = 1; i <= pagingInfo.TotalPages; i++) 14 { 15 TagBuilder tag = new TagBuilder("a"); 16 tag.MergeAttribute("href", pageUrl(i)); 17 tag.InnerHtml = i.ToString(); 18 if (i == pagingInfo.CurrentPage) 19 { 20 tag.AddCssClass("selected"); 21 tag.AddCssClass("btn-primary"); 22 } 23 tag.AddCssClass("btn btn-default"); 24 result.Append(tag.ToString()); 25 } 26 return MvcHtmlString.Create(result.ToString()); 27 } 28 } 29 }
- PageLinks扩展方法使用PagingInfo对象的信息生成HTML的分页链接。
- Func参数接受一个用于向视图生成链接的代理方法。
- 这里还利用了TagBuilder类,调用ToString方法生成HTML的链接字符串。
有个这个扩展方法后,还需要在视图文件夹Views里的web.config文件中对定义这个方法的所在类进行声明,声明后的视图才能够使用这个扩展方法。
在Views文件夹内找到文件web.config。
找到节点system.web.webPages.razor,在namespaces里添加:<add namespace="SportsStore.WebUI.HtmlHelpers"/>。
最后是修改List.cshtml视图文件。
1 @model SportsStore.WebUI.Models.ProductsListViewModel 2 3 @{ 4 ViewBag.Title = "Products"; 5 } 6 7 @foreach (var p in Model.Products) { 8 <div> 9 <h3>@p.Name</h3> 10 @p.Description 11 <h4>@p.Price.ToString("c")</h4> 12 </div> 13 } 14 <div> 15 @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x })) 16 </div>
- @model SportsStore.WebUI.Models.ProductsListViewModel:修改视图声明使用新的视图模型。
- Model.Products:Model代表了新的ProductListViewModel类型对象,通过他获得Products列表
- @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x })):调用Html的扩展方法PageLinks,获得分页的HTML字符串,显示到页面上。第一个参数是Model.PagingInfo,第二个参数是一个Func委托,委托使用Url.Action方法生成超链接。
运行程序,得到运行结果:
第一页:
第二页:
第三页:
改进分页链接
现在的分页url是这样的:http://localhost:17596/?page=2,需要传入一个request参数。这样可读性不强。
可以将分页的url改成:http://localhost:17596/page2,这样可读性更好。
需要修改路由信息达到这个目的。
还是找到文件RouteConfig.cs,修改RegisterRoutes方法。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 using System.Web.Mvc; 6 using System.Web.Routing; 7 8 namespace SportsStore 9 { 10 public class RouteConfig 11 { 12 public static void RegisterRoutes(RouteCollection routes) 13 { 14 routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 15 16 routes.MapRoute( 17 name: null, 18 url: "Page{page}", 19 defaults: new { Controller = "Product", action = "List" } 20 ); 21 22 routes.MapRoute( 23 name: "Default", 24 url: "{controller}/{action}/{id}", 25 defaults: new { controller = "Product", action = "List", id = UrlParameter.Optional } 26 ); 27 } 28 } 29 }
- routes方法MapRoute在路由表对象routes里的首部插入新的路由信息,路由将从上往下匹配url,将找到的第一个路由信息返回,生成url字符串,然后结束查找。
- 这里定义新的路由url是:Page{page},{page}跟传入action方法的参数int page = 1对应。
运行程序,得到运行结果,注意生成的链接指向的url变成了Page{page}。
翻到第二页:
第三页:
为应用程序添加样式
到目前为止,这个应用程序没有应用任何样式。我将使用BootStrap作为视图的样式表。
BootStrap是Twitter公司在2012年开发的一个前端样式表框架,现在已经广泛使用在web应用程序中。
添加BootStrap的package。
安装后展开Content文件夹,自动给应用程序添加了bootstrap样式表。fonts文件夹下添加了bootstrap字体,Scripts文件夹下添加了bootstrap的JavaScript文件。
修改_Layout.cshtml文件,应用bootstrap。
1 <!DOCTYPE html> 2 3 <html> 4 <head> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <link href="~/Content/bootstrap.css" rel="stylesheet" /> 7 <link href="~/Content/bootstrap-theme.css" rel="stylesheet" /> 8 <title>@ViewBag.Title</title> 9 </head> 10 <body> 11 <div class="navbar navbar-inverse" role="navigation"> 12 <a class="navbar-brand" href="#">SPORTS STORE</a> 13 </div> 14 <div class="row panel"> 15 <div id="categories" class="col-xs-3"> 16 Put something useful here later 17 </div> 18 <div class="col-xs-8"> 19 @RenderBody() 20 </div> 21 </div> 22 </body> 23 </html>
修改List.cshtml文件,应用bootstrap。
1 @model SportsStore.WebUI.Models.ProductsListViewModel 2 3 @{ 4 ViewBag.Title = "Products"; 5 } 6 7 @foreach (var p in Model.Products) 8 { 9 <div class="well"> 10 <h3> 11 <strong>@p.Name</strong> 12 <span class="pull-right label label-primary">@p.Price.ToString("c")</span> 13 </h3> 14 <span class="lead"> @p.Description</span> 15 </div> 16 } 17 <div> 18 @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x })) 19 </div>
运行程序,得到运行结果。
创建Partial视图
下面我将使用Partial视图来简化List.cshtml视图。Partial视图是一个可以嵌入到另一个视图的内容的片段。Partial视图被他们自己的文件所包含,在多个视图中被重用。这样,如果你需要在应用程序中的许多地方呈现相同样子的数据的时候,可以帮助减少很多重复的代码。
在Views文件夹中创建代码文件ProductSummary.cshtml。将List.cshtml代码中,foreach包含的代码复制到ProductSummary.cshtml中,并将 p 改成Model。
1 @model SportsStore.Domain.Entities.Product 2 3 <div class="well"> 4 <h3> 5 <strong>@Model.Name</strong> 6 <span class="pull-right label labelprimary">@Model.Price.ToString("c")</span> 7 </h3> 8 <span class="lead">@Model.Description</span> 9 </div>
这里,也需要在第一行中声明Partial视图使用的模型类类型,此处是SportsStore.Domain.Entities.Product。
修改List.cshtml文件,调用Partial视图。
1 @model SportsStore.WebUI.Models.ProductsListViewModel 2 3 @{ 4 ViewBag.Title = "Products"; 5 } 6 7 @foreach (var p in Model.Products) 8 { 9 @Html.Partial("ProductSummary", p) 10 } 11 <div> 12 @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x })) 13 </div>
调用Html帮助类的Partial方法,呈现Partial视图。第一个参数是Partial视图的名称,也就是Partial视图的文件名。第二个参数传入传给这个视图的模型对象,这里是Product对象。
运行程序,得到相同的运行结果。