ASP.NET + MVC5 入门完整教程八 -—-- 一个完整的应用程序(上)

SportsStore

1、开始

创建Visual Studio 解决方案和项目

这里打算创建一个解决方案,它含有 3 个项目: 域模型项目、MVC应用程序项目和单元测试项目。首先,创建一个新的 Visual Studio解决方案,其名称为“ Sportsstore",所采用的模板是“Blank Solution(空解决方案)”,该模板位于“ New Project(新项目)”对话框的“ other Project Types(其他项目类型)”→“ Visual Studio Solutions( Visual studio解决方案)”中,如图所示。单击“OK”按钮,创建该解决方案。

      Visual studio解决方案是一个含有一个或多个项目的容器。本示例应用程序需要3个项目,如下图所示。在解决方案资源管理器窗口中右击“ Solution(解决方案)”,并从弹出的菜单中选择“Add(添加)”→“ New Project(新项目)”,便可以添加一个项目。添加完成下面三个项目。



      本人喜欢为“ASP.NET MVC Web Application ”模板选用“Empty”选项,这样可以针对项目添加一些初始设置,如 JavaScript 库、CSS样式表和一些C#类,用于配置应用程序的一些特性,如安全性以及路由等。所有这些内容和配置都可以手工设置,这样做有利于更深入地了解 MVC 框架的工作机制。创建上述3个项目后,解决方案资源管理器窗口看上图所示。其中已经删除了 Visual Studio 添加到 SportsStore.Domain 项目中的 class1.cs文件,它没什么用处。为了使调试更容易,右击 SportsStore.WebUI 项目,并从弹出的菜单中选择“ Set as StartupProject(设为启动项目)”。意即,当从“ Debug(调试)”菜单中选择 Start Debugging(启动调试)”或“ Start without Debugging(开始执行(不调试))”时,它是应用程序的启动项目。Visual studio会在启动调试时,尝试导航到某个视图文件。因此可以在解决方案资源管理器窗口中右击 Sportsstore.WebUI 项目,然后从弹出的菜单中选择“ Properties(属性)”。单击“web”可打开与Web有关的属性,选中“ Specific Page(特定页)”选项。在“ Specific Page”文本框中不需要输入值,只要选择该选项,就可以让 Visual Studio 不必在启动调试后去猜测要查看的页面URL,而让浏览器直接请求应用程序的根URL。

安装工具包

SportsStore.WebUI 安装 Ninject、Moq,SportsStore.UnitTests 安装 Moq 。如果不会参见:

ASP.NET + MVC5 入门完整教程七 -—-- MVC基本工具(上) 内含安装 Ninject方法
ASP.NET + MVC5 入门完整教程七 -—-- MVC基本工具(下) 内含安装 Moq方法

添加项目之间引用

     需要设置项目之间的一些依赖项和微软程序集的引用。为此,在解决方案资源管理器窗口中右击各个项目,选择“ Add Reference(添加引用)”,然后从“ Assemblies(程序集)”→“ Framework(框架)”、“ Assemb1ies(程序集)”→“ Extensions(扩展)”或者“ Solution(解决方案)”中进行选择,添加如下表所示的引用。右键 SportsStore.WebUI 项目,设置为启动项目。

项目依赖项
项目名 解决方案依赖项 程序集引用
SportsStore.Domain System.Component.Model.DataAnnotations
SportsStore.WebUI SportsStore.Domain
SportsStore.UnitTests

SportsStore.WebUI

SportsStore.Domain

System.Web

Microsoft.CSharp

设置 DI 容器

ASP.NET + MVC5 入门完整教程七 -—-- MVC基本工具(上)中介绍了如何使用 Ninject 创建一个自定义的依赖项解析器,以便 MVC 框架用它创建整个应用程序的实例化对象。首先在 SportsStore.WebUI 项目中添加一个 Infrastructure 文件夹,该文件夹下添加 NinjectDenpendencyResolver.cs 类,编辑类如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Ninject;

namespace SportsStore.WebUI.Infrastructure
{
    public class NinjectDependencyResolver:IDependencyResolver
    {
        private IKernel kernel;

        public NinjectDependencyResolver(IKernel kernelParam)
        {
            kernel = kernelParam;
            AddBindings();
        }
        public object GetService(Type serviceType)
        {
            return kernel.TryGet(serviceType);
        }

        public IEnumerable<object> GetServices(Type serviceType)
        {
            return kernel.GetAll(serviceType);
        }

        private void AddBindings()
        {
            //数据绑定
        }
    }
}

接下来一的一步就是搭建 NinjectDenpendencyResolver 类与 MVC 依赖项注入支持之间的桥梁,这一步操作是在 App_Start/ Ninject.Web.Common.cs 中完成,如下所示:

 private static void RegisterServices(IKernel kernel)
        {
            System.Web.Mvc.DependencyResolver.SetResolver(new SportsStore.WebUI.Infrastructure.NinjectDependencyResolver(kernel));
        }    

运行应用程序

此时,如果从“Dubug”菜单中运行程序,会看到下图错误页面,这是因为程序请求的 URL 关联的不是一个存在的控制器。


2、从域模型开始

       所有的 MVC 框架项目都是从创建域模型开始的,因为 MVC 框架应用程序中每一件事情都是围绕域模型而展开的。由于这是一个电子商务应用程序,笔者所需要的最明显的域实体是产品( Product)。在 SportsStore.Domain 项目中创建一个新文件夹,其名称为“ Entities”,然后在其中创建一个新的 C# 类文件,其名称为“ Product.cs”,可以从下图中看到其目录结构。编辑 Product.cs类文件,使其如下所示。

      这里所遵循的技术是在一个独立的 Visual studio 项目中定义域模型。这意味着,类必须标记为 public。虽然你不一定要遵守这一约定,但笔者发现这么做有助于保持模型与控制器分离,这在大型和复杂项目中是有用的。

创建抽象存储库

       笔者需要某种方式来获取数据库中的 Product 实体。模型含有持久化逻辑,这种逻辑用于从持久化数据存储库中存储和接收数据。但即使在模型中,笔者也希望在数据模型实体与存储接收逻辑之间保持一定程度的分离。这一目的可以使用“存储库模式( Repository Pattern)”来实现。此刻不必担心会如何实现数据的持久化,但首先会为其定义一个接口在 SportsStore.Domain 项目中创建一个新的顶级文件夹,其名称为 Abstract,并在其中创建一个新的接口文件,名称为 Iproductsrepository.cs,其内容如下所示。右击 Abstract 文件夹,选择“ Add New Item(添加新项)”,然后选择“ Interface(接口)”模板,便可以添加个新接口。


      该接口使用了 IQueryable<T>接口,可以让调用程序获取一个 Product 对象序列,而不必说明从哪儿或如何获取和接收数据。一个类可以依靠 IProductsRepository 这一接口获取 Product对象,而不必知道这些对象从哪儿来,也不必知道该接口的实现类如何递交这些对象,这就是存储库模式的本质。在添加特性的整个开发过程中,会不断修订这一 IProductsRepository 接口。

创建模仿存储库

       现在,已经定义了一个抽象接口,于是可以实现持久化机制并将其挂接到一个数据库。但在此之前,笔者想先做一点应用程序其他方面的事情。为此,打算创建一个 IProductRepository 接口的模仿实现,直到讨论数据存储论题时再考虑替换掉这种存储库模仿实现的方式。笔者在 SportSstore.WebUI 项目 NinjectDependencyResolver 类的 AddBindings 方法中定义了这一模仿实现,并将该实现绑定到了 IProductRepository 接口,如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Ninject;
using Moq;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;

namespace SportsStore.WebUI.Infrastructure
{
    public class NinjectDependencyResolver:IDependencyResolver
    {
        private IKernel kernel;

        public NinjectDependencyResolver(IKernel kernelParam)
        {
            kernel = kernelParam;
            AddBindings();
        }
        public object GetService(Type serviceType)
        {
            return kernel.TryGet(serviceType);
        }

        public IEnumerable<object> GetServices(Type serviceType)
        {
            return kernel.GetAll(serviceType);
        }

        private void AddBindings()
        {
            //数据绑定
            Mock<IProductsRepository> mock = new Mock<IProductsRepository>();
            mock.Setup(m => m.Products).Returns(new List<Products> { new Products { Name="Surf board",Price=25},
                new Products { Name = "Football", Price = 25 },
                new Products { Name = "Pingpang", Price = 95 } });
            kernel.Bind<IProductsRepository>().ToConstant(mock.Object);
        }
    }
}

       这里必须在该文件中添加几个命名空间,但用来创建模仿存储库实现的过程使用的是ASP.NET + MVC5 教程 所示同样的Moq技术。笔者希望 Ninject 每次接收到一个 IProductRepository 接口实现的请求时,返回的都是同样的模仿对象,于是用 ToConstant方法设置了 Ninject的作用域,如下所示:

kernel.Bind<IProductsRepository>().ToConstant(mock.Object);

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

3、显示产品列表

添加控制器

      右击 SportsStore.WebUI 项目中的 Controllers文件夹,并从弹出的菜单中选择“Add(添加)”→“ Controller(控制器)”。选择“MvC5 Controller- Empty(MvC5控制器空)”选项,单击“Add(添加)”按钮,并将名称设置为 ProductController。单击“Add(添加)”按钮后, Visual studio将创建一个新的类文件,其名称为 ProductController.cs,编辑内容,使其与下相吻合。

      除了删除 Index 动作方法外,还添加了一个构造函数。它声明了一个对 IProductRepository 接口的依赖项,这将导致该控制器实例化时,Ninject 会为产品存储库注入其依赖项。另外,还导入了 SportsStore.Domain 命名空间,这便可以引用存储库和模型类,而不必带有它们的限定名。步添加了一个动作方法,名称为”List“,它将渲染一个视图,以显示产品的完整列表,如下所示。


      像这样调用 View 方法(未指定视图名称),是告诉框架为该动作方法渲染一个默认视图。给 View 方法传递 Products 对象列表,是给框架提供数据,以便用这些数据填充强类型视图中的Model 对象。

添加布局、起始视图文件及视图

       现在,需要为List动作方法添加默认视图。右击 HomeController 类中的 List 动作方法,并从弹出的菜单中选择“ Add view(添加视图)”。将“ View name(视图名)”设置为“List”,将“ Template(模板)”设置为“ Empty(空)”,并在“ Model class(模型类)”中选择“ Product( SportsStore.Domain.Entities)”,如下图所示。要确保勾选了“ Use a layout page(使用布局页)”复选框,然后单击“Add(添加)”按钮,便可以创建该视图在单击“Add(添加)”按钮后,Visual studio会创建List. cshtml 文件,还会创建一个Viewstart.cshtml 文件和一个 Shared/ Layout.cshtml 文件。这是一个非常有用的特性。但是_Layout.cshtml 文件中包含了一些笔者并不想使用或不需要的模板内容。


渲染试图数据

      尽管笔者将该视图的模型类型设置为 Product 类,但实际上想使用的是 IEnumerable<Product>,它是 Product 控制器从存储库获取并传递给视图的。在List.cshtml 中可以看到笔者已经编辑了@ model表达式,并添加了一些 HTML 和 Raor 表达式,以显示产品的细节。如下所示:

@using SportsStore.Domain.Entities
@model IEnumerable<Product>
@{ 
    ViewBag.Title = "Products";
}
{
@foreach (var item in Model)
{
    <div>
        <h3>@item.Name</h3>
        @item.Description
        <h4>@item.Price.ToString("c")</h4>
    </div>
}
      上述也修改了页面的标题。注意,这里并不需要使用 Razor 的 @: 元素来显示视图数据。这是因为代码体中的每个内容行,或者是一个 Razor指示符,或者是以HTML元素开头的。

■提示:上述使用了 ToString("c")方法,将 Price 属性转换成一个字符串,它会根据服务器的语言设置,将数字值渲染成货币形式。例如,如果服务器的语言设置为en-US,那么(1002.3) ToString("c")将返回$1082.3,但如果服务器的语言设置为en-GB,那么同一方法将返回1682.38。你可以修改服务器的语言设置,只要把<globalization culture="fr-FR" uiCulture ="fr-FR"/>添加到 Web.config 的<system.web>节点处即可。

设置默认路由

      在需要告诉MVC框架,应该将应用程序根 Url(Http: //mysite/)的请求发送给 ProductController 类的 List 动作方法。这可以通过编辑 App Start/ RouteConfig.cs文件的 RegisterRoutes方法实现如下所示:


运行应用程序


4、准备数据库

       前面已经可以显示含有产品细节的简单视图,但其显示的只是模仿的 IProductRepository 所返回的测试数据。在实现真实的存储库之前,还需要建立一个数据库,并用一些数据填充它这里打算以 SQL Server作为数据库,并用 Entity Framework(实体框架,EF)来访问该数据库 EF 是 Microsoft.NET 的ORM(对象关系映射)框架。ORM框架让开发人员可以用规则的C#对象来表示关系数据库的表、列和行。

创建数据库

       Visual studio 和 SQL Server 的一个很好的特性是 LocalDB,是特意为开发者而设计的一个免管理的 SQL Server核心功能。使用该特性使笔者在建立项目以及后面将数据库部署到完整版的 SQL Server期间,可以跳过数据库的设置过程。大多数MVC应用程序都会部署到由专业管理人员运营的托管环境,因此 LocalDB 特性意味着,数据库配置可以留给数据库管理员(DBA)去负责,而开发人员只需进行编码。第一个步骤是在 Visual studio中创建数据库连接。从“view(视图)”菜单中打开“ Server Exporer(服务器资源管理器)”窗口。

       下一步将看到“ Add Connection(添加连接)”对话框,请将“ Server name(服务器名)”设置为“.”,这是一个特殊名称,表示你希望使用 Local 数据库特性。选中“ (使用 SQL 身份认证)”输入用户名sa和密码(自己安装的SQL数据库的密码),单选按
钮并将数据库名称设置为 SportsStore。

定义数据库方案

       为了创建数据库表,在服务器资源管理器窗口中新建的 SportsStore 数据库上右击“Tables(表)”条目,然后选择“ Add New Table(添加新表)”如图7-10所示。Visual studio将显示一个创建新表的设计器窗口。利用该设计器的可视化部分,可以创建新的数据库表,但这里打算使用T-SQL,因为它能以更简洁和准确的方式描述所需的表格规范。输入如下所示的SQL语句,并单击数据表设计窗口左上角的“ Update(更新)”按钮。

CREATE TABLE Products
(
	[ProductId] INT NOT NULL PRIMARY KEY IDENTITY,
	[Name] NVARCHAR(100) NOT NULL,
	[Description] NVARCHAR(500) NOT NULL,
	[Price] DECIMAL(16,2) NOT NULL,
	[Category] NVARCHAR(100) NOT NULL
)
当单击“更新”按钮,显示如下:

      单击“ Update Database(更新数据库)”按钮,可以执行该SQL语句,并在数据库中创建Products表。如果在服务器资源管理器窗口中单击“ Refresh(刷新)”按钮,能够看到更新的效果。“ Tables(表格)”部分显示了这一新的 Product表,以及每行的详细信息。
提示:更新数据库后,你可以关闭dbo.Products窗口。 Visual studio给你提供一个机会,让你保存创建这一数据库的SQL脚本,本次不需要保存该脚本,在实际的项目中,如果需要配置多个数据库,保存脚本是有用的。

向数据库添加数据

      笔者打算对该数据库添加一些数据,以便在之后添加分类管理特性之前,有一些可以使用的数据在“ Server Explorer(服务器资源管理器)”窗口中,展开 Sportsstore数据库的“ Tables(表)”条目,右击 Products表,并选择“ Show Tab1 e Data(显示表数据)”,然后输入如图所示的数据。在操作过程中,可以用Tab键逐行移动光标:在一行的最后按Tab键,将移到下一行并更新数据库中的数据。注意:必须让 ProductId 为空,他是一个标识列,当调到下一行,SQL 会自动生成一个唯一的值。

      

点击显示表数据,输入以下内容:

创建 Entity Framework 上下文

       Entity Framework 的最新版包含了一个很好的特性,叫做“code- First(代码先行)”。其思想是先定义模型中的类,再通过这些类生成数据库。这很适合于绿地( Green- field)开发项目,但这些项目并不多见(“绿地开发项目”是一种全新的开发项目,由于刚才创建了数据库,已经不能算是“全新”项目了)。因此,笔者打算演示的是种变异的Code- First,以此将模型类与现有的数据库关联起来。在 Visual studio中选择“ Tools(工具 ->Library Package Manager(库包管理器)”,NuGet 安装即可。

     给解决方案添加 Entity Framework包。笔者需要在 Domain和 Webui项目中也安装相同的包,以便在 Domain和 WebUI 两个项目中创建一些访问数据库的类。下一个步骤是创建一个上下文类( Context class),以便模型与数据库关联起来。在SportsStore.Domain 项目中创建一个新的文件夹,名称为“ Concrete“,并在其中添加一个新类,名称为“ EFdbcontext.cs”。编辑内容,使其如下所示:

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 结果的类型参数为模型类型,由 Entity Framework用于表示数据表的各个数据行。在这个例子中,属性名是 Products(数据库中的表名),而参数类型是Product(用来表示该表中一行数据的模型类( Product类)的类型( Product),意即, Entity Framework应该使用 Product 模型类来表 Products 表的各个行。下一步需要告诉 Entity Framework如何连接到数据库,其做法是在 SportsStore.WebUl 项目的 web.config 文件中添加一条数据库连接字符串,该连接字符串的名称与这个上下文类的名称相同,如下所示:


创建 Product 存储库

剩下的工作就是在 SportsStore.Domain 项目的 Concrete 文件夹中添加一个类文件 EFProductRepository,编辑如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;

namespace SportsStore.Domain.Concrete
{
    public class EFProductRepository:IProductsRepository
    {
        private EFdbcontext context = new EFdbcontext();

        public IEnumerable<Product>Products
        {
            get { return context.Products; }
        }

    }
}

       这就是存储库类,实现了 IProductRepository接口,并使用一个 EFDbContext实例,以便用 Entity Framework 接收数据库的数据。在对该存储库添加特性时,便会看到笔者是如何使用 EntityFramework的(而且它是多么简单)。

      为使用这个新的存储库类,需要编辑 Ninject绑定,并用一个实际存储库的绑定替换之前的模仿存储库。编辑 Sportsstore. WebUI 项目中的 NinjectDependency Resolver.cs类文件,使其中的AddBindings方法如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Ninject;
using Moq;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.Domain.Concrete;

namespace SportsStore.WebUI.Infrastructure
{
    public class NinjectDependencyResolver:IDependencyResolver
    {
        private IKernel kernel;

        public NinjectDependencyResolver(IKernel kernelParam)
        {
            kernel = kernelParam;
            AddBindings();
        }
        public object GetService(Type serviceType)
        {
            return kernel.TryGet(serviceType);
        }

        public IEnumerable<object> GetServices(Type serviceType)
        {
            return kernel.GetAll(serviceType);
        }

        private void AddBindings()
        {
            ////模仿存储库数据绑定
            //Mock<IProductsRepository> mock = new Mock<IProductsRepository>();
            //mock.Setup(m => m.Products).Returns(new List<Product> { new Product { Name="Surf board",Price=125},
            //    new Product { Name = "Football", Price = 25 },
            //    new Product { Name = "Pingpang", Price = 95 } });
            //kernel.Bind<IProductsRepository>().ToConstant(mock.Object);
          
            kernel.Bind<IProductsRepository>().To<EFProductRepository>();  //存储库数据绑定
        }
    }
}

  运行实例效果如下:

5、添加分页

从上面结果可以看出,List.cshtml 视图将数据库中的所有产品都显示在一个页面,接下来我们将实现分页显示效果。为此,在 List 方法上添加一个参数,如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.Domain.Entities;
using SportsStore.Domain.Abstract;

namespace SportsStore.WebUI.Controllers
{
    public class ProductController : Controller
    {
        // GET: Product
        private IProductsRepository repository;

        public int PageSize = 4;
        public ProductController (IProductsRepository productRepository)
        {
            this.repository = productRepository;
        }
        public ViewResult List(int page=1)
        {
            return View(repository.Products.OrderBy(p=>p.ProductId).Skip((page-1)*PageSize).Take(PageSize));
        }
    }
}

      Pagesize字段指明笔者希望每页显示4个产品,稍后将用一个更好的机制来替换它。对List方法添加了一个可选参数。这意味着,如果调用不带参数的方法 List(),该调用会被处理成就好像已经提供了参数定义中指定的值List(1)一样。其结果是,当MVC框架不带参数调用该方法时,显示产品的第一页。在动作方法体中,笔者获取了 Product 对象,按主键排序,略过当前页之前的产品数,然后取出由 Pagesize 字段指定的产品个数。

单元测试:分页

        通过这样的方法可以对分页特性进行单元测试:创建一个模仿存储库,将其注入到 ProductController 类的构造器之中,然后调用List方法来请求一个特定的页面,接着可以把得到的产品对象与模仿实现中的测试数据预期的结果进行比较。以下是为此目的在 SportsStore.UnitTest 项目的 UnitTest1.cs文件中创建的单元测试。

using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SportsStore.WebUI.Controllers;
using SportsStore.Domain.Entities;
using SportsStore.Domain.Abstract;
using Moq;
using System.Linq;

namespace SportsStore.UnitTests
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void Can_Paginate()
        {
            Mock<IProductsRepository> mock = new Mock<IProductsRepository>();
            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> redult = (IEnumerable < Product>)controller.List(2).Model;


            Product[] prodArrary = redult.ToArray();
            Assert.IsTrue(prodArrary.Length==2);
            Assert.AreEqual(prodArrary[0].Name,"P4");
            Assert.AreEqual(prodArrary[1].Name, "P5");
        }
    }
}

显示页面链接

       如果运行该应用程序,将会看到只有4个条目显示在页面上。如果想查看另一页,可以把查询字符串参数加到URL的末尾,像这样 http //localhost: 59489/?page=2。
       注意要修改URL的端口号,使其与正在运行的 ASPNET开发服务器端口号相匹配。运用这种查询字符串,可以对整个产品分类进行导航。当然,这里未提供任何提示,让客户猜出可以使用这种的查询字符串参数,而且即使有提示可能客户也不会喜欢以这种方式进行导航。因此,笔者需要在每个产品列表的底部渲染某种页面链接,以使客户能够在页面之间进行导航。为此,笔者打算实现一个可重用的HTML辅助器方法,它类似于使用的Htm1.TextBoxFor 和 Htm1.BeginForm方法。该辅助器方法将为所需的导航链接生成HTML标记。

添加视图模型

      为了支持HTML辅助器方法,笔者打算把可用页面数、当前页,以及存储库中产品总数等方面的信息传递给视图。最容易的做法是创建一个视图模型。在SportsStore.WebUI 项目的 Models 文件夹中添加一个类文件,名称为 PagingInfo,内容如下所示。

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 TotalPage
        {
            get { return (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage); }
    }
}

     视图模型不属于域模型,它只是一种便于在控制器与视图之间传递数据的类。为了强调这一点,笔者将这个类放在了 SportsStore.WebUI 项目中,以使它与域模型的类分离开来(读者应该体会到,将视图模型类放在MVC框架项目的 Models 文件夹,而不是放在类库项目中,这种做法足以说明视图模型不是域模型,明确了概念,也使应用程序的结构更清晰)

添加 HTML 辅助器方法

       现在,有了这个视图模型便可以实现 HTML 辅助器方法,笔者将其称为“ PageLinks”。在Sportsstore.WebUI 项目中创建一个新文件夹,名称为“ HtmIHelpers”,并添加一个新的类文件名称为“ PagingHelpers.cs",文件的内容如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Text;
using SportsStore.WebUI.Models;

namespace SportsStore.WebUI.HtmIHelpers
{
    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.TotalPage; 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 对象中提供的信息生成一组页面链接的HIML标记。Func参数接受一个委托,该委托用于生成查看其他页面的链接。
      只有当包含扩展方法的命名空间在范围内时,其中的扩展方法才是可用的。在一个代码文件中这是用 using 语句来完成的:但对于一个 Razor视图,必须在Web.config文件中添加一条配置条目,或在视图上添加一条@ using语句。容易混淆的是,在一个 Razor的Mvc项目中有两个Web.config文件:主配置文件位于应用程序的根目录,而视图专用的配置文件位于 Views 文件夹需要修改的是 Views/web.config文件,如下所示。

在一个 Razor 视图中需要引用的每一个命名空间,都要在 Web.config 文件中声明或者在视图中运用 @using 表达式。

添加视图的模型数据

       还需要为视图添加一个 PagingInfo 视图模型类实例,可以用 View Bag(视图包)特性来做这件事,但更好的做法是将控制器发送给视图的所有数据封装成一个单一的视图模型类。为此,笔者在 Sportsstore.WebUI项目的 Models 文件夹中添加了一个新的类文件,名称为“ ProductsListViewModel.cs”。如下显示了该文件的内容。

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 方法了,以便使用这个 ProductsListViewModel ,如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.Domain.Entities;
using SportsStore.Domain.Abstract;
using SportsStore.WebUI.Models;

namespace SportsStore.WebUI.Controllers
{
    public class ProductController : Controller
    {
        // GET: Product
        private IProductsRepository repository;

        public int PageSize = 4;
        public ProductController (IProductsRepository productRepository)
        {
            this.repository = productRepository;
        }
        public ViewResult List(int page=1)
        {
            ProductsListViewModel model = new Models.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 对象作为模型传递给视图。

显示页面链接

       现在已经做好了在 List 视图上添加页面链接的所有准备。前面已经创建了含有分页信息的视图模型,更新了控制器以使这个信息能够传递给视图,并修改了@model 指示符以匹配新的模型视图类型。剩下的事情便是在视图中调用这个HTML 辅助器方法,如下:

@model SportsStore.WebUI.Models.ProductsListViewModel
@{
    ViewBag.Title = "Products";
}
@foreach (var item in Model.Products)
{
    <div>
        <h3>@item.Name</h3>
        @item.Description
        <h4>@item.Price.ToString("c")</h4>
    </div>
}

<div>
    @Html.PageLinks(Model.PagingInfo,x=>Url.Action("List",new { page=x}))
</div>


改进 URL

页面链接已经可以起作用了,但是,为了将分页信息传递给服务器,这些链接使用的仍然是查询字符串形式,格式如下:
http //localhost/?page=2
可以根据“可组合URL”模式创建一种更具吸引力的URL方案。“可组合URL”是一种对用户有意义的形式,其格式如下:
http //localhost/page2
MVC很容易修改应用程序中的URL方案,因为它使用了 ASPNET的路由特性。所要做的只是在 RouteConfig.Cs文件中的 RegisterRoutes方法中添加一条新的路由,RouteConfig.CS可以在 Sportsstore. WebuI项目的App_ Start文件夹中找到。如下可以看到对该文件的修改。

运行后如下:


6、设置内容样式

      前面已经建立了大量的基础结构,而且应用程序也开始真正地整合在一起了,但并未把注意力放到其外观上,但 Sports Store应用程序设计也会因为太糟糕的样式而破坏它的技术强度。本次将做一些常规的事情,实现一个经典的带有页头的两栏布局,如下所示:

安装 BootStrap 包

同理,利用NuGet 安装。


在布局中运用 BootStrap 样式

       Razor布局的工作机制以及如何运用布局。在为 Product控制器创建List.cshtml 视图时,曾要求你选中“使用布局”复选框,但让那个文本框为空。其效果是使用 Views/ _Viewstart.cshtml 文件(视图起始文件)中指定的布局,该布局是 Visual Studio随视图自动创建的。这个视图起始文件的内容如下所示:

       Layout 属性的值指明,在视图未明确指定布局时,将使用 views/ Shared/_Layout.cstml 文件作为布局。笔者重写了前面的 _Layout.cstml 文件的内容,删除了 Visual studio添加的模板内容,从下面可以看出如何添加 Bootstrap 的CSS文件并运用其定义的一些CSS样式。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title</title>
    <link href="~/Content/bootstrap.css" rel="stylesheet" />
    <link href="~/Content/Site.css" rel="stylesheet" />
    <link href="~/Content/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <div class="navbar navbar-inverse" role="navigation">
    <a class="navbar-brand" href="#">SPORTS STORE</a>
    </div>
    <div class="row panel">
    <div id="cateagories" class="col-xs-3">
        Put something useful here later
    </div>
        <div class="col-xs-8">
            @RenderBody()
        </div>
    </div>
</body>
</html>

这里用 link 元素给布局添加了 bootstrap.css和 bootstra.min.css文件,并运用各种 Bootstrap 的 class创建了一个简单的布局。另外,还需要修改List.cstml 文件,如下所示:

@model SportsStore.WebUI.Models.ProductsListViewModel
@{
    ViewBag.Title = "Products";
}
@foreach (var item in Model.Products)
{
    <div class="well">
        <h3>
            <strong>@item.Name</strong>
            <span class="pull-right label label-primary">@item.Price.ToString("c")</span>
        </h3>
        <span class="lead">@item.Description</span>
    </div>
}

<div class="btn-group pull-right">
    @Html.PageLinks(Model.PagingInfo,x=>Url.Action("List",new { page=x}))
</div>

运行程序,如下:


创建分部视图

       最后一个技巧是打算重构应用程序,以简化 List.cstml 视图。笔者打算创建一个分部视图( Partial view),这种分部视图是嵌入到另一个视图中的一个内容片段,而不是一个模板。分部图是一种自包含的文件,且可以跨视图重用,这有助于减少重复,尤其是需要在应用程序的几个地方渲染同样的数据时。为了添加分部视图,右击 SportsStore.WebUI 项目中的/ Views/ Shared文件夹,然后从弹出的菜单中选择“Add(添加)”→“View(视图)”。在打开的对话框中将视图命名为“ ProductSummary",设置“ Template(模板)”为“ Empty(空)”,从“ Model class(模型类)”下拉列表中选择“ Product"并选中“ Create as a partial view(创建为分部视图)”复选框,如下所示单击“Add(添加)”按钮,Visual studio将创建一个名称为/ Views/ Shared/ Productsummary.cshtml 的分部视图文件。分部视图与常规视图十分相似,只是它产生的是一个HIML片段,而不是整个HTML文档。打开这个 Productsummary视图,将看到该视图只包含了 model视图指示符,它被设置为 Product域模型类。运用如下所示的修改。

对 ProductSummary.cshtml 文件编辑如下:

@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"></span>
</div>

接下来更新 List.cshtml 文件如下:

@model SportsStore.WebUI.Models.ProductsListViewModel
@{
    ViewBag.Title = "Products";
}
@foreach (var item in Model.Products)
{
    @Html.Partial("ProductSummary",item)
}

<div class="btn-group pull-right">
    @Html.PageLinks(Model.PagingInfo,x=>Url.Action("List",new { page=x}))
</div>

这里去掉了 list 之前视图的 foreach 内部内容,将内容转移到新的部分视图中。用 @Html.Partial("ProductSummary",item)辅助器方法调用这个分布视图,他不改变程序外观,运行如下所示:


到此,一个简单的实例已经初步完成。

源代码下载:https://download.csdn.net/download/qq_21419015/10451384

猜你喜欢

转载自blog.csdn.net/qq_21419015/article/details/80509513
今日推荐