王志岳

塔塔信息技术(中国)有限公司 | CoderBus

导航

使用MVC4,Ninject,EF,Moq,构建一个真实的应用电子商务SportsStore(四)

上篇中,我们将数据库中的数据显示到了 UI上,在这里我要强调一点,在上篇中我们应用了强类型的View,不要与model业务混淆,有关强类型view的知识点,不在本实例范畴之内,请参阅相关文档。对于任何一个电子商务网站来说,都需要使用户能方便的浏览所有的商品,并能够从一页迁移到另一页,这是个非常实用、也非常基本的功能,但在MVC4中,怎么实现它呢,现在就让我们一步一步的完善这个功能。
 
首先,我们要为我们的Product控制器的List 方法添加一个参数,用它来代表浏览的页号,代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;

namespace SportsStore.WebUI.Controllers
{
    public class ProductController : Controller
    {
        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字段指定了每一页要显示的产品数量,稍后我们将使用更好的机制来替换它,现在你只需要理解它。
我们还添加了一个可选的参数到List方法,这就表示如果我们调用的方法没有参数(List()), 我们将会使用(List(1)) 来处理,就是默认为显示第一页。
 
现在我们添加一个测试文件到你的SportsStore.UnitTests工程
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Controllers;
using System.Collections.Generic;
using System.Linq;
using SportsStore.WebUI.Models;
using System;

namespace SportsStore.UnitTests {
    [TestClass]
    public class UnitTest1 {
        [TestMethod]
        public void Can_Paginate() {
                // Arrange
                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"}
                }.AsQueryable());
                ProductController controller = new ProductController(mock.Object);
                controller.PageSize = 3;
                // Act
                IEnumerable<Product> result =
                (IEnumerable<Product>)controller.List(2).Model;
                // Assert
                Product[] prodArray = result.ToArray();
                Assert.IsTrue(prodArray.Length == 2);
                Assert.AreEqual(prodArray[0].Name, "P4");
                Assert.AreEqual(prodArray[1].Name, "P5");
        }


    }
}
这里请注意看,我们是如何轻松的从一个控制器的结果集中获得数据的,在这里,我们反转了这个结果集到一个数组,并检查单个对象的长度和值。
运行工程,可以看到如下结果:
 
添加View Model
View Model 不是我们领域模型的一部分,只是为了方便在控制器和View 之间传递数据,所以我们把它放在SportsStore.WebUI工程的Models文件夹中,命名为PagingInfo,代码如下:
using System;

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); }
        }

    }
}
添加HTML Helper 方法
现在,我们需要添加一个文件夹命名为HtmlHelpers,并添加一个文件,命名为PagingHelpers。
using System;
using System.Text;
using System.Web.Mvc;
using SportsStore.WebUI.Models;
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"); // Construct an <a> tag
                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());
        }
    }
}
这个PageLinks 扩展方法为页链接集合产生HTML,这个页链接集合使用了PagingInfo对象, Func 参数提供了传递代理的能力,代理被用来产生链接到其他页面的链接。
 
测试我们的HtmlHelpers
在我们测试文件中添加如下引用和方法:
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Controllers;
using System.Collections.Generic;
using System.Linq;
using SportsStore.WebUI.Models;
using System;
using System.Web.Mvc;
using SportsStore.WebUI.HtmlHelpers;

namespace SportsStore.UnitTests {
    [TestClass]
    public class UnitTest1 {
        [TestMethod]
        public void Can_Paginate() {
                // Arrange
                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"}
                }.AsQueryable());
                ProductController controller = new ProductController(mock.Object);
                controller.PageSize = 3;
                // Act
                IEnumerable<Product> result =
                (IEnumerable<Product>)controller.List(2).Model;
                // Assert
                Product[] prodArray = result.ToArray();
                Assert.IsTrue(prodArray.Length == 2);
                Assert.AreEqual(prodArray[0].Name, "P4");
                Assert.AreEqual(prodArray[1].Name, "P5");
        }


        [TestMethod]
        public void Can_Generate_Page_Links()
        {
            // Arrange - define an HTML helper - we need to do this
            // in order to apply the extension method
            HtmlHelper myHelper = null;
            // Arrange - create PagingInfo data
            PagingInfo pagingInfo = new PagingInfo
            {
                CurrentPage = 2,
                TotalItems = 28,
                ItemsPerPage = 10
            };
            // Arrange - set up the delegate using a lambda expression
            Func<int, string> pageUrlDelegate = i => "Page" + i;
            // Act
            MvcHtmlString result = myHelper.PageLinks(pagingInfo, pageUrlDelegate);
            // Assert
            Assert.AreEqual(result.ToString(), @"<a href=""Page1"">1</a>"
            + @"<a class=""selected"" href=""Page2"">2</a>"
            + @"<a href=""Page3"">3</a>");
        }
    }
}
要使扩展方法有效,在代码中,我们需要确保引用它所在的namespace,我使用了using语句,但是对于一个 Razor View,我们必须配置web.config文件,而一个MVC工程,有2个web.config文件,一个是在工程的根目录下,另一个在View文件夹中,我们需要配置文件夹中的这个,添加如下语句到namespace标签中:
<add namespace="SportsStore.WebUI.HtmlHelpers"/>
<namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Optimization"/>
        <add namespace="System.Web.Routing" />
        <add namespace="SportsStore.WebUI.HtmlHelpers"/>
      </namespaces>

 

 
添加View Model数据
我们并没有打算完全使用HTML helper方法,我们依然需要提供一个PagingInfo view model类的实例到View,我们可以使用view bag的特性,但我们更倾向于打包所有的Controller数据发送到一个单一的View,要实现这一点,我们要添加一个新类到Model文件夹,命名为ProductsListViewModel。
using System.Collections.Generic;
using SportsStore.Domain.Entities;

namespace SportsStore.WebUI.Models {

    public class ProductsListViewModel {

        public IEnumerable<Product> Products { get; set; }
        public PagingInfo PagingInfo { get; set; }
    }
}

好了,现在我们再更新一下ProductController的代码,用ProductsListViewModel类提供给View更详细的数据。

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

namespace SportsStore.WebUI.Controllers
{
    public class ProductController : Controller
    {
        private IProductsRepository repository;
        public int PageSize = 4;

        public ProductController(IProductsRepository productRepository)
        {
            this.repository = productRepository;
        }

        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);
        }

    }
}


修改测试文件代码:

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

namespace SportsStore.UnitTests {
    [TestClass]
    public class UnitTest1 {
        [TestMethod]
        public void Can_Paginate() {
                // Arrange
                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"}
                }.AsQueryable());
                ProductController controller = new ProductController(mock.Object);
                controller.PageSize = 3;

                // Action
                ProductsListViewModel result = (ProductsListViewModel)controller.List(2).Model;

                // Assert
                Product[] prodArray = result.Products.ToArray();
                Assert.IsTrue(prodArray.Length == 2);
                Assert.AreEqual(prodArray[0].Name, "P4");
                Assert.AreEqual(prodArray[1].Name, "P5");
        }

        [TestMethod]
        public void Can_Send_Pagination_View_Model()
        {
            // Arrange
            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"}
                                                            }.AsQueryable());
            // Arrange
            ProductController controller = new ProductController(mock.Object);
            controller.PageSize = 3;
            // Act
            ProductsListViewModel result = (ProductsListViewModel)controller.List(2).Model;
            // Assert
            PagingInfo pageInfo = result.PagingInfo;
            Assert.AreEqual(pageInfo.CurrentPage, 2);
            Assert.AreEqual(pageInfo.ItemsPerPage, 3);
            Assert.AreEqual(pageInfo.TotalItems, 5);
            Assert.AreEqual(pageInfo.TotalPages, 2);
        }

        [TestMethod]
        public void Can_Generate_Page_Links()
        {
            // Arrange - define an HTML helper - we need to do this
            // in order to apply the extension method
            HtmlHelper myHelper = null;
            // Arrange - create PagingInfo data
            PagingInfo pagingInfo = new PagingInfo
            {
                CurrentPage = 2,
                TotalItems = 28,
                ItemsPerPage = 10
            };
            // Arrange - set up the delegate using a lambda expression
            Func<int, string> pageUrlDelegate = i => "Page" + i;
            // Act
            MvcHtmlString result = myHelper.PageLinks(pagingInfo, pageUrlDelegate);
            // Assert
            Assert.AreEqual(result.ToString(), @"<a href=""Page1"">1</a>"
            + @"<a class=""selected"" href=""Page2"">2</a>"
            + @"<a href=""Page3"">3</a>");
        }
    }
}

 

运行程序,你将看到如下结果:
 
改进我们的页面链接
我们的页面链接都是工作的,但它看起来是这样的:
http://localhost/?page=2
我们能做的更好些, 尤其是通过创建一个scheme,它遵循可组装URLs. 这使得用户更容易理解,并且更有效率,它看起来应该像下面的地址:
http://localhost/Page2
MVC很容易去改变URL scheme,因为它使用了ASP.NET的routing特性,我们要做的就是添加一个新的route 到RouteConfig.cs文件的RegisterRoutes,打开App_Start文件夹,找到我们要改的文件。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace SportsStore.WebUI
{
    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 }
            );
        }
    }
}

把我的Route放在默认的Route前是很重要的,Route的处理是按照它们被列出的顺序进行的,我们需要用我们新的Route优先于默认的,现在你暂时先了解这些,以后,我们会更加详细的讲解它。

 好了,这个博客的编辑器太难用了,总是不停的刷新,无法固定页面位置,今天的内容比较多,希望大家能仔细看好每一步,这里面没有一个字是多余的!剩下没写完的下次再写吧!请继续关注我的续篇。
 

posted on 2013-06-03 15:29  王志岳  阅读(3818)  评论(11编辑  收藏  举报

开发者导航