第六话 Asp.Net MVC 3.0【MVC实战项目の二】
我们已经可以显示简单的视图,但是我们仍然是模拟IProductRepository实现返回的是一些测试数据,这个时候我们就需要相应的数据库来存储我们项目相关的东西,所以我们需要创建数据库。我们将使用SQL Server作为数据库,我们将访问数据库使用的实体框架(EF)EntityFramework,这是.Net ORM框架。(ORM框架:称"对象关系映射",ORM 主要是把数据库中的关系数据映射称为程序中的对象).我们使用实体框架有几个原因。首先,它是简单和易懂容易上手。第二,用LINQ是意会一流.第三个原因是,它实际上是相当不错的。早期的版本有一点相对不理想的,但是随着版本的演变当前版本是非常优雅和功能丰富。
首先,创建数据库
第一步是创建数据库,我们要做的,使用内置的数据库管理工具包括在Visual Studio。打开服务器资源管理器窗口(如下图1)
图1.右键单击数据连接并选择创建新的数据库从弹出式菜单。进入你的数据库服务器和名称的名称设置新的数据库以SportStore。如下图2
图2.
数据库创建好之后,接下来就是定义数据模型,我们需要在我们的数据库创建一张表,将会使用存储我们的产品数据。添加表及表的数据的步骤一次如下图3-图5.
图3.
图4.
图5.
图5.的数据没有添加完成,下面的程序还加入了更多的数据。
接着,创建实体框架(Entity Framework )环境
实体框架的4.1版本包括一个很棒的功能,称为"代码优先"。我们可以在我们的模型中定义的类,也可以生成一个数据库映射一个类。添加Entity Fremework到我们的项目,方法是在我们需要添加的项目右键,选择"NuGet程序包管理",然后安装EF(Entity Frmework)框架到我们的项目,如下图6.
图6.项目添加好EF(Entity Framwork)框架后,我们需要一个交互的类让他来把我们简单的数据模型来和数据库交互。然后我们写这么一个类(EFDbContext),具体代码如下:
//EFDbContext类可以使简单数据模型与数据库交互 public class EFDbContext : DbContext { public DbSet<Product> Products { get; set; } }
代码相信没有什么可以解释的,接着我们需要告诉实体框架如何连接到数据库,并且我们可以通过添加一个添加一个连接字符串添加到Web.config里面。具体代码如下:
<connectionStrings> <add name="EFDbContext" connectionString="Data Source=.\XXXX;Initial Catalog=SportsStore;Persist Security Info=True;User ID=sa;Password=123;Pooling=False" providerName="System.Data.SqlClient"/> </connectionStrings>
注:上面红色部分大家在自己的环境,去配置自己的连接字符串即可,我这里在写博的时候改成XXX,不要造成什么误会.
配置和连接字符串EF就能知道怎么让我们定义的类和数据库友好的交互了,这个过程实在很妙。接下来我们创建一个储存商品的库房,然后在我们的模型域类库项目(SportsStore.Domain)添加一个文件夹(Concrete)来放我们的存储商品库房类(EFProductRepository),它的具体代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using SportsStore.Domain.Abstract; using System.Data.Entity; using SportsStore.Domain.Entities; namespace SportsStore.Domain.Concrete { public class EFProductRepository : IProductRepository { private EFDbContext context = new EFDbContext(); public IQueryable<Product> Products { get { return this.context.Products; } } } //EFDbContext类可以使简单数据模型与数据库交互 public class EFDbContext : DbContext { public DbSet<Product> Products { get; set; } } }
这个类,它实现了IProductRepository接口和使用了一个EFDbContext实例他会从数据库检索数据使用Entity框架。然后修改我们之前使用Mock模拟IProductRepository绑定的数据,使用真正的数据来绑定,具体代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using Ninject; using System.Web.Routing; using Moq; using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using SportsStore.Domain.Concrete; namespace SportsStore.WebUI.Infrastructure { public class NinjectControllerFactory : DefaultControllerFactory { private IKernel ninjectKernel; public NinjectControllerFactory() { this.ninjectKernel = new StandardKernel(); AddBindings(); } protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType); } private void AddBindings() { //绑定额外数据 this.ninjectKernel.Bind<IProductRepository>().To<EFProductRepository>(); } } }
修改NinjectControllerFactory里的Addbindings方法为上面代码加粗部分即可。项目到这里就可以跑起来,只不过这次是从数据库拿的数据,如下图7(由于当时做到这里时候图片没有保存好,所以下面的图可能是这话结束的图带了一点样式).
图6.原本应该是我图中红色框框的部分,这个是后来加了样式的,还请大家不要见怪。
接下来不啰嗦,搞分页,一个页面显示过多的商品就显得很不要好了,所以我们就搞个分页来让这种不友好消失在分页上。修改ProductController给List方法(Action)添加分页,具体代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using SportsStore.Domain.Abstract; using SportsStore.Domain.Concrete; using SportsStore.WebUI.Models; namespace SportsStore.WebUI.Controllers { public class ProductController : Controller { public int PageSize = 4; //设置一页显示多少商品 private IProductRepository repository; public ProductController(IProductRepository productReposittory) { this.repository = productReposittory; } //返回一个视图 public ViewResult List(int page = 1) { return this.View(this.repository.Products.OrderBy(p => p.ProductID) .Skip((page - 1) * PageSize) .Take(PageSize)); } } }
OK,这样搞是没错的,怎么跑起来页面只有4个商品信息,分页呢!其实分页我们已经实现了,那其他的数据呢,我们在地址栏后面继续输入,如下图就可以访问到其他的数据,如下图7.
图7.我们需要这样的拼写连接才能访问,这样似乎比我们原来直接页面展示完所有的所有的数据的情况还要糟糕。这样我们开发人员当然明了其中的原有,那要是换做客户,他们可是不会知道这个的。所以我们需要在每个页面的底部显示出页面的超连接,用户通过点击这个连接可以访问到不同的页面,所以我们要实现一个可重用的HTML Help方法供我们使用。我们HTML Help将会生成的HTML标记导航链接我们需要。
添加视图模型
支持HTML Helper类,我们将信息的数量传递到视图的页面可用,当前页面,产品在存储的库总数量。添加一个视图模型类(PagingInfo)到Mvc Web项目的Models文件下,具体代码如下:
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); } } } }
一个视图模型并不是模型域(Domain)的一部分,这个类只不过方便了控制器(Controller)和视图(View)之间的数据传递。
然后添加HTML Helper方法,既然我们有了视图模型,我们可以实现HTML helper方法,我们可以使用PageLinks实现数据交互。在我们Mvc Web项目里添加一个文件夹(HtmlHelpers)创建一个类称为"PagingHelpers"。具体代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using SportsStore.WebUI.Models; using System.Text; 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()); } } }
PageLink扩张方法生成的HTML的一组页面链接,使用信息来自PagingInfo对象中提供。生成的连接可以转移到其他页面展示相应页面的数据信息。
OK,先不急于功能的实现了,我们创建项目的时候不是创建了一个单元测试的模块,那现在就目前的所写的代码,我们可以试着去写一些的测试方法。来写一个测试创建页面链接的单元测试,在我们的SportsStore.UnitTests项目里添加我们的测试连接的类(PagingHelpersTest),我们需要给SportsStore.UnitTests项目项目添加2个引用,具体如下图8.
图8.添加好单元测试的DLL后继续PagingHelpersTest类的代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Web.Mvc; using SportsStore.WebUI.HtmlHelpers; using SportsStore.WebUI.Models; namespace SportsStore.UnitTests { [TestClass()] public class PagingHelpersTest { [TestMethod] public void Can_Generate_Page_Links() { //为了应用扩张方法 HtmlHelper myHelper = null; //创建PagingInfo数据 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(result.ToString(), @"<a href=""Page1"">1</a><a class=""selected"" href=""Page2"">2</a><a href=""Page3"">3</a>"); } } }
这个单元测试的方法先写到这里,后面在继续补充有关单元测试的东东。
接着单元测试之前继续,一个扩展方法(PagingHelpers类的方法)可供只有当名称空间,其中包含它在作用域中使用。在每一个代码文件,都要做了一次使用声明,但对于一个Razor视图,我们必须对Web.config文件经行配置,这样我们就要对我们的Mvc Web项目的Views文件夹下的Web.config经行配置,具体配置如下:
<system.web.webPages.razor> <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.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.Routing" /> <add namespace="SportsStore.WebUI.HtmlHelpers"/> </namespaces> </pages> </system.web.webPages.razor>
配置如上面红色加粗的部分.
添加视图数据模型
我们不准备使用我们的HTML Helper方法。我们目前还没有提供的一个实例的PagingInfo模型类视图。视图数据或视图包特性,但是我们将需要处理转换为正确的类型。那么我们包装所有的数据给控制器和视图单一的数据类型。我们在Web项目里的Models文件夹下添加一个类(ProductsListViewModel),具体代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using SportsStore.Domain.Entities; namespace SportsStore.WebUI.Models { public class ProductsListViewModel { public IEnumerable<Product> Products { get; set; } public PagingInfo PagingInfo { get; set; } } }
我们现在可以更新在ProductController(控制器)类里的List方法(Action)使用ProductsListViewModel类提供视图与产品的细节上显示的页面和详细的分页.修改的ProductController(控制器)如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using SportsStore.Domain.Abstract; using SportsStore.Domain.Concrete; using SportsStore.WebUI.Models; namespace SportsStore.WebUI.Controllers { public class ProductController : Controller { public int PageSize = 4; //设置一页显示多少商品 private IProductRepository repository; public ProductController(IProductRepository productReposittory) { this.repository = productReposittory; } //返回一个视图 public ViewResult List(int page = 1) { //return this.View(this.repository.Products.OrderBy(p => p.ProductID) // .Skip((page - 1) * PageSize) // .Take(PageSize)); ProductsListViewModel viewModel = new ProductsListViewModel { Products = this.repository.Products.OrderBy(h => h.ProductID) .Skip((page - 1) * PageSize) .Take(PageSize), PagingInfo = new PagingInfo { CurrentPage = page, ItemsPerPage = PageSize, TotalItems = this.repository.Products.Count() } }; return this.View(viewModel); } } }
这样修改后视图是预期返回Product对象的序列,所以我们需要修改前台List.cshtml,具体代码如下:
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Product List"; } @foreach (var p in Model.Products) { <div class="item"> <h3>@p.Name</h3> @p.Description <h4>@p.Price.ToString("c")</h4> </div> }
上面红色加粗部分就是告诉Razor引擎我们已经在使用一个不同的类型,所以我们也需要更新数据源。这样写好,可以运行一下程序,如下图9
图9.这里可以看到分页的标识已经显示出来了。
那么这里为什么不直接使用一个GridView吗? 因为我们用Asp.Net Mvc构建了一个稳固并且可维护的架构,这里面包含了分解关注点的思想。不像使用GridView控件,将UI跟数据访问耦合在了一起,这样做非常快而且方便,但从长远看,这只是图一时的方便给后期的维护留下一个大坑。
有这么一个不太爽的地方,不知道朋友注意到了没有,我们现在运行起来的Web项目似乎的URL看着不太友好,如下图10.
这样的URL换做谁都不想让他存在,我们是不是可以弄个更加漂亮的URL地址出来呢!比如这种:Http://localhst:XXXX/page2或者Http://localhst:XXXX/page_2....这样看起来或许自己或许舒服点,在我们的MVC项目里,我们可以给他添加一条路由来改变它,因为MVC有Asp.net路由特性。我们在web项目的Global.asax文件里给他添加一条新的路由,具体如下:
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); //添加一条路由 routes.MapRoute( null, "Page{page}", new { controller = "Product", action = "List" } ); routes.MapRoute( "Default", // 路由名称 "{controller}/{action}/{id}", // 带有参数的 URL new { controller = "Product", action = "List", id = UrlParameter.Optional } // 参数默认值 ); }
上面加粗部分就是我们添加新的路由,现在运行项目我们可以看到如下如11的URL。
图11.和图10的相比起来这个URL是不是看起来就爽多了。
因为我们要做一个简单的项目,一直用这样没有样式的页面不太好了吧!那就给我们项目加入一点简单的样式(Css),首先来修改一下我们的模板(_Layout.cshtml)页面,具体修改如下:
<!DOCTYPE html> <html> <head> <title>@ViewBag.Title</title> <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" /> <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script> </head> <body> <div id="header"> <div class="title">SPOPTS STORE</div> </div> <div id="categories">Will put something useful here later</div> <div id="content"> @RenderBody() </div> </body> </html>
然后添加样式,我们在Mvc Web项目的Conten文件夹下的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; } DIV#categories A { font: bold 1.1em "Arial Narrow","Franklin Gothic Medium",Arial; display: block; text-decoration: none; padding: .6em; color: Black; border-bottom: 1px solid silver; } DIV#categories A.selected { background-color: #666; color: White; } DIV#categories A:hover { background-color: #CCC; } DIV#categories A.selected:hover { background-color: #666; } FORM { margin: 0; padding: 0; } DIV.item FORM { float:right; } DIV.item INPUT { color:White; background-color: #333; border: 1px solid black; cursor:pointer; } H2 { margin-top: 0.3em } TFOOT TD { border-top: 1px dotted gray; font-weight: bold; } .actionButtons A, INPUT.actionButtons { font: .8em Arial; color: White; margin: .5em; text-decoration: none; padding: .15em 1.5em .2em 1.5em; background-color: #353535; border: 1px solid black; } DIV#cart { float:right; margin: .8em; color: Silver; background-color: #555; padding: .5em .5em .5em 1em; } DIV#cart A { text-decoration: none; padding: .4em 1em .4em 1em; line-height:2.1em; margin-left: .5em; background-color: #333; color:White; border: 1px solid black;}
OK,添加好样式后,运行我们的程序如下图12.
图12.这样看起来虽然有点丑了,但是比起之前的页面要好多了。
我们展示数据的部分完全可以使用"局部视图"来显示的,我们可以创建一个局部视图页面来嵌套在List展示页面,这样有什么好处呢!我们知道,作为Action的响应,最常见的做法是Return View();也就是说,返回一个视图。但是如果我们某的操作只是要返回页面的一部分(局部的部分【局部视图】),典型的情况就是,在页面上实现局部的刷新功能。
那么我们来创建一个局部视图,在我们Mvc Web项目的Views文件夹里的Shared文件夹里创建一个视图,如下图13.
图13.我们新建的局部试图也是强类型。创建好后我们的ProductSummary.cshtml页面的代码如下:
@model SportsStore.Domain.Entities.Product @{ ViewBag.Title = "ProductSummary"; } <div class="item"> <h3>@Model.Name</h3> @Model.Description <h4>@Model.Price.ToString("C")</h4> </div>
OK,我们的局部视图创建完成后,我们在List.cshtml页面要用它,在程序运行时要从局部试图页面展示出数据到list页面上,那么List.cshtml的代码修改如下:
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Product List"; } @foreach (var Pro in Model.Products) { Html.RenderPartial("ProductSummary", Pro); } <div class="pager"> @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { Page = x })) </div>
上面代码红色加粗的部分就是引入局部试图到List页面。Html.RenderPartial方法需要传入对象参数。
添加好局部试图,运行我们的程序如下图12.
图12.好了,今天的项目就写到这里,后续继续,因为这话的东西配置方面的东西比较多,比较杂,想学习的同学还是有这个耐心看完的,文章写的有些仓促,所以肯定有描述错误的地方,还请路过的前辈,朋友给点指点。共同进步,谢谢!