9.SportsStore——一个真实的应用程序

  • 时间:2019-06-07
  • 作者:Charles
  • 热度:446

从本文开始,接下来的几篇文章将使用MVC框架建立一个在线商城项目。主要实现产品分类浏览,添加商品到购物车,提交订单,以及网站后台管理,包括管理员登陆验证,商品的增删改查,商品分类管理。

一:开始

1.1   创建Visual Studio解决方案和项目

本项目打算创建一个含有三个项目的vs解决方案,一个项目包含域模型,一个是mvc应用程序,第三个包含单元测试。首先,新建项目,选择空解决方案模板,命名为SportsStore,如下图所示。

然后再SportStore解决方案下依次添加三个项目,分别名为:

1.     SportsStore.Domain,使用类库项目模板,用于存放实体与逻辑。

2.     SportsStore.WebUI,使用MVC5应用程序模板,包含vs自带的基本页面,用于存放控制器与视图,充当SportsStore应用程序的UI。

3.     SportsStore.Test,在添加SportsStore.WebUI时选中创建单元测试,存放用于上述两个项目的单元测试。

本文使用VS自带的MVC项目基本模板,默认安装了BootStrap前端框架,并基于Bootstrap搭建了首页,关于我们,联系我们三个简单的页面。项目搭建完之后的文件如下图所示。

为了方便调试,右击SportsStore.WebUI项目,从菜单中选中设为启动项,其名称将为粗体。

1.2 添加引用

首先需要添加项目对工具库的引用,使用NuGet包管理器,为SportsStore.WebUI和SportsStore.Test项目安装Ninject和Moq。

其次添加项目之间的引用,SportsStore.Demain不依赖其他两个项目,SportsStore.WebUI依赖SportsStore.Domain,SportsStore.Test依赖SportsStore.WebUI和SportsStore.Domain。

最后添加对微软程序集的引用,为SportsStore.Domain添加System.Web.Mvc和System.ComponentModel.DataAnnotations的引用。为SportsStore.Test添加System.Web.Mvc、System.Web、Micorsoft.CSharp引用。

1.3设置DI容器

在之前MVC工具文章中介绍过使用Ninject创建一个自定义依赖性解析器,以便MVC框架用它创建整个应用程序的实例化对象。在本例打算采用不同方式,即创建一个自定义的控制器工厂,这是MVC框架中许多扩展点示例之一,用户可以在其中添加自定义代码,以改变MVC框架的默认行为,或者像这里所作的一样,将DI限制到应用程序的一部分。

在SportsStore.WebUI项目中添加一个名为Infrastructure文件夹,然后在文件夹下创建一个名为NinjectControllerFactory的类,此类继承自默认控制器工厂,编辑文件如下图所示。

目前未添加任何绑定。接下来告诉MVC希望使用此控制器工厂类来创建控制器对象,其方法时在SportsStore.WebUI项目的Global.asax.cs文件的Application_Start方法中添加此工厂类,如下图所示。

二:从域模型开始

首先建立一个Product实体类,在SportsStore.Domain项目下新建一个名为Entities的文件夹,在文件夹下新建一个名为Product的类文件,结构如下图所示。

2.1 创建一个抽象的存储库

现在需要通过某种方式来获取数据库中的Product实体。人们希望持久化逻辑与域模型实体是分离的,此事通过使用存储库来实现。此刻,不必担心会如何实现持久化,不过,将从定义它的接口来开始这一过程。

在SportsStore.Domain项目下新建一个名为Abstract的新文件夹,在文件夹下创建一个名为IProductsRepository的新接口。如下图所示。

该接口使用了IQueryable<T>接口,以便能够获得一系列Product对象,而不必说明数据如何存储、存储在哪儿,以及如何接收数据。

2.2 创建模仿存储库

现在已经定义了一个抽象接口,可以实现持久化机制,并将其挂接到一个数据库,但是目前并不打算做这件事,为了能够开始编写应用程序的其他部分,先创建一个IProductsRepository接口的模仿实现。编辑NinjectControllerFactory类的AddBindings方法,如下图所示。

AsQueryable方法是Linq的一个扩展方法,它将IEnumberable<T>转换成IQueryable<T>,此处需要它来匹配接口签名。

我们希望,Ninject无论何时接收到一个IProductsRepository接口实现的请求,都返回同样的模仿对象,这便是使用ToConstant方法的原因,如下所示。

ninjectKernel.Bind<IProductRepository>().ToConstant(mock.Object);

Ninject会一直以该模仿对象来满足对IProductsRepository接口的请求,而不是每次都创建一个新的实现对象实例。

三:显示产品列表

本节将创建一个控制器和一个动作方法,它能够显示存储库中的产品细节,当然,这将只针对模仿存储库中的数据。

3.1添加控制器

在SportsStore.WebUI项目下Controllers文件夹下添加一个名为ProductController的控制器,编辑该文件,如下图所示。

除了删除Index动作方法外,还添加了一个以IProductRepository为参数额构造器,这将允许Ninject在实例化这个控制器类时,注入产品存储的依赖性。

添加一个动作方法,名为List,它将渲染要给显示完整产品列表的视图。如下图所示。

3.2添加视图

右击List动作方法,添加一个名为List的强类型视图,视图模型类为IEnumerable<SportsStore.Domain.Entities.Product>,需要手动输入。使用项目的默认布局,所以要选中“使用布局或母版页”复选框,但让此文本框保持为空。如下图所示。

渲染视图数据

在视图中,通过一个foreach的Razor循环来创建一个列表,如下图所示。

注意:ToString(“c”)方法是吧Price属性值转换成一个字符串,它会根据服务器的语言设置把数字渲染成货币。如果服务器语言设置为en-US,那么它将返回前置为$符号的字符串。可以在Web.config文件的<system.web>节点下添加一个子节点来手动设置语言,设置方法:<globalization culture="en-US" uiCulture="en-US"/>。

3.3设置路由

现在要做的工作时告诉MVC框架,抵达网站根的请求应该被映射到ProductController类的List动作方法上。这可以通过编辑SportsStore.WebUI项目下App_Start文件夹内的RouteConfig文件来实现,如下图所示。

3.4运行应用程序

至此,所有基础工作均已就绪。现在已经有一个含有一个动作方法的控制器,该动作方法在默认URL被请求时被调用,它依赖于存储库接口的一个模仿实现,该存储库接口生成了一些简单的测试数据。这些测试数据被传递给与动作方法相关联在一起的视图,而视图对每个产品创建一个简单的细节列表。运行应用程序,将会看到如下所示的结果。

四:准备数据库

前面已经可以显示含有产品细节的简单视图,但其显示的只是模仿的IProductRepository所返回的测试数据。在可以显示真实的存储库之前,还需要建立一个数据库,并用一些数据填充它。

本例使用SQL Server作为数据库,并用Entity Framework来访问该数据库。EF是.NET的ORM框架。ORM框架让开发人员可以使用规则的C#对象来使用关系数据库的表、列和行。

4.1创建数据库和数据表

本机使用SQL Server2008R2作为数据库服务器,打开数据库管理工具,连接到数据库,新建一个名为SportsStore的数据库。

目前数据库只需要一个数据表,来存储Product数据。右击SportsStore数据库,点击菜单中的新建查询,在打开的查询编辑窗口编写如下SQL语句。

该语句创建了一个名为Products的表,它又不同的列对应着Product模型类中所定义的属性。

点击左上角的执行按钮,如果执行成功,在对象资源管理器窗口点击刷新按钮,会看到SportsStore数据库下表文件夹下新建的Products表。

右击表名,在菜单中选中编辑前200行,手动填充一些数据,如下图所示。

4.2创建实体框架上下文

Entity Framework包含了一个叫做“Code-First(代码先行)”的特性,其思想是可以先定义模型中的类,然后再通过这些类生成数据库。本文不打算使用这一特性,而是使用Code-First的一种变异,以此把模型类与现有的数据库关联在一起。

首先使用NuGet包管理器添加Entity Framework到SportsStore.Domain项目中。然后创建一个将前面建立的简单模型与数据库关联起来的上下文类(Context Class)。创建一个新文件夹,名为Concrete,在其中添加一个名为EFDbContext的新类,编辑内容如下图所示。

为了利用Code-First特性,需要创建一个派生于System.Data.Entity.DbContext的类。这个类会为用户要使用的数据库中的表自动地定义一个属性。

该属性指明了表名,并把DbSet结果的类型参数指定为实体框架用来表示行的模型。在此例中,该属性名是Products,而参数类型是Product。即希望用Product模型类型来表示Products表的各行。

要告诉Entity Framework如何连接到数据库,需要在SportsStore.WebUI项目下的Web.config文件中以这个上下文类同样的名字添加一条数据库连接字符串即可。如下图所示。

4.3创建Product存储库

现在,我们已经做好真正实现IProductRepository类所需要的各种准备。在SportsStore.Domain项目的Concrete文件夹下添加一个名为EFProductRepository的类,编辑此文件,如下图所示。

这就是存储库类,它实现了IProductRepository接口,并使用了一个EFDbContext实例,以便于EF接收数据库的数据。

最后一步是把Ninject对模仿存储库的绑定替换为对实际存储库的绑定,编辑NinjectControllerFactory类下的AddBindings方法如下图所示。

新的绑定告诉Ninject,用户希望创建EFProductRepository类的实例来对IProductRepository接口的请求进行服务。再次运行应用程序,会看到如下图的结果。

五:添加分页

数据库的所有产品都显示在单一的页面上是不利于用户体验的,本小节将添加对分页的支持,以便在一个页面上显示一定数目的产品,用户可以逐页查看整个产品分类。编辑Product控制器中的List方法,如下图所示。

为控制器添加一个int类型的PageSize字段,在此例中设置为4。对List方法添加一个可选参数,默认值为1,当调用不带参数的方法时,得到的将是第一个页面。

5.1显示页面链接

如果现在运行这个应用程序,将会看到只有四个条目显示在页面上,如果想查看另一页,可以把查询字符串加入到URL的末尾,如下所示。

http://localhost:14131/product/list/?page=2

用这样的方式让浏览者翻页是不现实的,需要在每个页面的底部渲染一些页面的链接,以使浏览者在不同的页面之间进行导航。为了实现这一目的,本例打算实现一个可重用的HTML辅助器方法,它类似于之前用到的Html.TextFor和Html.BeginForm方法,该辅助器方法将为所需要的分页链接生成Html标记。

添加视图模型

为了支持HTML辅助器方法,本例打算把可用页面数、当前页,以及存储库中产品总数等方面的信息传递给视图。做这件事最简单的方法是创建一个视图模型。在SportsStore.WebUI项目的Models文件夹下新建一个名为PagingInfo的类,编辑内容如下。

视图模型并不是域模型的一部分,它是一种便于在控制器与视图之间传递数据的类。为了强调这一点,将这类放在SportsStore.WebUI项目中。

添加HTML辅助器方法

现在有了视图模型,便可以实现这个HTML辅助器方法了。在SportsStore.WebUI项目下新建一个名为HtmlHelpers的文件夹,并添加一个名为PagingHelpers的新的静态类,如下图所示。

这个PageLinks方法使用PagingInfo对象中提供的信息生成一组页面链接的HTML。Func参数提供了在其中传递委托的能力,该委托用于生成查看其他页面的链接。

只有当包含扩展方法的命名空间在范围内时,其中的扩展方法才是可用的。在一个代码文件中,这是用using语句来完成的;但是对于一个Razor视图,必须把一个配置条目添加到Web.config文件中,或在这个视图上添加一条@using语句。注意:此Web.config文件是Views文件夹下的。修改文件里的namespaces节点,如下图所示。

添加视图模型数据

目前还没有做好使用HTML辅助器方法的准备,还需要把这个PagingInfo视图模型类的一个实例提供给视图。可以用ViewBag来做这件事,但是一个更好的方法是把控制器发送给视图的所有数据封装成一个单一的视图模型类。为此,需要在SportsStore.WebUI的Models文件夹下新建一个名为ProductsListViewModel的类,如下图所示。

现在更新Product控制器中的List方法,以使用这个ProductsListViewModel类给视图提供在页面上显示的产品细节和分页细节。如下图所示。

还需要更新List.cshtml来处理这个新视图模型类型,如下图所示。

此时运行程序可以看到,已经添加了页面链接,并可以点击浏览每页信息。

 

5.2改进URL

页面链接虽然可以起作用,但它们使用仍然是查询字符串,以便将分页信息给服务器,如下所示。

http://localhost:14131/?page=2

一个更好的方法是专门创建一种遵循可组合URL模式的方案。“可组合URL”是一种对用户很有意义的方式,其形式如下。

http://localhost:14131/page2

使用ASP.NET的路由特性,我们很容易修改URL方案,把一条新路由添加到RegisterRoutes方法,如下所示。

这是唯一需要对产品分页的URL方案进行修改的地方。MVC框架与路由功能是密切集成的,因此这样的修改将自动地反映到Url.Action方法的处理结果中。运行应用程序,点击分页链接,将会看到所希望的URL方案。

六:设置内容样式

前文已经说过,此MVC项目已经默认安装了Bootstrap前端框架,并基于此框架创建了几个默认页面。Bootstrap是一款广泛应用优秀的框架,我们可以利用它快速搭建起漂亮的排版页面,而且可以同时适应PC、平板电脑和手机浏览器。

6.1修改PageHelpers

Bootstrap预先定义了很多页面常用部件的样式,其中就包括分页部件,下面我们修改PageHelpers下的PageLinks方法,生成符合Bootstrap预定义样式的HTML,如下图所示。

增加了上一页和下一页的按钮,其中使用的class属性都是由Bootstrap定义好的,我们不用再写样式。

6.2更新List视图

Bootstrap 提供了一套响应式、移动设备优先的流式网格系统,随着屏幕或视口(viewport)尺寸的增加,系统会自动分为最多12列。下面根据网格系统,对List视图进行重新排版,如下所示。

具体的排版方法在这里不再赘述,可以前往Bootstrap官网参考相关文档。此时再运行应用程序,我们将看到如下布局的页面。

 

6.3创建分部视图

本文最后一个小节是重构应用程序,以简化List.cshtml视图。方法是创建一个分部视图(Partial View),这种分部视图是嵌入在另一个视图中的一个内容片段。分部视图是自包含文件,且可以跨视图重用,这有助于减少重复,尤其是需要再应用程序的几个地方渲染同样的数据时。

右击SportsStore.WebUI项目中/Views/Shared文件夹,从弹出菜单中选择添加视图,命名为ProductSummary,模型类选择Product,选中创建为分部视图复选框。如下图所示。

分部视图与常规视图十分相似,只是它被渲染时产生的是一个HTML片段,而不是整个HTML文档。修改此分部视图,如下图所示。

现在,更新List.cshtml,使它能够使用这个分部视图,如下所示。

用Html.RenderPartial辅助器方法来调用这个分部视图,参数时视图的名称和视图模型对象。此时运行应用程序,将会看到仍和之前页面一样。

提示:RenderPartial方法并不像大多数其他辅助器方法那样返回HTML标记,它把内容直接写到响应流,因此必须用一个分号,并且像完整的C#代码一样来调用它。这比缓冲已渲染的分部视图的HTML更有效一些,因为它将被写到响应流。可以用Html.partial方法实现与RenderPartial方法同样的功能,但是它返回的时一个HTML片段。



博主声明

1、本博客主要为原创文章,转载请注明出处。

2、部分文章来自网络,已注明出处,如有侵权请与本人联系。

3、如果文章内容有误,或者您有其他更好的意见、建议请给我留言,我会及时处理!